pi-subagents 0.54.0 → 0.55.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 +35 -1
- package/agents/reviewer.md +12 -2
- package/docs/agents.md +4 -4
- package/docs/configuration.md +4 -4
- package/docs/extension-api.md +14 -4
- package/docs/models.md +24 -3
- package/docs/observability.md +14 -3
- package/docs/tool-reference.md +21 -6
- package/docs/workflows.md +7 -1
- package/package.json +1 -1
- package/prompts/council.md +9 -0
- package/prompts/parallel-review.md +5 -1
- package/prompts/review-loop.md +9 -5
- package/skills/council-mode/SKILL.md +33 -10
- package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
- package/skills/pi-subagents/references/execution-controls.md +11 -4
- package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +8 -8
- package/src/agents/agent-management.ts +30 -9
- package/src/agents/agent-serializer.ts +2 -2
- package/src/agents/agents.ts +146 -41
- package/src/api/external-job-provider.ts +10 -1
- package/src/api/preflight.ts +26 -17
- package/src/api/project-panes.ts +2 -0
- package/src/extension/index.ts +20 -1
- package/src/extension/public-execution.ts +12 -9
- package/src/extension/rpc.ts +77 -3
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -6
- package/src/inspectors/herdr/focus.ts +55 -0
- package/src/inspectors/herdr/project-panes.ts +228 -44
- package/src/integrations/herdr-status.ts +26 -4
- package/src/runs/background/async-execution.ts +103 -9
- package/src/runs/background/async-job-tracker.ts +33 -14
- package/src/runs/background/async-resume.ts +20 -2
- package/src/runs/background/async-status.ts +10 -0
- package/src/runs/background/control-channel.ts +98 -10
- package/src/runs/background/notify.ts +62 -1
- package/src/runs/background/run-status.ts +15 -1
- package/src/runs/background/subagent-runner.ts +226 -37
- package/src/runs/foreground/async-stop-action.ts +23 -2
- package/src/runs/foreground/execution.ts +81 -7
- package/src/runs/foreground/subagent-executor.ts +348 -40
- package/src/runs/foreground/workflow-detach-reconcile.ts +3 -0
- package/src/runs/shared/acceptance.ts +1 -0
- package/src/runs/shared/child-identity.ts +36 -0
- package/src/runs/shared/completion-guard.ts +50 -1
- package/src/runs/shared/external-job-bridge.ts +53 -37
- package/src/runs/shared/external-job-runner.ts +126 -23
- package/src/runs/shared/model-fallback.ts +10 -0
- package/src/runs/shared/orca-progress-tabs.ts +71 -9
- package/src/runs/shared/parallel-utils.ts +12 -0
- package/src/runs/shared/pi-args.ts +9 -12
- package/src/runs/shared/subagent-prompt-runtime.ts +3 -1
- package/src/runs/shared/tool-availability.ts +1 -3
- package/src/shared/launch-contract.ts +6 -5
- package/src/shared/thinking-ceiling.ts +52 -0
- package/src/shared/types.ts +59 -2
- package/src/tui/fleet-status.ts +66 -13
- package/src/tui/render.ts +5 -4
- package/src/workflows/scripted-workflow.ts +107 -10
|
@@ -28,12 +28,14 @@ import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentMode
|
|
|
28
28
|
import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
|
|
29
29
|
import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
|
|
30
30
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
31
|
+
import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
|
|
31
32
|
import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
|
|
32
33
|
import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
|
|
33
34
|
import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
|
|
34
35
|
import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
|
|
35
36
|
import { resolveEffectiveAcceptance, validateAcceptanceInput, validateExecutionAcceptance } from "../shared/acceptance.ts";
|
|
36
37
|
import { createRunFanoutBudget, writeRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
38
|
+
import { validateImplementationToolContract } from "../shared/completion-guard.ts";
|
|
37
39
|
import {
|
|
38
40
|
type AcceptanceInput,
|
|
39
41
|
type AgentContract,
|
|
@@ -187,6 +189,7 @@ interface AsyncChainParams {
|
|
|
187
189
|
/** Global cap on simultaneously-running subagent tasks within the async run. */
|
|
188
190
|
globalConcurrencyLimit?: number;
|
|
189
191
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
192
|
+
thinkingCeiling?: ThinkingLevel;
|
|
190
193
|
runFanoutBudget?: RunFanoutBudgetDescriptor;
|
|
191
194
|
parentWorkflowRunId?: string;
|
|
192
195
|
workflowKey?: string;
|
|
@@ -249,10 +252,19 @@ interface AsyncSingleParams {
|
|
|
249
252
|
toolTimeoutMsEnv?: string | undefined;
|
|
250
253
|
allowZeroToolBudget?: boolean;
|
|
251
254
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
255
|
+
thinkingCeiling?: ThinkingLevel;
|
|
252
256
|
runFanoutBudget?: RunFanoutBudgetDescriptor;
|
|
253
257
|
parentWorkflowRunId?: string;
|
|
254
258
|
workflowKey?: string;
|
|
259
|
+
workflowAwaitAsync?: boolean;
|
|
255
260
|
activeAsyncCapacity?: ActiveAsyncCapacityHandle;
|
|
261
|
+
externalJobFollowUp?: {
|
|
262
|
+
sourceRunId: string;
|
|
263
|
+
sourceStepIndex: number;
|
|
264
|
+
parentProviderJobId: string;
|
|
265
|
+
requestId: string;
|
|
266
|
+
requestDigest: string;
|
|
267
|
+
};
|
|
256
268
|
}
|
|
257
269
|
|
|
258
270
|
interface AsyncExecutionResult {
|
|
@@ -292,6 +304,7 @@ export interface AsyncRunnerStepBuildParams {
|
|
|
292
304
|
/** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
|
|
293
305
|
toolTimeoutMsEnv?: string | undefined;
|
|
294
306
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
307
|
+
thinkingCeiling?: ThinkingLevel;
|
|
295
308
|
}
|
|
296
309
|
|
|
297
310
|
export type AsyncRunnerStepBuildResult =
|
|
@@ -792,17 +805,30 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
792
805
|
const task = namespaceOutputPath ? taskText : injectSingleOutputInstruction(taskText, outputPath, a);
|
|
793
806
|
|
|
794
807
|
const modelScopes = resolveModelScopesForAgent(ctx.modelScope, a.name, ctx.currentModel);
|
|
808
|
+
const primaryModelFromParent = inheritsParentModel(s.model, a.model, ctx.currentModel);
|
|
795
809
|
const primaryModel = externalRunner ? undefined : resolveEffectiveSubagentModel(
|
|
796
810
|
s.model,
|
|
797
811
|
a.model,
|
|
798
812
|
ctx.currentModel,
|
|
799
813
|
availableModels,
|
|
800
|
-
ctx.currentModelProvider,
|
|
814
|
+
a.modelProvider ?? ctx.currentModelProvider,
|
|
801
815
|
{ scope: modelScopes },
|
|
802
816
|
);
|
|
803
817
|
const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
|
|
804
818
|
const effectiveThinking = externalRunner ? undefined : thinkingOverride ?? a.thinking;
|
|
805
819
|
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
|
|
820
|
+
const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
|
|
821
|
+
params.thinkingCeiling,
|
|
822
|
+
a.maxThinking,
|
|
823
|
+
decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
|
|
824
|
+
);
|
|
825
|
+
if (!externalRunner) {
|
|
826
|
+
try {
|
|
827
|
+
assertThinkingWithinCeiling({ model, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
|
|
828
|
+
} catch (error) {
|
|
829
|
+
throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
806
832
|
const agentContract = s.agentContract ?? params.agentContract;
|
|
807
833
|
const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
|
|
808
834
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
@@ -822,6 +848,34 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
822
848
|
if (externalRunner && permissionRules) {
|
|
823
849
|
throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}', which cannot enforce native Pi child permission rules.`);
|
|
824
850
|
}
|
|
851
|
+
if (!externalRunner) {
|
|
852
|
+
const contractTools = toolPlan.explicitToolAllowlist ? toolPlan.effectiveToolAllowlist : undefined;
|
|
853
|
+
const contractError = validateImplementationToolContract({
|
|
854
|
+
agent: a.name,
|
|
855
|
+
task,
|
|
856
|
+
tools: contractTools,
|
|
857
|
+
mcpDirectTools: toolPlan.effectiveMcpTools,
|
|
858
|
+
configuredExtensions: toolPlan.configuredExtensions,
|
|
859
|
+
requestedTools: toolPlan.requestedBuiltinTools,
|
|
860
|
+
acceptanceRole: a.acceptanceRole,
|
|
861
|
+
completionGuard: a.completionGuard,
|
|
862
|
+
});
|
|
863
|
+
if (contractError) throw new AsyncStartValidationError(contractError);
|
|
864
|
+
}
|
|
865
|
+
const modelCandidates = externalRunner ? [] : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, a.modelProvider ?? ctx.currentModelProvider, {
|
|
866
|
+
scope: modelScopes,
|
|
867
|
+
primaryModelFromParent,
|
|
868
|
+
}).flatMap((candidate) => {
|
|
869
|
+
const resolved = applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined);
|
|
870
|
+
return resolved ? [resolved] : [];
|
|
871
|
+
});
|
|
872
|
+
if (!externalRunner) {
|
|
873
|
+
try {
|
|
874
|
+
for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
|
|
875
|
+
} catch (error) {
|
|
876
|
+
throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
|
|
877
|
+
}
|
|
878
|
+
}
|
|
825
879
|
return {
|
|
826
880
|
parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
|
|
827
881
|
permissionRules,
|
|
@@ -839,13 +893,11 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
839
893
|
cwd: stepCwd,
|
|
840
894
|
model,
|
|
841
895
|
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
896
|
+
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
842
897
|
launchResolvedExtensions,
|
|
843
|
-
modelCandidates: externalRunner ? undefined :
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}).map((candidate) =>
|
|
847
|
-
applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined),
|
|
848
|
-
),
|
|
898
|
+
modelCandidates: externalRunner ? undefined : modelCandidates,
|
|
899
|
+
...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
|
|
900
|
+
...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
|
|
849
901
|
tools: a.tools,
|
|
850
902
|
extensions: a.extensions,
|
|
851
903
|
subagentOnlyExtensions: a.subagentOnlyExtensions,
|
|
@@ -962,6 +1014,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
962
1014
|
acceptanceRole: agent.acceptanceRole,
|
|
963
1015
|
...(s.agentContract ?? params.agentContract ? { agentContract: s.agentContract ?? params.agentContract } : {}),
|
|
964
1016
|
...(s.gateOn ? { gateOn: s.gateOn } : {}),
|
|
1017
|
+
...(parallel.thinkingCeiling ? { thinkingCeiling: parallel.thinkingCeiling } : {}),
|
|
965
1018
|
};
|
|
966
1019
|
}
|
|
967
1020
|
const sequential = s as SequentialStep;
|
|
@@ -1096,6 +1149,7 @@ export function executeAsyncChain(
|
|
|
1096
1149
|
configToolTimeoutMs: params.configToolTimeoutMs,
|
|
1097
1150
|
toolTimeoutMsEnv: params.toolTimeoutMsEnv ?? toolTimeoutFromEnv(),
|
|
1098
1151
|
capabilityCeiling,
|
|
1152
|
+
thinkingCeiling: params.thinkingCeiling,
|
|
1099
1153
|
});
|
|
1100
1154
|
if ("error" in built) {
|
|
1101
1155
|
try {
|
|
@@ -1432,6 +1486,18 @@ export function executeAsyncSingle(
|
|
|
1432
1486
|
);
|
|
1433
1487
|
const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
|
|
1434
1488
|
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
|
|
1489
|
+
const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
|
|
1490
|
+
params.thinkingCeiling,
|
|
1491
|
+
agentConfig.maxThinking,
|
|
1492
|
+
decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
|
|
1493
|
+
);
|
|
1494
|
+
if (!externalRunner) {
|
|
1495
|
+
try {
|
|
1496
|
+
assertThinkingWithinCeiling({ model, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: agentConfig.name, runId: id });
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1435
1501
|
const toolBudgetInput = params.toolBudget ?? agentConfig.toolBudget ?? params.configToolBudget;
|
|
1436
1502
|
const resolvedToolBudget = validateToolBudgetConfig(toolBudgetInput, params.toolBudget ? "toolBudget" : agentConfig.toolBudget ? "agent.toolBudget" : "config.toolBudget");
|
|
1437
1503
|
if (resolvedToolBudget.error) return formatAsyncStartError("single", resolvedToolBudget.error);
|
|
@@ -1456,7 +1522,7 @@ export function executeAsyncSingle(
|
|
|
1456
1522
|
: undefined;
|
|
1457
1523
|
const modelCandidates = externalRunner
|
|
1458
1524
|
? []
|
|
1459
|
-
: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, {
|
|
1525
|
+
: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider, {
|
|
1460
1526
|
scope: modelScopes,
|
|
1461
1527
|
primaryModelFromParent: params.modelOverrideFromParent,
|
|
1462
1528
|
})
|
|
@@ -1464,6 +1530,13 @@ export function executeAsyncSingle(
|
|
|
1464
1530
|
const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
|
|
1465
1531
|
return resolved ? [resolved] : [];
|
|
1466
1532
|
});
|
|
1533
|
+
if (!externalRunner) {
|
|
1534
|
+
try {
|
|
1535
|
+
for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: agentConfig.name, runId: id });
|
|
1536
|
+
} catch (error) {
|
|
1537
|
+
return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1467
1540
|
const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(systemPrompt, params.turnBudget);
|
|
1468
1541
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
1469
1542
|
tools: agentConfig.tools,
|
|
@@ -1479,12 +1552,27 @@ export function executeAsyncSingle(
|
|
|
1479
1552
|
permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
|
|
1480
1553
|
});
|
|
1481
1554
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
1555
|
+
if (!externalRunner) {
|
|
1556
|
+
const contractTools = toolPlan.explicitToolAllowlist ? toolPlan.effectiveToolAllowlist : undefined;
|
|
1557
|
+
const contractError = validateImplementationToolContract({
|
|
1558
|
+
agent: agentConfig.name,
|
|
1559
|
+
task: taskText,
|
|
1560
|
+
tools: contractTools,
|
|
1561
|
+
mcpDirectTools: toolPlan.effectiveMcpTools,
|
|
1562
|
+
configuredExtensions: toolPlan.configuredExtensions,
|
|
1563
|
+
requestedTools: toolPlan.requestedBuiltinTools,
|
|
1564
|
+
acceptanceRole: agentConfig.acceptanceRole,
|
|
1565
|
+
completionGuard: agentConfig.completionGuard,
|
|
1566
|
+
});
|
|
1567
|
+
if (contractError) return formatAsyncStartError("single", contractError);
|
|
1568
|
+
}
|
|
1482
1569
|
const launchContractDigest = launchBindingDigest({
|
|
1483
1570
|
definitionDigest: agentDefinitionDigest(agentConfig),
|
|
1484
1571
|
task,
|
|
1485
1572
|
...(model ? { model } : {}),
|
|
1486
1573
|
modelCandidates,
|
|
1487
1574
|
...(resolveEffectiveThinking(model, effectiveThinking) ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
|
|
1575
|
+
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
1488
1576
|
systemPrompt: effectiveSystemPrompt,
|
|
1489
1577
|
systemPromptMode: agentConfig.systemPromptMode,
|
|
1490
1578
|
inheritProjectContext: agentConfig.inheritProjectContext,
|
|
@@ -1518,9 +1606,11 @@ export function executeAsyncSingle(
|
|
|
1518
1606
|
...(sessionFile ? { sessionFile } : {}),
|
|
1519
1607
|
cwd: runnerCwd,
|
|
1520
1608
|
...(model ? { model } : {}),
|
|
1609
|
+
...(recoveryAgentConfig.modelProvider ? { modelProvider: recoveryAgentConfig.modelProvider } : {}),
|
|
1521
1610
|
...(params.modelOverrideFromParent ? { modelOverrideFromParent: true } : {}),
|
|
1522
1611
|
...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
|
|
1523
1612
|
...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
|
|
1613
|
+
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
1524
1614
|
...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
|
|
1525
1615
|
...(recoveryAgentConfig.extensions ? { extensions: [...recoveryAgentConfig.extensions] } : {}),
|
|
1526
1616
|
...(recoveryAgentConfig.subagentOnlyExtensions ? { subagentOnlyExtensions: [...recoveryAgentConfig.subagentOnlyExtensions] } : {}),
|
|
@@ -1572,11 +1662,15 @@ export function executeAsyncSingle(
|
|
|
1572
1662
|
agent,
|
|
1573
1663
|
task: taskText,
|
|
1574
1664
|
...(agentConfig.runner ? { runner: agentConfig.runner } : {}),
|
|
1665
|
+
...(params.externalJobFollowUp ? { externalJobFollowUp: params.externalJobFollowUp } : {}),
|
|
1575
1666
|
...(params.context ? { context: params.context } : {}),
|
|
1576
1667
|
cwd: runnerCwd,
|
|
1577
1668
|
model,
|
|
1578
1669
|
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
1670
|
+
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
1579
1671
|
modelCandidates,
|
|
1672
|
+
...(params.modelOverrideFromParent ? { skipPrimaryModelVerification: true } : {}),
|
|
1673
|
+
...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
|
|
1580
1674
|
tools: agentConfig.tools,
|
|
1581
1675
|
extensions: agentConfig.extensions,
|
|
1582
1676
|
subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
|
|
@@ -1604,7 +1698,7 @@ export function executeAsyncSingle(
|
|
|
1604
1698
|
...(params.worktree === true ? { worktree: true } : {}),
|
|
1605
1699
|
},
|
|
1606
1700
|
],
|
|
1607
|
-
resultPath: params.parentWorkflowRunId !== undefined && params.revivalLease !== undefined
|
|
1701
|
+
resultPath: params.parentWorkflowRunId !== undefined && (params.revivalLease !== undefined || params.workflowAwaitAsync === true)
|
|
1608
1702
|
? workflowAwaitedAsyncResultPath(asyncDir)
|
|
1609
1703
|
: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : resultFilePath(DIRS.results, id),
|
|
1610
1704
|
cwd: runnerCwd,
|
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
type AsyncStartedEvent,
|
|
9
9
|
type ControlEvent,
|
|
10
10
|
type SteeringNotice,
|
|
11
|
+
type SubagentChildStatusEvent,
|
|
11
12
|
type SubagentState,
|
|
12
|
-
POLL_INTERVAL_MS,
|
|
13
13
|
DIRS,
|
|
14
|
+
SUBAGENT_CHILD_STATUS_EVENT,
|
|
14
15
|
SUBAGENT_CONTROL_EVENT,
|
|
15
16
|
SUBAGENT_CONTROL_INTERCOM_EVENT,
|
|
16
17
|
SUBAGENT_STEERING_NOTICE_EVENT,
|
|
18
|
+
WIDGET_ANIMATION_INTERVAL_MS,
|
|
17
19
|
} from "../../shared/types.ts";
|
|
18
20
|
import { readStatus, resolveWatchPath } from "../../shared/utils.ts";
|
|
19
21
|
import { normalizeParallelGroups } from "./parallel-groups.ts";
|
|
@@ -71,6 +73,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
71
73
|
const runningJobIds = new Set<string>();
|
|
72
74
|
let rootWatcher: fs.FSWatcher | undefined;
|
|
73
75
|
let nextLivenessAt = Date.now() + livenessIntervalMs;
|
|
76
|
+
let nextWidgetAnimationAt = Date.now() + WIDGET_ANIMATION_INTERVAL_MS;
|
|
74
77
|
const watch = options.watch ?? fs.watch;
|
|
75
78
|
const useNativeWatcher = () => shouldUseNativeFsWatch("async-job-tracker", options.platform);
|
|
76
79
|
const terminalStatus = (status: string) => status === "complete" || status === "failed" || status === "paused" || status === "stopped";
|
|
@@ -93,17 +96,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
93
96
|
}
|
|
94
97
|
};
|
|
95
98
|
const requestLastWidgetRender = () => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
if (ctx.hasUI) (ctx.ui as { requestRender?: () => void }).requestRender?.();
|
|
100
|
-
} catch (error) {
|
|
101
|
-
if (error instanceof Error && error.message.includes("extension ctx is stale")) {
|
|
102
|
-
state.lastUiContext = null;
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
99
|
+
if (options.widgetEnabled === false) return;
|
|
100
|
+
rerenderLastWidget();
|
|
107
101
|
};
|
|
108
102
|
const refreshWidget = (ctx: ExtensionContext) => rerenderWidget(ctx);
|
|
109
103
|
const restoredControlEventCursor = (asyncDir: string) => {
|
|
@@ -227,6 +221,28 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
227
221
|
return;
|
|
228
222
|
}
|
|
229
223
|
if (!parsed || typeof parsed !== "object") return;
|
|
224
|
+
if ((parsed as { type?: unknown }).type === "subagent.child-status") {
|
|
225
|
+
const event = parsed as Partial<SubagentChildStatusEvent>;
|
|
226
|
+
if (event.version !== 1 || typeof event.runId !== "string" || typeof event.childId !== "string" || (event.status !== "stopping" && event.status !== "stopped") || typeof event.ts !== "number") return;
|
|
227
|
+
pi.events.emit(SUBAGENT_CHILD_STATUS_EVENT, {
|
|
228
|
+
type: "subagent.child-status",
|
|
229
|
+
version: 1,
|
|
230
|
+
runId: event.runId,
|
|
231
|
+
childId: event.childId,
|
|
232
|
+
status: event.status,
|
|
233
|
+
ts: event.ts,
|
|
234
|
+
...(typeof event.reason === "string" ? { reason: event.reason } : {}),
|
|
235
|
+
source: event.source === "rpc" ? "rpc" : "async",
|
|
236
|
+
asyncDir: job.asyncDir,
|
|
237
|
+
...(typeof event.stepIndex === "number" ? { stepIndex: event.stepIndex } : {}),
|
|
238
|
+
...(typeof event.agent === "string" ? { agent: event.agent } : {}),
|
|
239
|
+
...(typeof event.childRunId === "string" ? { childRunId: event.childRunId } : {}),
|
|
240
|
+
...(typeof event.workflowKey === "string" ? { workflowKey: event.workflowKey } : {}),
|
|
241
|
+
...(typeof event.phase === "string" ? { phase: event.phase } : {}),
|
|
242
|
+
...(typeof event.label === "string" ? { label: event.label } : {}),
|
|
243
|
+
} satisfies SubagentChildStatusEvent);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
230
246
|
if ((parsed as { type?: unknown }).type === "subagent.steering.notice") {
|
|
231
247
|
const notice = parsed as Partial<SteeringNotice>;
|
|
232
248
|
if (typeof notice.requestId !== "string" || typeof notice.runId !== "string" || (notice.state !== "failed" && notice.state !== "partial" && notice.state !== "recovered") || typeof notice.message !== "string") return;
|
|
@@ -556,8 +572,11 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
556
572
|
}
|
|
557
573
|
if (widgetChanged) rerenderLastWidget();
|
|
558
574
|
}
|
|
559
|
-
if (runningJobIds.size > 0)
|
|
560
|
-
|
|
575
|
+
if (runningJobIds.size > 0 && now >= nextWidgetAnimationAt) {
|
|
576
|
+
nextWidgetAnimationAt = now + WIDGET_ANIMATION_INTERVAL_MS;
|
|
577
|
+
requestLastWidgetRender();
|
|
578
|
+
}
|
|
579
|
+
}, Math.min(WIDGET_ANIMATION_INTERVAL_MS, livenessIntervalMs));
|
|
561
580
|
state.poller.unref?.();
|
|
562
581
|
};
|
|
563
582
|
|
|
@@ -11,6 +11,7 @@ import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
|
11
11
|
import { resultFilePath, resultPayloadPathForIndexedRun } from "./result-files.ts";
|
|
12
12
|
import { canScanAsyncRunPrefix, MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH } from "./run-id-query.ts";
|
|
13
13
|
import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/parallel-handoff.ts";
|
|
14
|
+
import { intersectThinkingCeilings, parseThinkingLevel, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
|
|
14
15
|
|
|
15
16
|
export interface AsyncResumeParams {
|
|
16
17
|
id?: string;
|
|
@@ -43,9 +44,12 @@ export type AsyncResumeTarget = {
|
|
|
43
44
|
sessionFile?: string;
|
|
44
45
|
model?: string;
|
|
45
46
|
thinking?: string;
|
|
47
|
+
thinkingCeiling?: ThinkingLevel;
|
|
46
48
|
recoveryDescriptor?: SteeringRecoveryDescriptor;
|
|
47
49
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
48
50
|
launchContractDigest?: string;
|
|
51
|
+
runner?: NonNullable<AsyncStatus["steps"]>[number]["runner"];
|
|
52
|
+
externalJob?: NonNullable<AsyncStatus["steps"]>[number]["externalJob"];
|
|
49
53
|
};
|
|
50
54
|
|
|
51
55
|
interface AsyncResultFile {
|
|
@@ -265,6 +269,7 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
|
|
|
265
269
|
if (stepRecord.sessionFile !== undefined && typeof stepRecord.sessionFile !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionFile must be a string.`);
|
|
266
270
|
if (stepRecord.model !== undefined && typeof stepRecord.model !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].model must be a string.`);
|
|
267
271
|
if (stepRecord.thinking !== undefined && typeof stepRecord.thinking !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].thinking must be a string.`);
|
|
272
|
+
if (stepRecord.thinkingCeiling !== undefined) stepRecord.thinkingCeiling = parseThinkingLevel(stepRecord.thinkingCeiling, `async status '${source}' steps[${index}].thinkingCeiling`);
|
|
268
273
|
if (stepRecord.launchContractDigest !== undefined && typeof stepRecord.launchContractDigest !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].launchContractDigest must be a string.`);
|
|
269
274
|
if (stepRecord.capabilityCeiling !== undefined) stepRecord.capabilityCeiling = parseSubagentCapabilityCeiling(stepRecord.capabilityCeiling, `async status '${source}' steps[${index}].capabilityCeiling`);
|
|
270
275
|
});
|
|
@@ -318,7 +323,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
318
323
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
|
|
319
324
|
const parsed = value as Record<string, unknown>;
|
|
320
325
|
const allowedFields = new Set([
|
|
321
|
-
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelOverrideFromParent", "fallbackModels", "thinking", "tools", "extensions",
|
|
326
|
+
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "fallbackModels", "thinking", "thinkingCeiling", "tools", "extensions",
|
|
322
327
|
"subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
|
|
323
328
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
324
329
|
"artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
@@ -338,6 +343,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
338
343
|
throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
339
344
|
}
|
|
340
345
|
if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
|
|
346
|
+
if (parsed.thinkingCeiling !== undefined) parsed.thinkingCeiling = parseThinkingLevel(parsed.thinkingCeiling, `async recovery descriptor '${descriptorPath}' thinkingCeiling`);
|
|
341
347
|
if (parsed.agentContract !== undefined) {
|
|
342
348
|
if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
|
|
343
349
|
const contract = parsed.agentContract as Record<string, unknown>;
|
|
@@ -356,7 +362,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
356
362
|
if (item !== undefined && (!Array.isArray(item) || item.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must contain non-empty strings.`);
|
|
357
363
|
}
|
|
358
364
|
if (parsed.systemPrompt !== undefined && typeof parsed.systemPrompt !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPrompt must be a string.`);
|
|
359
|
-
for (const field of ["launchContractDigest", "sessionFile", "model", "thinking", "agentFilePath", "outputPath", "sessionDir", "artifactsDir"] as const) {
|
|
365
|
+
for (const field of ["launchContractDigest", "sessionFile", "model", "modelProvider", "thinking", "agentFilePath", "outputPath", "sessionDir", "artifactsDir"] as const) {
|
|
360
366
|
if (parsed[field] !== undefined && (typeof parsed[field] !== "string" || !(parsed[field] as string).trim())) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
|
|
361
367
|
}
|
|
362
368
|
if (parsed.completionGuard !== undefined && typeof parsed.completionGuard !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': completionGuard must be a boolean.`);
|
|
@@ -487,7 +493,10 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
487
493
|
model: selectedStep.model,
|
|
488
494
|
thinking: selectedStep.thinking,
|
|
489
495
|
launchContractDigest: selectedStep.launchContractDigest ?? result?.results?.[requestedIndex]?.launchContractDigest ?? result?.launchContractDigest ?? recoveryDescriptor?.launchContractDigest,
|
|
496
|
+
...(selectedStep.runner ? { runner: selectedStep.runner } : {}),
|
|
497
|
+
...(selectedStep.externalJob ? { externalJob: selectedStep.externalJob } : {}),
|
|
490
498
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
499
|
+
...(selectedStep.thinkingCeiling ? { thinkingCeiling: selectedStep.thinkingCeiling } : {}),
|
|
491
500
|
...(recoveryDescriptor ? { recoveryDescriptor } : {}),
|
|
492
501
|
};
|
|
493
502
|
}
|
|
@@ -515,7 +524,10 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
515
524
|
model: selected.step.model,
|
|
516
525
|
thinking: selected.step.thinking,
|
|
517
526
|
launchContractDigest: selected.step.launchContractDigest ?? result?.results?.[selected.index]?.launchContractDigest ?? result?.launchContractDigest ?? recoveryDescriptor?.launchContractDigest,
|
|
527
|
+
...(selected.step.runner ? { runner: selected.step.runner } : {}),
|
|
528
|
+
...(selected.step.externalJob ? { externalJob: selected.step.externalJob } : {}),
|
|
518
529
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
530
|
+
...(selected.step.thinkingCeiling ? { thinkingCeiling: selected.step.thinkingCeiling } : {}),
|
|
519
531
|
...(recoveryDescriptor ? { recoveryDescriptor } : {}),
|
|
520
532
|
};
|
|
521
533
|
}
|
|
@@ -537,6 +549,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
537
549
|
const resolvedSessionFile = sessionFile ? validateResumeSessionFile(runId, sessionFile) : undefined;
|
|
538
550
|
const stepModel = statusSteps[index]?.model ?? resultSteps[index]?.model ?? (stepCount === 1 ? result?.model : undefined);
|
|
539
551
|
const stepThinking = statusSteps[index]?.thinking ?? resultSteps[index]?.thinking ?? (stepCount === 1 ? result?.thinking : undefined);
|
|
552
|
+
const thinkingCeiling = statusSteps[index]?.thinkingCeiling ?? (stepCount === 1 ? recoveryDescriptor?.thinkingCeiling : undefined);
|
|
540
553
|
const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, statusSteps[index]?.capabilityCeiling, result?.capabilityCeiling, resultSteps[index]?.capabilityCeiling);
|
|
541
554
|
const managedWorktreeCwd = location.asyncDir
|
|
542
555
|
? resolveRetainedWorktreeCwd(parallelHandoffPath(location.asyncDir), runId, index)
|
|
@@ -556,7 +569,10 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
556
569
|
...(stepModel ? { model: stepModel } : {}),
|
|
557
570
|
...(stepThinking ? { thinking: stepThinking } : {}),
|
|
558
571
|
launchContractDigest: statusSteps[index]?.launchContractDigest ?? resultSteps[index]?.launchContractDigest ?? result?.launchContractDigest ?? recoveryDescriptor?.launchContractDigest,
|
|
572
|
+
...(statusSteps[index]?.runner ? { runner: statusSteps[index]!.runner } : {}),
|
|
573
|
+
...(statusSteps[index]?.externalJob ? { externalJob: statusSteps[index]!.externalJob } : {}),
|
|
559
574
|
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
575
|
+
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
560
576
|
...(recoveryDescriptor ? { recoveryDescriptor } : {}),
|
|
561
577
|
};
|
|
562
578
|
}
|
|
@@ -565,8 +581,10 @@ export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descr
|
|
|
565
581
|
return {
|
|
566
582
|
...agentConfig,
|
|
567
583
|
model: descriptor.model,
|
|
584
|
+
modelProvider: descriptor.modelProvider,
|
|
568
585
|
fallbackModels: descriptor.fallbackModels ? [...descriptor.fallbackModels] : undefined,
|
|
569
586
|
thinking: descriptor.thinking,
|
|
587
|
+
maxThinking: intersectThinkingCeilings(descriptor.thinkingCeiling, agentConfig.maxThinking),
|
|
570
588
|
tools: descriptor.tools ? [...descriptor.tools] : undefined,
|
|
571
589
|
extensions: descriptor.extensions ? [...descriptor.extensions] : undefined,
|
|
572
590
|
subagentOnlyExtensions: descriptor.subagentOnlyExtensions ? [...descriptor.subagentOnlyExtensions] : undefined,
|
|
@@ -15,14 +15,18 @@ import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal
|
|
|
15
15
|
import { ACTIVE_RUN_INDEX_DIR, DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS, activeRunMarkerAgeMs, isActiveAsyncState, readActiveRunIndex, releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
|
|
16
16
|
import { readRecentTerminalRunIndex, TERMINAL_RUN_INDEX_DIR } from "./terminal-run-index.ts";
|
|
17
17
|
import { canScanAsyncRunPrefix } from "./run-id-query.ts";
|
|
18
|
+
import { asyncStatusChildIdentity } from "../shared/child-identity.ts";
|
|
18
19
|
|
|
19
20
|
interface AsyncRunStepSummary {
|
|
20
21
|
index: number;
|
|
22
|
+
childId?: string;
|
|
21
23
|
agent: string;
|
|
22
24
|
context?: ContextMode;
|
|
23
25
|
label?: string;
|
|
24
26
|
description?: string;
|
|
25
27
|
phase?: string;
|
|
28
|
+
workflowKey?: string;
|
|
29
|
+
runId?: string;
|
|
26
30
|
outputName?: string;
|
|
27
31
|
structured?: boolean;
|
|
28
32
|
status: AsyncJobStep["status"];
|
|
@@ -251,11 +255,14 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
251
255
|
const stepLastActivityAt = step.lastActivityAt;
|
|
252
256
|
return {
|
|
253
257
|
index,
|
|
258
|
+
childId: asyncStatusChildIdentity(step, index),
|
|
254
259
|
agent: step.agent,
|
|
255
260
|
...(step.context ? { context: step.context } : {}),
|
|
256
261
|
...(step.label ? { label: step.label } : {}),
|
|
257
262
|
...(step.description ? { description: step.description } : {}),
|
|
258
263
|
...(step.phase ? { phase: step.phase } : {}),
|
|
264
|
+
...(step.workflowKey ? { workflowKey: step.workflowKey } : {}),
|
|
265
|
+
...(step.runId ? { runId: step.runId } : {}),
|
|
259
266
|
...(step.outputName ? { outputName: step.outputName } : {}),
|
|
260
267
|
...(step.structured ? { structured: step.structured } : {}),
|
|
261
268
|
status: step.status,
|
|
@@ -277,12 +284,15 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
277
284
|
...(step.skills ? { skills: step.skills } : {}),
|
|
278
285
|
...(step.model ? { model: step.model } : {}),
|
|
279
286
|
...(step.thinking ? { thinking: step.thinking } : {}),
|
|
287
|
+
...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
|
|
280
288
|
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
281
289
|
...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
|
|
282
290
|
...(step.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
|
|
283
291
|
...(step.error ? { error: step.error } : {}),
|
|
284
292
|
...(step.timedOut !== undefined ? { timedOut: step.timedOut } : {}),
|
|
285
293
|
...(step.stopped !== undefined ? { stopped: step.stopped } : {}),
|
|
294
|
+
...(step.stopRequested !== undefined ? { stopRequested: step.stopRequested } : {}),
|
|
295
|
+
...(step.stopRequestedAt !== undefined ? { stopRequestedAt: step.stopRequestedAt } : {}),
|
|
286
296
|
...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
|
|
287
297
|
...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
|
|
288
298
|
...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
|