pi-subagents 0.52.0 → 0.53.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 +58 -0
- package/README.md +4 -0
- package/docs/configuration.md +11 -1
- package/docs/extension-api.md +3 -1
- package/docs/workflows.md +2 -0
- package/package.json +2 -1
- package/prompts/council.md +48 -0
- package/skills/council-mode/SKILL.md +230 -0
- package/skills/pi-subagents/SKILL.md +2 -0
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -0
- package/skills/pi-subagents/references/execution-controls.md +11 -0
- package/skills/pi-subagents/references/multi-lane-orchestration.md +39 -0
- package/src/agents/agent-management.ts +22 -3
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +29 -13
- package/src/agents/builtin-names.ts +9 -0
- package/src/agents/runtime-agent-registry.ts +418 -0
- package/src/api/agents.ts +7 -0
- package/src/api/external-job-provider.ts +3 -2
- package/src/api/preflight.ts +1 -1
- package/src/extension/config.ts +3 -0
- package/src/extension/doctor.ts +1 -0
- package/src/extension/index.ts +17 -2
- package/src/extension/rpc.ts +41 -1
- package/src/extension/schemas.ts +7 -4
- package/src/extension/tool-description.ts +2 -2
- package/src/runs/background/async-execution.ts +2 -1
- package/src/runs/background/async-job-tracker.ts +4 -3
- package/src/runs/background/async-resume.ts +2 -1
- package/src/runs/background/async-status-snapshot.ts +14 -5
- package/src/runs/background/auto-drain.ts +1 -0
- package/src/runs/background/result-watcher.ts +8 -0
- package/src/runs/background/subagent-runner.ts +7 -4
- package/src/runs/background/subagent-wait.ts +9 -5
- package/src/runs/background/terminal-run-index.ts +15 -6
- package/src/runs/background/wait-tool.ts +1 -0
- package/src/runs/foreground/execution.ts +5 -1
- package/src/runs/foreground/subagent-executor.ts +182 -46
- package/src/runs/foreground/workflow-detach-reconcile.ts +83 -15
- package/src/runs/shared/acceptance.ts +44 -1
- package/src/runs/shared/model-exclusions.ts +242 -0
- package/src/runs/shared/model-fallback.ts +55 -2
- package/src/runs/shared/subagent-control.ts +25 -3
- package/src/shared/fork-context.ts +17 -1
- package/src/shared/model-info.ts +20 -0
- package/src/shared/settings.ts +2 -2
- package/src/shared/types.ts +35 -0
- package/src/slash/slash-commands.ts +20 -6
- package/src/slash/slash-live-state.ts +3 -3
- package/src/tui/fleet-status.ts +86 -1
- package/src/tui/fleet.ts +55 -2
- package/src/tui/render.ts +73 -3
- package/src/workflows/scripted-workflow.ts +100 -12
- package/src/workflows/workflow-receipt.ts +140 -0
package/src/extension/index.ts
CHANGED
|
@@ -19,7 +19,8 @@ import * as path from "node:path";
|
|
|
19
19
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
20
20
|
import { keyText, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
|
|
22
|
-
import { discoverAgents } from "../agents/agents.ts";
|
|
22
|
+
import { discoverAgents, discoverAgentsAll, type AgentConfig, type AgentScope } from "../agents/agents.ts";
|
|
23
|
+
import { clearRuntimeAgentsForPi, listRuntimeAgentConfigs, mergeRuntimeAgents } from "../agents/runtime-agent-registry.ts";
|
|
23
24
|
import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
|
|
24
25
|
import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
|
|
25
26
|
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
@@ -489,6 +490,18 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
489
490
|
if (scheduledRunManager.observedCompletionRunIds().size > 0) return true;
|
|
490
491
|
return missionObserverResultCandidateFiles(DIRS.results).length > 0;
|
|
491
492
|
};
|
|
493
|
+
const discoverAgentsForRuntime = (cwd: string, scope: AgentScope) => {
|
|
494
|
+
const discovered = discoverAgents(cwd, scope);
|
|
495
|
+
if (listRuntimeAgentConfigs(pi).length === 0) return discovered;
|
|
496
|
+
const all = discoverAgentsAll(cwd);
|
|
497
|
+
const configuredAgents: AgentConfig[] = [
|
|
498
|
+
...all.builtin,
|
|
499
|
+
...all.package,
|
|
500
|
+
...all.user,
|
|
501
|
+
...all.project,
|
|
502
|
+
];
|
|
503
|
+
return mergeRuntimeAgents(pi, discovered, configuredAgents);
|
|
504
|
+
};
|
|
492
505
|
const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs, dispose: disposeAsyncJobTracker } = createAsyncJobTracker(pi, state, DIRS.async, {
|
|
493
506
|
widgetEnabled: asyncWidgetEnabled,
|
|
494
507
|
onJobTerminal: () => refreshResultDelivery(),
|
|
@@ -504,6 +517,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
504
517
|
observedCompletionRunIds: () => scheduledRunManager.observedCompletionRunIds(),
|
|
505
518
|
hasDeliveryDemand: hasResultDeliveryDemand,
|
|
506
519
|
deliverIntercomResults: config.intercomBridge?.resultDelivery === true,
|
|
520
|
+
resultScanLogging: config.resultScanLogging ?? "all",
|
|
507
521
|
},
|
|
508
522
|
);
|
|
509
523
|
const { startResultWatcher, primeExistingResults, stopResultWatcher } = resultWatcher;
|
|
@@ -538,7 +552,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
538
552
|
tempArtifactsDir,
|
|
539
553
|
getSubagentSessionRoot,
|
|
540
554
|
expandTilde,
|
|
541
|
-
discoverAgents,
|
|
555
|
+
discoverAgents: discoverAgentsForRuntime,
|
|
542
556
|
activateSupervisorTransport: () => supervisorChannel.activateTransport(),
|
|
543
557
|
refreshResultDelivery: () => refreshResultDelivery(),
|
|
544
558
|
});
|
|
@@ -881,6 +895,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
881
895
|
if (runtimeCleaned) return;
|
|
882
896
|
runtimeCleaned = true;
|
|
883
897
|
const shuttingDownParentSession = parentSessionEnvValue;
|
|
898
|
+
clearRuntimeAgentsForPi(pi);
|
|
884
899
|
clearTimeout(resultIndexCleanupTimer);
|
|
885
900
|
clearTimeout(asyncRetentionTimer);
|
|
886
901
|
asyncRetentionAbort.abort();
|
package/src/extension/rpc.ts
CHANGED
|
@@ -27,7 +27,7 @@ export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
|
|
|
27
27
|
export const SUBAGENT_RPC_READY_EVENT = "subagents:rpc:v1:ready";
|
|
28
28
|
export const SUBAGENT_RPC_REPLY_EVENT_PREFIX = "subagents:rpc:v1:reply:";
|
|
29
29
|
|
|
30
|
-
export const SUBAGENT_RPC_METHODS = ["ping", "status", "spawn", "steer", "interrupt", "stop", "resume"] as const;
|
|
30
|
+
export const SUBAGENT_RPC_METHODS = ["ping", "status", "manage", "spawn", "steer", "interrupt", "stop", "resume"] as const;
|
|
31
31
|
export type SubagentRpcMethod = typeof SUBAGENT_RPC_METHODS[number];
|
|
32
32
|
|
|
33
33
|
export interface SubagentRpcRequestEnvelope {
|
|
@@ -58,6 +58,18 @@ export type SubagentRpcReplyEnvelope<T = unknown> = {
|
|
|
58
58
|
};
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
export const SUBAGENT_RPC_MANAGEMENT_ACTIONS = [
|
|
62
|
+
"schedule.list",
|
|
63
|
+
"schedule.show",
|
|
64
|
+
"schedule.history",
|
|
65
|
+
"schedule.pause",
|
|
66
|
+
"schedule.resume",
|
|
67
|
+
"schedule.run",
|
|
68
|
+
"schedule.delete",
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
type SubagentRpcManagementAction = typeof SUBAGENT_RPC_MANAGEMENT_ACTIONS[number];
|
|
72
|
+
|
|
61
73
|
type SubagentRpcErrorCode =
|
|
62
74
|
| "invalid_request"
|
|
63
75
|
| "invalid_params"
|
|
@@ -375,6 +387,7 @@ function pingData(ctx: ExtensionContext | null) {
|
|
|
375
387
|
methods: [...SUBAGENT_RPC_METHODS],
|
|
376
388
|
capabilities: {
|
|
377
389
|
status: true,
|
|
390
|
+
managementActions: [...SUBAGENT_RPC_MANAGEMENT_ACTIONS],
|
|
378
391
|
fleetStatus: { version: 1 },
|
|
379
392
|
asyncStatusSnapshot: { kind: ASYNC_STATUS_SNAPSHOT_KIND, version: ASYNC_STATUS_SNAPSHOT_VERSION },
|
|
380
393
|
asyncSpawn: true,
|
|
@@ -412,6 +425,30 @@ async function executeChecked(
|
|
|
412
425
|
return dataFromToolResult(result);
|
|
413
426
|
}
|
|
414
427
|
|
|
428
|
+
function manageParams(params: unknown): SubagentParamsLike {
|
|
429
|
+
const input = assertRecordParams(params, "manage");
|
|
430
|
+
if (typeof input.action !== "string" || !(SUBAGENT_RPC_MANAGEMENT_ACTIONS as readonly string[]).includes(input.action)) {
|
|
431
|
+
throw new SubagentRpcError(
|
|
432
|
+
"invalid_params",
|
|
433
|
+
`RPC manage action must be one of: ${SUBAGENT_RPC_MANAGEMENT_ACTIONS.join(", ")}.`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
if (input.id !== undefined && (typeof input.id !== "string" || !input.id.trim())) {
|
|
437
|
+
throw new SubagentRpcError("invalid_params", "RPC manage id must be a non-empty string.");
|
|
438
|
+
}
|
|
439
|
+
const action = input.action as SubagentRpcManagementAction;
|
|
440
|
+
const requiresId = action !== "schedule.list";
|
|
441
|
+
if (requiresId && typeof input.id !== "string") {
|
|
442
|
+
throw new SubagentRpcError("invalid_params", `RPC manage ${action} requires id.`);
|
|
443
|
+
}
|
|
444
|
+
const output: SubagentParamsLike = {
|
|
445
|
+
action,
|
|
446
|
+
...(typeof input.id === "string" ? { id: input.id.trim() } : {}),
|
|
447
|
+
};
|
|
448
|
+
assertSubagentParams(output, "RPC manage params");
|
|
449
|
+
return output;
|
|
450
|
+
}
|
|
451
|
+
|
|
415
452
|
function spawnParams(params: unknown): SubagentParamsLike {
|
|
416
453
|
const input = assertRecordParams(params, "spawn");
|
|
417
454
|
const normalized = normalizePublicSubagentExecution(input);
|
|
@@ -535,6 +572,9 @@ async function handleRequest(
|
|
|
535
572
|
if (request.method === "ping") return pingData(ctx);
|
|
536
573
|
if (!ctx) throw new SubagentRpcError("no_active_session", "No active extension context for subagent RPC.");
|
|
537
574
|
|
|
575
|
+
if (request.method === "manage") {
|
|
576
|
+
return executeChecked(options, ctx, request.requestId, request.method, manageParams(request.params));
|
|
577
|
+
}
|
|
538
578
|
if (request.method === "spawn") {
|
|
539
579
|
return executeChecked(options, ctx, request.requestId, request.method, spawnParams(request.params));
|
|
540
580
|
}
|
package/src/extension/schemas.ts
CHANGED
|
@@ -307,13 +307,13 @@ const SubagentParamProperties = {
|
|
|
307
307
|
],
|
|
308
308
|
description: "Agent config for create/update. Object or JSON string."
|
|
309
309
|
})),
|
|
310
|
-
workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Normally async unless asyncByDefault:false; set async:true when async matters. Use async:false only when the parent must block until completion, never for reviews or gates. Use explicit return for output. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), runs.all([...]), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]); do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout, and each must later be observed with direct await, Promise.race, or Promise.all. runs.steer targets a prior stable child key, never a raw run id, and must be awaited or returned. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Compose sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree; child fields override workflow defaults. gate is one host-run command and cannot be combined with acceptance. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
|
|
310
|
+
workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Normally async unless asyncByDefault:false; set async:true when async matters. Use async:false only when the parent must block until completion, never for reviews or gates. Use explicit return for output. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), where resume is a retained run id or {workflowRunId,key,latest:true} from a durable async workflow receipt. Use runs.all([...]), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]); do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout, and each must later be observed with direct await, Promise.race, or Promise.all. runs.steer targets a prior stable child key, never a raw run id, and must be awaited or returned. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Compose sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree; child fields override workflow defaults. gate is one host-run command and cannot be combined with acceptance. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
|
|
311
311
|
chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; it is off otherwise. Explicit live-card requires same-repository async:false; async workflows should omit chatProgress or use auto/off." })),
|
|
312
312
|
isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation. none runs in the shared cwd; worktree requires managed git worktree isolation." })),
|
|
313
313
|
worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })),
|
|
314
314
|
context: Type.Optional(Type.String({
|
|
315
|
-
enum: ["fresh", "fork"],
|
|
316
|
-
description: "'fresh' or 'fork' to branch from parent session. Explicit
|
|
315
|
+
enum: ["fresh", "fork", "profile"],
|
|
316
|
+
description: "'fresh' or 'fork' to branch from parent session, or 'profile' to require the selected agent's declared defaultContext. Explicit fresh/fork overrides every child; profile ignores config defaultSubagentContext and fails when an agent has no defaultContext. If omitted, config defaultSubagentContext wins over each agent defaultContext; implicit fork needs a persisted parent session and leaf, else fresh.",
|
|
317
317
|
})),
|
|
318
318
|
async: Type.Optional(Type.Boolean({ description: "Run in background unless asyncByDefault:false. Set false only when the parent must block until completion." })),
|
|
319
319
|
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
|
|
@@ -341,7 +341,7 @@ const SubagentParamProperties = {
|
|
|
341
341
|
})),
|
|
342
342
|
outputMode: Type.Optional(OutputModeOverride),
|
|
343
343
|
skill: Type.Optional(SkillOverride),
|
|
344
|
-
model: Type.Optional(Type.String({ description: "Default child model override
|
|
344
|
+
model: Type.Optional(Type.String({ description: "Default child model override. Full provider/id values are accepted; bare ids resolve from the active registry." })),
|
|
345
345
|
outputSchema: Type.Optional(JsonSchemaObject),
|
|
346
346
|
agentContract: Type.Optional(AgentContractOverride),
|
|
347
347
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
@@ -370,6 +370,9 @@ const SubagentWaitParamsSchema = Type.Object({
|
|
|
370
370
|
minimum: 1,
|
|
371
371
|
description: "Give up waiting after this many milliseconds (the runs keep going regardless). Defaults to 1800000 (30 minutes).",
|
|
372
372
|
})),
|
|
373
|
+
stopOnAttention: Type.Optional(Type.Boolean({
|
|
374
|
+
description: "Blocking waits stop when a run needs attention by default. Set false to keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.",
|
|
375
|
+
})),
|
|
373
376
|
});
|
|
374
377
|
|
|
375
378
|
export const SubagentWaitParams = keepTopLevelParameterDescriptions(SubagentWaitParamsSchema);
|
|
@@ -36,7 +36,7 @@ EXECUTION:
|
|
|
36
36
|
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children; do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
|
|
37
37
|
• Sequential example: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
|
|
38
38
|
• Parallel example: { workflowScript: "const [a,b] = await runs.all([{key:'correctness',agent:'agent-a',task:'Review correctness'},{key:'tests',agent:'agent-b',task:'Review tests'}]); return {correctness:a.output,tests:b.output}" }
|
|
39
|
-
• Optional context is "fresh" or "
|
|
39
|
+
• Optional context is "fresh", "fork", or "profile". profile requires the selected agent's declared defaultContext and ignores config defaultSubagentContext. Explicit fresh/fork wins. When omitted, config defaultSubagentContext wins over agent defaultContext. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
|
|
40
40
|
• Durable mission attachment is automatic by default. Use missionId to attach an existing mission, mission:{...} to override auto-create, or mission:false for ephemeral work. A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
|
|
41
41
|
|
|
42
42
|
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
@@ -53,7 +53,7 @@ EXECUTE:
|
|
|
53
53
|
• SINGLE {agent:"worker",task:"..."} starts exactly one child through the workflow runtime. Workflow-level fields remain child defaults. Do not combine agent/task with action or workflowScript.
|
|
54
54
|
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel work; do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Await runs.steer(key,message,options?) to guide a prior keyed child; it returns queued, delivered, missed, or failed and never accepts a raw run id. Always await or return steering calls. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. async:false blocks the parent until completion and auto-enables a same-repo live chat card unless chatProgress is off; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off.
|
|
55
55
|
• Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
|
|
56
|
-
• context can be fresh or
|
|
56
|
+
• context can be fresh, fork, or profile. profile requires the selected agent's declared defaultContext and ignores defaultSubagentContext. Explicit fresh/fork wins; omitted context follows defaultSubagentContext before agent defaultContext. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls.
|
|
57
57
|
|
|
58
58
|
MANAGE / CONTROL:
|
|
59
59
|
• Use action without execution fields for list/get/models/guide/authoring, refine/refine.show/refine.rollback, mission, watchdog, status, interrupt, stop, resume, steer, script-only scheduling, diagnostics, and other management actions. guide reads shipped current-version docs by topic.
|
|
@@ -1406,7 +1406,7 @@ export function executeAsyncSingle(
|
|
|
1406
1406
|
const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
|
|
1407
1407
|
const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, instructionCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined));
|
|
1408
1408
|
systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, agentConfig);
|
|
1409
|
-
const outputMode = params.outputMode ?? "inline";
|
|
1409
|
+
const outputMode = params.outputMode ?? agentConfig.outputMode ?? "inline";
|
|
1410
1410
|
const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
|
|
1411
1411
|
if (validationError) return formatAsyncStartError("single", validationError);
|
|
1412
1412
|
const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath, agentConfig);
|
|
@@ -1534,6 +1534,7 @@ export function executeAsyncSingle(
|
|
|
1534
1534
|
...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
|
|
1535
1535
|
...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
|
|
1536
1536
|
...(controlConfig ? { controlConfig } : {}),
|
|
1537
|
+
...(params.context ? { context: params.context } : {}),
|
|
1537
1538
|
...(params.intercomBridge !== undefined ? { intercomBridge: params.intercomBridge } : {}),
|
|
1538
1539
|
...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
|
|
1539
1540
|
...(initialTurnBudget ? { initialTurnBudget: { maxTurns: initialTurnBudget.maxTurns, graceTurns: initialTurnBudget.graceTurns } } : {}),
|
|
@@ -324,7 +324,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
324
324
|
};
|
|
325
325
|
|
|
326
326
|
const refreshJob = (job: AsyncJobState): boolean => {
|
|
327
|
-
const
|
|
327
|
+
const widgetExpanded = state.lastUiContext?.hasUI ? state.lastUiContext.ui.getToolsExpanded?.() ?? false : false;
|
|
328
|
+
const widgetStateBefore = widgetRenderKey(job, widgetExpanded);
|
|
328
329
|
let nestedRefreshFailed = false;
|
|
329
330
|
const refreshNestedProjection = () => {
|
|
330
331
|
try {
|
|
@@ -422,7 +423,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
422
423
|
scheduleCleanup(job.asyncId);
|
|
423
424
|
}
|
|
424
425
|
}
|
|
425
|
-
return widgetRenderKey(job) !== widgetStateBefore;
|
|
426
|
+
return widgetRenderKey(job, widgetExpanded) !== widgetStateBefore;
|
|
426
427
|
}
|
|
427
428
|
if (job.status === "queued") {
|
|
428
429
|
job.status = "running";
|
|
@@ -439,7 +440,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
439
440
|
rememberFleetJob(state, job);
|
|
440
441
|
if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) scheduleCleanup(job.asyncId);
|
|
441
442
|
}
|
|
442
|
-
return widgetRenderKey(job) !== widgetStateBefore;
|
|
443
|
+
return widgetRenderKey(job, widgetExpanded) !== widgetStateBefore;
|
|
443
444
|
};
|
|
444
445
|
|
|
445
446
|
const scheduleJobRefresh = (asyncId: string, delayMs = EVENT_REFRESH_DEBOUNCE_MS) => {
|
|
@@ -321,7 +321,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
321
321
|
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelOverrideFromParent", "fallbackModels", "thinking", "tools", "extensions",
|
|
322
322
|
"subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
|
|
323
323
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
324
|
-
"artifactsDir", "maxOutput", "controlConfig", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
324
|
+
"artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
325
325
|
"launchResolvedExtensions", "runFanoutBudget",
|
|
326
326
|
]);
|
|
327
327
|
for (const field of Object.keys(parsed)) {
|
|
@@ -345,6 +345,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
345
345
|
}
|
|
346
346
|
if (parsed.systemPromptMode !== "append" && parsed.systemPromptMode !== "replace") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPromptMode is invalid.`);
|
|
347
347
|
if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
|
|
348
|
+
if (parsed.context !== undefined && parsed.context !== "fresh" && parsed.context !== "fork") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': context is invalid.`);
|
|
348
349
|
if (parsed.modelOverrideFromParent !== undefined && typeof parsed.modelOverrideFromParent !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOverrideFromParent must be a boolean.`);
|
|
349
350
|
for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
|
|
350
351
|
if (typeof parsed[field] !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a boolean.`);
|
|
@@ -227,12 +227,21 @@ function snapshotBytes(snapshot: AsyncStatusSnapshotV1): number {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
function enforceByteLimit(snapshot: AsyncStatusSnapshotV1): void {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) return;
|
|
231
|
+
snapshot.omitted.byteLimitExceeded = true;
|
|
232
|
+
const runs = snapshot.runs;
|
|
233
|
+
const initialOmittedRuns = snapshot.omitted.runs;
|
|
234
|
+
let lower = 0;
|
|
235
|
+
let upper = Math.max(0, runs.length - 1);
|
|
236
|
+
while (lower < upper) {
|
|
237
|
+
const retained = Math.ceil((lower + upper) / 2);
|
|
238
|
+
snapshot.runs = runs.slice(0, retained);
|
|
239
|
+
snapshot.omitted.runs = initialOmittedRuns + runs.length - retained;
|
|
240
|
+
if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) lower = retained;
|
|
241
|
+
else upper = retained - 1;
|
|
234
242
|
}
|
|
235
|
-
|
|
243
|
+
snapshot.runs = runs.slice(0, lower);
|
|
244
|
+
snapshot.omitted.runs = initialOmittedRuns + runs.length - lower;
|
|
236
245
|
}
|
|
237
246
|
|
|
238
247
|
export function buildAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
|
|
@@ -57,6 +57,8 @@ type ResultWatcherDeps = {
|
|
|
57
57
|
coalesceDelayMs?: number;
|
|
58
58
|
/** Returns true while a durable completion source needs periodic delivery checks. */
|
|
59
59
|
hasDeliveryDemand?: () => boolean;
|
|
60
|
+
/** Control how slow result-index scans are logged. Defaults to \"all\". */
|
|
61
|
+
resultScanLogging?: "all" | "activity" | "off";
|
|
60
62
|
platform?: NodeJS.Platform;
|
|
61
63
|
};
|
|
62
64
|
|
|
@@ -604,6 +606,12 @@ export function createResultWatcher(
|
|
|
604
606
|
const logScanStats = (stats: ResultScanStats) => {
|
|
605
607
|
const elapsed = Date.now() - stats.startedAt;
|
|
606
608
|
if (elapsed < SLOW_RESULT_SCAN_MS) return;
|
|
609
|
+
if (deps.resultScanLogging === "off") return;
|
|
610
|
+
// A scan that inspected and scheduled nothing is a quiet no-op (e.g. the
|
|
611
|
+
// healthy periodic rescan while no async runs are pending). Under
|
|
612
|
+
// "activity", skip it so empty scans do not burn context tokens in the
|
|
613
|
+
// session transcript.
|
|
614
|
+
if (deps.resultScanLogging === "activity" && stats.files === 0 && stats.scheduled === 0) return;
|
|
607
615
|
console.error(`Subagent result scan inspected ${stats.files} indexed result file(s), scheduled ${stats.scheduled} in ${elapsed}ms (${resultsDir}).`);
|
|
608
616
|
};
|
|
609
617
|
const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
|
|
@@ -85,7 +85,7 @@ import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
|
|
|
85
85
|
import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
|
|
86
86
|
import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
|
|
87
87
|
import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
|
|
88
|
-
import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
88
|
+
import { formatModelAttemptNote, isRetryableModelFailure, recordRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
89
89
|
import {
|
|
90
90
|
SUBAGENT_STARTUP_RETRY_DELAYS_MS,
|
|
91
91
|
formatSubagentExtensionConflictError,
|
|
@@ -1432,8 +1432,8 @@ async function runSingleStepInner(
|
|
|
1432
1432
|
});
|
|
1433
1433
|
}
|
|
1434
1434
|
|
|
1435
|
-
const candidates = step.modelCandidates
|
|
1436
|
-
? step.modelCandidates
|
|
1435
|
+
const candidates = step.modelCandidates !== undefined
|
|
1436
|
+
? step.modelCandidates.length > 0 ? step.modelCandidates : [undefined]
|
|
1437
1437
|
: step.model
|
|
1438
1438
|
? [step.model]
|
|
1439
1439
|
: [undefined];
|
|
@@ -1769,7 +1769,9 @@ async function runSingleStepInner(
|
|
|
1769
1769
|
finalResult.finalOutput = startupError;
|
|
1770
1770
|
break modelAttemptsLoop;
|
|
1771
1771
|
}
|
|
1772
|
-
|
|
1772
|
+
const retryableModelFailure = isRetryableModelFailure(error);
|
|
1773
|
+
if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
|
|
1774
|
+
if (!retryableModelFailure || modelIndex === candidates.length - 1) break modelAttemptsLoop;
|
|
1773
1775
|
attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
|
|
1774
1776
|
modelIndex += 1;
|
|
1775
1777
|
startupAttemptIndex = 0;
|
|
@@ -3330,6 +3332,7 @@ async function runSubagent(
|
|
|
3330
3332
|
startedAt: step.startedAt ?? overallStartTime,
|
|
3331
3333
|
lastActivityAt,
|
|
3332
3334
|
currentTool: step.currentTool,
|
|
3335
|
+
thinking: step.thinking,
|
|
3333
3336
|
now,
|
|
3334
3337
|
}));
|
|
3335
3338
|
if (idleState === "needs_attention") {
|
|
@@ -86,6 +86,8 @@ export interface SubagentWaitParams {
|
|
|
86
86
|
all?: boolean;
|
|
87
87
|
/** Give up after this many milliseconds. Defaults to 30 minutes. */
|
|
88
88
|
timeoutMs?: number;
|
|
89
|
+
/** False keeps a blocking wait open through idle attention; supervisor/contact requests still stop the wait. */
|
|
90
|
+
stopOnAttention?: boolean;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
/** Minimal event-bus surface wait subscribes to (matches pi.events). */
|
|
@@ -110,6 +112,8 @@ export interface SubagentWaitDeps {
|
|
|
110
112
|
stopOnAttention?: boolean;
|
|
111
113
|
/** Internal auto-drain mode surfaces failed terminal subagent runs as errors. */
|
|
112
114
|
failOnFailedRuns?: boolean;
|
|
115
|
+
/** Internal auto-drain mode surfaces actionable attention as an error. */
|
|
116
|
+
failOnAttention?: boolean;
|
|
113
117
|
/** Arm a durable exact-target wait subscription in a long-lived interactive runtime. */
|
|
114
118
|
subscribe?: (input: { targetKind: "async" | "foreground"; runId: string; requestedId: string; timeoutMs: number }) => { token: string; expiresAt: number };
|
|
115
119
|
/** Injectable provider protocol surfaces for deterministic tests. */
|
|
@@ -453,7 +457,7 @@ async function waitForDetachedForegroundRun(
|
|
|
453
457
|
return result(`Remembered foreground run "${run.runId}" disappeared before a terminal child result was recorded. Completion cannot be confirmed; do not launch a replacement without checking the originating child session.`, true);
|
|
454
458
|
}
|
|
455
459
|
const pending = current.children.filter((child) => initialDetachedIndices.has(child.index) && child.status === "detached");
|
|
456
|
-
const attention =
|
|
460
|
+
const attention = foregroundChildrenNeedingAttention(current, initialDetachedIndices);
|
|
457
461
|
if (attention.length > 0) return formatForegroundAttention(current, attention, now() - startedAt);
|
|
458
462
|
if (pending.length === 0) {
|
|
459
463
|
const outcome = summarizeForegroundChildren(current, initialDetachedIndices);
|
|
@@ -555,11 +559,11 @@ export async function waitForSubagents(
|
|
|
555
559
|
const initialProviderIds = new Set(providerActive.map(backgroundWorkIdentity));
|
|
556
560
|
const initialProviderNames = new Set(providerActive.map((item) => item.provider));
|
|
557
561
|
const initialCount = initialAsyncIds.size + initialProviderIds.size;
|
|
558
|
-
const stopOnAttention = deps.stopOnAttention !== false;
|
|
562
|
+
const stopOnAttention = params.stopOnAttention ?? deps.stopOnAttention !== false;
|
|
559
563
|
let attention = active.filter((run) => needsAttention(run));
|
|
560
564
|
|
|
561
565
|
const isDone = (): boolean => {
|
|
562
|
-
if (
|
|
566
|
+
if (attention.some((run) => initialAsyncIds.has(run.id) && (stopOnAttention || hasSupervisorTool(run)))) return true;
|
|
563
567
|
const activeAsyncIds = new Set(active.map((run) => run.id));
|
|
564
568
|
const activeProviderIds = new Set(providerActive.map(backgroundWorkIdentity));
|
|
565
569
|
if (waitForAll) {
|
|
@@ -643,7 +647,7 @@ export async function waitForSubagents(
|
|
|
643
647
|
const status = relevantAttention.length > 0 ? "attention required" : "done";
|
|
644
648
|
return result(
|
|
645
649
|
`Waited ${elapsed} for ${scope}; ${status}.${outcome}${resumeGuidance}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
646
|
-
deps.failOnFailedRuns === true && failedAsyncCount > 0,
|
|
650
|
+
(deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
|
|
647
651
|
completions,
|
|
648
652
|
);
|
|
649
653
|
}
|
|
@@ -660,7 +664,7 @@ export async function waitForSubagents(
|
|
|
660
664
|
: `${finishedCount} of ${initialCount} ${subject} finished`;
|
|
661
665
|
return result(
|
|
662
666
|
`Waited ${elapsed}; ${progress}.${outcome}${resumeGuidance}${attentionNote}${remainder} Relevant completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
663
|
-
deps.failOnFailedRuns === true && failedAsyncCount > 0,
|
|
667
|
+
(deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
|
|
664
668
|
completions,
|
|
665
669
|
);
|
|
666
670
|
}
|
|
@@ -68,11 +68,16 @@ function removeInvalidMarker(marker: string): void {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
interface MarkerFile {
|
|
72
|
+
dir: string;
|
|
73
|
+
name: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function markerFiles(dir: string): MarkerFile[] {
|
|
72
77
|
try {
|
|
73
78
|
return fs.readdirSync(dir, { withFileTypes: true })
|
|
74
79
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
75
|
-
.map((entry) =>
|
|
80
|
+
.map((entry) => ({ dir, name: entry.name }));
|
|
76
81
|
} catch (error) {
|
|
77
82
|
const code = (error as NodeJS.ErrnoException).code;
|
|
78
83
|
if (code === "ENOENT" || code === "ENOTDIR") return [];
|
|
@@ -93,12 +98,16 @@ function sessionDirs(asyncDirRoot: string, sessionId: string | undefined): strin
|
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
|
|
101
|
+
function recentMarkerFiles(dirs: string[], limit: number): string[] {
|
|
102
|
+
return dirs.flatMap(markerFiles)
|
|
103
|
+
.sort((left, right) => right.name.localeCompare(left.name))
|
|
104
|
+
.slice(0, limit)
|
|
105
|
+
.map((marker) => path.join(marker.dir, marker.name));
|
|
106
|
+
}
|
|
107
|
+
|
|
96
108
|
export function readRecentTerminalRunIndex(asyncDirRoot: string, options: { sessionId?: string; limit?: number } = {}): string[] {
|
|
97
109
|
const limit = options.limit === undefined ? Number.POSITIVE_INFINITY : Math.max(0, Math.floor(options.limit));
|
|
98
|
-
const candidates = sessionDirs(asyncDirRoot, options.sessionId)
|
|
99
|
-
.flatMap(markerFiles)
|
|
100
|
-
.sort((left, right) => path.basename(right).localeCompare(path.basename(left)))
|
|
101
|
-
.slice(0, limit);
|
|
110
|
+
const candidates = recentMarkerFiles(sessionDirs(asyncDirRoot, options.sessionId), limit);
|
|
102
111
|
const runIds: string[] = [];
|
|
103
112
|
const seen = new Set<string>();
|
|
104
113
|
for (const marker of candidates) {
|
|
@@ -16,6 +16,7 @@ In an interactive chat, do not call this merely to wait: return control to the u
|
|
|
16
16
|
• { all: true } — wait for every async run and provider item that was active when the call began.
|
|
17
17
|
• { id: "..." } — wait for one async or remembered detached foreground subagent run (id or prefix).
|
|
18
18
|
• { id: "...", nonBlocking: true } — resolve the prefix once, persist an exact-run wake subscription, and return immediately. The originating interactive session wakes on completion, failure, attention, reconciliation failure, or timeout.
|
|
19
|
+
• { stopOnAttention: false } — for blocking waits only, keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.
|
|
19
20
|
• { timeoutMs: 600000 } — stop waiting after N ms; active work keeps running.
|
|
20
21
|
|
|
21
22
|
Non-blocking subscriptions are visible in subagent status and differ from disabling waitTool: waitTool.enabled=false returns immediately without registering any future wake. Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`subagent_wait\` in the child tool allowlist and load each provider through the agent's extensions or subagentOnlyExtensions; this tool never loads providers or grants tools itself.${enabled ? "" : "\n\nConfigured behavior: subagent_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and returns immediately without blocking."}`,
|
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
buildModelCandidates,
|
|
75
75
|
formatModelAttemptNote,
|
|
76
76
|
isRetryableModelFailure,
|
|
77
|
+
recordRetryableModelFailure,
|
|
77
78
|
} from "../shared/model-fallback.ts";
|
|
78
79
|
import {
|
|
79
80
|
SUBAGENT_STARTUP_RETRY_DELAYS_MS,
|
|
@@ -872,6 +873,7 @@ async function runSingleAttempt(
|
|
|
872
873
|
startedAt: startTime,
|
|
873
874
|
lastActivityAt: progress.lastActivityAt,
|
|
874
875
|
currentTool: progress.currentTool,
|
|
876
|
+
thinking: resolvedThinking,
|
|
875
877
|
now,
|
|
876
878
|
});
|
|
877
879
|
if (idleState === "needs_attention") {
|
|
@@ -1860,7 +1862,9 @@ async function runSyncCompletionInner(
|
|
|
1860
1862
|
attempt.error = startupError;
|
|
1861
1863
|
break modelAttemptsLoop;
|
|
1862
1864
|
}
|
|
1863
|
-
|
|
1865
|
+
const retryableModelFailure = isRetryableModelFailure(result.error);
|
|
1866
|
+
if (retryableModelFailure) recordRetryableModelFailure(result.model ?? candidate, result.error);
|
|
1867
|
+
if (!retryableModelFailure || modelIndex === modelsToTry.length - 1) break modelAttemptsLoop;
|
|
1864
1868
|
attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[modelIndex + 1]));
|
|
1865
1869
|
break;
|
|
1866
1870
|
}
|