feat(py): stream tool send_chunk and send_partial on generate - #6314
huangjeff5 wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements support for streaming tool-role chunks via ToolRunContext.send_chunk during generate_stream. It updates the chunk accumulator to handle role changes properly, propagates the tool chunk streaming callback through the tool execution pipeline, and adds comprehensive tests. Feedback is provided on normalize_send_chunk_parts to optimize list validation by avoiding an unnecessary list copy and using typing.cast instead.
| if isinstance(chunk, list): | ||
| out: list[Part] = [] | ||
| for item in chunk: | ||
| if not isinstance(item, Part): | ||
| raise GenkitError( | ||
| status='INVALID_ARGUMENT', | ||
| message=f'send_chunk parts must be Part values, got {type(item).__name__}.', | ||
| ) | ||
| out.append(item) | ||
| return out |
There was a problem hiding this comment.
For improved efficiency and code clarity, you can avoid creating a new list here. After validating the items, you can use typing.cast to inform the type checker about the list's contents. This avoids an unnecessary list copy and is a common pattern for this type of validation.
if isinstance(chunk, list):
for item in chunk:
if not isinstance(item, Part):
raise GenkitError(
status='INVALID_ARGUMENT',
message=f'send_chunk parts must be Part values, got {type(item).__name__}.',
)
return cast(list[Part], chunk)There was a problem hiding this comment.
We keep the copy so send_chunk does not hand the caller's list through to generate. After this returns they can keep mutating that list; the streamed chunk stays what they passed.
51f5443 to
de8691d
Compare
During
generate_stream, a tool can push mid-run updates two ways.ctx.send_chunk(...)takes aPartor list ofParts. Each call arrives as aModelResponseChunkwithrole='tool'and those parts.ctx.send_partial(value)takes any value. Each call arrives as a tool-role chunk with oneToolResponsePart: this tool request's name and ref,metadata.partialTrue, andoutputset to the value you passed.History and the next Agent turn keep only the completed tool return, including after a wire hop.
tool.stream()yieldssend_chunkParts only. Tool-rolechunk.outputstays empty.Decisions
send_chunktakes aPartorlist[Part]. Each call is one tool-roleModelResponseChunkwith those parts.send_chunkis unstamped (no name or ref).send_partial(value)takes any value. Each call is one tool-role chunk with oneToolResponsePart: this tool request's name and ref,metadata.partialTrue, output is the value you passed. Two tools sending at once each stamp their own name and ref.tool.stream()yieldssend_chunkParts only.send_partialdoes not appear there.chunk.outputstays empty. Read text orpart.tool_response.outputfrom the parts.await generate(...)/ no listener:send_chunkvalidates then no-ops;send_partialno-ops.generate.send_streamcarries the same generate chunks. Walk the parts (metadata.partial/ text) the same way.