Skip to content

feat(#1366): phase 1 — sub-agent sync-dispatch scaffold + KODA_SUBAGENT_SYNC kill switch#1367

Closed
lijunzh wants to merge 1 commit into
mainfrom
epic/1366-phase1-sync-dispatch-scaffold
Closed

feat(#1366): phase 1 — sub-agent sync-dispatch scaffold + KODA_SUBAGENT_SYNC kill switch#1367
lijunzh wants to merge 1 commit into
mainfrom
epic/1366-phase1-sync-dispatch-scaffold

Conversation

@lijunzh
Copy link
Copy Markdown
Owner

@lijunzh lijunzh commented May 10, 2026

Phase 1 of #1366 — sub-agent sync-dispatch scaffold

Epic: #1366 — lean sub-agent rewrite (fg-sync delegation, kill mailbox/bg/pill)
Status: phase 1 of 7 — purely additive scaffolding, no behavior change at default settings

What this PR does

Adds the public sync-dispatch entry point and a runtime kill switch so the rest of the #1366 epic can land incrementally without disturbing the legacy bg-spawn path from #1163.

  • execute_sub_agent_sync() — new public symbol in sub_agent_dispatch.rs. Thin wrapper over execute_sub_agent(..., inline_only=true), promoting the existing recursion-guard branch to a first-class API.
  • subagent_sync_enabled() + KODA_SUBAGENT_SYNC env var — runtime kill switch. Default unset → legacy bg-spawn (no behavior change). Set to 1/true → new sync streaming dispatch.
  • tool_dispatch.rs callsite gating — both InvokeAgent and SpawnAgent branch on the env var. Both paths coexist for the migration window.

Why this shape

Behavior

KODA_SUBAGENT_SYNC Dispatch Tool result shape
unset / 0 / false (default) Legacy bg-spawn (#1163) Background agent 'X' started (agent:N). Results will be injected when complete.
1 / true / TRUE / True New sync streaming Sub-agent's actual final output

Test status

Suite Default (unset) Opt-in (KODA_SUBAGENT_SYNC=1)
koda-core lib (1397 tests) ✅ pass ✅ pass
e2e_agent_test (12 tests) ✅ pass ⚠️ 6 fail (expected)
agent_mailbox_e2e_test ✅ pass n/a (tests legacy path)
tool_wiring_test ✅ pass ✅ pass
new_tools_test ✅ pass ✅ pass
cargo fmt --check
cargo clippy -p koda-core

The 6 e2e failures with the flag SET are expected and out of scope for phase 1: they assert on the bg-spawn registry's terminal-status ChildTaskUpdate events that the FG path deliberately does not emit (documented at sub_agent_dispatch.rs:1075). Migrating those assertions to the sync-path event shape is phase 2 work.

Phase roadmap (from #1366)

# What This PR
1 Sync-dispatch scaffold + env-var gate ✓ this PR
2 Flip default to sync; migrate e2e tests follow-up
3 Delete WaitForMail + bg-dispatch path follow-up
4 Delete mailbox infrastructure (~1100 LOC) follow-up
5 Delete pill + overlay + child_activity (~1200 LOC) follow-up
6 Add SubAgentBlock widget (codex ExecCell-inspired) follow-up
7 Add SubAgentGroup container (gemini-style parallel-N) follow-up

Manual smoke test

# Default behavior (unchanged)
koda  # InvokeAgent returns task_id message, model uses WaitForMail

# New sync path
KODA_SUBAGENT_SYNC=1 koda  # InvokeAgent blocks, returns sub-agent's final output directly

Risk

Very low. Default code path is byte-identical to pre-PR behavior — same callsite, same inline_only=false, same legacy bg-spawn branch. The new symbol is unreachable without explicit opt-in via env var.

Refs: #1366

…NT_SYNC kill switch

Adds the public sync-dispatch entry point (`execute_sub_agent_sync`) and
a runtime kill switch (`KODA_SUBAGENT_SYNC=1`) so the rest of the #1366
epic can land incrementally without disturbing the legacy bg-spawn path
from #1163.

The wrapper is intentionally a thin call to `execute_sub_agent(...,
inline_only=true)`, promoting the existing recursion-guard branch to a
public symbol. This keeps phase 1 purely additive — phase 2 will be a
one-line callsite swap in tool_dispatch to flip the default.

Default behavior unchanged (env var unset → legacy bg-spawn path).
With KODA_SUBAGENT_SYNC=1 the FG inference path runs synchronously and
returns the sub-agent's final output as the tool result, no task_id
round-trip via WaitForMail.

Tests
- New `sync_dispatch_tests::subagent_sync_env_var_parsing` pins the
  truthy/falsy/unset matrix (single #[test] because cargo parallelizes
  and the env slot is process-global).
- 1397 koda-core lib tests pass with flag unset (default behavior).
- 12 e2e_agent_test cases pass with flag unset.
- With flag SET, 6/12 e2e cases fail because they assert on bg-spawn
  registry terminal-status events the FG path deliberately doesn't
  emit (sub_agent_dispatch.rs:1075 documents this). Migrating those
  assertions is phase 2 work, not scope for this PR.

Wire format unchanged — ACP / headless clients unaffected.

Refs: #1366
@lijunzh
Copy link
Copy Markdown
Owner Author

lijunzh commented May 10, 2026

Closing — env-var kill switch violates DESIGN.md P1 ("compile decisions in, don't configure them at runtime"). Redoing as a single commit that swaps the callsite directly + migrates the 6 e2e tests in the same PR. New PR incoming.

@lijunzh lijunzh closed this May 10, 2026
@lijunzh lijunzh deleted the epic/1366-phase1-sync-dispatch-scaffold branch May 10, 2026 02:27
lijunzh added a commit that referenced this pull request May 10, 2026
#1368)

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
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