pi-subagents 0.65.0 → 0.65.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/docs/agents.md +1 -1
- package/docs/configuration.md +16 -0
- package/docs/extension-api.md +3 -0
- package/docs/tool-reference.md +8 -2
- package/docs/workflows.md +8 -0
- package/package.json +3 -1
- package/runner-server-preload.mjs +13 -0
- package/skills/pi-subagents/SKILL.md +2 -1
- package/skills/pi-subagents/references/execution-controls.md +5 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- package/src/api/preflight.ts +5 -1
- package/src/extension/config.ts +4 -2
- package/src/extension/index.ts +31 -2
- package/src/extension/schemas.ts +1 -1
- package/src/extension/tool-description.ts +5 -1
- package/src/integrations/pi-web-session-liveness.ts +73 -0
- package/src/intercom/native-supervisor-channel.ts +22 -36
- package/src/intercom/supervisor-ui.ts +3 -2
- package/src/missions/workflow-state.ts +37 -16
- package/src/runs/background/async-execution.ts +8 -1
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-retention.ts +9 -0
- package/src/runs/background/notify.ts +2 -0
- package/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/src/runs/background/run-child-session.ts +2 -1
- package/src/runs/background/runner-aliases.ts +32 -5
- package/src/runs/background/subagent-runner.ts +19 -0
- package/src/runs/foreground/execution.ts +15 -1
- package/src/runs/foreground/foreground-history.ts +3 -1
- package/src/runs/foreground/prompt-audit.ts +9 -5
- package/src/runs/foreground/subagent-executor.ts +61 -34
- package/src/runs/shared/acceptance.ts +14 -1
- package/src/runs/shared/child-session.ts +13 -18
- package/src/runs/shared/llm-intent-arbiter.ts +20 -11
- package/src/runs/shared/model-exclusions.ts +2 -1
- package/src/runs/shared/model-fallback.ts +31 -2
- package/src/runs/shared/nested-events.ts +3 -3
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/worktree-cleanup-plan.ts +6 -3
- package/src/runs/shared/worktree.ts +75 -10
- package/src/shared/model-response-aliases.ts +13 -0
- package/src/shared/types.ts +8 -0
- package/src/shared/utils.ts +3 -0
- package/src/shared/watch-strategy.ts +2 -0
- package/src/tui/fleet-status.ts +1 -1
- package/src/tui/render.ts +21 -10
- package/src/workflows/scripted-workflow.ts +32 -6
- package/src/workflows/workflow-checklist.ts +4 -3
|
@@ -17,6 +17,7 @@ export interface SupervisorRequestMessageDetails {
|
|
|
17
17
|
childIndex?: number;
|
|
18
18
|
childTarget?: string;
|
|
19
19
|
interview?: unknown;
|
|
20
|
+
requestBody?: string;
|
|
20
21
|
replyHint?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -107,7 +108,7 @@ function optionalString(value: unknown): boolean {
|
|
|
107
108
|
|
|
108
109
|
function requestDetails(value: unknown): SupervisorRequestMessageDetails | undefined {
|
|
109
110
|
if (!isRecord(value)) return undefined;
|
|
110
|
-
if (!optionalString(value.id) || !optionalString(value.requestId) || !optionalString(value.replyHint) || !optionalString(value.runId) || !optionalString(value.agent) || !optionalString(value.childTarget)) return undefined;
|
|
111
|
+
if (!optionalString(value.id) || !optionalString(value.requestId) || !optionalString(value.replyHint) || !optionalString(value.requestBody) || !optionalString(value.runId) || !optionalString(value.agent) || !optionalString(value.childTarget)) return undefined;
|
|
111
112
|
if (value.reason !== undefined && !isSupervisorReason(value.reason)) return undefined;
|
|
112
113
|
if (value.expectsReply !== undefined && typeof value.expectsReply !== "boolean") return undefined;
|
|
113
114
|
if (value.childIndex !== undefined && (typeof value.childIndex !== "number" || !Number.isFinite(value.childIndex))) return undefined;
|
|
@@ -153,7 +154,7 @@ function requestLines(message: SupervisorMessageLike, details: SupervisorRequest
|
|
|
153
154
|
if (details.childTarget) lines.push(`Child target: ${boundedField(details.childTarget)}`);
|
|
154
155
|
lines.push(`Request ID: ${requestId}`);
|
|
155
156
|
if (details.expectsReply) lines.push(`Reply with: ${displayText(details.replyHint ?? supervisorReplyHint(requestId), MAX_BODY_CHARS, expanded)}`);
|
|
156
|
-
lines.push("", "Request:", displayText(contentText(message.content) || "(no request body)", MAX_BODY_CHARS, expanded));
|
|
157
|
+
lines.push("", "Request:", displayText((details.requestBody ?? contentText(message.content)) || "(no request body)", MAX_BODY_CHARS, expanded));
|
|
157
158
|
if (details.interview !== undefined) lines.push("", "Interview shape:", interviewText(details.interview, expanded));
|
|
158
159
|
return lines;
|
|
159
160
|
}
|
|
@@ -18,6 +18,12 @@ export interface MissionWorkflowState {
|
|
|
18
18
|
set(key: string, value: unknown): void;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export interface MissionWorkflowStateOptions {
|
|
22
|
+
isProcessAlive?: (pid: number) => boolean;
|
|
23
|
+
getProcessStartKey?: (pid: number) => string | undefined;
|
|
24
|
+
retryDelaysMs?: readonly number[];
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
export function missionStatePath(location: MissionStoreLocation, missionId: string): string {
|
|
22
28
|
return path.join(location.missionDir, validateMissionId(missionId), "state.json");
|
|
23
29
|
}
|
|
@@ -66,13 +72,22 @@ function windowsProcessStartKey(pid: number): string | undefined {
|
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
let currentProcessKey: string | undefined | null = null;
|
|
76
|
+
|
|
69
77
|
function processStartKey(pid: number): string | undefined {
|
|
78
|
+
// Foreign PIDs can be reused while this process lives, so resolve them afresh.
|
|
79
|
+
if (pid !== process.pid) return computeProcessStartKey(pid);
|
|
80
|
+
if (currentProcessKey === null) currentProcessKey = computeProcessStartKey(pid);
|
|
81
|
+
return currentProcessKey;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function computeProcessStartKey(pid: number): string | undefined {
|
|
70
85
|
if (process.platform === "linux") return linuxProcessStartKey(pid) ?? psProcessStartKey(pid);
|
|
71
86
|
if (process.platform === "win32") return windowsProcessStartKey(pid);
|
|
72
87
|
return undefined;
|
|
73
88
|
}
|
|
74
89
|
|
|
75
|
-
|
|
90
|
+
type StateLockOptions = Required<MissionWorkflowStateOptions>;
|
|
76
91
|
|
|
77
92
|
function readStateLockOwner(lockPath: string): StateLockOwner | undefined {
|
|
78
93
|
try {
|
|
@@ -91,13 +106,13 @@ function readStateLockOwner(lockPath: string): StateLockOwner | undefined {
|
|
|
91
106
|
return undefined;
|
|
92
107
|
}
|
|
93
108
|
|
|
94
|
-
function stateLockIsStale(lockPath: string, now = Date.now()): boolean {
|
|
109
|
+
function stateLockIsStale(lockPath: string, options: StateLockOptions, now = Date.now()): boolean {
|
|
95
110
|
const owner = readStateLockOwner(lockPath);
|
|
96
111
|
if (owner) {
|
|
97
|
-
if (!isProcessAlive(owner.pid)) return true;
|
|
112
|
+
if (!options.isProcessAlive(owner.pid)) return true;
|
|
98
113
|
if (owner.processKey) {
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
114
|
+
const observedProcessKey = options.getProcessStartKey(owner.pid);
|
|
115
|
+
if (observedProcessKey) return owner.processKey !== observedProcessKey;
|
|
101
116
|
if (owner.pid === process.pid) return true;
|
|
102
117
|
}
|
|
103
118
|
return false;
|
|
@@ -140,11 +155,11 @@ function waitForStateLock(delayMs: number | undefined, lockPath: string): void {
|
|
|
140
155
|
waitForFileSystemRetry(delayMs);
|
|
141
156
|
}
|
|
142
157
|
|
|
143
|
-
function reclaimStaleStateLock(lockPath: string, reclaimPath: string): boolean {
|
|
144
|
-
if (!stateLockIsStale(lockPath)) return false;
|
|
158
|
+
function reclaimStaleStateLock(lockPath: string, reclaimPath: string, options: StateLockOptions): boolean {
|
|
159
|
+
if (!stateLockIsStale(lockPath, options)) return false;
|
|
145
160
|
if (!tryMakeDirectory(reclaimPath, 0o700)) return false;
|
|
146
161
|
try {
|
|
147
|
-
if (!stateLockIsStale(lockPath)) return false;
|
|
162
|
+
if (!stateLockIsStale(lockPath, options)) return false;
|
|
148
163
|
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
149
164
|
return true;
|
|
150
165
|
} finally {
|
|
@@ -152,7 +167,7 @@ function reclaimStaleStateLock(lockPath: string, reclaimPath: string): boolean {
|
|
|
152
167
|
}
|
|
153
168
|
}
|
|
154
169
|
|
|
155
|
-
function withStateFileLock<T>(filePath: string, operation: () => T): T {
|
|
170
|
+
function withStateFileLock<T>(filePath: string, operation: () => T, options: StateLockOptions): T {
|
|
156
171
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
157
172
|
const lockPath = `${filePath}.lock`;
|
|
158
173
|
const reclaimPath = `${lockPath}.reclaim`;
|
|
@@ -163,7 +178,7 @@ function withStateFileLock<T>(filePath: string, operation: () => T): T {
|
|
|
163
178
|
fs.rmSync(reclaimPath, { recursive: true, force: true });
|
|
164
179
|
continue;
|
|
165
180
|
}
|
|
166
|
-
waitForStateLock(
|
|
181
|
+
waitForStateLock(options.retryDelaysMs[attempt], lockPath);
|
|
167
182
|
continue;
|
|
168
183
|
}
|
|
169
184
|
let acquired = false;
|
|
@@ -171,17 +186,18 @@ function withStateFileLock<T>(filePath: string, operation: () => T): T {
|
|
|
171
186
|
acquired = tryMakeDirectory(lockPath, 0o700);
|
|
172
187
|
} catch (error) {
|
|
173
188
|
if (isRetryableFileSystemError(error)) {
|
|
174
|
-
waitForStateLock(
|
|
189
|
+
waitForStateLock(options.retryDelaysMs[attempt], lockPath);
|
|
175
190
|
continue;
|
|
176
191
|
}
|
|
177
192
|
throw new Error(`Failed to acquire mission state lock '${lockPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
178
193
|
}
|
|
179
194
|
if (!acquired) {
|
|
180
|
-
if (reclaimStaleStateLock(lockPath, reclaimPath)) continue;
|
|
181
|
-
waitForStateLock(
|
|
195
|
+
if (reclaimStaleStateLock(lockPath, reclaimPath, options)) continue;
|
|
196
|
+
waitForStateLock(options.retryDelaysMs[attempt], lockPath);
|
|
182
197
|
continue;
|
|
183
198
|
}
|
|
184
|
-
|
|
199
|
+
const key = options.getProcessStartKey(process.pid);
|
|
200
|
+
owner = { pid: process.pid, token: randomUUID(), createdAt: Date.now(), ...(key ? { processKey: key } : {}) };
|
|
185
201
|
try {
|
|
186
202
|
fs.writeFileSync(path.join(lockPath, "owner.json"), JSON.stringify(owner), { encoding: "utf-8", mode: 0o600 });
|
|
187
203
|
} catch (error) {
|
|
@@ -205,8 +221,13 @@ function validateStateKey(value: unknown): string {
|
|
|
205
221
|
return value;
|
|
206
222
|
}
|
|
207
223
|
|
|
208
|
-
export function createMissionWorkflowState(location: MissionStoreLocation, missionId: string): MissionWorkflowState {
|
|
224
|
+
export function createMissionWorkflowState(location: MissionStoreLocation, missionId: string, options: MissionWorkflowStateOptions = {}): MissionWorkflowState {
|
|
209
225
|
const filePath = missionStatePath(location, missionId);
|
|
226
|
+
const lockOptions: StateLockOptions = {
|
|
227
|
+
isProcessAlive: options.isProcessAlive ?? isProcessAlive,
|
|
228
|
+
getProcessStartKey: options.getProcessStartKey ?? processStartKey,
|
|
229
|
+
retryDelaysMs: options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS,
|
|
230
|
+
};
|
|
210
231
|
let loaded = false;
|
|
211
232
|
let values: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
212
233
|
|
|
@@ -254,7 +275,7 @@ export function createMissionWorkflowState(location: MissionStoreLocation, missi
|
|
|
254
275
|
writePrivateAtomicJson(filePath, next);
|
|
255
276
|
values = next;
|
|
256
277
|
loaded = true;
|
|
257
|
-
});
|
|
278
|
+
}, lockOptions);
|
|
258
279
|
},
|
|
259
280
|
};
|
|
260
281
|
}
|
|
@@ -146,6 +146,7 @@ interface AsyncExecutionContext {
|
|
|
146
146
|
currentModel?: ParentModel;
|
|
147
147
|
/** Optional model-scope enforcement resolved from subagent settings. */
|
|
148
148
|
modelScope?: ModelScopeConfig;
|
|
149
|
+
modelResponseAliases?: Record<string, string[]>;
|
|
149
150
|
/** Whether the parent session has an interactive UI. */
|
|
150
151
|
interactive?: boolean;
|
|
151
152
|
/** The executor's own child runtime when the launch comes from an in-process child. */
|
|
@@ -586,7 +587,10 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
586
587
|
stdoutFd = fs.openSync(logPaths.stdoutPath, "a");
|
|
587
588
|
stderrFd = fs.openSync(logPaths.stderrPath, "a");
|
|
588
589
|
}
|
|
589
|
-
const
|
|
590
|
+
const preload = hostPeerAliases.supplemental.length > 0
|
|
591
|
+
? ["--import", new URL("../../../runner-server-preload.mjs", import.meta.url).href]
|
|
592
|
+
: [];
|
|
593
|
+
const proc = spawn(nodeCommand, [...preload, jitiCliPath, runner, cfgPath], {
|
|
590
594
|
cwd,
|
|
591
595
|
...backgroundProcessOptions(),
|
|
592
596
|
stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
|
|
@@ -1021,6 +1025,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
1021
1025
|
modelCandidates: externalRunner ? undefined : modelCandidates,
|
|
1022
1026
|
...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
|
|
1023
1027
|
...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
|
|
1028
|
+
...(ctx.modelResponseAliases ? { modelResponseAliases: ctx.modelResponseAliases } : {}),
|
|
1024
1029
|
tools: a.tools,
|
|
1025
1030
|
excludeTools: a.excludeTools,
|
|
1026
1031
|
allowNestedSubagents: a.allowNestedSubagents,
|
|
@@ -1822,6 +1827,7 @@ export function executeAsyncSingle(
|
|
|
1822
1827
|
});
|
|
1823
1828
|
const recoveryAgentConfig = params.recoveryAgentConfig ?? agentConfig;
|
|
1824
1829
|
const recoveryDescriptor: SteeringRecoveryDescriptor = {
|
|
1830
|
+
...(ctx.modelResponseAliases ? { modelResponseAliases: ctx.modelResponseAliases } : {}),
|
|
1825
1831
|
version: 1,
|
|
1826
1832
|
...(lane ? { lane } : {}),
|
|
1827
1833
|
launchContractDigest,
|
|
@@ -1910,6 +1916,7 @@ export function executeAsyncSingle(
|
|
|
1910
1916
|
modelCandidates,
|
|
1911
1917
|
...(modelOrigin === "inherited" ? { skipPrimaryModelVerification: true } : {}),
|
|
1912
1918
|
...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
|
|
1919
|
+
...(ctx.modelResponseAliases ? { modelResponseAliases: ctx.modelResponseAliases } : {}),
|
|
1913
1920
|
tools: agentConfig.tools,
|
|
1914
1921
|
excludeTools: agentConfig.excludeTools,
|
|
1915
1922
|
allowNestedSubagents: agentConfig.allowNestedSubagents,
|
|
@@ -15,6 +15,7 @@ import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/paral
|
|
|
15
15
|
import { normalizeWorktreeBaseRef } from "../shared/worktree.ts";
|
|
16
16
|
import { intersectThinkingCeilings, parseThinkingLevel, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
|
|
17
17
|
import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
|
|
18
|
+
import { validateModelResponseAliases } from "../../shared/model-response-aliases.ts";
|
|
18
19
|
|
|
19
20
|
export interface AsyncResumeParams {
|
|
20
21
|
id?: string;
|
|
@@ -317,7 +318,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
317
318
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
|
|
318
319
|
const parsed = value as Record<string, unknown>;
|
|
319
320
|
const allowedFields = new Set([
|
|
320
|
-
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
|
|
321
|
+
"modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
|
|
321
322
|
"subagentOnlyExtensions", "mcpDirectTools", "excludeTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
|
|
322
323
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
323
324
|
"artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
@@ -337,6 +338,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
337
338
|
} catch (error) {
|
|
338
339
|
throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
339
340
|
}
|
|
341
|
+
validateModelResponseAliases(parsed.modelResponseAliases, `async recovery descriptor '${descriptorPath}' modelResponseAliases`);
|
|
340
342
|
if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
|
|
341
343
|
if (parsed.thinkingCeiling !== undefined) parsed.thinkingCeiling = parseThinkingLevel(parsed.thinkingCeiling, `async recovery descriptor '${descriptorPath}' thinkingCeiling`);
|
|
342
344
|
if (parsed.extensionBindings !== undefined) parsed.extensionBindings = normalizeExtensionBindings(parsed.extensionBindings)!.value;
|
|
@@ -320,7 +320,16 @@ function readCursor(root: string): RetentionCursor {
|
|
|
320
320
|
return value?.version === 1 ? value as unknown as RetentionCursor : { version: 1 };
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
let currentProcessStartIdentity: string | undefined | null = null;
|
|
324
|
+
|
|
323
325
|
function processStartIdentity(pid: number): string | undefined {
|
|
326
|
+
// Foreign PIDs can be reused while this process lives, so resolve them afresh.
|
|
327
|
+
if (pid !== process.pid) return computeProcessStartIdentity(pid);
|
|
328
|
+
if (currentProcessStartIdentity === null) currentProcessStartIdentity = computeProcessStartIdentity(pid);
|
|
329
|
+
return currentProcessStartIdentity;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function computeProcessStartIdentity(pid: number): string | undefined {
|
|
324
333
|
if (process.platform === "linux") {
|
|
325
334
|
try {
|
|
326
335
|
const stat = fs.readFileSync(`/proc/${pid}/stat`).toString("utf8");
|
|
@@ -123,6 +123,7 @@ export interface RegisterSubagentNotifyOptions {
|
|
|
123
123
|
|
|
124
124
|
export interface CompletionNotifier {
|
|
125
125
|
deliver(result: CompletionNotification): Promise<boolean>;
|
|
126
|
+
hasPendingDelivery(): boolean;
|
|
126
127
|
dispose(): void;
|
|
127
128
|
}
|
|
128
129
|
|
|
@@ -628,6 +629,7 @@ export default function registerSubagentNotify(
|
|
|
628
629
|
|
|
629
630
|
return {
|
|
630
631
|
deliver,
|
|
632
|
+
hasPendingDelivery: () => pending.size > 0,
|
|
631
633
|
dispose() {
|
|
632
634
|
if (disposed) return;
|
|
633
635
|
disposed = true;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { SubagentState } from "../../shared/types.ts";
|
|
3
|
+
import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
|
|
4
|
+
import { hasLiveNestedDescendants, projectNestedEvents } from "../shared/nested-events.ts";
|
|
5
|
+
|
|
6
|
+
interface RetainedNestedRouteTrackerOptions {
|
|
7
|
+
pollIntervalMs?: number;
|
|
8
|
+
platform?: NodeJS.Platform;
|
|
9
|
+
watch?: typeof fs.watch;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
13
|
+
const REFRESH_DEBOUNCE_MS = 25;
|
|
14
|
+
|
|
15
|
+
export function createRetainedNestedRouteTracker(
|
|
16
|
+
state: Pick<SubagentState, "retainedForegroundNestedRoutes">,
|
|
17
|
+
options: RetainedNestedRouteTrackerOptions = {},
|
|
18
|
+
): { track: (rootRunId: string) => void; clear: () => void } {
|
|
19
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
20
|
+
const watch = options.watch ?? fs.watch;
|
|
21
|
+
const watchers = new Map<string, fs.FSWatcher>();
|
|
22
|
+
const refreshTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
23
|
+
let poller: ReturnType<typeof setInterval> | undefined;
|
|
24
|
+
|
|
25
|
+
const close = (rootRunId: string): void => {
|
|
26
|
+
watchers.get(rootRunId)?.close();
|
|
27
|
+
watchers.delete(rootRunId);
|
|
28
|
+
const timer = refreshTimers.get(rootRunId);
|
|
29
|
+
if (timer) clearTimeout(timer);
|
|
30
|
+
refreshTimers.delete(rootRunId);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const refresh = (rootRunId: string): void => {
|
|
34
|
+
const retained = state.retainedForegroundNestedRoutes?.get(rootRunId);
|
|
35
|
+
if (!retained) {
|
|
36
|
+
close(rootRunId);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
if (hasLiveNestedDescendants(projectNestedEvents(retained).children)) return;
|
|
41
|
+
state.retainedForegroundNestedRoutes?.delete(rootRunId);
|
|
42
|
+
close(rootRunId);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(`Failed to refresh retained nested descendants for foreground run '${rootRunId}':`, error);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const scheduleRefresh = (rootRunId: string): void => {
|
|
49
|
+
if (refreshTimers.has(rootRunId)) return;
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
refreshTimers.delete(rootRunId);
|
|
52
|
+
refresh(rootRunId);
|
|
53
|
+
}, REFRESH_DEBOUNCE_MS);
|
|
54
|
+
timer.unref?.();
|
|
55
|
+
refreshTimers.set(rootRunId, timer);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const ensurePoller = (): void => {
|
|
59
|
+
if (poller || (state.retainedForegroundNestedRoutes?.size ?? 0) === 0) return;
|
|
60
|
+
poller = setInterval(() => {
|
|
61
|
+
for (const rootRunId of state.retainedForegroundNestedRoutes?.keys() ?? []) refresh(rootRunId);
|
|
62
|
+
if ((state.retainedForegroundNestedRoutes?.size ?? 0) > 0) return;
|
|
63
|
+
if (poller) clearInterval(poller);
|
|
64
|
+
poller = undefined;
|
|
65
|
+
}, pollIntervalMs);
|
|
66
|
+
poller.unref?.();
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const track = (rootRunId: string): void => {
|
|
70
|
+
const retained = state.retainedForegroundNestedRoutes?.get(rootRunId);
|
|
71
|
+
if (!retained) return;
|
|
72
|
+
if (shouldUseNativeFsWatch("retained-nested-route-tracker", options.platform) && !watchers.has(rootRunId)) {
|
|
73
|
+
try {
|
|
74
|
+
const watcher = watch(retained.eventSink, () => scheduleRefresh(rootRunId));
|
|
75
|
+
watcher.on("error", () => close(rootRunId));
|
|
76
|
+
watcher.unref?.();
|
|
77
|
+
watchers.set(rootRunId, watcher);
|
|
78
|
+
} catch {
|
|
79
|
+
// The bounded safety poll below covers unsupported or failed watchers.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
scheduleRefresh(rootRunId);
|
|
83
|
+
ensurePoller();
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const clear = (): void => {
|
|
87
|
+
if (poller) clearInterval(poller);
|
|
88
|
+
poller = undefined;
|
|
89
|
+
for (const rootRunId of watchers.keys()) close(rootRunId);
|
|
90
|
+
for (const timer of refreshTimers.values()) clearTimeout(timer);
|
|
91
|
+
refreshTimers.clear();
|
|
92
|
+
state.retainedForegroundNestedRoutes?.clear();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return { track, clear };
|
|
96
|
+
}
|
|
@@ -93,6 +93,7 @@ export interface RunChildSessionInput {
|
|
|
93
93
|
runDeadlineAt?: number;
|
|
94
94
|
expectedModelForVerification?: string;
|
|
95
95
|
modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string }>;
|
|
96
|
+
modelResponseAliases?: Record<string, string[]>;
|
|
96
97
|
mutationTools?: readonly string[];
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -468,7 +469,7 @@ export function runChildSession(input: RunChildSessionInput): Promise<RunChildSe
|
|
|
468
469
|
if (event.message.model) {
|
|
469
470
|
model = event.message.model;
|
|
470
471
|
if (input.expectedModelForVerification && !hasToolCall) {
|
|
471
|
-
const modelVerificationError = formatSubagentModelVerificationError(input.expectedModelForVerification, event.message.model, input.modelVerificationRegistry);
|
|
472
|
+
const modelVerificationError = formatSubagentModelVerificationError(input.expectedModelForVerification, event.message.model, input.modelVerificationRegistry, input.modelResponseAliases);
|
|
472
473
|
if (modelVerificationError && !error) error = modelVerificationError;
|
|
473
474
|
}
|
|
474
475
|
}
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
* this extension and are not installed next to it. pi's own extension loader
|
|
8
8
|
* aliases those specifiers to the copies shipped inside the installed pi
|
|
9
9
|
* package; the parent computes the same map and hands it to the runner
|
|
10
|
-
* through `JITI_ALIAS`, so
|
|
11
|
-
*
|
|
10
|
+
* through `JITI_ALIAS`, so child sessions and hooks retain host API identity.
|
|
11
|
+
* Only Pi 0.85.0's missing server exports may come from this extension.
|
|
12
12
|
*/
|
|
13
13
|
import * as fs from "node:fs";
|
|
14
14
|
import * as path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
15
16
|
|
|
16
17
|
export const JITI_ALIAS_ENV = "JITI_ALIAS";
|
|
17
18
|
|
|
@@ -19,6 +20,12 @@ export const JITI_ALIAS_ENV = "JITI_ALIAS";
|
|
|
19
20
|
export const HOST_PEER_ALIASES: ReadonlyArray<{ specifier: string; pkg: string; subpath: string }> = [
|
|
20
21
|
{ specifier: "@earendil-works/pi-coding-agent", pkg: "@earendil-works/pi-coding-agent", subpath: "." },
|
|
21
22
|
{ specifier: "@earendil-works/pi-agent-core", pkg: "@earendil-works/pi-agent-core", subpath: "." },
|
|
23
|
+
{ specifier: "@earendil-works/pi-agent-core/node", pkg: "@earendil-works/pi-agent-core", subpath: "./node" },
|
|
24
|
+
{ specifier: "@earendil-works/chord", pkg: "@earendil-works/chord", subpath: "." },
|
|
25
|
+
{ specifier: "@earendil-works/chord/context", pkg: "@earendil-works/chord", subpath: "./context" },
|
|
26
|
+
{ specifier: "@earendil-works/pi-server", pkg: "@earendil-works/pi-server", subpath: "." },
|
|
27
|
+
{ specifier: "@earendil-works/pi-server/unix", pkg: "@earendil-works/pi-server", subpath: "./unix" },
|
|
28
|
+
{ specifier: "@earendil-works/pi-client/unix", pkg: "@earendil-works/pi-client", subpath: "./unix" },
|
|
22
29
|
{ specifier: "@earendil-works/pi-tui", pkg: "@earendil-works/pi-tui", subpath: "." },
|
|
23
30
|
{ specifier: "@earendil-works/pi-ai", pkg: "@earendil-works/pi-ai", subpath: "./compat" },
|
|
24
31
|
{ specifier: "@earendil-works/pi-ai/compat", pkg: "@earendil-works/pi-ai", subpath: "./compat" },
|
|
@@ -31,6 +38,7 @@ export const HOST_PEER_ALIASES: ReadonlyArray<{ specifier: string; pkg: string;
|
|
|
31
38
|
|
|
32
39
|
interface PackageManifest {
|
|
33
40
|
name?: unknown;
|
|
41
|
+
version?: unknown;
|
|
34
42
|
main?: unknown;
|
|
35
43
|
exports?: unknown;
|
|
36
44
|
}
|
|
@@ -112,14 +120,33 @@ export function findHostPeerPackageDir(piPackageRoot: string, pkg: string): stri
|
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
/** The alias map the runner needs, or the specifiers that could not be resolved. */
|
|
115
|
-
export function resolveHostPeerAliases(
|
|
123
|
+
export function resolveHostPeerAliases(
|
|
124
|
+
piPackageRoot: string,
|
|
125
|
+
extensionRoot = fileURLToPath(new URL("../../../", import.meta.url)),
|
|
126
|
+
): { aliases: Record<string, string>; missing: string[]; supplemental: string[] } {
|
|
116
127
|
const aliases: Record<string, string> = {};
|
|
117
128
|
const missing: string[] = [];
|
|
129
|
+
const supplemental: string[] = [];
|
|
118
130
|
for (const { specifier, pkg, subpath } of HOST_PEER_ALIASES) {
|
|
119
131
|
const packageDir = findHostPeerPackageDir(piPackageRoot, pkg);
|
|
120
|
-
|
|
132
|
+
let target = packageDir ? resolvePackageSubpath(packageDir, subpath) : undefined;
|
|
133
|
+
// Pi 0.85.0 omitted this runtime dependency. Never replace working host
|
|
134
|
+
// exports or extend this exact version contract to other peers/hosts.
|
|
135
|
+
if ((!target || !fs.existsSync(target))
|
|
136
|
+
&& (specifier === "@earendil-works/pi-server" || specifier === "@earendil-works/pi-server/unix")
|
|
137
|
+
&& readManifest(piPackageRoot)?.version === "0.85.0"
|
|
138
|
+
&& (!packageDir || readManifest(packageDir)?.version === "0.85.0")) {
|
|
139
|
+
const localDir = findHostPeerPackageDir(extensionRoot, pkg);
|
|
140
|
+
if (localDir && readManifest(localDir)?.version === "0.85.0") {
|
|
141
|
+
const localTarget = resolvePackageSubpath(localDir, subpath);
|
|
142
|
+
if (localTarget && fs.existsSync(localTarget)) {
|
|
143
|
+
target = localTarget;
|
|
144
|
+
supplemental.push(specifier);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
121
148
|
if (target && fs.existsSync(target)) aliases[specifier] = target;
|
|
122
149
|
else missing.push(specifier);
|
|
123
150
|
}
|
|
124
|
-
return { aliases, missing };
|
|
151
|
+
return { aliases, missing, supplemental };
|
|
125
152
|
}
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
2
3
|
import * as fs from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
3
5
|
import * as path from "node:path";
|
|
4
6
|
import { pathToFileURL } from "node:url";
|
|
5
7
|
import type { Message } from "@earendil-works/pi-ai";
|
|
8
|
+
|
|
9
|
+
// Detached runners skip Pi's CLI proxy setup. Keep fetch on the same Undici dispatcher.
|
|
10
|
+
function ensureProxyAwareHttpDispatcher(): void {
|
|
11
|
+
try {
|
|
12
|
+
// SAFETY: require loads the pinned direct dependency described by these types.
|
|
13
|
+
const undici = createRequire(import.meta.url)("undici") as typeof import("undici");
|
|
14
|
+
const dispatcher = new undici.EnvHttpProxyAgent({ allowH2: false });
|
|
15
|
+
// Fetch rejects stream errors; the listener prevents an unhandled EventEmitter error.
|
|
16
|
+
EventEmitter.prototype.on.call(dispatcher, "error", () => {});
|
|
17
|
+
undici.setGlobalDispatcher(dispatcher);
|
|
18
|
+
undici.install();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(`[pi-subagents] proxy-aware HTTP dispatcher not installed: ${error instanceof Error ? error.message : String(error)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
ensureProxyAwareHttpDispatcher();
|
|
6
24
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
7
25
|
import { writeAsyncResultFile, writePendingAsyncResultFile } from "./result-files.ts";
|
|
8
26
|
import { createFileCoalescer } from "../../shared/file-coalescer.ts";
|
|
@@ -1188,6 +1206,7 @@ async function runSingleStepInner(
|
|
|
1188
1206
|
runDeadlineAt: ctx.deadlineAt,
|
|
1189
1207
|
expectedModelForVerification,
|
|
1190
1208
|
modelVerificationRegistry: step.modelVerificationRegistry,
|
|
1209
|
+
modelResponseAliases: step.modelResponseAliases,
|
|
1191
1210
|
mutationTools: step.mutationTools,
|
|
1192
1211
|
}));
|
|
1193
1212
|
const toolDiagnostic = run.exitCode === 0 && !run.error ? launch.capture.toolDiagnostic() : undefined;
|
|
@@ -983,6 +983,20 @@ async function runSingleAttempt(
|
|
|
983
983
|
compactionStartedReceived = false;
|
|
984
984
|
afterCompactionSettlement = false;
|
|
985
985
|
}
|
|
986
|
+
if (evt.type === "agent_start") {
|
|
987
|
+
const diagnostic = capture.toolDiagnostic();
|
|
988
|
+
if (diagnostic) {
|
|
989
|
+
const message = formatChildToolDiagnostic(diagnostic, { host: "parent" });
|
|
990
|
+
toolAvailabilityError = message;
|
|
991
|
+
result.error = message;
|
|
992
|
+
result.finalOutput = message;
|
|
993
|
+
progress.status = "failed";
|
|
994
|
+
progress.error = message;
|
|
995
|
+
fireUpdate();
|
|
996
|
+
abortChild();
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
986
1000
|
const lifecycleAction = projectChildLifecycle(evt, false, childLifecycleState);
|
|
987
1001
|
if (evt.type === "agent_settled" && lifecycleAction === "start-drain") {
|
|
988
1002
|
agentSettledReceived = true;
|
|
@@ -1086,7 +1100,7 @@ async function runSingleAttempt(
|
|
|
1086
1100
|
progress.model = evt.message.model;
|
|
1087
1101
|
if (!result.model) result.model = evt.message.model;
|
|
1088
1102
|
if (expectedModelForVerification && !hasToolCall) {
|
|
1089
|
-
const modelVerificationError = formatSubagentModelVerificationError(expectedModelForVerification, evt.message.model, options.availableModels);
|
|
1103
|
+
const modelVerificationError = formatSubagentModelVerificationError(expectedModelForVerification, evt.message.model, options.availableModels, options.modelResponseAliases);
|
|
1090
1104
|
if (modelVerificationError && !result.error) result.error = modelVerificationError;
|
|
1091
1105
|
}
|
|
1092
1106
|
}
|
|
@@ -5,6 +5,7 @@ import { DIRS } from "../../shared/types.ts";
|
|
|
5
5
|
import { writePrivateAtomicJson } from "../../shared/atomic-json.ts";
|
|
6
6
|
import { utf8Tail } from "../../shared/utf8.ts";
|
|
7
7
|
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
8
|
+
import { validateModelResponseAliases } from "../../shared/model-response-aliases.ts";
|
|
8
9
|
|
|
9
10
|
export const MAX_REMEMBERED_FOREGROUND_RUNS = 50;
|
|
10
11
|
const HISTORY_VERSION = 1;
|
|
@@ -71,11 +72,12 @@ function isRestorableResumeContract(value: unknown): boolean {
|
|
|
71
72
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
72
73
|
try {
|
|
73
74
|
if (Buffer.byteLength(JSON.stringify(value), "utf8") > MAX_RESUME_CONTRACT_BYTES) return false;
|
|
75
|
+
validateModelResponseAliases((value as ForegroundResumeChild["resumeContract"])?.modelResponseAliases);
|
|
74
76
|
} catch {
|
|
75
77
|
return false;
|
|
76
78
|
}
|
|
77
79
|
const contract = value as NonNullable<ForegroundResumeChild["resumeContract"]>;
|
|
78
|
-
if (Object.keys(contract).some((key) => !["outputSchema", "agentContract", "acceptance", "output", "outputMode"].includes(key))) return false;
|
|
80
|
+
if (Object.keys(contract).some((key) => !["modelResponseAliases", "outputSchema", "agentContract", "acceptance", "output", "outputMode"].includes(key))) return false;
|
|
79
81
|
if (contract.outputSchema !== undefined && (!contract.outputSchema || typeof contract.outputSchema !== "object" || Array.isArray(contract.outputSchema))) return false;
|
|
80
82
|
if (contract.agentContract !== undefined && (!contract.agentContract || typeof contract.agentContract !== "object" || Array.isArray(contract.agentContract) || contract.agentContract.version !== 1)) return false;
|
|
81
83
|
if (validateAcceptanceInput(contract.acceptance).length > 0) return false;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { Agent,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import type { Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
1
|
+
import type { Agent, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { ProviderHeaders } from "@earendil-works/pi-ai";
|
|
5
4
|
import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
|
|
6
5
|
import type { ForegroundRunControl } from "../../shared/types.ts";
|
|
7
6
|
export type PromptAuditView = "authored" | "runtime" | "effective";
|
|
@@ -21,7 +20,7 @@ export interface LivePromptAudit {
|
|
|
21
20
|
|
|
22
21
|
const livePrompts = new WeakMap<ForegroundRunControl, Map<number, LivePromptAudit>>();
|
|
23
22
|
|
|
24
|
-
type RegistryModel =
|
|
23
|
+
type RegistryModel = NonNullable<ExtensionContext["model"]>;
|
|
25
24
|
|
|
26
25
|
function fullModelId(model: Pick<RegistryModel, "provider" | "id">): string {
|
|
27
26
|
return `${model.provider}/${model.id}`;
|
|
@@ -64,6 +63,11 @@ export async function rewritePromptWithGuidance(input: {
|
|
|
64
63
|
}): Promise<string> {
|
|
65
64
|
const model = input.ctx.model;
|
|
66
65
|
if (!model) throw new Error("Prompt redo needs the current session model to rewrite the authored task.");
|
|
66
|
+
const [{ Agent }, { convertToLlm }, { streamSimple }] = await Promise.all([
|
|
67
|
+
import("@earendil-works/pi-agent-core"),
|
|
68
|
+
import("@earendil-works/pi-coding-agent"),
|
|
69
|
+
import("@earendil-works/pi-ai/compat"),
|
|
70
|
+
]);
|
|
67
71
|
const auth = await resolveRewriteAuth(input.ctx, model);
|
|
68
72
|
const registeredProvider = (input.ctx.modelRegistry as {
|
|
69
73
|
getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;
|