pi-subagents 0.65.1 → 0.67.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 +123 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/researcher.md +23 -13
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +20 -3
- package/docs/configuration.md +25 -5
- package/docs/extension-api.md +124 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +59 -2
- package/docs/observability.md +46 -6
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +20 -10
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +40 -19
- package/inspector-runner.mjs +2 -2
- package/package.json +2 -1
- package/prompts/parallel-review.md +1 -1
- package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
- package/skills/pi-subagents/SKILL.md +14 -0
- package/skills/pi-subagents/references/execution-controls.md +20 -5
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +94 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +14 -0
- package/src/agents/builtin-names.ts +1 -0
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +76 -45
- package/src/api/shared-types.ts +3 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +58 -8
- package/src/extension/public-execution.ts +4 -3
- package/src/extension/rpc.ts +8 -21
- package/src/extension/schemas.ts +71 -80
- package/src/extension/tool-description.ts +29 -81
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +104 -67
- package/src/runs/background/active-async-capacity.ts +22 -18
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +44 -30
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +117 -13
- package/src/runs/background/owned-process-tree.ts +35 -8
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +121 -36
- package/src/runs/background/run-status.ts +78 -5
- package/src/runs/background/runner-aliases.ts +28 -9
- package/src/runs/background/runner-child-launch.ts +88 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +458 -239
- package/src/runs/background/subagent-wait.ts +54 -8
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +37 -7
- package/src/runs/foreground/execution.ts +145 -56
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +584 -297
- package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
- package/src/runs/shared/acceptance.ts +7 -4
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +51 -47
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +167 -3
- package/src/runs/shared/child-launch.ts +28 -13
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +3 -1
- package/src/runs/shared/child-session.ts +75 -8
- package/src/runs/shared/child-tool-plan.ts +124 -5
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +6 -3
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +21 -11
- package/src/runs/shared/model-fallback.ts +12 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +7 -1
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +366 -208
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +65 -2
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/types.ts +85 -61
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -1
- package/src/slash/subagents-admin.ts +11 -4
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +27 -19
- package/src/tui/render.ts +172 -33
- package/src/watchdog/child-status.ts +8 -0
- package/src/watchdog/model-selection.ts +20 -0
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +1 -0
- package/src/watchdog/register-main.ts +31 -27
- package/src/watchdog/review.ts +132 -67
- package/src/watchdog/runtime.ts +82 -20
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +9 -3
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +4 -0
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +275 -17
- package/src/workflows/workflow-checklist.ts +13 -17
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import
|
|
2
|
+
import { listBackgroundWorkProviders } from "../../api/background-work.ts";
|
|
3
|
+
import { ReadonlyDrainObservation } from "./readonly-drain-observation.ts";
|
|
4
|
+
import registerFanoutChildSubagentExtension, { createChildSafeState } from "../../extension/fanout-child.ts";
|
|
3
5
|
import registerSubagentFastModeExtension from "./fast-mode-extension.ts";
|
|
4
6
|
import registerSubagentPromptRuntime from "./subagent-prompt-runtime.ts";
|
|
5
7
|
import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
|
|
8
|
+
import type { ChildToolDiagnostic } from "./tool-availability.ts";
|
|
9
|
+
import type { ChildSessionLaunch } from "./child-session.ts";
|
|
10
|
+
import type { ArbiterModelContext } from "./llm-intent-arbiter.ts";
|
|
11
|
+
import type { ChildTranscriptWriter } from "../../shared/child-transcript.ts";
|
|
12
|
+
import { projectRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
|
|
6
13
|
|
|
7
14
|
/** Inline extension shape accepted by pi's resource loader (`extensionFactories`). */
|
|
8
15
|
export interface ChildHookExtension {
|
|
@@ -10,16 +17,173 @@ export interface ChildHookExtension {
|
|
|
10
17
|
factory: (pi: ExtensionAPI) => void;
|
|
11
18
|
}
|
|
12
19
|
|
|
20
|
+
type OwnedCapture = Required<Pick<ChildRuntimeConfig, "toolDiagnostic" | "runtimeAcknowledgements">>;
|
|
21
|
+
type PromptProof = { config: ChildRuntimeConfig; snapshot: string; factories?: ChildHookExtension["factory"][]; observeNext?: ReadonlyDrainObservation; observation?: ReadonlyDrainObservation; capture?: OwnedCapture; reporting?: { launch: ChildSessionLaunch; callback: ChildSessionLaunch["onExtensionError"] } };
|
|
22
|
+
const promptProofs = new WeakMap<ChildHookExtension["factory"], PromptProof>();
|
|
23
|
+
|
|
24
|
+
function ownedProof(hooks: ChildHookExtension[]): PromptProof | undefined {
|
|
25
|
+
const proof = hooks[0] && promptProofs.get(hooks[0].factory);
|
|
26
|
+
return proof?.factories?.length === hooks.length && proof.factories.every((factory, index) => hooks[index]?.factory === factory) ? proof : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The host's mandatory transcript reporting, owned by the existing hook certificate. */
|
|
30
|
+
export function withChildSessionErrorReporting(session: Omit<ChildSessionLaunch, "onExtensionError">, transcriptWriter: ChildTranscriptWriter | undefined): ChildSessionLaunch {
|
|
31
|
+
const launch: ChildSessionLaunch = { ...session, onExtensionError: (error) => {
|
|
32
|
+
transcriptWriter?.writeStderrLine(`Extension error (${error.extensionPath}, ${error.event}): ${error.error instanceof Error ? error.error.message : String(error.error)}`);
|
|
33
|
+
} };
|
|
34
|
+
const proof = ownedProof(launch.hooks);
|
|
35
|
+
if (proof?.capture && proof.config === launch.runtime) proof.reporting = { launch, callback: launch.onExtensionError };
|
|
36
|
+
return launch;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isReadonlyChildSessionReporting(launch: ChildSessionLaunch): boolean {
|
|
40
|
+
const proof = ownedProof(launch.hooks);
|
|
41
|
+
const descriptor = Object.getOwnPropertyDescriptor(launch, "onExtensionError");
|
|
42
|
+
if (descriptor && (!descriptor.enumerable || !("value" in descriptor))) return false;
|
|
43
|
+
return proof?.reporting ? proof.reporting.launch === launch && proof.reporting.callback === descriptor?.value : !descriptor && !("onExtensionError" in launch);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Admit only own enumerable data: inspecting descriptors must not invoke getters. */
|
|
47
|
+
function dataKeys(value: unknown): string[] | undefined {
|
|
48
|
+
if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype) return undefined;
|
|
49
|
+
const keys = Reflect.ownKeys(value);
|
|
50
|
+
for (const key of keys) {
|
|
51
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
|
|
52
|
+
if (typeof key !== "string" || !descriptor.enumerable || !("value" in descriptor)) return undefined;
|
|
53
|
+
}
|
|
54
|
+
return keys as string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Only reviewed data and the capture callbacks constructed below; no caller callback opt-in. */
|
|
58
|
+
function readonlyConfig(config: ChildRuntimeConfig, capture?: OwnedCapture): string | undefined {
|
|
59
|
+
const strings = ["runId", "agent", "sessionName", "forkCacheKey", "parentSessionId", "orchestratorSessionId"];
|
|
60
|
+
const numbers = ["childIndex", "depth", "maxDepth"];
|
|
61
|
+
const booleans = ["inheritProjectContext", "inheritGlobalContext", "inheritSkills"];
|
|
62
|
+
const keys = dataKeys(config);
|
|
63
|
+
if (!keys) return undefined;
|
|
64
|
+
if (capture && (config.toolDiagnostic !== capture.toolDiagnostic || config.runtimeAcknowledgements !== capture.runtimeAcknowledgements)) return undefined;
|
|
65
|
+
const waitKeys = dataKeys(config.waitTool);
|
|
66
|
+
if (!waitKeys || waitKeys.some((key) => key !== "enabled")) return undefined;
|
|
67
|
+
if (config.fast !== false || config.fanoutChild !== false || config.waitTool.enabled !== false) return undefined;
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
const value = Object.getOwnPropertyDescriptor(config, key)!.value;
|
|
70
|
+
if (["fast", "fanoutChild", "waitTool"].includes(key)) continue;
|
|
71
|
+
if (capture && (key === "toolDiagnostic" || key === "runtimeAcknowledgements")) continue;
|
|
72
|
+
if (capture && key === "requiredTools" && Array.isArray(value) && Object.getPrototypeOf(value) === Array.prototype) {
|
|
73
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
74
|
+
if (Reflect.ownKeys(descriptors).length !== value.length + 1) return undefined;
|
|
75
|
+
for (let i = 0; i < value.length; i++) {
|
|
76
|
+
const descriptor = descriptors[String(i)];
|
|
77
|
+
if (!descriptor?.enumerable || !("value" in descriptor) || !["read", "ls"].includes(descriptor.value)) return undefined;
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (strings.includes(key) && (value === undefined || typeof value === "string")) continue;
|
|
82
|
+
if (numbers.includes(key) && (value === undefined || (typeof value === "number" && Number.isInteger(value) && value >= 0))) continue;
|
|
83
|
+
if (booleans.includes(key) && (value === undefined || typeof value === "boolean")) continue;
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
return JSON.stringify(config);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function noBackgroundProviders(): boolean {
|
|
90
|
+
try {
|
|
91
|
+
return listBackgroundWorkProviders().length === 0;
|
|
92
|
+
} catch { return false; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Internal proof of the captured closure/config, not its caller-controlled display name. */
|
|
96
|
+
export function isReadonlyChildHookProfile(hooks: ChildHookExtension[], config: ChildRuntimeConfig): boolean {
|
|
97
|
+
const proof = ownedProof(hooks);
|
|
98
|
+
if (!proof) return false;
|
|
99
|
+
const valid = proof.config === config && readonlyConfig(config, proof.capture) === proof.snapshot && noBackgroundProviders();
|
|
100
|
+
if (!valid) proof.observation?.deny();
|
|
101
|
+
return valid && (proof.observation?.check() ?? true);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Arm just the next installation; ordinary installations retain the original API/handlers. */
|
|
105
|
+
export function observeReadonlyChildHookDrain(hooks: ChildHookExtension[], enabled: boolean, sessionFile: string): void {
|
|
106
|
+
const proof = ownedProof(hooks);
|
|
107
|
+
if (!proof) return;
|
|
108
|
+
if (enabled) {
|
|
109
|
+
proof.observation?.deny();
|
|
110
|
+
proof.observation = undefined;
|
|
111
|
+
}
|
|
112
|
+
proof.observeNext = enabled ? new ReadonlyDrainObservation(sessionFile, () => readonlyConfig(proof.config, proof.capture) === proof.snapshot && noBackgroundProviders()) : undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function captureReadonlyChildDrain(hooks: ChildHookExtension[]): (() => boolean) {
|
|
116
|
+
const proof = ownedProof(hooks);
|
|
117
|
+
const observation = proof?.observation;
|
|
118
|
+
return () => !!observation && proof?.observation === observation && observation.settled();
|
|
119
|
+
}
|
|
120
|
+
|
|
13
121
|
/**
|
|
14
122
|
* The child-side hooks pi-subagents installs in every child, keyed off the
|
|
15
123
|
* launch config. The registrations live in `subagent-prompt-runtime.ts`,
|
|
16
124
|
* `fast-mode-extension.ts`, and `fanout-child.ts`.
|
|
17
125
|
*/
|
|
18
126
|
export function createChildHooks(config: ChildRuntimeConfig): ChildHookExtension[] {
|
|
127
|
+
return childHooks(config);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Launch-owned bookkeeping, paired with the same private hook certificate (no callback registration API). */
|
|
131
|
+
export function createCapturedChildHooks(config: ChildRuntimeConfig, runner = false) {
|
|
132
|
+
let diagnostic: ChildToolDiagnostic | undefined;
|
|
133
|
+
let acknowledgedIds: string[] | undefined;
|
|
134
|
+
let completionIntentContext: ArbiterModelContext | undefined;
|
|
135
|
+
let finalDrainHeld = false;
|
|
136
|
+
const capture: OwnedCapture = {
|
|
137
|
+
toolDiagnostic: (value) => { diagnostic = value; },
|
|
138
|
+
runtimeAcknowledgements: (ids) => { acknowledgedIds = ids; },
|
|
139
|
+
};
|
|
140
|
+
Object.assign(config, capture);
|
|
141
|
+
const hooks = childHooks(config, capture, (held) => { finalDrainHeld = held; });
|
|
142
|
+
if (runner) {
|
|
143
|
+
hooks.push({ name: "pi-subagents:completion-intent", factory: (pi) => pi.on("session_start", (_event, childCtx) => {
|
|
144
|
+
// Retain only attempt model services and the session id string, not the live child session.
|
|
145
|
+
completionIntentContext = {
|
|
146
|
+
model: childCtx.model,
|
|
147
|
+
modelRegistry: childCtx.modelRegistry,
|
|
148
|
+
sessionId: childCtx.sessionManager.getSessionId(),
|
|
149
|
+
};
|
|
150
|
+
}) });
|
|
151
|
+
const proof = promptProofs.get(hooks[0]!.factory);
|
|
152
|
+
if (proof) proof.factories = hooks.map((hook) => hook.factory);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
hooks,
|
|
156
|
+
completionIntentContext: () => completionIntentContext,
|
|
157
|
+
toolDiagnostic: () => diagnostic,
|
|
158
|
+
runtimeAcknowledgedExtensions: () => acknowledgedIds ? projectRuntimeAcknowledgedExtensions(acknowledgedIds) : undefined,
|
|
159
|
+
finalDrainHeld: () => finalDrainHeld,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function childHooks(config: ChildRuntimeConfig, capture?: OwnedCapture, holdFinalDrain?: (held: boolean) => void): ChildHookExtension[] {
|
|
164
|
+
const snapshot = readonlyConfig(config, capture);
|
|
165
|
+
const proof: PromptProof | undefined = snapshot === undefined ? undefined : { config, snapshot, capture };
|
|
166
|
+
const runtime = Object.create(config) as ChildRuntimeConfig;
|
|
167
|
+
const ownedState = Object.getOwnPropertyDescriptor(config, "runtimeState");
|
|
168
|
+
if (!ownedState || !("value" in ownedState) || ownedState.value == null) {
|
|
169
|
+
Object.defineProperty(runtime, "runtimeState", { configurable: true, enumerable: true, writable: true, value: createChildSafeState() });
|
|
170
|
+
}
|
|
171
|
+
if (holdFinalDrain) {
|
|
172
|
+
Object.defineProperty(runtime, "holdFinalDrain", { configurable: true, enumerable: true, writable: true, value: holdFinalDrain });
|
|
173
|
+
}
|
|
19
174
|
const hooks: ChildHookExtension[] = [
|
|
20
|
-
{ name: "pi-subagents:prompt-runtime", factory:
|
|
175
|
+
{ name: "pi-subagents:prompt-runtime", factory: function promptRuntime(pi) {
|
|
176
|
+
if (!proof?.observeNext) return registerSubagentPromptRuntime(pi, runtime);
|
|
177
|
+
proof.observation = proof.observeNext;
|
|
178
|
+
proof.observeNext = undefined;
|
|
179
|
+
registerSubagentPromptRuntime(pi, runtime, proof.observation);
|
|
180
|
+
} },
|
|
21
181
|
];
|
|
182
|
+
if (proof) {
|
|
183
|
+
proof.factories = hooks.map((hook) => hook.factory);
|
|
184
|
+
promptProofs.set(hooks[0]!.factory, proof);
|
|
185
|
+
}
|
|
22
186
|
if (config.fast) hooks.push({ name: "pi-subagents:fast-mode", factory: (pi) => registerSubagentFastModeExtension(pi) });
|
|
23
|
-
if (config.fanoutChild) hooks.push({ name: "pi-subagents:fanout-child", factory: (pi) => registerFanoutChildSubagentExtension(pi,
|
|
187
|
+
if (config.fanoutChild) hooks.push({ name: "pi-subagents:fanout-child", factory: (pi) => registerFanoutChildSubagentExtension(pi, runtime) });
|
|
24
188
|
return hooks;
|
|
25
189
|
}
|
|
@@ -11,7 +11,7 @@ import type { ThinkingLevel } from "../../shared/model-info.ts";
|
|
|
11
11
|
import { intersectThinkingCeilings } from "../../shared/thinking-ceiling.ts";
|
|
12
12
|
import {
|
|
13
13
|
resolveChildDepth,
|
|
14
|
-
type
|
|
14
|
+
type LaunchResolvedChildExtensions,
|
|
15
15
|
type ResolvedToolBudget,
|
|
16
16
|
type RunFanoutBudgetDescriptor,
|
|
17
17
|
} from "../../shared/types.ts";
|
|
@@ -20,8 +20,7 @@ import type { McpRuntimeSnapshotHost } from "./mcp-direct-tool-allowlist.ts";
|
|
|
20
20
|
import type { PermissionRules } from "./permissions.ts";
|
|
21
21
|
import type { StructuredOutputRuntime } from "./structured-output.ts";
|
|
22
22
|
import type { ChildToolDiagnostic } from "./tool-availability.ts";
|
|
23
|
-
import type {
|
|
24
|
-
import { projectRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
|
|
23
|
+
import type { RuntimeAcknowledgedChildExtensions } from "../../shared/types.ts";
|
|
25
24
|
import { encodeExtensionBindings, PI_SUBAGENT_EXTENSION_BINDINGS_ENV, type ExtensionBindings } from "./extension-bindings.ts";
|
|
26
25
|
import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "./capability-ceiling.ts";
|
|
27
26
|
import {
|
|
@@ -32,8 +31,10 @@ import {
|
|
|
32
31
|
type PiLaunchToolPlan,
|
|
33
32
|
} from "./child-tool-plan.ts";
|
|
34
33
|
import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
|
|
35
|
-
import {
|
|
34
|
+
import { createCapturedChildHooks, withChildSessionErrorReporting } from "./child-hooks.ts";
|
|
35
|
+
import type { ChildTranscriptWriter } from "../../shared/child-transcript.ts";
|
|
36
36
|
import type { ChildSessionLaunch, ChildSessionStorage } from "./child-session.ts";
|
|
37
|
+
import type { ArbiterModelContext } from "./llm-intent-arbiter.ts";
|
|
37
38
|
|
|
38
39
|
/** Environment variable pi-mcp-adapter reads for the tools a child may expose. */
|
|
39
40
|
export const MCP_DIRECT_TOOLS_ENV = "MCP_DIRECT_TOOLS";
|
|
@@ -111,12 +112,21 @@ export interface BuildInProcessChildLaunchInput {
|
|
|
111
112
|
* them and exposes the child environment external extensions read.
|
|
112
113
|
*/
|
|
113
114
|
host: "parent" | "runner";
|
|
115
|
+
/**
|
|
116
|
+
* Builtin tool names the host runtime provides. When set, child tool plans
|
|
117
|
+
* intersect declared agent tools with this set, omitting tools the host
|
|
118
|
+
* cannot provide. Review/scout lanes fail closed when a requested,
|
|
119
|
+
* still-permitted repository inspection tool is missing from that set.
|
|
120
|
+
*/
|
|
121
|
+
hostAvailableBuiltins?: readonly string[];
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
export interface InProcessChildCapture {
|
|
125
|
+
completionIntentContext?(): ArbiterModelContext | undefined;
|
|
117
126
|
structuredOutput(): { called: boolean; value?: unknown; acceptanceReport?: unknown; acceptanceReportProvided: boolean };
|
|
118
127
|
toolDiagnostic(): ChildToolDiagnostic | undefined;
|
|
119
|
-
runtimeAcknowledgedExtensions():
|
|
128
|
+
runtimeAcknowledgedExtensions(): RuntimeAcknowledgedChildExtensions | undefined;
|
|
129
|
+
finalDrainHeld(): boolean;
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
export interface InProcessChildLaunch {
|
|
@@ -124,11 +134,16 @@ export interface InProcessChildLaunch {
|
|
|
124
134
|
config: ChildRuntimeConfig;
|
|
125
135
|
session: Omit<ChildSessionLaunch, "onExtensionError">;
|
|
126
136
|
capture: InProcessChildCapture;
|
|
127
|
-
launchResolvedExtensions:
|
|
137
|
+
launchResolvedExtensions: LaunchResolvedChildExtensions;
|
|
128
138
|
warnings: string[];
|
|
129
139
|
capabilityAudit?: SubagentCapabilityAudit;
|
|
130
140
|
}
|
|
131
141
|
|
|
142
|
+
/** Actual host create-input boundary. Evidence remains explicitly opt-in and dormant in production. */
|
|
143
|
+
export function createReportedChildSessionInput(launch: InProcessChildLaunch, transcriptWriter?: ChildTranscriptWriter): ChildSessionLaunch {
|
|
144
|
+
return withChildSessionErrorReporting(launch.session, transcriptWriter);
|
|
145
|
+
}
|
|
146
|
+
|
|
132
147
|
/** Escape XML-significant characters in a string for safe attribute interpolation. */
|
|
133
148
|
function escapeXmlAttr(value: string): string {
|
|
134
149
|
return value
|
|
@@ -185,6 +200,7 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
|
|
|
185
200
|
agentName: input.childAgentName,
|
|
186
201
|
permissionRules: input.permissionRules,
|
|
187
202
|
runtimeSnapshotHost: input.runtimeSnapshotHost,
|
|
203
|
+
hostAvailableBuiltins: input.hostAvailableBuiltins,
|
|
188
204
|
});
|
|
189
205
|
|
|
190
206
|
const inherited = input.inherited;
|
|
@@ -214,8 +230,6 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
|
|
|
214
230
|
let structuredAcceptanceReport: unknown;
|
|
215
231
|
let structuredCalled = false;
|
|
216
232
|
let structuredAcceptanceProvided = false;
|
|
217
|
-
let toolDiagnostic: ChildToolDiagnostic | undefined;
|
|
218
|
-
let acknowledgedIds: string[] | undefined;
|
|
219
233
|
|
|
220
234
|
const config: ChildRuntimeConfig = {
|
|
221
235
|
...(input.runId ? { runId: input.runId } : {}),
|
|
@@ -264,10 +278,9 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
|
|
|
264
278
|
: {}),
|
|
265
279
|
...(toolPlan.requiredChildTools.length > 0 ? { requiredTools: toolPlan.requiredChildTools } : {}),
|
|
266
280
|
...(toolPlan.effectiveMcpTools.length > 0 ? { mcpDirectTools: toolPlan.effectiveMcpTools } : {}),
|
|
267
|
-
toolDiagnostic: (diagnostic) => { toolDiagnostic = diagnostic; },
|
|
268
|
-
runtimeAcknowledgements: (ids) => { acknowledgedIds = ids; },
|
|
269
281
|
fast: input.fast === true,
|
|
270
282
|
};
|
|
283
|
+
const capturedHooks = createCapturedChildHooks(config, input.host === "runner");
|
|
271
284
|
|
|
272
285
|
const extensionPaths = toolPlan.extensionArgs.filter((extensionPath) => !isSubagentRuntimeExtensionPath(extensionPath));
|
|
273
286
|
const ambientExtensions = input.host === "runner" && !toolPlan.disableAmbientExtensions;
|
|
@@ -288,7 +301,7 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
|
|
|
288
301
|
...(!toolPlan.explicitToolAllowlist && toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
|
|
289
302
|
extensionPaths,
|
|
290
303
|
ambientExtensions,
|
|
291
|
-
hooks:
|
|
304
|
+
hooks: capturedHooks.hooks,
|
|
292
305
|
...(input.host === "runner" ? { processEnv: childProcessEnv(input, toolPlan) } : {}),
|
|
293
306
|
runtime: config,
|
|
294
307
|
noSkills: !input.inheritSkills,
|
|
@@ -303,9 +316,11 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
|
|
|
303
316
|
config,
|
|
304
317
|
session,
|
|
305
318
|
capture: {
|
|
319
|
+
completionIntentContext: capturedHooks.completionIntentContext,
|
|
306
320
|
structuredOutput: () => ({ called: structuredCalled, value: structuredValue, acceptanceReport: structuredAcceptanceReport, acceptanceReportProvided: structuredAcceptanceProvided }),
|
|
307
|
-
toolDiagnostic:
|
|
308
|
-
runtimeAcknowledgedExtensions:
|
|
321
|
+
toolDiagnostic: capturedHooks.toolDiagnostic,
|
|
322
|
+
runtimeAcknowledgedExtensions: capturedHooks.runtimeAcknowledgedExtensions,
|
|
323
|
+
finalDrainHeld: capturedHooks.finalDrainHeld,
|
|
309
324
|
},
|
|
310
325
|
launchResolvedExtensions,
|
|
311
326
|
warnings: toolPlan.warnings,
|
|
@@ -14,11 +14,14 @@ export function projectChildLifecycle(event: { type?: string; willRetry?: unknow
|
|
|
14
14
|
if (state) state.compactionRetryActive = event.willRetry === true;
|
|
15
15
|
return event.willRetry === true ? "cancel-drain" : "none";
|
|
16
16
|
}
|
|
17
|
-
if (event.type === "agent_start" || event.type === "auto_retry_start") {
|
|
17
|
+
if (event.type === "agent_start" || event.type === "auto_retry_start" || event.type === "turn_start") {
|
|
18
18
|
if (state) state.compactionRetryActive = false;
|
|
19
|
+
return "cancel-drain";
|
|
20
|
+
}
|
|
21
|
+
if (event.type === "agent_end") {
|
|
22
|
+
if (event.willRetry !== true && state) state.compactionRetryActive = false;
|
|
23
|
+
return "cancel-drain";
|
|
19
24
|
}
|
|
20
|
-
if (event.type === "agent_end" && event.willRetry === true) return "cancel-drain";
|
|
21
|
-
if (event.type === "agent_end" && state) state.compactionRetryActive = false;
|
|
22
25
|
if (event.type === "agent_settled") return state?.compactionRetryActive ? "none" : "start-drain";
|
|
23
26
|
if (terminalAssistantStop) return "start-drain";
|
|
24
27
|
return "none";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { JsonSchemaObject, ResolvedToolBudget, RunFanoutBudgetDescriptor } from "../../shared/types.ts";
|
|
1
|
+
import type { JsonSchemaObject, ResolvedToolBudget, RunFanoutBudgetDescriptor, SubagentState } from "../../shared/types.ts";
|
|
2
2
|
import type { ThinkingLevel } from "../../shared/model-info.ts";
|
|
3
3
|
import type { NestedPathEntry } from "./nested-path.ts";
|
|
4
4
|
import type { PermissionRules } from "./permissions.ts";
|
|
@@ -86,6 +86,8 @@ export interface ChildRuntimeConfig {
|
|
|
86
86
|
/** Receives child watchdog status events. */
|
|
87
87
|
watchdogStatus?: (event: ChildWatchdogStatusEvent) => void;
|
|
88
88
|
waitTool: ResolvedWaitToolConfig;
|
|
89
|
+
runtimeState?: SubagentState;
|
|
90
|
+
holdFinalDrain?: (held: boolean) => void;
|
|
89
91
|
structuredOutput?: ChildStructuredOutput;
|
|
90
92
|
requiredTools?: string[];
|
|
91
93
|
mcpDirectTools?: string[];
|
|
@@ -12,6 +12,14 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
|
12
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { getAgentDir } from "../../shared/utils.ts";
|
|
14
14
|
import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
|
|
15
|
+
import { prepareReadonlySessionEvidence } from "./readonly-session-evidence.ts";
|
|
16
|
+
import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
|
|
17
|
+
|
|
18
|
+
// Private runtime authority for host continuation planning; injected factories have none.
|
|
19
|
+
const readonlyModels = new WeakMap<ChildSession, { current: ModelInfo; resolve(reference: string): ModelInfo | undefined; requestBytes: number }>();
|
|
20
|
+
export function getReadonlyChildModels(child: ChildSession) {
|
|
21
|
+
return readonlyModels.get(child);
|
|
22
|
+
}
|
|
15
23
|
|
|
16
24
|
export interface ChildSessionEvent {
|
|
17
25
|
type: string;
|
|
@@ -85,6 +93,8 @@ export interface ChildSession {
|
|
|
85
93
|
abort(): Promise<void>;
|
|
86
94
|
/** Emits `session_shutdown` to the child's extensions and disposes the session; resolves once that shutdown work is done. */
|
|
87
95
|
dispose(): Promise<void>;
|
|
96
|
+
/** True while Pi still has steering or follow-up input that has not started a turn. */
|
|
97
|
+
hasQueuedMessages?(): boolean;
|
|
88
98
|
readonly messages: readonly AgentMessage[];
|
|
89
99
|
readonly sessionFile: string | undefined;
|
|
90
100
|
readonly sessionId: string;
|
|
@@ -95,6 +105,14 @@ export interface ChildSession {
|
|
|
95
105
|
shutDown?: boolean;
|
|
96
106
|
}
|
|
97
107
|
|
|
108
|
+
export function childSessionHasQueuedMessages(session: ChildSession | undefined): boolean {
|
|
109
|
+
try {
|
|
110
|
+
return session?.hasQueuedMessages?.() === true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
export interface ChildSessionFactory {
|
|
99
117
|
create(launch: ChildSessionLaunch): Promise<ChildSession>;
|
|
100
118
|
/** Abort and dispose every live attached child; detached children keep running and hold the shared runtime. */
|
|
@@ -116,6 +134,21 @@ export interface DefaultChildSessionFactoryOptions {
|
|
|
116
134
|
|
|
117
135
|
type ModelRuntimeInstance = Awaited<ReturnType<PiCodingAgentModule["ModelRuntime"]["create"]>>;
|
|
118
136
|
|
|
137
|
+
const CHILD_PROMPT_RUNTIME_EXTENSION_PATH = "<inline:pi-subagents:prompt-runtime>";
|
|
138
|
+
|
|
139
|
+
/** The prompt runtime filters parent-only context before ambient extensions inspect
|
|
140
|
+
* the child prompt. Other inline hooks keep their normal position after ambient
|
|
141
|
+
* extensions, and ambient extension order stays unchanged. */
|
|
142
|
+
function prioritizeChildPromptRuntime<T extends { extensions: Array<{ path: string }> }>(result: T): T {
|
|
143
|
+
const index = result.extensions.findIndex(({ path }) => path === CHILD_PROMPT_RUNTIME_EXTENSION_PATH);
|
|
144
|
+
if (index <= 0) return result;
|
|
145
|
+
const extensions = [...result.extensions];
|
|
146
|
+
const [promptRuntime] = extensions.splice(index, 1);
|
|
147
|
+
if (!promptRuntime) return result;
|
|
148
|
+
extensions.unshift(promptRuntime);
|
|
149
|
+
return { ...result, extensions };
|
|
150
|
+
}
|
|
151
|
+
|
|
119
152
|
/** One launch at a time from env application through `session_start`, so parallel launches never observe each other's `processEnv` while their extensions load and start. */
|
|
120
153
|
let loading: Promise<unknown> = Promise.resolve();
|
|
121
154
|
|
|
@@ -185,12 +218,17 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
185
218
|
};
|
|
186
219
|
return {
|
|
187
220
|
async create(launch) {
|
|
221
|
+
const observeReadonly = prepareReadonlySessionEvidence(launch);
|
|
188
222
|
const pi = await loadPiCodingAgent();
|
|
189
223
|
const modelRuntime = await sharedRuntime(pi);
|
|
190
224
|
const agentDir = getAgentDir();
|
|
191
225
|
const settingsManager = pi.SettingsManager.create(launch.cwd, agentDir);
|
|
192
|
-
//
|
|
193
|
-
|
|
226
|
+
// Foreground children share Pi's global theme with the parent, so reinitializing it
|
|
227
|
+
// would overwrite the parent's active light/dark appearance. Detached runners have
|
|
228
|
+
// no initialized theme and must initialize one for headless extension renderers.
|
|
229
|
+
const themeKey = Symbol.for("@earendil-works/pi-coding-agent:theme");
|
|
230
|
+
const themeInitialized = Boolean((globalThis as Record<symbol, unknown>)[themeKey]);
|
|
231
|
+
if (!themeInitialized && typeof pi.initTheme === "function") pi.initTheme(settingsManager.getTheme());
|
|
194
232
|
const loader = new pi.DefaultResourceLoader({
|
|
195
233
|
cwd: launch.cwd,
|
|
196
234
|
agentDir,
|
|
@@ -202,14 +240,18 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
202
240
|
noContextFiles: launch.noContextFiles,
|
|
203
241
|
additionalExtensionPaths: launch.extensionPaths,
|
|
204
242
|
extensionFactories: launch.hooks,
|
|
243
|
+
extensionsOverride: prioritizeChildPromptRuntime,
|
|
205
244
|
...(launch.systemPrompt !== undefined ? { systemPrompt: launch.systemPrompt } : {}),
|
|
206
245
|
...(launch.appendSystemPrompt !== undefined ? { appendSystemPrompt: [launch.appendSystemPrompt] } : {}),
|
|
207
246
|
});
|
|
208
247
|
const open = async () => {
|
|
209
248
|
applyProcessEnv(launch.processEnv);
|
|
210
249
|
if (!resetExtensionCacheOnReload(loader) && (launch.ambientExtensions || launch.extensionPaths.length)) launch.onExtensionError?.({ extensionPath: "<loader>", event: "load", error: new Error("pi's extension cache reset is unavailable; extensions loaded into this child share module state with other sessions in this process.") });
|
|
211
|
-
|
|
250
|
+
observeReadonly?.loadingHooks(true);
|
|
251
|
+
try { await loader.reload(); } finally { observeReadonly?.loadingHooks(false); }
|
|
212
252
|
await flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError);
|
|
253
|
+
// No await between receipt validation and the SDK's permissive file open.
|
|
254
|
+
observeReadonly?.beforeOpen();
|
|
213
255
|
const sessionManager = launch.storage.kind === "file"
|
|
214
256
|
? pi.SessionManager.open(launch.storage.sessionFile, undefined, launch.cwd)
|
|
215
257
|
: launch.storage.kind === "dir"
|
|
@@ -217,6 +259,7 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
217
259
|
: launch.storage.kind === "memory"
|
|
218
260
|
? pi.SessionManager.inMemory(launch.cwd)
|
|
219
261
|
: pi.SessionManager.create(launch.cwd);
|
|
262
|
+
observeReadonly?.opened(sessionManager);
|
|
220
263
|
const resolvedModel = launch.model
|
|
221
264
|
? pi.resolveCliModel({ cliModel: launch.model, modelRuntime })
|
|
222
265
|
: undefined;
|
|
@@ -248,6 +291,9 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
248
291
|
const opened = loading.catch(() => {}).then(open);
|
|
249
292
|
loading = opened;
|
|
250
293
|
const session = await opened;
|
|
294
|
+
let evidence: ReturnType<NonNullable<typeof observeReadonly>["observe"]>;
|
|
295
|
+
try { evidence = observeReadonly?.observe(pi, modelRuntime, session); }
|
|
296
|
+
catch (error) { session.dispose(); throw error; }
|
|
251
297
|
let pending: Promise<void> | undefined;
|
|
252
298
|
// pi's own hosts emit `session_shutdown` before disposing a session so the
|
|
253
299
|
// extensions loaded into it (ambient extensions included) release their
|
|
@@ -255,19 +301,30 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
255
301
|
const shutdown = async (): Promise<void> => {
|
|
256
302
|
try {
|
|
257
303
|
const runner = session.extensionRunner;
|
|
258
|
-
if (runner.hasHandlers("session_shutdown"))
|
|
304
|
+
if (runner.hasHandlers("session_shutdown")) {
|
|
305
|
+
evidence?.beforeShutdown();
|
|
306
|
+
const settled = await Promise.race([runner.emit({ type: "session_shutdown", reason: "quit" }).then(() => true), new Promise<false>((resolve) => setTimeout(() => resolve(false), shutdownTimeoutMs).unref?.())]);
|
|
307
|
+
if (!settled) evidence?.invalidate();
|
|
308
|
+
}
|
|
259
309
|
} catch (error) {
|
|
310
|
+
evidence?.invalidate();
|
|
260
311
|
launch.onExtensionError?.({ extensionPath: "<session>", event: "session_shutdown", error });
|
|
261
312
|
} finally {
|
|
262
313
|
session.dispose();
|
|
314
|
+
evidence?.finish(child);
|
|
263
315
|
}
|
|
264
316
|
};
|
|
265
317
|
const child: ChildSession = {
|
|
266
318
|
subscribe: (listener) => session.subscribe((event) => listener(event as unknown as ChildSessionEvent)),
|
|
267
|
-
prompt: (text) =>
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
319
|
+
prompt: (text) => {
|
|
320
|
+
if (!evidence) return session.prompt(text);
|
|
321
|
+
try { evidence.start(); } catch (error) { return Promise.reject(error); }
|
|
322
|
+
return session.prompt(text).then(() => evidence?.settled(), (error) => { evidence?.invalidate(); throw error; });
|
|
323
|
+
},
|
|
324
|
+
steer: (text) => { evidence?.invalidate(); return session.steer(text); },
|
|
325
|
+
followUp: (text) => { evidence?.invalidate(); return session.followUp(text); },
|
|
326
|
+
abort: () => { evidence?.invalidate(); return session.abort(); },
|
|
327
|
+
hasQueuedMessages: () => session.agent?.hasQueuedMessages?.() === true,
|
|
271
328
|
dispose: () => {
|
|
272
329
|
if (!pending) {
|
|
273
330
|
live.delete(child);
|
|
@@ -283,6 +340,16 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
283
340
|
get sessionId() { return session.sessionId; },
|
|
284
341
|
get modelId() { return session.model ? `${session.model.provider}/${session.model.id}` : undefined; },
|
|
285
342
|
};
|
|
343
|
+
if (evidence && session.model) readonlyModels.set(child, {
|
|
344
|
+
current: toModelInfo(session.model),
|
|
345
|
+
requestBytes: Buffer.byteLength(session.systemPrompt) + Buffer.byteLength(JSON.stringify(session.agent.state.tools)),
|
|
346
|
+
resolve(reference) {
|
|
347
|
+
try {
|
|
348
|
+
const resolved = pi.resolveCliModel({ cliModel: reference, modelRuntime });
|
|
349
|
+
return !resolved.error && resolved.model ? toModelInfo(resolved.model) : undefined;
|
|
350
|
+
} catch { return undefined; }
|
|
351
|
+
},
|
|
352
|
+
});
|
|
286
353
|
live.add(child);
|
|
287
354
|
return child;
|
|
288
355
|
},
|