pi-subagents 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +46 -2
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +29 -1
- package/src/agents/agent-management.ts +14 -0
- package/src/agents/agent-serializer.ts +10 -0
- package/src/agents/agents.ts +41 -1
- package/src/extension/fanout-child.ts +1 -0
- package/src/extension/index.ts +4 -2
- package/src/extension/schemas.ts +4 -11
- package/src/intercom/result-intercom.ts +5 -0
- package/src/runs/background/async-execution.ts +4 -0
- package/src/runs/background/subagent-runner.ts +65 -8
- package/src/runs/foreground/chain-execution.ts +45 -1
- package/src/runs/foreground/execution.ts +171 -10
- package/src/runs/foreground/subagent-executor.ts +59 -3
- package/src/runs/shared/acceptance-contract.ts +27 -0
- package/src/runs/shared/acceptance-finalization.ts +12 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/workflow-graph.ts +6 -2
- package/src/shared/types.ts +16 -2
- package/src/shared/utils.ts +7 -0
- package/src/tui/render.ts +18 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.28.0] - 2026-06-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added foreground-only `timeoutMs`/`maxRuntimeMs` for single, parallel, and chain subagent runs. Timed-out children are soft-interrupted, keep completed sibling/prior results, and return `timedOut: true` with a stable timeout message.
|
|
9
|
+
- Added per-agent `maxExecutionTimeMs` and `maxTokens` resource limits. Foreground and async children stop with a clear `resourceLimitExceeded` result when the configured runtime or observed token budget is reached.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Strengthened tool and skill guidance so writer subagents launched from plans, specs, issues, or broad fixes proactively use structured `acceptance` instead of burying validation requirements only in task prose.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Removed a provider-unfriendly required-only subschema from the public `acceptance` tool schema so Kimi models served through OpenCode Go can load the `subagent` tool, while keeping runtime validation for empty acceptance contracts.
|
|
16
|
+
- Clarified acceptance-report prompts so required evidence like `diff-summary` must be copied into structured JSON fields such as `diffSummary`, not only described in visible prose.
|
|
17
|
+
|
|
5
18
|
## [0.27.0] - 2026-05-30
|
|
6
19
|
|
|
7
20
|
### Changed
|
package/README.md
CHANGED
|
@@ -145,7 +145,7 @@ Use `~/.pi/agent/settings.json` for a user override or `.pi/settings.json` for a
|
|
|
145
145
|
|
|
146
146
|
## Where running subagents show up
|
|
147
147
|
|
|
148
|
-
Foreground runs stream progress in the conversation while they run.
|
|
148
|
+
Foreground runs stream progress in the conversation while they run. Use `timeoutMs` or its alias `maxRuntimeMs` when a foreground run must return within a wall-clock budget. When the timeout expires, running children are soft-interrupted, completed children stay in the result, and timed-out children return `timedOut: true` with a stable timeout message.
|
|
149
149
|
|
|
150
150
|
Background runs keep working after control returns to you. Inspect active runs with `subagent({ action: "status" })`, or a specific run with `subagent({ action: "status", id: "..." })`.
|
|
151
151
|
|
|
@@ -436,6 +436,8 @@ defaultProgress: true
|
|
|
436
436
|
completionGuard: false
|
|
437
437
|
interactive: true
|
|
438
438
|
maxSubagentDepth: 1
|
|
439
|
+
maxExecutionTimeMs: 600000
|
|
440
|
+
maxTokens: 50000
|
|
439
441
|
---
|
|
440
442
|
|
|
441
443
|
Your system prompt goes here.
|
|
@@ -462,6 +464,8 @@ Important fields:
|
|
|
462
464
|
| `completionGuard` | Set `false` only for non-implementation agents that may mention implementation words while using mutation-capable tools such as `bash`. |
|
|
463
465
|
| `interactive` | Parsed for compatibility but not enforced in v1. |
|
|
464
466
|
| `maxSubagentDepth` | Tightens nested delegation for this agent’s children. |
|
|
467
|
+
| `maxExecutionTimeMs` | Stops each foreground or async child run for this agent after the given number of milliseconds. |
|
|
468
|
+
| `maxTokens` | Stops each foreground or async child run for this agent when observed input plus output tokens reach the limit. Token enforcement is best-effort because usage is reported after model events arrive. |
|
|
465
469
|
|
|
466
470
|
### Tool and extension selection
|
|
467
471
|
|
|
@@ -793,6 +797,7 @@ Agent definitions are not loaded into context by default. Management actions let
|
|
|
793
797
|
| `model` | string | agent default | Override model. |
|
|
794
798
|
| `tasks` | array | - | Top-level parallel tasks. Supports `agent`, `task`, `cwd`, `count`, `output`, `outputMode`, `reads`, `progress`, `skill`, `model`, and `acceptance`. |
|
|
795
799
|
| `concurrency` | number | config or `4` | Top-level parallel concurrency. |
|
|
800
|
+
| `timeoutMs` / `maxRuntimeMs` | number | - | Foreground wall-clock timeout for single, parallel, and chain runs. Timed-out children return `timedOut: true`; async/background runs reject it. |
|
|
796
801
|
| `worktree` | boolean | false | Create isolated git worktrees for parallel tasks. |
|
|
797
802
|
| `chain` | array | - | Sequential, static parallel, and dynamic fanout chain steps. Sequential steps and parallel child tasks support `phase`, `label`, `as`, `outputSchema`, and `acceptance` in addition to the usual execution fields. Dynamic fanout uses `expand`, one child `parallel` template, and `collect`; group-level acceptance is not supported because there is no child session to finalize. |
|
|
798
803
|
| `context` | `fresh \| fork` | agent default or `fresh` | `fork` creates real branched sessions from the parent leaf. Packaged `planner`, `worker`, and `oracle` default to `fork`. |
|
|
@@ -911,6 +916,19 @@ Session directory precedence is: `params.sessionDir`, then `config.defaultSessio
|
|
|
911
916
|
|
|
912
917
|
Controls nested delegation when no inherited `PI_SUBAGENT_MAX_DEPTH` is already in effect. Per-agent `maxSubagentDepth` can tighten the limit for that agent’s child runs, but cannot relax an inherited stricter limit. This applies even to children that explicitly declare `tools: subagent`; at the cap, execution fanout is blocked instead of silently hiding nested work.
|
|
913
918
|
|
|
919
|
+
### Agent resource limits
|
|
920
|
+
|
|
921
|
+
Set `maxExecutionTimeMs` and `maxTokens` in agent frontmatter or through `subagent({ action: "create" | "update", config })` to bound a specific agent across foreground and async runs.
|
|
922
|
+
|
|
923
|
+
```yaml
|
|
924
|
+
maxExecutionTimeMs: 600000
|
|
925
|
+
maxTokens: 50000
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
When a limit is reached, the child receives a soft interrupt, the run fails with a clear `Resource limit exceeded...` error, and the result includes `resourceLimitExceeded` with the limit kind, configured limit, and observed token count when available. Resource-limit failures do not trigger fallback model retries. `maxTokens` is best-effort because providers report usage after message events; a child may exceed the exact limit before the runtime can stop it.
|
|
929
|
+
|
|
930
|
+
Spawn-count and per-agent child-concurrency quotas are not part of this release; use `maxSubagentDepth` and parallel `concurrency` for those boundaries today.
|
|
931
|
+
|
|
914
932
|
### `intercomBridge`
|
|
915
933
|
|
|
916
934
|
```json
|
|
@@ -969,7 +987,7 @@ Debug artifacts live under `{sessionDir}/subagent-artifacts/` or a user-scoped t
|
|
|
969
987
|
- `{runId}_{agent}.jsonl`
|
|
970
988
|
- `{runId}_{agent}_meta.json`
|
|
971
989
|
|
|
972
|
-
Metadata records timing, usage, exit code, final model, attempted models,
|
|
990
|
+
Metadata records timing, usage, exit code, final model, attempted models, fallback attempt outcomes, and any resource-limit termination reason.
|
|
973
991
|
|
|
974
992
|
Session files are stored under a per-run session directory. With `context: "fork"`, each child starts with `--session <branched-session-file>` produced from the parent’s current leaf. That is a real session fork, not an injected summary.
|
|
975
993
|
|
|
@@ -1018,6 +1036,32 @@ Public acceptance config is evidence-driven. There is no public `level` field an
|
|
|
1018
1036
|
|
|
1019
1037
|
Self-review finalization never counts as `reviewed`, and it never counts as `verified` unless configured runtime verification commands actually pass. The visible child output remains the initial answer; finalization reports and residual risks are stored in the acceptance ledger and async/status details.
|
|
1020
1038
|
|
|
1039
|
+
When delegating implementation from a plan or spec, keep the task focused on what to implement and put the definition of done in `acceptance` so the runtime can finalize and evaluate it:
|
|
1040
|
+
|
|
1041
|
+
```ts
|
|
1042
|
+
subagent({
|
|
1043
|
+
agent: "worker",
|
|
1044
|
+
async: true,
|
|
1045
|
+
task: "Implement the plan at /Users/me/docs/mcp-alignment-plan.md. Use scout artifacts in ./handoff/ as context. Do not commit the scout artifacts.",
|
|
1046
|
+
acceptance: {
|
|
1047
|
+
criteria: [
|
|
1048
|
+
"Implementation follows /Users/me/docs/mcp-alignment-plan.md",
|
|
1049
|
+
"Plan acceptance checks are addressed",
|
|
1050
|
+
"Scout handoff artifacts are not committed",
|
|
1051
|
+
"Focused validation for changed behavior passes",
|
|
1052
|
+
"Residual risks or skipped checks are reported"
|
|
1053
|
+
],
|
|
1054
|
+
evidence: ["changed-files", "commands-run", "validation-output", "residual-risks"],
|
|
1055
|
+
verify: [{ id: "focused", command: "npm test -- --runInBand" }],
|
|
1056
|
+
stopRules: [
|
|
1057
|
+
"Do not edit unrelated files",
|
|
1058
|
+
"Stop and report if the plan requires an unapproved product decision"
|
|
1059
|
+
],
|
|
1060
|
+
maxFinalizationTurns: 3
|
|
1061
|
+
}
|
|
1062
|
+
})
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1021
1065
|
## Live progress
|
|
1022
1066
|
|
|
1023
1067
|
Foreground runs show compact live progress for single, chain, and parallel modes: current tool, recent output, token counts, duration, activity freshness, current-tool duration, and chain graph metadata when available.
|
package/package.json
CHANGED
|
@@ -134,7 +134,7 @@ subagent({
|
|
|
134
134
|
{ agent: "reviewer", phase: "Planning", label: "Scheduler contract", as: "schedulerPlan", task: "Plan fixes for scheduler contract. Inspect the current diff. Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "plans/scheduler.md", outputMode: "file-only" },
|
|
135
135
|
{ agent: "reviewer", phase: "Planning", label: "Sandbox/security", as: "sandboxPlan", task: "Plan fixes for sandbox/security. Inspect the current diff. Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "plans/sandbox.md", outputMode: "file-only" }
|
|
136
136
|
], concurrency: 3 },
|
|
137
|
-
{ agent: "worker", phase: "Implementation", label: "Apply accepted fixes", as: "workerResult", task: "Apply only the accepted fixes from these planning summaries. You are the sole writer for the active worktree
|
|
137
|
+
{ agent: "worker", phase: "Implementation", label: "Apply accepted fixes", as: "workerResult", task: "Apply only the accepted fixes from these planning summaries. You are the sole writer for the active worktree.\n\nDeploy plan:\n{outputs.deployPlan}\n\nScheduler plan:\n{outputs.schedulerPlan}\n\nSandbox plan:\n{outputs.sandboxPlan}", acceptance: { criteria: ["Accepted fixes from each planning summary are applied", "Focused validation for changed behavior passes", "Changed files, validation commands, failures, and residual risks are reported"], evidence: ["changed-files", "commands-run", "validation-output", "residual-risks"], stopRules: ["Do not expand product scope beyond accepted fixes", "Stop and report if a fix requires an unapproved decision"], maxFinalizationTurns: 3 }, output: "worker/fixes.md", outputMode: "file-only", progress: true },
|
|
138
138
|
{ parallel: [
|
|
139
139
|
{ agent: "reviewer", phase: "Validation", label: "Deploy/scheduler validation", task: "Validate the post-worker diff for deploy and scheduler fixes. Start from the worker result: {outputs.workerResult}. Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "validation/deploy-scheduler.md", outputMode: "file-only" },
|
|
140
140
|
{ agent: "reviewer", phase: "Validation", label: "Sandbox validation", task: "Validate the post-worker diff for sandbox/security fixes. Start from the worker result: {outputs.workerResult}. Do not modify project/source files; returning findings via the configured output artifact is allowed.", output: "validation/sandbox.md", outputMode: "file-only" }
|
|
@@ -696,6 +696,34 @@ Use the structured `acceptance` field when the run should carry an explicit acce
|
|
|
696
696
|
|
|
697
697
|
Goal-style requests map to `acceptance`. If the user says `/goal`, “goal”, “active goal”, “continue until evidence says done”, or “verify against a goal” for a subagent run, create an explicit run-scoped acceptance contract: `criteria` for the target, `evidence` and `verify` for proof, `stopRules` for constraints, and `maxFinalizationTurns` for the bounded loop budget.
|
|
698
698
|
|
|
699
|
+
When launching a writer/worker from a plan, PRD, spec, issue, or broad fix, set structured `acceptance` proactively. Put implementation instructions, plan paths, and handoff artifacts in `task`; put the definition of done in `acceptance.criteria`, proof requirements in `acceptance.evidence` and `acceptance.verify`, constraints in `acceptance.stopRules`, and usually set `maxFinalizationTurns: 3`. Do not bury all validation requirements only in the task prompt.
|
|
700
|
+
|
|
701
|
+
Example writer handoff:
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
subagent({
|
|
705
|
+
agent: "worker",
|
|
706
|
+
async: true,
|
|
707
|
+
task: "Implement the plan at /Users/me/docs/mcp-alignment-plan.md. Use scout artifacts in ./handoff/ as context. Do not commit the scout artifacts.",
|
|
708
|
+
acceptance: {
|
|
709
|
+
criteria: [
|
|
710
|
+
"Implementation follows /Users/me/docs/mcp-alignment-plan.md",
|
|
711
|
+
"Plan acceptance checks are addressed",
|
|
712
|
+
"Scout handoff artifacts are not committed",
|
|
713
|
+
"Focused validation for changed behavior passes",
|
|
714
|
+
"Residual risks or skipped checks are reported"
|
|
715
|
+
],
|
|
716
|
+
evidence: ["changed-files", "commands-run", "validation-output", "residual-risks"],
|
|
717
|
+
verify: [{ id: "focused", command: "npm test -- --runInBand" }],
|
|
718
|
+
stopRules: [
|
|
719
|
+
"Do not edit unrelated files",
|
|
720
|
+
"Stop and report if the plan requires an unapproved product decision"
|
|
721
|
+
],
|
|
722
|
+
maxFinalizationTurns: 3
|
|
723
|
+
}
|
|
724
|
+
})
|
|
725
|
+
```
|
|
726
|
+
|
|
699
727
|
The first `worker` implements the approved plan. The parent continues with independent inspection or validation prep while it runs, not parallel edits to the same worktree. When the async worker completes, treat its handoff as the transition into review, not as final completion, unless the user explicitly asked for worker-only work, review-only output, or to stop after implementation. Parallel reviewers inspect the resulting diff from fresh context. Validators check behavior with the best available evidence: commands, tests, browser/CLI interaction, screenshots, logs, or manual reproduction notes. The final `worker` applies synthesized review fixes in forked context, then the parent looks over the final diff before completing. The parent may launch these steps as an initial async chain when the workflow is already clear, or as follow-up subagent runs after each async completion. Initial chains should pass `async: true` so the main chat is unblocked; avoid `clarify: true` unless the user asked for foreground clarification. Do not stop after parallel review unless the user explicitly asked for review-only output or the review surfaced a decision that needs approval first.
|
|
700
728
|
|
|
701
729
|
For complex work, risky changes, broad refactors, or many changed lines, increase review and validation fanout rather than trusting one reviewer. Use distinct angles such as correctness/regressions, tests/validation, simplicity/maintainability, security/privacy, performance, docs/API contracts, and user-flow behavior. When reviewers find non-trivial issues or the fix worker touches many lines, run another focused review round before final validation.
|
|
@@ -313,6 +313,18 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
|
|
|
313
313
|
target.maxSubagentDepth = cfg.maxSubagentDepth;
|
|
314
314
|
} else return "config.maxSubagentDepth must be an integer >= 0 or false when provided.";
|
|
315
315
|
}
|
|
316
|
+
if (hasKey(cfg, "maxExecutionTimeMs")) {
|
|
317
|
+
if (cfg.maxExecutionTimeMs === false || cfg.maxExecutionTimeMs === "") target.maxExecutionTimeMs = undefined;
|
|
318
|
+
else if (typeof cfg.maxExecutionTimeMs === "number" && Number.isInteger(cfg.maxExecutionTimeMs) && cfg.maxExecutionTimeMs >= 1) {
|
|
319
|
+
target.maxExecutionTimeMs = cfg.maxExecutionTimeMs;
|
|
320
|
+
} else return "config.maxExecutionTimeMs must be an integer >= 1 or false when provided.";
|
|
321
|
+
}
|
|
322
|
+
if (hasKey(cfg, "maxTokens")) {
|
|
323
|
+
if (cfg.maxTokens === false || cfg.maxTokens === "") target.maxTokens = undefined;
|
|
324
|
+
else if (typeof cfg.maxTokens === "number" && Number.isInteger(cfg.maxTokens) && cfg.maxTokens >= 1) {
|
|
325
|
+
target.maxTokens = cfg.maxTokens;
|
|
326
|
+
} else return "config.maxTokens must be an integer >= 1 or false when provided.";
|
|
327
|
+
}
|
|
316
328
|
if (hasKey(cfg, "completionGuard")) {
|
|
317
329
|
if (typeof cfg.completionGuard !== "boolean") return "config.completionGuard must be a boolean when provided.";
|
|
318
330
|
target.completionGuard = cfg.completionGuard;
|
|
@@ -386,6 +398,8 @@ function formatAgentDetail(agent: AgentConfig): string {
|
|
|
386
398
|
if (agent.defaultReads?.length) lines.push(`Reads: ${agent.defaultReads.join(", ")}`);
|
|
387
399
|
if (agent.defaultProgress) lines.push("Progress: true");
|
|
388
400
|
if (agent.maxSubagentDepth !== undefined) lines.push(`Max subagent depth: ${agent.maxSubagentDepth}`);
|
|
401
|
+
if (agent.maxExecutionTimeMs !== undefined) lines.push(`Max execution time: ${agent.maxExecutionTimeMs}ms`);
|
|
402
|
+
if (agent.maxTokens !== undefined) lines.push(`Max tokens: ${agent.maxTokens}`);
|
|
389
403
|
if (agent.completionGuard === false) lines.push("Completion guard: false");
|
|
390
404
|
if (agent.systemPrompt.trim()) lines.push("", "System Prompt:", agent.systemPrompt);
|
|
391
405
|
return lines.join("\n");
|
|
@@ -21,6 +21,8 @@ export const KNOWN_FIELDS = new Set([
|
|
|
21
21
|
"defaultProgress",
|
|
22
22
|
"interactive",
|
|
23
23
|
"maxSubagentDepth",
|
|
24
|
+
"maxExecutionTimeMs",
|
|
25
|
+
"maxTokens",
|
|
24
26
|
"completionGuard",
|
|
25
27
|
]);
|
|
26
28
|
|
|
@@ -71,6 +73,14 @@ export function serializeAgent(config: AgentConfig): string {
|
|
|
71
73
|
if (typeof maxSubagentDepth === "number" && Number.isInteger(maxSubagentDepth) && maxSubagentDepth >= 0) {
|
|
72
74
|
lines.push(`maxSubagentDepth: ${maxSubagentDepth}`);
|
|
73
75
|
}
|
|
76
|
+
const maxExecutionTimeMs = config.maxExecutionTimeMs;
|
|
77
|
+
if (typeof maxExecutionTimeMs === "number" && Number.isInteger(maxExecutionTimeMs) && maxExecutionTimeMs >= 1) {
|
|
78
|
+
lines.push(`maxExecutionTimeMs: ${maxExecutionTimeMs}`);
|
|
79
|
+
}
|
|
80
|
+
const maxTokens = config.maxTokens;
|
|
81
|
+
if (typeof maxTokens === "number" && Number.isInteger(maxTokens) && maxTokens >= 1) {
|
|
82
|
+
lines.push(`maxTokens: ${maxTokens}`);
|
|
83
|
+
}
|
|
74
84
|
if (config.completionGuard === false) lines.push("completionGuard: false");
|
|
75
85
|
|
|
76
86
|
if (config.extraFields) {
|
package/src/agents/agents.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface BuiltinAgentOverrideBase {
|
|
|
46
46
|
skills?: string[];
|
|
47
47
|
tools?: string[];
|
|
48
48
|
mcpDirectTools?: string[];
|
|
49
|
+
maxExecutionTimeMs?: number;
|
|
50
|
+
maxTokens?: number;
|
|
49
51
|
completionGuard?: boolean;
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -61,6 +63,8 @@ interface BuiltinAgentOverrideConfig {
|
|
|
61
63
|
systemPrompt?: string;
|
|
62
64
|
skills?: string[] | false;
|
|
63
65
|
tools?: string[] | false;
|
|
66
|
+
maxExecutionTimeMs?: number | false;
|
|
67
|
+
maxTokens?: number | false;
|
|
64
68
|
completionGuard?: boolean;
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -94,6 +98,8 @@ export interface AgentConfig {
|
|
|
94
98
|
defaultProgress?: boolean;
|
|
95
99
|
interactive?: boolean;
|
|
96
100
|
maxSubagentDepth?: number;
|
|
101
|
+
maxExecutionTimeMs?: number;
|
|
102
|
+
maxTokens?: number;
|
|
97
103
|
completionGuard?: boolean;
|
|
98
104
|
disabled?: boolean;
|
|
99
105
|
extraFields?: Record<string, string>;
|
|
@@ -203,6 +209,8 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
|
203
209
|
skills: agent.skills ? [...agent.skills] : undefined,
|
|
204
210
|
tools: agent.tools ? [...agent.tools] : undefined,
|
|
205
211
|
mcpDirectTools: agent.mcpDirectTools ? [...agent.mcpDirectTools] : undefined,
|
|
212
|
+
maxExecutionTimeMs: agent.maxExecutionTimeMs,
|
|
213
|
+
maxTokens: agent.maxTokens,
|
|
206
214
|
completionGuard: agent.completionGuard,
|
|
207
215
|
};
|
|
208
216
|
}
|
|
@@ -222,6 +230,8 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
|
|
|
222
230
|
...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
|
|
223
231
|
...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
|
|
224
232
|
...(override.tools !== undefined ? { tools: override.tools === false ? false : [...override.tools] } : {}),
|
|
233
|
+
...(override.maxExecutionTimeMs !== undefined ? { maxExecutionTimeMs: override.maxExecutionTimeMs } : {}),
|
|
234
|
+
...(override.maxTokens !== undefined ? { maxTokens: override.maxTokens } : {}),
|
|
225
235
|
...(override.completionGuard !== undefined ? { completionGuard: override.completionGuard } : {}),
|
|
226
236
|
};
|
|
227
237
|
}
|
|
@@ -359,6 +369,22 @@ function parseBuiltinOverrideEntry(
|
|
|
359
369
|
}
|
|
360
370
|
}
|
|
361
371
|
|
|
372
|
+
if ("maxExecutionTimeMs" in input) {
|
|
373
|
+
if (input.maxExecutionTimeMs === false || (typeof input.maxExecutionTimeMs === "number" && Number.isInteger(input.maxExecutionTimeMs) && input.maxExecutionTimeMs >= 1)) {
|
|
374
|
+
override.maxExecutionTimeMs = input.maxExecutionTimeMs;
|
|
375
|
+
} else {
|
|
376
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'maxExecutionTimeMs'; expected an integer >= 1 or false.`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if ("maxTokens" in input) {
|
|
381
|
+
if (input.maxTokens === false || (typeof input.maxTokens === "number" && Number.isInteger(input.maxTokens) && input.maxTokens >= 1)) {
|
|
382
|
+
override.maxTokens = input.maxTokens;
|
|
383
|
+
} else {
|
|
384
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'maxTokens'; expected an integer >= 1 or false.`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
362
388
|
if ("completionGuard" in input) {
|
|
363
389
|
if (typeof input.completionGuard === "boolean") {
|
|
364
390
|
override.completionGuard = input.completionGuard;
|
|
@@ -439,6 +465,8 @@ function applyBuiltinOverride(
|
|
|
439
465
|
next.tools = tools;
|
|
440
466
|
next.mcpDirectTools = mcpDirectTools;
|
|
441
467
|
}
|
|
468
|
+
if (override.maxExecutionTimeMs !== undefined) next.maxExecutionTimeMs = override.maxExecutionTimeMs === false ? undefined : override.maxExecutionTimeMs;
|
|
469
|
+
if (override.maxTokens !== undefined) next.maxTokens = override.maxTokens === false ? undefined : override.maxTokens;
|
|
442
470
|
if (override.completionGuard !== undefined) next.completionGuard = override.completionGuard;
|
|
443
471
|
|
|
444
472
|
return next;
|
|
@@ -479,7 +507,7 @@ function applyBuiltinOverrides(
|
|
|
479
507
|
|
|
480
508
|
export function buildBuiltinOverrideConfig(
|
|
481
509
|
base: BuiltinAgentOverrideBase,
|
|
482
|
-
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "completionGuard">,
|
|
510
|
+
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "maxExecutionTimeMs" | "maxTokens" | "completionGuard">,
|
|
483
511
|
): BuiltinAgentOverrideConfig | undefined {
|
|
484
512
|
const override: BuiltinAgentOverrideConfig = {};
|
|
485
513
|
|
|
@@ -497,6 +525,8 @@ export function buildBuiltinOverrideConfig(
|
|
|
497
525
|
const baseTools = joinToolList(base);
|
|
498
526
|
const draftTools = joinToolList(draft);
|
|
499
527
|
if (!arraysEqual(draftTools, baseTools)) override.tools = draftTools ? [...draftTools] : false;
|
|
528
|
+
if (draft.maxExecutionTimeMs !== base.maxExecutionTimeMs) override.maxExecutionTimeMs = draft.maxExecutionTimeMs ?? false;
|
|
529
|
+
if (draft.maxTokens !== base.maxTokens) override.maxTokens = draft.maxTokens ?? false;
|
|
500
530
|
if ((draft.completionGuard !== false) !== (base.completionGuard !== false)) {
|
|
501
531
|
override.completionGuard = draft.completionGuard !== false;
|
|
502
532
|
}
|
|
@@ -665,6 +695,8 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
665
695
|
}
|
|
666
696
|
|
|
667
697
|
const parsedMaxSubagentDepth = Number(frontmatter.maxSubagentDepth);
|
|
698
|
+
const parsedMaxExecutionTimeMs = Number(frontmatter.maxExecutionTimeMs);
|
|
699
|
+
const parsedMaxTokens = Number(frontmatter.maxTokens);
|
|
668
700
|
const completionGuard = frontmatter.completionGuard === "false"
|
|
669
701
|
? false
|
|
670
702
|
: frontmatter.completionGuard === "true"
|
|
@@ -698,6 +730,14 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
698
730
|
Number.isInteger(parsedMaxSubagentDepth) && parsedMaxSubagentDepth >= 0
|
|
699
731
|
? parsedMaxSubagentDepth
|
|
700
732
|
: undefined,
|
|
733
|
+
maxExecutionTimeMs:
|
|
734
|
+
Number.isInteger(parsedMaxExecutionTimeMs) && parsedMaxExecutionTimeMs >= 1
|
|
735
|
+
? parsedMaxExecutionTimeMs
|
|
736
|
+
: undefined,
|
|
737
|
+
maxTokens:
|
|
738
|
+
Number.isInteger(parsedMaxTokens) && parsedMaxTokens >= 1
|
|
739
|
+
? parsedMaxTokens
|
|
740
|
+
: undefined,
|
|
701
741
|
completionGuard,
|
|
702
742
|
extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
|
|
703
743
|
});
|
|
@@ -157,6 +157,7 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
|
|
|
157
157
|
description: [
|
|
158
158
|
"Delegate to subagents from child-safe fanout mode.",
|
|
159
159
|
"For goal-style requests such as /goal, goal, active goal, or work until evidence says done, use explicit acceptance on the delegated run: criteria for the target, evidence/verify for proof, stopRules for constraints, and maxFinalizationTurns for the bounded loop.",
|
|
160
|
+
"For implementation handoffs from a plan, PRD, spec, issue, or broad fix, put implementation instructions and plan paths in task, and put the definition of done, evidence, verification commands, constraints, and loop cap in acceptance.",
|
|
160
161
|
"Allowed management/control actions: list, get, status, interrupt, resume, doctor.",
|
|
161
162
|
"Agent config mutation actions create, update, and delete are blocked in this mode.",
|
|
162
163
|
].join("\n"),
|
package/src/extension/index.ts
CHANGED
|
@@ -394,8 +394,10 @@ EXECUTION (use exactly ONE mode):
|
|
|
394
394
|
• SINGLE: { agent, task? } - one task; omit task for self-contained agents
|
|
395
395
|
• CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
|
|
396
396
|
• PARALLEL: { tasks: [{agent,task,count?,output?,reads?,progress?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
|
|
397
|
+
• Foreground timeout: { timeoutMs } or { maxRuntimeMs } - wall-clock limit for foreground single, parallel, and chain runs. Timed-out children return timedOut:true with completed sibling/prior results preserved. Not for async/background runs.
|
|
397
398
|
• Optional context: { context: "fresh" | "fork" } (default: if any requested agent has defaultContext: "fork", the whole invocation uses fork; otherwise "fresh"; inspect agent defaults via { action: "list" })
|
|
398
399
|
• Goal-style requests: when the user says “/goal”, “goal”, “active goal”, “work until evidence says done”, or “verify against a goal”, model that as explicit acceptance. Use acceptance.criteria for the target, acceptance.evidence/verify for proof, acceptance.stopRules for constraints, and acceptance.maxFinalizationTurns for the bounded loop.
|
|
400
|
+
• Plan/spec implementation handoffs: when delegating a plan, PRD, spec, issue, or broad fix to an editing-capable child, prefer structured acceptance instead of burying validation requirements in task prose. Put the implementation instructions and plan paths in task; put the definition of done, evidence, verification commands, constraints, and loop cap in acceptance.
|
|
399
401
|
|
|
400
402
|
CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
401
403
|
• {task} - The original task/request from the user
|
|
@@ -407,8 +409,8 @@ Example: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", t
|
|
|
407
409
|
MANAGEMENT (use action field, omit agent/task/chain/tasks):
|
|
408
410
|
• { action: "list" } - discover executable agents/chains
|
|
409
411
|
• { action: "get", agent: "name" } - full detail; packaged agents use dotted runtime names like "package.agent"
|
|
410
|
-
• { action: "create", config: { name: "custom-agent", package: "code-analysis", systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, ... } }
|
|
411
|
-
• { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis", ... } } - merge
|
|
412
|
+
• { action: "create", config: { name: "custom-agent", package: "code-analysis", systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, maxExecutionTimeMs, maxTokens, ... } }
|
|
413
|
+
• { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis", maxExecutionTimeMs, maxTokens, ... } } - merge
|
|
412
414
|
• { action: "delete", agent: "code-analysis.custom-agent" }
|
|
413
415
|
• Use chainName for chain operations; packaged chains also use dotted runtime names
|
|
414
416
|
|
package/src/extension/schemas.ts
CHANGED
|
@@ -96,16 +96,7 @@ const AcceptanceOverride = Type.Unsafe({
|
|
|
96
96
|
maxFinalizationTurns: { type: "integer", minimum: 1, maximum: 10 },
|
|
97
97
|
},
|
|
98
98
|
additionalProperties: false,
|
|
99
|
-
|
|
100
|
-
anyOf: [
|
|
101
|
-
{ required: ["criteria"] },
|
|
102
|
-
{ required: ["evidence"] },
|
|
103
|
-
{ required: ["verify"] },
|
|
104
|
-
{ required: ["review"] },
|
|
105
|
-
{ required: ["stopRules"] },
|
|
106
|
-
],
|
|
107
|
-
}],
|
|
108
|
-
description: "Optional acceptance contract. Use this for goal-style requests such as /goal, goal, active goal, or work until evidence says done: criteria define the target, evidence/verify define proof, stopRules define constraints, and maxFinalizationTurns defines the bounded loop. When present, the child must complete a same-session self-review/repair loop before acceptance is evaluated.",
|
|
99
|
+
description: "Optional acceptance contract. Use this for goal-style requests and for implementation handoffs from plans, PRDs, specs, issues, or broad fixes. Put implementation instructions and plan paths in task; put the definition of done in criteria, proof in evidence/verify, constraints in stopRules, and the bounded loop budget in maxFinalizationTurns. Runtime validation still requires at least one of criteria, evidence, verify, review, or stopRules. When present, the child must complete a same-session self-review/repair loop before acceptance is evaluated.",
|
|
109
100
|
});
|
|
110
101
|
|
|
111
102
|
const TaskItem = Type.Object({
|
|
@@ -259,10 +250,12 @@ export const SubagentParams = Type.Object({
|
|
|
259
250
|
{ type: "object", additionalProperties: true },
|
|
260
251
|
{ type: "string" },
|
|
261
252
|
],
|
|
262
|
-
description: "Agent or chain config for create/update. Agent: name, package (optional namespace; runtime name becomes package.name), description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, package, description, scope, steps (array of {agent, task?, output?, outputMode?, reads?, model?, skill?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
|
|
253
|
+
description: "Agent or chain config for create/update. Agent: name, package (optional namespace; runtime name becomes package.name), description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth, maxExecutionTimeMs, maxTokens. Chain: name, package, description, scope, steps (array of {agent, task?, output?, outputMode?, reads?, model?, skill?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
|
|
263
254
|
})),
|
|
264
255
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?, outputMode?, reads?, progress?}, ...]" })),
|
|
265
256
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
|
|
257
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Foreground execution wall-clock timeout in milliseconds. When it expires, running children are soft-interrupted and timed-out results are returned. Foreground only; async/background runs ignore this field." })),
|
|
258
|
+
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias for timeoutMs. Use only one unless both values are identical." })),
|
|
266
259
|
worktree: Type.Optional(Type.Boolean({
|
|
267
260
|
description: "Create isolated git worktrees for each parallel task. " +
|
|
268
261
|
"Prevents filesystem conflicts. Requires clean git state. " +
|
|
@@ -20,8 +20,10 @@ export function resolveSubagentResultStatus(input: {
|
|
|
20
20
|
state?: string;
|
|
21
21
|
interrupted?: boolean;
|
|
22
22
|
detached?: boolean;
|
|
23
|
+
timedOut?: boolean;
|
|
23
24
|
}): SubagentResultStatus {
|
|
24
25
|
if (input.detached) return "detached";
|
|
26
|
+
if (input.timedOut || input.state === "timed-out") return "timed-out";
|
|
25
27
|
if (input.interrupted || input.state === "paused") return "paused";
|
|
26
28
|
if (typeof input.success === "boolean") return input.success ? "completed" : "failed";
|
|
27
29
|
if (input.state === "complete") return "completed";
|
|
@@ -36,6 +38,7 @@ function countStatuses(children: SubagentResultIntercomChild[]): Record<Subagent
|
|
|
36
38
|
failed: 0,
|
|
37
39
|
paused: 0,
|
|
38
40
|
detached: 0,
|
|
41
|
+
"timed-out": 0,
|
|
39
42
|
};
|
|
40
43
|
for (const child of children) {
|
|
41
44
|
counts[child.status] += 1;
|
|
@@ -49,6 +52,7 @@ function formatStatusCounts(counts: Record<SubagentResultStatus, number>): strin
|
|
|
49
52
|
counts.failed ? `${counts.failed} failed` : undefined,
|
|
50
53
|
counts.paused ? `${counts.paused} paused` : undefined,
|
|
51
54
|
counts.detached ? `${counts.detached} detached` : undefined,
|
|
55
|
+
counts["timed-out"] ? `${counts["timed-out"]} timed out` : undefined,
|
|
52
56
|
].filter((part): part is string => Boolean(part));
|
|
53
57
|
return parts.length ? parts.join(", ") : "0 results";
|
|
54
58
|
}
|
|
@@ -56,6 +60,7 @@ function formatStatusCounts(counts: Record<SubagentResultStatus, number>): strin
|
|
|
56
60
|
function resolveGroupedStatus(children: SubagentResultIntercomChild[]): SubagentResultStatus {
|
|
57
61
|
const counts = countStatuses(children);
|
|
58
62
|
if (counts.failed > 0) return "failed";
|
|
63
|
+
if (counts["timed-out"] > 0) return "timed-out";
|
|
59
64
|
if (counts.paused > 0) return "paused";
|
|
60
65
|
if (counts.completed > 0) return "completed";
|
|
61
66
|
if (counts.detached > 0) return "detached";
|
|
@@ -370,6 +370,8 @@ export function executeAsyncChain(
|
|
|
370
370
|
outputMode: behavior.outputMode,
|
|
371
371
|
sessionFile,
|
|
372
372
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
|
|
373
|
+
maxExecutionTimeMs: a.maxExecutionTimeMs,
|
|
374
|
+
maxTokens: a.maxTokens,
|
|
373
375
|
effectiveAcceptance: resolveEffectiveAcceptance({
|
|
374
376
|
explicit: s.acceptance,
|
|
375
377
|
agentName: s.agent,
|
|
@@ -689,6 +691,8 @@ export function executeAsyncSingle(
|
|
|
689
691
|
outputMode,
|
|
690
692
|
sessionFile,
|
|
691
693
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
|
|
694
|
+
maxExecutionTimeMs: agentConfig.maxExecutionTimeMs,
|
|
695
|
+
maxTokens: agentConfig.maxTokens,
|
|
692
696
|
effectiveAcceptance: resolveEffectiveAcceptance({
|
|
693
697
|
explicit: params.acceptance,
|
|
694
698
|
agentName: agent,
|