{
  "schema_version": 2,
  "id": "develop/ai/context-engine/agent-mesh/fastapi-sse-sdk",
  "title": "Integrate Agent Mesh with FastAPI",
  "url": "https://redis.io/docs/latest/develop/ai/context-engine/agent-mesh/fastapi-sse-sdk/",
  "summary": "Add resumable application event streams to a FastAPI application.",
  "tags": [
    "docs",
    "develop",
    "ai"
  ],
  "last_updated": "2026-11-03T09:50:14-05:00",
  "page_type": "content",
  "content_hash": "c019c553387368ab7d82a8fbe9c42ac61ccd604cb727ffb7eb5d535ffda4af6d",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "Use the Agent Mesh software development kit (SDK) to run a producer independently\nof its client connection. FastAPI clients reconnect with a saved cursor to replay\nretained server-sent events (SSE), then follow new events from the same run.\n\nStart with the [FastAPI browser demo](https://redis.io/docs/latest/develop/ai/context-engine/agent-mesh/quickstart#resume-fastapi-events).\nThis guide covers application setup, authorization, execution and recovery.\nFor an existing agent, use the [agent SDK recipes](#agent-sdk-recipes)."
    },
    {
      "id": "install-the-integration",
      "title": "Install the integration",
      "role": "setup",
      "text": "Use Python 3.11 or later and Redis 8.6.2 or later with native producer idempotency\n(`XADD IDMP`). Redis 8.6.6 or later is recommended by the upstream streaming guide.\n\n[code example]\n\nThe independent A2A integration is optional. Install both with\n`redis-iris[a2a,fastapi-sse]`; their shared redis-py range is `>=8.0.1,<9`.\nSee the [database setup](https://redis.io/docs/latest/develop/ai/context-engine/agent-mesh/quickstart#choose-your-redis-database)\nfor local Redis or Redis Cloud connections. Deployment qualification is separate\nfrom configuring an endpoint."
    },
    {
      "id": "configure-the-application",
      "title": "Configure the application",
      "role": "configuration",
      "text": "The facade re-exports the upstream FastAPI Redis SDK objects unchanged. Configure\nits standard lifespan and streaming feature before application startup:\n\n[code example]\n\nSet `REDIS_URL` for the connection, including any Transport Layer Security (TLS)\nparameters. Set `REDIS_PREFIX` for the namespace. The SDK creates and closes its\nRedis pools and stops its in-process producers when the application shuts down.\nUse its `REDIS_*` configuration and `.env` sources; settings are cached globally.\nThe integration does not accept injected clients, per-app settings or a separate\nblocking-client argument.\n\nKeep `REDIS_SOCKET_TIMEOUT` unset, or longer than\n`REDIS_STREAM_HEARTBEAT_INTERVAL` (15 seconds by default), so blocking reads can\ncomplete. Each live reader holds one connection from the shared pool. If you set\n`REDIS_MAX_CONNECTIONS`, reserve capacity for readers plus writes and other\ncommands. There are no relay buckets or subscriber queues."
    },
    {
      "id": "authorize-scoped-runs",
      "title": "Authorize scoped runs",
      "role": "security",
      "text": "A run is identified by a `scope` and `stream_id`. Choose a scope for the resource\nyou authorize, such as a tenant's chat or job. Multiple runs can share a scope;\nyour application tracks the current run and enforces any one-run-at-a-time policy.\n\nAuthenticate and authorize before passing the scope and run ID to the backend.\nNeither `StreamBackendDep` nor `backend.sse()` performs application access checks.\nThe application dependency in this example verifies access to the job and returns\nits authorized tenant and run identifiers:\n\n[code example]\n\nAuthorize every reconnect. IDs and cursors grant no access. Scopes accept\nletters, digits, underscores, periods, colons and hyphens; run IDs accept letters,\ndigits, underscores and hyphens. Keep credentials out of URLs.\n\nSame-origin cookie authentication works with native `EventSource`. Bearer tokens\nneed a fetch-based SSE client or another client that accepts authorization\nheaders. Pass a timezone-aware verified credential deadline as `expires_at` to\n`backend.sse()` to bound the open connection. This does not provide immediate\nrevocation."
    },
    {
      "id": "start-a-producer-and-publish-events",
      "title": "Start a producer and publish events",
      "role": "content",
      "text": "Supply an async producer that receives a `StreamWriter`:\n\n[code example]\n\n`start()` opens the run and starts the producer only when it was newly created.\nA repeated start with the same scope and retained ID returns a retry handle\nwithout spawning a second producer. `writer.append(type, data)` and\n`writer.finish(type, data)` return Redis entry IDs. Event types are application\nnames; the `stream.` prefix is reserved for SDK events.\n\nFor additional work between opening and spawning, use `await streams.open(...)`\nand call `streams.spawn(writer, producer)` only when `writer.created` is true.\nTo read without HTTP, use\n`async with await streams.follow(run_id, scope=scope, after=cursor)`.\nEvents expose `id`, `type`, `data` and `final` fields.\n\nThe SDK owns the producer task and generates its write-retry identities.\nApplications cannot supply producer/event idempotency keys. Disconnecting a\nreader leaves the task running. `cancel()` ends the run for readers and cancels\na producer in the same process. A producer in another process keeps running;\nits later events are unread. Runs cannot be handed to another process or restart\nafter a process exit. Publisher-only external workers are not supported."
    },
    {
      "id": "agent-sdk-recipes",
      "title": "Agent SDK recipes",
      "role": "content",
      "text": "Connect an existing agent to resumable server-sent events (SSE) by translating\nits asynchronous events into `StreamWriter` calls. Choose one recipe for the\nOpenAI Agents SDK, PydanticAI, Google Agent Development Kit (ADK), or LangGraph.\nThe agent framework stays responsible for tools, model credentials, and\nconversation state. Iris carries one turn's events.\n\nInstall `redis-iris[fastapi-sse]` and your chosen framework with its model-provider\ndependencies. These recipes adapt the\n[upstream agent recipes](https://github.com/redis/fastapi-redis-sdk/blob/99b27df0f2b5100d4a4ac04762b004f2aeef986b/docs/guide/streams.md#agent-sdk-recipes),\nwhich target `openai-agents` 0.24, `pydantic-ai-slim` 2.54, `google-adk` 2.7,\nand `langgraph` 1.2 with `langchain-core` 1.7. Check event types when upgrading.\nEach snippet takes your configured agent, runner, or compiled graph as an\nargument; it does not configure a model or create a conversation store."
    },
    {
      "id": "use-a-shared-event-format",
      "title": "Use a shared event format",
      "role": "content",
      "text": "The four recipes publish the same application event types:\n\n| Type | Data | Write method |\n| --- | --- | --- |\n| `delta` | `{\"text\": ...}` | `append()` |\n| `tool_call` | `{\"id\": ..., \"name\": ..., \"args\": ...}` | `append()` |\n| `tool_result` | `{\"id\": ..., \"output\": ...}` | `append()` |\n| `message` | `{\"text\": ...}`, the completed answer | `finish()` |\n| `turn.interrupted` | `{\"interrupts\": [...]}`, an approval or input request | `finish()` |\n\nAppend `delta` text in the browser. Use the final `message` to set the completed\nanswer; appending it would duplicate the streamed text. The LangGraph recipe\nalso demonstrates `turn.interrupted`.\n\nMap only the fields your interface needs. Whole framework events can contain\nagents, tools, or conversation history that cannot be encoded or exceed\n`stream_max_event_bytes`. Summarize large tool results. The default payload\ncoder accepts Pydantic models and dataclasses, but the output still must fit\nthe event limit. Tool arguments and results can contain sensitive data; publish\nonly what the authorized reader should see."
    },
    {
      "id": "start-and-serve-a-turn",
      "title": "Start and serve a turn",
      "role": "content",
      "text": "Configure your FastAPI application as described in\n[Configure the application](#configure-the-application). Save your chosen\n`run_turn` recipe in your application's `myapp.streaming` module. For OpenAI\nAgents or PydanticAI, start it from an authorized route like this:\n\n[code example]\n\n`myapp.agent` supplies your configured agent. `require_job_access` is your own\nauthentication and authorization dependency, as in\n[Authorize scoped runs](#authorize-scoped-runs). It returns the authorized job\nand its application-assigned run ID. Reuse that ID for retries of the same turn;\nassign a new ID for a new turn. For ADK and LangGraph, replace the `producer`\nassignment with the call shown in their recipe.\n\nServe the run using the existing `job_events` route. Authorize cancellation\nthrough the same dependency:\n\n[code example]\n\nDisconnecting a reader leaves the producer running. With the SDK revision used\nby these recipes, cancellation in the producer's process cancels the task and\nreaches its awaited model call or tool. A cancellation handled by another\nprocess ends delivery to readers but does not stop that producer or its model\nspend. Do not catch and suppress task cancellation in your producer.\n\nIf the SDK raises, the backend ends the stream with\n`{\"reason\": \"error\", \"code\": \"producer_error\"}` instead of exposing exception\ntext. A string `code` attribute on the exception overrides `producer_error`.\nKeep conversation history in the framework's session or checkpoint store;\nreplaying SSE events does not resume agent computation."
    },
    {
      "id": "openai-agents-sdk",
      "title": "OpenAI Agents SDK",
      "role": "content",
      "text": "Pass your configured `agents.Agent` as `agent`. Text deltas come from raw\nResponses API events; tool calls and results come from run-item events.\n\n[code example]\n\nRead `final_output` after consuming `stream_events()`. This recipe handles turns\nwithout tool approvals. If your tools require approval, handle\n`result.interruptions` before writing the final `message`, as described in\n[Handle human input](#handle-human-input)."
    },
    {
      "id": "pydanticai",
      "title": "PydanticAI",
      "role": "content",
      "text": "Pass your configured `pydantic_ai.Agent` as `agent`. Consume both the initial text\npart and subsequent text deltas so the first chunk is preserved.\n\n[code example]\n\nUse `async with` for `run_stream_events()` so leaving the loop closes the run.\nPublish only the result output, rather than its whole message history. This\nrecipe assumes tools finish within the turn; deferred-tool output needs the\n[human-input pattern](#handle-human-input)."
    },
    {
      "id": "google-adk",
      "title": "Google ADK",
      "role": "content",
      "text": "Pass your configured ADK `Runner` as `runner`. Its session must exist before the\nproducer starts. Resolve `user_id` and `session_id` from authorized application\nstate, and create or load that session through your session service.\n\n[code example]\n\nUse this producer assignment in the start route, with your application-owned\nrunner and the authorized ADK user and session identifiers:\n\n[code example]\n\n`StreamingMode.SSE` enables partial text events. The final non-partial event\nrepeats the answer, so it becomes the final `message`, not another delta. The\nrecipe excludes thought parts from the browser output. Long-running tools that\nwait for a person need the [human-input pattern](#handle-human-input)."
    },
    {
      "id": "langgraph",
      "title": "LangGraph",
      "role": "content",
      "text": "Pass a compiled graph with a checkpointer as `graph`. This recipe expects graph\nstate with a `messages` key and message-bearing node updates. It uses `messages`\nfor text deltas and `updates` for tool calls, tool results, and interrupts.\n\n[code example]\n\nFor a new turn, use an application-authorized checkpoint thread ID:\n\n[code example]\n\nAfter an interrupt, start a new stream with a new run ID and the same checkpoint\nthread. Validate and authorize the answer before resuming:\n\n[code example]\n\n`message.text` joins text blocks for providers that return structured content.\nAvoid the `values` stream mode here; it sends the whole state after every step.\nKeep checkpoint storage and access isolation in your application."
    },
    {
      "id": "handle-human-input",
      "title": "Handle human input",
      "role": "content",
      "text": "When an agent pauses for approval or an answer, finish the current stream with\n`writer.finish(\"turn.interrupted\", ...)`. Publish only the approval fields the\nreader needs, and persist resumable framework state in the session or checkpoint\nstore. After authorized input arrives, resume through the framework and start\na new stream with a new run ID.\n\nThe LangGraph recipe implements this for `__interrupt__` updates. For OpenAI\nAgents SDK interruptions, PydanticAI deferred tools, and ADK long-running tools,\nadd the corresponding framework-specific state and approval handling before\nusing these recipes with those features. Do not serialize whole framework\nobjects as the interruption payload.\n\nThese recipes use asynchronous producers in the FastAPI process. They do not\nadd synchronous producers, external durable workers, or automatic computation\nrestart after deployment."
    },
    {
      "id": "resume-and-recover",
      "title": "Resume and recover",
      "role": "content",
      "text": "Save each event cursor after applying its update. Reconnect to the same scope\nand run with `Last-Event-ID` or `?after=`. Selection order is explicit\n`backend.sse(after=...)`, then the header, then the query, then the start of the\nstream. Every supplied cursor is validated; repeated or malformed values fail.\nA reconnect at the final cursor returns HTTP 204.\n\nRetention bounds replay. Provide an authorized application endpoint returning\ncurrent state together with a matching cursor when clients need recovery.\nReload state when reconnecting returns HTTP 404 for an unknown or expired run,\nor HTTP 410 for externally removed history.\nA `stream.gap` event during delivery reports missing entries and the reader\ncontinues; a client requiring complete history should recover application state.\n\nBefore headers, errors have the shape `{\"detail\": \"...\", \"code\": \"...\"}`.\nRead the top-level `code` field:\n\n| HTTP status | Code or condition | Client action |\n| --- | --- | --- |\n| 401 | `credential_expired` | Refresh verified credentials. |\n| 404 | `stream_not_found` | Retrieve application state. |\n| 410 | `stream_expired` | Reload state; entries were removed externally. |\n| 409 | `cursor_ahead` | Reconcile application state and cursor. |\n| 422 | `invalid_cursor`, `invalid_stream_name` | Correct the request. |\n| 503 | `stream_unavailable`, `stream_unsupported`, or reader capacity | Retry transient failures with backoff; check server requirements for unsupported features. |\n| 204 | Final event consumed | Stop reconnecting. |\n\nAuthentication dependencies retain their own status and challenge headers.\nAfter headers, transient failures close the connection and clients reconnect.\nHandle SDK events `stream.end` (including `error`, `cancelled` and\n`producer_lost` outcomes) and `stream.error` (credential expiry or read-only\nproducer-loss reporting). Native\n`EventSource` cannot inspect refused-connection bodies; use the application\nstate endpoint or a fetch-based client to explain failures."
    },
    {
      "id": "configure-retention",
      "title": "Configure retention",
      "role": "configuration",
      "text": "A run has a fixed lifetime from `open()`, bounded by `stream_max_run`. Nothing\nextends it. Completion shortens remaining retention to at most `stream_done_ttl`.\nSet `stream_max_run` for the longest expected run, up to 86,400 seconds.\n\nA reader that sees no new entries for `stream_silence_timeout` ends the run with\na `producer_lost` final event. `spawn()` runs `writer.keepalive()` around the\nproducer, writing hidden pings during quiet work. An application-managed producer\ncan enter that context itself. Pings prevent silence detection but do not extend\nthe run's lifetime. A producer lost on a process exit is not restarted.\n\nThe SDK retains all events until the run expires. A writer that exceeds\n`stream_maxlen` raises `ValueError` rather than trimming earlier events.\n\n| Setting (prefix with `REDIS_`) | Default | Purpose |\n| --- | --- | --- |\n| `STREAM_MAX_RUN` | 3,600 seconds | Fixed run lifetime from creation. |\n| `STREAM_DONE_TTL` | 120 seconds | Maximum remaining retention after completion. |\n| `STREAM_MAXLEN` | 10,000 | Maximum events one writer can append. |\n| `STREAM_MAX_EVENT_BYTES` | 65,536 | Largest encoded event. |\n| `STREAM_HEARTBEAT_INTERVAL` | 15 seconds | SSE ping, blocking-read bound and keepalive check interval. |\n| `STREAM_SILENCE_TIMEOUT` | 60 seconds | Silence before a reader ends a lost producer's run; must exceed twice the heartbeat interval. |\n| `STREAM_MAX_READERS` | 256 | Live-reader admission per process. |\n| `STREAM_REPLAY_PAGE` | 100 | Events per Redis read. |\n| `STREAM_MAX_CONNECTION_DURATION` | 300 seconds | Connection cap before reconnect. |\n| `STREAM_READ_RETRY_WINDOW` | 30 seconds | Retry window for transient read failures. |\n| `STREAM_SEND_TIMEOUT` | 10 seconds | Bound for stalled sends. |\n\nThe demo sets `REDIS_STREAM_DONE_TTL=3600`; replay still ends at the run's\noriginal lifetime. Application state in your own database has separate retention.\n\nSee the\n[beta limits](https://redis.io/docs/latest/develop/ai/context-engine/agent-mesh#requirements-and-limits)\nfor deployment qualification."
    }
  ],
  "examples": [
    {
      "id": "install-the-integration-ex0",
      "language": "bash",
      "code": "pip install 'redis-iris[fastapi-sse]'",
      "section_id": "install-the-integration"
    },
    {
      "id": "configure-the-application-ex0",
      "language": "python",
      "code": "from fastapi import FastAPI\nfrom redis_iris.agent_mesh.fastapi_sse import FastAPIRedis\n\napp = FastAPI()\nFastAPIRedis(app).lifespan().streams()",
      "section_id": "configure-the-application"
    },
    {
      "id": "authorize-scoped-runs-ex0",
      "language": "python",
      "code": "from fastapi import Depends, Request\nfrom fastapi.responses import StreamingResponse\nfrom redis_iris.agent_mesh.fastapi_sse import StreamBackendDep\n\nfrom myapp.auth import require_job_access\n\n\n@app.get(\n    \"/jobs/{job_id}/events\",\n    response_class=StreamingResponse,\n    responses={200: {\"content\": {\"text/event-stream\": {}}}},\n)\nasync def job_events(\n    request: Request,\n    streams: StreamBackendDep,\n    job=Depends(require_job_access),\n):\n    scope = f\"tenant:{job.tenant_id}:job:{job.id}\"\n    return await streams.sse(job.run_id, request=request, scope=scope)",
      "section_id": "authorize-scoped-runs"
    },
    {
      "id": "start-a-producer-and-publish-events-ex0",
      "language": "python",
      "code": "from redis_iris.agent_mesh.fastapi_sse import StreamWriter\n\n\nasync def run_job(writer: StreamWriter):\n    result = await perform_job()\n    await writer.append(\"job.progress\", {\"percent\": 100})\n    await writer.finish(\"job.completed\", {\"result\": result})\n\n\n# In your authorized start endpoint, with a StreamBackendDep named streams:\n# scope and run_id come from the application's job record.\nwriter = await streams.start(run_job, scope=scope, stream_id=run_id)",
      "section_id": "start-a-producer-and-publish-events"
    },
    {
      "id": "start-and-serve-a-turn-ex0",
      "language": "python",
      "code": "from fastapi import Depends\nfrom pydantic import BaseModel\nfrom redis_iris.agent_mesh.fastapi_sse import StreamBackendDep\n\nfrom myapp.agent import agent\nfrom myapp.auth import require_job_access\nfrom myapp.streaming import run_turn\n\n\nclass Prompt(BaseModel):\n    text: str\n\n\n@app.post(\"/jobs/{job_id}/runs\", status_code=202)\nasync def start_turn(\n    body: Prompt,\n    streams: StreamBackendDep,\n    job=Depends(require_job_access),\n):\n    producer = run_turn(body.text, agent=agent)\n    writer = await streams.start(\n        producer,\n        scope=f\"tenant:{job.tenant_id}:job:{job.id}\",\n        stream_id=job.run_id,\n    )\n    return {\"stream_id\": writer.stream_id}",
      "section_id": "start-and-serve-a-turn"
    },
    {
      "id": "start-and-serve-a-turn-ex1",
      "language": "python",
      "code": "@app.post(\"/jobs/{job_id}/runs/cancel\", status_code=204)\nasync def cancel_turn(\n    streams: StreamBackendDep,\n    job=Depends(require_job_access),\n) -> None:\n    await streams.cancel(\n        job.run_id, scope=f\"tenant:{job.tenant_id}:job:{job.id}\"\n    )",
      "section_id": "start-and-serve-a-turn"
    },
    {
      "id": "openai-agents-sdk-ex0",
      "language": "python",
      "code": "import json\n\nfrom agents import Runner\nfrom openai.types.responses import ResponseTextDeltaEvent\nfrom redis_iris.agent_mesh.fastapi_sse import StreamWriter\n\n\ndef run_turn(prompt: str, *, agent):\n    async def produce(writer: StreamWriter) -> None:\n        result = Runner.run_streamed(agent, prompt)\n        async for event in result.stream_events():\n            if event.type == \"raw_response_event\":\n                if isinstance(event.data, ResponseTextDeltaEvent):\n                    await writer.append(\"delta\", {\"text\": event.data.delta})\n            elif event.type == \"run_item_stream_event\":\n                if event.name == \"tool_called\":\n                    call = event.item.raw_item\n                    await writer.append(\"tool_call\", {\n                        \"id\": call.call_id,\n                        \"name\": call.name,\n                        \"args\": json.loads(call.arguments or \"{}\"),\n                    })\n                elif event.name == \"tool_output\":\n                    await writer.append(\"tool_result\", {\n                        \"id\": event.item.raw_item[\"call_id\"],\n                        \"output\": event.item.output,\n                    })\n        await writer.finish(\"message\", {\"text\": str(result.final_output)})\n\n    return produce",
      "section_id": "openai-agents-sdk"
    },
    {
      "id": "pydanticai-ex0",
      "language": "python",
      "code": "from pydantic_ai import (\n    AgentRunResultEvent, FunctionToolCallEvent, FunctionToolResultEvent,\n    PartDeltaEvent, PartStartEvent, TextPart, TextPartDelta,\n)\nfrom redis_iris.agent_mesh.fastapi_sse import StreamWriter\n\n\ndef run_turn(prompt: str, *, agent):\n    async def produce(writer: StreamWriter) -> None:\n        async with agent.run_stream_events(prompt) as events:\n            async for event in events:\n                if isinstance(event, PartStartEvent) and isinstance(event.part, TextPart):\n                    if event.part.content:\n                        await writer.append(\"delta\", {\"text\": event.part.content})\n                elif isinstance(event, PartDeltaEvent) and isinstance(event.delta, TextPartDelta):\n                    await writer.append(\"delta\", {\"text\": event.delta.content_delta})\n                elif isinstance(event, FunctionToolCallEvent):\n                    await writer.append(\"tool_call\", {\n                        \"id\": event.part.tool_call_id,\n                        \"name\": event.part.tool_name,\n                        \"args\": event.part.args_as_dict(),\n                    })\n                elif isinstance(event, FunctionToolResultEvent):\n                    await writer.append(\"tool_result\", {\n                        \"id\": event.part.tool_call_id,\n                        \"output\": event.part.content,\n                    })\n                elif isinstance(event, AgentRunResultEvent):\n                    await writer.finish(\"message\", {\"text\": event.result.output})\n\n    return produce",
      "section_id": "pydanticai"
    },
    {
      "id": "google-adk-ex0",
      "language": "python",
      "code": "from google.adk.agents.run_config import RunConfig, StreamingMode\nfrom google.genai import types\nfrom redis_iris.agent_mesh.fastapi_sse import StreamWriter\n\n\ndef run_turn(prompt: str, *, runner, user_id: str, session_id: str):\n    async def produce(writer: StreamWriter) -> None:\n        message = types.Content(role=\"user\", parts=[types.Part(text=prompt)])\n        config = RunConfig(streaming_mode=StreamingMode.SSE)\n        answer = \"\"\n        async for event in runner.run_async(user_id=user_id, session_id=session_id,\n                                            new_message=message, run_config=config):\n            if event.partial:\n                # A chunk of the answer.  The final, non-partial event repeats\n                # the whole text, so only chunks become deltas.\n                parts = event.content.parts if event.content else None\n                for part in parts or []:\n                    if part.text and not part.thought:\n                        await writer.append(\"delta\", {\"text\": part.text})\n                continue\n            for call in event.get_function_calls():\n                await writer.append(\"tool_call\",\n                                    {\"id\": call.id, \"name\": call.name, \"args\": call.args})\n            for response in event.get_function_responses():\n                await writer.append(\"tool_result\",\n                                    {\"id\": response.id, \"output\": response.response})\n            if event.is_final_response() and event.content and event.content.parts:\n                answer = \"\".join(part.text or \"\" for part in event.content.parts\n                                 if not part.thought)\n        await writer.finish(\"message\", {\"text\": answer})\n\n    return produce",
      "section_id": "google-adk"
    },
    {
      "id": "google-adk-ex1",
      "language": "python",
      "code": "producer = run_turn(\n    body.text, runner=runner, user_id=user_id, session_id=session_id\n)",
      "section_id": "google-adk"
    },
    {
      "id": "langgraph-ex0",
      "language": "python",
      "code": "from langchain_core.messages import AIMessage, AIMessageChunk, ToolMessage\nfrom redis_iris.agent_mesh.fastapi_sse import StreamWriter\n\n\ndef run_turn(inputs, *, graph, config):\n    async def produce(writer: StreamWriter) -> None:\n        answer = \"\"\n        async for mode, chunk in graph.astream(inputs, config,\n                                               stream_mode=[\"messages\", \"updates\"]):\n            if mode == \"messages\":\n                message, _metadata = chunk\n                if isinstance(message, AIMessageChunk) and message.text:\n                    await writer.append(\"delta\", {\"text\": message.text})\n                continue\n            if \"__interrupt__\" in chunk:\n                # The graph waits for a person.  End this turn; the answer\n                # resumes the graph as a new turn, on a new stream.\n                interrupts = [item.value for item in chunk[\"__interrupt__\"]]\n                await writer.finish(\"turn.interrupted\", {\"interrupts\": interrupts})\n                return\n            for update in chunk.values():\n                for message in (update or {}).get(\"messages\", []):\n                    if isinstance(message, ToolMessage):\n                        await writer.append(\"tool_result\", {\n                            \"id\": message.tool_call_id, \"output\": message.content})\n                    elif isinstance(message, AIMessage) and message.tool_calls:\n                        for call in message.tool_calls:\n                            await writer.append(\"tool_call\", {\n                                \"id\": call[\"id\"], \"name\": call[\"name\"],\n                                \"args\": call[\"args\"]})\n                    elif isinstance(message, AIMessage):\n                        answer = message.text\n        await writer.finish(\"message\", {\"text\": answer})\n\n    return produce",
      "section_id": "langgraph"
    },
    {
      "id": "langgraph-ex1",
      "language": "python",
      "code": "thread = {\"configurable\": {\"thread_id\": checkpoint_thread_id}}\nproducer = run_turn(\n    {\"messages\": [(\"user\", body.text)]}, graph=graph, config=thread\n)",
      "section_id": "langgraph"
    },
    {
      "id": "langgraph-ex2",
      "language": "python",
      "code": "from langgraph.types import Command\n\nproducer = run_turn(Command(resume=answer), graph=graph, config=thread)",
      "section_id": "langgraph"
    }
  ]
}
