Repository navigation
Add forward_subagent_text option - #1206
Conversation
By default the CLI only forwards a foreground subagent's tool_use / tool_result blocks into the parent stream (as assistant/user messages whose parent_tool_use_id is the spawning Agent tool_use id), which is enough for a progress heartbeat but not for rendering the nested transcript. The TypeScript SDK exposes an opt-in `forwardSubagentText` initialize capability that makes the CLI forward the subagent's text and thinking blocks the same way; the Python SDK had no counterpart. Add ClaudeAgentOptions.forward_subagent_text and send it as `forwardSubagentText: true` in the initialize control request from both query() and ClaudeSDKClient (omitted when False, so older CLIs and the default path are untouched). SDKControlInitializeRequest now also lists the other optional initialize fields the SDK already sends. Unit tests cover the initialize payload and the plumbing from ClaudeAgentOptions through both entry points; e2e tests run a foreground subagent and assert that its text arrives attributed to the Agent tool_use id only when the option is set.
There was a problem hiding this comment.
LGTM — a small, opt-in plumbing change that mirrors the TypeScript SDK's forwardSubagentText and follows the SDK's established initialize-capability pattern.
What was reviewed
- Plumbing of
forward_subagent_textthrough both entry points (query()andClaudeSDKClient) intoQuery.initialize()— identical to the existingexcludeDynamicSections/skillshandling. - Wire-payload compatibility: the field is only sent when enabled, so the default initialize request is byte-identical to before and older CLIs are unaffected.
- The
SDKControlInitializeRequestTypedDict additions — documentation-only (the request is built as a plain dict), consistent with what the SDK already sends.
Extended reasoning...
Overview
This PR adds ClaudeAgentOptions.forward_subagent_text, the Python counterpart of the TypeScript SDK's forwardSubagentText initialize capability. It touches 7 files: a new dataclass field with a docstring in types.py, a new Query.__init__ parameter and one conditional line in Query.initialize(), one-line plumbing in each of the two entry points (_internal/client.py and client.py), TypedDict additions to SDKControlInitializeRequest, unit tests, and a new e2e test. No existing behavior changes.
Security risks
None. The change adds an opt-in boolean that is forwarded to the CLI over the existing control protocol. It does not touch auth, permissions, subprocess spawning, or input parsing. The forwarded subagent text flows through the same message-parsing path already used for tool_use/tool_result blocks; the SDK does not interpret it.
Level of scrutiny
Low-to-moderate. This is a mechanical option-plumbing change following a pattern used three times already in the same function (agents, excludeDynamicSections, skills). The key correctness property — that the default wire payload is unchanged so older CLIs are unaffected — is enforced by only sending the field when true, and is asserted by unit tests for both the enabled and default cases in both entry points.
Other factors
The bug hunting system found no issues. Test coverage is thorough for the size of the change: initialize-payload assertions at the Query layer, end-to-end plumbing assertions through query() and ClaudeSDKClient, and a real-CLI e2e test verifying both the enabled and default behaviors. The SDKControlInitializeRequest TypedDict additions are type-documentation only (the request dict is constructed untyped), so they carry no runtime risk. The docstring accurately matches the TypeScript SDK's documented semantics.
…uest Sends "forwardSubagentText": true in the initialize control request (omitted when false) so the CLI forwards subagent text and thinking blocks as messages in the stream, not just tool_use/tool_result blocks. Threaded through claude.Query, WarmQuery, and Client the same way Options.Agents/PresetPrompt.ExcludeDynamicSections already are. Port of Python SDK commit c97420c (anthropics/claude-agent-sdk-python#1206), matching the TypeScript SDK's existing forwardSubagentText. Closes #601
Sends "forwardSubagentText": true in the initialize control request (omitted when false) so the CLI forwards subagent text and thinking blocks as messages in the stream, not just tool_use/tool_result blocks. Threaded through claude.Query, WarmQuery, and Client the same way Options.Agents/PresetPrompt.ExcludeDynamicSections already are. Port of Python SDK commit c97420c (anthropics/claude-agent-sdk-python#1206), matching the TypeScript SDK's existing forwardSubagentText. Closes #601 Co-authored-by: Claude <noreply@anthropic.com>
Summary
Adds
ClaudeAgentOptions.forward_subagent_text, the Python counterpart of the TypeScript SDK'sforwardSubagentTextoption.By default the CLI only forwards a foreground subagent's
tool_use/tool_resultblocks into the parent stream (asAssistantMessage/UserMessageobjects whoseparent_tool_use_idis the spawning Agenttool_useid) — enough for a progress heartbeat, but not for rendering the nested transcript.forwardSubagentTextis an opt-ininitializecapability that makes the CLI forward the subagent's text and thinking blocks the same way. The Python SDK had no way to turn it on.Changes:
ClaudeAgentOptions.forward_subagent_text: bool = Falsewith a docstring.Query.initialize()sendsforwardSubagentText: truewhen set; omitted otherwise, so the default wire payload is unchanged and older CLIs are unaffected. Plumbed through bothquery()andClaudeSDKClient.SDKControlInitializeRequestnow also lists the optional initialize fields the SDK already sends (excludeDynamicSections,skills) alongside the new one.Test plan
tests/test_query.pyasserts the initialize payload with the option on/off and thatClaudeAgentOptions.forward_subagent_textreaches the initialize request viaquery();tests/test_streaming_client.pyasserts the same viaClaudeSDKClient.e2e-tests/test_forward_subagent_text.pyruns a foreground subagent (customAgentDefinitionwithbackground=False) against the real CLI and asserts that (a) with the option set, at least oneAssistantMessagecontaining aTextBlockarrives withparent_tool_use_idequal to the observed Agenttool_useid, and (b) without it, no attributed text is forwarded. Ran it several times locally; stable.ruff check,ruff format,mypy src/, fullpytest tests/(1372 passed).