pi-subagents 0.61.0 → 0.62.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 +24 -1
- package/docs/agents.md +6 -2
- package/docs/configuration.md +3 -3
- package/docs/extension-api.md +2 -2
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +2 -2
- package/install.mjs +0 -1
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +3 -4
- package/src/agents/agent-management.ts +19 -0
- package/src/agents/agent-serializer.ts +3 -0
- package/src/agents/agents.ts +16 -2
- package/src/agents/runtime-agent-registry.ts +5 -1
- package/src/api/preflight.ts +4 -0
- package/src/extension/public-execution.ts +1 -0
- package/src/extension/schemas.ts +6 -2
- package/src/extension/tool-description.ts +1 -1
- package/src/runs/background/active-async-capacity.ts +26 -7
- package/src/runs/background/async-execution.ts +47 -16
- package/src/runs/background/async-resume.ts +3 -2
- package/src/runs/background/process-terminal.ts +16 -0
- package/src/runs/background/scheduled-runs.ts +63 -6
- package/src/runs/background/steering.ts +4 -1
- package/src/runs/background/subagent-runner.ts +14 -7
- package/src/runs/background/wait-tool.ts +1 -7
- package/src/runs/foreground/execution.ts +14 -7
- package/src/runs/foreground/subagent-executor.ts +3 -2
- package/src/runs/shared/acceptance.ts +70 -9
- package/src/runs/shared/capability-ceiling.ts +1 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +2 -6
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/pi-args.ts +24 -12
- package/src/runs/shared/pi-spawn.ts +69 -35
- package/src/runs/shared/structured-output.ts +33 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
- package/src/runs/shared/task-intent.ts +17 -6
- package/src/runs/shared/tool-timeout.ts +1 -1
- package/src/shared/atomic-json.ts +3 -1
- package/src/shared/fork-context.ts +0 -12
- package/src/shared/fork-session-cwd.ts +27 -0
- package/src/shared/launch-contract.ts +3 -0
- package/src/shared/types.ts +3 -1
- package/src/slash/slash-commands.ts +1 -1
- package/src/slash/subagents-admin.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
|
|
4
4
|
## [Unreleased]
|
|
5
5
|
|
|
6
|
+
## [0.62.0] - 2026-08-31
|
|
7
|
+
|
|
8
|
+
### Highlights
|
|
9
|
+
- Child agents can report completion evidence more cleanly and stay away from tools they should not use.
|
|
10
|
+
- Session-only schedules keep personal scheduled work tied to the session that created it.
|
|
11
|
+
- Async forked runs now start and resume in the working directory you requested.
|
|
12
|
+
- Windows child launches are more reliable, with clearer errors when Pi cannot find a valid CLI.
|
|
13
|
+
- External CLI and read-only recovery paths are sturdier when workers disappear or prompts include unusual line separators.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Let native children with `outputSchema` include required acceptance evidence in the same `structured_output` call with `acceptance.report: "on"`; `acceptance.report: "off"` keeps fenced acceptance reports. Thanks [@mapleluvr](https://github.com/mapleluvr) for #1770.
|
|
17
|
+
- Add per-agent `excludeTools` deny-lists that compose with Pi's ambient or explicit child tool selection. Thanks [@expoli](https://github.com/expoli) for #1776.
|
|
18
|
+
- Add session-only durable schedules that only run in the session that created them. Thanks [@yangfeng20](https://github.com/yangfeng20) for #1777.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Keep async forked runs in the requested child `cwd` when they start or resume. Thanks [@stekman08](https://github.com/stekman08) for #1785.
|
|
22
|
+
- Accept JSON-encoded acceptance objects from model tool calls, while still failing clearly for malformed strings. Thanks [@mapleluvr](https://github.com/mapleluvr) for #1781.
|
|
23
|
+
- Keep steer and follow-up receipt statuses separate from their redacted message previews (#1773).
|
|
24
|
+
- Create async lifecycle sidecars before external CLI workers begin worktree changes, so disappeared runners are reported as failed runs. Thanks [@fkhawajagh](https://github.com/fkhawajagh) for #1764.
|
|
25
|
+
- Preserve explicit read-only intent when escaped line separators surround no-edit wording. Thanks [@fkhawajagh](https://github.com/fkhawajagh) for #1765.
|
|
26
|
+
- Launch child Pi processes through the resolved CLI JavaScript on Windows, and run JavaScript `PI_SUBAGENT_PI_BINARY` overrides with Node. Thanks [@caohuipeng](https://github.com/caohuipeng) for #1768.
|
|
27
|
+
- Resolve the installed Pi CLI on Windows wrapper hosts from the forwarded package root, and report a clear error when no verified CLI can be found. Thanks [@lux032](https://github.com/lux032) for #1780.
|
|
28
|
+
|
|
6
29
|
## [0.61.0] - 2026-08-31
|
|
7
30
|
|
|
8
31
|
### Highlights
|
|
@@ -15,7 +38,7 @@
|
|
|
15
38
|
### Added
|
|
16
39
|
- Add extension-owned named workflow resources so permission and policy extensions can distinguish trusted workflow resources from raw scripts. Thanks [@mathiasloh](https://github.com/mathiasloh) for #1751.
|
|
17
40
|
- Add workflow-only `globalConcurrencyLimit` and `maxSubagentSpawnsPerRun` overrides for top-level `workflowScript` calls. Thanks [@RapierCraft](https://github.com/RapierCraft) for #1760.
|
|
18
|
-
-
|
|
41
|
+
- Remove the deprecated compatibility wait alias; use `bg_wait` instead (#1729).
|
|
19
42
|
|
|
20
43
|
### Changed
|
|
21
44
|
- Show effective model mappings for discovered and runtime-registered subagents through management and `/subagents-models`. Thanks [@RapierCraft](https://github.com/RapierCraft) for #1732.
|
package/docs/agents.md
CHANGED
|
@@ -255,6 +255,7 @@ package: code-analysis
|
|
|
255
255
|
description: Fast codebase recon
|
|
256
256
|
aliases: explorer, code-scout
|
|
257
257
|
tools: read, grep, find, ls, bash, mcp:chrome-devtools
|
|
258
|
+
excludeTools: bash
|
|
258
259
|
extensions:
|
|
259
260
|
subagentOnlyExtensions: ./tools/child-only-search.ts
|
|
260
261
|
model: claude-haiku-4-5
|
|
@@ -283,7 +284,7 @@ allowNestedSubagents: true
|
|
|
283
284
|
Your system prompt goes here.
|
|
284
285
|
```
|
|
285
286
|
|
|
286
|
-
Simple-scalar list fields accept either a comma-separated form or a newline block list with one `- item` per line. This applies to `tools`, `defaultReads`, `skill`/`skills`, `skillPath`, `fallbackModels`, `extensions`, and `subagentOnlyExtensions`:
|
|
287
|
+
Simple-scalar list fields accept either a comma-separated form or a newline block list with one `- item` per line. This applies to `tools`, `excludeTools`, `defaultReads`, `skill`/`skills`, `skillPath`, `fallbackModels`, `extensions`, and `subagentOnlyExtensions`:
|
|
287
288
|
|
|
288
289
|
```yaml
|
|
289
290
|
tools:
|
|
@@ -301,6 +302,7 @@ Field notes:
|
|
|
301
302
|
| `package` | Optional package identifier. A file with `name: scout` and `package: code-analysis` registers as `code-analysis.scout`; serialization keeps `name` and `package` separate. |
|
|
302
303
|
| `aliases` | Optional comma-separated or block-list names that resolve to this agent for selection and explicit `agent` and task inputs. Runtime status, persistence, and config still use the canonical `name`. Exact canonical names take precedence over aliases, and alias collisions between distinct canonical agents fail as ambiguous. |
|
|
303
304
|
| `tools` | Strict child tool allowlist. Named extension tools must also have their provider loaded. `mcp:` entries select direct MCP tools when `pi-mcp-adapter` is installed. |
|
|
305
|
+
| `excludeTools` | Optional child tool deny-list applied after normal tool resolution. With an explicit `tools` allowlist, matching names are removed; when `tools` is omitted, the names are forwarded to Pi as `--exclude-tools` so the ambient tool set is inherited minus those names. Unknown names are ignored by Pi without making the agent definition invalid. |
|
|
304
306
|
| `allowNestedSubagents` | Set `true` to authorize the child-safe nested `subagent` runtime without making omitted `tools` an allowlist. Inherited depth and capability ceilings remain authoritative. |
|
|
305
307
|
| `extensions` | Omitted means normal extensions; empty means no extensions; list values allowlist specific extensions. |
|
|
306
308
|
| `subagentOnlyExtensions` | Extension paths loaded only in spawned child sessions for this agent. Tools registered there are unavailable to the main agent unless also installed through normal Pi extension configuration. |
|
|
@@ -319,7 +321,7 @@ Field notes:
|
|
|
319
321
|
| `defaultProgress` | Maintain `progress.md`. |
|
|
320
322
|
| `async` | Default a single-agent launch to background (`true`) or foreground (`false`) when the call omits `async`. Explicit call values and `forceTopLevelAsync` win. |
|
|
321
323
|
| `timeoutMs` | Positive integer default runtime deadline in milliseconds for single-agent launches. Foreground launches use 30 minutes when neither the call nor agent provides a timeout; explicit `timeoutMs`/`maxRuntimeMs` and agent defaults win. |
|
|
322
|
-
| `toolTimeoutMs` | Optional positive integer hard per-tool-call deadline in milliseconds. An explicit call value wins, then this agent default, global `toolTimeoutMs`, and `PI_SUBAGENT_TOOL_TIMEOUT_MS`. When omitted, known-fast built-in tools get a five-minute default; long-running tools get attention notices but no hard default. It does not extend the run-level deadline; `contact_supervisor`, `intercom`,
|
|
324
|
+
| `toolTimeoutMs` | Optional positive integer hard per-tool-call deadline in milliseconds. An explicit call value wins, then this agent default, global `toolTimeoutMs`, and `PI_SUBAGENT_TOOL_TIMEOUT_MS`. When omitted, known-fast built-in tools get a five-minute default; long-running tools get attention notices but no hard default. It does not extend the run-level deadline; `contact_supervisor`, `intercom`, and `bg_wait` are exempt. |
|
|
323
325
|
| `acceptance` | Acceptance default for single-agent launches. Use a scalar level such as `checked` or an inline/block YAML map such as `{ level: "none", reason: "lightweight lookup" }`. Explicit call values win; chain and parallel acceptance remains task/step configuration. |
|
|
324
326
|
| `acceptanceRole` | Optional `read-only` or `writer` role for automatic acceptance inference. Explicit task mutation or no-edit intent wins; otherwise the declared role replaces agent-name guessing. This does not grant or revoke tools. |
|
|
325
327
|
| `mutationTools` | Comma-separated extension tool names whose calls count as mutation attempts for the completion guard. This declares evidence only; list and load each tool through `tools` and its extension provider as usual. |
|
|
@@ -385,6 +387,8 @@ How `tools` behaves:
|
|
|
385
387
|
- `tools:` empty: emits `--no-tools`.
|
|
386
388
|
- `allowNestedSubagents: true`: explicitly enables child-safe nested fanout without turning omitted `tools` into an allowlist. Depth and inherited capability ceilings still apply.
|
|
387
389
|
|
|
390
|
+
`excludeTools` is applied after this resolution. It can narrow an explicit `tools` allowlist or, when `tools` is omitted, compose with Pi's ambient builtin tools through `--exclude-tools`. Runtime-injected tools are excluded only when their exact names are listed. An empty `excludeTools` list has no effect.
|
|
391
|
+
|
|
388
392
|
An allowlisted name does not load the extension that registers it. Load that provider through normal Pi extension discovery, `extensions`, `subagentOnlyExtensions`, or a path-like `tools` entry.
|
|
389
393
|
|
|
390
394
|
More rules:
|
package/docs/configuration.md
CHANGED
|
@@ -187,9 +187,9 @@ Controls the under-editor widget for active background runs. It defaults to `tru
|
|
|
187
187
|
{ "waitTool": { "enabled": true, "defaultTimeoutMs": 120000 } }
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
`defaultTimeoutMs` sets the blocking window used when a `bg_wait` call omits `timeoutMs`; explicit call values win, followed by this setting, then the 30-minute fallback.
|
|
190
|
+
`defaultTimeoutMs` sets the blocking window used when a `bg_wait` call omits `timeoutMs`; explicit call values win, followed by this setting, then the 30-minute fallback. `bg_wait` is the only registered wait tool. When the window elapses, the tool returns a non-error `window_elapsed` result with the still-active work identities, and that work keeps running. Set `enabled` to `false` to make direct calls return immediately instead of blocking. The default is enabled. You can also set `"waitTool": false`; set `PI_SUBAGENT_WAIT_TOOL_ENABLED=false` (or `0`, `off`, `disabled`) to override config for one process. The effective enabled and default-timeout values are passed explicitly to child runtimes. Headless `agent_end` auto-drain retains its own strict deadline and fails if required work remains unresolved. Invalid config or environment values fail instead of being coerced.
|
|
191
191
|
|
|
192
|
-
Blocking `bg_wait({ id: "..." })` keeps the current tool call open until that run changes. By default it returns when a run needs attention. Use `bg_wait({ stopOnAttention: false })` only for run-to-completion flows that should wait through idle or long-thinking attention; supervisor/contact requests still stop the wait. In a long-lived interactive parent session, `bg_wait({ id: "...", nonBlocking: true })` instead resolves the prefix once, persists the exact run identity, returns a subscription token immediately, and wakes that session on completion, failure, attention, reconciliation failure, or timeout. Use it for provider, detached, or other background work without a native completion notification; ordinary async subagent runs notify the parent natively and do not need a wait subscription. Armed subscriptions appear in ordinary `subagent({ action: "status" })` output and are not counted as active child work.
|
|
192
|
+
Blocking `bg_wait({ id: "..." })` keeps the current tool call open until that run changes. By default it returns when a run needs attention. Use `bg_wait({ stopOnAttention: false })` only for run-to-completion flows that should wait through idle or long-thinking attention; supervisor/contact requests still stop the wait. In a long-lived interactive parent session, `bg_wait({ id: "...", nonBlocking: true })` instead resolves the prefix once, persists the exact run identity, returns a subscription token immediately, and wakes that session on completion, failure, attention, reconciliation failure, or timeout. Use it for provider, detached, or other background work without a native completion notification; ordinary async subagent runs notify the parent natively and do not need a wait subscription. Armed subscriptions appear in ordinary `subagent({ action: "status" })` output and are not counted as active child work.
|
|
193
193
|
|
|
194
194
|
This is different from `waitTool.enabled=false`, which returns immediately without registering any future wake. Provider items remain available only to blocking fleet-wide waits; non-blocking subscriptions require one async or remembered detached foreground run id.
|
|
195
195
|
|
|
@@ -233,7 +233,7 @@ Optional hard per-tool-call deadline in milliseconds. When configured, a child t
|
|
|
233
233
|
|
|
234
234
|
Without a configured value, Pi still applies a five-minute hard timeout to known-fast built-in tools: `read`, `grep`, `find`, `ls`, `edit`, `write`, and `structured_output`. Long-running tools such as `bash`, custom tools, and MCP tools do not get a hard default. They get the normal open-tool attention notice after `activeNoticeAfterMs` and remain bounded by the run-level deadline.
|
|
235
235
|
|
|
236
|
-
The tool timer tracks each active `toolCallId` separately and never extends the run-level deadline: when the remaining run budget is shorter, the ordinary run-level timeout wins. `contact_supervisor`, `intercom`,
|
|
236
|
+
The tool timer tracks each active `toolCallId` separately and never extends the run-level deadline: when the remaining run budget is shorter, the ordinary run-level timeout wins. `contact_supervisor`, `intercom`, and `bg_wait` are exempt because their legitimate purpose can be to wait for a human, supervisor, or background run. Use hard tool timeouts only for wedge protection; an elapsed timeout is not a mutation-safe boundary. Configured values must be positive integers no greater than `2147483647`; invalid or out-of-range values are rejected with a visible error rather than silently ignored.
|
|
237
237
|
|
|
238
238
|
## `globalConcurrencyLimit`
|
|
239
239
|
|
package/docs/extension-api.md
CHANGED
|
@@ -309,7 +309,7 @@ Semantics:
|
|
|
309
309
|
- Providers share a registry through `Symbol.for("pi-subagents.background-work.v1")`, allowing independently loaded extension modules to meet in one Pi process.
|
|
310
310
|
- Registration is reload-safe: a new provider with the same name replaces the old callback, and the old disposer cannot remove the replacement. Call the disposer during extension shutdown when possible.
|
|
311
311
|
|
|
312
|
-
Child processes do not gain provider tools or extensions automatically. Add `bg_wait` to the child agent's `tools` allowlist
|
|
312
|
+
Child processes do not gain provider tools or extensions automatically. Add `bg_wait` to the child agent's `tools` allowlist and load each provider through `extensions` or `subagentOnlyExtensions`. The parent's effective `waitTool` setting is serialized through foreground, async, resume, chain, parallel, and fanout launch paths; `PI_SUBAGENT_WAIT_TOOL_ENABLED` keeps precedence.
|
|
313
313
|
|
|
314
314
|
## External job provider bridge
|
|
315
315
|
|
|
@@ -406,7 +406,7 @@ The API returns discriminated structured results with canonical project root, bi
|
|
|
406
406
|
|
|
407
407
|
A host that embeds this extension owns whether completion wakes can be delivered at all.
|
|
408
408
|
|
|
409
|
-
Ordinary async and foreground completion wakes use `registerSubagentNotify` and `sendCompletion`. They listen for completion events and deliver through `pi.sendMessage(..., { triggerTurn })`. Session shutdown stops the result watcher and disposes this completion notifier. `createWaitSubscriptionManager` is separate: it is the explicit non-blocking `bg_wait` subscription path for work without native notification, not the ordinary completion wake path.
|
|
409
|
+
Ordinary async and foreground completion wakes use `registerSubagentNotify` and `sendCompletion`. They listen for completion events and deliver through `pi.sendMessage(..., { triggerTurn })`. Session shutdown stops the result watcher and disposes this completion notifier. `createWaitSubscriptionManager` is separate: it is the explicit non-blocking `bg_wait` subscription path for work without native notification, not the ordinary completion wake path.
|
|
410
410
|
|
|
411
411
|
Detached children do not stop when the session does. They are the host process's children, not the session's, so the run keeps going, completes, and notifies nobody. What is lost is the notification, not the work.
|
|
412
412
|
|
package/docs/observability.md
CHANGED
|
@@ -157,7 +157,7 @@ For a top-level async run, `details.asyncDir` points at that directory; the fina
|
|
|
157
157
|
|
|
158
158
|
The result file is consumed and deleted once its completion notice is delivered. Before deletion, the watcher writes a versioned replay record under `<resultsDir>/completion-replay/<runId>.json` and a bounded output archive under `<resultsDir>/output-archives/<runId>.json`. Replay records expire with the completion deduplication window and are best-effort temporary state, not a permanent run ledger.
|
|
159
159
|
|
|
160
|
-
`bg_wait` surfaces a slim projection of each terminal payload it covered in its own tool-result `details.completions` — run identity, per-child agent/`runId`/success, artifact paths, and the bounded `archivePath`, without duplicating output text. It reads the replay when watcher delivery or a watcher restart has removed the one-shot result file and in-memory completion state is unavailable. Durable non-blocking wait subscriptions use the same replay in their delivered details.
|
|
160
|
+
`bg_wait` surfaces a slim projection of each terminal payload it covered in its own tool-result `details.completions` — run identity, per-child agent/`runId`/success, artifact paths, and the bounded `archivePath`, without duplicating output text. It reads the replay when watcher delivery or a watcher restart has removed the one-shot result file and in-memory completion state is unavailable. Durable non-blocking wait subscriptions use the same replay in their delivered details. Workflow result files record each child's `runId` explicitly, since a workflow child's `artifactPaths` entry points at its saved output rather than the artifact files keyed by the id. Extensions observing `tool_result` events can read run and artifact identity from there instead of parsing the text summary.
|
|
161
161
|
|
|
162
162
|
Output archives reference an existing child output artifact or session file when one is available. For children without either file, the archive stores a per-child `result-tail` entry with `resultIndex`, bounded to 64 KiB per child, and records whether it was truncated. Replay and archive JSON use `version: 1`; consumers must ignore unknown fields.
|
|
163
163
|
|
package/docs/tool-reference.md
CHANGED
|
@@ -109,7 +109,7 @@ The complete plain-JSON inventory is validated before the first launch (maximum
|
|
|
109
109
|
| `chatProgress` | `auto \| off \| live-card` | `auto` | WorkflowScript chat projection. `auto` renders a live in-chat card only for watched foreground workflows in the same Git repository, including managed worktrees; it is off otherwise. Explicit `live-card` requires `async:false` and the same Git repository. Async workflows have no inline live card, so omit `chatProgress` or use `auto`/`off`; use `async:false` only when the parent must block. |
|
|
110
110
|
| `isolation` | `none \| worktree` | - | Workflow child isolation. `none` runs in the shared cwd and does not need Git. `worktree` requires a managed Git worktree. Do not combine it with a contradictory `worktree` value. |
|
|
111
111
|
| `timeoutMs` / `maxRuntimeMs` | number | config `timeoutMs`, else 30 min foreground / single-agent async | Optional run-level max runtime in milliseconds. When omitted, the global [`timeoutMs`](configuration.md#timeoutms) config provides the default; absent that, foreground and plain single-agent async runs fall back to 30 minutes, while composite async runs (chains, parallel tasks, workflows) stay unbounded at the top level. Expiration of this run-level deadline is terminal and does not trigger `fallbackModels`. |
|
|
112
|
-
| `toolTimeoutMs` | number | fast-tool default | Optional positive hard per-tool-call deadline in milliseconds. Precedence: call value → agent frontmatter → config → `PI_SUBAGENT_TOOL_TIMEOUT_MS`. The timer starts on `tool_execution_start`, clears on the matching `tool_execution_end`, and terminates the run with `timedOut: true` if the tool remains open. When omitted, known-fast built-in tools get a five-minute default; long-running tools get attention notices but no hard default. It never extends the run deadline; `contact_supervisor`, `intercom`,
|
|
112
|
+
| `toolTimeoutMs` | number | fast-tool default | Optional positive hard per-tool-call deadline in milliseconds. Precedence: call value → agent frontmatter → config → `PI_SUBAGENT_TOOL_TIMEOUT_MS`. The timer starts on `tool_execution_start`, clears on the matching `tool_execution_end`, and terminates the run with `timedOut: true` if the tool remains open. When omitted, known-fast built-in tools get a five-minute default; long-running tools get attention notices but no hard default. It never extends the run deadline; `contact_supervisor`, `intercom`, and `bg_wait` are exempt. |
|
|
113
113
|
| `toolBudget` | object | none | Optional child tool-call budget `{ soft?, hard, block? }`. At `soft` the child is nudged to finalize. After `hard`, configured tools are blocked; `block` defaults to `read`, `grep`, `find`, and `ls`, while `"*"` blocks every tool call. Final assistant text is never blocked. |
|
|
114
114
|
| `usageBudget` | object | none | Optional root-only reported-usage budget `{ tokens?: { soft?, hard }, costUsd?: { soft?, hard } }`. Soft limits are status-only. Hard limits prevent later child launches after reported usage is reconciled; already-running children are not stopped and no reservations are made. |
|
|
115
115
|
| `cwd` | string | runtime cwd | Override working directory. |
|
|
@@ -420,7 +420,7 @@ Acceptance provenance is stored separately from child prose. `evidenceStatus` pr
|
|
|
420
420
|
|
|
421
421
|
### The acceptance report
|
|
422
422
|
|
|
423
|
-
For `attested` or stricter levels, the child prompt includes a standardized acceptance section and asks for a fenced `acceptance-report` JSON block.
|
|
423
|
+
For `attested` or stricter levels, the child prompt includes a standardized acceptance section and asks for a fenced `acceptance-report` JSON block. With `outputSchema`, set `acceptance.report: "on"` to require the same report in the final `structured_output` call, or `"off"` to keep the fenced-report path. Omitting `report` preserves the default behavior. Runs without `outputSchema` never gain a standalone structured-output tool from this option.
|
|
424
424
|
|
|
425
425
|
The parser canonicalizes known enum synonyms, snake_case report keys and wrappers, underscore fence tags, unambiguous scalar arrays, string booleans, and criterion-id separators. Unknown or ambiguous keys and enum values fail with field-level diagnostics. Explicit empty `changedFiles` and `testsAddedOrUpdated` arrays are recorded as not applicable; missing fields and empty required command or validation evidence still fail.
|
|
426
426
|
|
package/install.mjs
CHANGED
|
@@ -88,7 +88,6 @@ console.log(`
|
|
|
88
88
|
The extension is now available in pi. Tools added:
|
|
89
89
|
• subagent - Delegate tasks to agents and inspect run status
|
|
90
90
|
• bg_wait - Wait for background/provider/detached work without native completion notifications
|
|
91
|
-
(subagent_wait remains a deprecated compatibility alias)
|
|
92
91
|
|
|
93
92
|
Documentation: ${EXTENSION_DIR}/README.md
|
|
94
93
|
`);
|
package/package.json
CHANGED
|
@@ -168,13 +168,12 @@ from disabling `waitTool`, which returns immediately without arming a future
|
|
|
168
168
|
wake. If a foreground child detaches for supervisor coordination, reply first,
|
|
169
169
|
then wait on its id; do not resume or launch a replacement while it remains
|
|
170
170
|
detached. Headless sessions also auto-drain exact current-session work at
|
|
171
|
-
`agent_end` as a final safeguard.
|
|
172
|
-
deprecated compatibility alias for `bg_wait`.
|
|
171
|
+
`agent_end` as a final safeguard.
|
|
173
172
|
|
|
174
173
|
Providers are discovered through the `pi-subagents/background-work` registry and
|
|
175
174
|
must expose a stable item id and owning session id. Load a provider through the
|
|
176
|
-
child’s `extensions` or `subagentOnlyExtensions` and allow `bg_wait`
|
|
177
|
-
|
|
175
|
+
child’s `extensions` or `subagentOnlyExtensions` and allow `bg_wait` in its
|
|
176
|
+
tools. For
|
|
178
177
|
non-interactive fleets, launch N workers, wait for the next completion, react,
|
|
179
178
|
and replace as needed; use `all: true` only when intentionally draining the
|
|
180
179
|
fleet. If `PI_SUBAGENT_WAIT_TOOL_ENABLED` disables blocking, direct waits return
|
|
@@ -269,6 +269,7 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
|
|
|
269
269
|
skills: _skills,
|
|
270
270
|
skillPath: _skillPath,
|
|
271
271
|
tools: _tools,
|
|
272
|
+
excludeTools: _excludeTools,
|
|
272
273
|
mcpDirectTools: _mcpDirectTools,
|
|
273
274
|
subagentOnlyExtensions: _subagentOnlyExtensions,
|
|
274
275
|
mutationTools: _mutationTools,
|
|
@@ -301,6 +302,7 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
|
|
|
301
302
|
...(base.skills !== undefined ? { skills: [...base.skills] } : {}),
|
|
302
303
|
...(base.skillPath !== undefined ? { skillPath: [...base.skillPath] } : {}),
|
|
303
304
|
...(base.tools !== undefined ? { tools: [...base.tools] } : {}),
|
|
305
|
+
...(base.excludeTools !== undefined ? { excludeTools: [...base.excludeTools] } : {}),
|
|
304
306
|
...(base.mcpDirectTools !== undefined ? { mcpDirectTools: [...base.mcpDirectTools] } : {}),
|
|
305
307
|
...(base.extensions !== undefined ? { extensions: [...base.extensions] } : {}),
|
|
306
308
|
...(base.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...base.subagentOnlyExtensions] } : {}),
|
|
@@ -333,6 +335,7 @@ export function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<
|
|
|
333
335
|
if (hasKey(cfg, "model")) changed("model");
|
|
334
336
|
if (hasKey(cfg, "fallbackModels")) changed("fallbackModels");
|
|
335
337
|
if (hasKey(cfg, "tools")) changed("tools");
|
|
338
|
+
if (hasKey(cfg, "excludeTools")) changed("excludeTools");
|
|
336
339
|
if (hasKey(cfg, "skills")) changed("skill", "skills");
|
|
337
340
|
if (hasKey(cfg, "skillPath")) changed("skillPath");
|
|
338
341
|
if (hasKey(cfg, "extensions")) changed("extensions");
|
|
@@ -463,6 +466,18 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
|
|
|
463
466
|
else delete target.mcpDirectTools;
|
|
464
467
|
} else return "config.tools must be a comma-separated string or false when provided.";
|
|
465
468
|
}
|
|
469
|
+
if (hasKey(cfg, "excludeTools")) {
|
|
470
|
+
if (cfg.excludeTools === false || cfg.excludeTools === "") delete target.excludeTools;
|
|
471
|
+
else if (typeof cfg.excludeTools === "string") {
|
|
472
|
+
const excludeTools = parseCsv(cfg.excludeTools);
|
|
473
|
+
if (excludeTools.length) target.excludeTools = [...new Set(excludeTools)];
|
|
474
|
+
else delete target.excludeTools;
|
|
475
|
+
} else if (Array.isArray(cfg.excludeTools) && cfg.excludeTools.every((entry) => typeof entry === "string")) {
|
|
476
|
+
const excludeTools = [...new Set(cfg.excludeTools.map((entry) => entry.trim()).filter(Boolean))];
|
|
477
|
+
if (excludeTools.length) target.excludeTools = excludeTools;
|
|
478
|
+
else delete target.excludeTools;
|
|
479
|
+
} else return "config.excludeTools must be a comma-separated string, string array, or false when provided.";
|
|
480
|
+
}
|
|
466
481
|
if (hasKey(cfg, "skills")) {
|
|
467
482
|
if (cfg.skills === false || cfg.skills === "") delete target.skills;
|
|
468
483
|
else if (typeof cfg.skills === "string") {
|
|
@@ -595,6 +610,7 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
|
|
|
595
610
|
if (target.runner?.type === "external-cli" || target.runner?.type === "external-job") {
|
|
596
611
|
const unsupported = [
|
|
597
612
|
target.tools?.length || target.mcpDirectTools?.length ? "tools" : undefined,
|
|
613
|
+
target.excludeTools?.length ? "excludeTools" : undefined,
|
|
598
614
|
target.model ? "model" : undefined,
|
|
599
615
|
target.fallbackModels?.length ? "fallbackModels" : undefined,
|
|
600
616
|
target.thinking ? "thinking" : undefined,
|
|
@@ -701,6 +717,7 @@ function formatAgentCapabilitiesLine(agent: AgentConfig, providerNames: Set<stri
|
|
|
701
717
|
} else if (declaredTools.length > 0) {
|
|
702
718
|
tools = declaredTools.join(", ");
|
|
703
719
|
}
|
|
720
|
+
if (agent.excludeTools?.length) tools = `${tools}; excludes: ${agent.excludeTools.join(", ")}`;
|
|
704
721
|
let model = "inherits current session";
|
|
705
722
|
if (agent.model !== undefined) {
|
|
706
723
|
model = agent.model;
|
|
@@ -728,6 +745,7 @@ function agentCapabilityTools(agent: AgentConfig): AgentCapabilityRow["tools"] {
|
|
|
728
745
|
return {
|
|
729
746
|
ambient: agent.tools === undefined && agent.mcpDirectTools === undefined,
|
|
730
747
|
names: listOrEmpty(agent.tools),
|
|
748
|
+
...(agent.excludeTools !== undefined ? { excludeTools: [...agent.excludeTools] } : {}),
|
|
731
749
|
mcpDirectTools: listOrEmpty(agent.mcpDirectTools),
|
|
732
750
|
mutationTools: agent.mutationTools,
|
|
733
751
|
};
|
|
@@ -846,6 +864,7 @@ function formatAgentDetail(agent: AgentConfig): string {
|
|
|
846
864
|
if (agent.model) lines.push(`Model: ${agent.model}`);
|
|
847
865
|
if (agent.fallbackModels?.length) lines.push(`Fallback models: ${agent.fallbackModels.join(", ")}`);
|
|
848
866
|
if (tools.length) lines.push(`Tools: ${tools.join(", ")}`);
|
|
867
|
+
if (agent.excludeTools?.length) lines.push(`Excluded tools: ${agent.excludeTools.join(", ")}`);
|
|
849
868
|
if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
|
|
850
869
|
if (agent.skillPath?.length) lines.push(`Skill paths: ${agent.skillPath.join(", ")}`);
|
|
851
870
|
lines.push(`System prompt mode: ${agent.systemPromptMode}`);
|
|
@@ -9,6 +9,7 @@ export const KNOWN_FIELDS = new Set([
|
|
|
9
9
|
"alias",
|
|
10
10
|
"aliases",
|
|
11
11
|
"tools",
|
|
12
|
+
"excludeTools",
|
|
12
13
|
"allowNestedSubagents",
|
|
13
14
|
"model",
|
|
14
15
|
"fallbackModels",
|
|
@@ -70,6 +71,8 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
|
|
|
70
71
|
];
|
|
71
72
|
const toolsValue = joinComma(tools);
|
|
72
73
|
if (toolsValue || preserve("tools")) lines.push(`tools: ${toolsValue ?? ""}`);
|
|
74
|
+
const excludeToolsValue = joinComma(config.excludeTools);
|
|
75
|
+
if (excludeToolsValue || preserve("excludeTools")) lines.push(`excludeTools: ${excludeToolsValue ?? ""}`);
|
|
73
76
|
if (config.allowNestedSubagents === true || preserve("allowNestedSubagents")) {
|
|
74
77
|
lines.push(`allowNestedSubagents: ${config.allowNestedSubagents === undefined ? "" : config.allowNestedSubagents ? "true" : "false"}`);
|
|
75
78
|
}
|
package/src/agents/agents.ts
CHANGED
|
@@ -70,6 +70,7 @@ export interface BuiltinAgentOverrideBase {
|
|
|
70
70
|
skills?: string[];
|
|
71
71
|
skillPath?: string[];
|
|
72
72
|
tools?: string[];
|
|
73
|
+
excludeTools?: string[];
|
|
73
74
|
allowNestedSubagents?: boolean;
|
|
74
75
|
mcpDirectTools?: string[];
|
|
75
76
|
extensions?: string[];
|
|
@@ -99,6 +100,7 @@ interface BuiltinAgentOverrideConfig {
|
|
|
99
100
|
systemPrompt?: string;
|
|
100
101
|
skills?: string[] | false;
|
|
101
102
|
tools?: string[] | false | "inherit";
|
|
103
|
+
excludeTools?: string[] | false;
|
|
102
104
|
allowNestedSubagents?: boolean;
|
|
103
105
|
extensions?: string[] | false;
|
|
104
106
|
subagentOnlyExtensions?: string[] | false;
|
|
@@ -132,6 +134,7 @@ export interface AgentConfig {
|
|
|
132
134
|
description: string;
|
|
133
135
|
aliases?: string[];
|
|
134
136
|
tools?: string[];
|
|
137
|
+
excludeTools?: string[];
|
|
135
138
|
allowNestedSubagents?: boolean;
|
|
136
139
|
mcpDirectTools?: string[];
|
|
137
140
|
model?: string;
|
|
@@ -760,6 +763,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
|
760
763
|
...(agent.skills ? { skills: [...agent.skills] } : {}),
|
|
761
764
|
...(agent.skillPath ? { skillPath: [...agent.skillPath] } : {}),
|
|
762
765
|
...(agent.tools ? { tools: [...agent.tools] } : {}),
|
|
766
|
+
...(agent.excludeTools ? { excludeTools: [...agent.excludeTools] } : {}),
|
|
763
767
|
...(agent.allowNestedSubagents !== undefined ? { allowNestedSubagents: agent.allowNestedSubagents } : {}),
|
|
764
768
|
...(agent.mcpDirectTools ? { mcpDirectTools: [...agent.mcpDirectTools] } : {}),
|
|
765
769
|
...(!agent.extensionsFromDefault && agent.extensions ? { extensions: [...agent.extensions] } : {}),
|
|
@@ -793,6 +797,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
|
|
|
793
797
|
...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
|
|
794
798
|
...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
|
|
795
799
|
...(override.tools !== undefined ? { tools: Array.isArray(override.tools) ? [...override.tools] : override.tools } : {}),
|
|
800
|
+
...(override.excludeTools !== undefined ? { excludeTools: override.excludeTools === false ? false : [...override.excludeTools] } : {}),
|
|
796
801
|
...(override.allowNestedSubagents !== undefined ? { allowNestedSubagents: override.allowNestedSubagents } : {}),
|
|
797
802
|
...(override.extensions !== undefined ? { extensions: override.extensions === false ? false : [...override.extensions] } : {}),
|
|
798
803
|
...(override.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: override.subagentOnlyExtensions === false ? false : [...override.subagentOnlyExtensions] } : {}),
|
|
@@ -1085,6 +1090,8 @@ function parseBuiltinOverrideEntry(
|
|
|
1085
1090
|
|
|
1086
1091
|
const tools = parseToolsOverride(input.tools, { filePath, name });
|
|
1087
1092
|
if (tools !== undefined) override.tools = tools;
|
|
1093
|
+
const excludeTools = parseOverrideStringArrayOrFalse(input.excludeTools, { filePath, name, field: "excludeTools" });
|
|
1094
|
+
if (excludeTools !== undefined) override.excludeTools = excludeTools;
|
|
1088
1095
|
if ("allowNestedSubagents" in input) {
|
|
1089
1096
|
if (typeof input.allowNestedSubagents === "boolean") override.allowNestedSubagents = input.allowNestedSubagents;
|
|
1090
1097
|
else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'allowNestedSubagents'; expected a boolean.`);
|
|
@@ -1371,6 +1378,7 @@ function applyBuiltinOverride(
|
|
|
1371
1378
|
if (override.systemPrompt !== undefined) next.systemPrompt = override.systemPrompt;
|
|
1372
1379
|
if (override.skills !== undefined) { if (override.skills === false) delete next.skills; else next.skills = [...override.skills]; }
|
|
1373
1380
|
if (override.tools !== undefined) applyToolsOverride(next, override.tools);
|
|
1381
|
+
if (override.excludeTools !== undefined) { if (override.excludeTools === false) delete next.excludeTools; else next.excludeTools = [...override.excludeTools]; }
|
|
1374
1382
|
if (override.allowNestedSubagents !== undefined) next.allowNestedSubagents = override.allowNestedSubagents;
|
|
1375
1383
|
if (override.extensions !== undefined) { if (override.extensions === false) delete next.extensions; else next.extensions = [...override.extensions]; }
|
|
1376
1384
|
if (override.subagentOnlyExtensions !== undefined) { if (override.subagentOnlyExtensions === false) delete next.subagentOnlyExtensions; else next.subagentOnlyExtensions = [...override.subagentOnlyExtensions]; }
|
|
@@ -1536,6 +1544,9 @@ function applyCustomAgentOverride(
|
|
|
1536
1544
|
applyToolsOverride(mutable(), override.tools);
|
|
1537
1545
|
anyFilled = true;
|
|
1538
1546
|
}
|
|
1547
|
+
if (override.excludeTools !== undefined) {
|
|
1548
|
+
fill("excludeTools", ["excludeTools"], override.excludeTools === false ? undefined : [...override.excludeTools]);
|
|
1549
|
+
}
|
|
1539
1550
|
if (override.allowNestedSubagents !== undefined) {
|
|
1540
1551
|
fill("allowNestedSubagents", ["allowNestedSubagents"], override.allowNestedSubagents);
|
|
1541
1552
|
}
|
|
@@ -1591,7 +1602,7 @@ function applyCustomAgentOverrides(
|
|
|
1591
1602
|
|
|
1592
1603
|
export function buildBuiltinOverrideConfig(
|
|
1593
1604
|
base: BuiltinAgentOverrideBase,
|
|
1594
|
-
draft: Pick<AgentConfig, "model" | "modelProvider" | "fallbackModels" | "fast" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritGlobalContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "allowNestedSubagents" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "mutationTools" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description" | "output" | "outputMode" | "defaultReads">>,
|
|
1605
|
+
draft: Pick<AgentConfig, "model" | "modelProvider" | "fallbackModels" | "fast" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritGlobalContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "allowNestedSubagents" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "mutationTools" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description" | "output" | "outputMode" | "defaultReads" | "excludeTools">>,
|
|
1595
1606
|
): BuiltinAgentOverrideConfig | undefined {
|
|
1596
1607
|
const override: BuiltinAgentOverrideConfig = {};
|
|
1597
1608
|
|
|
@@ -1620,6 +1631,7 @@ export function buildBuiltinOverrideConfig(
|
|
|
1620
1631
|
const baseTools = joinToolList(base);
|
|
1621
1632
|
const draftTools = joinToolList(draft);
|
|
1622
1633
|
if (!arraysEqual(draftTools, baseTools)) override.tools = draftTools ? [...draftTools] : false;
|
|
1634
|
+
if (!arraysEqual(draft.excludeTools, base.excludeTools)) override.excludeTools = draft.excludeTools ? [...draft.excludeTools] : false;
|
|
1623
1635
|
if (draft.allowNestedSubagents !== base.allowNestedSubagents) override.allowNestedSubagents = draft.allowNestedSubagents === true;
|
|
1624
1636
|
if (!arraysEqual(draft.extensions, base.extensions)) override.extensions = draft.extensions ? [...draft.extensions] : false;
|
|
1625
1637
|
if (!arraysEqual(draft.subagentOnlyExtensions, base.subagentOnlyExtensions)) {
|
|
@@ -1984,7 +1996,7 @@ function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string)
|
|
|
1984
1996
|
|
|
1985
1997
|
function validateExternalRunnerProfile(frontmatter: Record<string, string>, agentName: string, runner: AgentRunnerConfig | undefined): void {
|
|
1986
1998
|
if (runner?.type !== "external-cli" && runner?.type !== "external-job") return;
|
|
1987
|
-
const unsupported = ["tools", "allowNestedSubagents", "model", "fallbackModels", "thinking", "extensions", "subagentOnlyExtensions", "mutationTools", "maxSubagentDepth", "completionGuard", "skills", "skill", "skillPath", "toolBudget", "permission", "permissions"]
|
|
1999
|
+
const unsupported = ["tools", "excludeTools", "allowNestedSubagents", "model", "fallbackModels", "thinking", "extensions", "subagentOnlyExtensions", "mutationTools", "maxSubagentDepth", "completionGuard", "skills", "skill", "skillPath", "toolBudget", "permission", "permissions"]
|
|
1988
2000
|
.filter((field) => frontmatter[field] !== undefined);
|
|
1989
2001
|
if (unsupported.length > 0) {
|
|
1990
2002
|
throw new Error(`Agent '${agentName}' uses runner.type='${runner.type}' and declares unsupported Pi-only fields: ${unsupported.join(", ")}.`);
|
|
@@ -2066,6 +2078,7 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
|
|
|
2066
2078
|
const parsedTools = splitToolList(rawTools);
|
|
2067
2079
|
const tools = parsedTools.tools ?? [];
|
|
2068
2080
|
const mcpDirectTools = parsedTools.mcpDirectTools ?? [];
|
|
2081
|
+
const excludeTools = parseFrontmatterList(frontmatter.excludeTools);
|
|
2069
2082
|
const defaultReads = parseFrontmatterList(frontmatter.defaultReads);
|
|
2070
2083
|
const aliases = normalizeAgentAliases(parseFrontmatterList(frontmatter.aliases ?? frontmatter.alias), runtimeName);
|
|
2071
2084
|
const profileError = validateCodeOwnedProfileRunner({ name: runtimeName, localName, aliases, runner });
|
|
@@ -2187,6 +2200,7 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
|
|
|
2187
2200
|
description: frontmatter.description,
|
|
2188
2201
|
...(aliases !== undefined ? { aliases } : {}),
|
|
2189
2202
|
...(rawTools !== undefined ? { tools } : {}),
|
|
2203
|
+
...(excludeTools !== undefined ? { excludeTools } : {}),
|
|
2190
2204
|
...(allowNestedSubagents !== undefined ? { allowNestedSubagents } : {}),
|
|
2191
2205
|
...(mcpDirectTools.length > 0 ? { mcpDirectTools } : {}),
|
|
2192
2206
|
...(frontmatter.model !== undefined ? { model: frontmatter.model } : {}),
|
|
@@ -20,6 +20,7 @@ export interface RuntimeAgentDefinition {
|
|
|
20
20
|
systemPrompt: string;
|
|
21
21
|
aliases?: readonly string[];
|
|
22
22
|
tools?: readonly string[];
|
|
23
|
+
excludeTools?: readonly string[];
|
|
23
24
|
allowNestedSubagents?: boolean;
|
|
24
25
|
mcpDirectTools?: readonly string[];
|
|
25
26
|
model?: string;
|
|
@@ -198,7 +199,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
|
|
|
198
199
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Runtime agent definition must be an object.");
|
|
199
200
|
const definition = value as Record<string, unknown>;
|
|
200
201
|
const supported = new Set([
|
|
201
|
-
"description", "systemPrompt", "aliases", "tools", "allowNestedSubagents", "mcpDirectTools", "model", "fallbackModels", "thinking",
|
|
202
|
+
"description", "systemPrompt", "aliases", "tools", "excludeTools", "allowNestedSubagents", "mcpDirectTools", "model", "fallbackModels", "thinking",
|
|
202
203
|
"systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "defaultContext", "defaultAsync", "defaultTimeoutMs",
|
|
203
204
|
"defaultToolTimeoutMs", "defaultAcceptance", "acceptanceRole", "runner", "skills", "skillPath",
|
|
204
205
|
"extensions", "subagentOnlyExtensions", "mutationTools", "output", "outputMode", "defaultReads", "defaultProgress", "interactive",
|
|
@@ -218,6 +219,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
|
|
|
218
219
|
if (outputMode !== undefined && outputMode !== "inline" && outputMode !== "file-only") throw new Error("Runtime agent definition outputMode must be 'inline' or 'file-only'.");
|
|
219
220
|
const aliases = validateStringList(definition.aliases, "Runtime agent definition aliases");
|
|
220
221
|
const tools = validateStringList(definition.tools, "Runtime agent definition tools");
|
|
222
|
+
const excludeTools = validateStringList(definition.excludeTools, "Runtime agent definition excludeTools");
|
|
221
223
|
const allowNestedSubagents = validateBoolean(definition.allowNestedSubagents, "Runtime agent definition allowNestedSubagents");
|
|
222
224
|
const mcpDirectTools = validateStringList(definition.mcpDirectTools, "Runtime agent definition mcpDirectTools");
|
|
223
225
|
const model = validateOptionalString(definition.model, "Runtime agent definition model");
|
|
@@ -248,6 +250,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
|
|
|
248
250
|
systemPrompt: validateString(definition.systemPrompt, "Runtime agent definition systemPrompt", MAX_SYSTEM_PROMPT_LENGTH),
|
|
249
251
|
...(aliases ? { aliases } : {}),
|
|
250
252
|
...(tools ? { tools } : {}),
|
|
253
|
+
...(excludeTools ? { excludeTools } : {}),
|
|
251
254
|
...(allowNestedSubagents !== undefined ? { allowNestedSubagents } : {}),
|
|
252
255
|
...(mcpDirectTools ? { mcpDirectTools } : {}),
|
|
253
256
|
...(model ? { model } : {}),
|
|
@@ -327,6 +330,7 @@ function toAgentConfig(name: string, definition: RuntimeAgentDefinition): AgentC
|
|
|
327
330
|
...(aliases ? { aliases } : {}),
|
|
328
331
|
...(definition.runner !== undefined ? { runner: definition.runner } : {}),
|
|
329
332
|
...(definition.tools !== undefined ? { tools: [...definition.tools] } : {}),
|
|
333
|
+
...(definition.excludeTools !== undefined ? { excludeTools: [...definition.excludeTools] } : {}),
|
|
330
334
|
...(definition.allowNestedSubagents !== undefined ? { allowNestedSubagents: definition.allowNestedSubagents } : {}),
|
|
331
335
|
...(definition.mcpDirectTools !== undefined ? { mcpDirectTools: [...definition.mcpDirectTools] } : {}),
|
|
332
336
|
...(definition.model !== undefined ? { model: definition.model } : {}),
|
package/src/api/preflight.ts
CHANGED
|
@@ -111,6 +111,7 @@ export interface SubagentLaunchContractSkills {
|
|
|
111
111
|
export interface SubagentLaunchContractTools {
|
|
112
112
|
requestedBuiltin: string[];
|
|
113
113
|
declaredBuiltin: string[];
|
|
114
|
+
excludeTools?: string[];
|
|
114
115
|
effectiveAllowlist: string[];
|
|
115
116
|
explicitAllowlist: boolean;
|
|
116
117
|
requiredChildTools: string[];
|
|
@@ -356,6 +357,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
356
357
|
try {
|
|
357
358
|
toolPlan = resolvePiLaunchToolPlan({
|
|
358
359
|
tools: agent.tools,
|
|
360
|
+
excludeTools: agent.excludeTools,
|
|
359
361
|
allowNestedSubagents: agent.allowNestedSubagents,
|
|
360
362
|
extensions: agent.extensions,
|
|
361
363
|
subagentOnlyExtensions: agent.subagentOnlyExtensions,
|
|
@@ -435,6 +437,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
435
437
|
tools: {
|
|
436
438
|
requestedBuiltin: toolPlan.requestedBuiltinTools,
|
|
437
439
|
declaredBuiltin: toolPlan.declaredBuiltinTools,
|
|
440
|
+
...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
|
|
438
441
|
effectiveAllowlist: toolPlan.effectiveToolAllowlist,
|
|
439
442
|
explicitAllowlist: toolPlan.explicitToolAllowlist,
|
|
440
443
|
requiredChildTools: toolPlan.requiredChildTools,
|
|
@@ -485,6 +488,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
485
488
|
inheritSkills: agent.inheritSkills,
|
|
486
489
|
skills: requestedSkills,
|
|
487
490
|
tools: toolPlan.effectiveToolAllowlist,
|
|
491
|
+
...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
|
|
488
492
|
extensions: toolPlan.extensionArgs,
|
|
489
493
|
mcpDirectTools: toolPlan.effectiveMcpTools,
|
|
490
494
|
...(outputPath ? { outputPath } : {}),
|
package/src/extension/schemas.ts
CHANGED
|
@@ -86,10 +86,13 @@ const AcceptanceOverride = Type.Unsafe({
|
|
|
86
86
|
deprecated: true,
|
|
87
87
|
description: "Invalid as an explicit policy. Recognized only so preflight can explain that reviewed is an achieved status.",
|
|
88
88
|
},
|
|
89
|
+
{
|
|
90
|
+
type: "string",
|
|
91
|
+
},
|
|
89
92
|
{ type: "boolean", enum: [false] },
|
|
90
93
|
{ type: "object", additionalProperties: true },
|
|
91
94
|
],
|
|
92
|
-
description: `Optional acceptance policy.
|
|
95
|
+
description: `Optional acceptance policy. Prefer an inline JSON object. JSON-encoded object strings are tolerated only during input normalization; invalid strings fail closed. Reviewer/read-only calls, omit acceptance. { level: "checked", evidence: ["commands-run", "changed-files"] }. Supported evidence kinds: ${AcceptanceEvidenceKinds.join(",")}. acceptance.review.required.`,
|
|
93
96
|
});
|
|
94
97
|
|
|
95
98
|
const AgentContractOverride = Type.Object({
|
|
@@ -317,8 +320,9 @@ const SubagentParamProperties = {
|
|
|
317
320
|
thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean", enum: [false] }], description: "Thinking level for action='watchdog.configure' only (off/minimal/low/medium/high/xhigh/max, inherit, or false for off). Ignored on dispatch; set per-run child thinking with a suffix on the model string, e.g. model: 'provider/id:high'." })),
|
|
318
321
|
at: Type.Optional(Type.String({ description: "One-shot trigger for action='schedule.create': a relative delay such as '+10m' or an ISO timestamp with timezone." })),
|
|
319
322
|
every: Type.Optional(Type.String({ description: "Fixed recurring interval for action='schedule.create', such as '30m', '6h', '2d', or '2w'." })),
|
|
323
|
+
sessionOnly: Type.Optional(Type.Boolean()),
|
|
320
324
|
on: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "integer" }], description: "Calendar selector reserved for a later schedule slice." })),
|
|
321
|
-
timezone: Type.Optional(Type.String(
|
|
325
|
+
timezone: Type.Optional(Type.String()),
|
|
322
326
|
overlap: Type.Optional(Type.String({ enum: ["skip"], description: "Overlap policy. This slice supports skip only." })),
|
|
323
327
|
catchUp: Type.Optional(Type.String({ enum: ["none", "latest"], description: "Missed occurrence policy for recurring schedules. Defaults to latest." })),
|
|
324
328
|
missionId: Type.Optional(Type.String({ description: "Mission id." })),
|
|
@@ -57,7 +57,7 @@ EXECUTION:
|
|
|
57
57
|
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
58
58
|
• validate checks workflowScript or workflowScriptPath syntax and statically decidable structure without launching children. list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, status, debug.run, doctor, grant-spawn-budget, worktree.discard, worktree.cleanup (plan-only), lane.status, lane.recordMerge, lane.recordSupersession, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
|
|
59
59
|
• status, interrupt, stop, resume, and steer manage live or persisted runs. Use status view:"fleet" for an overview or view:"transcript" with id and optional index to tail output.
|
|
60
|
-
• Create durable project schedules with { action:"schedule.create", id?, name?, at:"+10m" | ISO, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" }, or use workflowScriptPath instead. Manage them with schedule.list/show/history/pause/resume/run/run-due/delete. This first slice supports fixed intervals; calendar schedules and schedule mission attachment are deferred.
|
|
60
|
+
• Create durable project schedules with { action:"schedule.create", id?, name?, sessionOnly?:true, at:"+10m" | ISO, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" }, or use workflowScriptPath instead. With sessionOnly:true, the schedule records the creating session file and only that session can restore or execute it; omitted/false preserves project-wide behavior. Manage them with schedule.list/show/history/pause/resume/run/run-due/delete. This first slice supports fixed intervals; calendar schedules and schedule mission attachment are deferred.
|
|
61
61
|
|
|
62
62
|
${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
63
63
|
|
|
@@ -33,6 +33,7 @@ export interface ActiveAsyncCapacityHandle {
|
|
|
33
33
|
markStarted(runnerProcessInstanceId: string): void;
|
|
34
34
|
markWorkflowStarted(): void;
|
|
35
35
|
rollback(): boolean;
|
|
36
|
+
rollbackBeforeRunnerProceed(runnerProcessInstanceId: string): boolean;
|
|
36
37
|
reconcile(liveWorkflowRunIds?: ReadonlySet<string>): ActiveAsyncCapacitySnapshot;
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -43,6 +44,7 @@ interface CapacityOptions {
|
|
|
43
44
|
abandonedSlotReleaseAfterMs?: number | false;
|
|
44
45
|
pidLiveness?: (pid: number) => PidLiveness;
|
|
45
46
|
afterSlotRename?: (releasedDir: string) => void;
|
|
47
|
+
writeOwner?: (filePath: string, owner: ActiveAsyncCapacityOwnerV1) => void;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export interface ActiveAsyncCapacityReleaseEvidence {
|
|
@@ -380,6 +382,7 @@ function createSlot(poolDir: string, owner: ActiveAsyncCapacityOwnerV1): boolean
|
|
|
380
382
|
|
|
381
383
|
function handleFor(owner: ActiveAsyncCapacityOwnerV1, limit: number, options: CapacityOptions, rollbackOwner?: ActiveAsyncCapacityOwnerV1): ActiveAsyncCapacityHandle {
|
|
382
384
|
const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
|
|
385
|
+
const writeOwner = options.writeOwner ?? writePrivateAtomicJson;
|
|
383
386
|
const dir = slotDir(sessionDir(owner.ownerSessionId, rootDir), owner.slot);
|
|
384
387
|
return {
|
|
385
388
|
owner,
|
|
@@ -388,14 +391,10 @@ function handleFor(owner: ActiveAsyncCapacityOwnerV1, limit: number, options: Ca
|
|
|
388
391
|
const current = matchingOwner(dir, owner);
|
|
389
392
|
if (!current) return false;
|
|
390
393
|
const next = { ...current, runnerProcessInstanceId, runnerStartedAt: options.now?.() ?? Date.now() };
|
|
391
|
-
// Mark memory first
|
|
392
|
-
//
|
|
394
|
+
// Mark memory first so pre-proceed cleanup can distinguish a failed durable
|
|
395
|
+
// bind from an unrelated unstarted reservation.
|
|
393
396
|
Object.assign(owner, next);
|
|
394
|
-
|
|
395
|
-
writePrivateAtomicJson(path.join(dir, "owner.json"), next);
|
|
396
|
-
} catch (error) {
|
|
397
|
-
console.error(`Failed to bind active async capacity to runner '${runnerProcessInstanceId}'; capacity will remain occupied:`, error);
|
|
398
|
-
}
|
|
397
|
+
writeOwner(path.join(dir, "owner.json"), next);
|
|
399
398
|
return true;
|
|
400
399
|
});
|
|
401
400
|
if (!claimed.acquired || !claimed.value) throw new Error(`Active async capacity ownership changed for run '${owner.runId}'.`);
|
|
@@ -427,6 +426,26 @@ function handleFor(owner: ActiveAsyncCapacityOwnerV1, limit: number, options: Ca
|
|
|
427
426
|
});
|
|
428
427
|
return claimed.acquired && claimed.value;
|
|
429
428
|
},
|
|
429
|
+
rollbackBeforeRunnerProceed(runnerProcessInstanceId) {
|
|
430
|
+
const claimed = withSlotClaim(dir, () => {
|
|
431
|
+
const current = matchingOwner(dir, owner);
|
|
432
|
+
if (!current) return false;
|
|
433
|
+
const boundToRunner = current.runnerProcessInstanceId === runnerProcessInstanceId;
|
|
434
|
+
const bindingFailedBeforeProceed = current.runnerProcessInstanceId === undefined && current.runnerStartedAt === undefined && owner.runnerProcessInstanceId === runnerProcessInstanceId;
|
|
435
|
+
if (!boundToRunner && !bindingFailedBeforeProceed) return false;
|
|
436
|
+
if (rollbackOwner) {
|
|
437
|
+
writePrivateAtomicJson(path.join(dir, "owner.json"), rollbackOwner);
|
|
438
|
+
Object.assign(owner, rollbackOwner);
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
const releasedDir = path.join(path.dirname(dir), `.${path.basename(dir)}.released-${randomUUID()}`);
|
|
442
|
+
fs.renameSync(dir, releasedDir);
|
|
443
|
+
options.afterSlotRename?.(releasedDir);
|
|
444
|
+
fs.rmSync(releasedDir, { recursive: true, force: true });
|
|
445
|
+
return true;
|
|
446
|
+
});
|
|
447
|
+
return claimed.acquired && claimed.value;
|
|
448
|
+
},
|
|
430
449
|
reconcile(liveWorkflowRunIds) {
|
|
431
450
|
return reconcileActiveAsyncCapacity(owner.ownerSessionId, limit, { ...options, rootDir, liveWorkflowRunIds });
|
|
432
451
|
},
|