pi-subagents 0.65.0 → 0.65.1
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 +32 -0
- package/docs/agents.md +1 -1
- package/docs/configuration.md +16 -0
- package/docs/extension-api.md +3 -0
- package/docs/tool-reference.md +8 -2
- package/docs/workflows.md +8 -0
- package/package.json +3 -1
- package/runner-server-preload.mjs +13 -0
- package/skills/pi-subagents/SKILL.md +2 -1
- package/skills/pi-subagents/references/execution-controls.md +5 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- package/src/api/preflight.ts +5 -1
- package/src/extension/config.ts +4 -2
- package/src/extension/index.ts +31 -2
- package/src/extension/schemas.ts +1 -1
- package/src/extension/tool-description.ts +5 -1
- package/src/integrations/pi-web-session-liveness.ts +73 -0
- package/src/intercom/native-supervisor-channel.ts +22 -36
- package/src/intercom/supervisor-ui.ts +3 -2
- package/src/missions/workflow-state.ts +37 -16
- package/src/runs/background/async-execution.ts +8 -1
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-retention.ts +9 -0
- package/src/runs/background/notify.ts +2 -0
- package/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/src/runs/background/run-child-session.ts +2 -1
- package/src/runs/background/runner-aliases.ts +32 -5
- package/src/runs/background/subagent-runner.ts +19 -0
- package/src/runs/foreground/execution.ts +15 -1
- package/src/runs/foreground/foreground-history.ts +3 -1
- package/src/runs/foreground/prompt-audit.ts +9 -5
- package/src/runs/foreground/subagent-executor.ts +61 -34
- package/src/runs/shared/acceptance.ts +14 -1
- package/src/runs/shared/child-session.ts +13 -18
- package/src/runs/shared/llm-intent-arbiter.ts +20 -11
- package/src/runs/shared/model-exclusions.ts +2 -1
- package/src/runs/shared/model-fallback.ts +31 -2
- package/src/runs/shared/nested-events.ts +3 -3
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/worktree-cleanup-plan.ts +6 -3
- package/src/runs/shared/worktree.ts +75 -10
- package/src/shared/model-response-aliases.ts +13 -0
- package/src/shared/types.ts +8 -0
- package/src/shared/utils.ts +3 -0
- package/src/shared/watch-strategy.ts +2 -0
- package/src/tui/fleet-status.ts +1 -1
- package/src/tui/render.ts +21 -10
- package/src/workflows/scripted-workflow.ts +32 -6
- package/src/workflows/workflow-checklist.ts +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.65.1] - 2026-09-04
|
|
6
|
+
|
|
7
|
+
### Highlights
|
|
8
|
+
- Background sessions stay alive until their work and result delivery finish.
|
|
9
|
+
- Background runs work on Pi 0.85.0 without missing-package errors.
|
|
10
|
+
- Custom-provider models and proxy connections work more reliably in background runs.
|
|
11
|
+
- Parallel children keep separate session logs, even with a shared log directory.
|
|
12
|
+
- Worktree cleanup preserves changes until a complete, usable patch has been saved.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Clarify that switching execution modes after a failed run requires your approval (#1879).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Accept configured model aliases returned by gateways without changing the requested model. Thanks to [@drudko-ias](https://github.com/drudko-ias) for #1897.
|
|
19
|
+
- Hide the empty, unlimited async capacity summary in Fleet. Thanks to [@youssefsiam38](https://github.com/youssefsiam38) for #1892.
|
|
20
|
+
- Fix missing-server import errors in Pi 0.85.0 background runs, including runs that load extensions.
|
|
21
|
+
- Remove repeated metadata and reply instructions from supervisor request cards. Thanks to [@youssefsiam38](https://github.com/youssefsiam38) for #1893.
|
|
22
|
+
- Stop foreground runs before contacting a provider when required tools are missing. Respect workflow async defaults and show each failure once. Thanks to [@youssefsiam38](https://github.com/youssefsiam38) for #1891.
|
|
23
|
+
- Keep pi-web parent sessions alive while subagent work or completion delivery remains active. Thanks to [@vcing](https://github.com/vcing) for #1857.
|
|
24
|
+
- Avoid unnecessary startup delays and preserve active locks when process IDs are reused. Thanks to [@ducaoya](https://github.com/ducaoya) for #1878.
|
|
25
|
+
- Make extension-provided models available before starting child sessions, including on Pi versions without native provider queues. Thanks to [@kevinkirkup](https://github.com/kevinkirkup) for #1885.
|
|
26
|
+
- Handle long or unusual workflow IDs without creating invalid file paths. Thanks to [@jstillwa](https://github.com/jstillwa) for #1875.
|
|
27
|
+
- Avoid startup import errors when optional Pi packages are not installed beside the extension. Thanks to [@VladimirGVP](https://github.com/VladimirGVP) for #1860.
|
|
28
|
+
- Use custom provider streams for task classification only when they match the selected model's API. Thanks to [@pwguler](https://github.com/pwguler) for #1874.
|
|
29
|
+
- Give concurrent children separate session files under an explicit `sessionDir`. Thanks to [@dat9uy](https://github.com/dat9uy) for #1859 and #1858.
|
|
30
|
+
- Honor proxy environment variables in detached background runs. Thanks to [@jasonrale](https://github.com/jasonrale) for #1867 and #1866.
|
|
31
|
+
- Allow previously unavailable models to run once they reappear in the model registry. Thanks to [@x1prog](https://github.com/x1prog) for #1862.
|
|
32
|
+
- Resolve Pi package imports correctly in detached runs, including subpath imports. Thanks to [@plopezlpz](https://github.com/plopezlpz) for #1871 and [@pwguler](https://github.com/pwguler) for #1880.
|
|
33
|
+
- Initialize the theme before child extensions access `ctx.ui.theme`. Thanks to [@danielmarbach](https://github.com/danielmarbach) for #1865.
|
|
34
|
+
- Save complete worktree patches, including binary changes, regardless of Git display settings. Validate them before removing worktrees. Thanks to [@jeanduplessis](https://github.com/jeanduplessis) for #1868.
|
|
35
|
+
- Accept `acceptance: false` alongside `gate`, treating it as omitted.
|
|
36
|
+
|
|
5
37
|
## [0.65.0] - 2026-09-04
|
|
6
38
|
|
|
7
39
|
### Highlights
|
package/docs/agents.md
CHANGED
|
@@ -58,7 +58,7 @@ The Pi async run remains the source of truth for status, artifacts, wake/wait, m
|
|
|
58
58
|
|
|
59
59
|
### Advisory runner data boundary
|
|
60
60
|
|
|
61
|
-
External CLI agents use their own runner contract. Do not pass native Pi child options such as model override, structured output, acceptance/agent contract, tool budgets, fast mode, fork context, skills, or native Pi tools unless the adapter explicitly implements them.
|
|
61
|
+
External CLI agents use their own runner contract. They are deliberate execution modes, not implicit recovery paths for a failed native `subagent` workflow. For backlog lanes and other subagent-governed workflows, switching to an external, foreground, or CLI runner requires explicit owner approval after the exact failure/run/worktree state is recorded and the worktree is verified clean or its partial diff is captured. Do not pass native Pi child options such as model override, structured output, acceptance/agent contract, tool budgets, fast mode, fork context, skills, or native Pi tools unless the adapter explicitly implements them.
|
|
62
62
|
|
|
63
63
|
The built-in `codex-exec` and `codex-exec-writer` profiles are the supported Codex one-shot modes. Both require an installed and authenticated Codex CLI. The adapters own `codex exec --json` argv with ignored user config and rules, ephemeral sessions, approval policy `never`, and a final-message artifact.
|
|
64
64
|
|
package/docs/configuration.md
CHANGED
|
@@ -32,6 +32,22 @@ Add recursive user or project agent roots with `subagents.agentScanDirs` in Pi s
|
|
|
32
32
|
|
|
33
33
|
Entries support `~` expansion. A single `*` path segment expands one directory level, so package-like folders can each expose an `agents/` directory. Missing directories are ignored. Fixed user/project agent directories still win over same-name agents from scan roots.
|
|
34
34
|
|
|
35
|
+
## `modelResponseAliases`
|
|
36
|
+
|
|
37
|
+
In `~/.pi/agent/extensions/subagent/config.json` (top-level, not under `subagents`):
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"modelResponseAliases": {
|
|
42
|
+
"databricks-bedrock/ias-claude-opus-5": ["claude-opus-5"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Optionally accept exact response model IDs for an exact provider-qualified launch candidate. Keys use the resolved `provider/model` ID without its thinking suffix, including for fallback attempts; values are arrays of non-empty response ID strings. Alias matching is exact and case-sensitive, with no fuzzy or suffix matching. Empty arrays add no accepted IDs; malformed declarations fail config loading.
|
|
48
|
+
|
|
49
|
+
This is your explicit assertion that the declared response IDs identify the requested model, not proof from model output. It does not rewrite the outgoing model or provider route, authorize fallback models, or bypass verification for other routes. Foreground and background runs capture this declaration for launch and retain it on revival, including when no aliases were declared. Changing config affects new independent runs, not the retained declaration. Without a matching declaration, existing strict verification remains unchanged.
|
|
50
|
+
|
|
35
51
|
## `modelExclusions`
|
|
36
52
|
|
|
37
53
|
```json
|
package/docs/extension-api.md
CHANGED
|
@@ -414,6 +414,8 @@ Detached children do not stop when the session does. They are the host process's
|
|
|
414
414
|
|
|
415
415
|
This matters because "is the parent busy?" is the wrong idle signal. A parent that launches a detached run and hands control back — which is what the async launch output tells it to do — is not prompting, streaming, compacting, or running a shell command. A host that reaps sessions on those signals alone will dispose exactly the session that was waiting to be woken.
|
|
416
416
|
|
|
417
|
+
When pi-subagents runs inside a compatible pi-web host, it discovers the versioned `Symbol.for("@agegr/pi-web/session-liveness/v1")` registry and registers one provider for the current session. The provider reports live `queued`/`running` async jobs, active nested descendants (including foreground routes retained after their direct parent settles), foreground controls that still have a scheduling owner or active child, and completion notifications waiting for their batch-delivery timer. Retained terminal history, future schedules, and wait subscriptions do not make a session live by themselves. The registration is replaced on session changes and released during runtime shutdown or reload; other hosts remain unaffected.
|
|
418
|
+
|
|
417
419
|
If your host reclaims idle sessions, keep a session alive while it still has live detached work:
|
|
418
420
|
|
|
419
421
|
- Read run state from the status files under the async run directory rather than from event traffic. A long, quiet workflow sends almost nothing to the parent, so recent-activity heuristics conclude the wrong thing.
|
|
@@ -431,6 +433,7 @@ The main runtime files in this repository:
|
|
|
431
433
|
| File | Purpose |
|
|
432
434
|
|------|---------|
|
|
433
435
|
| `src/extension/index.ts` | Extension registration, tool registration, message/render wiring. |
|
|
436
|
+
| `src/integrations/pi-web-session-liveness.ts` | Optional pi-web idle-eviction liveness bridge. |
|
|
434
437
|
| `src/agents/agents.ts` | Agent and chain discovery, frontmatter parsing. |
|
|
435
438
|
| `src/runs/foreground/subagent-executor.ts` | Main execution routing for single, parallel, chain, management, status, interrupt, and doctor actions. |
|
|
436
439
|
| `src/runs/foreground/execution.ts` | Core foreground `runSync` handling: drives one in-process child session per attempt. |
|
package/docs/tool-reference.md
CHANGED
|
@@ -120,7 +120,7 @@ The complete plain-JSON inventory is validated before the first launch (maximum
|
|
|
120
120
|
| `share` | boolean | false | Upload session export to GitHub Gist. |
|
|
121
121
|
| `sessionDir` | string | derived | Override session log directory. |
|
|
122
122
|
| `acceptance` | string/object/false | inferred | Configure evidence gates. See [Acceptance gates](#acceptance-gates). |
|
|
123
|
-
| `gate` | string | - | One host-run verification command, shorthand for `acceptance: { level: "verified", verify: [{ id: "gate", command }] }`. Also valid on individual `runs.run`/`runs.all` items.
|
|
123
|
+
| `gate` | string | - | One host-run verification command, shorthand for `acceptance: { level: "verified", verify: [{ id: "gate", command }] }`. Also valid on individual `runs.run`/`runs.all` items. Rejects `acceptance` except `false` (treated as omitted), and rejects retained `resume`. |
|
|
124
124
|
|
|
125
125
|
### Budget guidance for writers
|
|
126
126
|
|
|
@@ -298,6 +298,12 @@ The manifest stores one of these fail-closed eligibility states: `active` (an ow
|
|
|
298
298
|
|
|
299
299
|
## Status and control actions
|
|
300
300
|
|
|
301
|
+
### Failed lane recovery and execution-mode boundaries
|
|
302
|
+
|
|
303
|
+
A failure in the subagent workflow, child launch, prompt runtime, extension loading, or child tooling setup is a lane infrastructure blocker, not permission to silently change execution mode. Stop and report the exact failure, run/status, and repo/cwd/worktree/branch/ref state. Before a same-protocol retry or asking the owner, verify the worktree is clean or capture the partial diff. Retry or fix the `subagent` path only through a clear same-protocol action.
|
|
304
|
+
|
|
305
|
+
For backlog lanes and other subagent-governed workflows, external/foreground/CLI fallback requires explicit owner approval. Do not silently switch to `interactive_shell`, `pi -ne`, Codex/Claude/Cursor CLI, a foreground agent, or another external mode. `interactive_shell` remains valid when the user explicitly requests visible foreground/CLI work or the task is outside the governed subagent protocol. Pi core may print a generic `pi -ne` extension-load hint; that out-of-repo hint is not protocol-approved fallback. Configured native model/provider fallback remains governed by its own contract.
|
|
306
|
+
|
|
301
307
|
```ts
|
|
302
308
|
subagent({ action: "status" })
|
|
303
309
|
subagent({ action: "status", view: "fleet" })
|
|
@@ -385,7 +391,7 @@ When one host-run command is the entire verification contract, use the `gate` sh
|
|
|
385
391
|
{ workflowScript: `return runs.run("impl", { agent: "worker", task: "Implement the fix", gate: "npm test" })` }
|
|
386
392
|
```
|
|
387
393
|
|
|
388
|
-
`gate` normalizes to verified acceptance with that single command, so the runtime executes it on the host and records the result as evidence. Verification results are memoized per tracked workspace state and effective environment, so an unchanged tree does not rerun the same command. Use explicit `acceptance.verify` when you need multiple commands, timeouts, or custom criteria. `gate`
|
|
394
|
+
`gate` normalizes to verified acceptance with that single command, so the runtime executes it on the host and records the result as evidence. Verification results are memoized per tracked workspace state and effective environment, so an unchanged tree does not rerun the same command. Use explicit `acceptance.verify` when you need multiple commands, timeouts, or custom criteria. `gate` rejects `acceptance` except `false` (treated as omitted), and rejects retained `resume` items. With `worktree: true`, the gate runs inside the child's managed worktree.
|
|
389
395
|
|
|
390
396
|
### Levels and inference
|
|
391
397
|
|
package/docs/workflows.md
CHANGED
|
@@ -19,6 +19,14 @@ Child-safety boundaries are enforced at runtime:
|
|
|
19
19
|
- By default, children do not register the `subagent` tool and receive boundary instructions that they are not the parent orchestrator and must not propose or run subagents.
|
|
20
20
|
- The explicit exception is an agent whose resolved builtin `tools` includes `subagent`; that child gets a child-safe `subagent` tool for the fanout work the parent assigned, still bounded by `maxSubagentDepth`.
|
|
21
21
|
|
|
22
|
+
### Failed lane recovery and execution-mode boundaries
|
|
23
|
+
|
|
24
|
+
A failure in the subagent workflow, child launch, prompt runtime, extension loading, or child tooling setup is a lane infrastructure blocker. It is not permission to silently retry through `interactive_shell`, `pi -ne`, Codex/Claude/Cursor CLI, a foreground agent, or another external execution mode.
|
|
25
|
+
|
|
26
|
+
Stop and report the exact failure, run/status, and repository/cwd/worktree/branch/ref state. Before a same-protocol retry or asking the owner, verify the worktree is clean or capture the partial diff. Retry or fix the `subagent` path only through a clear same-protocol action. For backlog lanes and other subagent-governed workflows, external/foreground/CLI fallback requires explicit owner approval. `interactive_shell` remains valid when the user explicitly requests visible foreground/CLI work or the task is outside the governed subagent protocol.
|
|
27
|
+
|
|
28
|
+
Pi core may print a generic `pi -ne` extension-load hint; that out-of-repo hint is not protocol-approved fallback. Configured native model/provider fallback remains governed by its own contract and does not authorize an execution-mode switch.
|
|
29
|
+
|
|
22
30
|
## Prompt shortcuts
|
|
23
31
|
|
|
24
32
|
The package includes reusable prompt templates for common workflows. You do not need them, but they are handy when you want the same shape every time:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-subagents",
|
|
3
|
-
"version": "0.65.
|
|
3
|
+
"version": "0.65.1",
|
|
4
4
|
"description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
|
|
5
5
|
"author": "Nico Bailon",
|
|
6
6
|
"license": "MIT",
|
|
@@ -90,9 +90,11 @@
|
|
|
90
90
|
}
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
|
+
"@earendil-works/pi-server": "0.85.0",
|
|
93
94
|
"acorn": "8.18.0",
|
|
94
95
|
"jiti": "2.7.0",
|
|
95
96
|
"typebox": "1.1.38",
|
|
97
|
+
"undici": "8.10.0",
|
|
96
98
|
"yaml": "2.8.3"
|
|
97
99
|
},
|
|
98
100
|
"devDependencies": {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Only loaded when the parent supplies Pi 0.85.0's missing server exports.
|
|
2
|
+
import { registerHooks } from "node:module";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
const aliases = JSON.parse(process.env.JITI_ALIAS);
|
|
6
|
+
registerHooks({
|
|
7
|
+
resolve(specifier, context, nextResolve) {
|
|
8
|
+
if (specifier === "@earendil-works/pi-server" || specifier === "@earendil-works/pi-server/unix") {
|
|
9
|
+
return nextResolve(pathToFileURL(aliases[specifier]).href, context);
|
|
10
|
+
}
|
|
11
|
+
return nextResolve(specifier, context);
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -91,7 +91,8 @@ review.
|
|
|
91
91
|
- Exact model names are deployment policy. Put them in user/project settings or profiles, not package guidance.
|
|
92
92
|
- Give every child a compact meta-prompt checklist: objective; repo/cwd/ref; authority/edit boundary; relevant files/contracts and constraints; success/acceptance criteria; validation; expected output/report; and stop/ask conditions. See `references/prompting-and-roles.md`.
|
|
93
93
|
- For mutation work, use an isolated lane/worktree when isolation, overlap, or concurrent juggling matters; keep one writer per cwd/worktree. See `references/multi-lane-orchestration.md` for lane mechanics.
|
|
94
|
-
- Keep long/high-output validation out of chat: prefer `interactive_shell` dispatch/background monitors, bounded logs, or subagent-owned reports; return a concise summary plus report path unless same-turn output is required.
|
|
94
|
+
- Keep long/high-output validation out of chat: prefer `interactive_shell` dispatch/background monitors, bounded logs, or subagent-owned reports; return a concise summary plus report path unless same-turn output is required. Do not use `interactive_shell` as an implicit fallback for a failed `subagent` lane; see `references/execution-controls.md`.
|
|
95
|
+
- Treat subagent workflow, child launch, prompt runtime, extension load, and child tooling setup failures as lane infrastructure blockers. Stop, report the exact failure and run/worktree state, verify a clean worktree or capture a partial diff, and use only a clear same-protocol retry or an owner-approved execution-mode fallback.
|
|
95
96
|
- For cross-codebase work, record the repo, explicit `cwd`, authority boundary, and expected output before launch.
|
|
96
97
|
- Make parallel prompts distinct by source seam, evidence, and decision. Do not clone prompts with only item numbers swapped.
|
|
97
98
|
- Prefer fresh-context review/validation fanout, then synthesize and apply fixes in the parent.
|
|
@@ -246,9 +246,13 @@ Use diagnostics when setup or child startup looks wrong:
|
|
|
246
246
|
subagent({ action: "doctor" })
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
+
### Failed lane recovery and execution-mode fallback
|
|
250
|
+
|
|
251
|
+
A failure in the subagent workflow, child launch, prompt runtime, extension loading, or child tooling setup is a lane infrastructure blocker, not permission to silently change execution mode. Stop and report the exact failure, run/status, and repo/cwd/worktree/branch/ref state. Retry or fix the `subagent` path only through a clear same-protocol retry; before retrying or asking the owner, verify the worktree is clean or capture the partial diff. For backlog lanes and other subagent-governed workflows, switching to `interactive_shell`, `pi -ne`, Codex/Claude/Cursor CLI, a foreground agent, or another external mode requires explicit owner approval. Pi core may print a generic `pi -ne` extension-load hint; that hint is outside this package and is not protocol-approved fallback. This execution-mode boundary does not prohibit configured native model/provider fallback.
|
|
252
|
+
|
|
249
253
|
### External terminal work
|
|
250
254
|
|
|
251
|
-
Use native `subagent` runs for unattended implementation, review, and gate work that needs managed isolation, durable artifacts, and process controls. Use `interactive_shell` for visible terminal work, alternate CLIs, trust prompts,
|
|
255
|
+
Use native `subagent` runs for unattended implementation, review, and gate work that needs managed isolation, durable artifacts, and process controls. Use `interactive_shell` for visible terminal work, alternate CLIs, trust prompts, or recovery only when the user explicitly requests that mode or the task is outside the governed subagent protocol; it is not an implicit replacement for a failed `subagent` lane.
|
|
252
256
|
|
|
253
257
|
A cooperating terminal runtime can register read-only external records through `pi-subagents/external-runs`. Records include the source, session, state, optional report path, and completion reason. They are observations only: pi-subagents does not start, stop, steer, or otherwise own the foreign process. Run unattended raw terminal agents in an explicit isolated cwd or worktree; do not use a live project checkout as disposable review space.
|
|
254
258
|
|
|
@@ -46,6 +46,8 @@ After a writer produces a candidate, run the required fresh-context, read-only r
|
|
|
46
46
|
|
|
47
47
|
Use stable lane-qualified artifact paths for reports and review output. A handoff states the lane status, repository and worktree, changed files, validation, open decisions, next action, and artifact or receipt paths. Copy only the final evidence to memory, a mission record, or a PR/comment, then remove scratch files from the active worktree before closing the lane.
|
|
48
48
|
|
|
49
|
+
For backlog lanes and other subagent-governed workflows, a setup/runtime/tooling failure remains an infrastructure blocker. Preserve the exact failure, run/status, and repository/cwd/worktree/branch/ref state; verify a clean worktree or capture any partial diff before a same-protocol retry or asking the owner. Do not start another writer or switch to an external, foreground, or CLI execution mode without explicit owner approval.
|
|
50
|
+
|
|
49
51
|
Keep a worktree until its handoff is durable, no run owns it, and no later gate needs it. Clean up only inside the recorded authority boundary. If a run stops or needs attention, preserve its worktree and artifacts, record the last known state and recovery owner, then resume that run or create one replacement lane from the handoff. Do not start another writer while worktree ownership is uncertain.
|
|
50
52
|
|
|
51
53
|
Before completion, inspect the board. Every lane must be terminal or blocked with a named next action. Confirm one writer per repo/cwd or worktree, required validation, required fresh read-only review, and a durable handoff. The parent reports outcomes, evidence, residual risks, and the next decision.
|
package/src/api/preflight.ts
CHANGED
|
@@ -73,6 +73,7 @@ export interface SubagentLaunchContractInput {
|
|
|
73
73
|
/** Current parent leaf required before an implicit `defaultContext: fork` stays `fork`. */
|
|
74
74
|
parentLeafId?: string | null;
|
|
75
75
|
sessionRoot?: string;
|
|
76
|
+
/** Caller directory used as a root keyed by the child run id ("preflight" placeholder when runId is omitted). */
|
|
76
77
|
sessionDir?: string;
|
|
77
78
|
runId?: string;
|
|
78
79
|
/** Root run id supplied by a host when projecting nested async lifecycle paths. */
|
|
@@ -380,7 +381,10 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
380
381
|
const artifactsDir = artifactsEnabled ? getArtifactsDir(input.parentSessionFile ?? null, effectiveCwd, input.artifactDir) : undefined;
|
|
381
382
|
const artifactPaths = artifactsDir ? getArtifactPaths(artifactsDir, runId, agent.name, 0) : undefined;
|
|
382
383
|
const outputPath = resolveSingleOutputPath(behavior.output, effectiveCwd, effectiveCwd, artifactsDir ? path.join(artifactsDir, "outputs", runId) : undefined);
|
|
383
|
-
|
|
384
|
+
// An explicit sessionDir is a root keyed by the child run id, matching the
|
|
385
|
+
// sibling sessionRoot derivation; hosts omitting runId get the documented
|
|
386
|
+
// deterministic "preflight" placeholder.
|
|
387
|
+
const sessionRoot = input.sessionDir ? path.join(path.resolve(input.sessionDir), runId) : input.sessionRoot ? path.join(path.resolve(input.sessionRoot), runId) : undefined;
|
|
384
388
|
const sessionDir = sessionRoot ? path.join(sessionRoot, "run-0") : undefined;
|
|
385
389
|
const lifecycleAsyncDir = input.nestedRootRunId
|
|
386
390
|
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", input.nestedRootRunId, runId)
|
package/src/extension/config.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { DEFAULT_MODEL_EXCLUSION_TTL_MS, MAX_MODEL_EXCLUSION_TTL_MS, setDefaultT
|
|
|
10
10
|
import { validatePermissionConfig } from "../runs/shared/permissions.ts";
|
|
11
11
|
import { MAX_ABANDONED_SLOT_RELEASE_AFTER_MS, MIN_ABANDONED_SLOT_RELEASE_AFTER_MS } from "../runs/background/active-async-capacity.ts";
|
|
12
12
|
import { normalizeWorktreeBranchPrefix } from "../runs/shared/worktree.ts";
|
|
13
|
+
import { validateModelResponseAliases } from "../shared/model-response-aliases.ts";
|
|
13
14
|
|
|
14
15
|
const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
|
|
15
16
|
const FLEET_KEYBINDING_ACTION_SET = new Set<string>(FLEET_KEYBINDING_ACTIONS);
|
|
@@ -177,6 +178,7 @@ function validateConfig(config: Record<string, unknown>): void {
|
|
|
177
178
|
validateArtifactConfig(config.artifactConfig);
|
|
178
179
|
validateCapacityConfig(config.capacity);
|
|
179
180
|
validateModelExclusionsConfig(config.modelExclusions);
|
|
181
|
+
validateModelResponseAliases(config.modelResponseAliases);
|
|
180
182
|
validateMainWindowRendererConfig(config.mainWindowRenderer);
|
|
181
183
|
validateOrcaProgressTabsConfig(config.orcaProgressTabs);
|
|
182
184
|
}
|
|
@@ -240,12 +242,12 @@ export function loadConfig(): ExtensionConfig {
|
|
|
240
242
|
return readConfigForUpdate(configPath);
|
|
241
243
|
} catch (error) {
|
|
242
244
|
if (error instanceof PrunedForkConfigError) throw error;
|
|
243
|
-
//
|
|
245
|
+
// Explicit route identity and worktree policies must not be silently
|
|
244
246
|
// discarded and replaced by the built-in defaults after validation fails.
|
|
245
247
|
try {
|
|
246
248
|
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8")) as unknown;
|
|
247
249
|
if (raw && typeof raw === "object" && !Array.isArray(raw)
|
|
248
|
-
&& (Object.hasOwn(raw, "worktreeProvider") || Object.hasOwn(raw, "worktreeBranchPrefix"))) throw error;
|
|
250
|
+
&& (Object.hasOwn(raw, "worktreeProvider") || Object.hasOwn(raw, "worktreeBranchPrefix") || Object.hasOwn(raw, "modelResponseAliases"))) throw error;
|
|
249
251
|
} catch (readError) {
|
|
250
252
|
if (readError === error) throw error;
|
|
251
253
|
}
|
package/src/extension/index.ts
CHANGED
|
@@ -54,6 +54,8 @@ import {
|
|
|
54
54
|
type SupervisorRequestMessageDetails,
|
|
55
55
|
} from "../intercom/supervisor-ui.ts";
|
|
56
56
|
import { registerHerdrStatusBridge, type HerdrStatusRun } from "../integrations/herdr-status.ts";
|
|
57
|
+
import { hasLiveSubagentWork, registerPiWebSessionLiveness } from "../integrations/pi-web-session-liveness.ts";
|
|
58
|
+
import { createRetainedNestedRouteTracker } from "../runs/background/retained-nested-route-tracker.ts";
|
|
57
59
|
import { listHerdrProjectPaneRoots, restoreHerdrProjectPaneSnapshots } from "../inspectors/herdr/project-panes.ts";
|
|
58
60
|
import { registerSubagentRpcBridge } from "./rpc.ts";
|
|
59
61
|
import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
|
|
@@ -492,6 +494,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
492
494
|
const mainWatchdog = registerMainWatchdog(pi);
|
|
493
495
|
const resultDeliveryOwnership = createResultDeliveryOwnership(state);
|
|
494
496
|
const completionNotifier = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch, ownership: resultDeliveryOwnership });
|
|
497
|
+
let retainedNestedRouteTracker: ReturnType<typeof createRetainedNestedRouteTracker> | undefined;
|
|
495
498
|
const fleetStatus = fleetViewEnabled
|
|
496
499
|
? new SubagentFleetStatus(state, async (itemKey) => {
|
|
497
500
|
const ctx = withLastUiContext((current) => current);
|
|
@@ -510,6 +513,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
510
513
|
let executorScheduled: ((id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
|
|
511
514
|
let goalTurnId = 0;
|
|
512
515
|
let parentSessionEnvValue: string | null = null;
|
|
516
|
+
let releaseHostSessionLiveness = () => {};
|
|
513
517
|
const scheduledStoreRoot = config.scheduledRuns?.storeRoot === undefined ? undefined : resolveScheduledStoreRoot(config.scheduledRuns.storeRoot);
|
|
514
518
|
const scheduledRunManager = createScheduledRunManager({
|
|
515
519
|
config,
|
|
@@ -592,7 +596,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
592
596
|
}, ASYNC_RETENTION_DELAY_MS);
|
|
593
597
|
asyncRetentionTimer.unref?.();
|
|
594
598
|
|
|
595
|
-
const
|
|
599
|
+
const executorDeps: Parameters<typeof createSubagentExecutor>[0] = {
|
|
596
600
|
pi,
|
|
597
601
|
state,
|
|
598
602
|
config,
|
|
@@ -607,7 +611,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
607
611
|
discoverAgents: discoverAgentsForRuntime,
|
|
608
612
|
activateSupervisorTransport: () => supervisorChannel.activateTransport(),
|
|
609
613
|
refreshResultDelivery: () => refreshResultDelivery(),
|
|
610
|
-
|
|
614
|
+
trackRetainedNestedRoute: undefined,
|
|
615
|
+
};
|
|
616
|
+
const executor = createSubagentExecutor(executorDeps);
|
|
611
617
|
executorScheduled = executor.executeScheduled;
|
|
612
618
|
|
|
613
619
|
pi.registerMessageRenderer<SupervisorRequestMessageDetails>(SUPERVISOR_REQUEST_MESSAGE_TYPE, renderSupervisorRequest);
|
|
@@ -942,6 +948,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
942
948
|
cleanupSessionArtifacts(ctx);
|
|
943
949
|
logSlowPhase("session-artifact-cleanup", phaseStartedAt);
|
|
944
950
|
state.foregroundControls.clear();
|
|
951
|
+
retainedNestedRouteTracker?.clear();
|
|
952
|
+
retainedNestedRouteTracker = undefined;
|
|
953
|
+
executorDeps.trackRetainedNestedRoute = undefined;
|
|
945
954
|
state.lastForegroundControlId = null;
|
|
946
955
|
phaseStartedAt = Date.now();
|
|
947
956
|
resetJobs(ctx);
|
|
@@ -978,6 +987,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
978
987
|
if (runtimeCleaned) return;
|
|
979
988
|
runtimeCleaned = true;
|
|
980
989
|
const shuttingDownParentSession = parentSessionEnvValue;
|
|
990
|
+
releaseHostSessionLiveness();
|
|
991
|
+
releaseHostSessionLiveness = () => {};
|
|
981
992
|
// Workflow continuations retain their launch context; abort them before
|
|
982
993
|
// teardown so a reload cannot launch through a stale context.
|
|
983
994
|
for (const controller of state.workflowControllers?.values() ?? []) {
|
|
@@ -1001,6 +1012,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1001
1012
|
for (const timer of state.cleanupTimers.values()) clearTimeout(timer);
|
|
1002
1013
|
state.cleanupTimers.clear();
|
|
1003
1014
|
state.asyncJobs.clear();
|
|
1015
|
+
retainedNestedRouteTracker?.clear();
|
|
1016
|
+
retainedNestedRouteTracker = undefined;
|
|
1017
|
+
executorDeps.trackRetainedNestedRoute = undefined;
|
|
1004
1018
|
for (const unsubscribe of eventUnsubscribes) {
|
|
1005
1019
|
try {
|
|
1006
1020
|
unsubscribe();
|
|
@@ -1094,6 +1108,21 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1094
1108
|
installRuntime(ctx);
|
|
1095
1109
|
const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
|
|
1096
1110
|
resetSessionState(ctx, recovering, event.previousSessionFile);
|
|
1111
|
+
releaseHostSessionLiveness();
|
|
1112
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1113
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1114
|
+
const liveness = sessionId
|
|
1115
|
+
? registerPiWebSessionLiveness({
|
|
1116
|
+
sessionId,
|
|
1117
|
+
...(sessionFile ? { sessionFile } : {}),
|
|
1118
|
+
isActive: () => hasLiveSubagentWork(state) || completionNotifier.hasPendingDelivery(),
|
|
1119
|
+
})
|
|
1120
|
+
: { registered: false, release: () => {} };
|
|
1121
|
+
releaseHostSessionLiveness = liveness.release;
|
|
1122
|
+
if (liveness.registered) {
|
|
1123
|
+
retainedNestedRouteTracker = createRetainedNestedRouteTracker(state);
|
|
1124
|
+
executorDeps.trackRetainedNestedRoute = retainedNestedRouteTracker.track;
|
|
1125
|
+
}
|
|
1097
1126
|
herdrStatusBridge.sessionStarted({
|
|
1098
1127
|
hasUI: ctx.hasUI === true,
|
|
1099
1128
|
runs: activeHerdrRuns(),
|
package/src/extension/schemas.ts
CHANGED
|
@@ -387,7 +387,7 @@ const SubagentParamProperties = {
|
|
|
387
387
|
outputSchema: Type.Optional(JsonSchemaObject),
|
|
388
388
|
agentContract: Type.Optional(AgentContractOverride),
|
|
389
389
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
390
|
-
gate: Type.Optional(Type.String({ minLength: 1, description: "Host gate command. Cannot be combined with acceptance." })),
|
|
390
|
+
gate: Type.Optional(Type.String({ minLength: 1, description: "Host gate command. Cannot be combined with acceptance; an explicit acceptance of false is treated as omitted." })),
|
|
391
391
|
};
|
|
392
392
|
|
|
393
393
|
const SubagentParamsSchema = Type.Object(SubagentParamProperties);
|
|
@@ -6,6 +6,7 @@ import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
|
|
|
6
6
|
const CUSTOM_TOOL_DESCRIPTION_FILE = "subagent-tool-description.md";
|
|
7
7
|
const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
|
|
8
8
|
const EXTERNAL_CLI_RUNNER_GUIDANCE = "External CLI agents (codex-exec, codex-exec-writer, claude-code, claude-code-writer, cursor-agent, cursor-agent-writer) use their own runner contract and do not support native Pi child options such as model override, structured output, acceptance/agent contract, tool budget, fast mode, fork context, skills, or native Pi tools unless the runner explicitly implements them.";
|
|
9
|
+
const SUBAGENT_FAILURE_RECOVERY_GUIDANCE = "If a subagent workflow, child launch, prompt runtime, extension load, or child tooling setup fails, treat it as a lane infrastructure blocker—not permission to change execution mode. Stop and report the exact failure, run/status, and repo/cwd/worktree/branch/ref state; verify the worktree is clean or capture a partial diff before retrying or asking the owner. Retry or fix the subagent path only through a clear same-protocol retry. Do not silently switch to interactive_shell, pi -ne, Codex/Claude/Cursor CLI, a foreground agent, or another external mode. For backlog lanes and other subagent-governed workflows, external/foreground/CLI fallback requires explicit owner approval. Pi core may print a generic pi -ne extension-load hint; that out-of-repo hint is not protocol-approved fallback. interactive_shell remains valid when the user explicitly requests foreground/CLI work or the task is outside the governed subagent protocol.";
|
|
9
10
|
const AGENT_SELECTION_GUIDANCE = "Before execution, call { action: \"list\", capabilities: true } and run only executable, non-disabled agents; for external-cli rows, also require runner.available === true. This is a passive PATH/PATHEXT/X_OK lookup, not authentication, version, or launch proof; launch preflight remains authoritative.";
|
|
10
11
|
const WORKFLOW_RESUME_KEY_GUIDANCE = "Each workflow key identifies one result lane: use a new stable workflow key for every distinct retained resume pass; same-key calls are reused only when launch parameters are identical, and incompatible parameters are rejected.";
|
|
11
12
|
const WORKFLOW_OUTPUT_BINDING_GUIDANCE = "For durable workflow child files, set output on runs.run/runs.all; task filename prose is not an output declaration, and return the child's outputReference, outputPathMapping, or artifactPaths instead of inventing a literal path.";
|
|
@@ -14,7 +15,7 @@ const WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE = "workflowScript rejects nested asyn
|
|
|
14
15
|
const WORKFLOW_RESOURCE_GUIDANCE = "For permission/policy-extension interoperability, use an extension-owned named resource such as {workflow:'review',args:{task:'...'}} or {workflow:'run-ci',args:{command:'npm test'}}. The host resolves the script and authority internally so policy can distinguish it from raw workflowScript/workflowScriptPath; args are bounded plain data, and do not combine workflow with agent, task, workflowScript, or workflowScriptPath.";
|
|
15
16
|
const WORKFLOW_HOST_GUIDANCE = "For permission-sensitive host calls, use an extension-owned resource such as {workflow:'run-ci',args:{command:'npm test'}}; raw workflowScript/workflowScriptPath have unknown resource provenance and cannot use runs.host. In a resource that grants it, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). runs.host has no per-step cwd: commands and relative output paths use the workflow cwd; set cwd on the outer subagent request instead (for example, {cwd:'/path/to/worktree',workflowScript:'...'}), or put a trusted directory change in the command (for example, 'cd /path/to/worktree && npm test'). v1 supports only command steps; output is bounded and command failure fails the workflow.";
|
|
16
17
|
|
|
17
|
-
export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration, workflowScriptPath to load a script from the request cwd, or a named workflow resource for permission/policy-aware execution. ${WORKFLOW_RESOURCE_GUIDANCE} ${AGENT_SELECTION_GUIDANCE} The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. ${WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE} ${WORKFLOW_LANES_GUIDANCE} ${WORKFLOW_HOST_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
|
|
18
|
+
export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration, workflowScriptPath to load a script from the request cwd, or a named workflow resource for permission/policy-aware execution. ${WORKFLOW_RESOURCE_GUIDANCE} ${AGENT_SELECTION_GUIDANCE} The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. ${WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE} ${WORKFLOW_LANES_GUIDANCE} ${WORKFLOW_HOST_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} ${SUBAGENT_FAILURE_RECOVERY_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
|
|
18
19
|
|
|
19
20
|
export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
|
|
20
21
|
|
|
@@ -28,6 +29,7 @@ export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
|
|
|
28
29
|
|
|
29
30
|
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
30
31
|
• ${AGENT_SELECTION_GUIDANCE}
|
|
32
|
+
• ${SUBAGENT_FAILURE_RECOVERY_GUIDANCE}
|
|
31
33
|
• Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
|
|
32
34
|
• Async/background runs are the normal default unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Use async:false only when the parent must block until completion. Async mode still shows progress. Final reviews and gate checks stay async; needing a result is not a blocking reason. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Ordinary async subagents notify this session natively, so return control and do not call bg_wait merely to get a completion wake. Do not sleep or poll status just to wait; use bg_wait only for provider, detached, or other background work without a native notification when this turn must receive its result.
|
|
33
35
|
• ${WORKFLOW_RESUME_KEY_GUIDANCE}
|
|
@@ -82,6 +84,7 @@ MANAGE / CONTROL:
|
|
|
82
84
|
• A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
|
|
83
85
|
|
|
84
86
|
ASYNC / SAFETY:
|
|
87
|
+
• ${SUBAGENT_FAILURE_RECOVERY_GUIDANCE}
|
|
85
88
|
• Omitted async follows asyncByDefault config; set async:true explicitly when async behavior matters. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Ordinary async subagents notify this session natively, so return control and do not call bg_wait merely to get a completion wake. Do not sleep or poll merely to wait; use bg_wait only for provider, detached, or other background work without a native notification when this turn must receive its result.
|
|
86
89
|
• ${WORKFLOW_RESUME_KEY_GUIDANCE}
|
|
87
90
|
• ${WORKFLOW_OUTPUT_BINDING_GUIDANCE}
|
|
@@ -197,6 +200,7 @@ function loadCustomToolDescription(options?: ToolDescriptionOptions): string | u
|
|
|
197
200
|
function withMandatorySafetyGuidance(description: string): string {
|
|
198
201
|
const customDescription = description
|
|
199
202
|
.split(SUBAGENT_SAFETY_GUIDANCE)
|
|
203
|
+
.flatMap((part) => part.split(SUBAGENT_FAILURE_RECOVERY_GUIDANCE))
|
|
200
204
|
.map((part) => part.trim())
|
|
201
205
|
.filter(Boolean)
|
|
202
206
|
.join("\n\n");
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { hasLiveNestedDescendants, projectNestedEvents } from "../runs/shared/nested-events.ts";
|
|
2
|
+
import type { NestedRouteInfo, SubagentState } from "../shared/types.ts";
|
|
3
|
+
|
|
4
|
+
export const PI_WEB_SESSION_LIVENESS_REGISTRY_KEY = "@agegr/pi-web/session-liveness/v1";
|
|
5
|
+
const PI_WEB_SESSION_LIVENESS_PROTOCOL_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
interface PiWebSessionLivenessProvider {
|
|
8
|
+
name: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
sessionFile?: string;
|
|
11
|
+
isActive(): boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PiWebSessionLivenessRegistry {
|
|
15
|
+
version: typeof PI_WEB_SESSION_LIVENESS_PROTOCOL_VERSION;
|
|
16
|
+
register(provider: PiWebSessionLivenessProvider): () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type SessionLivenessRegistration = Omit<PiWebSessionLivenessProvider, "name">;
|
|
20
|
+
|
|
21
|
+
export interface PiWebSessionLivenessHandle {
|
|
22
|
+
/** True only when the compatible host accepted the provider registration. */
|
|
23
|
+
registered: boolean;
|
|
24
|
+
release: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type LiveWorkState = Pick<SubagentState, "asyncJobs" | "foregroundControls" | "retainedForegroundNestedRoutes">;
|
|
28
|
+
|
|
29
|
+
function resolveRegistry(): PiWebSessionLivenessRegistry | null {
|
|
30
|
+
const value = (globalThis as Record<PropertyKey, unknown>)[Symbol.for(PI_WEB_SESSION_LIVENESS_REGISTRY_KEY)];
|
|
31
|
+
if (!value || typeof value !== "object") return null;
|
|
32
|
+
const registry = value as Partial<PiWebSessionLivenessRegistry>;
|
|
33
|
+
if (registry.version !== PI_WEB_SESSION_LIVENESS_PROTOCOL_VERSION || typeof registry.register !== "function") return null;
|
|
34
|
+
return registry as PiWebSessionLivenessRegistry;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function retainLiveForegroundNestedRoute(state: Pick<SubagentState, "retainedForegroundNestedRoutes">, route: NestedRouteInfo): boolean {
|
|
38
|
+
const nested = projectNestedEvents(route);
|
|
39
|
+
if (!hasLiveNestedDescendants(nested.children)) return false;
|
|
40
|
+
state.retainedForegroundNestedRoutes ??= new Map();
|
|
41
|
+
state.retainedForegroundNestedRoutes.set(route.rootRunId, route);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function hasLiveSubagentWork(state: LiveWorkState): boolean {
|
|
46
|
+
for (const job of state.asyncJobs.values()) {
|
|
47
|
+
if (job.status === "queued" || job.status === "running" || hasLiveNestedDescendants(job.nestedChildren)) return true;
|
|
48
|
+
}
|
|
49
|
+
for (const control of state.foregroundControls.values()) {
|
|
50
|
+
if ((control.schedulingOwners ?? 0) > 0
|
|
51
|
+
|| (control.activeChildren?.size ?? 0) > 0
|
|
52
|
+
|| hasLiveNestedDescendants(control.nestedChildren)) return true;
|
|
53
|
+
}
|
|
54
|
+
return (state.retainedForegroundNestedRoutes?.size ?? 0) > 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function registerPiWebSessionLiveness(registration: SessionLivenessRegistration): PiWebSessionLivenessHandle {
|
|
58
|
+
const registry = resolveRegistry();
|
|
59
|
+
if (!registry) return { registered: false, release: () => {} };
|
|
60
|
+
try {
|
|
61
|
+
const release = registry.register({
|
|
62
|
+
name: "pi-subagents",
|
|
63
|
+
sessionId: registration.sessionId,
|
|
64
|
+
...(registration.sessionFile ? { sessionFile: registration.sessionFile } : {}),
|
|
65
|
+
isActive: registration.isActive,
|
|
66
|
+
});
|
|
67
|
+
if (typeof release === "function") return { registered: true, release };
|
|
68
|
+
console.error("Failed to register pi-web session liveness: host registry returned no release function.");
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("Failed to register pi-web session liveness:", error);
|
|
71
|
+
}
|
|
72
|
+
return { registered: false, release: () => {} };
|
|
73
|
+
}
|
|
@@ -128,34 +128,6 @@ function reasonHeading(reason: SupervisorReason): string {
|
|
|
128
128
|
return "Subagent needs a supervisor decision.";
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
function formatChildMessage(input: {
|
|
132
|
-
reason: SupervisorReason;
|
|
133
|
-
message?: string;
|
|
134
|
-
interview?: unknown;
|
|
135
|
-
runId: string;
|
|
136
|
-
agent: string;
|
|
137
|
-
childIndex: number;
|
|
138
|
-
childTarget?: string;
|
|
139
|
-
}): string {
|
|
140
|
-
const lines = [
|
|
141
|
-
reasonHeading(input.reason),
|
|
142
|
-
`Run: ${input.runId}`,
|
|
143
|
-
`Agent: ${input.agent}`,
|
|
144
|
-
`Child index: ${input.childIndex}`,
|
|
145
|
-
];
|
|
146
|
-
if (input.childTarget) lines.push(`Child intercom target: ${input.childTarget}`);
|
|
147
|
-
lines.push("");
|
|
148
|
-
if (input.message?.trim()) lines.push(input.message.trim());
|
|
149
|
-
if (input.reason === "interview_request") {
|
|
150
|
-
lines.push(
|
|
151
|
-
"",
|
|
152
|
-
"Structured response requested. Reply with JSON, optionally fenced in ```json, matching the requested interview shape.",
|
|
153
|
-
);
|
|
154
|
-
if (input.interview !== undefined) lines.push(JSON.stringify(input.interview, null, "\t"));
|
|
155
|
-
}
|
|
156
|
-
return lines.join("\n").trimEnd();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
131
|
function parseStructuredReply(message: string): { value?: unknown; error?: string } {
|
|
160
132
|
const trimmed = message.trim();
|
|
161
133
|
const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1]?.trim();
|
|
@@ -210,8 +182,8 @@ async function waitForReply(channelDir: string, requestId: string, deadline: num
|
|
|
210
182
|
}
|
|
211
183
|
|
|
212
184
|
async function sendSupervisorRequest(params: ContactSupervisorParams, metadata: ChildSupervisorMetadata, signal?: AbortSignal, toolCallId?: string): Promise<AgentToolResult<Record<string, unknown>>> {
|
|
213
|
-
if (
|
|
214
|
-
throw new Error("message is required for supervisor decisions.");
|
|
185
|
+
if (!params.message?.trim() && params.reason !== "interview_request") {
|
|
186
|
+
throw new Error("message is required for supervisor decisions and progress updates.");
|
|
215
187
|
}
|
|
216
188
|
ensureSupervisorChannelDir(metadata.channelDir);
|
|
217
189
|
const requestId = randomUUID();
|
|
@@ -219,7 +191,7 @@ async function sendSupervisorRequest(params: ContactSupervisorParams, metadata:
|
|
|
219
191
|
const createdAt = Date.now();
|
|
220
192
|
const replyDeadline = createdAt + askTimeoutMs();
|
|
221
193
|
const expiresAt = expectsReply ? replyDeadline : undefined;
|
|
222
|
-
const message =
|
|
194
|
+
const message = params.message?.trim() ?? "";
|
|
223
195
|
const requestToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : undefined;
|
|
224
196
|
const request: SupervisorRequest = {
|
|
225
197
|
type: "subagent.supervisor.request",
|
|
@@ -299,7 +271,7 @@ function parseRequestFile(file: string, channelDir: string): PendingSupervisorRe
|
|
|
299
271
|
if (parsed.type !== "subagent.supervisor.request") return undefined;
|
|
300
272
|
if (typeof parsed.id !== "string" || !parsed.id) return undefined;
|
|
301
273
|
if (parsed.reason !== "need_decision" && parsed.reason !== "interview_request" && parsed.reason !== "progress_update") return undefined;
|
|
302
|
-
if (typeof parsed.message !== "string" || !parsed.message) return undefined;
|
|
274
|
+
if (typeof parsed.message !== "string" || (!parsed.message.trim() && parsed.reason !== "interview_request")) return undefined;
|
|
303
275
|
if (typeof parsed.runId !== "string" || typeof parsed.agent !== "string" || typeof parsed.childIndex !== "number") return undefined;
|
|
304
276
|
return {
|
|
305
277
|
...parsed as SupervisorRequest,
|
|
@@ -520,11 +492,24 @@ function formatPendingLine(request: PendingSupervisorRequest): string {
|
|
|
520
492
|
}
|
|
521
493
|
|
|
522
494
|
function requestVisibleText(request: PendingSupervisorRequest): string {
|
|
523
|
-
const lines = [
|
|
524
|
-
|
|
525
|
-
|
|
495
|
+
const lines = [
|
|
496
|
+
reasonHeading(request.reason),
|
|
497
|
+
`Run: ${request.runId}`,
|
|
498
|
+
`Agent: ${request.agent}`,
|
|
499
|
+
`Child index: ${request.childIndex}`,
|
|
500
|
+
];
|
|
501
|
+
if (request.childTarget) lines.push(`Child intercom target: ${request.childTarget}`);
|
|
502
|
+
lines.push("");
|
|
503
|
+
if (request.message) lines.push(request.message);
|
|
504
|
+
if (request.reason === "interview_request") {
|
|
505
|
+
lines.push(
|
|
506
|
+
"",
|
|
507
|
+
"Structured response requested. Reply with JSON, optionally fenced in ```json, matching the requested interview shape.",
|
|
508
|
+
);
|
|
509
|
+
if (request.interview !== undefined) lines.push(JSON.stringify(request.interview, null, "\t"));
|
|
526
510
|
}
|
|
527
|
-
|
|
511
|
+
if (request.expectsReply) lines.push("", `Reply with: ${supervisorReplyHint(request.id)}`);
|
|
512
|
+
return lines.join("\n").trimEnd();
|
|
528
513
|
}
|
|
529
514
|
|
|
530
515
|
function writeReply(request: PendingSupervisorRequest, message: string): SupervisorReply {
|
|
@@ -759,6 +744,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
759
744
|
childIndex: request.childIndex,
|
|
760
745
|
...(request.childTarget ? { childTarget: request.childTarget } : {}),
|
|
761
746
|
...(request.interview !== undefined ? { interview: request.interview } : {}),
|
|
747
|
+
requestBody: request.message,
|
|
762
748
|
...(request.expectsReply ? { replyHint: supervisorReplyHint(request.id) } : {}),
|
|
763
749
|
},
|
|
764
750
|
}, { triggerTurn: true });
|