pi-subagents 0.52.1 → 0.54.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 +69 -0
- package/README.md +4 -0
- package/docs/configuration.md +12 -2
- package/docs/extension-api.md +3 -1
- package/docs/models.md +17 -3
- package/docs/workflows.md +2 -0
- package/index.ts +10 -1
- package/package.json +2 -1
- package/prompts/council.md +51 -0
- package/skills/council-mode/SKILL.md +231 -0
- package/skills/pi-subagents/SKILL.md +5 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -0
- package/skills/pi-subagents/references/execution-controls.md +13 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +7 -0
- package/src/agents/agent-management.ts +107 -8
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +96 -37
- 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/preflight.ts +8 -3
- package/src/extension/config.ts +3 -0
- package/src/extension/doctor.ts +11 -0
- package/src/extension/fanout-child.ts +3 -2
- package/src/extension/index.ts +20 -4
- package/src/extension/public-execution.ts +1 -1
- package/src/extension/rpc.ts +41 -1
- package/src/extension/schemas.ts +6 -3
- package/src/extension/tool-description.ts +2 -2
- package/src/extension/tool-result.ts +19 -0
- package/src/inspectors/herdr/client.ts +3 -3
- package/src/runs/background/async-execution.ts +12 -6
- package/src/runs/background/async-job-tracker.ts +4 -3
- package/src/runs/background/async-resume.ts +2 -1
- package/src/runs/background/async-retention.ts +1 -1
- package/src/runs/background/async-status-snapshot.ts +14 -5
- package/src/runs/background/auto-drain.ts +1 -0
- package/src/runs/background/chain-root-attachment.ts +5 -0
- package/src/runs/background/result-watcher.ts +9 -0
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +32 -5
- package/src/runs/background/subagent-wait.ts +9 -5
- package/src/runs/background/terminal-run-index.ts +15 -6
- package/src/runs/background/wait-completions.ts +2 -0
- package/src/runs/background/wait-tool.ts +5 -3
- package/src/runs/foreground/execution.ts +34 -2
- package/src/runs/foreground/subagent-executor.ts +196 -52
- 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 +72 -16
- package/src/runs/shared/model-scope.ts +106 -39
- package/src/runs/shared/pi-args.ts +34 -1
- package/src/runs/shared/subagent-control.ts +25 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +36 -9
- 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 +47 -2
- 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/watchdog/permission-arbiter.ts +59 -51
- package/src/workflows/scripted-workflow.ts +100 -12
- package/src/workflows/workflow-receipt.ts +140 -0
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." })),
|
|
@@ -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.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert pi-subagents' internal logical-error result into the rejection Pi's
|
|
5
|
+
* public tool boundary uses to emit a canonical errored ToolResult.
|
|
6
|
+
*
|
|
7
|
+
* Keep this at registered tool boundaries. Internal workflows intentionally
|
|
8
|
+
* retain their return-based error handling.
|
|
9
|
+
*/
|
|
10
|
+
export function finalizeToolResult<T>(result: AgentToolResult<T>): AgentToolResult<T> {
|
|
11
|
+
if (result.isError !== true) return result;
|
|
12
|
+
|
|
13
|
+
const message = result.content
|
|
14
|
+
.flatMap((item) => item.type === "text" && typeof item.text === "string" ? [item.text] : [])
|
|
15
|
+
.join("\n")
|
|
16
|
+
.trim();
|
|
17
|
+
|
|
18
|
+
throw new Error(message || "pi-subagents reported a logical tool failure.");
|
|
19
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
|
|
3
3
|
export type HerdrErrorCode =
|
|
4
4
|
| "HERDR_UNAVAILABLE"
|
|
@@ -16,7 +16,7 @@ export interface HerdrClient {
|
|
|
16
16
|
run<T = unknown>(args: string[], options?: { timeoutMs?: number; signal?: AbortSignal; textOk?: boolean }): Promise<HerdrResult<T>>;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
type SpawnHerdr = (command: string, args: readonly string[], options: { shell: false; windowsHide: true; env: NodeJS.ProcessEnv }) =>
|
|
19
|
+
type SpawnHerdr = (command: string, args: readonly string[], options: { shell: false; windowsHide: true; env: NodeJS.ProcessEnv }) => ReturnType<typeof spawn>;
|
|
20
20
|
|
|
21
21
|
function error(code: HerdrErrorCode, message: string, details?: unknown): HerdrResult<never> {
|
|
22
22
|
return { ok: false, error: { code, message, ...(details !== undefined ? { details } : {}) } };
|
|
@@ -46,7 +46,7 @@ export function createHerdrClient(options: { bin?: string; spawn?: SpawnHerdr }
|
|
|
46
46
|
return {
|
|
47
47
|
run<T>(args: string[], runOptions: { timeoutMs?: number; signal?: AbortSignal; textOk?: boolean } = {}): Promise<HerdrResult<T>> {
|
|
48
48
|
return new Promise((resolve) => {
|
|
49
|
-
let child:
|
|
49
|
+
let child: ReturnType<typeof spawn>;
|
|
50
50
|
try {
|
|
51
51
|
child = spawnImpl(bin, args, { shell: false, windowsHide: true, env: process.env });
|
|
52
52
|
} catch (cause) {
|
|
@@ -26,7 +26,7 @@ import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
|
26
26
|
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
|
|
27
27
|
import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
|
|
28
28
|
import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
|
|
29
|
-
import type
|
|
29
|
+
import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
|
|
30
30
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
31
31
|
import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
|
|
32
32
|
import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
|
|
@@ -791,18 +791,20 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
791
791
|
const taskText = `${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`;
|
|
792
792
|
const task = namespaceOutputPath ? taskText : injectSingleOutputInstruction(taskText, outputPath, a);
|
|
793
793
|
|
|
794
|
+
const modelScopes = resolveModelScopesForAgent(ctx.modelScope, a.name, ctx.currentModel);
|
|
794
795
|
const primaryModel = externalRunner ? undefined : resolveEffectiveSubagentModel(
|
|
795
796
|
s.model,
|
|
796
797
|
a.model,
|
|
797
798
|
ctx.currentModel,
|
|
798
799
|
availableModels,
|
|
799
800
|
ctx.currentModelProvider,
|
|
800
|
-
{ scope:
|
|
801
|
+
{ scope: modelScopes },
|
|
801
802
|
);
|
|
802
803
|
const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
|
|
803
804
|
const effectiveThinking = externalRunner ? undefined : thinkingOverride ?? a.thinking;
|
|
804
805
|
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
|
|
805
806
|
const agentContract = s.agentContract ?? params.agentContract;
|
|
807
|
+
const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
|
|
806
808
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
807
809
|
tools: a.tools,
|
|
808
810
|
extensions: a.extensions,
|
|
@@ -814,9 +816,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
814
816
|
capabilityCeiling: params.capabilityCeiling,
|
|
815
817
|
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
816
818
|
agentName: a.name,
|
|
819
|
+
permissionRules,
|
|
817
820
|
});
|
|
818
821
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
819
|
-
const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
|
|
820
822
|
if (externalRunner && permissionRules) {
|
|
821
823
|
throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}', which cannot enforce native Pi child permission rules.`);
|
|
822
824
|
}
|
|
@@ -839,7 +841,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
839
841
|
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
840
842
|
launchResolvedExtensions,
|
|
841
843
|
modelCandidates: externalRunner ? undefined : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider, {
|
|
842
|
-
scope:
|
|
844
|
+
scope: modelScopes,
|
|
843
845
|
primaryModelFromParent: inheritsParentModel(s.model, a.model, ctx.currentModel),
|
|
844
846
|
}).map((candidate) =>
|
|
845
847
|
applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined),
|
|
@@ -1406,7 +1408,7 @@ export function executeAsyncSingle(
|
|
|
1406
1408
|
const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
|
|
1407
1409
|
const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, instructionCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined));
|
|
1408
1410
|
systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, agentConfig);
|
|
1409
|
-
const outputMode = params.outputMode ?? "inline";
|
|
1411
|
+
const outputMode = params.outputMode ?? agentConfig.outputMode ?? "inline";
|
|
1410
1412
|
const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
|
|
1411
1413
|
if (validationError) return formatAsyncStartError("single", validationError);
|
|
1412
1414
|
const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath, agentConfig);
|
|
@@ -1418,6 +1420,7 @@ export function executeAsyncSingle(
|
|
|
1418
1420
|
? `[Read from: ${readPaths.join(", ")}]\n\n`
|
|
1419
1421
|
: "";
|
|
1420
1422
|
const taskText = readsInstruction + taskWithOutputInstruction;
|
|
1423
|
+
const modelScopes = resolveModelScopesForAgent(ctx.modelScope, agentConfig.name, ctx.currentModel);
|
|
1421
1424
|
const primaryModel = externalRunner ? undefined : params.modelOverrideFromParent
|
|
1422
1425
|
? params.modelOverride
|
|
1423
1426
|
: resolveSubagentModelOverride(
|
|
@@ -1425,6 +1428,7 @@ export function executeAsyncSingle(
|
|
|
1425
1428
|
ctx.currentModel,
|
|
1426
1429
|
availableModels,
|
|
1427
1430
|
ctx.currentModelProvider,
|
|
1431
|
+
{ scope: modelScopes },
|
|
1428
1432
|
);
|
|
1429
1433
|
const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
|
|
1430
1434
|
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
|
|
@@ -1453,7 +1457,7 @@ export function executeAsyncSingle(
|
|
|
1453
1457
|
const modelCandidates = externalRunner
|
|
1454
1458
|
? []
|
|
1455
1459
|
: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, {
|
|
1456
|
-
scope:
|
|
1460
|
+
scope: modelScopes,
|
|
1457
1461
|
primaryModelFromParent: params.modelOverrideFromParent,
|
|
1458
1462
|
})
|
|
1459
1463
|
.flatMap((candidate) => {
|
|
@@ -1472,6 +1476,7 @@ export function executeAsyncSingle(
|
|
|
1472
1476
|
capabilityCeiling,
|
|
1473
1477
|
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
1474
1478
|
agentName: agentConfig.name,
|
|
1479
|
+
permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
|
|
1475
1480
|
});
|
|
1476
1481
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
1477
1482
|
const launchContractDigest = launchBindingDigest({
|
|
@@ -1534,6 +1539,7 @@ export function executeAsyncSingle(
|
|
|
1534
1539
|
...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
|
|
1535
1540
|
...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
|
|
1536
1541
|
...(controlConfig ? { controlConfig } : {}),
|
|
1542
|
+
...(params.context ? { context: params.context } : {}),
|
|
1537
1543
|
...(params.intercomBridge !== undefined ? { intercomBridge: params.intercomBridge } : {}),
|
|
1538
1544
|
...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
|
|
1539
1545
|
...(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.`);
|
|
@@ -600,7 +600,7 @@ function runRetentionDiscovery(input: {
|
|
|
600
600
|
};
|
|
601
601
|
const onAbort = (): void => fail(new RetentionCancelledError("Retention discovery was cancelled."));
|
|
602
602
|
input.signal?.addEventListener("abort", onAbort, { once: true });
|
|
603
|
-
worker.once("error", (error) => fail(error));
|
|
603
|
+
worker.once("error", (error) => fail(error instanceof Error ? error : new Error(String(error))));
|
|
604
604
|
worker.once("exit", (code) => {
|
|
605
605
|
if (!settled) fail(new Error(`Retention discovery worker exited before replying (${code}).`));
|
|
606
606
|
});
|
|
@@ -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 {
|
|
@@ -21,6 +21,7 @@ export interface ImportedAsyncRootResult {
|
|
|
21
21
|
model?: string;
|
|
22
22
|
attemptedModels?: string[];
|
|
23
23
|
modelAttempts?: ModelAttempt[];
|
|
24
|
+
contextOverflow?: boolean;
|
|
24
25
|
totalCost?: CostSummary;
|
|
25
26
|
structuredOutput?: unknown;
|
|
26
27
|
structuredOutputPath?: string;
|
|
@@ -49,6 +50,7 @@ interface AsyncResultFile {
|
|
|
49
50
|
model?: string;
|
|
50
51
|
attemptedModels?: string[];
|
|
51
52
|
modelAttempts?: ModelAttempt[];
|
|
53
|
+
contextOverflow?: boolean;
|
|
52
54
|
totalCost?: CostSummary;
|
|
53
55
|
structuredOutput?: unknown;
|
|
54
56
|
structuredOutputPath?: string;
|
|
@@ -110,6 +112,7 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
|
|
|
110
112
|
...(step?.model ? { model: step.model } : {}),
|
|
111
113
|
...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
112
114
|
...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
|
|
115
|
+
...(step?.contextOverflow ? { contextOverflow: true } : {}),
|
|
113
116
|
...(step?.totalCost ? { totalCost: step.totalCost } : {}),
|
|
114
117
|
...(step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
|
|
115
118
|
...(step?.structuredOutputPath ? { structuredOutputPath: step.structuredOutputPath } : {}),
|
|
@@ -131,6 +134,7 @@ function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null,
|
|
|
131
134
|
...(step?.model ? { model: step.model } : {}),
|
|
132
135
|
...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
133
136
|
...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
|
|
137
|
+
...(step?.contextOverflow ? { contextOverflow: true } : {}),
|
|
134
138
|
...(step?.totalCost ? { totalCost: step.totalCost } : {}),
|
|
135
139
|
};
|
|
136
140
|
}
|
|
@@ -158,6 +162,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
|
|
|
158
162
|
...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),
|
|
159
163
|
...(child?.attemptedModels ?? step?.attemptedModels ? { attemptedModels: child?.attemptedModels ?? step?.attemptedModels } : {}),
|
|
160
164
|
...(child?.modelAttempts ?? step?.modelAttempts ? { modelAttempts: child?.modelAttempts ?? step?.modelAttempts } : {}),
|
|
165
|
+
...(child?.contextOverflow || step?.contextOverflow ? { contextOverflow: true } : {}),
|
|
161
166
|
...(child?.totalCost ?? step?.totalCost ? { totalCost: child?.totalCost ?? step?.totalCost } : {}),
|
|
162
167
|
...(child?.structuredOutput !== undefined ? { structuredOutput: child.structuredOutput } : step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
|
|
163
168
|
...(child?.structuredOutputPath ?? step?.structuredOutputPath ? { structuredOutputPath: child?.structuredOutputPath ?? step?.structuredOutputPath } : {}),
|
|
@@ -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 \"activity\". */
|
|
61
|
+
resultScanLogging?: "all" | "activity" | "off";
|
|
60
62
|
platform?: NodeJS.Platform;
|
|
61
63
|
};
|
|
62
64
|
|
|
@@ -604,6 +606,13 @@ 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
|
+
const resultScanLogging = deps.resultScanLogging ?? "activity";
|
|
610
|
+
if (resultScanLogging === "off") return;
|
|
611
|
+
// A scan that inspected and scheduled nothing is a quiet no-op (e.g. the
|
|
612
|
+
// healthy periodic rescan while no async runs are pending). Under
|
|
613
|
+
// "activity", skip it so empty scans do not burn context tokens in the
|
|
614
|
+
// session transcript.
|
|
615
|
+
if (resultScanLogging === "activity" && stats.files === 0 && stats.scheduled === 0) return;
|
|
607
616
|
console.error(`Subagent result scan inspected ${stats.files} indexed result file(s), scheduled ${stats.scheduled} in ${elapsed}ms (${resultsDir}).`);
|
|
608
617
|
};
|
|
609
618
|
const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
|
|
@@ -97,6 +97,7 @@ interface ResultChildOutcome {
|
|
|
97
97
|
thinking?: string;
|
|
98
98
|
attemptedModels?: string[];
|
|
99
99
|
modelAttempts?: NonNullable<AsyncStatus["steps"]>[number]["modelAttempts"];
|
|
100
|
+
contextOverflow?: boolean;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
interface ResultRepairData {
|
|
@@ -163,6 +164,7 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
|
|
|
163
164
|
thinking,
|
|
164
165
|
attemptedModels: child?.attemptedModels ?? step.attemptedModels,
|
|
165
166
|
modelAttempts: child?.modelAttempts ?? step.modelAttempts,
|
|
167
|
+
contextOverflow: child?.contextOverflow ?? step.contextOverflow,
|
|
166
168
|
};
|
|
167
169
|
});
|
|
168
170
|
const terminalStatus: AsyncStatus = {
|
|
@@ -258,6 +260,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
|
|
|
258
260
|
model: step.model,
|
|
259
261
|
attemptedModels: step.attemptedModels,
|
|
260
262
|
modelAttempts: step.modelAttempts,
|
|
263
|
+
contextOverflow: step.contextOverflow,
|
|
261
264
|
sessionFile: step.sessionFile,
|
|
262
265
|
})),
|
|
263
266
|
exitCode: 1,
|
|
@@ -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, isContextOverflow, isRetryableModelFailure, recordRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
89
89
|
import {
|
|
90
90
|
SUBAGENT_STARTUP_RETRY_DELAYS_MS,
|
|
91
91
|
formatSubagentExtensionConflictError,
|
|
@@ -236,6 +236,8 @@ interface StepResult {
|
|
|
236
236
|
model?: string;
|
|
237
237
|
attemptedModels?: string[];
|
|
238
238
|
modelAttempts?: ModelAttempt[];
|
|
239
|
+
/** True when the dispatch failed because the input exceeded the model's context window. */
|
|
240
|
+
contextOverflow?: boolean;
|
|
239
241
|
totalCost?: CostSummary;
|
|
240
242
|
artifactPaths?: ArtifactPaths;
|
|
241
243
|
outputSaveError?: string;
|
|
@@ -1257,6 +1259,7 @@ async function runSingleStepInner(
|
|
|
1257
1259
|
model: imported.model,
|
|
1258
1260
|
attemptedModels: imported.attemptedModels,
|
|
1259
1261
|
modelAttempts: imported.modelAttempts,
|
|
1262
|
+
contextOverflow: imported.contextOverflow,
|
|
1260
1263
|
totalCost: imported.totalCost,
|
|
1261
1264
|
structuredOutput: timedOut || stopped ? undefined : imported.structuredOutput,
|
|
1262
1265
|
structuredOutputPath: timedOut || stopped ? undefined : imported.structuredOutputPath,
|
|
@@ -1432,8 +1435,8 @@ async function runSingleStepInner(
|
|
|
1432
1435
|
});
|
|
1433
1436
|
}
|
|
1434
1437
|
|
|
1435
|
-
const candidates = step.modelCandidates
|
|
1436
|
-
? step.modelCandidates
|
|
1438
|
+
const candidates = step.modelCandidates !== undefined
|
|
1439
|
+
? step.modelCandidates.length > 0 ? step.modelCandidates : [undefined]
|
|
1437
1440
|
: step.model
|
|
1438
1441
|
? [step.model]
|
|
1439
1442
|
: [undefined];
|
|
@@ -1458,6 +1461,8 @@ async function runSingleStepInner(
|
|
|
1458
1461
|
// Escalated to "file" after an unexplained zero-activity startup failure so
|
|
1459
1462
|
// retries keep the task text out of argv (endpoint pre-exec scans may deny it).
|
|
1460
1463
|
let taskDeliveryOverride: SubagentTaskDelivery | undefined;
|
|
1464
|
+
let contextOverflow = false;
|
|
1465
|
+
let launchWarningsEmitted = false;
|
|
1461
1466
|
modelAttemptsLoop: while (modelIndex < candidates.length) {
|
|
1462
1467
|
if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
|
|
1463
1468
|
const candidate = candidates[modelIndex];
|
|
@@ -1479,7 +1484,7 @@ async function runSingleStepInner(
|
|
|
1479
1484
|
childIndex: ctx.flatIndex,
|
|
1480
1485
|
})
|
|
1481
1486
|
: undefined;
|
|
1482
|
-
const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit } = buildPiArgs(omitUndefinedProperties({
|
|
1487
|
+
const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit, warnings } = buildPiArgs(omitUndefinedProperties({
|
|
1483
1488
|
parentSessionId: step.parentSessionId,
|
|
1484
1489
|
baseArgs: ["--mode", "json", "-p"],
|
|
1485
1490
|
task,
|
|
@@ -1525,6 +1530,10 @@ async function runSingleStepInner(
|
|
|
1525
1530
|
childWatchdog,
|
|
1526
1531
|
waitToolEnabled: step.waitToolEnabled,
|
|
1527
1532
|
}));
|
|
1533
|
+
if (!launchWarningsEmitted && warnings.length > 0) {
|
|
1534
|
+
for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
|
|
1535
|
+
launchWarningsEmitted = true;
|
|
1536
|
+
}
|
|
1528
1537
|
if (step.definitionDigest) {
|
|
1529
1538
|
const toolPlan = resolvePiLaunchToolPlan(omitUndefinedProperties({
|
|
1530
1539
|
tools: step.tools,
|
|
@@ -1536,6 +1545,7 @@ async function runSingleStepInner(
|
|
|
1536
1545
|
structuredOutput: Boolean(effectiveStructuredOutput),
|
|
1537
1546
|
capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
|
|
1538
1547
|
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
1548
|
+
permissionRules: step.permissionRules,
|
|
1539
1549
|
}));
|
|
1540
1550
|
launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
|
|
1541
1551
|
actualLaunchContractDigest = launchBindingDigest(omitUndefinedProperties({
|
|
@@ -1769,7 +1779,14 @@ async function runSingleStepInner(
|
|
|
1769
1779
|
finalResult.finalOutput = startupError;
|
|
1770
1780
|
break modelAttemptsLoop;
|
|
1771
1781
|
}
|
|
1772
|
-
|
|
1782
|
+
const retryableModelFailure = isRetryableModelFailure(error);
|
|
1783
|
+
if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
|
|
1784
|
+
if (isContextOverflow(error)) {
|
|
1785
|
+
contextOverflow = true;
|
|
1786
|
+
attemptNotes.push(`[fallback] ${attempt.model} failed: context overflow — the input exceeds this model's context window. Reduce the task input or use a model with a larger context window.`);
|
|
1787
|
+
break modelAttemptsLoop;
|
|
1788
|
+
}
|
|
1789
|
+
if (!retryableModelFailure || modelIndex === candidates.length - 1) break modelAttemptsLoop;
|
|
1773
1790
|
attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
|
|
1774
1791
|
modelIndex += 1;
|
|
1775
1792
|
startupAttemptIndex = 0;
|
|
@@ -1906,6 +1923,7 @@ async function runSingleStepInner(
|
|
|
1906
1923
|
model: finalResult?.model,
|
|
1907
1924
|
attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
|
|
1908
1925
|
modelAttempts,
|
|
1926
|
+
contextOverflow: contextOverflow || undefined,
|
|
1909
1927
|
totalCost: costSummaryFromAttempts(modelAttempts),
|
|
1910
1928
|
artifactPaths,
|
|
1911
1929
|
outputSaveError: artifactErrors.outputSaveError,
|
|
@@ -2518,6 +2536,7 @@ async function runSubagent(
|
|
|
2518
2536
|
model: step.model,
|
|
2519
2537
|
attemptedModels: step.attemptedModels,
|
|
2520
2538
|
modelAttempts: step.modelAttempts,
|
|
2539
|
+
contextOverflow: step.contextOverflow,
|
|
2521
2540
|
})),
|
|
2522
2541
|
exitCode: state === "complete" || state === "paused" ? 0 : 1,
|
|
2523
2542
|
timestamp: now,
|
|
@@ -3330,6 +3349,7 @@ async function runSubagent(
|
|
|
3330
3349
|
startedAt: step.startedAt ?? overallStartTime,
|
|
3331
3350
|
lastActivityAt,
|
|
3332
3351
|
currentTool: step.currentTool,
|
|
3352
|
+
thinking: step.thinking,
|
|
3333
3353
|
now,
|
|
3334
3354
|
}));
|
|
3335
3355
|
if (idleState === "needs_attention") {
|
|
@@ -3866,6 +3886,7 @@ async function runSubagent(
|
|
|
3866
3886
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
|
|
3867
3887
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
|
|
3868
3888
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
|
|
3889
|
+
setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
|
|
3869
3890
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
|
|
3870
3891
|
if (singleResult.totalCost) {
|
|
3871
3892
|
pendingParallelUsageCost = {
|
|
@@ -3935,6 +3956,7 @@ async function runSubagent(
|
|
|
3935
3956
|
model: pr.model,
|
|
3936
3957
|
attemptedModels: pr.attemptedModels,
|
|
3937
3958
|
modelAttempts: pr.modelAttempts,
|
|
3959
|
+
contextOverflow: pr.contextOverflow,
|
|
3938
3960
|
totalCost: pr.totalCost,
|
|
3939
3961
|
artifactPaths: pr.artifactPaths,
|
|
3940
3962
|
transcriptPath: pr.transcriptPath,
|
|
@@ -4260,6 +4282,7 @@ async function runSubagent(
|
|
|
4260
4282
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
|
|
4261
4283
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
|
|
4262
4284
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
|
|
4285
|
+
setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
|
|
4263
4286
|
setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
|
|
4264
4287
|
if (singleResult.totalCost) {
|
|
4265
4288
|
pendingParallelUsageCost = {
|
|
@@ -4363,6 +4386,7 @@ async function runSubagent(
|
|
|
4363
4386
|
model: pr.model,
|
|
4364
4387
|
attemptedModels: pr.attemptedModels,
|
|
4365
4388
|
modelAttempts: pr.modelAttempts,
|
|
4389
|
+
contextOverflow: pr.contextOverflow,
|
|
4366
4390
|
totalCost: pr.totalCost,
|
|
4367
4391
|
artifactPaths: pr.artifactPaths,
|
|
4368
4392
|
transcriptPath: pr.transcriptPath,
|
|
@@ -4580,6 +4604,7 @@ async function runSubagent(
|
|
|
4580
4604
|
model: singleResult.model,
|
|
4581
4605
|
attemptedModels: singleResult.attemptedModels,
|
|
4582
4606
|
modelAttempts: singleResult.modelAttempts,
|
|
4607
|
+
contextOverflow: singleResult.contextOverflow,
|
|
4583
4608
|
totalCost: singleResult.totalCost,
|
|
4584
4609
|
artifactPaths: singleResult.artifactPaths,
|
|
4585
4610
|
transcriptPath: singleResult.transcriptPath,
|
|
@@ -4658,6 +4683,7 @@ async function runSubagent(
|
|
|
4658
4683
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, flatIndex).thinking));
|
|
4659
4684
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "attemptedModels", singleResult.attemptedModels);
|
|
4660
4685
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "modelAttempts", singleResult.modelAttempts);
|
|
4686
|
+
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "contextOverflow", singleResult.contextOverflow);
|
|
4661
4687
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "totalCost", singleResult.totalCost);
|
|
4662
4688
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "error", stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error);
|
|
4663
4689
|
setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "transcriptPath", singleResult.transcriptPath ?? requiredStatusStep(statusPayload, flatIndex).transcriptPath);
|
|
@@ -4932,6 +4958,7 @@ async function runSubagent(
|
|
|
4932
4958
|
model: r.model,
|
|
4933
4959
|
attemptedModels: r.attemptedModels,
|
|
4934
4960
|
modelAttempts: r.modelAttempts,
|
|
4961
|
+
contextOverflow: r.contextOverflow,
|
|
4935
4962
|
totalCost: r.totalCost,
|
|
4936
4963
|
artifactPaths: r.artifactPaths,
|
|
4937
4964
|
outputSaveError: r.outputSaveError,
|