pi-subagents 0.63.0 → 0.65.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 +57 -1
- package/README.md +2 -2
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +1 -1
- package/docs/agents.md +18 -16
- package/docs/configuration.md +7 -15
- package/docs/extension-api.md +15 -6
- package/docs/missions.md +2 -0
- package/docs/observability.md +10 -12
- package/docs/tool-reference.md +7 -6
- package/docs/watchdog.md +92 -114
- package/docs/workflows.md +5 -3
- package/package.json +3 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +6 -5
- package/src/agents/agent-management.ts +47 -15
- package/src/api/capability-ceiling.ts +0 -1
- package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
- package/src/api/preflight.ts +2 -3
- package/src/extension/doctor.ts +2 -10
- package/src/extension/fanout-child.ts +9 -11
- package/src/extension/index.ts +27 -5
- package/src/extension/public-execution.ts +14 -0
- package/src/extension/rpc.ts +3 -2
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -8
- package/src/intercom/native-supervisor-channel.ts +138 -60
- package/src/intercom/supervisor-ui.ts +243 -0
- package/src/runs/background/async-execution.ts +46 -24
- package/src/runs/background/async-job-tracker.ts +11 -0
- package/src/runs/background/async-resume.ts +11 -2
- package/src/runs/background/control-channel.ts +2 -204
- package/src/runs/background/notify.ts +41 -2
- package/src/runs/background/process-terminal.ts +1 -1
- package/src/runs/background/run-child-session.ts +613 -0
- package/src/runs/background/run-status.ts +0 -1
- package/src/runs/background/runner-aliases.ts +125 -0
- package/src/runs/background/runner-child-sessions.ts +31 -0
- package/src/runs/background/scheduled-runs.ts +18 -4
- package/src/runs/background/subagent-runner.ts +203 -898
- package/src/runs/foreground/async-steering-action.ts +1 -17
- package/src/runs/foreground/execution.ts +196 -378
- package/src/runs/foreground/foreground-control.ts +4 -0
- package/src/runs/foreground/subagent-executor.ts +121 -58
- package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
- package/src/runs/shared/abort-recovery.ts +3 -3
- package/src/runs/shared/acceptance.ts +10 -0
- package/src/runs/shared/async-status-projection.ts +11 -43
- package/src/runs/shared/capability-ceiling.ts +1 -2
- package/src/runs/shared/child-hooks.ts +25 -0
- package/src/runs/shared/child-identity.ts +13 -2
- package/src/runs/shared/child-launch.ts +314 -0
- package/src/runs/shared/child-lifecycle.ts +25 -0
- package/src/runs/shared/child-runtime-config.ts +126 -0
- package/src/runs/shared/child-session.ts +342 -0
- package/src/runs/shared/child-tool-plan.ts +530 -0
- package/src/runs/shared/claude-code-adapter.ts +5 -1
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/external-cli-preflight.ts +16 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/src/runs/shared/model-exclusions.ts +82 -14
- package/src/runs/shared/model-fallback.ts +47 -4
- package/src/runs/shared/nested-events.ts +29 -45
- package/src/runs/shared/nested-path.ts +0 -14
- package/src/runs/shared/orca-progress-tabs.ts +12 -7
- package/src/runs/shared/parallel-utils.ts +0 -2
- package/src/runs/shared/permissions.ts +0 -13
- package/src/runs/shared/process-signal.ts +4 -1
- package/src/runs/shared/run-fanout-budget.ts +0 -13
- package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
- package/src/runs/shared/structured-output.ts +17 -4
- package/src/runs/shared/subagent-control.ts +6 -2
- package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
- package/src/runs/shared/tool-availability.ts +18 -62
- package/src/runs/shared/tool-budget.ts +0 -14
- package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
- package/src/runs/shared/worktree.ts +117 -30
- package/src/shared/child-session-name.ts +1 -1
- package/src/shared/jsonl-writer.ts +11 -0
- package/src/shared/thinking-ceiling.ts +0 -6
- package/src/shared/types.ts +64 -30
- package/src/shared/utils.ts +3 -4
- package/src/slash/slash-commands.ts +0 -6
- package/src/tui/fleet.ts +0 -1
- package/src/tui/render.ts +238 -31
- package/src/watchdog/child-status.ts +54 -34
- package/src/watchdog/diff-tool.ts +77 -0
- package/src/watchdog/emission-guard.ts +5 -3
- package/src/watchdog/guidance.ts +20 -0
- package/src/watchdog/register-child.ts +28 -25
- package/src/watchdog/register-main.ts +10 -9
- package/src/watchdog/render.ts +4 -5
- package/src/watchdog/review.ts +15 -4
- package/src/watchdog/rules.ts +70 -0
- package/src/watchdog/runtime.ts +75 -92
- package/src/watchdog/scope.ts +0 -11
- package/src/watchdog/settings.ts +48 -104
- package/src/watchdog/types.ts +18 -32
- package/src/watchdog/warning-format.ts +0 -1
- package/src/workflows/chat-progress.ts +3 -2
- package/src/workflows/scripted-workflow.ts +48 -1
- package/src/workflows/workflow-checklist.ts +10 -12
- package/src/workflows/workflow-preflight.ts +28 -1
- package/src/runs/shared/child-protocol.ts +0 -415
- package/src/runs/shared/pi-args.ts +0 -1062
- package/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/src/shared/post-exit-stdio-guard.ts +0 -85
|
@@ -3,41 +3,28 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { BeforeProviderRequestEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { registerNativeSupervisorClient } from "../../intercom/native-supervisor-channel.ts";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeAcknowledgedExtensionId, writeRuntimeAcknowledgedExtensions } from "./runtime-acknowledged-extensions.ts";
|
|
11
|
-
import { createStructuredOutputToolParameters, MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
|
|
6
|
+
import { permissionDecision } from "./permissions.ts";
|
|
7
|
+
import type { SteerRequest } from "../background/control-channel.ts";
|
|
8
|
+
import { RUNTIME_EXTENSION_ACK_EVENT, isRuntimeAcknowledgedExtensionId } from "./runtime-acknowledged-extensions.ts";
|
|
9
|
+
import { createStructuredOutputToolParameters, MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, validateStructuredOutputValue } from "./structured-output.ts";
|
|
12
10
|
import { validateAcceptanceReport } from "./acceptance.ts";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
MCP_DIRECT_CHILD_TOOLS_ENV,
|
|
17
|
-
REQUIRED_CHILD_TOOLS_ENV,
|
|
18
|
-
writeChildToolDiagnostic,
|
|
19
|
-
type ChildToolDiagnostic,
|
|
20
|
-
} from "./tool-availability.ts";
|
|
21
|
-
import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV, decodeToolBudgetEnv, shouldBlockToolForBudget, toolBudgetBlockedMessage, toolBudgetSoftNudge } from "./tool-budget.ts";
|
|
22
|
-
import type { JsonSchemaObject, ResolvedToolBudget, SubagentState } from "../../shared/types.ts";
|
|
11
|
+
import { formatChildToolDiagnostic } from "./tool-availability.ts";
|
|
12
|
+
import { shouldBlockToolForBudget, toolBudgetBlockedMessage, toolBudgetSoftNudge } from "./tool-budget.ts";
|
|
13
|
+
import type { ResolvedToolBudget, SubagentState } from "../../shared/types.ts";
|
|
23
14
|
import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
|
|
24
|
-
import { getAgentDir
|
|
15
|
+
import { getAgentDir } from "../../shared/utils.ts";
|
|
25
16
|
import { registerChildWatchdog } from "../../watchdog/register-child.ts";
|
|
26
|
-
import {
|
|
17
|
+
import type { ChildWatchdogConfig } from "../../watchdog/child-status.ts";
|
|
27
18
|
import { requestWatchdogPermission, type WatchdogPermissionRequest, type WatchdogPermissionResult } from "../../watchdog/permission-arbiter.ts";
|
|
28
19
|
import { SUBAGENT_WATCHDOG_WARNING_TYPE } from "../../watchdog/types.ts";
|
|
29
|
-
import { resolveWaitToolConfig } from "../background/wait-config.ts";
|
|
30
20
|
import { registerWaitTool } from "../background/wait-tool.ts";
|
|
31
21
|
import { drainOutstandingWork } from "../background/auto-drain.ts";
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export const SUBAGENT_SESSION_NAME_ENV = "PI_SUBAGENT_SESSION_NAME";
|
|
39
|
-
const STEERING_LEGACY_SETTLE_FALLBACK_MS = 1000;
|
|
40
|
-
const STEERING_SAFETY_POLL_INTERVAL_MS = 5000;
|
|
22
|
+
import {
|
|
23
|
+
childSupervisorMetadata,
|
|
24
|
+
evaluateChildToolDiagnostic,
|
|
25
|
+
type ChildPermissions,
|
|
26
|
+
type ChildRuntimeConfig,
|
|
27
|
+
} from "./child-runtime-config.ts";
|
|
41
28
|
|
|
42
29
|
const STRUCTURED_OUTPUT_INSTRUCTIONS = [
|
|
43
30
|
"This subagent step has a strict structured output contract.",
|
|
@@ -77,45 +64,8 @@ const PROJECT_CONTEXT_LEGACY_HEADER = "\n\n# Project Context\n\nProject-specific
|
|
|
77
64
|
const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
|
|
78
65
|
const DATE_HEADER = "\nCurrent date:";
|
|
79
66
|
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
if (value === undefined) return undefined;
|
|
83
|
-
return value !== "0";
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function readRequiredChildTools(): string[] | undefined {
|
|
87
|
-
const encoded = process.env[REQUIRED_CHILD_TOOLS_ENV]?.trim();
|
|
88
|
-
if (!encoded) return undefined;
|
|
89
|
-
const required = JSON.parse(encoded) as unknown;
|
|
90
|
-
if (!Array.isArray(required) || required.some((name) => typeof name !== "string" || !name)) {
|
|
91
|
-
throw new Error(`Invalid ${REQUIRED_CHILD_TOOLS_ENV} payload.`);
|
|
92
|
-
}
|
|
93
|
-
return required;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function readMcpDirectChildTools(): string[] | undefined {
|
|
97
|
-
const encoded = process.env[MCP_DIRECT_CHILD_TOOLS_ENV]?.trim();
|
|
98
|
-
if (!encoded) return undefined;
|
|
99
|
-
try {
|
|
100
|
-
const tools = JSON.parse(encoded) as unknown;
|
|
101
|
-
if (!Array.isArray(tools) || tools.some((name) => typeof name !== "string" || !name)) return undefined;
|
|
102
|
-
return tools;
|
|
103
|
-
} catch {
|
|
104
|
-
return undefined;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function refreshChildToolDiagnostic(pi: ExtensionAPI): ChildToolDiagnostic | undefined {
|
|
109
|
-
const filePath = process.env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV]?.trim();
|
|
110
|
-
const required = readRequiredChildTools();
|
|
111
|
-
if (!filePath || !required) return undefined;
|
|
112
|
-
const available = pi.getAllTools().map((tool) => tool.name);
|
|
113
|
-
return writeChildToolDiagnostic(filePath, required, available, process.env[SUBAGENT_CHILD_AGENT_ENV]?.trim(), readMcpDirectChildTools());
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function registerRuntimeExtensionAcknowledgements(pi: ExtensionAPI): void {
|
|
117
|
-
const outputPath = process.env[RUNTIME_EXTENSION_ACK_PATH_ENV]?.trim();
|
|
118
|
-
if (!outputPath) return;
|
|
67
|
+
function registerRuntimeExtensionAcknowledgements(pi: ExtensionAPI, sink: ((ids: string[]) => void) | undefined): void {
|
|
68
|
+
if (!sink) return;
|
|
119
69
|
const ids: string[] = [];
|
|
120
70
|
let finalized = false;
|
|
121
71
|
const acknowledge = (payload: unknown): undefined => {
|
|
@@ -127,7 +77,7 @@ function registerRuntimeExtensionAcknowledgements(pi: ExtensionAPI): void {
|
|
|
127
77
|
const finalize = (): undefined => {
|
|
128
78
|
if (finalized) return undefined;
|
|
129
79
|
finalized = true;
|
|
130
|
-
|
|
80
|
+
sink(ids);
|
|
131
81
|
return undefined;
|
|
132
82
|
};
|
|
133
83
|
try {
|
|
@@ -252,7 +202,7 @@ function stripChildBoundaryInstructions(prompt: string): string {
|
|
|
252
202
|
|
|
253
203
|
export function rewriteSubagentPrompt(
|
|
254
204
|
prompt: string,
|
|
255
|
-
options: { inheritProjectContext: boolean; inheritGlobalContext: boolean; inheritSkills: boolean; fanoutChild?: boolean },
|
|
205
|
+
options: { inheritProjectContext: boolean; inheritGlobalContext: boolean; inheritSkills: boolean; fanoutChild?: boolean; structuredOutput?: boolean },
|
|
256
206
|
): string {
|
|
257
207
|
let rewritten = prompt;
|
|
258
208
|
if (!options.inheritProjectContext) {
|
|
@@ -267,7 +217,7 @@ export function rewriteSubagentPrompt(
|
|
|
267
217
|
rewritten = stripSubagentOrchestrationSkill(rewritten);
|
|
268
218
|
rewritten = stripChildBoundaryInstructions(rewritten);
|
|
269
219
|
const boundary = options.fanoutChild ? CHILD_FANOUT_BOUNDARY_INSTRUCTIONS : CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS;
|
|
270
|
-
const structured =
|
|
220
|
+
const structured = options.structuredOutput ? `\n\n${STRUCTURED_OUTPUT_INSTRUCTIONS}` : "";
|
|
271
221
|
return `${boundary}${structured}\n\n${rewritten}`;
|
|
272
222
|
}
|
|
273
223
|
|
|
@@ -303,14 +253,14 @@ const PROMPT_CACHE_KEY_APIS = new Set([
|
|
|
303
253
|
"openai-responses",
|
|
304
254
|
]);
|
|
305
255
|
|
|
306
|
-
export function rewriteForkCacheProviderRequest(event: BeforeProviderRequestEvent, ctx
|
|
307
|
-
const
|
|
308
|
-
if (!
|
|
256
|
+
export function rewriteForkCacheProviderRequest(event: BeforeProviderRequestEvent, ctx: Pick<ExtensionContext, "model"> | undefined, forkCacheKey: string | undefined): unknown {
|
|
257
|
+
const key = forkCacheKey?.trim();
|
|
258
|
+
if (!key || !PROMPT_CACHE_KEY_APIS.has(ctx?.model?.api ?? "")) return undefined;
|
|
309
259
|
if (!event || typeof event !== "object") return undefined;
|
|
310
260
|
const payload = event.payload;
|
|
311
261
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
312
262
|
if (typeof (payload as { prompt_cache_key?: unknown }).prompt_cache_key !== "string") return undefined;
|
|
313
|
-
return { ...payload, prompt_cache_key:
|
|
263
|
+
return { ...payload, prompt_cache_key: key };
|
|
314
264
|
}
|
|
315
265
|
|
|
316
266
|
function portableToolId(id: string): string {
|
|
@@ -348,8 +298,8 @@ function stripAssistantSubagentToolCallBlocks(message: unknown): unknown | undef
|
|
|
348
298
|
return { ...m, content: filteredContent };
|
|
349
299
|
}
|
|
350
300
|
|
|
351
|
-
export function stripParentOnlySubagentMessages(messages: unknown[], options: { sanitizeToolIds?: boolean } = {}): unknown[] {
|
|
352
|
-
const preserveCurrentFanoutToolHistory =
|
|
301
|
+
export function stripParentOnlySubagentMessages(messages: unknown[], options: { sanitizeToolIds?: boolean; preserveFanoutToolHistory?: boolean } = {}): unknown[] {
|
|
302
|
+
const preserveCurrentFanoutToolHistory = options.preserveFanoutToolHistory === true;
|
|
353
303
|
const sanitizeToolIds = options.sanitizeToolIds ?? true;
|
|
354
304
|
let changed = false;
|
|
355
305
|
const filtered: unknown[] = [];
|
|
@@ -382,23 +332,20 @@ export function formatSteerMessage(request: SteerRequest): string {
|
|
|
382
332
|
|
|
383
333
|
export function registerPermissionGate(
|
|
384
334
|
pi: ExtensionAPI,
|
|
335
|
+
permissions: ChildPermissions | undefined,
|
|
336
|
+
childWatchdog: ChildWatchdogConfig | undefined,
|
|
385
337
|
requestPermission: (request: WatchdogPermissionRequest) => Promise<WatchdogPermissionResult> = requestWatchdogPermission,
|
|
386
338
|
): void {
|
|
387
|
-
const rules =
|
|
388
|
-
if (!rules) return;
|
|
339
|
+
const rules = permissions?.rules;
|
|
340
|
+
if (!rules || Object.keys(rules).length === 0) return;
|
|
341
|
+
const rawWatchdogConfig = childWatchdog ? JSON.stringify(childWatchdog) : undefined;
|
|
342
|
+
const timeoutMs = childWatchdog?.agentEndTimeoutMs ?? 30_000;
|
|
389
343
|
const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: { toolName?: string; input?: unknown }, ctx: ExtensionContext) => unknown) => void;
|
|
390
344
|
onRuntimeEvent("tool_call", async (event, ctx) => {
|
|
391
345
|
const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
|
|
392
346
|
const decision = permissionDecision(rules, toolName);
|
|
393
347
|
if (decision === "allow") return undefined;
|
|
394
348
|
if (decision === "deny") return { block: true, reason: `Blocked by pi-subagents permission rule: '${toolName}' is denied.` };
|
|
395
|
-
const rawWatchdogConfig = process.env[CHILD_WATCHDOG_CONFIG_ENV];
|
|
396
|
-
let timeoutMs = 30_000;
|
|
397
|
-
try {
|
|
398
|
-
timeoutMs = decodeChildWatchdogConfig(rawWatchdogConfig)?.agentEndTimeoutMs ?? timeoutMs;
|
|
399
|
-
} catch {
|
|
400
|
-
// The arbiter reports invalid configuration with the concrete decode error.
|
|
401
|
-
}
|
|
402
349
|
if (ctx.signal?.aborted) return { block: true, reason: "Blocked by pi-subagents permission rule: Watchdog permission decision was cancelled." };
|
|
403
350
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
404
351
|
let abort: (() => void) | undefined;
|
|
@@ -410,7 +357,7 @@ export function registerPermissionGate(
|
|
|
410
357
|
toolName,
|
|
411
358
|
args: event.input ?? {},
|
|
412
359
|
rawWatchdogConfig,
|
|
413
|
-
auditPath:
|
|
360
|
+
auditPath: permissions.auditPath,
|
|
414
361
|
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
415
362
|
}),
|
|
416
363
|
new Promise<WatchdogPermissionResult>((resolve) => {
|
|
@@ -454,245 +401,51 @@ function registerToolBudget(pi: ExtensionAPI, budget: ResolvedToolBudget | undef
|
|
|
454
401
|
});
|
|
455
402
|
}
|
|
456
403
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
let disposed = false;
|
|
477
|
-
let agentRunning = false;
|
|
478
|
-
let inTurn = false;
|
|
479
|
-
let awaitingSettlement = false;
|
|
480
|
-
let flushing = false;
|
|
481
|
-
let started = false;
|
|
482
|
-
let canSteer = typeof sendUserMessage === "function";
|
|
483
|
-
let watcher: fs.FSWatcher | undefined;
|
|
484
|
-
let interval: NodeJS.Timeout | undefined;
|
|
485
|
-
let safetyInterval: NodeJS.Timeout | undefined;
|
|
486
|
-
let settleFallback: NodeJS.Timeout | undefined;
|
|
487
|
-
const legacySettleFallbackMs = deps.legacySettleFallbackMs ?? STEERING_LEGACY_SETTLE_FALLBACK_MS;
|
|
488
|
-
const acknowledge = (request: SteerRequest, state: "delivered" | "queued" | "failed", message: string, deliveryStatus?: SteerDeliveryStatus): void => {
|
|
489
|
-
if (!ackDir || !Number.isInteger(childIndex) || childIndex < 0) return;
|
|
490
|
-
writeSteerAckAt(steerAckPathFromDir(ackDir, request.id), {
|
|
491
|
-
requestId: request.id,
|
|
492
|
-
index: childIndex,
|
|
493
|
-
ts: Date.now(),
|
|
494
|
-
state,
|
|
495
|
-
...(deliveryStatus ? { deliveryStatus } : {}),
|
|
496
|
-
message,
|
|
497
|
-
});
|
|
498
|
-
};
|
|
499
|
-
const publishCapability = (): void => {
|
|
500
|
-
if (!capabilityPath || !Number.isInteger(childIndex) || childIndex < 0) return;
|
|
501
|
-
writeSteerCapabilityAt(capabilityPath, { index: childIndex, pid: process.pid, readyAt: Date.now(), supported: canSteer });
|
|
502
|
-
};
|
|
503
|
-
const flush = (): void => {
|
|
504
|
-
if (disposed || flushing) return;
|
|
505
|
-
flushing = true;
|
|
506
|
-
try {
|
|
507
|
-
const requests = consumeSteerRequestsFromDir(steerInbox);
|
|
508
|
-
for (let index = 0; index < requests.length; index++) {
|
|
509
|
-
const request = requests[index]!;
|
|
510
|
-
if (!canSteer || typeof sendUserMessage !== "function") {
|
|
511
|
-
acknowledge(request, "failed", "Child Pi session does not support sendUserMessage steering.");
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
const requestedMode = request.mode ?? "steer";
|
|
515
|
-
const autoCanUseIdle = requestedMode === "auto" && !agentRunning && !awaitingSettlement;
|
|
516
|
-
const delivery = requestedMode === "follow_up" || (requestedMode === "auto" && (inTurn || awaitingSettlement)) ? "followUp" as const : "steer" as const;
|
|
517
|
-
const pendingFollowUps = [...pending.values()].reduce((count, entries) => count + entries.filter((entry) => entry.deliveryStatus === "queued").length, 0);
|
|
518
|
-
if (delivery === "followUp" && queued.length + pendingFollowUps >= MAX_STEER_QUEUE_SIZE) {
|
|
519
|
-
acknowledge(request, "failed", `Follow-up queue is full (${MAX_STEER_QUEUE_SIZE} messages).`);
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
const formatted = formatSteerMessage(request);
|
|
523
|
-
const entries = pending.get(formatted) ?? [];
|
|
524
|
-
entries.push({ request, deliveryStatus: delivery === "followUp" ? "queued" : "delivered" });
|
|
525
|
-
pending.set(formatted, entries);
|
|
526
|
-
try {
|
|
527
|
-
sendUserMessage(formatted, autoCanUseIdle ? undefined : { deliverAs: delivery });
|
|
528
|
-
} catch (error) {
|
|
529
|
-
entries.pop();
|
|
530
|
-
if (entries.length === 0) pending.delete(formatted);
|
|
531
|
-
acknowledge(request, "failed", error instanceof Error ? error.message : String(error));
|
|
532
|
-
for (const retry of requests.slice(index + 1)) writeSteerRequestToDir(steerInbox, retry);
|
|
533
|
-
break;
|
|
534
|
-
}
|
|
404
|
+
function registerStructuredOutputTool(pi: ExtensionAPI, structured: NonNullable<ChildRuntimeConfig["structuredOutput"]>): void {
|
|
405
|
+
const required = structured.acceptanceReport === "required";
|
|
406
|
+
const parameters = createStructuredOutputToolParameters(structured.schema, { acceptanceReport: structured.acceptanceReport });
|
|
407
|
+
const registerTool = pi.registerTool as unknown as (tool: {
|
|
408
|
+
name: string;
|
|
409
|
+
label: string;
|
|
410
|
+
description: string;
|
|
411
|
+
parameters: unknown;
|
|
412
|
+
execute: (_id: string, params: { value: unknown; acceptanceReport?: unknown }) => Promise<unknown>;
|
|
413
|
+
}) => void;
|
|
414
|
+
registerTool({
|
|
415
|
+
name: "structured_output",
|
|
416
|
+
label: "Structured Output",
|
|
417
|
+
description: "Submit the required final structured output for this subagent step. This terminates the step.",
|
|
418
|
+
parameters,
|
|
419
|
+
async execute(_id: string, params: { value: unknown; acceptanceReport?: unknown }) {
|
|
420
|
+
const validation = await validateStructuredOutputValue(structured.schema, params.value);
|
|
421
|
+
if (validation.status === "invalid") {
|
|
422
|
+
throw new Error(`Structured output validation failed: ${validation.message}`);
|
|
535
423
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
};
|
|
540
|
-
const onInput = (event: unknown): undefined => {
|
|
541
|
-
if (disposed || !event || typeof event !== "object") return undefined;
|
|
542
|
-
const input = event as { source?: unknown; streamingBehavior?: unknown; text?: unknown; content?: unknown };
|
|
543
|
-
if (input.source !== "extension") return undefined;
|
|
544
|
-
const text = typeof input.text === "string" ? input.text : typeof input.content === "string" ? input.content : undefined;
|
|
545
|
-
if (!text) return undefined;
|
|
546
|
-
const entries = pending.get(text);
|
|
547
|
-
const entry = entries?.shift();
|
|
548
|
-
if (!entry) return undefined;
|
|
549
|
-
if (entries?.length === 0) pending.delete(text);
|
|
550
|
-
if (entry.deliveryStatus === "queued") {
|
|
551
|
-
queued.push({ request: entry.request, ready: !inTurn });
|
|
552
|
-
acknowledge(entry.request, "queued", "Pi queued the correlated follow-up input.", "queued");
|
|
553
|
-
} else {
|
|
554
|
-
acknowledge(entry.request, "delivered", "Pi accepted the correlated steering input.", "delivered");
|
|
555
|
-
}
|
|
556
|
-
return undefined;
|
|
557
|
-
};
|
|
558
|
-
const start = (): void => {
|
|
559
|
-
if (started || disposed) return;
|
|
560
|
-
try {
|
|
561
|
-
fs.mkdirSync(steerInbox, { recursive: true });
|
|
562
|
-
publishCapability();
|
|
563
|
-
} catch {
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
started = true;
|
|
567
|
-
const startPolling = (): void => {
|
|
568
|
-
if (interval || disposed) return;
|
|
569
|
-
interval = (deps.timers?.setInterval ?? setInterval)(flush, 250) as NodeJS.Timeout;
|
|
570
|
-
interval.unref?.();
|
|
571
|
-
};
|
|
572
|
-
const startSafetyPolling = (): void => {
|
|
573
|
-
if (safetyInterval || disposed) return;
|
|
574
|
-
safetyInterval = (deps.timers?.setInterval ?? setInterval)(flush, deps.safetyPollIntervalMs ?? STEERING_SAFETY_POLL_INTERVAL_MS) as NodeJS.Timeout;
|
|
575
|
-
safetyInterval.unref?.();
|
|
576
|
-
};
|
|
577
|
-
if (!shouldUseNativeFsWatch("child-steering-inbox", deps.platform)) {
|
|
578
|
-
startPolling();
|
|
579
|
-
} else {
|
|
580
|
-
try {
|
|
581
|
-
watcher = (deps.watch ?? fs.watch)(resolveWatchPath(steerInbox, deps.nativeRealpath), () => flush());
|
|
582
|
-
watcher.on("error", startPolling);
|
|
583
|
-
startSafetyPolling();
|
|
584
|
-
} catch {
|
|
585
|
-
watcher = undefined;
|
|
586
|
-
startPolling();
|
|
424
|
+
if (required && params.acceptanceReport === undefined) {
|
|
425
|
+
throw new Error(MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR);
|
|
587
426
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
return undefined;
|
|
594
|
-
};
|
|
595
|
-
const clearSettleFallback = (): void => {
|
|
596
|
-
if (!settleFallback) return;
|
|
597
|
-
clearTimeout(settleFallback);
|
|
598
|
-
settleFallback = undefined;
|
|
599
|
-
};
|
|
600
|
-
const markSettled = (): undefined => {
|
|
601
|
-
clearSettleFallback();
|
|
602
|
-
agentRunning = false;
|
|
603
|
-
inTurn = false;
|
|
604
|
-
awaitingSettlement = false;
|
|
605
|
-
return activate();
|
|
606
|
-
};
|
|
607
|
-
const armLegacySettleFallback = (): void => {
|
|
608
|
-
clearSettleFallback();
|
|
609
|
-
settleFallback = setTimeout(() => {
|
|
610
|
-
settleFallback = undefined;
|
|
611
|
-
if (disposed || !awaitingSettlement) return;
|
|
612
|
-
agentRunning = false;
|
|
613
|
-
inTurn = false;
|
|
614
|
-
awaitingSettlement = false;
|
|
615
|
-
activate();
|
|
616
|
-
}, legacySettleFallbackMs);
|
|
617
|
-
settleFallback.unref?.();
|
|
618
|
-
};
|
|
619
|
-
|
|
620
|
-
const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: unknown, ctx?: unknown) => unknown) => void;
|
|
621
|
-
// Register input before the watcher so an accepted extension input cannot race request dispatch.
|
|
622
|
-
onRuntimeEvent("input", onInput);
|
|
623
|
-
onRuntimeEvent("session_start", () => start());
|
|
624
|
-
onRuntimeEvent("agent_start", () => {
|
|
625
|
-
clearSettleFallback();
|
|
626
|
-
agentRunning = true;
|
|
627
|
-
awaitingSettlement = false;
|
|
628
|
-
return activate();
|
|
629
|
-
});
|
|
630
|
-
onRuntimeEvent("agent_end", (event) => {
|
|
631
|
-
inTurn = false;
|
|
632
|
-
if ((event as { willRetry?: unknown } | undefined)?.willRetry === true) {
|
|
633
|
-
clearSettleFallback();
|
|
634
|
-
agentRunning = true;
|
|
635
|
-
awaitingSettlement = true;
|
|
636
|
-
return activate();
|
|
637
|
-
}
|
|
638
|
-
agentRunning = true;
|
|
639
|
-
awaitingSettlement = true;
|
|
640
|
-
armLegacySettleFallback();
|
|
641
|
-
return activate();
|
|
642
|
-
});
|
|
643
|
-
onRuntimeEvent("agent_settled", markSettled);
|
|
644
|
-
onRuntimeEvent("session_compact", () => {
|
|
645
|
-
const unresolved = [...pending.values()].flat();
|
|
646
|
-
pending.clear();
|
|
647
|
-
for (const entry of unresolved) {
|
|
648
|
-
try {
|
|
649
|
-
writeSteerRequestToDir(steerInbox, { ...entry.request, mode: "follow_up" });
|
|
650
|
-
} catch (error) {
|
|
651
|
-
acknowledge(entry.request, "failed", `Could not retry steering after compaction: ${error instanceof Error ? error.message : String(error)}`);
|
|
427
|
+
if (required && params.acceptanceReport !== undefined) {
|
|
428
|
+
const acceptanceValidation = validateAcceptanceReport(params.acceptanceReport, "acceptanceReport");
|
|
429
|
+
if (!acceptanceValidation.report) {
|
|
430
|
+
throw new Error(`Invalid structured output acceptance report: ${acceptanceValidation.errors.join("; ")}`);
|
|
431
|
+
}
|
|
652
432
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
inTurn = true;
|
|
661
|
-
const next = queued.findIndex((entry) => entry.ready);
|
|
662
|
-
if (next >= 0) {
|
|
663
|
-
const [entry] = queued.splice(next, 1);
|
|
664
|
-
if (entry) acknowledge(entry.request, "delivered", "Pi delivered the queued follow-up at a turn boundary.", "delivered");
|
|
665
|
-
}
|
|
666
|
-
return activate();
|
|
667
|
-
});
|
|
668
|
-
onRuntimeEvent("turn_end", () => {
|
|
669
|
-
inTurn = false;
|
|
670
|
-
for (const entry of queued) entry.ready = true;
|
|
671
|
-
return activate();
|
|
672
|
-
});
|
|
673
|
-
for (const eventName of ["message_start", "message_update", "message_end", "tool_execution_start", "tool_execution_end"] as const) {
|
|
674
|
-
onRuntimeEvent(eventName, activate);
|
|
675
|
-
}
|
|
676
|
-
onRuntimeEvent("session_shutdown", () => {
|
|
677
|
-
for (const entry of queued) acknowledge(entry.request, "failed", "Run ended before queued follow-up delivery.", "queued");
|
|
678
|
-
for (const entries of pending.values()) {
|
|
679
|
-
for (const entry of entries) acknowledge(entry.request, "failed", "Run ended before Pi confirmed steering input delivery.");
|
|
680
|
-
}
|
|
681
|
-
disposed = true;
|
|
682
|
-
clearSettleFallback();
|
|
683
|
-
try { watcher?.close(); } catch {}
|
|
684
|
-
if (interval) (deps.timers?.clearInterval ?? clearInterval)(interval);
|
|
685
|
-
if (safetyInterval) (deps.timers?.clearInterval ?? clearInterval)(safetyInterval);
|
|
433
|
+
structured.capture(params.value, structured.acceptanceReport ? params.acceptanceReport : undefined);
|
|
434
|
+
return {
|
|
435
|
+
content: [{ type: "text", text: "Structured output captured." }],
|
|
436
|
+
details: {},
|
|
437
|
+
terminate: true,
|
|
438
|
+
};
|
|
439
|
+
},
|
|
686
440
|
});
|
|
687
441
|
}
|
|
688
442
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
registerPermissionGate(pi);
|
|
693
|
-
registerToolBudget(pi,
|
|
694
|
-
registerChildWatchdog(pi);
|
|
695
|
-
const waitToolConfig = resolveWaitToolConfig();
|
|
443
|
+
/** Register every child-side hook the prompt runtime owns for one child session. */
|
|
444
|
+
export default function registerSubagentPromptRuntime(pi: ExtensionAPI, config: ChildRuntimeConfig): void {
|
|
445
|
+
registerRuntimeExtensionAcknowledgements(pi, config.runtimeAcknowledgements);
|
|
446
|
+
registerPermissionGate(pi, config.permissions, config.childWatchdog);
|
|
447
|
+
registerToolBudget(pi, config.toolBudget);
|
|
448
|
+
registerChildWatchdog(pi, config.childWatchdog, config.watchdogStatus);
|
|
696
449
|
const waitState = {
|
|
697
450
|
baseCwd: "",
|
|
698
451
|
currentSessionId: null,
|
|
@@ -707,12 +460,13 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
707
460
|
watcherRestartTimer: null,
|
|
708
461
|
resultFileCoalescer: { schedule: () => false, clear: () => {} },
|
|
709
462
|
} as unknown as SubagentState;
|
|
710
|
-
if (typeof pi.registerTool === "function") registerWaitTool(pi, waitState,
|
|
463
|
+
if (typeof pi.registerTool === "function") registerWaitTool(pi, waitState, config.waitTool.enabled, undefined, config.waitTool.defaultTimeoutMs);
|
|
464
|
+
const supervisorMetadata = childSupervisorMetadata(config);
|
|
711
465
|
let nativeSupervisorClientRegistered = false;
|
|
712
466
|
const registerNativeSupervisorClientOnce = (): void => {
|
|
713
467
|
if (nativeSupervisorClientRegistered) return;
|
|
714
468
|
nativeSupervisorClientRegistered = true;
|
|
715
|
-
registerNativeSupervisorClient(pi);
|
|
469
|
+
registerNativeSupervisorClient(pi, supervisorMetadata);
|
|
716
470
|
};
|
|
717
471
|
const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: unknown, ctx?: ExtensionContext) => unknown) => void;
|
|
718
472
|
onRuntimeEvent("session_start", (_event: unknown, ctx?: ExtensionContext) => {
|
|
@@ -721,72 +475,24 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
721
475
|
registerNativeSupervisorClientOnce();
|
|
722
476
|
});
|
|
723
477
|
onRuntimeEvent("agent_start", () => {
|
|
724
|
-
|
|
478
|
+
if (!config.requiredTools) return;
|
|
479
|
+
const diagnostic = evaluateChildToolDiagnostic(config, pi.getAllTools().map((tool) => tool.name));
|
|
480
|
+
config.toolDiagnostic?.(diagnostic);
|
|
725
481
|
if (diagnostic) throw new Error(formatChildToolDiagnostic(diagnostic));
|
|
726
482
|
});
|
|
727
483
|
onRuntimeEvent("agent_end", async (_event: unknown, ctx: unknown) => {
|
|
728
484
|
if ((ctx as { hasUI?: boolean } | undefined)?.hasUI === true) return;
|
|
729
485
|
await drainOutstandingWork({ state: waitState, events: pi.events });
|
|
730
486
|
});
|
|
731
|
-
|
|
732
|
-
const structuredAcceptanceReportPath = process.env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV];
|
|
733
|
-
const structuredAcceptanceReportRequired = process.env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] === "1";
|
|
734
|
-
const structuredSchemaPath = process.env[STRUCTURED_OUTPUT_SCHEMA_ENV];
|
|
735
|
-
if (structuredOutputPath && structuredSchemaPath) {
|
|
736
|
-
const schema = JSON.parse(fs.readFileSync(structuredSchemaPath, "utf-8")) as JsonSchemaObject;
|
|
737
|
-
const acceptanceReportMode = structuredAcceptanceReportPath
|
|
738
|
-
? structuredAcceptanceReportRequired ? "required" as const : "optional" as const
|
|
739
|
-
: undefined;
|
|
740
|
-
const parameters = createStructuredOutputToolParameters(schema, { acceptanceReport: acceptanceReportMode });
|
|
741
|
-
const registerTool = pi.registerTool as unknown as (tool: {
|
|
742
|
-
name: string;
|
|
743
|
-
label: string;
|
|
744
|
-
description: string;
|
|
745
|
-
parameters: unknown;
|
|
746
|
-
execute: (_id: string, params: { value: unknown; acceptanceReport?: unknown }) => Promise<unknown>;
|
|
747
|
-
}) => void;
|
|
748
|
-
registerTool({
|
|
749
|
-
name: "structured_output",
|
|
750
|
-
label: "Structured Output",
|
|
751
|
-
description: "Submit the required final structured output for this subagent step. This terminates the step.",
|
|
752
|
-
parameters,
|
|
753
|
-
async execute(_id: string, params: { value: unknown; acceptanceReport?: unknown }) {
|
|
754
|
-
const validation = await validateStructuredOutputValue(schema, params.value);
|
|
755
|
-
if (validation.status === "invalid") {
|
|
756
|
-
throw new Error(`Structured output validation failed: ${validation.message}`);
|
|
757
|
-
}
|
|
758
|
-
if (structuredAcceptanceReportRequired && params.acceptanceReport === undefined) {
|
|
759
|
-
throw new Error(MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR);
|
|
760
|
-
}
|
|
761
|
-
if (structuredAcceptanceReportRequired && params.acceptanceReport !== undefined) {
|
|
762
|
-
const acceptanceValidation = validateAcceptanceReport(params.acceptanceReport, "acceptanceReport");
|
|
763
|
-
if (!acceptanceValidation.report) {
|
|
764
|
-
throw new Error(`Invalid structured output acceptance report: ${acceptanceValidation.errors.join("; ")}`);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
fs.mkdirSync(path.dirname(structuredOutputPath), { recursive: true });
|
|
768
|
-
if (structuredAcceptanceReportPath && params.acceptanceReport !== undefined) {
|
|
769
|
-
fs.mkdirSync(path.dirname(structuredAcceptanceReportPath), { recursive: true });
|
|
770
|
-
fs.writeFileSync(structuredAcceptanceReportPath, JSON.stringify(params.acceptanceReport), { mode: 0o600 });
|
|
771
|
-
} else if (structuredAcceptanceReportPath && fs.existsSync(structuredAcceptanceReportPath)) {
|
|
772
|
-
fs.unlinkSync(structuredAcceptanceReportPath);
|
|
773
|
-
}
|
|
774
|
-
fs.writeFileSync(structuredOutputPath, JSON.stringify(params.value), { mode: 0o600 });
|
|
775
|
-
return {
|
|
776
|
-
content: [{ type: "text", text: "Structured output captured." }],
|
|
777
|
-
details: { path: structuredOutputPath },
|
|
778
|
-
terminate: true,
|
|
779
|
-
};
|
|
780
|
-
},
|
|
781
|
-
});
|
|
782
|
-
}
|
|
487
|
+
if (config.structuredOutput) registerStructuredOutputTool(pi, config.structuredOutput);
|
|
783
488
|
|
|
784
|
-
onRuntimeEvent("before_provider_request", (event: unknown, ctx?: ExtensionContext) => rewriteForkCacheProviderRequest(event as BeforeProviderRequestEvent, ctx));
|
|
489
|
+
onRuntimeEvent("before_provider_request", (event: unknown, ctx?: ExtensionContext) => rewriteForkCacheProviderRequest(event as BeforeProviderRequestEvent, ctx, config.forkCacheKey));
|
|
785
490
|
|
|
786
491
|
onRuntimeEvent("context", (event: unknown, ctx?: ExtensionContext) => {
|
|
787
492
|
if (!event || typeof event !== "object" || !("messages" in event) || !Array.isArray(event.messages)) return undefined;
|
|
788
493
|
const messages = stripParentOnlySubagentMessages(event.messages, {
|
|
789
494
|
sanitizeToolIds: !COMPOSITE_TOOL_ID_APIS.has(ctx?.model?.api ?? ""),
|
|
495
|
+
preserveFanoutToolHistory: config.fanoutChild,
|
|
790
496
|
});
|
|
791
497
|
if (messages === event.messages) return undefined;
|
|
792
498
|
return { messages };
|
|
@@ -798,24 +504,21 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
798
504
|
// The intercom target is a routing address and always wins; the display
|
|
799
505
|
// name (agent + task excerpt, computed by the parent at launch) only
|
|
800
506
|
// applies when the bridge is not addressing this child.
|
|
801
|
-
const
|
|
802
|
-
const displaySessionName = process.env[SUBAGENT_SESSION_NAME_ENV]?.trim();
|
|
803
|
-
const childSessionName = intercomSessionName || displaySessionName;
|
|
507
|
+
const childSessionName = config.intercomSessionName || config.sessionName;
|
|
804
508
|
if (childSessionName && typeof pi.setSessionName === "function") {
|
|
805
509
|
pi.setSessionName(childSessionName);
|
|
806
510
|
}
|
|
807
511
|
|
|
808
|
-
const inheritProjectContext =
|
|
809
|
-
const
|
|
810
|
-
const inheritSkills = readBooleanEnv(SUBAGENT_INHERIT_SKILLS_ENV);
|
|
811
|
-
const fanoutChild = readBooleanEnv(SUBAGENT_FANOUT_CHILD_ENV);
|
|
512
|
+
const { inheritProjectContext, inheritGlobalContext, inheritSkills } = config;
|
|
513
|
+
const fanoutChild = config.fanoutChild;
|
|
812
514
|
let rewritten = event.systemPrompt;
|
|
813
|
-
if (inheritProjectContext !== undefined || inheritGlobalContext !== undefined || inheritSkills !== undefined || fanoutChild
|
|
515
|
+
if (inheritProjectContext !== undefined || inheritGlobalContext !== undefined || inheritSkills !== undefined || fanoutChild) {
|
|
814
516
|
rewritten = rewriteSubagentPrompt(event.systemPrompt, {
|
|
815
517
|
inheritProjectContext: inheritProjectContext ?? true,
|
|
816
518
|
inheritGlobalContext: inheritGlobalContext ?? true,
|
|
817
519
|
inheritSkills: inheritSkills ?? true,
|
|
818
|
-
fanoutChild
|
|
520
|
+
fanoutChild,
|
|
521
|
+
structuredOutput: Boolean(config.structuredOutput),
|
|
819
522
|
});
|
|
820
523
|
}
|
|
821
524
|
if (rewritten === event.systemPrompt) return;
|