symfonic.core.tools.streaming¶
streaming ¶
Async-generator tool streaming support — Roadmap Item 7.
A tool authored as async def gen(...): yield ... carries semantics
the LangGraph prebuilt ToolNode cannot directly consume: it calls
tool.ainvoke(...) and expects a single awaitable result, not an
async iterator. This module bridges the gap without introducing a
new transport:
wrap_async_gen_tooldetectsinspect.isasyncgenfunctionon aStructuredTool'scoroutineattribute and returns a replacement tool whosecoroutineis a plainasync defthat drains the generator, dispatching every non-final yield as a custom event namedtool_progressvialangchain_core.callbacks.adispatch_custom_event. The final yield is returned as the tool result.prepare_tools_for_streamingapplies the wrap helper to a list of tools, returning a list of identical length where async-gen tools are replaced and every other tool is passed through by identity (zero behaviour change for callers that do not opt in).
Why adispatch_custom_event is the right channel: LangGraph's
astream_events(v2) already pipes such events to consumers as
{"event": "on_custom_event", "name": ..., "data": ..., "run_id": ...}
where run_id is the parent tool call. The transpiler
(EventTranspiler._handle_tool_progress) then converts them to typed
ToolProgressEvent items, so the new event flows through the same
backpressure-aware iterator that already buffers ToolResultEvent.
prepare_tools_for_streaming ¶
Apply wrap_async_gen_tool to each tool, preserving order.
Tools that are not async-generator StructuredTools are returned
by identity, so callers can pass the result directly to
langgraph.prebuilt.ToolNode with zero behaviour change for
existing non-generator tools.
Source code in src/symfonic/core/tools/streaming.py
wrap_async_gen_tool ¶
Replace an async-generator tool's coroutine with a streaming wrapper.
If tool is not a StructuredTool carrying an async-generator
coroutine, the original tool is returned by identity. This
keeps the wrap helper safe to call indiscriminately from preset
wiring (see prepare_tools_for_streaming).
The replacement tool is constructed via
StructuredTool.from_function rather than mutating the original
object so the input tool remains frozen — preserving Pydantic
invariants on BaseTool subclasses.