The shipped subagent example could say a child was done while it was
still working, lose the useful part of process failures, and return a
normal-looking tool result when the delegated work had actually failed.
Single and chain output could also exceed the normal tool limit.
This also adds a dedicated regression suite covering progress state,
cancellation, process failures, output limits, cwd-based agent
selection, and persisted tool-result error state.
After this change:
- queued, running, completed, failed, and aborted work stay distinct
throughout the run;
- interrupting the parent run preserves partial child results, stops
queued work from starting, and escalates when a child ignores SIGTERM;
- signal exits, spawn failures, provider errors, stderr, and assistant
output remain visible instead of collapsing to `(no output)`;
- failed subagents become failed tool results, so the parent model can
react instead of treating them as successful calls;
- single and chain results stay within the normal 50 KB / 2,000-line
limit;
- project agents are selected and confirmed from each task's actual
working directory rather than the dispatcher's directory.
## Further potential improvements
These are useful follow-ups, but they are separate product and storage
decisions rather than part of the failure-handling fixes in this PR.
<details>
<summary>Potential follow-ups</summary>
### Keep persisted transcripts bounded
Subagent tool results persist the full child message history in
`details`, which can make parent session files large and slow to reload.
A follow-up could compact that history or spill it to disk.
### Define an aggregate parallel-output budget
Parallel mode still gives every task its own 50 KB budget. A follow-up
could keep every task represented while enforcing one overall
tool-result limit.
### Report child usage to the calling session
Per-child token and cost usage is visible in result cards but is not
added to the calling session's totals.
### Isolate malformed agent definitions
One malformed YAML agent can still break discovery for every valid agent
in the same directory.
### Limit subagent recursion
A nesting limit would prevent accidental process and paid-request
multiplication when subagents can invoke the subagent tool again.
* https://github.com/earendil-works/pi/issues/8195
### Add a configurable timeout
Unattended runs would benefit from a generous opt-in timeout for
children that never complete.
</details>
## Related issues
### Spawn failures lose their diagnostics
This directly addresses the reported case where a nonexistent child
working directory produces `(no output)` instead of the actual spawn
error. The regression suite includes that same reproduction.
* https://github.com/earendil-works/pi/issues/8129
### Persisted child history can bloat sessions
This PR intentionally keeps full child history in tool-result details,
so session compaction or spill-to-disk remains follow-up work rather
than something fixed here.
* https://github.com/earendil-works/pi/issues/7452
`parseFrontmatter` runs a real YAML parser, so `tools: [read, bash]`
arrives as an array while `tools: read, bash` arrives as a string. The
loader only handled the string form and called `.split(",")`, which
throws on the array form.
The throw escapes `loadAgentsFromDir` -- only `readdirSync` and
`readFileSync` are guarded -- so one agent file using the array form
broke discovery for every agent in that scope, not just itself.
Normalize both spellings in one helper, and type the frontmatter fields
as `unknown` so the string assumptions are explicit. The previous
`parseFrontmatter<Record<string, string>>` annotation asserted a shape
the YAML parser never guarantees, which is what hid the mismatch from
tsc while `AgentConfig.tools` was already typed `string[]`.
* fix(typebox): migrate to v1 with extension compat
Replace AJV-based validation with TypeBox-native validation, keep legacy extension imports working (including @sinclair/typebox/compiler), and restore coercion for serialized/plain JSON schemas.
This change closes#3112.
* fix(typebox): use canonical imports and harden coercion
Switch first-party code to canonical typebox imports while retaining legacy extension aliases in the loader.
Remove obsolete runtime codegen guards, expand serialized JSON-schema coercion coverage, and update related tests and fixtures.
Fixes#3112.
---------
Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
The presence of `process.argv[1]` in the current invocation does not
imply that the current `pi` process is being invoked as a script.
Specifically, in the case of a bun compiled binary, `process.argv[1]`
will be the virtual path `/$bunfs/root/pi`, which is not a real file on
the filesystem. This causes the subagent extension to inject this path
into the child `pi` invocation, which is interpreted as part of the
prompt, which confuses the subagent.
This change uses the script only if it is not a bunfs virtual script
Co-authored-by: nathyong <nathyong@noreply.github.com>
The invalid-params error paths already listed available agents,
but the unknown-agent path in runSingleAgent just returned
'Unknown agent: claude' with no hint what exists.
Now: 'Unknown agent: "claude". Available agents: "worker".'
Model would guess names like "claude", "default", or skill names
like "brave-search". Now it self-corrects on the next call.
Tested with Opus 4.6: without fix, model gave up on subagent
and ran the tool calls itself instead. With fix, 1 wasted call.
BREAKING CHANGE: ToolDefinition.execute parameter order changed from
(id, params, onUpdate, ctx, signal) to (id, params, signal, onUpdate, ctx).
This aligns with AgentTool.execute so wrapping built-in tools no longer
requires parameter reordering. Update extensions by swapping signal and
onUpdate parameters.
Breaking changes:
- Settings: 'hooks' and 'customTools' arrays replaced with 'extensions'
- CLI: '--hook' and '--tool' flags replaced with '--extension' / '-e'
- API: HookMessage renamed to CustomMessage, role 'hookMessage' to 'custom'
- API: FileSlashCommand renamed to PromptTemplate
- API: discoverSlashCommands() renamed to discoverPromptTemplates()
- Directories: commands/ renamed to prompts/ for prompt templates
Migration:
- Session version bumped to 3 (auto-migrates v2 sessions)
- Old 'hookMessage' role entries converted to 'custom'
Structural changes:
- src/core/hooks/ and src/core/custom-tools/ merged into src/core/extensions/
- src/core/slash-commands.ts renamed to src/core/prompt-templates.ts
- examples/hooks/ and examples/custom-tools/ merged into examples/extensions/
- docs/hooks.md and docs/custom-tools.md merged into docs/extensions.md
New test coverage:
- test/extensions-runner.test.ts (10 tests)
- test/extensions-discovery.test.ts (26 tests)
- test/prompt-templates.test.ts