feat(py): stream attributed progress from automatically executed tools - #6274
feat(py): stream attributed progress from automatically executed tools#6274huangjeff5 wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for streaming partial tool responses (progress updates) in Genkit. It adds tool_responses to AgentChunk and ModelResponseChunk, implements a mechanism to stream partial tool outputs via ToolRunContext.send_partial, and ensures that transient progress is skipped when stitching or persisting messages. The review feedback is highly constructive and identifies several key improvement opportunities: adding a defensive check in ModelResponseChunk.tool_responses to handle cases where self.content is None, binding the middleware-modified p.tool_request_part instead of the original trp or restart_trp when invoking on_partial in _generate.py, and storing references to background tasks created with asyncio.create_task in the test files to prevent premature garbage collection.
| @property | ||
| def tool_responses(self) -> list[ToolResponsePart]: | ||
| """Tool response parts carried by this chunk.""" | ||
| return [part.root for part in self.content if isinstance(part.root, ToolResponsePart)] |
There was a problem hiding this comment.
If self.content is None, iterating over it will raise a TypeError. We should add a defensive check or default to an empty list to prevent potential runtime crashes when a chunk has no content.
| return [part.root for part in self.content if isinstance(part.root, ToolResponsePart)] | |
| return [part.root for part in (self.content or []) if isinstance(part.root, ToolResponsePart)] |
| tool=p.tool, | ||
| tool_request_part=p.tool_request_part, | ||
| ctx=c, | ||
| on_partial=partial(on_partial, trp) if on_partial is not None else None, |
There was a problem hiding this comment.
When invoking on_partial inside next_fn, we should bind p.tool_request_part instead of trp. If any wrap_tool middleware modifies the tool request part (e.g., updating metadata or input), using the original trp would pass stale information to the progress reporter.
| on_partial=partial(on_partial, trp) if on_partial is not None else None, | |
| on_partial=partial(on_partial, p.tool_request_part) if on_partial is not None else None, |
| tool=p.tool, | ||
| restart_trp=p.tool_request_part, | ||
| ctx=c, | ||
| on_partial=partial(on_partial, restart_trp) if on_partial is not None else None, |
There was a problem hiding this comment.
Similarly to the fresh tool execution path, we should bind p.tool_request_part instead of restart_trp when invoking on_partial inside next_fn for restarted tools. This ensures that any middleware-modified tool request part is correctly propagated to the progress reporter.
| on_partial=partial(on_partial, restart_trp) if on_partial is not None else None, | |
| on_partial=partial(on_partial, p.tool_request_part) if on_partial is not None else None, |
| ctx.send_partial('late') | ||
| late_finished.set() | ||
|
|
||
| asyncio.create_task(later()) |
There was a problem hiding this comment.
In Python's asyncio, tasks created with asyncio.create_task should have a reference kept to prevent them from being garbage collected mid-execution. We should store the task in a variable or a set to ensure it runs to completion reliably.
| asyncio.create_task(later()) | |
| _task = asyncio.create_task(later()) |
| ctx.send_partial('late') | ||
| late_finished.set() | ||
|
|
||
| asyncio.create_task(later()) |
Python tools can now call
ToolRunContext.send_partial(...)to report structured, attributed progress whilegenerate_streamis automatically executing them. Progress arrives as transient tool-roleToolResponsePartchunks carrying the active tool name and ref, and agent callers can consume the same updates throughAgentChunk.tool_responses. Typed model output remains scoped to model-role chunks.Agent streams expose the same structured progress without adding it to conversation state:
Decisions:
send_partialis the structured progress API. Its payload is wrapped in an attributedToolResponsePart, so consumers can associate updates with the active call.send_chunkremains the raw streaming API for direct action execution only. Raw chunks are not injected intogenerate_stream, which keeps generated model and tool protocols distinct.AgentChunk.tool_responsesexposes progress at the agent layer without requiring callers to decode raw transport chunks.ModelResponseChunkvalues from tools is out of scope. Tool progress has a structured wire shape rather than model-authored chunk semantics.