docs: add subagent completion design spec

This commit is contained in:
pi
2026-04-11 01:19:40 +01:00
parent 0077379f3d
commit e99edfee42
2 changed files with 139 additions and 0 deletions

View File

@@ -21,6 +21,7 @@ Applies to entire `pi-subagents` package in this repo root.
- `src/tool.ts` / `src/schema.ts` — tool contract and parameter/result types.
- `src/wrapper/cli.mjs` — child-session wrapper that writes artifacts/results.
- `src/*.test.ts`, `src/wrapper/*.test.ts` — regression tests.
- `docs/superpowers/specs/` — design docs created during brainstorming before implementation.
- `docs/superpowers/plans/` — implementation plans and migration notes.
## Working rules

View File

@@ -0,0 +1,138 @@
# Subagent Wrapper Semantic Completion Design
**Date:** 2026-04-11
**Package:** `pi-subagents`
## Goal
Fix bug where a subagent has already produced its final assistant output, but the parent tool never reports completion and the main agent stays stuck waiting.
## Root cause
Current completion depends on `result.json` being written by `src/wrapper/cli.mjs`.
Shared failure mode across both runners:
1. Child `pi` process emits a terminal assistant event (`message_end` → normalized `assistant_text` with `stopReason`).
2. Wrapper records the final text in `events.jsonl` / transcript output.
3. Wrapper still waits for the child process to exit before writing `result.json`.
4. If the child process lingers after terminal output, `result.json` is never written.
5. `src/monitor.ts` waits forever for `result.json`, so the tool never finishes.
This is shared across process and tmux runners because both use the same wrapper and file-based monitor contract.
## Chosen approach
Implement **wrapper-level semantic completion** in `src/wrapper/cli.mjs`.
When the wrapper observes a terminal normalized `assistant_text` event:
- treat that event as semantic proof that the subagent conversation is complete,
- allow a short grace window for natural child-process exit,
- if the child still has not exited after the grace window, terminate it,
- preserve the captured final text / model / stop reason,
- write `result.json` and let the parent tool complete.
## Scope
### Modify
- `src/wrapper/cli.mjs`
- `src/wrapper/cli.test.ts`
- `AGENTS.md` — document `docs/superpowers/specs/`
### Do not modify
- `src/tmux.ts` — keep tmux helpers only
- tool/schema registration behavior
- runner selection/config behavior
- model resolution behavior
- artifact file layout
## Detailed behavior
### 1. Completion trigger
Terminal completion is detected only from the wrappers normalized event stream:
- event type: `assistant_text`
- terminal signal: `stopReason` present
This keeps the rule aligned with the existing wrapper event model instead of adding a second completion protocol.
### 2. Grace window
After terminal assistant output is observed, wait a short internal grace period (target: about 250 ms) for the child `pi` process to exit on its own.
Reasoning:
- normal runs should still exit naturally,
- short lag after final output is acceptable,
- lingering processes should not block the parent forever.
### 3. Forced cleanup
If the child is still alive after the grace window:
1. send `SIGTERM`,
2. wait a second short cleanup window,
3. escalate to `SIGKILL` only if still required.
This cleanup is wrapper-internal and applies equally regardless of whether the top-level runner is process or tmux.
### 4. Result semantics
If terminal assistant output was already captured before cleanup:
- semantic outcome comes from the captured terminal event, not from the cleanup signal,
- `finalText` comes from captured assistant output,
- `resolvedModel` stays based on captured model/event data,
- `stopReason` stays the terminal assistant stop reason,
- forced cleanup after semantic completion must **not** downgrade a normal completion into an error.
This means:
- normal terminal completion stays successful even if cleanup was needed afterward,
- terminal error/aborted outcomes stay error/aborted instead of being rewritten.
If terminal assistant output was **not** captured, existing error behavior remains unchanged.
### 5. Existing guarantees to preserve
- best-effort artifact appends must still never block final `result.json` writing,
- child runs must still set `PI_SUBAGENTS_CHILD=1`,
- github-copilot initiator behavior must stay unchanged,
- no tmux-specific logic should leak into non-tmux helper files.
## Testing strategy
Follow TDD.
### First failing regression test
Add a wrapper regression test that:
1. creates a fake `pi` executable,
2. emits one terminal `message_end` JSON event with final text,
3. intentionally stays alive instead of exiting,
4. verifies the wrapper exits promptly,
5. verifies `result.json` is still written,
6. verifies a normal terminal completion still produces a successful result with preserved final text.
### Verification after implementation
Run at least:
- targeted wrapper test file
- full package test suite (`npm test`)
## Non-goals
- redesigning `src/monitor.ts`
- changing runner contracts or schema types
- adding user-configurable timeout settings
- broad refactors outside the wrapper regression fix
## Expected outcome
After the change, once a subagent has emitted its terminal assistant output, the parent tool will complete even if the spawned child `pi` process lingers instead of exiting promptly. This removes the stuck-tool behavior for both runner modes while keeping the fix on the smallest shared surface.