Queued Agent Turns (QueuedAgentChatController & QueuedTurnManager)¶
Long-running multi-step AgentRuntime turns that invoke external APIs, MCP tools, or specialist AgentGroup handoffs can outlive standard HTTP reverse-proxy timeouts. litestar_mcp.queues provides QueuedAgentChatController, QueuedTurnManager, AgentRegistry, PrincipalSnapshot, run_agent_turn_task, and emit_agent_frame to offload agent turns onto litestar-queues while streaming sequenced AgentStreamFrame events over Litestar's ChannelsPlugin.
End-to-End Wiring Example¶
The snippet below wires Agent, AgentRuntime, LitestarStoreSessionStore, AgentRegistry, QueuedTurnManager, QueuedAgentChatController, ChannelsPlugin, and QueuePlugin into a complete Litestar application:
"""Snippet: background queued agent turns with QueuedAgentChatController and ChannelsPlugin."""
# start-example
from litestar import Litestar
from litestar.channels import ChannelsPlugin
from litestar.channels.backends.memory import MemoryChannelsBackend
from litestar.di import Provide
from litestar.stores.memory import MemoryStore
from litestar_queues import QueuePlugin
from litestar_mcp.agent import Agent, AgentRuntime, MockModelClient, ModelDelta
from litestar_mcp.agent.sessions import LitestarStoreSessionStore
from litestar_mcp.queues import (
AgentRegistry,
QueuedAgentChatController,
QueuedTurnManager,
set_agent_registry,
)
session_store = LitestarStoreSessionStore(MemoryStore())
model_client = MockModelClient(
responses=[
[ModelDelta(event_type="delta", text="Background analysis completed.")],
],
)
support_agent = Agent(
name="root",
description="Background analyst agent running on litestar-queues.",
instructions="Analyze the user request and report findings.",
model=model_client,
)
runtime = AgentRuntime(
target=support_agent,
sessions=session_store,
)
registry = AgentRegistry({"root": runtime})
set_agent_registry(registry)
channels_plugin = ChannelsPlugin(
backend=MemoryChannelsBackend(history=1000),
arbitrary_channels_allowed=True,
)
queue_plugin = QueuePlugin()
def provide_turn_manager() -> QueuedTurnManager:
"""Provide QueuedTurnManager bound to the active QueueService."""
return QueuedTurnManager(queue_service=queue_plugin.get_service())
app = Litestar(
route_handlers=[QueuedAgentChatController],
dependencies={
"turn_manager": Provide(provide_turn_manager, sync_to_thread=False),
},
plugins=[
channels_plugin,
queue_plugin,
runtime,
],
)
# end-example
def build() -> Litestar:
"""Return the configured Litestar application for snippet smoke testing."""
return app
Queued Turn Execution Pipeline¶
flowchart LR
Post["POST /agent/turns (202 Accepted)"] --> Manager["QueuedTurnManager (agent-turn:{session}:{turn})"]
Manager --> Worker["run_agent_turn_task (AgentRegistry -> AgentRuntime)"]
Worker --> Emit["emit_agent_frame + LitestarStoreSessionStore"]
Emit --> Stream["GET /agent/turns/{turn_id}/stream (SSE Replay + Live)"]
1. Enqueuing a Turn (POST /agent/turns)¶
QueuedAgentChatController mounts at /agent and exposes POST /agent/turns:
- Request Payload: Accepts a JSON object containing
user_message(ormessage), and optionalsession_id,turn_id,agent_name(defaults to"root"), anddynamic_context. PrincipalSnapshotCapture: Inspectsrequest.userandrequest.auth(plusrequest.state.tenant) and builds a serializablePrincipalSnapshot(user_id=..., tenant=..., roles=[...], scopes=[...]).QueuedTurnManager.enqueue_turn: Wraps the parameters andPrincipalSnapshotinto aQueuedTurnstruct (msgspec.to_builtins) and enqueuesrun_agent_turn_task(litestar_mcp.agent.run_turn) on the"agent"queue with idempotency keyf"agent-turn:{session_id}:{turn_id}".202 AcceptedResponse: Immediately returns HTTP202 Acceptedwith the correlation identifiers and SSE stream URL:
2. Worker Execution (run_agent_turn_task)¶
When a litestar-queues worker claims litestar_mcp.agent.run_turn (configured with queue="agent", retries=0, timeout=1800):
- Envelope Deserialization: Converts the task dictionary back into a typed
QueuedTurnstruct. AgentRegistryLookup: Retrieves the targetAgentRuntimefromget_agent_registry().get(queued_turn.agent_name). Worker processes register theirAgentRuntimeinstances at startup viaset_agent_registry(registry)and bind the activeChannelsPluginviaset_channels_service(channels).ToolContext& Session Rehydration: ReconstructsToolContext(user={"id": principal.user_id}, tenant_id=principal.tenant, session_id=..., turn_id=...)and builds aTurnRequest. BecauseAgentRuntimeusesLitestarStoreSessionStorebacked by a shared LitestarStore, conversation history is loaded and saved under the caller'sowner_idacross worker replicas.- Sequenced Frame Emission (
emit_agent_frame): Iterates overruntime.stream(turn_request)with a monotonically increasing 1-based counter (seq = 1, 2, ...). For every yieldedAgentStreamFrame(session,thought,delta,tool_call,tool_result,agent_transfer,complete,error), the worker calls:emit_agent_framestampsframe.seq = seq, serializes the frame to JSON, and publishes it toagent_turn_channel(turn_id)(f"litestar_mcp:agent:{turn_id}").
Store-Backed Frame Replay & Live ChannelsPlugin SSE¶
Clients consume the turn output by connecting to GET /agent/turns/{turn_id:str}/stream on QueuedAgentChatController:
- History Replay + Live Wakeup:
QueuedAgentChatController.stream_turnopens a subscription onagent_turn_channel(turn_id)withchannels.start_subscription([channel_name], history=self.replay_limit)(defaultreplay_limit = 1000). If the worker already emitted frames—or even completed the turn—before the client opened the SSE stream, the buffered history frames are replayed immediately in order before transitioning seamlessly to live channel publications. - Deterministic Sequence Numbers &
Last-Event-ID: EveryAgentStreamFramecarries a 1-basedseqinteger.frame.to_sse_message()sets the SSEidfield tostr(frame.seq)andeventtoframe.event_type:id: 1 event: session data: {"event_type":"session","turn_id":"4f3c...","seq":1,"payload":{"session_id":"9b1d..."}} id: 2 event: delta data: {"event_type":"delta","turn_id":"4f3c...","seq":2,"agent_name":"root","text":"Background analysis completed."} id: 3 event: complete data: {"event_type":"complete","turn_id":"4f3c...","seq":3,"agent_name":"root","payload":{"output":"Background analysis completed."}} - Deduplication & Keep-Alive Pings:
stream_turntrackslast_seqand drops any duplicate frame whereframe.seq <= last_seq. While waiting for the next model or tool frame, ifself.ping_interval(15.0seconds) elapses without an event, the generator yields an SSEevent: pingkeep-alive frame (anyio.move_on_after(self.ping_interval)). - Automatic Stream Termination: As soon as a frame with
frame.event_type in TERMINAL_EVENTS("complete"or"error") is yielded,stream_turncloses the subscription and terminates the SSE response cleanly underStreamCleanupMiddleware.
Core Components Reference¶
| Symbol | Module | Role |
|---|---|---|
QueuedAgentChatController |
litestar_mcp.queues |
Litestar Controller mounting POST /agent/turns (202 Accepted) and GET /agent/turns/{turn_id:str}/stream (SSE). Configurable path = "/agent", replay_limit = 1000, and ping_interval = 15.0. |
QueuedTurnManager |
litestar_mcp.queues |
Service injected as turn_manager that serializes QueuedTurn envelopes and enqueues run_agent_turn_task on litestar-queues under agent-turn:{session_id}:{turn_id}. |
QueuedTurn |
litestar_mcp.queues |
msgspec.Struct dispatch envelope holding session_id, turn_id, user_message, agent_name, dynamic_context, and principal (PrincipalSnapshot). |
PrincipalSnapshot |
litestar_mcp.queues |
msgspec.Struct capturing user_id, tenant, roles, and scopes from the originating HTTP request for worker ToolContext reconstruction. |
AgentRegistry |
litestar_mcp.queues |
Name-to-AgentRuntime registry queried by run_agent_turn_task inside worker processes (set_agent_registry / get_agent_registry). |
run_agent_turn_task |
litestar_mcp.queues |
@task("litestar_mcp.agent.run_turn", queue="agent", retries=0, timeout=1800) executing runtime.stream(turn_request) and publishing frames via emit_agent_frame. |
emit_agent_frame |
litestar_mcp.queues |
Assigns seq onto an AgentStreamFrame, serializes to JSON, and publishes to agent_turn_channel(turn_id) (litestar_mcp:agent:{turn_id}). |