feat: stream intermediate assistant turns over MCP

Install a per-request `after_llm_call` hook that emits each intermediate
assistant turn as an MCP `notifications/message`, so users see
substantive text from earlier loop iterations instead of only the final
`agent.send()` return value.

Add tests covering the hook's payload shape, error handling, and
lifecycle via `install_for_request`.
This commit is contained in:
2026-05-28 06:09:41 -04:00
parent 8a5046fef0
commit 63ced9ba2b
2 changed files with 321 additions and 0 deletions

View File

@@ -28,6 +28,7 @@ from fast_agent.core.logging.logger import get_logger
from fast_agent.mcp.server import AgentMCPServer
from fast_agent.types import PromptMessageExtended, RequestParams
from pallas.assistant_stream import install_for_request as _install_assistant_stream
from pallas.progress import EnrichedMCPToolProgressManager
from pallas import metrics as _pallas_metrics
from fastmcp import Context as MCPContext
@@ -220,6 +221,20 @@ class MultimodalAgentMCPServer(AgentMCPServer):
agent_context = getattr(agent, "context", None)
metrics_start = time.perf_counter()
metrics_outcome = "ok"
# Install per-request after_llm_call hook that ships every
# intermediate assistant turn over MCP as a notifications/message.
# Without this, only the final ``agent.send()`` return value
# crosses the MCP boundary — substantive assistant text emitted
# in earlier loop iterations stays trapped inside fast-agent's
# ``message_history`` and the user sees a spinner that ends with
# a thin wrap-up sentence.
restore_hooks = _install_assistant_stream(
agent,
ctx=ctx,
agent_name=agent_name,
conversation_id=conversation_id,
)
try:
# Seed the freshly-created instance's message_history from the
# caller-supplied history so the agent sees the full
@@ -313,6 +328,14 @@ class MultimodalAgentMCPServer(AgentMCPServer):
_pallas_metrics.send_message_total.labels(
agent=agent_name, outcome=metrics_outcome
).inc()
# Restore the agent's prior tool_runner_hooks before the
# instance is released — defensive against any future
# shared-instance mode where leaking per-request hooks
# across requests would mis-attribute notifications.
try:
restore_hooks()
except Exception:
pass
await self._release_instance(ctx, instance)