feat(dispatch): pivot to all-async dispatch via push-callback#20
Open
randomm wants to merge 3 commits into
Open
feat(dispatch): pivot to all-async dispatch via push-callback#20randomm wants to merge 3 commits into
randomm wants to merge 3 commits into
Conversation
…19) Every dispatch tool (dispatch_specialist, dispatch_parallel, adversarial_loop, dispatch_lens_review) is now fire-and-forget from Pi's perspective: tool execute() returns a {jobId} handle in <100ms, the child runs under extension supervision, and on completion the extension fires pi.sendUserMessage(report, {deliverAs: "steer"}) to push the result back to the parent agent. This fixes the responsiveness problem: under the old sync semantics the PM was locked mid-tool-call for the entire duration of any dispatch (often minutes), so the user could not /steer, ask questions, or redirect. Now the PM ends its turn after dispatching → Pi goes idle waiting for the next prompt → user can interact freely → our push delivery wakes the PM when results arrive. Invariants enforced (see #19): - Parent agent only sees the child's final assistant text (same bytes sync dispatch would have returned). No transcript dump. - Async adds zero context bloat over sync — envelope is ~165 bytes (header + footer + formatting) per the smoke test. - Batched orchestrators (dispatch_parallel, lens review) emit ONE consolidated steer when ALL members settle — never N out-of-order arrivals. Preserves the "I called the tool, I expect one return" mental model. - PM doctrine forbids reading transcript files under ensemble-runs/. New tools: - dispatch_status — metadata-only snapshot of in-flight jobs - dispatch_kill <jobId> — abort a running subagent or batch Doctrine updates: - agents-base/project-manager.md "Sync vs Async Task Mode" section rewritten as "Async Dispatch Protocol" — describes the [ensemble:async] message flow, status/kill tools, anti-patterns - pi-prompts/work.md gets a one-paragraph "How dispatch works" preamble before Step 1 - cancel_task references replaced with dispatch_kill Smoke test (extension/smoke-tests/test-async-dispatch.ts): stubs pi.sendUserMessage, asserts startJob returns instantly, exactly ONE steer fires on completion, body bounded, killJob aborts via AbortSignal, batch fires ONE consolidated message after all N settle. 33 assertions, all green.
The async pivot mentioned /steer as a way for users to interact with PM during async dispatch. In practice if PM is mid-turn the user just waits until they can type (typically <10s), so teaching /steer is noise. Stay focused on the simple model: PM ends its turn fast, user types normally when they want to interject.
Two cleanups now that the adversarial loop is programmatic:
1. PM doctrine had a stale "Adversarial Review (MANDATORY)" section
telling PM to manually orchestrate rounds (dispatch adversarial → if
issues, dispatch developer → re-dispatch). That predated the
adversarial_loop tool which encapsulates the entire 3-round saga in
TypeScript. PM now gets a single instruction: call adversarial_loop,
wait for the report, surface escalation options verbatim if REJECTED.
2. The escalation message itself was vague ("Halt the workflow and ask
the user for guidance"). Per the opencode pattern, the user should
be presented with concrete options:
(a) authorise another 3-round pass
(b) accept current state and commit anyway (logged override)
(c) abandon and rework approach
(d) take over manually
The tool returns these options in its REJECTED text so the PM just
relays them. No new logic; clearer escalation semantics.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #19.
Summary
Every dispatch tool (
dispatch_specialist,dispatch_parallel,adversarial_loop,dispatch_lens_review) is now fire-and-forget from Pi's perspective: toolexecute()returns a{ jobId }handle in < 100ms, the child runs under extension supervision, and on completion the extension firespi.sendUserMessage(report, { deliverAs: "steer" })to push the result back to the parent agent.The motivating problem: under sync semantics the PM was locked mid-tool-call for the entire duration of any dispatch (often minutes). The user could not
/steer, ask questions, or redirect. With async, the PM ends its turn after dispatching → Pi goes idle waiting for the next prompt → user can interact freely → our push delivery wakes the PM when results arrive.Invariants enforced
dispatch_parallel/lens review wait for all N to settle, then fire one reportproject-manager.mdmakes this explicitWhat's new
extension/src/async-jobs.ts— job registry,startJob/startBatch, abort lifecycle, bounded report formattersextension/src/dispatch-status.ts— newdispatch_status(metadata-only snapshot) anddispatch_killtoolsextension/smoke-tests/test-async-dispatch.ts— 33 assertions covering single jobs, batched jobs, cancellation, fail reportsagents-base/project-manager.md("Async Dispatch Protocol" section) andpi-prompts/work.mdTest plan
/workin a real Pi session, verify user can/steermid-dispatch[ensemble:async]user message arrives via steer when child exitsRelease-please
This is a
feat:commit → minor bump → v0.4.0 once merged.🤖 Generated with Claude Code