Skip to content

feat(harness): invoke_stream + sub-agent delta propagation + scripted mock streaming#17

Merged
senamakel merged 3 commits into
tinyhumansai:mainfrom
senamakel:feat/invoke-stream
Jul 4, 2026
Merged

feat(harness): invoke_stream + sub-agent delta propagation + scripted mock streaming#17
senamakel merged 3 commits into
tinyhumansai:mainfrom
senamakel:feat/invoke-stream

Conversation

@senamakel

Copy link
Copy Markdown
Member

Summary

Adds a caller-consumable streaming API to the agent harness and the sub-agent
propagation + mock fidelity that make it useful.

invoke_stream (Step 1)

AgentHarness::invoke_stream(...) returns an async Stream<Item = AgentStreamItem>
— live Event(EventRecord) items in emission order (model/reasoning deltas,
ToolStarted/ToolCompleted lifecycle, sub-agent lifecycle, usage, …), then
exactly one terminal Completed(run) / Failed(err). It's a thin projection
over the existing ordered EventSink::emit fan-out — a ChannelListener
forwards each record into an unbounded channel the stream drains — not a
parallel event system. The run advances only while the stream is polled.

Sub-agent model-delta propagation (Step 2)

A streaming parent now sees a child sub-agent's own model/reasoning deltas, not
just its start/complete bracket. The drive mode is threaded down: RunContext
gains a streaming flag (set by the loop driver), ToolExecutionContext
carries it, and SubAgentTool runs the child through the streaming path when
the parent is streaming. Because sub-agents already share the parent's
EventSink, child deltas land on the parent stream stamped with the child's
own run_id and depth. A non-streaming parent is unchanged (flag stays
false).

MockModel::streaming_script (Step 4)

A MockBehavior::StreamScript(Vec<ModelStreamItem>) variant + constructor:
stream() emits the scripted items verbatim so tests exercise truly
incremental streaming (fine-grained text/reasoning/tool-call deltas), while
invoke() folds them through a StreamAccumulator into the equivalent unary
response.

Tool-call lifecycle events (Step 3 in the plan) already exist upstream and
surface through the stream unchanged.

Tests

invoke_stream event-then-terminal ordering, tool-lifecycle surfacing, Failed
terminal; nested sub-agent asserts child deltas (child run id) arrive before
the parent completes, and that a non-streaming parent emits none; scripted
incremental deltas surface reasoning + multiple text deltas; invoke-fold.

Commands run

  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test (939 lib + integration, green)

Ref: OpenHuman docs/tinyagents-vendor-improvement-plan/03-streaming.md (V3, Steps 1/2/4).

senamakel added 3 commits July 4, 2026 18:13
Adds AgentHarness::invoke_stream, the streaming counterpart to
invoke_streaming that returns an async Stream of the run's events instead of
only the final AgentRun.

The run's EventSink already fans out every AgentEvent in a single ordered
sequence (model/reasoning deltas, ToolStarted/ToolCompleted lifecycle,
SubAgentStarted/Completed — the latter reach the stream because sub-agents
share the parent's sink). invoke_stream subscribes a ChannelListener that
forwards each EventRecord into an unbounded channel, drives the shared
streaming loop path, and yields AgentStreamItem::Event(..) for each event
followed by exactly one terminal AgentStreamItem::Completed(run) /
Failed(err). It is a thin projection over the same emit fan-out, not a
parallel event system; the run advances only while the stream is polled.

Lineage rides the events themselves (ModelDelta stamps run_id; sub-agent
events stamp depth). Streaming a child agent's own model deltas into the
parent — which needs the streaming flag routed through the sub-agent runner
(children run the non-streaming path today) — is tracked as follow-up.

Tests: event-then-Completed ordering, tool-lifecycle surfacing
(ToolStarted before ToolCompleted), and the Failed terminal on a limit trip.

Ref: docs/tinyagents-vendor-improvement-plan/03-streaming.md Step 1 (V3)
A streaming parent (invoke_stream) previously saw a sub-agent's start/complete
lifecycle but none of the child's own model/reasoning deltas: run_child always
drove the child through the unary invoke_in_context path.

Thread the drive mode down to the child so it matches the parent:
- RunContext gains a streaming flag, set by the loop driver (drive).
- ToolExecutionContext carries it (from_run_context), so a tool sees whether
  its caller run is streaming.
- SubAgentTool::call_with_context passes context.streaming to run_child, and
  invoke_in_parent forwards parent.streaming; run_child then drives the child
  through invoke_streaming_in_context when streaming.

Because sub-agents already share the parent's EventSink, the child's deltas
then land on the parent stream stamped with the child's own run_id and depth.
A non-streaming parent leaves streaming=false, so its event stream is
unchanged (direct SubAgent::invoke/invoke_with_events stay non-streaming).

Tests: nested run asserts child ModelDeltas (run id 'researcher-d1…') arrive
before the parent's RunCompleted, plus SubAgentStarted/Completed at depth 1;
and that a non-streaming parent emits no child deltas.

Ref: docs/tinyagents-vendor-improvement-plan/03-streaming.md Step 2 (V3)
Adds a MockBehavior::StreamScript(Vec<ModelStreamItem>) variant and the
MockModel::streaming_script constructor. stream() emits the scripted items
verbatim — so tests exercise truly incremental streaming (fine-grained
text/reasoning/tool-call deltas in an exact order) rather than the one-or-two
synthetic deltas the other constructors replay — while invoke() folds the same
items through a StreamAccumulator to produce the equivalent unary response.

Tests: invoke_stream over a scripted reasoning+text stream surfaces the
reasoning delta and multiple incremental text deltas; invoke folds a
delta-only script into the reconstructed response.

Ref: docs/tinyagents-vendor-improvement-plan/03-streaming.md Step 4 (V3)
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a35a32fe-d0a7-4e1d-bf38-624733e86205

📥 Commits

Reviewing files that changed from the base of the PR and between 9442bd2 and 45bd370.

📒 Files selected for processing (13)
  • src/graph/goals/test.rs
  • src/graph/todos/test.rs
  • src/harness/agent_loop/entry.rs
  • src/harness/agent_loop/mod.rs
  • src/harness/agent_loop/stream.rs
  • src/harness/agent_loop/test.rs
  • src/harness/context/mod.rs
  • src/harness/context/types.rs
  • src/harness/providers/mock.rs
  • src/harness/providers/types.rs
  • src/harness/subagent/mod.rs
  • src/harness/subagent/test.rs
  • src/harness/tool/types.rs

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 45bd370f07

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Done,
}

impl<State: Send + Sync, Ctx: Send + Sync + 'static> AgentHarness<State, Ctx> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow borrowed contexts in invoke_stream

When an application uses a borrowed per-run context type (for example AgentHarness<State, &RequestCtx>), this new API is unavailable because the impl requires Ctx: 'static, even though the stream owns ctx_data, is lifetime-bound to &self/state, and is not spawned. The existing invoke paths accept non-'static contexts, so this makes the caller-consumable streaming path unusable for otherwise valid request-scoped contexts; relaxing this to a method-level Ctx: 'a bound (or equivalent) would preserve the intended lifetime without forcing 'static.

Useful? React with 👍 / 👎.

@senamakel
senamakel merged commit ced59ca into tinyhumansai:main Jul 4, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant