pi-subagents 0.66.0 → 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 +68 -0
- package/README.md +4 -3
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +6 -3
- package/docs/configuration.md +7 -5
- package/docs/extension-api.md +33 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +1 -1
- package/docs/observability.md +4 -4
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +18 -8
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +26 -12
- package/inspector-runner.mjs +2 -2
- package/package.json +1 -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 +7 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agents.ts +6 -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 +2 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +20 -8
- package/src/extension/public-execution.ts +4 -2
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +67 -78
- package/src/extension/tool-description.ts +29 -82
- 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 +22 -13
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/auto-drain.ts +6 -3
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +31 -1
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/run-child-session.ts +61 -4
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +11 -3
- package/src/runs/background/runner-child-launch.ts +2 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +47 -31
- package/src/runs/background/subagent-wait.ts +51 -8
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/execution.ts +44 -31
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +110 -100
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +5 -2
- package/src/runs/shared/async-status-projection.ts +4 -0
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch.ts +12 -2
- 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 +33 -2
- package/src/runs/shared/child-tool-plan.ts +122 -3
- package/src/runs/shared/completion-guard.ts +5 -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/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-fallback.ts +2 -0
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +9 -3
- 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.ts +42 -12
- 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 +4 -1
- 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 +16 -14
- package/src/tui/render.ts +149 -28
- 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/scripted-workflow.ts +237 -7
- package/src/workflows/workflow-checklist.ts +2 -2
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
9
|
import type { ChildWatchdogConfig, ChildWatchdogStatusEvent } from "../../watchdog/child-status.ts";
|
|
11
10
|
import type { ThinkingLevel } from "../../shared/model-info.ts";
|
|
12
11
|
import { intersectThinkingCeilings } from "../../shared/thinking-ceiling.ts";
|
|
@@ -35,6 +34,7 @@ import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
|
|
|
35
34
|
import { createCapturedChildHooks, withChildSessionErrorReporting } from "./child-hooks.ts";
|
|
36
35
|
import type { ChildTranscriptWriter } from "../../shared/child-transcript.ts";
|
|
37
36
|
import type { ChildSessionLaunch, ChildSessionStorage } from "./child-session.ts";
|
|
37
|
+
import type { ArbiterModelContext } from "./llm-intent-arbiter.ts";
|
|
38
38
|
|
|
39
39
|
/** Environment variable pi-mcp-adapter reads for the tools a child may expose. */
|
|
40
40
|
export const MCP_DIRECT_TOOLS_ENV = "MCP_DIRECT_TOOLS";
|
|
@@ -112,13 +112,21 @@ export interface BuildInProcessChildLaunchInput {
|
|
|
112
112
|
* them and exposes the child environment external extensions read.
|
|
113
113
|
*/
|
|
114
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[];
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
export interface InProcessChildCapture {
|
|
118
|
-
completionIntentContext?():
|
|
125
|
+
completionIntentContext?(): ArbiterModelContext | undefined;
|
|
119
126
|
structuredOutput(): { called: boolean; value?: unknown; acceptanceReport?: unknown; acceptanceReportProvided: boolean };
|
|
120
127
|
toolDiagnostic(): ChildToolDiagnostic | undefined;
|
|
121
128
|
runtimeAcknowledgedExtensions(): RuntimeAcknowledgedChildExtensions | undefined;
|
|
129
|
+
finalDrainHeld(): boolean;
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
export interface InProcessChildLaunch {
|
|
@@ -192,6 +200,7 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
|
|
|
192
200
|
agentName: input.childAgentName,
|
|
193
201
|
permissionRules: input.permissionRules,
|
|
194
202
|
runtimeSnapshotHost: input.runtimeSnapshotHost,
|
|
203
|
+
hostAvailableBuiltins: input.hostAvailableBuiltins,
|
|
195
204
|
});
|
|
196
205
|
|
|
197
206
|
const inherited = input.inherited;
|
|
@@ -311,6 +320,7 @@ export function buildInProcessChildLaunch(input: BuildInProcessChildLaunchInput)
|
|
|
311
320
|
structuredOutput: () => ({ called: structuredCalled, value: structuredValue, acceptanceReport: structuredAcceptanceReport, acceptanceReportProvided: structuredAcceptanceProvided }),
|
|
312
321
|
toolDiagnostic: capturedHooks.toolDiagnostic,
|
|
313
322
|
runtimeAcknowledgedExtensions: capturedHooks.runtimeAcknowledgedExtensions,
|
|
323
|
+
finalDrainHeld: capturedHooks.finalDrainHeld,
|
|
314
324
|
},
|
|
315
325
|
launchResolvedExtensions,
|
|
316
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[];
|
|
@@ -93,6 +93,8 @@ export interface ChildSession {
|
|
|
93
93
|
abort(): Promise<void>;
|
|
94
94
|
/** Emits `session_shutdown` to the child's extensions and disposes the session; resolves once that shutdown work is done. */
|
|
95
95
|
dispose(): Promise<void>;
|
|
96
|
+
/** True while Pi still has steering or follow-up input that has not started a turn. */
|
|
97
|
+
hasQueuedMessages?(): boolean;
|
|
96
98
|
readonly messages: readonly AgentMessage[];
|
|
97
99
|
readonly sessionFile: string | undefined;
|
|
98
100
|
readonly sessionId: string;
|
|
@@ -103,6 +105,14 @@ export interface ChildSession {
|
|
|
103
105
|
shutDown?: boolean;
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
export function childSessionHasQueuedMessages(session: ChildSession | undefined): boolean {
|
|
109
|
+
try {
|
|
110
|
+
return session?.hasQueuedMessages?.() === true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
106
116
|
export interface ChildSessionFactory {
|
|
107
117
|
create(launch: ChildSessionLaunch): Promise<ChildSession>;
|
|
108
118
|
/** Abort and dispose every live attached child; detached children keep running and hold the shared runtime. */
|
|
@@ -124,6 +134,21 @@ export interface DefaultChildSessionFactoryOptions {
|
|
|
124
134
|
|
|
125
135
|
type ModelRuntimeInstance = Awaited<ReturnType<PiCodingAgentModule["ModelRuntime"]["create"]>>;
|
|
126
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
|
+
|
|
127
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. */
|
|
128
153
|
let loading: Promise<unknown> = Promise.resolve();
|
|
129
154
|
|
|
@@ -198,8 +223,12 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
198
223
|
const modelRuntime = await sharedRuntime(pi);
|
|
199
224
|
const agentDir = getAgentDir();
|
|
200
225
|
const settingsManager = pi.SettingsManager.create(launch.cwd, agentDir);
|
|
201
|
-
//
|
|
202
|
-
|
|
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());
|
|
203
232
|
const loader = new pi.DefaultResourceLoader({
|
|
204
233
|
cwd: launch.cwd,
|
|
205
234
|
agentDir,
|
|
@@ -211,6 +240,7 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
211
240
|
noContextFiles: launch.noContextFiles,
|
|
212
241
|
additionalExtensionPaths: launch.extensionPaths,
|
|
213
242
|
extensionFactories: launch.hooks,
|
|
243
|
+
extensionsOverride: prioritizeChildPromptRuntime,
|
|
214
244
|
...(launch.systemPrompt !== undefined ? { systemPrompt: launch.systemPrompt } : {}),
|
|
215
245
|
...(launch.appendSystemPrompt !== undefined ? { appendSystemPrompt: [launch.appendSystemPrompt] } : {}),
|
|
216
246
|
});
|
|
@@ -294,6 +324,7 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
294
324
|
steer: (text) => { evidence?.invalidate(); return session.steer(text); },
|
|
295
325
|
followUp: (text) => { evidence?.invalidate(); return session.followUp(text); },
|
|
296
326
|
abort: () => { evidence?.invalidate(); return session.abort(); },
|
|
327
|
+
hasQueuedMessages: () => session.agent?.hasQueuedMessages?.() === true,
|
|
297
328
|
dispose: () => {
|
|
298
329
|
if (!pending) {
|
|
299
330
|
live.delete(child);
|
|
@@ -7,6 +7,7 @@ import { createHash } from "node:crypto";
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
11
|
import {
|
|
11
12
|
formatUnresolvedMcpDirectToolSelectors,
|
|
12
13
|
resolveMcpDirectToolResolution,
|
|
@@ -60,6 +61,41 @@ const FAST_MODE_ALLOWED_MODELS = new Set([
|
|
|
60
61
|
"openai-codex/gpt-5.6-sol",
|
|
61
62
|
]);
|
|
62
63
|
const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
64
|
+
const PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"]);
|
|
65
|
+
const REPOSITORY_INSPECTION_TOOLS = new Set(["read", "grep", "find", "ls", "bash", "powershell"]);
|
|
66
|
+
const REVIEW_OR_SCOUT_AGENT_PATTERN = /\b(?:reviewer|scout)\b/i;
|
|
67
|
+
// These providers come from child hooks, not the host's builtin tool registry.
|
|
68
|
+
const NATIVE_COORDINATION_TOOL_NAMES = new Set(["subagent", "contact_supervisor", "subagent_supervisor"]);
|
|
69
|
+
|
|
70
|
+
export function isReviewOrScoutLaneAgent(agentName: string | undefined): boolean {
|
|
71
|
+
return typeof agentName === "string" && REVIEW_OR_SCOUT_AGENT_PATTERN.test(agentName);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function missingPermittedRepositoryInspectionTools(
|
|
75
|
+
unavailableHostBuiltins: readonly string[],
|
|
76
|
+
excludeTools: readonly string[] = [],
|
|
77
|
+
): string[] {
|
|
78
|
+
const excluded = new Set(excludeTools);
|
|
79
|
+
return unavailableHostBuiltins.filter((tool) => REPOSITORY_INSPECTION_TOOLS.has(tool) && !excluded.has(tool));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function formatReviewLaneToolContractFailure(input: {
|
|
83
|
+
agentName?: string;
|
|
84
|
+
missingTools: readonly string[];
|
|
85
|
+
requestedTools?: readonly string[];
|
|
86
|
+
effectiveTools: readonly string[];
|
|
87
|
+
ceilingSources?: readonly string[];
|
|
88
|
+
excludeTools?: readonly string[];
|
|
89
|
+
}): string {
|
|
90
|
+
const subject = input.agentName ? `Agent '${input.agentName}'` : "Subagent";
|
|
91
|
+
return [
|
|
92
|
+
`${subject}: tool contract could not be satisfied; host runtime does not provide permitted required repository tools [${input.missingTools.join(", ")}].`,
|
|
93
|
+
`Requested tool names: ${input.requestedTools ? `[${input.requestedTools.join(", ")}]` : "not explicitly specified"}; effective tool allowlist: [${input.effectiveTools.join(", ")}].`,
|
|
94
|
+
...(input.ceilingSources?.length ? [`Active capability ceiling sources: [${input.ceilingSources.join(", ")}].`] : []),
|
|
95
|
+
...(input.excludeTools?.length ? [`Explicit excludeTools: [${input.excludeTools.join(", ")}].`] : []),
|
|
96
|
+
"This is a lane infrastructure failure, not a completed review/scout result.",
|
|
97
|
+
].join(" ");
|
|
98
|
+
}
|
|
63
99
|
|
|
64
100
|
export function deriveForkPromptCacheKey(parentSessionId: string | undefined): string | undefined {
|
|
65
101
|
const parent = parentSessionId?.trim();
|
|
@@ -151,6 +187,15 @@ export interface ResolvePiLaunchToolPlanInput {
|
|
|
151
187
|
agentName?: string;
|
|
152
188
|
permissionRules?: PermissionRules;
|
|
153
189
|
runtimeSnapshotHost?: McpRuntimeSnapshotHost;
|
|
190
|
+
/**
|
|
191
|
+
* When provided, child tool plans intersect declared builtin tools with
|
|
192
|
+
* this set. Tools the agent declares but the host does not provide are
|
|
193
|
+
* omitted with a non-fatal warning (tracked in `unavailableHostBuiltins`).
|
|
194
|
+
* Review/scout lanes fail closed when a requested, still-permitted
|
|
195
|
+
* repository inspection tool is among those host omissions. Intentionally
|
|
196
|
+
* empty or ceiling-restricted allowlists are not a minimum-tool contract.
|
|
197
|
+
*/
|
|
198
|
+
hostAvailableBuiltins?: readonly string[];
|
|
154
199
|
}
|
|
155
200
|
|
|
156
201
|
export interface PiLaunchToolPlan {
|
|
@@ -174,6 +219,8 @@ export interface PiLaunchToolPlan {
|
|
|
174
219
|
capabilityAudit?: SubagentCapabilityAudit;
|
|
175
220
|
/** Non-fatal launch warnings; they do not change behavior. */
|
|
176
221
|
warnings: string[];
|
|
222
|
+
/** Builtin tools the agent declared but the host runtime does not provide. */
|
|
223
|
+
unavailableHostBuiltins: string[];
|
|
177
224
|
}
|
|
178
225
|
|
|
179
226
|
function extensionIdentifier(value: string): string {
|
|
@@ -289,6 +336,31 @@ export function resolvePermissionSystemExtension(): string | undefined {
|
|
|
289
336
|
return undefined;
|
|
290
337
|
}
|
|
291
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Extract the names of builtin tools the host provides. Use this to pass
|
|
341
|
+
* `hostAvailableBuiltins` to `resolvePiLaunchToolPlan` so child tool plans
|
|
342
|
+
* intersect declared agent tools with what the host actually supports.
|
|
343
|
+
*
|
|
344
|
+
* Returns `undefined` when builtin tool discovery fails or yields nothing,
|
|
345
|
+
* so callers skip the intersection (fail-safe to allowing all declared tools).
|
|
346
|
+
* This handles test mocks without proper tool registration and hosts whose
|
|
347
|
+
* getAllTools() throws before extensions load.
|
|
348
|
+
*/
|
|
349
|
+
export function getHostBuiltinToolNames(pi: Pick<ExtensionAPI, "getAllTools">): string[] | undefined {
|
|
350
|
+
try {
|
|
351
|
+
const builtins = pi
|
|
352
|
+
.getAllTools()
|
|
353
|
+
.filter((tool) => {
|
|
354
|
+
const source = (tool.sourceInfo as { source?: string } | undefined)?.source;
|
|
355
|
+
return source === "builtin" || (source === "auto" && PI_BUILTIN_TOOL_NAMES.has(tool.name));
|
|
356
|
+
})
|
|
357
|
+
.map((tool) => tool.name);
|
|
358
|
+
return builtins.length > 0 ? builtins : undefined;
|
|
359
|
+
} catch {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
292
364
|
export function resolvePiLaunchToolPlan(
|
|
293
365
|
input: ResolvePiLaunchToolPlanInput,
|
|
294
366
|
): PiLaunchToolPlan {
|
|
@@ -300,17 +372,27 @@ export function resolvePiLaunchToolPlan(
|
|
|
300
372
|
capabilityCeiling?.allowedTools === undefined
|
|
301
373
|
? undefined
|
|
302
374
|
: new Set(capabilityCeiling.allowedTools);
|
|
375
|
+
const hostAvailableSet =
|
|
376
|
+
input.hostAvailableBuiltins === undefined
|
|
377
|
+
? undefined
|
|
378
|
+
: new Set(input.hostAvailableBuiltins);
|
|
303
379
|
const requestedBuiltinTools =
|
|
304
380
|
input.tools?.filter(
|
|
305
381
|
(tool) =>
|
|
306
382
|
!(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")),
|
|
307
383
|
) ?? [];
|
|
384
|
+
if (input.requireReadTool && hostAvailableSet && !hostAvailableSet.has("read")) {
|
|
385
|
+
const agentLabel = input.agentName ? ` for agent '${input.agentName}'` : "";
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Host runtime does not provide required tool 'read'${agentLabel} for lazy skill loading.`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
308
390
|
if (input.requireReadTool && allowedToolSet && !allowedToolSet.has("read")) {
|
|
309
391
|
throw new Error(
|
|
310
392
|
`Capability ceiling from ${capabilityCeiling?.sources.join(", ") || "unknown source"} excludes required tool 'read' for lazy skill loading.`,
|
|
311
393
|
);
|
|
312
394
|
}
|
|
313
|
-
const
|
|
395
|
+
const ceilingFilteredBuiltinTools =
|
|
314
396
|
input.tools === undefined
|
|
315
397
|
? allowedToolSet
|
|
316
398
|
? [...allowedToolSet]
|
|
@@ -322,6 +404,12 @@ export function resolvePiLaunchToolPlan(
|
|
|
322
404
|
? ["read", ...requestedBuiltinTools]
|
|
323
405
|
: requestedBuiltinTools
|
|
324
406
|
).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
|
|
407
|
+
const declaredBuiltinTools = hostAvailableSet
|
|
408
|
+
? ceilingFilteredBuiltinTools.filter((tool) => hostAvailableSet.has(tool) || NATIVE_COORDINATION_TOOL_NAMES.has(tool))
|
|
409
|
+
: ceilingFilteredBuiltinTools;
|
|
410
|
+
const unavailableHostBuiltins = hostAvailableSet
|
|
411
|
+
? ceilingFilteredBuiltinTools.filter((tool) => !hostAvailableSet.has(tool) && !NATIVE_COORDINATION_TOOL_NAMES.has(tool))
|
|
412
|
+
: [];
|
|
325
413
|
const excludeTools = [...new Set((input.excludeTools ?? []).map((tool) => tool.trim()).filter(Boolean))];
|
|
326
414
|
const excludedToolSet = new Set(excludeTools);
|
|
327
415
|
const effectiveDeclaredBuiltinTools = declaredBuiltinTools.filter((tool) => !excludedToolSet.has(tool));
|
|
@@ -330,6 +418,9 @@ export function resolvePiLaunchToolPlan(
|
|
|
330
418
|
!excludedToolSet.has("subagent") &&
|
|
331
419
|
(!allowedToolSet || allowedToolSet.has("subagent"))
|
|
332
420
|
);
|
|
421
|
+
if (effectiveDeclaredBuiltinTools.includes("subagent_supervisor") && !fanoutAuthorized) {
|
|
422
|
+
throw new Error("Tool 'subagent_supervisor' requires fanout authorization: include 'subagent' in the effective tools allowlist or enable allowNestedSubagents.");
|
|
423
|
+
}
|
|
333
424
|
const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
|
|
334
425
|
? []
|
|
335
426
|
: (input.tools ?? []).filter(
|
|
@@ -370,8 +461,8 @@ export function resolvePiLaunchToolPlan(
|
|
|
370
461
|
...internalTools,
|
|
371
462
|
]),
|
|
372
463
|
];
|
|
373
|
-
//
|
|
374
|
-
//
|
|
464
|
+
// Upward contact stays in the --tools allowlist but is not a strict
|
|
465
|
+
// requirement: children register contact_supervisor at runtime through
|
|
375
466
|
// the native supervisor channel (or pi-intercom). The pre-0.50 bridge always
|
|
376
467
|
// appended intercom alongside contact_supervisor, so that exact pairing is
|
|
377
468
|
// legacy plumbing, not a user demand for an external intercom provider;
|
|
@@ -437,6 +528,32 @@ export function resolvePiLaunchToolPlan(
|
|
|
437
528
|
]),
|
|
438
529
|
]
|
|
439
530
|
: undefined;
|
|
531
|
+
const missingPermittedRepositoryTools = input.tools !== undefined
|
|
532
|
+
? missingPermittedRepositoryInspectionTools(unavailableHostBuiltins, excludeTools)
|
|
533
|
+
: [];
|
|
534
|
+
if (missingPermittedRepositoryTools.length > 0 && isReviewOrScoutLaneAgent(input.agentName)) {
|
|
535
|
+
throw new Error(formatReviewLaneToolContractFailure({
|
|
536
|
+
agentName: input.agentName,
|
|
537
|
+
missingTools: missingPermittedRepositoryTools,
|
|
538
|
+
requestedTools: requestedToolNames,
|
|
539
|
+
effectiveTools: effectiveToolAllowlist,
|
|
540
|
+
ceilingSources: capabilityCeiling?.sources,
|
|
541
|
+
excludeTools,
|
|
542
|
+
}));
|
|
543
|
+
}
|
|
544
|
+
// Host pruning also happens without a ceiling (and therefore without an
|
|
545
|
+
// audit). Use the existing non-fatal launch warnings rather than inventing
|
|
546
|
+
// a ceiling or treating the requested allowlist as a minimum requirement.
|
|
547
|
+
if (unavailableHostBuiltins.length > 0) {
|
|
548
|
+
const subject = input.agentName ? `Agent '${input.agentName}'` : "Subagent";
|
|
549
|
+
warnings.push(
|
|
550
|
+
`${subject}: host runtime tool availability omitted [${unavailableHostBuiltins.join(", ")}]. `
|
|
551
|
+
+ `Requested tool names: ${requestedToolNames ? `[${requestedToolNames.join(", ")}]` : "not explicitly specified"}; effective tool allowlist: [${effectiveToolAllowlist.join(", ")}]. `
|
|
552
|
+
+ (capabilityCeiling ? `Active capability ceiling sources: [${capabilityCeiling.sources.join(", ") || "unknown source"}]. ` : "")
|
|
553
|
+
+ (excludeTools.length > 0 ? `Explicit excludeTools: [${excludeTools.join(", ")}]. ` : "")
|
|
554
|
+
+ "This is a non-fatal tool-plan diagnostic, not verification of the child's runtime tool menu.",
|
|
555
|
+
);
|
|
556
|
+
}
|
|
440
557
|
const capabilityAudit = capabilityCeiling
|
|
441
558
|
? ({
|
|
442
559
|
ceiling: capabilityCeiling,
|
|
@@ -474,6 +591,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
474
591
|
capabilityCeilingAgentRestrictionSources(capabilityCeiling),
|
|
475
592
|
}
|
|
476
593
|
: {}),
|
|
594
|
+
...(unavailableHostBuiltins.length > 0 ? { unavailableHostBuiltins } : {}),
|
|
477
595
|
} satisfies SubagentCapabilityAudit)
|
|
478
596
|
: undefined;
|
|
479
597
|
return {
|
|
@@ -495,6 +613,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
495
613
|
extensionArgs,
|
|
496
614
|
disableAmbientExtensions,
|
|
497
615
|
warnings,
|
|
616
|
+
unavailableHostBuiltins,
|
|
498
617
|
...(capabilityAudit ? { capabilityAudit } : {}),
|
|
499
618
|
};
|
|
500
619
|
}
|
|
@@ -98,10 +98,12 @@ export function validateImplementationToolContract(input: {
|
|
|
98
98
|
const declaredMutationToolsWereRemoved = requestedMutationTools.length > 0 && !hasBuiltinMutationTool(input.tools);
|
|
99
99
|
const configuredExtensionCapability = (input.configuredExtensions?.length ?? 0) > 0 && !declaredMutationToolsWereRemoved;
|
|
100
100
|
if (hasMutationToolCapability(input.tools, input.mcpDirectTools) || configuredExtensionCapability) return undefined;
|
|
101
|
-
const intent = classifyTaskMutationIntent(input.agent, input.task);
|
|
101
|
+
const intent = classifyTaskMutationIntent(input.acceptanceRole === "writer" ? "worker" : input.agent, input.task);
|
|
102
102
|
if (intent.kind === "read-only") return undefined;
|
|
103
|
-
const writerTaskMayMutate =
|
|
104
|
-
|
|
103
|
+
const writerTaskMayMutate = input.acceptanceRole === "writer"
|
|
104
|
+
? true
|
|
105
|
+
: isWriterRole(input.agent, input.acceptanceRole)
|
|
106
|
+
&& (taskMayMutate(input.task) || WRITER_DELIVERY_PATTERN.test(input.task));
|
|
105
107
|
if (intent.kind !== "implementation" && !writerTaskMayMutate && !declaredMutationToolsWereRemoved) return undefined;
|
|
106
108
|
return `Agent '${input.agent}' was given an implementation task, but its tool allowlist has no mutation-capable tools. Add bash, edit, write, or another mutation-capable tool to the agent, or use a read-only task/agent.`;
|
|
107
109
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AgentConfig } from "../../agents/agents.ts";
|
|
2
|
+
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
3
|
+
import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
|
|
4
|
+
import { buildSkillInjection } from "../../agents/skills.ts";
|
|
5
|
+
import { injectOutputPathSystemPrompt } from "./single-output.ts";
|
|
6
|
+
|
|
7
|
+
export interface EffectiveSystemPromptInput {
|
|
8
|
+
/** Agent as handed to the child, including runtime-declared overlays such as the Intercom bridge. */
|
|
9
|
+
agent: AgentConfig;
|
|
10
|
+
resolvedSkills: Parameters<typeof buildSkillInjection>[0];
|
|
11
|
+
/** Directory that scopes memory and refinement lookups. */
|
|
12
|
+
cwd: string;
|
|
13
|
+
/** Omit when the caller injects the output path through another channel. */
|
|
14
|
+
outputPath?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function appendSection(prompt: string, section: string): string {
|
|
18
|
+
return prompt ? `${prompt}\n\n${section}` : section;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Child system prompt in the order preflight and every execution path hash:
|
|
23
|
+
* base prompt, skills, memory, refinement overlay, output path. Runtime
|
|
24
|
+
* acceptance prose is appended later and stays outside launch identity.
|
|
25
|
+
*/
|
|
26
|
+
export function buildEffectiveSystemPrompt(input: EffectiveSystemPromptInput): string {
|
|
27
|
+
let prompt = input.agent.systemPrompt?.trim() ?? "";
|
|
28
|
+
if (input.resolvedSkills.length > 0) prompt = appendSection(prompt, buildSkillInjection(input.resolvedSkills));
|
|
29
|
+
const memoryInjection = buildAgentMemoryInjection(input.agent, input.cwd);
|
|
30
|
+
if (memoryInjection) prompt = appendSection(prompt, memoryInjection);
|
|
31
|
+
prompt = appendAgentRefinementOverlay(prompt, { cwd: input.cwd, agentName: input.agent.name });
|
|
32
|
+
return injectOutputPathSystemPrompt(prompt, input.outputPath, input.agent);
|
|
33
|
+
}
|
|
@@ -3,7 +3,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { finished } from "node:stream/promises";
|
|
6
|
-
import type { ExternalProcessStatus } from "../../shared/types.ts";
|
|
6
|
+
import type { ExternalProcessStatus, ProcessTreeTerminal } from "../../shared/types.ts";
|
|
7
7
|
import { createOwnedProcessTreeController, type OwnedProcessTreeController } from "../background/owned-process-tree.ts";
|
|
8
8
|
import { omitExtensionBindingsEnv } from "./extension-bindings.ts";
|
|
9
9
|
import {
|
|
@@ -141,7 +141,7 @@ function classifyInvalidation(error: string): "auth" | "permission" | "launch" {
|
|
|
141
141
|
return "launch";
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
function terminateExternalProcessTree(pid: number, controller: OwnedProcessTreeController): Promise<
|
|
144
|
+
function terminateExternalProcessTree(pid: number, controller: OwnedProcessTreeController): Promise<ProcessTreeTerminal> {
|
|
145
145
|
if (process.platform !== "win32") return controller.terminate();
|
|
146
146
|
return new Promise((resolve) => {
|
|
147
147
|
const cleanup = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
@@ -239,7 +239,7 @@ export function runExternalCli(input: {
|
|
|
239
239
|
let settled = false;
|
|
240
240
|
let processTree: OwnedProcessTreeController | undefined;
|
|
241
241
|
let processPid: number | undefined;
|
|
242
|
-
let termination: Promise<
|
|
242
|
+
let termination: Promise<ProcessTreeTerminal> | undefined;
|
|
243
243
|
const flushProgress = () => {
|
|
244
244
|
if (!latestProgress) return;
|
|
245
245
|
input.onParserProgress?.(latestProgress);
|
|
@@ -381,8 +381,7 @@ export function runExternalCli(input: {
|
|
|
381
381
|
input.registerTimeout?.(undefined);
|
|
382
382
|
input.registerStop?.(undefined);
|
|
383
383
|
void (async () => {
|
|
384
|
-
|
|
385
|
-
else if (processTree) await processTree.finishAfterWriterClose();
|
|
384
|
+
const treeProof = termination ? await termination : processTree ? await processTree.finishAfterWriterClose() : undefined;
|
|
386
385
|
const endedAt = Date.now();
|
|
387
386
|
const externalProcess: ExternalProcessStatus = {
|
|
388
387
|
...initialProcess,
|
|
@@ -398,15 +397,18 @@ export function runExternalCli(input: {
|
|
|
398
397
|
input.onProcess?.(externalProcess);
|
|
399
398
|
const stderr = stderrTail.text().trim();
|
|
400
399
|
const parserFailure = parserError?.message ?? (parserTerminal?.state === "failed" ? parserTerminal.error ?? "External CLI parser reported terminal failure." : undefined);
|
|
400
|
+
const treeFailure = process.platform !== "win32" && treeProof?.state === "unknown"
|
|
401
|
+
? `Process-tree cleanup failed: ${treeProof.reason}.`
|
|
402
|
+
: undefined;
|
|
401
403
|
const error = stopped
|
|
402
404
|
? input.stopMessage ?? "Subagent stopped by user."
|
|
403
405
|
: timedOut
|
|
404
406
|
? input.timeoutMessage ?? "Subagent timed out."
|
|
405
|
-
: spawnError?.message ?? parserFailure ?? (exitCode === 0 ? undefined : stderr || `External CLI exited with code ${exitCode}.`);
|
|
407
|
+
: spawnError?.message ?? parserFailure ?? treeFailure ?? (exitCode === 0 ? undefined : stderr || `External CLI exited with code ${exitCode}.`);
|
|
406
408
|
if (error && input.preflight && !parserError) invalidateExternalCliPreflight(input.command, input.preflight, classifyInvalidation(error));
|
|
407
409
|
const result: ExternalCliRunResult = {
|
|
408
410
|
output: (!parserError && parserTerminal?.state === "completed" ? parserTerminal.output ?? "" : stdoutTail.text()).trim(),
|
|
409
|
-
exitCode: timedOut || stopped || spawnError || parserFailure ? 1 : exitCode,
|
|
411
|
+
exitCode: timedOut || stopped || spawnError || parserFailure || treeFailure ? 1 : exitCode,
|
|
410
412
|
...(error ? { error } : {}),
|
|
411
413
|
...(timedOut ? { timedOut: true } : {}),
|
|
412
414
|
...(stopped ? { stopped: true } : {}),
|
|
@@ -4,6 +4,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import type { ProviderHeaders } from "@earendil-works/pi-ai";
|
|
5
5
|
import { Type, type Static } from "typebox";
|
|
6
6
|
import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
|
|
7
|
+
import { opencodeSessionHeaders } from "../../shared/opencode-session-headers.ts";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* LLM intent arbiter for the completion mutation guard.
|
|
@@ -56,6 +57,8 @@ interface ArbiterRuntime {
|
|
|
56
57
|
/** Registered provider stream, usable only when its api matches the model. */
|
|
57
58
|
registeredStreamFn?: StreamFn;
|
|
58
59
|
registeredApi?: string;
|
|
60
|
+
/** Session id for OpenCode session-routing headers, when the caller has one. */
|
|
61
|
+
sessionId?: string;
|
|
59
62
|
timeoutMs: number;
|
|
60
63
|
}
|
|
61
64
|
|
|
@@ -76,7 +79,11 @@ export interface TaskMutationArbiterOptions {
|
|
|
76
79
|
const DEFAULT_ARBITER_TIMEOUT_MS = 10_000;
|
|
77
80
|
|
|
78
81
|
type RegistryModel = ReturnType<NonNullable<ExtensionContext["modelRegistry"]["find"]>>;
|
|
79
|
-
|
|
82
|
+
/** Model services the arbiter needs, plus the caller's session id for OpenCode session-routing headers. */
|
|
83
|
+
export type ArbiterModelContext = Pick<ExtensionContext, "model" | "modelRegistry"> & {
|
|
84
|
+
/** Session id the headers attach to (the child's, captured by detached runners; the parent's, passed by foreground callers). */
|
|
85
|
+
sessionId?: string;
|
|
86
|
+
};
|
|
80
87
|
|
|
81
88
|
function resolveArbiterModel(
|
|
82
89
|
ctx: ArbiterModelContext,
|
|
@@ -113,6 +120,7 @@ function resolveArbiterRuntime(
|
|
|
113
120
|
explicitStreamFn: options?.streamFn,
|
|
114
121
|
registeredStreamFn: registered?.streamSimple,
|
|
115
122
|
registeredApi: registered?.api,
|
|
123
|
+
sessionId: ctx.sessionId,
|
|
116
124
|
timeoutMs: options?.timeoutMs ?? DEFAULT_ARBITER_TIMEOUT_MS,
|
|
117
125
|
};
|
|
118
126
|
}
|
|
@@ -151,12 +159,13 @@ async function resolveArbiterAuth(
|
|
|
151
159
|
function authWrappedStreamFn(
|
|
152
160
|
base: StreamFn,
|
|
153
161
|
auth: ArbiterAuth,
|
|
162
|
+
sessionId: string | undefined,
|
|
154
163
|
): StreamFn {
|
|
155
164
|
return (model, context, streamOptions) => base(model, context, {
|
|
156
165
|
...(streamOptions ?? {}),
|
|
157
166
|
...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
|
|
158
167
|
...(auth.env || streamOptions?.env ? { env: { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } } : {}),
|
|
159
|
-
headers: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
|
|
168
|
+
headers: { ...opencodeSessionHeaders(model, sessionId), ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
|
|
160
169
|
});
|
|
161
170
|
}
|
|
162
171
|
|
|
@@ -200,7 +209,7 @@ async function runArbitration(
|
|
|
200
209
|
tools: [tool],
|
|
201
210
|
},
|
|
202
211
|
convertToLlm,
|
|
203
|
-
...agentStreamOptions(authWrappedStreamFn(streamFn, auth)),
|
|
212
|
+
...agentStreamOptions(authWrappedStreamFn(streamFn, auth, runtime.sessionId)),
|
|
204
213
|
getApiKey: (providerName) =>
|
|
205
214
|
providerName === runtime.model.provider ? auth.apiKey : undefined,
|
|
206
215
|
beforeToolCall: async ({ toolCall }) =>
|
|
@@ -543,6 +543,8 @@ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
|
543
543
|
/quota/i,
|
|
544
544
|
/billing/i,
|
|
545
545
|
/credit/i,
|
|
546
|
+
// OpenRouter can return only a status-prefixed body, without auth-related prose.
|
|
547
|
+
/^\s*401\s*:/,
|
|
546
548
|
/auth(?:entication)?/i,
|
|
547
549
|
/unauthori[sz]ed/i,
|
|
548
550
|
/forbidden/i,
|
|
@@ -29,7 +29,7 @@ const ORCA_CREATE_WATCHDOG_SCRIPT = [
|
|
|
29
29
|
"function exists(file){try{return fs.existsSync(file)}catch{return false}}",
|
|
30
30
|
"function keepQueued(){try{const now=new Date();fs.utimesSync(done,now,now)}catch{}}",
|
|
31
31
|
"function predecessorReady(){if(previous==='-')return true;if(exists(previous.replace(/\\.pending$/,'.ready')))return true;try{return Date.now()-fs.statSync(previous).mtimeMs>=waitTimeout}catch{return true}}",
|
|
32
|
-
"function updateManifest(state,stdout=''){if(manifest==='-')return;try{const payload=JSON.parse(fs.readFileSync(manifest,'utf8'));payload.state=state;payload.updatedAt=new Date().toISOString();const raw=stdout.trim()
|
|
32
|
+
"function updateManifest(state,stdout=''){if(manifest==='-')return;try{const payload=JSON.parse(fs.readFileSync(manifest,'utf8'));payload.state=state;payload.updatedAt=new Date().toISOString();const raw=stdout.trim();if(raw){try{payload.orca=JSON.parse(raw)}catch{payload.orcaRaw=raw.slice(0,4096)}}fs.writeFileSync(manifest,JSON.stringify(payload,null,2)+'\\n')}catch{}}",
|
|
33
33
|
"function start(){",
|
|
34
34
|
" try{",
|
|
35
35
|
" const child=spawn(command,args,{stdio:['ignore','pipe','ignore'],windowsHide:true});",
|
|
@@ -49,6 +49,7 @@ export interface PiSpawnDeps {
|
|
|
49
49
|
platform?: NodeJS.Platform;
|
|
50
50
|
execPath?: string;
|
|
51
51
|
argv1?: string;
|
|
52
|
+
bunVersion?: string;
|
|
52
53
|
existsSync?: (filePath: string) => boolean;
|
|
53
54
|
realpathSync?: (filePath: string) => string;
|
|
54
55
|
readFileSync?: (filePath: string, encoding: "utf-8") => string;
|
|
@@ -58,6 +59,15 @@ export interface PiSpawnDeps {
|
|
|
58
59
|
env?: NodeJS.ProcessEnv;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/** Compiled Pi's entrypoint is virtual; execPath is the real (possibly renamed) image. */
|
|
63
|
+
export function resolveBunPiExecutable(deps: PiSpawnDeps = {}): string | undefined {
|
|
64
|
+
const bunVersion = deps.bunVersion ?? process.versions.bun;
|
|
65
|
+
const entry = deps.argv1 ?? process.argv[1];
|
|
66
|
+
if (!bunVersion || !entry?.startsWith("/$bunfs/")) return undefined;
|
|
67
|
+
const env = deps.env ?? process.env;
|
|
68
|
+
return env[PI_SUBAGENT_PI_BINARY_ENV]?.trim() || (deps.execPath ?? process.execPath);
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
interface PiSpawnCommand {
|
|
62
72
|
command: string;
|
|
63
73
|
args: string[];
|
|
@@ -451,10 +451,11 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI, config?:
|
|
|
451
451
|
registerPermissionGate(pi, config.permissions, config.childWatchdog);
|
|
452
452
|
registerToolBudget(pi, config.toolBudget);
|
|
453
453
|
registerChildWatchdog(pi, config.childWatchdog, config.watchdogStatus);
|
|
454
|
-
const waitState = {
|
|
454
|
+
const waitState = config.runtimeState ?? {
|
|
455
455
|
baseCwd: "",
|
|
456
456
|
currentSessionId: null,
|
|
457
457
|
asyncJobs: new Map(),
|
|
458
|
+
foregroundRuns: new Map(),
|
|
458
459
|
foregroundControls: new Map(),
|
|
459
460
|
lastForegroundControlId: null,
|
|
460
461
|
cleanupTimers: new Map(),
|
|
@@ -486,13 +487,18 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI, config?:
|
|
|
486
487
|
if (diagnostic) throw new Error(formatChildToolDiagnostic(diagnostic));
|
|
487
488
|
});
|
|
488
489
|
onRuntimeEvent("agent_end", async (_event: unknown, ctx: unknown) => {
|
|
489
|
-
if ((ctx as { hasUI?: boolean } | undefined)?.hasUI === true)
|
|
490
|
+
if ((ctx as { hasUI?: boolean } | undefined)?.hasUI === true) drainObservation?.deny();
|
|
490
491
|
if (drainObservation) {
|
|
491
492
|
try {
|
|
492
493
|
if ((ctx as ExtensionContext)?.sessionManager?.getSessionFile() !== waitState.currentSessionId) drainObservation.deny();
|
|
493
494
|
} catch { drainObservation.deny(); }
|
|
494
495
|
}
|
|
495
|
-
|
|
496
|
+
config.holdFinalDrain?.(true);
|
|
497
|
+
try {
|
|
498
|
+
await drainOutstandingWork({ state: waitState, events: pi.events }, drainObservation);
|
|
499
|
+
} finally {
|
|
500
|
+
config.holdFinalDrain?.(false);
|
|
501
|
+
}
|
|
496
502
|
});
|
|
497
503
|
if (config.structuredOutput) registerStructuredOutputTool(pi, config.structuredOutput);
|
|
498
504
|
|