Skip to content

fix(agent): route parallel/council fan-out to one concurrent spawn_parallel_agents call (#4754)#4757

Merged
senamakel merged 2 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/GH-4754-parallel-fanout-routing
Jul 13, 2026
Merged

fix(agent): route parallel/council fan-out to one concurrent spawn_parallel_agents call (#4754)#4757
senamakel merged 2 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/GH-4754-parallel-fanout-routing

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

What

An agent-efficiency eval found the orchestrator never fanning out sub-agents concurrently (PARALLEL=0 across the whole run):

  • "spin up a separate researcher for each … in parallel" → 1 spawn (ignored the fan-out).
  • "convene a council … multiple independent opinions" → 1 spawn.
  • "fan out and summarize each of my last 4 threads" → 3 spawns at 9.8s / 155.2s / 355.5s — strictly serial, each sub-agent finishing before the next started.

Root cause (verified in code) — routing, not harness concurrency

I traced the concurrency path end to end:

  • tinyagents spawn_parallel_agentsmap_reduce is genuinely concurrentfutures::stream::iter(...).buffer_unordered(N) with eagerly-constructed per-worker futures. openhuman's adapter passes max_concurrency = N and FailurePolicy::CollectAll. As a single tool call it also sidesteps the harness's serial-multi-call path.
  • So the concurrent fan-out primitive already works. The eval's failures come from the orchestrator using serial spawn_subagent (single or repeated) instead of the one concurrent spawn_parallel_agents call. The "only 1 spawn" council/parallel cases are decisive: the model never fanned out at all, which no harness change can fix.

This is an openhuman routing defect: the model reaches for spawn_subagent even when the user explicitly asks for parallel/council work.

Fix (minimal) — steer at the two decision points the model actually uses

  1. orchestrator/prompt.md — explicit trigger→tool mapping: "in parallel" / "a separate researcher for each X" / "convene a council / multiple independent opinions" / "fan out over N" ⇒ one spawn_parallel_agents call (one task per worker), with a warning that looping spawn_subagent runs strictly one-at-a-time (~145s+ each) and defeats the parallel intent.
  2. spawn_subagent.rs tool description — the string the model reads at tool-selection time now redirects concurrent fan-out to spawn_parallel_agents and calls out that repeated spawn_subagent serializes.
  3. tests/orchestrator_parallel_fanout_routing.rs — regression test pinning the routing guidance in both places (mirrors the existing orchestrator_*_wiring.rs include-str tests).

Secondary finding (reported, out of scope)

The tinyagents harness does serialize multi-tool-call turns whenever tool-wrap middleware is registered (openhuman always registers 5: schema-guard, approval, cli-rpc, tool-policy, credential-scrub) — because ToolMiddleware::wrap_tool(&mut RunContext) holds an exclusive borrow across the call (agent_loop/tools.rs: tool_calls.len() > 1 && tool_middleware_len() == 0). That's a real latency limitation for legitimate in-turn multi-tool fan-out, but it's documented as deliberate, its fix is architectural (not minimal), and it would not address the confirmed single-spawn cases here. Flagging for separate tracking rather than changing the harness in this PR.

Testing

No local build (disk constrained); CI will compile/run. Failing-before/passing-after is deterministic: on main the prompt contains council 0× and the serial-loop warning 0×, and spawn_subagent's description mentions spawn_parallel_agents 0×; with the fix all three are present. (Note: the repo's cargo test --lib target has a pre-existing, unrelated compile break on mainweb_tests.rs imports super::compose_system_prompt_suffix, which web/mod.rs doesn't re-export — so this regression is written as an integration test that compiles against the clean lib, per the existing orchestrator_*_wiring.rs convention.)

Closes #4754

@M3gA-Mind
M3gA-Mind requested a review from a team July 9, 2026 17:09
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

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: 41 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: dc270808-6046-4d23-9c0b-cca8eb4e213b

📥 Commits

Reviewing files that changed from the base of the PR and between 54511e6 and 9e404f1.

📒 Files selected for processing (3)
  • src/openhuman/agent_orchestration/tools/spawn_subagent.rs
  • src/openhuman/agent_registry/agents/orchestrator/prompt.md
  • tests/orchestrator_parallel_fanout_routing.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: 6e8d1a2c93

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/openhuman/agent_orchestration/tools/spawn_subagent.rs Outdated
…rallel_agents call (tinyhumansai#4754)

An efficiency eval found the orchestrator never fanning out workers
concurrently: parallel / "separate researcher for each" / council prompts
either single-spawned or issued serial `spawn_subagent` calls 145-200s apart
(each sub-agent finishing before the next started), defeating the request.

Root cause is routing, not harness concurrency. `spawn_parallel_agents`
already fans out concurrently (tinyagents `map_reduce` / `buffer_unordered`)
as a single tool call, but the orchestrator reached for serial `spawn_subagent`
instead — and the "only 1 spawn" council case proves the model simply never
fanned out, which no harness change can fix.

Steer fan-out to one `spawn_parallel_agents` call in the two places the model
actually decides:
- orchestrator prompt: map "in parallel" / "a separate X for each" / council /
  "fan out over N" to a single `spawn_parallel_agents` call, and warn that
  looping `spawn_subagent` runs strictly one-at-a-time.
- `spawn_subagent` tool description: redirect concurrent fan-out to
  `spawn_parallel_agents` (the description is what the model reads at
  tool-selection time).

Note: tinyagents also serializes multi-tool-call turns when tool-wrap
middleware is present (openhuman always registers several) because
`ToolMiddleware::wrap_tool` holds `&mut RunContext`; that is a separate,
deliberate harness limitation and out of scope here.

Regression: tests/orchestrator_parallel_fanout_routing.rs.
@M3gA-Mind
M3gA-Mind force-pushed the fix/GH-4754-parallel-fanout-routing branch from 6e8d1a2 to 8a78f18 Compare July 9, 2026 18:49
@M3gA-Mind M3gA-Mind self-assigned this Jul 9, 2026
…g blocking (addresses @chatgpt-codex-connector on spawn_subagent.rs:79)

The new tool description opened with "Runs ONE sub-agent and blocks until
it returns", but the default path (blocking unset) routes to
SpawnAsyncSubagentTool and returns immediately. Reword to state the async
default and point at `blocking: true` for inline output, while keeping the
fan-out redirect to `spawn_parallel_agents` intact.
@YellowSnnowmann

Copy link
Copy Markdown
Contributor

Approve. The fix delivers what #4754 needs, and the root-cause call is correct.

The issue guessed the bug was in the tinyagents harness. Verified it is not: map_reduce fans out concurrently via futures::stream::iter(...).buffer_unordered(concurrency) with eagerly-built per-item futures (vendor/tinyagents/src/graph/parallel/mod.rs:62-80), and spawn_parallel_agents is registered for the orchestrator (agent.toml:239, tools/ops_tests.rs:190/221). So the concurrent primitive already works — the eval's PARALLEL=0 failures, especially the two cases that produced only ONE spawn, are a routing failure (model reaching for serial spawn_subagent) that no harness change could address. Steering the model at both decision points it reads is the right minimal lever.

Notes:

  • The prompt fan-out block (orchestrator/prompt.md:117-125) sits cleanly after the existing "Parallel fan-out (spawn_parallel_agents)" section and reinforces, rather than contradicts, the surrounding guidance.
  • The 3 include-str assertions in tests/orchestrator_parallel_fanout_routing.rs are all satisfiable against shipped text (spawn_parallel_agents, council at prompt.md:119, never a loop of at prompt.md:117, and spawn_parallel_agents inside the description() body) — meaningful pins, not tautologies.
  • Secondary finding (tinyagents serializes in-turn multi-tool turns when tool-wrap middleware is registered — agent_loop/tools.rs:101, tool_calls.len() > 1 && tool_middleware_len() == 0) is real and accurately reported as out-of-scope: it's architectural and wouldn't fix the confirmed single-spawn cases. Fine to track separately.

Comment resolution: Codex P2 (description promising synchronous behavior) is genuinely addressed in 9e404f1 — the HEAD description() now reads "by default it runs as a reusable async worker and returns immediately — pass blocking: true to run it inline", which matches the actual if !blocking async-delegation default (spawn_subagent.rs:425). Thread is GitHub-unresolved but substantively handled; the author's reply documents the change. Worth clicking Resolve.

@YellowSnnowmann YellowSnnowmann left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving — feature satisfied, no blocker/major issues, review comments addressed in code, CI green. Detailed review in the PR comment above.

@senamakel
senamakel merged commit 73edfc1 into tinyhumansai:main Jul 13, 2026
16 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.

[agent][efficiency] Orchestrator never fans out sub-agents in parallel — serializes 145-200s apart (defeats parallel/council prompts)

3 participants