fix: Accept Stream instances in as_tool() and subagent_tool() stream parameter#2951
fix: Accept Stream instances in as_tool() and subagent_tool() stream parameter#2951Oxygen56 wants to merge 1 commit into
Conversation
|
Hi @Oxygen56 — this PR now has merge conflicts with Please rebase onto the current ⏳ If there's no update within 30 days, this PR will be marked stale and closed 7 days after that. |
genisis0x
left a comment
There was a problem hiding this comment.
This is a clean fix for #3021 — widening stream to Stream | StreamFactory and auto-wrapping an instance in a factory is the right shape, and I like that you added the TypeError guard, the docstrings, and test coverage. Confirmed the discriminator is safe: the Stream protocol has no __call__ and MemoryStream doesn't define one, so not callable(stream) correctly separates an instance from a factory, and the isinstance(stream, Stream) check backs it up.
Two things worth thinking about before merge:
-
Shared-instance semantics under
background_agent_tool. The auto-wrap returns the same stream object on every factory call (lambda _agent, _ctx: _captured_stream). Forsubagent_tool/as_toolthat's fine and is exactly what #3021 wants — the user's stream captures the sub-agent's events. Butbackground_agent_toolis fire-and-forget, so a parent can kick off several background sub-agents that now all share oneMemoryStream.MemoryStreamcarries a per-instance_ag2_turn_lock, so those concurrent background runs will serialize on that lock and interleave their turns into the one event log. That's arguably the desired "observe everything in one place" behavior, but it's a meaningful semantic change for the background case (independent tasks now contend on a shared lock and share history) and I'd suggest calling it out in the docstring, plus a test that spins up two concurrent background runs against one shared stream to pin down the interleaving contract. -
Minor DRY: the normalize block (
if stream is not None and not callable(stream): ...) is now duplicated verbatim inbackground.pyandsubagent_tool.py. Sinceas_toolalready delegates tosubagent_tool, pulling this into a small_as_stream_factory(stream) -> StreamFactory | Nonehelper (raising the sameTypeError) would keep the two call sites from drifting.
Neither is a correctness blocker for the non-background path — the core fix looks right.
…parameter Previously, Agent.as_tool(stream=MemoryStream()) silently failed because as_tool() expected a StreamFactory callable, not a Stream instance. When a raw Stream was passed, subagent_tool tried to call it as a function, the sub-agent fell back to creating an ephemeral stream in run_task(), and the user's stream stayed empty. This fix: - Accepts Stream | StreamFactory | None in as_tool(), subagent_tool(), and background_agent_tool() stream parameters - Auto-wraps Stream instances in a lambda factory - Raises TypeError for invalid stream types - Adds 4 tests covering Stream instance, factory, None, and invalid type Fixes #2888 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11e8673 to
58919f6
Compare
|
Thanks for the review and for flagging the merge conflict. I rebased the branch onto the current I also addressed both follow-up suggestions:
After the package move, the affected paths are now under Verification:
The PR is now mergeable against |
There was a problem hiding this comment.
Following up on my earlier review now that _as_stream_factory is in — thanks for pulling the normalize logic into one helper. One thing on its ordering: it checks callable(stream) before isinstance(stream, Stream), which is correct today only because no Stream type defines __call__. If a Stream subclass ever becomes callable (a stream that doubles as its own factory isn't far-fetched), a real instance would take the callable branch and get invoked as stream(agent, ctx) — the exact failure this PR fixes. Checking isinstance(stream, Stream) first, then callable, then None would be robust regardless. Minor and non-blocking; the rest still looks good.
What
Fixes #3021 —
MemoryStreampassed toas_tool()does not capture sub-agent events.Root Cause
Agent.as_tool(stream=...)only acceptedStreamFactory(a callable), not aStreaminstance. When users naturally passedMemoryStream(), the code tried to call it as a function, the sub-agent silently fell back to creating an ephemeral stream inrun_task(), and the user's stream stayed empty.Fix
agent.py: Changedstreamparameter type fromStreamFactory | NonetoStream | StreamFactory | Noneinas_tool()subagent_tool.py: Added auto-wrapping logic — if aStreaminstance is passed, it's wrapped in a lambda factory. RaisesTypeErrorfor invalid types.background.py: Same fix forbackground_agent_tool()which had the same patterntest/beta/tools/test_task.py::TestAsToolStreamcovering:Verification
$ pytest test/beta/tools/test_task.py -xvs 37 passed # all existing tests + 4 new testsBackward Compatibility
✅ Fully backward compatible —
StreamFactorycallables andNonecontinue to work.