pi-subagents 0.32.0 → 0.33.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 +30 -3
- package/README.md +147 -58
- package/install.mjs +2 -1
- package/package.json +3 -2
- package/skills/pi-subagents/SKILL.md +70 -14
- package/src/agents/agent-management.ts +177 -5
- package/src/agents/agent-memory.ts +254 -0
- package/src/agents/agent-serializer.ts +11 -0
- package/src/agents/agents.ts +142 -12
- package/src/agents/chain-serializer.ts +27 -2
- package/src/extension/doctor.ts +1 -9
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +65 -90
- package/src/extension/rpc.ts +369 -0
- package/src/extension/schemas.ts +52 -8
- package/src/extension/tool-description.ts +200 -0
- package/src/intercom/intercom-bridge.ts +21 -253
- package/src/intercom/native-supervisor-channel.ts +510 -0
- package/src/runs/background/async-execution.ts +51 -7
- package/src/runs/background/async-job-tracker.ts +12 -2
- package/src/runs/background/async-status.ts +27 -2
- package/src/runs/background/completion-batcher.ts +166 -0
- package/src/runs/background/control-channel.ts +106 -1
- package/src/runs/background/fleet-view.ts +515 -0
- package/src/runs/background/notify.ts +161 -44
- package/src/runs/background/result-watcher.ts +1 -2
- package/src/runs/background/run-id-resolver.ts +3 -2
- package/src/runs/background/run-status.ts +166 -6
- package/src/runs/background/scheduled-runs.ts +514 -0
- package/src/runs/background/subagent-runner.ts +409 -35
- package/src/runs/background/wait.ts +353 -0
- package/src/runs/foreground/chain-execution.ts +95 -21
- package/src/runs/foreground/execution.ts +150 -21
- package/src/runs/foreground/subagent-executor.ts +378 -64
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +167 -20
- package/src/runs/shared/model-scope.ts +128 -0
- package/src/runs/shared/nested-events.ts +31 -0
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/pi-args.ts +30 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +108 -2
- package/src/runs/shared/tool-budget.ts +74 -0
- package/src/runs/shared/turn-budget.ts +52 -0
- package/src/shared/artifacts.ts +1 -0
- package/src/shared/atomic-json.ts +15 -2
- package/src/shared/child-transcript.ts +212 -0
- package/src/shared/settings.ts +3 -1
- package/src/shared/types.ts +134 -19
- package/src/slash/prompt-workflows.ts +330 -0
- package/src/slash/slash-commands.ts +16 -2
- package/src/tui/render.ts +16 -8
- package/src/extension/companion-suggestions.ts +0 -359
|
@@ -16,8 +16,10 @@ import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveS
|
|
|
16
16
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
17
17
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
18
18
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
19
|
+
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
19
20
|
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, resolveChildCwd } from "../../shared/utils.ts";
|
|
20
21
|
import { buildModelCandidates, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
|
|
22
|
+
import type { ModelScopeConfig } from "../shared/model-scope.ts";
|
|
21
23
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
22
24
|
import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
|
|
23
25
|
import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
|
|
@@ -31,6 +33,8 @@ import {
|
|
|
31
33
|
type MaxOutputConfig,
|
|
32
34
|
type NestedRouteInfo,
|
|
33
35
|
type ResolvedControlConfig,
|
|
36
|
+
type ResolvedTurnBudget,
|
|
37
|
+
type ResolvedToolBudget,
|
|
34
38
|
type SubagentRunMode,
|
|
35
39
|
ASYNC_DIR,
|
|
36
40
|
RESULTS_DIR,
|
|
@@ -41,6 +45,8 @@ import {
|
|
|
41
45
|
resolveChildMaxSubagentDepth,
|
|
42
46
|
} from "../../shared/types.ts";
|
|
43
47
|
import { nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
|
|
48
|
+
import { initialTurnBudgetState } from "../shared/turn-budget.ts";
|
|
49
|
+
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
44
50
|
import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
|
|
45
51
|
|
|
46
52
|
const require = createRequire(import.meta.url);
|
|
@@ -100,6 +106,8 @@ interface AsyncExecutionContext {
|
|
|
100
106
|
parentSessionId?: string;
|
|
101
107
|
currentModelProvider?: string;
|
|
102
108
|
currentModel?: ParentModel;
|
|
109
|
+
/** Optional model-scope enforcement resolved from subagent settings. */
|
|
110
|
+
modelScope?: ModelScopeConfig;
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
interface AsyncChainParams {
|
|
@@ -131,6 +139,9 @@ interface AsyncChainParams {
|
|
|
131
139
|
nestedRoute?: NestedRouteInfo;
|
|
132
140
|
acceptance?: AcceptanceInput;
|
|
133
141
|
timeoutMs?: number;
|
|
142
|
+
turnBudget?: ResolvedTurnBudget;
|
|
143
|
+
toolBudget?: ResolvedToolBudget;
|
|
144
|
+
configToolBudget?: ResolvedToolBudget;
|
|
134
145
|
/** Global cap on simultaneously-running subagent tasks within the async run. */
|
|
135
146
|
globalConcurrencyLimit?: number;
|
|
136
147
|
}
|
|
@@ -164,6 +175,9 @@ interface AsyncSingleParams {
|
|
|
164
175
|
nestedRoute?: NestedRouteInfo;
|
|
165
176
|
acceptance?: AcceptanceInput;
|
|
166
177
|
timeoutMs?: number;
|
|
178
|
+
turnBudget?: ResolvedTurnBudget;
|
|
179
|
+
toolBudget?: ResolvedToolBudget;
|
|
180
|
+
configToolBudget?: ResolvedToolBudget;
|
|
167
181
|
}
|
|
168
182
|
|
|
169
183
|
interface AsyncExecutionResult {
|
|
@@ -191,6 +205,8 @@ export interface AsyncRunnerStepBuildParams {
|
|
|
191
205
|
asyncDir: string;
|
|
192
206
|
outputBaseDir?: string;
|
|
193
207
|
validateOutputBindings?: boolean;
|
|
208
|
+
toolBudget?: ResolvedToolBudget;
|
|
209
|
+
configToolBudget?: ResolvedToolBudget;
|
|
194
210
|
}
|
|
195
211
|
|
|
196
212
|
export type AsyncRunnerStepBuildResult =
|
|
@@ -208,8 +224,8 @@ export function formatAsyncStartedMessage(headline: string): string {
|
|
|
208
224
|
headline,
|
|
209
225
|
"",
|
|
210
226
|
"The async run is detached. Do not run sleep timers or polling loops just to wait for it.",
|
|
211
|
-
"If you have independent work, continue that work.
|
|
212
|
-
"Use subagent({ action: \"status\", id: \"...\" }) when you need
|
|
227
|
+
"If you have independent work, continue that work. When you have nothing left to do until the async result arrives, call wait() — it blocks until the run finishes and delivers the completion here. Only if you are certain you will get another turn (an interactive session where the user will prompt you again) can you instead stop and let Pi wake you; inside a skill that must run to completion, or in a non-interactive run, there is no next turn, so use wait().",
|
|
228
|
+
"Use subagent({ action: \"status\", id: \"...\" }) when you need a one-shot status/result or to inspect a blocked/stale run. To block until completion, prefer wait(). Do not poll in a loop just to wait.",
|
|
213
229
|
].join("\n");
|
|
214
230
|
}
|
|
215
231
|
|
|
@@ -404,6 +420,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
404
420
|
};
|
|
405
421
|
const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number) => {
|
|
406
422
|
const a = agents.find((x) => x.name === s.agent)!;
|
|
423
|
+
const toolBudgetInput = s.toolBudget ?? params.toolBudget ?? a.toolBudget ?? params.configToolBudget;
|
|
424
|
+
const resolvedToolBudget = validateToolBudgetConfig(toolBudgetInput, s.toolBudget ? "toolBudget" : a.toolBudget ? "agent.toolBudget" : "config.toolBudget");
|
|
425
|
+
if (resolvedToolBudget.error) throw new AsyncStartValidationError(resolvedToolBudget.error);
|
|
407
426
|
const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
|
|
408
427
|
const instructionCwd = behaviorCwd ?? stepCwd;
|
|
409
428
|
const behavior = suppressProgressForReadOnlyTask(resolvedBehavior ?? resolveStepBehavior(a, buildStepOverrides(s), chainSkills), s.task, originalTask);
|
|
@@ -416,6 +435,10 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
416
435
|
const injection = buildSkillInjection(resolvedSkills);
|
|
417
436
|
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
|
|
418
437
|
}
|
|
438
|
+
const memoryInjection = buildAgentMemoryInjection(a, stepCwd);
|
|
439
|
+
if (memoryInjection) {
|
|
440
|
+
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
|
|
441
|
+
}
|
|
419
442
|
|
|
420
443
|
const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false);
|
|
421
444
|
const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
|
|
@@ -431,7 +454,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
431
454
|
const task = injectSingleOutputInstruction(`${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`, outputPath);
|
|
432
455
|
|
|
433
456
|
const requestedModel = behavior.model ?? a.model;
|
|
434
|
-
const primaryModel = resolveSubagentModelOverride(requestedModel, ctx.currentModel, availableModels, ctx.currentModelProvider);
|
|
457
|
+
const primaryModel = resolveSubagentModelOverride(requestedModel, ctx.currentModel, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope, source: behavior.model ? "explicit" : "inherited" });
|
|
435
458
|
const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
|
|
436
459
|
const effectiveThinking = thinkingOverride ?? a.thinking;
|
|
437
460
|
const model = applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
|
|
@@ -446,7 +469,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
446
469
|
cwd: stepCwd,
|
|
447
470
|
model,
|
|
448
471
|
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
449
|
-
modelCandidates: buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
|
|
472
|
+
modelCandidates: buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope }).map((candidate) =>
|
|
450
473
|
applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined),
|
|
451
474
|
),
|
|
452
475
|
tools: a.tools,
|
|
@@ -473,6 +496,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
473
496
|
}),
|
|
474
497
|
...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
|
|
475
498
|
...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output")) } : {}),
|
|
499
|
+
...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
|
|
476
500
|
};
|
|
477
501
|
};
|
|
478
502
|
|
|
@@ -638,6 +662,8 @@ export function executeAsyncChain(
|
|
|
638
662
|
maxSubagentDepth,
|
|
639
663
|
worktreeBaseDir,
|
|
640
664
|
asyncDir,
|
|
665
|
+
toolBudget: params.toolBudget,
|
|
666
|
+
configToolBudget: params.configToolBudget,
|
|
641
667
|
});
|
|
642
668
|
if ("error" in built) {
|
|
643
669
|
try {
|
|
@@ -649,6 +675,7 @@ export function executeAsyncChain(
|
|
|
649
675
|
}
|
|
650
676
|
const { steps, runnerCwd, workflowGraph, eventChain } = built;
|
|
651
677
|
const deadlineAt = params.timeoutMs !== undefined ? Date.now() + params.timeoutMs : undefined;
|
|
678
|
+
const initialTurnBudget = params.turnBudget ? initialTurnBudgetState(params.turnBudget) : undefined;
|
|
652
679
|
let childTargetIndex = 0;
|
|
653
680
|
const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
|
|
654
681
|
if (!("parallel" in step) && step.importAsyncRoot) {
|
|
@@ -687,6 +714,8 @@ export function executeAsyncChain(
|
|
|
687
714
|
worktreeSetupHookTimeoutMs,
|
|
688
715
|
worktreeBaseDir,
|
|
689
716
|
controlConfig,
|
|
717
|
+
turnBudget: params.turnBudget,
|
|
718
|
+
toolBudget: params.toolBudget,
|
|
690
719
|
controlIntercomTarget,
|
|
691
720
|
childIntercomTargets,
|
|
692
721
|
resultMode,
|
|
@@ -767,6 +796,7 @@ export function executeAsyncChain(
|
|
|
767
796
|
chainStepCount: eventChain.length,
|
|
768
797
|
parallelGroups,
|
|
769
798
|
...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
|
|
799
|
+
...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
|
|
770
800
|
startedAt: now,
|
|
771
801
|
lastUpdate: now,
|
|
772
802
|
},
|
|
@@ -797,6 +827,7 @@ export function executeAsyncChain(
|
|
|
797
827
|
cwd: runnerCwd,
|
|
798
828
|
asyncDir,
|
|
799
829
|
...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
|
|
830
|
+
...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
|
|
800
831
|
nestedRoute,
|
|
801
832
|
});
|
|
802
833
|
}
|
|
@@ -809,7 +840,7 @@ export function executeAsyncChain(
|
|
|
809
840
|
|
|
810
841
|
return {
|
|
811
842
|
content: [{ type: "text", text: formatAsyncStartedMessage(`Async ${resultMode}: ${chainDesc} [${id}]`) }],
|
|
812
|
-
details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph, ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}) },
|
|
843
|
+
details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph, ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
|
|
813
844
|
};
|
|
814
845
|
}
|
|
815
846
|
|
|
@@ -851,6 +882,10 @@ export function executeAsyncSingle(
|
|
|
851
882
|
const injection = buildSkillInjection(resolvedSkills);
|
|
852
883
|
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
|
|
853
884
|
}
|
|
885
|
+
const memoryInjection = buildAgentMemoryInjection(agentConfig, runnerCwd);
|
|
886
|
+
if (memoryInjection) {
|
|
887
|
+
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
|
|
888
|
+
}
|
|
854
889
|
|
|
855
890
|
const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
|
|
856
891
|
const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
|
|
@@ -883,7 +918,11 @@ export function executeAsyncSingle(
|
|
|
883
918
|
);
|
|
884
919
|
const effectiveThinking = params.thinkingOverride ?? agentConfig.thinking;
|
|
885
920
|
const model = applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
|
|
921
|
+
const toolBudgetInput = params.toolBudget ?? agentConfig.toolBudget ?? params.configToolBudget;
|
|
922
|
+
const resolvedToolBudget = validateToolBudgetConfig(toolBudgetInput, params.toolBudget ? "toolBudget" : agentConfig.toolBudget ? "agent.toolBudget" : "config.toolBudget");
|
|
923
|
+
if (resolvedToolBudget.error) return formatAsyncStartError("single", resolvedToolBudget.error);
|
|
886
924
|
const deadlineAt = params.timeoutMs !== undefined ? Date.now() + params.timeoutMs : undefined;
|
|
925
|
+
const initialTurnBudget = params.turnBudget ? initialTurnBudgetState(params.turnBudget) : undefined;
|
|
887
926
|
let spawnResult: { pid?: number; error?: string } = {};
|
|
888
927
|
try {
|
|
889
928
|
spawnResult = spawnRunner(
|
|
@@ -897,7 +936,7 @@ export function executeAsyncSingle(
|
|
|
897
936
|
cwd: runnerCwd,
|
|
898
937
|
model,
|
|
899
938
|
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
900
|
-
modelCandidates: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
|
|
939
|
+
modelCandidates: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, { scope: ctx.modelScope }).map((candidate) =>
|
|
901
940
|
applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined),
|
|
902
941
|
),
|
|
903
942
|
tools: agentConfig.tools,
|
|
@@ -921,6 +960,7 @@ export function executeAsyncSingle(
|
|
|
921
960
|
mode: "single",
|
|
922
961
|
async: true,
|
|
923
962
|
}),
|
|
963
|
+
...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
|
|
924
964
|
},
|
|
925
965
|
],
|
|
926
966
|
resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
|
|
@@ -941,6 +981,8 @@ export function executeAsyncSingle(
|
|
|
941
981
|
controlConfig,
|
|
942
982
|
timeoutMs: params.timeoutMs,
|
|
943
983
|
deadlineAt,
|
|
984
|
+
turnBudget: params.turnBudget,
|
|
985
|
+
toolBudget: params.toolBudget,
|
|
944
986
|
controlIntercomTarget,
|
|
945
987
|
childIntercomTargets: childIntercomTarget ? [childIntercomTarget(agent, 0)] : undefined,
|
|
946
988
|
resultMode: "single",
|
|
@@ -991,6 +1033,7 @@ export function executeAsyncSingle(
|
|
|
991
1033
|
agents: [agent],
|
|
992
1034
|
chainStepCount: 1,
|
|
993
1035
|
...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
|
|
1036
|
+
...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
|
|
994
1037
|
startedAt: now,
|
|
995
1038
|
lastUpdate: now,
|
|
996
1039
|
},
|
|
@@ -1010,12 +1053,13 @@ export function executeAsyncSingle(
|
|
|
1010
1053
|
cwd: runnerCwd,
|
|
1011
1054
|
asyncDir,
|
|
1012
1055
|
...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
|
|
1056
|
+
...(initialTurnBudget ? { turnBudget: initialTurnBudget } : {}),
|
|
1013
1057
|
nestedRoute,
|
|
1014
1058
|
});
|
|
1015
1059
|
}
|
|
1016
1060
|
|
|
1017
1061
|
return {
|
|
1018
1062
|
content: [{ type: "text", text: formatAsyncStartedMessage(`Async: ${agent} [${id}]`) }],
|
|
1019
|
-
details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}) },
|
|
1063
|
+
details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
|
|
1020
1064
|
};
|
|
1021
1065
|
}
|
|
@@ -89,6 +89,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
89
89
|
timeoutMs: run.timeoutMs,
|
|
90
90
|
deadlineAt: run.deadlineAt,
|
|
91
91
|
timedOut: run.timedOut,
|
|
92
|
+
turnBudget: run.turnBudget,
|
|
93
|
+
turnBudgetExceeded: run.turnBudgetExceeded,
|
|
94
|
+
wrapUpRequested: run.wrapUpRequested,
|
|
92
95
|
sessionDir: run.sessionDir,
|
|
93
96
|
outputFile: run.outputFile,
|
|
94
97
|
totalTokens: run.totalTokens,
|
|
@@ -305,6 +308,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
305
308
|
job.timeoutMs = status.timeoutMs ?? job.timeoutMs;
|
|
306
309
|
job.deadlineAt = status.deadlineAt ?? job.deadlineAt;
|
|
307
310
|
job.timedOut = status.timedOut ?? job.timedOut;
|
|
311
|
+
job.turnBudget = status.turnBudget ?? job.turnBudget;
|
|
312
|
+
job.turnBudgetExceeded = status.turnBudgetExceeded ?? job.turnBudgetExceeded;
|
|
313
|
+
job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
|
|
308
314
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
309
315
|
if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
310
316
|
scheduleCleanup(job.asyncId);
|
|
@@ -337,6 +343,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
337
343
|
const handleStarted = (data: unknown) => {
|
|
338
344
|
const info = data as AsyncStartedEvent;
|
|
339
345
|
if (!info.id) return;
|
|
346
|
+
if (typeof state.currentSessionId === "string" && info.sessionId !== state.currentSessionId) return;
|
|
340
347
|
const now = Date.now();
|
|
341
348
|
const asyncDir = info.asyncDir ?? path.join(asyncDirRoot, info.id);
|
|
342
349
|
const rawAgents = info.agents?.length ? info.agents : info.chain && info.chain.length > 0 ? info.chain : info.agent ? [info.agent] : undefined;
|
|
@@ -364,6 +371,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
364
371
|
updatedAt: now,
|
|
365
372
|
timeoutMs: info.timeoutMs,
|
|
366
373
|
deadlineAt: info.deadlineAt,
|
|
374
|
+
turnBudget: info.turnBudget,
|
|
367
375
|
controlEventCursor: 0,
|
|
368
376
|
});
|
|
369
377
|
ensurePoller();
|
|
@@ -373,7 +381,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
373
381
|
};
|
|
374
382
|
|
|
375
383
|
const handleComplete = (data: unknown) => {
|
|
376
|
-
const result = data as { id?: string; success?: boolean; asyncDir?: string };
|
|
384
|
+
const result = data as { id?: string; success?: boolean; asyncDir?: string; sessionId?: string };
|
|
385
|
+
if (typeof state.currentSessionId === "string" && result.sessionId !== state.currentSessionId) return;
|
|
377
386
|
const asyncId = result.id;
|
|
378
387
|
if (!asyncId) return;
|
|
379
388
|
const job = state.asyncJobs.get(asyncId);
|
|
@@ -412,9 +421,10 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
412
421
|
|
|
413
422
|
const restoreActiveJobs = (ctx?: ExtensionContext) => {
|
|
414
423
|
if (ctx?.hasUI) state.lastUiContext = ctx;
|
|
424
|
+
if (!state.currentSessionId) return;
|
|
415
425
|
let runs: AsyncRunSummary[];
|
|
416
426
|
try {
|
|
417
|
-
runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], resultsDir, kill: options.kill, now: options.now });
|
|
427
|
+
runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], sessionId: state.currentSessionId, resultsDir, kill: options.kill, now: options.now });
|
|
418
428
|
} catch (error) {
|
|
419
429
|
console.error(`Failed to restore active async jobs from '${asyncDirRoot}':`, error);
|
|
420
430
|
return;
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
|
|
4
4
|
import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
|
|
5
|
-
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type NestedRunSummary, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
|
|
5
|
+
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type NestedRunSummary, type SubagentRunMode, type TokenUsage, type TurnBudgetState } from "../../shared/types.ts";
|
|
6
6
|
import { readStatus } from "../../shared/utils.ts";
|
|
7
7
|
import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
8
8
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
@@ -27,6 +27,8 @@ interface AsyncRunStepSummary {
|
|
|
27
27
|
recentOutput?: string[];
|
|
28
28
|
turnCount?: number;
|
|
29
29
|
toolCount?: number;
|
|
30
|
+
steerCount?: number;
|
|
31
|
+
lastSteerAt?: number;
|
|
30
32
|
durationMs?: number;
|
|
31
33
|
tokens?: TokenUsage;
|
|
32
34
|
totalCost?: CostSummary;
|
|
@@ -36,6 +38,9 @@ interface AsyncRunStepSummary {
|
|
|
36
38
|
attemptedModels?: string[];
|
|
37
39
|
error?: string;
|
|
38
40
|
timedOut?: boolean;
|
|
41
|
+
turnBudget?: TurnBudgetState;
|
|
42
|
+
turnBudgetExceeded?: boolean;
|
|
43
|
+
wrapUpRequested?: boolean;
|
|
39
44
|
children?: NestedRunSummary[];
|
|
40
45
|
}
|
|
41
46
|
|
|
@@ -52,6 +57,8 @@ export interface AsyncRunSummary {
|
|
|
52
57
|
currentPath?: string;
|
|
53
58
|
turnCount?: number;
|
|
54
59
|
toolCount?: number;
|
|
60
|
+
steerCount?: number;
|
|
61
|
+
lastSteerAt?: number;
|
|
55
62
|
mode: SubagentRunMode;
|
|
56
63
|
cwd?: string;
|
|
57
64
|
startedAt: number;
|
|
@@ -60,6 +67,9 @@ export interface AsyncRunSummary {
|
|
|
60
67
|
timeoutMs?: number;
|
|
61
68
|
deadlineAt?: number;
|
|
62
69
|
timedOut?: boolean;
|
|
70
|
+
turnBudget?: TurnBudgetState;
|
|
71
|
+
turnBudgetExceeded?: boolean;
|
|
72
|
+
wrapUpRequested?: boolean;
|
|
63
73
|
currentStep?: number;
|
|
64
74
|
chainStepCount?: number;
|
|
65
75
|
pendingAppends?: number;
|
|
@@ -168,6 +178,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
168
178
|
...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
|
|
169
179
|
...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
|
|
170
180
|
...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
|
|
181
|
+
...(step.steerCount !== undefined ? { steerCount: step.steerCount } : {}),
|
|
182
|
+
...(step.lastSteerAt !== undefined ? { lastSteerAt: step.lastSteerAt } : {}),
|
|
171
183
|
...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
|
|
172
184
|
...(step.tokens ? { tokens: step.tokens } : {}),
|
|
173
185
|
...(step.totalCost ? { totalCost: step.totalCost } : {}),
|
|
@@ -177,6 +189,9 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
177
189
|
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
178
190
|
...(step.error ? { error: step.error } : {}),
|
|
179
191
|
...(step.timedOut !== undefined ? { timedOut: step.timedOut } : {}),
|
|
192
|
+
...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
|
|
193
|
+
...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
|
|
194
|
+
...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
|
|
180
195
|
...(step.children?.length ? { children: step.children } : {}),
|
|
181
196
|
};
|
|
182
197
|
});
|
|
@@ -194,6 +209,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
194
209
|
currentPath: status.currentPath,
|
|
195
210
|
turnCount: status.turnCount,
|
|
196
211
|
toolCount: status.toolCount,
|
|
212
|
+
steerCount: status.steerCount,
|
|
213
|
+
lastSteerAt: status.lastSteerAt,
|
|
197
214
|
mode: status.mode,
|
|
198
215
|
cwd: status.cwd,
|
|
199
216
|
startedAt: status.startedAt,
|
|
@@ -202,6 +219,9 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
202
219
|
...(status.timeoutMs !== undefined ? { timeoutMs: status.timeoutMs } : {}),
|
|
203
220
|
...(status.deadlineAt !== undefined ? { deadlineAt: status.deadlineAt } : {}),
|
|
204
221
|
...(status.timedOut !== undefined ? { timedOut: status.timedOut } : {}),
|
|
222
|
+
...(status.turnBudget ? { turnBudget: status.turnBudget } : {}),
|
|
223
|
+
...(status.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: status.turnBudgetExceeded } : {}),
|
|
224
|
+
...(status.wrapUpRequested !== undefined ? { wrapUpRequested: status.wrapUpRequested } : {}),
|
|
205
225
|
currentStep: status.currentStep,
|
|
206
226
|
...(status.chainStepCount !== undefined ? { chainStepCount: status.chainStepCount } : {}),
|
|
207
227
|
...(status.pendingAppends !== undefined ? { pendingAppends: status.pendingAppends } : {}),
|
|
@@ -288,13 +308,18 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
288
308
|
return options.limit !== undefined ? sorted.slice(0, options.limit) : sorted;
|
|
289
309
|
}
|
|
290
310
|
|
|
291
|
-
function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number; currentPath?: string; turnCount?: number; toolCount?: number }): string | undefined {
|
|
311
|
+
function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number; currentPath?: string; turnCount?: number; toolCount?: number; steerCount?: number; lastSteerAt?: number; turnBudget?: TurnBudgetState; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean }): string | undefined {
|
|
292
312
|
const facts: string[] = [];
|
|
293
313
|
if (input.currentTool && input.currentToolStartedAt !== undefined) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
|
|
294
314
|
else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
|
|
295
315
|
if (input.currentPath) facts.push(shortenPath(input.currentPath));
|
|
296
316
|
if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
|
|
317
|
+
if (input.turnBudgetExceeded && input.turnBudget) facts.push(`turn budget exceeded ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
|
|
318
|
+
else if (input.wrapUpRequested && input.turnBudget) facts.push(`wrap-up requested ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}`);
|
|
319
|
+
else if (input.turnBudget) facts.push(`turn budget ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
|
|
297
320
|
if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
|
|
321
|
+
if (input.steerCount !== undefined) facts.push(`${input.steerCount} steers`);
|
|
322
|
+
if (typeof input.lastSteerAt === "number" && Number.isFinite(input.lastSteerAt)) facts.push(`last steer ${new Date(input.lastSteerAt).toISOString()}`);
|
|
298
323
|
const activity = formatActivityLabel(input.lastActivityAt, input.activityState);
|
|
299
324
|
return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
|
|
300
325
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart completion batching with straggler handling.
|
|
3
|
+
*
|
|
4
|
+
* Holds successful async-completion notifications briefly so sibling jobs that
|
|
5
|
+
* finish within a short window arrive as a single grouped message. A hard
|
|
6
|
+
* max-wait cap (measured from the first item in a group) prevents holding
|
|
7
|
+
* notifications indefinitely. After a group is emitted, late-finishing
|
|
8
|
+
* siblings that arrive within the straggler window join a shorter "straggler"
|
|
9
|
+
* group with reduced debounce and max-wait timers.
|
|
10
|
+
*
|
|
11
|
+
* Failure and attention signals bypass this batcher entirely. Callers must
|
|
12
|
+
* flush() held items and emit those signals immediately so failures and
|
|
13
|
+
* needs-attention notices are never delayed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { CompletionBatchConfig } from "../../shared/types.ts";
|
|
17
|
+
|
|
18
|
+
export type { CompletionBatchConfig };
|
|
19
|
+
|
|
20
|
+
export interface ResolvedCompletionBatchConfig {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
debounceMs: number;
|
|
23
|
+
maxWaitMs: number;
|
|
24
|
+
stragglerDebounceMs: number;
|
|
25
|
+
stragglerMaxWaitMs: number;
|
|
26
|
+
stragglerWindowMs: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_COMPLETION_BATCH_CONFIG: ResolvedCompletionBatchConfig = {
|
|
30
|
+
enabled: true,
|
|
31
|
+
debounceMs: 150,
|
|
32
|
+
maxWaitMs: 1000,
|
|
33
|
+
stragglerDebounceMs: 75,
|
|
34
|
+
stragglerMaxWaitMs: 400,
|
|
35
|
+
stragglerWindowMs: 2000,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function parsePositiveInt(value: unknown): number | undefined {
|
|
39
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1) return undefined;
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resolveCompletionBatchConfig(
|
|
44
|
+
globalConfig?: CompletionBatchConfig,
|
|
45
|
+
override?: CompletionBatchConfig,
|
|
46
|
+
): ResolvedCompletionBatchConfig {
|
|
47
|
+
const enabled = typeof override?.enabled === "boolean"
|
|
48
|
+
? override.enabled
|
|
49
|
+
: typeof globalConfig?.enabled === "boolean"
|
|
50
|
+
? globalConfig.enabled
|
|
51
|
+
: DEFAULT_COMPLETION_BATCH_CONFIG.enabled;
|
|
52
|
+
return {
|
|
53
|
+
enabled,
|
|
54
|
+
debounceMs: parsePositiveInt(override?.debounceMs) ?? parsePositiveInt(globalConfig?.debounceMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.debounceMs,
|
|
55
|
+
maxWaitMs: parsePositiveInt(override?.maxWaitMs) ?? parsePositiveInt(globalConfig?.maxWaitMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.maxWaitMs,
|
|
56
|
+
stragglerDebounceMs: parsePositiveInt(override?.stragglerDebounceMs) ?? parsePositiveInt(globalConfig?.stragglerDebounceMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerDebounceMs,
|
|
57
|
+
stragglerMaxWaitMs: parsePositiveInt(override?.stragglerMaxWaitMs) ?? parsePositiveInt(globalConfig?.stragglerMaxWaitMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerMaxWaitMs,
|
|
58
|
+
stragglerWindowMs: parsePositiveInt(override?.stragglerWindowMs) ?? parsePositiveInt(globalConfig?.stragglerWindowMs) ?? DEFAULT_COMPLETION_BATCH_CONFIG.stragglerWindowMs,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type TimerHandle = unknown;
|
|
63
|
+
|
|
64
|
+
interface TimerApi {
|
|
65
|
+
setTimeout(handler: () => void, delayMs: number): TimerHandle;
|
|
66
|
+
clearTimeout(handle: TimerHandle): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const defaultTimers: TimerApi = {
|
|
70
|
+
setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
|
|
71
|
+
clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function unrefHandle(handle: TimerHandle): void {
|
|
75
|
+
if (handle && typeof handle === "object" && "unref" in handle && typeof (handle as { unref: unknown }).unref === "function") {
|
|
76
|
+
(handle as { unref: () => void }).unref();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CompletionBatcherOptions<T> {
|
|
81
|
+
config: ResolvedCompletionBatchConfig;
|
|
82
|
+
emit: (items: T[]) => void;
|
|
83
|
+
timers?: TimerApi;
|
|
84
|
+
now?: () => number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface CompletionBatcher<T> {
|
|
88
|
+
/** Add a batchable item. Emits immediately when batching is disabled. */
|
|
89
|
+
push(item: T): void;
|
|
90
|
+
/** Emit any held items immediately as a single group. */
|
|
91
|
+
flush(): void;
|
|
92
|
+
/** Clear timers without emitting. */
|
|
93
|
+
dispose(): void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a completion batcher. The batcher is single-use per registration: it
|
|
98
|
+
* holds at most one open group. `flush` forces emission; `dispose` tears down
|
|
99
|
+
* timers for reload/shutdown without emitting.
|
|
100
|
+
*/
|
|
101
|
+
export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>): CompletionBatcher<T> {
|
|
102
|
+
const timers = options.timers ?? defaultTimers;
|
|
103
|
+
const now = options.now ?? Date.now;
|
|
104
|
+
const config = options.config;
|
|
105
|
+
|
|
106
|
+
if (!config.enabled) {
|
|
107
|
+
return {
|
|
108
|
+
push(item: T) {
|
|
109
|
+
options.emit([item]);
|
|
110
|
+
},
|
|
111
|
+
flush() {},
|
|
112
|
+
dispose() {},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let pending: T[] = [];
|
|
117
|
+
let debounceTimer: TimerHandle | null = null;
|
|
118
|
+
let maxWaitTimer: TimerHandle | null = null;
|
|
119
|
+
let straggler = false;
|
|
120
|
+
let lastEmitAt: number | null = null;
|
|
121
|
+
|
|
122
|
+
const clearTimers = () => {
|
|
123
|
+
if (debounceTimer) {
|
|
124
|
+
timers.clearTimeout(debounceTimer);
|
|
125
|
+
debounceTimer = null;
|
|
126
|
+
}
|
|
127
|
+
if (maxWaitTimer) {
|
|
128
|
+
timers.clearTimeout(maxWaitTimer);
|
|
129
|
+
maxWaitTimer = null;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const emitGroup = () => {
|
|
134
|
+
clearTimers();
|
|
135
|
+
if (pending.length === 0) return;
|
|
136
|
+
const items = pending;
|
|
137
|
+
pending = [];
|
|
138
|
+
lastEmitAt = now();
|
|
139
|
+
options.emit(items);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
push(item: T) {
|
|
144
|
+
if (pending.length === 0) {
|
|
145
|
+
straggler = lastEmitAt !== null && (now() - lastEmitAt) < config.stragglerWindowMs;
|
|
146
|
+
}
|
|
147
|
+
pending.push(item);
|
|
148
|
+
|
|
149
|
+
if (debounceTimer) timers.clearTimeout(debounceTimer);
|
|
150
|
+
const debounceDelay = straggler ? config.stragglerDebounceMs : config.debounceMs;
|
|
151
|
+
debounceTimer = timers.setTimeout(emitGroup, debounceDelay);
|
|
152
|
+
unrefHandle(debounceTimer);
|
|
153
|
+
|
|
154
|
+
if (!maxWaitTimer) {
|
|
155
|
+
const maxWaitDelay = straggler ? config.stragglerMaxWaitMs : config.maxWaitMs;
|
|
156
|
+
maxWaitTimer = timers.setTimeout(emitGroup, maxWaitDelay);
|
|
157
|
+
unrefHandle(maxWaitTimer);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
flush: emitGroup,
|
|
161
|
+
dispose() {
|
|
162
|
+
clearTimers();
|
|
163
|
+
pending = [];
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|