pi-subagents 0.49.0 → 0.51.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 +97 -0
- package/agents/gpt-pro.md +17 -0
- package/agents/oracle.md +7 -5
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +37 -2
- package/docs/configuration.md +76 -14
- package/docs/extension-api.md +78 -1
- package/docs/missions.md +1 -1
- package/docs/observability.md +20 -4
- package/docs/tool-reference.md +55 -39
- package/docs/workflows.md +171 -5
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +5 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
- package/skills/pi-subagents/references/execution-controls.md +22 -18
- package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
- package/src/agents/agent-management.ts +100 -345
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +135 -25
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +42 -11
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +36 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +210 -90
- package/src/extension/public-execution.ts +31 -2
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +14 -36
- package/src/extension/tool-description.ts +37 -24
- package/src/inspectors/herdr/actions.ts +5 -9
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +4 -8
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +49 -51
- package/src/missions/goal-driver.ts +3 -1
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +94 -1
- package/src/runs/background/async-execution.ts +98 -38
- package/src/runs/background/async-job-tracker.ts +21 -4
- package/src/runs/background/async-resume.ts +30 -17
- package/src/runs/background/async-retention.ts +888 -0
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +47 -56
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/notify.ts +3 -1
- package/src/runs/background/result-files.ts +505 -0
- package/src/runs/background/result-watcher.ts +250 -51
- package/src/runs/background/retained-children.ts +79 -20
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +37 -29
- package/src/runs/background/run-status.ts +34 -20
- package/src/runs/background/scheduled-runs.ts +71 -27
- package/src/runs/background/stale-run-reconciler.ts +33 -16
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +535 -161
- package/src/runs/background/subagent-wait.ts +9 -7
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +22 -2
- package/src/runs/background/wait-subscriptions.ts +80 -1
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +221 -15
- package/src/runs/foreground/subagent-executor.ts +529 -1519
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/external-job-bridge.ts +450 -0
- package/src/runs/shared/external-job-runner.ts +286 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/src/runs/shared/model-fallback.ts +34 -3
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +437 -0
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +6 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/subagent-control.ts +41 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +12 -1
- package/src/shared/atomic-json.ts +22 -2
- package/src/shared/capacity-resilient-json.ts +102 -0
- package/src/shared/completion-owner.ts +14 -0
- package/src/shared/file-system-retry.ts +49 -1
- package/src/shared/fork-context.ts +42 -0
- package/src/shared/prompt-resources.ts +0 -40
- package/src/shared/settings.ts +3 -27
- package/src/shared/types.ts +88 -27
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-commands.ts +45 -28
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +186 -28
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +7 -5
- package/src/workflows/scripted-workflow.ts +424 -125
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1565
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { ExternalJobProviderError, type ExternalJobHandle, type ExternalJobResult, type ExternalJobState } from "../../api/external-job-provider.ts";
|
|
5
|
+
import type { ExternalJobStatus } from "../../shared/types.ts";
|
|
6
|
+
import { requestExternalJobOperation } from "./external-job-bridge.ts";
|
|
7
|
+
|
|
8
|
+
const STATUS_POLL_INTERVAL_MS = 1_000;
|
|
9
|
+
|
|
10
|
+
export interface ExternalJobRunResult {
|
|
11
|
+
output: string;
|
|
12
|
+
exitCode: number;
|
|
13
|
+
error?: string;
|
|
14
|
+
timedOut?: boolean;
|
|
15
|
+
stopped?: boolean;
|
|
16
|
+
externalJob: ExternalJobStatus;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function externalJobPromptDigest(prompt: string): string {
|
|
20
|
+
return createHash("sha256").update(prompt).digest("hex");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sleep(ms: number): Promise<void> {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function terminal(state: ExternalJobState): boolean {
|
|
28
|
+
return state === "completed" || state === "failed" || state === "stopped" || state === "blocked";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatError(error: ExternalJobProviderError, provider: string): string {
|
|
32
|
+
const blocking = error.blockingJobId ? ` Blocking provider job: ${error.blockingJobId}.` : "";
|
|
33
|
+
return `External-job provider '${provider}' failed closed (${error.code}): ${error.message}.${blocking}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isNotFound(error: unknown): boolean {
|
|
37
|
+
return typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
41
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function malformedStatus(statusPath: string): ExternalJobProviderError {
|
|
45
|
+
return new ExternalJobProviderError(`Malformed external-job status '${statusPath}'. Refusing to redispatch the prompt.`, { code: "status-unreadable" });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseExternalJobStatus(value: unknown, statusPath: string): ExternalJobStatus {
|
|
49
|
+
if (!isRecord(value)) throw malformedStatus(statusPath);
|
|
50
|
+
const provider = value.provider;
|
|
51
|
+
const promptDigest = value.promptDigest;
|
|
52
|
+
const state = value.state;
|
|
53
|
+
if (typeof provider !== "string" || provider.length === 0) throw malformedStatus(statusPath);
|
|
54
|
+
if (typeof promptDigest !== "string" || promptDigest.length === 0) throw malformedStatus(statusPath);
|
|
55
|
+
if (state !== "queued" && state !== "running" && state !== "completed" && state !== "failed" && state !== "stopped" && state !== "blocked") throw malformedStatus(statusPath);
|
|
56
|
+
if (value.options !== undefined && !isRecord(value.options)) throw malformedStatus(statusPath);
|
|
57
|
+
const providerJobId = value.providerJobId;
|
|
58
|
+
const handleUrl = value.handleUrl;
|
|
59
|
+
const conversationUrl = value.conversationUrl;
|
|
60
|
+
const resultArtifactPath = value.resultArtifactPath;
|
|
61
|
+
const failureCode = value.failureCode;
|
|
62
|
+
const failureMessage = value.failureMessage;
|
|
63
|
+
const blockingJobId = value.blockingJobId;
|
|
64
|
+
const startedAt = value.startedAt;
|
|
65
|
+
const updatedAt = value.updatedAt;
|
|
66
|
+
if (providerJobId !== undefined && (typeof providerJobId !== "string" || providerJobId.length === 0)) throw malformedStatus(statusPath);
|
|
67
|
+
if (handleUrl !== undefined && typeof handleUrl !== "string") throw malformedStatus(statusPath);
|
|
68
|
+
if (conversationUrl !== undefined && typeof conversationUrl !== "string") throw malformedStatus(statusPath);
|
|
69
|
+
if (resultArtifactPath !== undefined && typeof resultArtifactPath !== "string") throw malformedStatus(statusPath);
|
|
70
|
+
if (failureCode !== undefined && typeof failureCode !== "string") throw malformedStatus(statusPath);
|
|
71
|
+
if (failureMessage !== undefined && typeof failureMessage !== "string") throw malformedStatus(statusPath);
|
|
72
|
+
if (blockingJobId !== undefined && typeof blockingJobId !== "string") throw malformedStatus(statusPath);
|
|
73
|
+
if (startedAt !== undefined && typeof startedAt !== "number") throw malformedStatus(statusPath);
|
|
74
|
+
if (updatedAt !== undefined && typeof updatedAt !== "number") throw malformedStatus(statusPath);
|
|
75
|
+
return {
|
|
76
|
+
provider,
|
|
77
|
+
promptDigest,
|
|
78
|
+
options: value.options ?? {},
|
|
79
|
+
state,
|
|
80
|
+
...(providerJobId ? { providerJobId } : {}),
|
|
81
|
+
...(handleUrl ? { handleUrl } : {}),
|
|
82
|
+
...(conversationUrl ? { conversationUrl } : {}),
|
|
83
|
+
...(resultArtifactPath ? { resultArtifactPath } : {}),
|
|
84
|
+
...(failureCode ? { failureCode } : {}),
|
|
85
|
+
...(failureMessage ? { failureMessage } : {}),
|
|
86
|
+
...(blockingJobId ? { blockingJobId } : {}),
|
|
87
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
88
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readExistingExternalJob(asyncDir: string, stepIndex: number): ExternalJobStatus | undefined {
|
|
93
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
94
|
+
let raw: string;
|
|
95
|
+
try {
|
|
96
|
+
raw = fs.readFileSync(statusPath, "utf-8");
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (isNotFound(error)) return undefined;
|
|
99
|
+
throw new ExternalJobProviderError(`Unreadable external-job status '${statusPath}'. Refusing to redispatch the prompt.`, { code: "status-unreadable", cause: error });
|
|
100
|
+
}
|
|
101
|
+
let parsed: unknown;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(raw);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw new ExternalJobProviderError(`Malformed external-job status '${statusPath}'. Refusing to redispatch the prompt.`, { code: "status-unreadable", cause: error });
|
|
106
|
+
}
|
|
107
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.steps)) throw malformedStatus(statusPath);
|
|
108
|
+
const step = parsed.steps[stepIndex];
|
|
109
|
+
if (step === undefined || !isRecord(step)) throw malformedStatus(statusPath);
|
|
110
|
+
if (step.externalJob === undefined) return undefined;
|
|
111
|
+
return parseExternalJobStatus(step.externalJob, statusPath);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function statusFromHandle(input: {
|
|
115
|
+
provider: string;
|
|
116
|
+
promptDigest: string;
|
|
117
|
+
options: Record<string, unknown>;
|
|
118
|
+
startedAt?: number;
|
|
119
|
+
previous?: ExternalJobStatus;
|
|
120
|
+
handle: ExternalJobHandle;
|
|
121
|
+
resultArtifactPath?: string;
|
|
122
|
+
}): ExternalJobStatus {
|
|
123
|
+
return {
|
|
124
|
+
provider: input.provider,
|
|
125
|
+
providerJobId: input.handle.providerJobId,
|
|
126
|
+
promptDigest: input.promptDigest,
|
|
127
|
+
options: input.options,
|
|
128
|
+
handleUrl: input.handle.handleUrl ?? input.previous?.handleUrl,
|
|
129
|
+
conversationUrl: input.handle.conversationUrl ?? input.previous?.conversationUrl,
|
|
130
|
+
resultArtifactPath: input.resultArtifactPath ?? input.previous?.resultArtifactPath,
|
|
131
|
+
state: input.handle.state,
|
|
132
|
+
failureCode: input.handle.failureCode,
|
|
133
|
+
failureMessage: input.handle.failureMessage,
|
|
134
|
+
blockingJobId: input.handle.blockingJobId,
|
|
135
|
+
startedAt: input.previous?.startedAt ?? input.startedAt,
|
|
136
|
+
updatedAt: Date.now(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function failureStatus(input: {
|
|
141
|
+
provider: string;
|
|
142
|
+
promptDigest: string;
|
|
143
|
+
options: Record<string, unknown>;
|
|
144
|
+
previous?: ExternalJobStatus;
|
|
145
|
+
code: string;
|
|
146
|
+
message: string;
|
|
147
|
+
blockingJobId?: string;
|
|
148
|
+
}): ExternalJobStatus {
|
|
149
|
+
return {
|
|
150
|
+
provider: input.provider,
|
|
151
|
+
providerJobId: input.previous?.providerJobId,
|
|
152
|
+
promptDigest: input.promptDigest,
|
|
153
|
+
options: input.options,
|
|
154
|
+
handleUrl: input.previous?.handleUrl,
|
|
155
|
+
conversationUrl: input.previous?.conversationUrl,
|
|
156
|
+
resultArtifactPath: input.previous?.resultArtifactPath,
|
|
157
|
+
state: input.blockingJobId ? "blocked" : "failed",
|
|
158
|
+
failureCode: input.code,
|
|
159
|
+
failureMessage: input.message,
|
|
160
|
+
blockingJobId: input.blockingJobId,
|
|
161
|
+
startedAt: input.previous?.startedAt,
|
|
162
|
+
updatedAt: Date.now(),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resultOutput(result: ExternalJobResult, artifactPath: string | undefined): string {
|
|
167
|
+
if (result.output?.trim()) return result.output.trim();
|
|
168
|
+
if (artifactPath) return `External job finished. Result artifact: ${artifactPath}`;
|
|
169
|
+
return "External job finished without text output.";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function blocksStartRedispatch(status: ExternalJobStatus, provider: string, promptDigest: string): boolean {
|
|
173
|
+
return status.provider === provider
|
|
174
|
+
&& status.promptDigest === promptDigest
|
|
175
|
+
&& !status.providerJobId
|
|
176
|
+
&& status.failureCode !== "provider-unavailable";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function runExternalJob(input: {
|
|
180
|
+
provider: string;
|
|
181
|
+
options?: Record<string, unknown>;
|
|
182
|
+
cwd: string;
|
|
183
|
+
prompt: string;
|
|
184
|
+
asyncDir: string;
|
|
185
|
+
stepIndex: number;
|
|
186
|
+
runId: string;
|
|
187
|
+
agent: string;
|
|
188
|
+
sessionId?: string;
|
|
189
|
+
registerTimeout?: (stop: (() => void) | undefined) => void;
|
|
190
|
+
registerStop?: (stop: (() => void) | undefined) => void;
|
|
191
|
+
timeoutMessage?: string;
|
|
192
|
+
stopMessage?: string;
|
|
193
|
+
onExternalJob?: (status: ExternalJobStatus) => void;
|
|
194
|
+
}): Promise<ExternalJobRunResult> {
|
|
195
|
+
const provider = input.provider;
|
|
196
|
+
const options = input.options ?? {};
|
|
197
|
+
const promptDigest = externalJobPromptDigest(input.prompt);
|
|
198
|
+
const startedAt = Date.now();
|
|
199
|
+
let timedOut = false;
|
|
200
|
+
let stopped = false;
|
|
201
|
+
let current: ExternalJobStatus | undefined;
|
|
202
|
+
const timeout = () => { timedOut = true; };
|
|
203
|
+
const stop = () => { stopped = true; };
|
|
204
|
+
input.registerTimeout?.(timeout);
|
|
205
|
+
input.registerStop?.(stop);
|
|
206
|
+
const publish = (status: ExternalJobStatus) => {
|
|
207
|
+
current = status;
|
|
208
|
+
input.onExternalJob?.(status);
|
|
209
|
+
};
|
|
210
|
+
const localCancellation = () => {
|
|
211
|
+
if (!timedOut && !stopped) return undefined;
|
|
212
|
+
const message = stopped
|
|
213
|
+
? input.stopMessage ?? `Subagent stopped locally before external provider '${provider}' returned a job id. The provider start may still be running.`
|
|
214
|
+
: input.timeoutMessage ?? `Subagent timed out locally before external provider '${provider}' returned a job id. The provider start may still be running.`;
|
|
215
|
+
return new ExternalJobProviderError(message, { code: stopped ? "local-stop" : "local-timeout" });
|
|
216
|
+
};
|
|
217
|
+
try {
|
|
218
|
+
current = readExistingExternalJob(input.asyncDir, input.stepIndex);
|
|
219
|
+
let handle: ExternalJobHandle;
|
|
220
|
+
if (current?.providerJobId) {
|
|
221
|
+
if (current.provider !== provider || current.promptDigest !== promptDigest) {
|
|
222
|
+
const message = `Existing external job '${current.providerJobId}' does not match provider or prompt digest. Refusing to redispatch prompt.`;
|
|
223
|
+
const status = failureStatus({ provider, promptDigest, options, previous: current, code: "recovery-mismatch", message });
|
|
224
|
+
publish(status);
|
|
225
|
+
return { output: message, exitCode: 1, error: message, externalJob: status };
|
|
226
|
+
}
|
|
227
|
+
handle = await requestExternalJobOperation<ExternalJobHandle>(input.asyncDir, { operation: "reattach", provider, providerJobId: current.providerJobId });
|
|
228
|
+
} else {
|
|
229
|
+
if (current && blocksStartRedispatch(current, provider, promptDigest)) {
|
|
230
|
+
const message = `External-job start for provider '${provider}' previously ended without a durable provider job id. Refusing to redispatch the prompt automatically.`;
|
|
231
|
+
const status = failureStatus({ provider, promptDigest, options, previous: current, code: "start-redispatch-blocked", message });
|
|
232
|
+
publish(status);
|
|
233
|
+
return { output: message, exitCode: 1, error: message, externalJob: status };
|
|
234
|
+
}
|
|
235
|
+
publish({ provider, promptDigest, options, state: "queued", startedAt, updatedAt: Date.now() });
|
|
236
|
+
handle = await requestExternalJobOperation<ExternalJobHandle>(input.asyncDir, {
|
|
237
|
+
operation: "start",
|
|
238
|
+
provider,
|
|
239
|
+
start: {
|
|
240
|
+
prompt: input.prompt,
|
|
241
|
+
promptDigest,
|
|
242
|
+
cwd: input.cwd,
|
|
243
|
+
runId: input.runId,
|
|
244
|
+
stepIndex: input.stepIndex,
|
|
245
|
+
agent: input.agent,
|
|
246
|
+
options,
|
|
247
|
+
...(input.sessionId ? { sessionId: input.sessionId } : {}),
|
|
248
|
+
},
|
|
249
|
+
}, undefined, localCancellation);
|
|
250
|
+
}
|
|
251
|
+
publish(statusFromHandle({ provider, promptDigest, options, startedAt, previous: current, handle }));
|
|
252
|
+
while (!terminal(handle.state)) {
|
|
253
|
+
if (timedOut || stopped) {
|
|
254
|
+
const message = stopped
|
|
255
|
+
? input.stopMessage ?? `Subagent stopped locally; external provider job '${handle.providerJobId}' may still be running.`
|
|
256
|
+
: input.timeoutMessage ?? `Subagent timed out locally; external provider job '${handle.providerJobId}' may still be running.`;
|
|
257
|
+
return { output: message, exitCode: 1, error: message, ...(timedOut ? { timedOut: true } : {}), ...(stopped ? { stopped: true } : {}), externalJob: current! };
|
|
258
|
+
}
|
|
259
|
+
await sleep(STATUS_POLL_INTERVAL_MS);
|
|
260
|
+
handle = await requestExternalJobOperation<ExternalJobHandle>(input.asyncDir, { operation: "status", provider, providerJobId: handle.providerJobId });
|
|
261
|
+
publish(statusFromHandle({ provider, promptDigest, options, previous: current, handle }));
|
|
262
|
+
}
|
|
263
|
+
const result = await requestExternalJobOperation<ExternalJobResult>(input.asyncDir, { operation: "result", provider, providerJobId: handle.providerJobId });
|
|
264
|
+
let artifactPath = result.artifactPath;
|
|
265
|
+
if (!artifactPath && result.output !== undefined) {
|
|
266
|
+
artifactPath = path.join(input.asyncDir, `external-job-${input.stepIndex}.result.md`);
|
|
267
|
+
fs.writeFileSync(artifactPath, result.output, "utf-8");
|
|
268
|
+
}
|
|
269
|
+
const finalStatus = statusFromHandle({ provider, promptDigest, options, previous: current, handle: result, resultArtifactPath: artifactPath });
|
|
270
|
+
publish(finalStatus);
|
|
271
|
+
const output = resultOutput(result, artifactPath);
|
|
272
|
+
const error = result.state === "completed" ? undefined : result.failureMessage ?? `External job ${result.state}.`;
|
|
273
|
+
return { output, exitCode: result.state === "completed" ? 0 : 1, ...(error ? { error } : {}), externalJob: finalStatus };
|
|
274
|
+
} catch (error) {
|
|
275
|
+
const providerError = error instanceof ExternalJobProviderError
|
|
276
|
+
? error
|
|
277
|
+
: new ExternalJobProviderError(error instanceof Error ? error.message : String(error), { code: "provider-error", cause: error });
|
|
278
|
+
const message = formatError(providerError, provider);
|
|
279
|
+
const status = failureStatus({ provider, promptDigest, options, previous: current, code: providerError.code, message, ...(providerError.blockingJobId ? { blockingJobId: providerError.blockingJobId } : {}) });
|
|
280
|
+
publish(status);
|
|
281
|
+
return { output: message, exitCode: 1, error: message, ...(timedOut ? { timedOut: true } : {}), ...(stopped ? { stopped: true } : {}), externalJob: status };
|
|
282
|
+
} finally {
|
|
283
|
+
input.registerTimeout?.(undefined);
|
|
284
|
+
input.registerStop?.(undefined);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -32,6 +32,12 @@ interface ServerEntry {
|
|
|
32
32
|
cwd?: string;
|
|
33
33
|
url?: string;
|
|
34
34
|
headers?: Record<string, string>;
|
|
35
|
+
requestHeadersCommand?: {
|
|
36
|
+
command: string;
|
|
37
|
+
args?: string[];
|
|
38
|
+
env?: Record<string, string>;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
};
|
|
35
41
|
auth?: "oauth" | "bearer" | false;
|
|
36
42
|
bearerToken?: string;
|
|
37
43
|
bearerTokenEnv?: string;
|
|
@@ -279,6 +285,14 @@ export function computeMcpServerHash(definition: ServerEntry): string {
|
|
|
279
285
|
cwd: resolveConfigPath(definition.cwd),
|
|
280
286
|
url: resolveServerUrl(definition),
|
|
281
287
|
headers: interpolateEnvRecord(definition.headers),
|
|
288
|
+
requestHeadersCommand: definition.requestHeadersCommand
|
|
289
|
+
? {
|
|
290
|
+
command: interpolateEnvVars(definition.requestHeadersCommand.command),
|
|
291
|
+
args: definition.requestHeadersCommand.args?.map(interpolateEnvVars),
|
|
292
|
+
env: interpolateEnvRecord(definition.requestHeadersCommand.env),
|
|
293
|
+
timeoutMs: definition.requestHeadersCommand.timeoutMs,
|
|
294
|
+
}
|
|
295
|
+
: undefined,
|
|
282
296
|
auth: definition.auth,
|
|
283
297
|
bearerToken: resolveBearerToken(definition),
|
|
284
298
|
bearerTokenEnv: definition.bearerTokenEnv,
|
|
@@ -163,6 +163,29 @@ export function resolveModelCandidate(
|
|
|
163
163
|
return model;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
function resolveSubagentModelCandidate(
|
|
167
|
+
model: string,
|
|
168
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
169
|
+
preferredProvider?: string,
|
|
170
|
+
): string | undefined {
|
|
171
|
+
if (!availableModels || availableModels.length === 0) return model;
|
|
172
|
+
const resolvedWhole = resolveBaseModelCandidate(model, availableModels, preferredProvider);
|
|
173
|
+
if (resolvedWhole) return resolvedWhole;
|
|
174
|
+
const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
|
|
175
|
+
const resolvedBase = thinkingSuffix ? resolveBaseModelCandidate(baseModel, availableModels, preferredProvider) : undefined;
|
|
176
|
+
return resolvedBase ? `${resolvedBase}${thinkingSuffix}` : undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function resolveRequiredSubagentModelCandidate(
|
|
180
|
+
model: string,
|
|
181
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
182
|
+
preferredProvider?: string,
|
|
183
|
+
): string {
|
|
184
|
+
const resolved = resolveSubagentModelCandidate(model, availableModels, preferredProvider);
|
|
185
|
+
if (resolved) return resolved;
|
|
186
|
+
throw new Error(`Unknown subagent model '${model}' in the active Pi model registry.`);
|
|
187
|
+
}
|
|
188
|
+
|
|
166
189
|
export interface ResolveSubagentModelOverrideOptions {
|
|
167
190
|
/** When set with `enforce: true`, out-of-scope models are rejected. */
|
|
168
191
|
scope?: ModelScopeConfig;
|
|
@@ -207,7 +230,7 @@ export function resolveSubagentModelOverride(
|
|
|
207
230
|
if (explicit === undefined) {
|
|
208
231
|
resolved = parentModel ? `${parentModel.provider}/${parentModel.id}` : undefined;
|
|
209
232
|
} else {
|
|
210
|
-
resolved =
|
|
233
|
+
resolved = resolveRequiredSubagentModelCandidate(explicit, availableModels, preferredProvider);
|
|
211
234
|
}
|
|
212
235
|
if (resolved && options?.scope?.enforce) {
|
|
213
236
|
const source: ModelSource = explicit === undefined ? "inherited" : (options.source ?? "inherited");
|
|
@@ -264,8 +287,15 @@ export function buildModelCandidates(
|
|
|
264
287
|
for (let index = 0; index < rawCandidates.length; index++) {
|
|
265
288
|
const raw = rawCandidates[index];
|
|
266
289
|
if (!raw) continue;
|
|
267
|
-
const
|
|
268
|
-
|
|
290
|
+
const model = raw.trim();
|
|
291
|
+
const normalized = index === 0
|
|
292
|
+
? resolveRequiredSubagentModelCandidate(model, availableModels, preferredProvider)
|
|
293
|
+
: resolveSubagentModelCandidate(model, availableModels, preferredProvider);
|
|
294
|
+
if (!normalized) {
|
|
295
|
+
console.warn(`[pi-subagents] Skipping fallback model '${model}' because it is unavailable in this environment.`);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (seen.has(normalized)) continue;
|
|
269
299
|
if ((index > 0 || options?.scope?.strict === true) && options?.scope?.enforce) {
|
|
270
300
|
const violation = checkModelScope(normalized, options.scope, "inherited");
|
|
271
301
|
if (violation) {
|
|
@@ -281,6 +311,7 @@ export function buildModelCandidates(
|
|
|
281
311
|
|
|
282
312
|
const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
283
313
|
/rate\s*limit/i,
|
|
314
|
+
/usage\s*limit/i,
|
|
284
315
|
/too many requests/i,
|
|
285
316
|
/\b429\b/,
|
|
286
317
|
/quota/i,
|
|
@@ -34,6 +34,7 @@ import { THINKING_LEVELS } from "../../shared/model-info.ts";
|
|
|
34
34
|
export const NESTED_EVENTS_DIR = path.join(TEMP_ROOT_DIR, "nested-subagent-events");
|
|
35
35
|
const ROUTE_FILE = "route.json";
|
|
36
36
|
const REGISTRY_FILE = "registry.json";
|
|
37
|
+
const ROUTE_INDEX_DIR = ".route-index";
|
|
37
38
|
const MAX_EVENT_BYTES = 64 * 1024;
|
|
38
39
|
const MAX_STEPS = 12;
|
|
39
40
|
const MAX_CHILDREN = 16;
|
|
@@ -105,6 +106,18 @@ function commonRouteRoot(route: Pick<NestedRoute, "eventSink" | "controlInbox">)
|
|
|
105
106
|
return path.dirname(path.resolve(route.eventSink));
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
function routeIndexRoot(): string {
|
|
110
|
+
return path.join(NESTED_EVENTS_DIR, ROUTE_INDEX_DIR, "roots");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function routeIndexDir(rootRunId: string): string {
|
|
114
|
+
return path.join(routeIndexRoot(), encodeURIComponent(rootRunId));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function routeIndexPath(rootRunId: string, capabilityToken: string): string {
|
|
118
|
+
return path.join(routeIndexDir(rootRunId), `${encodeURIComponent(capabilityToken)}.json`);
|
|
119
|
+
}
|
|
120
|
+
|
|
108
121
|
function validateRouteShape(route: NestedRoute): void {
|
|
109
122
|
assertSafeId("rootRunId", route.rootRunId);
|
|
110
123
|
assertSafeId("capabilityToken", route.capabilityToken);
|
|
@@ -121,7 +134,10 @@ export function createNestedRoute(rootRunId: string): NestedRoute {
|
|
|
121
134
|
const controlInbox = path.join(routeRoot, "controls");
|
|
122
135
|
fs.mkdirSync(eventSink, { recursive: true, mode: 0o700 });
|
|
123
136
|
fs.mkdirSync(controlInbox, { recursive: true, mode: 0o700 });
|
|
124
|
-
|
|
137
|
+
const createdAt = Date.now();
|
|
138
|
+
fs.mkdirSync(routeIndexDir(rootRunId), { recursive: true, mode: 0o700 });
|
|
139
|
+
writeAtomicJson(routeIndexPath(rootRunId, capabilityToken), { rootRunId, capabilityToken, routeRoot: path.basename(routeRoot), createdAt });
|
|
140
|
+
writeAtomicJson(path.join(routeRoot, ROUTE_FILE), { rootRunId, capabilityToken, createdAt });
|
|
125
141
|
return { rootRunId, eventSink, controlInbox, capabilityToken };
|
|
126
142
|
}
|
|
127
143
|
|
|
@@ -515,68 +531,57 @@ function registryPath(route: NestedRoute): string {
|
|
|
515
531
|
return path.join(commonRouteRoot(route), REGISTRY_FILE);
|
|
516
532
|
}
|
|
517
533
|
|
|
534
|
+
function routeFromIndexEntry(rootRunId: string, filePath: string): NestedRoute | undefined {
|
|
535
|
+
try {
|
|
536
|
+
const metadata = JSON.parse(fs.readFileSync(filePath, "utf-8")) as { rootRunId?: unknown; capabilityToken?: unknown; routeRoot?: unknown; createdAt?: unknown };
|
|
537
|
+
if (metadata.rootRunId !== rootRunId || typeof metadata.capabilityToken !== "string" || typeof metadata.routeRoot !== "string") return undefined;
|
|
538
|
+
const routeRoot = path.join(NESTED_EVENTS_DIR, path.basename(metadata.routeRoot));
|
|
539
|
+
const route = {
|
|
540
|
+
rootRunId,
|
|
541
|
+
eventSink: path.join(routeRoot, "events"),
|
|
542
|
+
controlInbox: path.join(routeRoot, "controls"),
|
|
543
|
+
capabilityToken: metadata.capabilityToken,
|
|
544
|
+
};
|
|
545
|
+
validateRouteShape(route);
|
|
546
|
+
if (!fs.statSync(route.eventSink).isDirectory() || !fs.statSync(route.controlInbox).isDirectory()) return undefined;
|
|
547
|
+
const routeFilePath = path.join(routeRoot, ROUTE_FILE);
|
|
548
|
+
try {
|
|
549
|
+
const routeFile = JSON.parse(fs.readFileSync(routeFilePath, "utf-8")) as { rootRunId?: unknown; capabilityToken?: unknown };
|
|
550
|
+
if (routeFile.rootRunId !== rootRunId || routeFile.capabilityToken !== metadata.capabilityToken) return undefined;
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return undefined;
|
|
553
|
+
try {
|
|
554
|
+
writeAtomicJson(routeFilePath, { rootRunId, capabilityToken: metadata.capabilityToken, createdAt: typeof metadata.createdAt === "number" ? metadata.createdAt : Date.now() });
|
|
555
|
+
} catch {
|
|
556
|
+
// The route index already carries enough data for reload lookup.
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return route;
|
|
560
|
+
} catch {
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
518
565
|
export function findNestedRouteForRootId(rootRunId: string): NestedRoute | undefined {
|
|
519
566
|
assertSafeId("rootRunId", rootRunId);
|
|
520
567
|
let entries: string[];
|
|
521
568
|
try {
|
|
522
|
-
entries = fs.readdirSync(
|
|
569
|
+
entries = fs.readdirSync(routeIndexDir(rootRunId));
|
|
523
570
|
} catch (error) {
|
|
524
571
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
525
572
|
throw error;
|
|
526
573
|
}
|
|
527
574
|
for (const entry of entries) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
try {
|
|
531
|
-
const metadata = JSON.parse(fs.readFileSync(path.join(routeRoot, ROUTE_FILE), "utf-8")) as { rootRunId?: unknown; capabilityToken?: unknown };
|
|
532
|
-
if (metadata.rootRunId !== rootRunId || typeof metadata.capabilityToken !== "string") continue;
|
|
533
|
-
const route = {
|
|
534
|
-
rootRunId,
|
|
535
|
-
eventSink: path.join(routeRoot, "events"),
|
|
536
|
-
controlInbox: path.join(routeRoot, "controls"),
|
|
537
|
-
capabilityToken: metadata.capabilityToken,
|
|
538
|
-
};
|
|
539
|
-
validateRouteShape(route);
|
|
540
|
-
return route;
|
|
541
|
-
} catch {
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
575
|
+
const route = routeFromIndexEntry(rootRunId, path.join(routeIndexDir(rootRunId), entry));
|
|
576
|
+
if (route) return route;
|
|
544
577
|
}
|
|
545
578
|
return undefined;
|
|
546
579
|
}
|
|
547
580
|
|
|
548
|
-
/**
|
|
549
|
-
* Scan the nested-events directory once and index every route by its root run
|
|
550
|
-
* id. Use this when resolving routes for many runs (e.g. listAsyncRuns) so the
|
|
551
|
-
* cost is O(routes) total instead of O(runs * routes) from calling
|
|
552
|
-
* findNestedRouteForRootId per run.
|
|
553
|
-
*/
|
|
554
581
|
export function buildNestedRouteIndex(): Map<string, NestedRoute> {
|
|
555
|
-
let entries: string[];
|
|
556
|
-
try {
|
|
557
|
-
entries = fs.readdirSync(NESTED_EVENTS_DIR);
|
|
558
|
-
} catch (error) {
|
|
559
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return new Map();
|
|
560
|
-
throw error;
|
|
561
|
-
}
|
|
562
582
|
const index = new Map<string, NestedRoute>();
|
|
563
|
-
for (const
|
|
564
|
-
|
|
565
|
-
try {
|
|
566
|
-
const metadata = JSON.parse(fs.readFileSync(path.join(routeRoot, ROUTE_FILE), "utf-8")) as { rootRunId?: unknown; capabilityToken?: unknown };
|
|
567
|
-
if (typeof metadata.rootRunId !== "string" || typeof metadata.capabilityToken !== "string") continue;
|
|
568
|
-
if (index.has(metadata.rootRunId)) continue;
|
|
569
|
-
const route: NestedRoute = {
|
|
570
|
-
rootRunId: metadata.rootRunId,
|
|
571
|
-
eventSink: path.join(routeRoot, "events"),
|
|
572
|
-
controlInbox: path.join(routeRoot, "controls"),
|
|
573
|
-
capabilityToken: metadata.capabilityToken,
|
|
574
|
-
};
|
|
575
|
-
validateRouteShape(route);
|
|
576
|
-
index.set(metadata.rootRunId, route);
|
|
577
|
-
} catch {
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
583
|
+
for (const route of listNestedRoutes()) {
|
|
584
|
+
if (!index.has(route.rootRunId)) index.set(route.rootRunId, route);
|
|
580
585
|
}
|
|
581
586
|
return index;
|
|
582
587
|
}
|
|
@@ -630,27 +635,26 @@ function collectScopedNestedRuns(children: NestedRunSummary[] | undefined, scope
|
|
|
630
635
|
}
|
|
631
636
|
|
|
632
637
|
function listNestedRoutes(): NestedRoute[] {
|
|
633
|
-
let
|
|
638
|
+
let rootEntries: string[];
|
|
634
639
|
try {
|
|
635
|
-
|
|
640
|
+
rootEntries = fs.readdirSync(routeIndexRoot());
|
|
636
641
|
} catch (error) {
|
|
637
642
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
638
643
|
throw error;
|
|
639
644
|
}
|
|
640
645
|
const routes: NestedRoute[] = [];
|
|
641
|
-
for (const
|
|
642
|
-
|
|
646
|
+
for (const rootEntry of rootEntries) {
|
|
647
|
+
let rootRunId: string;
|
|
643
648
|
try {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
routes.push(route);
|
|
649
|
+
rootRunId = decodeURIComponent(rootEntry);
|
|
650
|
+
} catch {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
for (const entry of fs.readdirSync(path.join(routeIndexRoot(), rootEntry))) {
|
|
655
|
+
const route = routeFromIndexEntry(rootRunId, path.join(routeIndexRoot(), rootEntry, entry));
|
|
656
|
+
if (route) routes.push(route);
|
|
657
|
+
}
|
|
654
658
|
} catch {
|
|
655
659
|
continue;
|
|
656
660
|
}
|