pi-subagents 0.59.0 → 0.61.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/docs/agents.md +2 -2
- package/docs/configuration.md +9 -5
- package/docs/extension-api.md +14 -7
- package/docs/models.md +1 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +15 -3
- package/docs/workflows.md +15 -14
- package/install.mjs +2 -1
- package/package.json +1 -1
- package/skills/council-mode/SKILL.md +48 -243
- package/skills/council-mode/references/pass-contracts.md +150 -0
- package/skills/pi-subagents/SKILL.md +89 -37
- package/skills/pi-subagents/references/constraints-and-recipes.md +30 -234
- package/skills/pi-subagents/references/execution-controls.md +79 -7
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +35 -28
- package/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/src/agents/agent-management.ts +256 -88
- package/src/agents/agents.ts +527 -221
- package/src/api/background-work.ts +7 -2
- package/src/api/external-runs.ts +67 -4
- package/src/api/preflight.ts +13 -8
- package/src/api/shared-types.ts +3 -0
- package/src/extension/index.ts +7 -4
- package/src/extension/public-execution.ts +48 -4
- package/src/extension/rpc.ts +62 -4
- package/src/extension/schemas.ts +12 -7
- package/src/extension/tool-description.ts +16 -16
- package/src/runs/background/async-execution.ts +53 -37
- package/src/runs/background/async-job-tracker.ts +65 -3
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-status.ts +104 -11
- package/src/runs/background/auto-drain.ts +1 -1
- package/src/runs/background/control-channel.ts +3 -2
- package/src/runs/background/fleet-view.ts +1 -1
- package/src/runs/background/result-watcher.ts +1 -1
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/run-status.ts +15 -4
- package/src/runs/background/subagent-runner.ts +6 -3
- package/src/runs/background/subagent-wait.ts +30 -23
- package/src/runs/background/wait-completions.ts +4 -1
- package/src/runs/background/wait-tool.ts +24 -18
- package/src/runs/foreground/execution.ts +72 -4
- package/src/runs/foreground/subagent-executor.ts +276 -59
- package/src/runs/shared/acceptance.ts +43 -18
- package/src/runs/shared/async-status-projection.ts +138 -4
- package/src/runs/shared/background-process-options.ts +9 -0
- package/src/runs/shared/host-step-status.ts +1 -0
- package/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
- package/src/runs/shared/model-fallback.ts +61 -17
- package/src/runs/shared/mutation-evidence.ts +52 -3
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/pi-args.ts +47 -1
- package/src/runs/shared/single-output.ts +45 -18
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -2
- package/src/runs/shared/tool-timeout.ts +1 -1
- package/src/runs/shared/workflow-graph.ts +16 -0
- package/src/shared/types.ts +74 -4
- package/src/shared/workflow-child-permit.ts +91 -0
- package/src/slash/prompt-template-bridge.ts +37 -1
- package/src/slash/slash-commands.ts +18 -26
- package/src/tui/fleet-status.ts +11 -3
- package/src/tui/render-helpers.ts +31 -0
- package/src/tui/render.ts +673 -145
- package/src/watchdog/change-signature.ts +40 -1
- package/src/workflows/host-command.ts +6 -1
- package/src/workflows/scripted-workflow.ts +206 -6
- package/src/workflows/workflow-child-summary.ts +1 -1
- package/src/workflows/workflow-receipt.ts +41 -4
- package/src/workflows/workflow-resources.ts +150 -0
|
@@ -9,31 +9,26 @@ const EXTERNAL_CLI_RUNNER_GUIDANCE = "External CLI agents (codex-exec, codex-exe
|
|
|
9
9
|
const WORKFLOW_RESUME_KEY_GUIDANCE = "Each workflow key identifies one result lane: use a new stable workflow key for every distinct retained resume pass; same-key calls are reused only when launch parameters are identical, and incompatible parameters are rejected.";
|
|
10
10
|
const WORKFLOW_OUTPUT_BINDING_GUIDANCE = "For durable workflow child files, set output on runs.run/runs.all; task filename prose is not an output declaration, and return the child's outputReference, outputPathMapping, or artifactPaths instead of inventing a literal path.";
|
|
11
11
|
const WORKFLOW_LANES_GUIDANCE = "For bounded parallel sequential chains, use runs.lanes([{key,stages:[{key,agent,task},{key,resume:'previous',task},...]}]); first stages run together, later stages sequence per lane, and the bounded board reports lane-local failures. Only an explicit structuredOutput.verdict === 'blocked' blocks a successful stage; reviewer prose is not parsed.";
|
|
12
|
-
const
|
|
12
|
+
const WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE = "workflowScript rejects nested async function, arrow, and method helpers; use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains instead.";
|
|
13
|
+
const WORKFLOW_RESOURCE_GUIDANCE = "For permission/policy-extension interoperability, use an extension-owned named resource such as {workflow:'review',args:{task:'...'}} or {workflow:'run-ci',args:{command:'npm test'}}. The host resolves the script and authority internally so policy can distinguish it from raw workflowScript/workflowScriptPath; args are bounded plain data, and do not combine workflow with agent, task, workflowScript, or workflowScriptPath.";
|
|
14
|
+
const WORKFLOW_HOST_GUIDANCE = "For permission-sensitive host calls, use an extension-owned resource such as {workflow:'run-ci',args:{command:'npm test'}}; raw workflowScript/workflowScriptPath have unknown resource provenance and cannot use runs.host. In a resource that grants it, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). runs.host has no per-step cwd: commands and relative output paths use the workflow cwd; set cwd on the outer subagent request instead (for example, {cwd:'/path/to/worktree',workflowScript:'...'}), or put a trusted directory change in the command (for example, 'cd /path/to/worktree && npm test'). v1 supports only command steps; output is bounded and command failure fails the workflow.";
|
|
13
15
|
|
|
14
|
-
export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration,
|
|
16
|
+
export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration, workflowScriptPath to load a script from the request cwd, or a named workflow resource for permission/policy-aware execution. ${WORKFLOW_RESOURCE_GUIDANCE} The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. ${WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE} ${WORKFLOW_LANES_GUIDANCE} ${WORKFLOW_HOST_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
|
|
15
17
|
|
|
16
18
|
export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
|
|
17
19
|
|
|
18
20
|
export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"workflowScript
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
WORKFLOW_RESUME_KEY_GUIDANCE,
|
|
25
|
-
WORKFLOW_OUTPUT_BINDING_GUIDANCE,
|
|
26
|
-
"For ordinary parallel work, use await runs.all([{key,agent,task}, ...]); it resolves to an ordered array, not a key map, so use results[0], destructuring, or results.map(...), not results.<key>. 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.",
|
|
27
|
-
"Keep one writer per cwd/worktree unless writers run in isolated worktrees.",
|
|
28
|
-
"To pass an explicit model to a child, first call { action: \"models\" } and copy an exact provider/id (e.g. openai-codex/gpt-5.6-sol); bare ids resolve only when unique in the registry, and agent names (gpt-pro, advisor) are not model ids. Set per-run thinking with a suffix on the model string (e.g. openai-codex/gpt-5.6-sol:high; off/minimal/low/medium/high/xhigh/max); the suffix wins over the agent's thinking default. The thinking field only applies to action='watchdog.configure' and is ignored on dispatch.",
|
|
29
|
-
EXTERNAL_CLI_RUNNER_GUIDANCE,
|
|
30
|
-
"Use guide or the pi-subagents skill for advanced scheduling, missions, steering, and retention.",
|
|
21
|
+
'Use subagent only when delegation is needed. Before execution, call { action: "list" } and run only executable, non-disabled agents.',
|
|
22
|
+
'Omit action for execution; use { agent, task? } for one child. For multi-step or parallel work, make exactly one top-level { workflowScript, async: true } call and launch children only inside it. Use action only for management/control.',
|
|
23
|
+
"workflowScript rejects nested async function, arrow, and method helpers; use top-level await, plain helper functions, or explicit Promise chains.",
|
|
24
|
+
"Inside workflowScript, use runs.run/runs.all and await their results. runs.all returns an ordered array, not a key map; stored runs.run promises must later be observed with direct await, Promise.race, or Promise.all.",
|
|
25
|
+
'Keep one writer per cwd/worktree; isolate concurrent writers. For durable files, set output on runs.run/runs.all and return the child\'s outputReference, outputPathMapping, or artifactPaths. For advanced workflows, read the bundled pi-subagents skill or call { action: "guide", topic: "workflows" }.',
|
|
31
26
|
];
|
|
32
27
|
|
|
33
28
|
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
34
29
|
• Use { action: "list" } before execution and only run executable/non-disabled agents.
|
|
35
30
|
• Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
|
|
36
|
-
• Async/background runs are the normal default unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Use async:false only when the parent must block until completion. Async mode still shows progress. Final reviews and gate checks stay async; needing a result is not a blocking reason. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll status just to wait; use
|
|
31
|
+
• Async/background runs are the normal default unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Use async:false only when the parent must block until completion. Async mode still shows progress. Final reviews and gate checks stay async; needing a result is not a blocking reason. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Ordinary async subagents notify this session natively, so return control and do not call bg_wait merely to get a completion wake. Do not sleep or poll status just to wait; use bg_wait only for provider, detached, or other background work without a native notification when this turn must receive its result.
|
|
37
32
|
• ${WORKFLOW_RESUME_KEY_GUIDANCE}
|
|
38
33
|
• ${WORKFLOW_OUTPUT_BINDING_GUIDANCE}
|
|
39
34
|
• ${WORKFLOW_HOST_GUIDANCE}
|
|
@@ -44,6 +39,8 @@ export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
|
44
39
|
|
|
45
40
|
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for inline orchestration or { workflowScriptPath } to load it from the request cwd. The script inputs are mutually exclusive. Omit action for execution. Use action only for management/control actions.
|
|
46
41
|
|
|
42
|
+
${WORKFLOW_RESOURCE_GUIDANCE}
|
|
43
|
+
|
|
47
44
|
EXECUTION:
|
|
48
45
|
• ${EXTERNAL_CLI_RUNNER_GUIDANCE}
|
|
49
46
|
• Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
|
|
@@ -66,6 +63,8 @@ ${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
|
66
63
|
|
|
67
64
|
export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task? }; use { workflowScript } for inline orchestration or { workflowScriptPath } to load it from the request cwd. The script inputs are mutually exclusive. Omit action for execution. Use action only for management/control actions.
|
|
68
65
|
|
|
66
|
+
${WORKFLOW_RESOURCE_GUIDANCE}
|
|
67
|
+
|
|
69
68
|
EXECUTE:
|
|
70
69
|
• ${EXTERNAL_CLI_RUNNER_GUIDANCE}
|
|
71
70
|
• Call { action:"list" } first and use only executable/non-disabled agents.
|
|
@@ -82,9 +81,10 @@ MANAGE / CONTROL:
|
|
|
82
81
|
• 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}.
|
|
83
82
|
|
|
84
83
|
ASYNC / SAFETY:
|
|
85
|
-
• Omitted async follows asyncByDefault config; set async:true explicitly when async behavior matters. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll merely to wait; use
|
|
84
|
+
• Omitted async follows asyncByDefault config; set async:true explicitly when async behavior matters. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Ordinary async subagents notify this session natively, so return control and do not call bg_wait merely to get a completion wake. Do not sleep or poll merely to wait; use bg_wait only for provider, detached, or other background work without a native notification when this turn must receive its result.
|
|
86
85
|
• ${WORKFLOW_RESUME_KEY_GUIDANCE}
|
|
87
86
|
• ${WORKFLOW_OUTPUT_BINDING_GUIDANCE}
|
|
87
|
+
• ${WORKFLOW_HOST_GUIDANCE}
|
|
88
88
|
• Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
|
|
89
89
|
• Oracle/advisor consultations use available supervisor dialogue for material unknowns; request one-shot when desired.
|
|
90
90
|
• Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
|
|
@@ -22,10 +22,11 @@ import type { ContextMode } from "../shared/context-mode.ts";
|
|
|
22
22
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
23
23
|
import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
|
|
24
24
|
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
25
|
+
import { backgroundProcessOptions } from "../shared/background-process-options.ts";
|
|
25
26
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
26
27
|
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
27
28
|
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
|
|
28
|
-
import { buildModelCandidates,
|
|
29
|
+
import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelOrigin, resolveSubagentModelOverride, type AvailableModelInfo, type ModelOrigin, type ParentModel } from "../shared/model-fallback.ts";
|
|
29
30
|
import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
|
|
30
31
|
import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
|
|
31
32
|
import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
@@ -234,6 +235,7 @@ interface AsyncSingleParams {
|
|
|
234
235
|
structuredOutputSchema?: JsonSchemaObject;
|
|
235
236
|
modelOverride?: string;
|
|
236
237
|
modelOverrideFromParent?: boolean;
|
|
238
|
+
modelOrigin?: ModelOrigin;
|
|
237
239
|
fast?: boolean;
|
|
238
240
|
thinkingOverride?: AgentConfig["thinking"];
|
|
239
241
|
availableModels?: AvailableModelInfo[];
|
|
@@ -338,15 +340,15 @@ export function formatAsyncStartedMessage(headline: string, interactive: boolean
|
|
|
338
340
|
const guidance = interactive
|
|
339
341
|
? [
|
|
340
342
|
"The async run is detached and running in the background.",
|
|
341
|
-
"You are in an interactive session.
|
|
342
|
-
"
|
|
343
|
-
"
|
|
343
|
+
"You are in an interactive session. Return control to the user now; Pi will wake you through the native completion notification when this subagent completes or needs attention. Do not run sleep/polling loops to wait for this async subagent; it does not need a wait call.",
|
|
344
|
+
"Use bg_wait only for provider, detached, or other background work that lacks a native completion notification.",
|
|
345
|
+
"If the current turn must receive results from work without a native notification before it ends, call blocking bg_wait(); ordinary async subagent runs do not need a wait call because their completion is delivered natively.",
|
|
344
346
|
"Otherwise, continue any independent work or return control to the user. Use subagent({ action: \"status\", id: \"...\" }) for a one-shot status/result or to inspect a blocked/stale run, never as a wait loop.",
|
|
345
347
|
]
|
|
346
348
|
: [
|
|
347
349
|
"The async run is detached. Do not run sleep timers or polling loops just to wait for it.",
|
|
348
|
-
"This is a non-interactive run: Pi auto-drains current-session
|
|
349
|
-
"Use subagent({ action: \"status\", id: \"...\" }) when you need a one-shot status/result or to inspect a blocked/stale run
|
|
350
|
+
"This is a non-interactive run: Pi auto-drains current-session subagent work at agent_end so detached children are not abandoned. Use bg_wait only when this turn must receive provider, detached, or other background-work results that have no native completion notification.",
|
|
351
|
+
"Use subagent({ action: \"status\", id: \"...\" }) when you need a one-shot status/result or to inspect a blocked/stale run; do not poll in a loop.",
|
|
350
352
|
];
|
|
351
353
|
return [headline, "", ...guidance].join("\n");
|
|
352
354
|
}
|
|
@@ -562,9 +564,8 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
562
564
|
}
|
|
563
565
|
const proc = spawn(nodeCommand, [jitiCliPath, runner, cfgPath], {
|
|
564
566
|
cwd,
|
|
565
|
-
|
|
567
|
+
...backgroundProcessOptions(),
|
|
566
568
|
stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
|
|
567
|
-
windowsHide: true,
|
|
568
569
|
env: {
|
|
569
570
|
...omitExtensionBindingsEnv(process.env),
|
|
570
571
|
...(piPackageRoot ? { [PI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot } : {}),
|
|
@@ -857,14 +858,15 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
857
858
|
const task = namespaceOutputPath ? taskText : injectSingleOutputInstruction(taskText, outputPath, a);
|
|
858
859
|
|
|
859
860
|
const modelScopes = resolveModelScopesForAgent(ctx.modelScope, a.name, ctx.currentModel);
|
|
860
|
-
const
|
|
861
|
+
const modelOrigin = resolveModelOrigin({ explicitModel: s.model, agentModel: a.model, parentModel: ctx.currentModel });
|
|
862
|
+
const primaryModelFromParent = modelOrigin === "inherited";
|
|
861
863
|
const primaryModel = externalRunner ? undefined : resolveEffectiveSubagentModel(
|
|
862
864
|
s.model,
|
|
863
865
|
a.model,
|
|
864
866
|
ctx.currentModel,
|
|
865
867
|
availableModels,
|
|
866
868
|
a.modelProvider ?? ctx.currentModelProvider,
|
|
867
|
-
{ scope: modelScopes },
|
|
869
|
+
{ scope: modelScopes, source: modelOrigin === "explicit" ? "explicit" : "inherited" },
|
|
868
870
|
);
|
|
869
871
|
const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
|
|
870
872
|
const effectiveThinking = externalRunner ? undefined : thinkingOverride ?? a.thinking;
|
|
@@ -884,15 +886,17 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
884
886
|
}
|
|
885
887
|
const agentContract = s.agentContract ?? params.agentContract;
|
|
886
888
|
const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
|
|
887
|
-
|
|
888
|
-
scope: modelScopes,
|
|
889
|
-
primaryModelFromParent,
|
|
890
|
-
}).flatMap((candidate) => {
|
|
891
|
-
const resolved = applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined);
|
|
892
|
-
return resolved ? [resolved] : [];
|
|
893
|
-
});
|
|
889
|
+
let modelCandidates: string[] = [];
|
|
894
890
|
if (!externalRunner) {
|
|
895
891
|
try {
|
|
892
|
+
modelCandidates = buildModelCandidates(primaryModel, a.fallbackModels, availableModels, a.modelProvider ?? ctx.currentModelProvider, {
|
|
893
|
+
scope: modelScopes,
|
|
894
|
+
primaryModelFromParent,
|
|
895
|
+
origin: modelOrigin,
|
|
896
|
+
}).flatMap((candidate) => {
|
|
897
|
+
const resolved = applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined);
|
|
898
|
+
return resolved ? [resolved] : [];
|
|
899
|
+
});
|
|
896
900
|
for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
|
|
897
901
|
} catch (error) {
|
|
898
902
|
throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
|
|
@@ -1588,15 +1592,27 @@ export function executeAsyncSingle(
|
|
|
1588
1592
|
: "";
|
|
1589
1593
|
const taskText = readsInstruction + taskWithOutputInstruction;
|
|
1590
1594
|
const modelScopes = resolveModelScopesForAgent(ctx.modelScope, agentConfig.name, ctx.currentModel);
|
|
1591
|
-
const
|
|
1592
|
-
|
|
1593
|
-
:
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1595
|
+
const modelOrigin = resolveModelOrigin({
|
|
1596
|
+
fromParent: params.modelOverrideFromParent,
|
|
1597
|
+
storedOrigin: params.modelOrigin,
|
|
1598
|
+
explicitModel: params.modelOverrideFromParent ? undefined : params.modelOverride,
|
|
1599
|
+
agentModel: agentConfig.model,
|
|
1600
|
+
parentModel: ctx.currentModel,
|
|
1601
|
+
});
|
|
1602
|
+
let primaryModel: string | undefined;
|
|
1603
|
+
try {
|
|
1604
|
+
primaryModel = externalRunner ? undefined : modelOrigin === "inherited"
|
|
1605
|
+
? params.modelOverride ?? (ctx.currentModel ? `${ctx.currentModel.provider}/${ctx.currentModel.id}` : undefined)
|
|
1606
|
+
: resolveSubagentModelOverride(
|
|
1607
|
+
params.modelOverride ?? agentConfig.model,
|
|
1608
|
+
ctx.currentModel,
|
|
1609
|
+
availableModels,
|
|
1610
|
+
ctx.currentModelProvider,
|
|
1611
|
+
{ scope: modelScopes, source: modelOrigin === "explicit" ? "explicit" : "inherited" },
|
|
1612
|
+
);
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
|
|
1615
|
+
}
|
|
1600
1616
|
const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
|
|
1601
1617
|
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
|
|
1602
1618
|
const contextLimit = model ? findModelInfo(model, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider)?.contextWindow : undefined;
|
|
@@ -1633,18 +1649,17 @@ export function executeAsyncSingle(
|
|
|
1633
1649
|
const structuredOutput = params.structuredOutputSchema
|
|
1634
1650
|
? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"), { captureAcceptanceReport: params.acceptance !== false })
|
|
1635
1651
|
: undefined;
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1652
|
+
let modelCandidates: string[] = [];
|
|
1653
|
+
if (!externalRunner) {
|
|
1654
|
+
try {
|
|
1655
|
+
modelCandidates = buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider, {
|
|
1656
|
+
scope: modelScopes,
|
|
1657
|
+
primaryModelFromParent: modelOrigin === "inherited",
|
|
1658
|
+
origin: modelOrigin,
|
|
1659
|
+
}).flatMap((candidate) => {
|
|
1643
1660
|
const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
|
|
1644
1661
|
return resolved ? [resolved] : [];
|
|
1645
1662
|
});
|
|
1646
|
-
if (!externalRunner) {
|
|
1647
|
-
try {
|
|
1648
1663
|
for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: agentConfig.name, runId: id });
|
|
1649
1664
|
} catch (error) {
|
|
1650
1665
|
return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
|
|
@@ -1730,7 +1745,8 @@ export function executeAsyncSingle(
|
|
|
1730
1745
|
...(model ? { model } : {}),
|
|
1731
1746
|
...(params.fast ?? recoveryAgentConfig.fast ? { fast: params.fast ?? recoveryAgentConfig.fast } : {}),
|
|
1732
1747
|
...(recoveryAgentConfig.modelProvider ? { modelProvider: recoveryAgentConfig.modelProvider } : {}),
|
|
1733
|
-
...(
|
|
1748
|
+
...(modelOrigin === "inherited" ? { modelOverrideFromParent: true } : {}),
|
|
1749
|
+
modelOrigin,
|
|
1734
1750
|
...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
|
|
1735
1751
|
...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
|
|
1736
1752
|
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
@@ -1799,7 +1815,7 @@ export function executeAsyncSingle(
|
|
|
1799
1815
|
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
1800
1816
|
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
1801
1817
|
modelCandidates,
|
|
1802
|
-
...(
|
|
1818
|
+
...(modelOrigin === "inherited" ? { skipPrimaryModelVerification: true } : {}),
|
|
1803
1819
|
...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
|
|
1804
1820
|
tools: agentConfig.tools,
|
|
1805
1821
|
allowNestedSubagents: agentConfig.allowNestedSubagents,
|
|
@@ -73,7 +73,22 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
73
73
|
const steeringNoticeSeen = new Map<string, number>();
|
|
74
74
|
const jobWatchers = new Map<string, { watchers: Map<string, fs.FSWatcher>; retryTimer?: ReturnType<typeof setTimeout> }>();
|
|
75
75
|
const refreshTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
76
|
+
let widgetRerenderTimer: ReturnType<typeof setTimeout> | undefined;
|
|
76
77
|
const runningJobIds = new Set<string>();
|
|
78
|
+
const externalJobBridgeRuns = new Set<string>();
|
|
79
|
+
const externalJobBridgeEligibility = (steps: AsyncJobState["steps"]): "required" | "not-required" | "unknown" => {
|
|
80
|
+
if (!Array.isArray(steps)) return "unknown";
|
|
81
|
+
for (const step of steps) {
|
|
82
|
+
const runner = (step as { runner?: unknown } | null)?.runner;
|
|
83
|
+
if (runner === undefined) continue;
|
|
84
|
+
if (!runner || typeof runner !== "object" || Array.isArray(runner)) return "unknown";
|
|
85
|
+
const runnerType = (runner as { type?: unknown }).type;
|
|
86
|
+
if (typeof runnerType !== "string") return "unknown";
|
|
87
|
+
if (runnerType === "external-job") return "required";
|
|
88
|
+
if (runnerType !== "pi" && runnerType !== "external-cli") return "unknown";
|
|
89
|
+
}
|
|
90
|
+
return "not-required";
|
|
91
|
+
};
|
|
77
92
|
let rootWatcher: fs.FSWatcher | undefined;
|
|
78
93
|
let nextLivenessAt = Date.now() + livenessIntervalMs;
|
|
79
94
|
let nextWidgetAnimationAt = Date.now() + WIDGET_ANIMATION_INTERVAL_MS;
|
|
@@ -94,9 +109,22 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
94
109
|
const rerenderLastWidget = (jobs = Array.from(state.asyncJobs.values())) => {
|
|
95
110
|
withLastUiContext((ctx) => rerenderWidget(ctx, jobs));
|
|
96
111
|
};
|
|
112
|
+
const scheduleLastWidgetRerender = () => {
|
|
113
|
+
if (widgetRerenderTimer) return;
|
|
114
|
+
widgetRerenderTimer = setTimeout(() => {
|
|
115
|
+
widgetRerenderTimer = undefined;
|
|
116
|
+
rerenderLastWidget();
|
|
117
|
+
}, EVENT_REFRESH_DEBOUNCE_MS);
|
|
118
|
+
widgetRerenderTimer.unref?.();
|
|
119
|
+
};
|
|
97
120
|
const requestLastWidgetRender = () => {
|
|
98
121
|
if (options.widgetEnabled === false) return;
|
|
99
|
-
|
|
122
|
+
withLastUiContext((ctx) => {
|
|
123
|
+
if (state.widgetsSuspended) return;
|
|
124
|
+
const requestRender = (ctx.ui as { requestRender?: () => void }).requestRender;
|
|
125
|
+
if (requestRender) requestRender.call(ctx.ui);
|
|
126
|
+
else renderWidget(ctx, Array.from(state.asyncJobs.values()));
|
|
127
|
+
});
|
|
100
128
|
};
|
|
101
129
|
const refreshWidget = (ctx: ExtensionContext) => rerenderWidget(ctx);
|
|
102
130
|
const restoredControlEventCursor = (asyncDir: string) => {
|
|
@@ -146,6 +174,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
146
174
|
chainStepCount: run.chainStepCount,
|
|
147
175
|
parallelGroups: groups,
|
|
148
176
|
hostSteps: run.hostSteps,
|
|
177
|
+
...(run.mode === "workflow" && run.workflowGraph ? { workflowGraph: run.workflowGraph } : {}),
|
|
149
178
|
preflight: run.preflight,
|
|
150
179
|
steps: visibleSteps,
|
|
151
180
|
stepsTotal: visibleSteps.length,
|
|
@@ -339,6 +368,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
339
368
|
if (timer) clearTimeout(timer);
|
|
340
369
|
refreshTimers.delete(asyncId);
|
|
341
370
|
runningJobIds.delete(asyncId);
|
|
371
|
+
externalJobBridgeRuns.delete(asyncId);
|
|
342
372
|
};
|
|
343
373
|
|
|
344
374
|
const refreshJob = (job: AsyncJobState): boolean => {
|
|
@@ -353,9 +383,15 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
353
383
|
console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
|
|
354
384
|
}
|
|
355
385
|
};
|
|
386
|
+
let bridgeSweepAttempted = false;
|
|
356
387
|
try {
|
|
357
388
|
emitNewControlEvents(job);
|
|
358
|
-
|
|
389
|
+
const bridgeAlreadyRequired = externalJobBridgeRuns.has(job.asyncId) || externalJobBridgeEligibility(job.steps) === "required";
|
|
390
|
+
if (bridgeAlreadyRequired) {
|
|
391
|
+
externalJobBridgeRuns.add(job.asyncId);
|
|
392
|
+
bridgeSweepAttempted = true;
|
|
393
|
+
serviceExternalJobBridgeRequests(job.asyncDir);
|
|
394
|
+
}
|
|
359
395
|
try {
|
|
360
396
|
if (job.nestedRoute) reconcileNestedAsyncDescendants(job.nestedRoute, { resultsDir, kill: options.kill, now: options.now });
|
|
361
397
|
} catch (error) {
|
|
@@ -381,6 +417,15 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
381
417
|
},
|
|
382
418
|
});
|
|
383
419
|
const status = reconciliation.status ?? readStatus(job.asyncDir);
|
|
420
|
+
if (!bridgeAlreadyRequired) {
|
|
421
|
+
const bridgeEligibility = externalJobBridgeEligibility(status?.steps);
|
|
422
|
+
if (bridgeEligibility === "required") externalJobBridgeRuns.add(job.asyncId);
|
|
423
|
+
// Missing or ambiguous status retains the legacy sweep for recovery races.
|
|
424
|
+
if (bridgeEligibility !== "not-required") {
|
|
425
|
+
bridgeSweepAttempted = true;
|
|
426
|
+
serviceExternalJobBridgeRequests(job.asyncDir);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
384
429
|
if (status) {
|
|
385
430
|
const previousStatus = job.status;
|
|
386
431
|
job.status = status.state;
|
|
@@ -401,6 +446,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
401
446
|
job.workflowKey = status.workflowKey ?? job.workflowKey;
|
|
402
447
|
job.lane = status.lane ?? job.lane;
|
|
403
448
|
job.workflow = status.workflow ?? job.workflow;
|
|
449
|
+
if (status.mode === "workflow") job.workflowGraph = status.workflowGraph ?? job.workflowGraph;
|
|
404
450
|
job.hostSteps = validHostStepNodes(status.workflowGraph);
|
|
405
451
|
const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
|
|
406
452
|
if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error("workflowChildren.workflowRunId does not match async status runId.");
|
|
@@ -454,6 +500,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
454
500
|
runningJobIds.add(job.asyncId);
|
|
455
501
|
}
|
|
456
502
|
} catch (error) {
|
|
503
|
+
if (!bridgeSweepAttempted) {
|
|
504
|
+
try {
|
|
505
|
+
bridgeSweepAttempted = true;
|
|
506
|
+
serviceExternalJobBridgeRequests(job.asyncDir);
|
|
507
|
+
} catch (bridgeError) {
|
|
508
|
+
console.error(`Failed to service external job bridge for '${job.asyncDir}':`, bridgeError);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
457
511
|
if (job.status !== "failed") {
|
|
458
512
|
console.error(`Failed to read async status for '${job.asyncDir}':`, error);
|
|
459
513
|
job.status = "failed";
|
|
@@ -471,7 +525,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
471
525
|
const timer = setTimeout(() => {
|
|
472
526
|
refreshTimers.delete(asyncId);
|
|
473
527
|
const job = state.asyncJobs.get(asyncId);
|
|
474
|
-
if (job && refreshJob(job))
|
|
528
|
+
if (job && refreshJob(job)) scheduleLastWidgetRerender();
|
|
475
529
|
}, delayMs);
|
|
476
530
|
timer.unref?.();
|
|
477
531
|
refreshTimers.set(asyncId, timer);
|
|
@@ -602,6 +656,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
602
656
|
: rawAgents;
|
|
603
657
|
const sessionRoot = state.liveAsyncSessionRoots?.get(info.id);
|
|
604
658
|
state.liveAsyncSessionRoots?.delete(info.id);
|
|
659
|
+
externalJobBridgeRuns.delete(info.id);
|
|
605
660
|
state.asyncJobs.set(info.id, {
|
|
606
661
|
asyncId: info.id,
|
|
607
662
|
asyncDir,
|
|
@@ -627,6 +682,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
627
682
|
turnBudget: info.turnBudget,
|
|
628
683
|
parentWorkflowRunId: info.parentWorkflowRunId,
|
|
629
684
|
workflowKey: info.workflowKey,
|
|
685
|
+
...(info.mode === "workflow" && info.workflowGraph ? { workflowGraph: info.workflowGraph } : {}),
|
|
630
686
|
controlEventCursor: 0,
|
|
631
687
|
});
|
|
632
688
|
const job = state.asyncJobs.get(info.id)!;
|
|
@@ -674,11 +730,15 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
674
730
|
for (const asyncId of jobWatchers.keys()) closeJobWatcher(asyncId);
|
|
675
731
|
for (const timer of refreshTimers.values()) clearTimeout(timer);
|
|
676
732
|
refreshTimers.clear();
|
|
733
|
+
if (widgetRerenderTimer) clearTimeout(widgetRerenderTimer);
|
|
734
|
+
widgetRerenderTimer = undefined;
|
|
677
735
|
runningJobIds.clear();
|
|
736
|
+
externalJobBridgeRuns.clear();
|
|
678
737
|
};
|
|
679
738
|
|
|
680
739
|
const resetJobs = (ctx?: ExtensionContext) => {
|
|
681
740
|
dispose();
|
|
741
|
+
state.statusProjectionSessionId = null;
|
|
682
742
|
for (const timer of state.cleanupTimers.values()) clearTimeout(timer);
|
|
683
743
|
state.cleanupTimers.clear();
|
|
684
744
|
state.asyncJobs.clear();
|
|
@@ -695,6 +755,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
695
755
|
|
|
696
756
|
const restoreActiveJobs = (ctx?: ExtensionContext) => {
|
|
697
757
|
if (ctx?.hasUI) state.lastUiContext = ctx;
|
|
758
|
+
state.statusProjectionSessionId = null;
|
|
698
759
|
if (!state.currentSessionId) return;
|
|
699
760
|
let runs: AsyncRunSummary[];
|
|
700
761
|
try {
|
|
@@ -710,6 +771,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
710
771
|
rememberFleetJob(state, job);
|
|
711
772
|
watchJob(job);
|
|
712
773
|
}
|
|
774
|
+
state.statusProjectionSessionId = state.currentSessionId;
|
|
713
775
|
if (runs.length === 0) return;
|
|
714
776
|
ensurePoller();
|
|
715
777
|
rerenderLastWidget();
|
|
@@ -314,7 +314,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
314
314
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
|
|
315
315
|
const parsed = value as Record<string, unknown>;
|
|
316
316
|
const allowedFields = new Set([
|
|
317
|
-
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "fallbackModels", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
|
|
317
|
+
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
|
|
318
318
|
"subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
|
|
319
319
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
320
320
|
"artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
@@ -347,6 +347,8 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
347
347
|
if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
|
|
348
348
|
if (parsed.context !== undefined && parsed.context !== "fresh" && parsed.context !== "fork") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': context is invalid.`);
|
|
349
349
|
if (parsed.modelOverrideFromParent !== undefined && typeof parsed.modelOverrideFromParent !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOverrideFromParent must be a boolean.`);
|
|
350
|
+
if (parsed.modelOrigin !== undefined && parsed.modelOrigin !== "explicit" && parsed.modelOrigin !== "inherited" && parsed.modelOrigin !== "configured") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOrigin must be 'explicit', 'inherited', or 'configured'.`);
|
|
351
|
+
if (parsed.modelOrigin === undefined && parsed.model !== undefined) parsed.modelOrigin = parsed.modelOverrideFromParent ? "inherited" : "configured";
|
|
350
352
|
for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
|
|
351
353
|
if (typeof parsed[field] !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a boolean.`);
|
|
352
354
|
}
|