Skip to content

feat(#1366): phase 1 — sub-agents are now synchronous tool delegations#1368

Merged
lijunzh merged 1 commit intomainfrom
epic/1366-phase1-sync-dispatch
May 10, 2026
Merged

feat(#1366): phase 1 — sub-agents are now synchronous tool delegations#1368
lijunzh merged 1 commit intomainfrom
epic/1366-phase1-sync-dispatch

Conversation

@lijunzh
Copy link
Copy Markdown
Owner

@lijunzh lijunzh commented May 10, 2026

Phase 1 of #1366 — sub-agents are now synchronous tool delegations

Epic: #1366 — lean sub-agent rewrite (sync delegation, kill mailbox/bg/pill)
Phase: 1 of 5 — the swap. Phases 2–5 delete the now-dead bg infrastructure.
Replaces: #1367 (closed; gated the change behind KODA_SUBAGENT_SYNC env var, violating DESIGN.md P1 "compile decisions in, don't configure them at runtime").

What this PR does

Drops the bg-spawn dispatch path from #1163 and reverts the public sub-agent surface to synchronous delegation (codex ExecCell / gemini SubagentGroupDisplay shape). InvokeAgent and SpawnAgent now drive the sub-agent's inference loop inline on the parent's task and return its final answer as ToolCallResult.output — same shape as every other tool call.

The dispatcher change is two lines: inline_only=falsetrue at the InvokeAgent and SpawnAgent callsites in tool_dispatch.rs. Everything else is the ripple effect.

Why no env var

Per DESIGN.md P1 ("compile decisions in, don't configure them at runtime"), the sync swap is unconditional. A previous draft (#1367) gated the change behind KODA_SUBAGENT_SYNC=1 so the bg path could be tested side-by-side; that draft was rejected as a principle violation and abandoned. Default behavior IS the new behavior, full stop.

Tool-result shape change (BREAKING for the LLM contract)

Era InvokeAgent returns How the model consumes results
pre-#1163 (FG default) sub-agent's final output directly from ToolCallResult.output
#1163 → pre-#1366 (bg only) task_id confirmation string poll via WaitForMail or wait for auto-drain on a future iteration
#1366 (sync, this PR) sub-agent's final output directly from ToolCallResult.output

ACP / headless wire format is unchanged. EngineEvent::SubAgentStart still fires at dispatch time. The tool description was rewritten to reflect the new shape.

Test migrations

Test file Change
e2e_agent_test.rs 6 cases migrated from collect_bg_events_after(...).find_map(AgentStatus::Completed)events.iter().find_map(EngineEvent::ToolCallResult). The pre-#1163 wait-free assertion shape returns.
e2e_agent_test.rs bg_agent_iter_counter_advances_via_status_channel deleted — pinned the bg path's Running { iter: n } heartbeat, now unreachable from the public dispatcher.
e2e_sub_agent_trust_test.rs 2 cases lose their #1321/#1323/#1327 deflake waits — the race they guarded against (parent-loop termination vs bg-agent completion) does not exist on the sync path.
tools/agent.rs test_invoke_agent_description_documents_spawn_only_model → renamed/rewritten as test_invoke_agent_description_documents_sync_dispatch_model. Asserts on synchronously / BLOCKS / final answer and forbids task_id / WaitForMail / auto-drain / in the background as a regression guard.
tools/agent.rs test_invoke_agent_top_level_documents_drain_semantics and test_invoke_agent_description_discourages_immediate_wait deleted — drain semantics and the WaitForMail anti-pattern only existed because of bg-spawn.
tools/agent.rs test_invoke_agent_description_explains_error_marker_convention extended to also recognize the dispatcher's Err arm format ("Error invoking sub-agent: {chain}") as the second structural-failure shape.
tools/agent.rs test_invoke_agent_schema_does_not_carry_background_param kept — schema is unchanged.

Verification

Suite Result
cargo test -p koda-core --lib ✅ 1394 passed, 1 ignored
cargo test -p koda-core --tests (all integration) ✅ all green
cargo test -p koda-cli --lib ✅ 656 passed
cargo clippy --workspace --lib --tests --all-features ✅ clean
cargo fmt --check ✅ clean
pre-push hook (fmt + clippy) ✅ passed

Diff stats

 CHANGELOG.md                                |   2 +
 koda-core/src/tool_dispatch.rs              |  49 ++--
 koda-core/src/tools/agent.rs                | 224 +++++++----------
 koda-core/tests/e2e_agent_test.rs           | 363 ++++++++--------------------
 koda-core/tests/e2e_sub_agent_trust_test.rs |  54 +----
 5 files changed, 224 insertions(+), 468 deletions(-)

Net −244 LOC even before the deletion phases.

Phase roadmap (from #1366)

# What LOC delta This PR
1 Swap dispatch to sync; migrate tests −244 ✓ this PR
2 Delete WaitForMail tool + mailbox bridge ~−1,500 follow-up
3 Delete ChildAgentActivity event family + BgRegistry + run_bg_agent ~−2,000 follow-up
4 Delete activity pill + child-activity overlay UI ~−1,200 follow-up
5 Add SubAgentBlock (codex ExecCell-style) + SubAgentGroup (gemini parallel-N) widgets +~600 follow-up

Risk

The behavior change is intentional and load-bearing for the epic. The migrated tests cover every previously-asserted concern (sub-agent output reaches the parent, grace turn fires, preflight bails, max_turns marker surfaces, file writes execute, destructive bash blocks). Wire format is unchanged.

The deleted bg_agent_iter_counter_advances_via_status_channel test pinned a code path that's now dead from the public surface; phase 2/3 deletes the underlying code.

Refs: #1366
Closes: (none — phase 1 is foundational; the epic stays open through phase 5)

Drops the bg-spawn dispatch path from #1163 and reverts the public
sub-agent surface to synchronous delegation (codex ExecCell / gemini
SubagentGroupDisplay shape). InvokeAgent and SpawnAgent now drive the
sub-agent's inference loop inline on the parent's task and return its
final answer as ToolCallResult.output — same shape as every other tool.

No env-var / runtime config (per DESIGN.md P1: "compile decisions in,
don't configure them at runtime"). The swap is unconditional. A draft
that gated the change behind KODA_SUBAGENT_SYNC was rejected and
abandoned (closed #1367).

Code change is two lines: inline_only=false → true at the InvokeAgent
and SpawnAgent callsites in tool_dispatch.rs. Everything else is the
ripple effect:

- InvokeAgent tool description rewritten for sync semantics
  (BLOCKS / final answer / no task_id-WaitForMail-auto-drain).
- 4 description-pinning tests rewritten or deleted; new
  test_invoke_agent_description_documents_sync_dispatch_model
  forbids the async-era ghost terms as a regression guard.
- 6 e2e cases in e2e_agent_test.rs migrated from
  collect_bg_events_after(..).find_map(AgentStatus::Completed)
  to events.iter().find_map(EngineEvent::ToolCallResult).
- 2 e2e cases in e2e_sub_agent_trust_test.rs lose their #1321/
  #1323/#1327 deflake waits — the race they guarded against
  (parent-loop termination vs bg-agent completion) does not
  exist on the sync path.
- bg_agent_iter_counter_advances_via_status_channel test deleted
  (pinned the bg path's Running { iter: n } heartbeat, now
  unreachable from the public dispatcher).

Phases 2–5 of #1366 will delete the now-dead bg infrastructure
(WaitForMail, mailbox bridge, ChildAgentActivity, BgRegistry,
activity-pill UI, child-activity overlay — ~5,000 LOC).

Verified
- 1394 koda-core lib tests pass
- 656 koda-cli lib tests pass
- All koda-core integration suites pass
- cargo clippy --workspace --lib --tests --all-features clean
- cargo fmt --check clean

Net diff: +224 / −468 (−244 LOC).

Refs: #1366
@lijunzh lijunzh merged commit 2c4b17f into main May 10, 2026
16 checks passed
@lijunzh lijunzh deleted the epic/1366-phase1-sync-dispatch branch May 10, 2026 03:03
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