pi-subagents 0.35.0 → 0.36.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 +59 -0
- package/README.md +132 -24
- package/agents/advisor.md +73 -0
- package/package.json +8 -12
- package/skills/pi-subagents/SKILL.md +22 -9
- package/src/agents/agents.ts +22 -5
- package/src/api/delegation.ts +125 -0
- package/src/extension/config.ts +7 -1
- package/src/extension/index.ts +50 -38
- package/src/extension/rpc.ts +27 -2
- package/src/extension/schemas.ts +22 -2
- package/src/extension/tool-description.ts +2 -2
- package/src/intercom/intercom-bridge.ts +1 -1
- package/src/intercom/native-supervisor-channel.ts +45 -6
- package/src/intercom/result-intercom.ts +7 -0
- package/src/runs/background/async-execution.ts +34 -4
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +27 -5
- package/src/runs/background/async-status.ts +76 -3
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/completion-batcher.ts +6 -4
- package/src/runs/background/completion-dedupe.ts +2 -11
- package/src/runs/background/fleet-view.ts +9 -4
- package/src/runs/background/notify.ts +132 -120
- package/src/runs/background/result-watcher.ts +138 -78
- package/src/runs/background/run-status.ts +3 -1
- package/src/runs/background/subagent-runner.ts +225 -43
- package/src/runs/background/subagent-wait.ts +130 -4
- package/src/runs/background/wait-tool.ts +2 -2
- package/src/runs/foreground/chain-execution.ts +176 -111
- package/src/runs/foreground/execution.ts +90 -36
- package/src/runs/foreground/foreground-control.ts +90 -0
- package/src/runs/foreground/subagent-executor.ts +394 -163
- package/src/runs/shared/acceptance.ts +55 -13
- package/src/runs/shared/agent-contract.ts +38 -0
- package/src/runs/shared/child-protocol.ts +1 -1
- package/src/runs/shared/completion-guard.ts +36 -5
- package/src/runs/shared/context-mode.ts +44 -0
- package/src/runs/shared/dynamic-fanout.ts +4 -4
- package/src/runs/shared/long-running-guard.ts +4 -0
- package/src/runs/shared/nested-events.ts +27 -2
- package/src/runs/shared/parallel-handoff.ts +154 -0
- package/src/runs/shared/parallel-utils.ts +6 -0
- package/src/runs/shared/pi-args.ts +23 -14
- package/src/runs/shared/run-history.ts +90 -5
- package/src/runs/shared/structured-output.ts +112 -7
- package/src/runs/shared/subagent-control.ts +4 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
- package/src/runs/shared/task-intent.ts +10 -5
- package/src/runs/shared/tool-availability.ts +3 -1
- package/src/runs/shared/tool-budget.ts +11 -5
- package/src/runs/shared/turn-budget.ts +2 -1
- package/src/runs/shared/worktree.ts +63 -14
- package/src/shared/accessible-dir.ts +25 -0
- package/src/shared/artifacts.ts +37 -7
- package/src/shared/atomic-json.ts +14 -42
- package/src/shared/child-transcript.ts +52 -0
- package/src/shared/file-system-retry.ts +47 -0
- package/src/shared/settings.ts +9 -1
- package/src/shared/types.ts +211 -22
- package/src/slash/delegation-adapters.ts +152 -5
- package/src/slash/delegation-json.ts +108 -0
- package/src/slash/delegation-request.ts +182 -36
- package/src/slash/prompt-template-bridge.ts +222 -37
- package/src/slash/selector.ts +147 -0
- package/src/slash/slash-commands.ts +14 -5
- package/src/slash/slash-live-state.ts +2 -2
- package/src/slash/subagents-admin.ts +42 -42
- package/src/tui/fleet-status.ts +362 -0
- package/src/tui/fleet-transcript.ts +472 -0
- package/src/tui/fleet.ts +318 -59
- package/src/tui/render.ts +25 -15
- package/src/watchdog/change-signature.ts +105 -12
- package/src/watchdog/review.ts +7 -2
- package/src/watchdog/runtime.ts +5 -3
- package/src/slash/subagents-editor.ts +0 -86
|
@@ -447,6 +447,43 @@ function requestMatchesContext(request: SupervisorRequest, state: Pick<SubagentS
|
|
|
447
447
|
return Boolean(currentSessionId && request.orchestratorSessionId === currentSessionId);
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
+
function rememberedForegroundChild(request: SupervisorRequest, state: SubagentState) {
|
|
451
|
+
const run = state.foregroundRuns?.get(request.runId);
|
|
452
|
+
const child = run?.children.find((candidate) => candidate.index === request.childIndex && candidate.agent === request.agent)
|
|
453
|
+
?? run?.children[request.childIndex];
|
|
454
|
+
return run && child ? { run, child } : undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function markForegroundSupervisorAttention(request: SupervisorRequest, state: SubagentState): void {
|
|
458
|
+
const remembered = rememberedForegroundChild(request, state);
|
|
459
|
+
if (!remembered || remembered.child.status !== "detached") return;
|
|
460
|
+
const updatedAt = Date.now();
|
|
461
|
+
remembered.run.updatedAt = updatedAt;
|
|
462
|
+
remembered.child.activityState = "needs_attention";
|
|
463
|
+
remembered.child.lastActivityAt = request.createdAt;
|
|
464
|
+
remembered.child.currentTool = "contact_supervisor";
|
|
465
|
+
remembered.child.currentToolStartedAt = request.createdAt;
|
|
466
|
+
remembered.child.updatedAt = updatedAt;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function clearForegroundSupervisorAttention(request: SupervisorRequest, pending: Map<string, PendingSupervisorRequest>, state: SubagentState): void {
|
|
470
|
+
if ([...pending.values()].some((candidate) =>
|
|
471
|
+
candidate.expectsReply
|
|
472
|
+
&& candidate.runId === request.runId
|
|
473
|
+
&& candidate.agent === request.agent
|
|
474
|
+
&& candidate.childIndex === request.childIndex
|
|
475
|
+
)) return;
|
|
476
|
+
const remembered = rememberedForegroundChild(request, state);
|
|
477
|
+
if (!remembered || remembered.child.status !== "detached" || remembered.child.currentTool !== "contact_supervisor") return;
|
|
478
|
+
const updatedAt = Date.now();
|
|
479
|
+
remembered.run.updatedAt = updatedAt;
|
|
480
|
+
remembered.child.activityState = undefined;
|
|
481
|
+
remembered.child.lastActivityAt = updatedAt;
|
|
482
|
+
remembered.child.currentTool = undefined;
|
|
483
|
+
remembered.child.currentToolStartedAt = undefined;
|
|
484
|
+
remembered.child.updatedAt = updatedAt;
|
|
485
|
+
}
|
|
486
|
+
|
|
450
487
|
function removeRequestFile(file: string): void {
|
|
451
488
|
try {
|
|
452
489
|
fs.rmSync(file, { force: true });
|
|
@@ -465,10 +502,8 @@ function requestExpiresAt(request: SupervisorRequest, now: number): number {
|
|
|
465
502
|
|
|
466
503
|
function requestRunInactive(request: SupervisorRequest, state: SubagentState): boolean {
|
|
467
504
|
if (state.foregroundControls.has(request.runId)) return false;
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
?? foregroundRun?.children[request.childIndex];
|
|
471
|
-
if (foregroundChild) return foregroundChild.status !== "detached";
|
|
505
|
+
const foreground = rememberedForegroundChild(request, state);
|
|
506
|
+
if (foreground) return foreground.child.status !== "detached";
|
|
472
507
|
|
|
473
508
|
const asyncJob = state.asyncJobs.get(request.runId);
|
|
474
509
|
if (!asyncJob) return false;
|
|
@@ -580,6 +615,7 @@ function buildParentIntercomTool(pending: Map<string, PendingSupervisorRequest>,
|
|
|
580
615
|
const request = resolvePendingRequest(pending, input);
|
|
581
616
|
writeReply(request, input.message ?? "");
|
|
582
617
|
pending.delete(request.id);
|
|
618
|
+
clearForegroundSupervisorAttention(request, pending, state);
|
|
583
619
|
return { content: [{ type: "text", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } };
|
|
584
620
|
}
|
|
585
621
|
if (input.action === "send" || input.action === "ask") {
|
|
@@ -629,7 +665,10 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
629
665
|
continue;
|
|
630
666
|
}
|
|
631
667
|
seenFiles.add(file);
|
|
632
|
-
if (request.expectsReply)
|
|
668
|
+
if (request.expectsReply) {
|
|
669
|
+
pending.set(request.id, request);
|
|
670
|
+
markForegroundSupervisorAttention(request, state);
|
|
671
|
+
}
|
|
633
672
|
else {
|
|
634
673
|
removeRequestFile(request.requestFile);
|
|
635
674
|
}
|
|
@@ -645,7 +684,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
645
684
|
agent: request.agent,
|
|
646
685
|
childIndex: request.childIndex,
|
|
647
686
|
},
|
|
648
|
-
});
|
|
687
|
+
}, { triggerTurn: true });
|
|
649
688
|
if (request.expectsReply) {
|
|
650
689
|
(pi as { events?: IntercomEventBus }).events?.emit(INTERCOM_DETACH_REQUEST_EVENT, {
|
|
651
690
|
requestId: request.id,
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type IntercomEventBus,
|
|
6
6
|
type NestedRunSummary,
|
|
7
7
|
type PublicNestedRunSummary,
|
|
8
|
+
type ParallelHandoffReference,
|
|
8
9
|
type SingleResult,
|
|
9
10
|
type SubagentResultIntercomChild,
|
|
10
11
|
type SubagentResultIntercomPayload,
|
|
@@ -179,6 +180,7 @@ interface GroupedResultIntercomMessageInput {
|
|
|
179
180
|
asyncId?: string;
|
|
180
181
|
asyncDir?: string;
|
|
181
182
|
chainSteps?: number;
|
|
183
|
+
parallelHandoff?: ParallelHandoffReference;
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
function asyncResumeGuidance(input: {
|
|
@@ -207,6 +209,7 @@ function formatSubagentResultIntercomMessage(input: {
|
|
|
207
209
|
asyncId?: string;
|
|
208
210
|
asyncDir?: string;
|
|
209
211
|
chainSteps?: number;
|
|
212
|
+
parallelHandoff?: ParallelHandoffReference;
|
|
210
213
|
}): string {
|
|
211
214
|
const counts = countStatuses(input.children);
|
|
212
215
|
const lines: string[] = [
|
|
@@ -222,6 +225,7 @@ function formatSubagentResultIntercomMessage(input: {
|
|
|
222
225
|
}
|
|
223
226
|
if (input.asyncId) lines.push(`Async id: ${input.asyncId}`);
|
|
224
227
|
if (input.asyncDir) lines.push(`Async dir: ${input.asyncDir}`);
|
|
228
|
+
if (input.parallelHandoff) lines.push(`Parallel handoff: ${input.parallelHandoff.path}`);
|
|
225
229
|
const resumeGuidance = asyncResumeGuidance(input);
|
|
226
230
|
if (resumeGuidance) lines.push(resumeGuidance);
|
|
227
231
|
if (input.children.some((child) => child.intercomTarget)) {
|
|
@@ -266,6 +270,7 @@ export function buildSubagentResultIntercomPayload(input: GroupedResultIntercomM
|
|
|
266
270
|
...(input.asyncId ? { asyncId: input.asyncId } : {}),
|
|
267
271
|
...(input.asyncDir ? { asyncDir: input.asyncDir } : {}),
|
|
268
272
|
...(typeof input.chainSteps === "number" ? { chainSteps: input.chainSteps } : {}),
|
|
273
|
+
...(input.parallelHandoff ? { parallelHandoff: input.parallelHandoff } : {}),
|
|
269
274
|
...(firstChild?.agent ? { agent: firstChild.agent } : {}),
|
|
270
275
|
...(firstChild?.index !== undefined ? { index: firstChild.index } : {}),
|
|
271
276
|
...(firstChild?.artifactPath ? { artifactPath: firstChild.artifactPath } : {}),
|
|
@@ -352,6 +357,8 @@ export function formatSubagentResultReceipt(input: {
|
|
|
352
357
|
`Children: ${formatStatusCounts(counts)}`,
|
|
353
358
|
];
|
|
354
359
|
|
|
360
|
+
if (input.payload.parallelHandoff) lines.push(`Parallel handoff: ${input.payload.parallelHandoff.path}`);
|
|
361
|
+
|
|
355
362
|
const artifacts = input.payload.children.filter((child) => typeof child.artifactPath === "string");
|
|
356
363
|
if (artifacts.length > 0) {
|
|
357
364
|
lines.push("Artifacts:");
|
|
@@ -15,6 +15,7 @@ import { applyThinkingSuffix } from "../shared/pi-args.ts";
|
|
|
15
15
|
import { injectOutputPathSystemPrompt, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
16
16
|
import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
17
17
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
18
|
+
import type { ContextMode } from "../shared/context-mode.ts";
|
|
18
19
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
19
20
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
20
21
|
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
@@ -29,8 +30,10 @@ import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
|
|
|
29
30
|
import { resolveEffectiveAcceptance } from "../shared/acceptance.ts";
|
|
30
31
|
import {
|
|
31
32
|
type AcceptanceInput,
|
|
33
|
+
type AgentContract,
|
|
32
34
|
type ArtifactConfig,
|
|
33
35
|
type Details,
|
|
36
|
+
type JsonSchemaObject,
|
|
34
37
|
type MaxOutputConfig,
|
|
35
38
|
type NestedRouteInfo,
|
|
36
39
|
type ResolvedControlConfig,
|
|
@@ -131,9 +134,11 @@ interface AsyncChainParams {
|
|
|
131
134
|
artifactConfig: ArtifactConfig;
|
|
132
135
|
shareEnabled: boolean;
|
|
133
136
|
sessionRoot?: string;
|
|
137
|
+
agentContract?: AgentContract;
|
|
134
138
|
chainSkills?: string[];
|
|
135
139
|
sessionFilesByFlatIndex?: (string | undefined)[];
|
|
136
140
|
thinkingOverridesByFlatIndex?: (AgentConfig["thinking"] | undefined)[];
|
|
141
|
+
contextForAgent?: (agentName: string) => ContextMode;
|
|
137
142
|
progressDir?: string;
|
|
138
143
|
dynamicFanoutMaxItems?: number;
|
|
139
144
|
maxSubagentDepth: number;
|
|
@@ -170,10 +175,13 @@ interface AsyncSingleParams {
|
|
|
170
175
|
sessionDir?: string;
|
|
171
176
|
sessionFile?: string;
|
|
172
177
|
revivalLease?: SessionLeaseRequest;
|
|
178
|
+
context?: ContextMode;
|
|
173
179
|
skills?: string[];
|
|
174
180
|
output?: string | boolean;
|
|
175
181
|
outputMode?: "inline" | "file-only";
|
|
176
182
|
outputBaseDir?: string;
|
|
183
|
+
agentContract?: AgentContract;
|
|
184
|
+
structuredOutputSchema?: JsonSchemaObject;
|
|
177
185
|
modelOverride?: string;
|
|
178
186
|
thinkingOverride?: AgentConfig["thinking"];
|
|
179
187
|
availableModels?: AvailableModelInfo[];
|
|
@@ -212,7 +220,9 @@ export interface AsyncRunnerStepBuildParams {
|
|
|
212
220
|
chainSkills?: string[];
|
|
213
221
|
sessionFilesByFlatIndex?: (string | undefined)[];
|
|
214
222
|
thinkingOverridesByFlatIndex?: (AgentConfig["thinking"] | undefined)[];
|
|
223
|
+
contextForAgent?: (agentName: string) => ContextMode;
|
|
215
224
|
progressDir?: string;
|
|
225
|
+
agentContract?: AgentContract;
|
|
216
226
|
dynamicFanoutMaxItems?: number;
|
|
217
227
|
maxSubagentDepth: number;
|
|
218
228
|
waitToolEnabled?: boolean;
|
|
@@ -624,10 +634,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
624
634
|
const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
|
|
625
635
|
const effectiveThinking = thinkingOverride ?? a.thinking;
|
|
626
636
|
const model = applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
|
|
637
|
+
const agentContract = s.agentContract ?? params.agentContract;
|
|
627
638
|
return {
|
|
628
639
|
parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
|
|
629
640
|
agent: s.agent,
|
|
630
641
|
task,
|
|
642
|
+
...(params.contextForAgent ? { context: params.contextForAgent(s.agent) } : {}),
|
|
643
|
+
...(agentContract ? { agentContract } : {}),
|
|
631
644
|
phase: s.phase,
|
|
632
645
|
label: s.label,
|
|
633
646
|
outputName: s.as,
|
|
@@ -662,9 +675,11 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
662
675
|
mode: resultMode,
|
|
663
676
|
async: true,
|
|
664
677
|
dynamic: false,
|
|
678
|
+
agentContract,
|
|
665
679
|
}),
|
|
666
680
|
acceptanceInput: s.acceptance,
|
|
667
681
|
acceptanceRole: a.acceptanceRole,
|
|
682
|
+
...(s.gateOn ? { gateOn: s.gateOn } : {}),
|
|
668
683
|
...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
|
|
669
684
|
...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output")) } : {}),
|
|
670
685
|
...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
|
|
@@ -707,7 +722,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
707
722
|
}
|
|
708
723
|
}
|
|
709
724
|
const staticStep = nextFlatStep();
|
|
710
|
-
return buildSeqStep(t, staticStep.sessionFile, behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex], staticStep.index, { stepIndex, taskIndex });
|
|
725
|
+
return buildSeqStep({ ...t, agentContract: t.agentContract ?? s.agentContract, gateOn: t.gateOn ?? s.gateOn }, staticStep.sessionFile, behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex], staticStep.index, { stepIndex, taskIndex });
|
|
711
726
|
}),
|
|
712
727
|
concurrency: s.concurrency,
|
|
713
728
|
failFast: s.failFast,
|
|
@@ -724,7 +739,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
724
739
|
}
|
|
725
740
|
const maxItems = s.expand.maxItems ?? params.dynamicFanoutMaxItems ?? 0;
|
|
726
741
|
const dynamicFlatSteps = Array.from({ length: maxItems }, () => nextFlatStep());
|
|
727
|
-
const parallel = buildSeqStep(s.parallel as SequentialStep, undefined, undefined, progressPrecreated, behavior, undefined, { stepIndex });
|
|
742
|
+
const parallel = buildSeqStep({ ...(s.parallel as SequentialStep), agentContract: s.parallel.agentContract ?? s.agentContract, gateOn: s.parallel.gateOn ?? s.gateOn }, undefined, undefined, progressPrecreated, behavior, undefined, { stepIndex });
|
|
728
743
|
return {
|
|
729
744
|
expand: s.expand,
|
|
730
745
|
parallel,
|
|
@@ -743,9 +758,12 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
743
758
|
mode: resultMode,
|
|
744
759
|
async: true,
|
|
745
760
|
dynamicGroup: true,
|
|
761
|
+
agentContract: s.agentContract ?? params.agentContract,
|
|
746
762
|
}),
|
|
747
763
|
acceptanceInput: s.acceptance,
|
|
748
764
|
acceptanceRole: agent.acceptanceRole,
|
|
765
|
+
...(s.agentContract ?? params.agentContract ? { agentContract: s.agentContract ?? params.agentContract } : {}),
|
|
766
|
+
...(s.gateOn ? { gateOn: s.gateOn } : {}),
|
|
749
767
|
};
|
|
750
768
|
}
|
|
751
769
|
const staticStep = nextFlatStep();
|
|
@@ -844,7 +862,9 @@ export function executeAsyncChain(
|
|
|
844
862
|
chainSkills: params.chainSkills,
|
|
845
863
|
sessionFilesByFlatIndex,
|
|
846
864
|
thinkingOverridesByFlatIndex,
|
|
865
|
+
contextForAgent: params.contextForAgent,
|
|
847
866
|
progressDir: params.progressDir ?? (artifactsDir ? path.join(artifactsDir, "progress", id) : resultMode === "parallel" ? path.join(asyncDir, "progress") : undefined),
|
|
867
|
+
agentContract: params.agentContract,
|
|
848
868
|
outputBaseDir: artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined,
|
|
849
869
|
dynamicFanoutMaxItems: params.dynamicFanoutMaxItems,
|
|
850
870
|
maxSubagentDepth,
|
|
@@ -1126,6 +1146,9 @@ export function executeAsyncSingle(
|
|
|
1126
1146
|
if (timeoutMs !== undefined && timeoutMs <= 0) return formatAsyncStartError("single", "The source run's absolute deadline expired before recovery could launch.");
|
|
1127
1147
|
const initialTurnBudget = params.turnBudget ? initialTurnBudgetState(params.turnBudget) : undefined;
|
|
1128
1148
|
const resolvedSessionDir = params.sessionDir ?? (sessionRoot ? path.join(sessionRoot, `async-${id}`) : undefined);
|
|
1149
|
+
const structuredOutput = params.structuredOutputSchema
|
|
1150
|
+
? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"))
|
|
1151
|
+
: undefined;
|
|
1129
1152
|
const resolvedAcceptance = resolveEffectiveAcceptance({
|
|
1130
1153
|
explicit: params.acceptance,
|
|
1131
1154
|
agentName: agent,
|
|
@@ -1133,10 +1156,12 @@ export function executeAsyncSingle(
|
|
|
1133
1156
|
task,
|
|
1134
1157
|
mode: "single",
|
|
1135
1158
|
async: true,
|
|
1159
|
+
agentContract: params.agentContract,
|
|
1136
1160
|
});
|
|
1137
1161
|
const recoveryDescriptor: SteeringRecoveryDescriptor = {
|
|
1138
1162
|
version: 1,
|
|
1139
1163
|
sourceRunId: id,
|
|
1164
|
+
...(params.agentContract ? { agentContract: params.agentContract } : {}),
|
|
1140
1165
|
agent,
|
|
1141
1166
|
...(sessionFile ? { sessionFile } : {}),
|
|
1142
1167
|
cwd: runnerCwd,
|
|
@@ -1158,7 +1183,8 @@ export function executeAsyncSingle(
|
|
|
1158
1183
|
...(agentConfig.memory ? { memory: { ...agentConfig.memory } } : {}),
|
|
1159
1184
|
...(outputPath ? { outputPath } : {}),
|
|
1160
1185
|
outputMode,
|
|
1161
|
-
...(
|
|
1186
|
+
...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
|
|
1187
|
+
...(params.acceptance !== undefined ? { acceptance: params.acceptance } : {}),
|
|
1162
1188
|
...(controlConfig ? { controlConfig } : {}),
|
|
1163
1189
|
...(deadlineAt !== undefined ? { absoluteDeadlineAt: deadlineAt } : {}),
|
|
1164
1190
|
...(params.turnBudget ? { initialTurnBudget: params.turnBudget } : {}),
|
|
@@ -1185,6 +1211,7 @@ export function executeAsyncSingle(
|
|
|
1185
1211
|
parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
|
|
1186
1212
|
agent,
|
|
1187
1213
|
task: taskWithOutputInstruction,
|
|
1214
|
+
...(params.context ? { context: params.context } : {}),
|
|
1188
1215
|
cwd: runnerCwd,
|
|
1189
1216
|
model,
|
|
1190
1217
|
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
@@ -1206,7 +1233,10 @@ export function executeAsyncSingle(
|
|
|
1206
1233
|
sessionFile,
|
|
1207
1234
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
|
|
1208
1235
|
waitToolEnabled: params.waitToolEnabled,
|
|
1236
|
+
...(params.agentContract ? { agentContract: params.agentContract } : {}),
|
|
1209
1237
|
effectiveAcceptance: resolvedAcceptance,
|
|
1238
|
+
...(structuredOutput ? { structuredOutput } : {}),
|
|
1239
|
+
...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
|
|
1210
1240
|
...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
|
|
1211
1241
|
},
|
|
1212
1242
|
],
|
|
@@ -1309,6 +1339,6 @@ export function executeAsyncSingle(
|
|
|
1309
1339
|
|
|
1310
1340
|
return {
|
|
1311
1341
|
content: [{ type: "text", text: formatAsyncStartedMessage(`Async: ${agent} [${id}]`, ctx.interactive === true) }],
|
|
1312
|
-
details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
|
|
1342
|
+
details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, ...(params.context ? { context: params.context } : {}), ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}), ...(params.turnBudget ? { turnBudget: params.turnBudget } : {}), ...(params.toolBudget ? { toolBudget: params.toolBudget } : {}) },
|
|
1313
1343
|
};
|
|
1314
1344
|
}
|
|
@@ -91,6 +91,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
91
91
|
toolCount: run.toolCount,
|
|
92
92
|
steering: run.steering,
|
|
93
93
|
mode: run.mode,
|
|
94
|
+
context: run.context,
|
|
95
|
+
cwd: run.cwd,
|
|
94
96
|
agents: visibleSteps.map((step) => step.agent),
|
|
95
97
|
currentStep: run.currentStep,
|
|
96
98
|
chainStepCount: run.chainStepCount,
|
|
@@ -397,10 +399,12 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
397
399
|
state.asyncJobs.set(info.id, {
|
|
398
400
|
asyncId: info.id,
|
|
399
401
|
asyncDir,
|
|
402
|
+
...(typeof info.cwd === "string" ? { cwd: path.resolve(info.cwd) } : {}),
|
|
400
403
|
status: "queued",
|
|
401
404
|
pid: typeof info.pid === "number" ? info.pid : undefined,
|
|
402
405
|
...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
|
|
403
406
|
mode: info.mode ?? (info.chain ? "chain" : "single"),
|
|
407
|
+
description: info.goal ?? info.task,
|
|
404
408
|
agents,
|
|
405
409
|
chainStepCount: info.chainStepCount,
|
|
406
410
|
parallelGroups: validParallelGroups,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
|
|
3
|
+
import { ASYNC_DIR, RESULTS_DIR, type AcceptanceInput, type AsyncStatus, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
|
|
4
4
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
5
5
|
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
6
6
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
@@ -250,6 +250,21 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): AcceptanceInput | undefined {
|
|
254
|
+
if (value && typeof value === "object" && !Array.isArray(value) && ("explicit" in value || "inferredReason" in value)) {
|
|
255
|
+
const { explicit, inferredReason: _inferredReason, ...publicAcceptance } = value as Record<string, unknown>;
|
|
256
|
+
if (explicit === false) return undefined;
|
|
257
|
+
if (publicAcceptance.level === "reviewed") {
|
|
258
|
+
publicAcceptance.level = "verified";
|
|
259
|
+
delete publicAcceptance.review;
|
|
260
|
+
}
|
|
261
|
+
value = publicAcceptance;
|
|
262
|
+
}
|
|
263
|
+
const errors = validateAcceptanceInput(value, "recoveryDescriptor.acceptance");
|
|
264
|
+
if (errors.length) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${errors.join(" ")}`);
|
|
265
|
+
return value as AcceptanceInput;
|
|
266
|
+
}
|
|
267
|
+
|
|
253
268
|
export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): SteeringRecoveryDescriptor | undefined {
|
|
254
269
|
if (!asyncDir) return undefined;
|
|
255
270
|
const descriptorPath = path.join(asyncDir, "recovery-descriptor.json");
|
|
@@ -263,9 +278,9 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
263
278
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
|
|
264
279
|
const parsed = value as Record<string, unknown>;
|
|
265
280
|
const allowedFields = new Set([
|
|
266
|
-
"version", "sourceRunId", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
|
|
281
|
+
"version", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
|
|
267
282
|
"subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
|
|
268
|
-
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "acceptance", "sessionDir", "artifactConfig",
|
|
283
|
+
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
269
284
|
"artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share",
|
|
270
285
|
]);
|
|
271
286
|
for (const field of Object.keys(parsed)) {
|
|
@@ -276,6 +291,11 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
276
291
|
if (typeof parsed[field] !== "string" || !(parsed[field] as string).trim()) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
|
|
277
292
|
}
|
|
278
293
|
if (parsed.version !== 1) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': version must be 1.`);
|
|
294
|
+
if (parsed.agentContract !== undefined) {
|
|
295
|
+
if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
|
|
296
|
+
const contract = parsed.agentContract as Record<string, unknown>;
|
|
297
|
+
if (contract.version !== 1 || Object.keys(contract).some((key) => key !== "version")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be { version: 1 }.`);
|
|
298
|
+
}
|
|
279
299
|
if (parsed.systemPromptMode !== "append" && parsed.systemPromptMode !== "replace") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPromptMode is invalid.`);
|
|
280
300
|
if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
|
|
281
301
|
for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
|
|
@@ -291,6 +311,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
291
311
|
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.`);
|
|
292
312
|
}
|
|
293
313
|
if (parsed.completionGuard !== undefined && typeof parsed.completionGuard !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': completionGuard must be a boolean.`);
|
|
314
|
+
if (parsed.structuredOutputSchema !== undefined && (!parsed.structuredOutputSchema || typeof parsed.structuredOutputSchema !== "object" || Array.isArray(parsed.structuredOutputSchema))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': structuredOutputSchema must be an object.`);
|
|
294
315
|
if (parsed.memory !== undefined) {
|
|
295
316
|
if (!parsed.memory || typeof parsed.memory !== "object" || Array.isArray(parsed.memory)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': memory must be an object.`);
|
|
296
317
|
const memory = parsed.memory as Record<string, unknown>;
|
|
@@ -335,8 +356,9 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
335
356
|
if (!Array.isArray(control.notifyChannels) || control.notifyChannels.some((item) => item !== "event" && item !== "async" && item !== "intercom")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.notifyChannels is invalid.`);
|
|
336
357
|
}
|
|
337
358
|
if (parsed.acceptance !== undefined) {
|
|
338
|
-
const
|
|
339
|
-
if (
|
|
359
|
+
const acceptance = normalizeRecoveryAcceptance(parsed.acceptance, descriptorPath);
|
|
360
|
+
if (acceptance === undefined) delete parsed.acceptance;
|
|
361
|
+
else parsed.acceptance = acceptance;
|
|
340
362
|
}
|
|
341
363
|
return parsed as unknown as SteeringRecoveryDescriptor;
|
|
342
364
|
}
|
|
@@ -7,11 +7,13 @@ 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";
|
|
9
9
|
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
10
|
+
import { contextModeLabel, summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
|
|
10
11
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
11
12
|
|
|
12
13
|
interface AsyncRunStepSummary {
|
|
13
14
|
index: number;
|
|
14
15
|
agent: string;
|
|
16
|
+
context?: ContextMode;
|
|
15
17
|
label?: string;
|
|
16
18
|
phase?: string;
|
|
17
19
|
outputName?: string;
|
|
@@ -35,12 +37,19 @@ interface AsyncRunStepSummary {
|
|
|
35
37
|
model?: string;
|
|
36
38
|
thinking?: string;
|
|
37
39
|
attemptedModels?: string[];
|
|
40
|
+
sessionFile?: string;
|
|
41
|
+
transcriptPath?: string;
|
|
38
42
|
error?: string;
|
|
39
43
|
timedOut?: boolean;
|
|
40
44
|
stopped?: boolean;
|
|
41
45
|
turnBudget?: TurnBudgetState;
|
|
42
46
|
turnBudgetExceeded?: boolean;
|
|
43
47
|
wrapUpRequested?: boolean;
|
|
48
|
+
acceptance?: AsyncJobStep["acceptance"];
|
|
49
|
+
agentContract?: AsyncJobStep["agentContract"];
|
|
50
|
+
execution?: AsyncJobStep["execution"];
|
|
51
|
+
review?: AsyncJobStep["review"];
|
|
52
|
+
effects?: AsyncJobStep["effects"];
|
|
44
53
|
children?: NestedRunSummary[];
|
|
45
54
|
}
|
|
46
55
|
|
|
@@ -59,6 +68,7 @@ export interface AsyncRunSummary {
|
|
|
59
68
|
toolCount?: number;
|
|
60
69
|
steering?: SteeringStatus;
|
|
61
70
|
mode: SubagentRunMode;
|
|
71
|
+
context?: ContextSummary;
|
|
62
72
|
cwd?: string;
|
|
63
73
|
startedAt: number;
|
|
64
74
|
lastUpdate?: number;
|
|
@@ -92,6 +102,7 @@ interface AsyncRunListOptions {
|
|
|
92
102
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
93
103
|
now?: () => number;
|
|
94
104
|
reconcile?: boolean;
|
|
105
|
+
runId?: string;
|
|
95
106
|
}
|
|
96
107
|
|
|
97
108
|
function getErrorMessage(error: unknown): string {
|
|
@@ -117,6 +128,45 @@ function isAsyncRunDir(root: string, entry: string): boolean {
|
|
|
117
128
|
}
|
|
118
129
|
}
|
|
119
130
|
|
|
131
|
+
type TargetedAsyncRunResolution =
|
|
132
|
+
| { kind: "exact"; id: string }
|
|
133
|
+
| { kind: "scan" }
|
|
134
|
+
| { kind: "reject" };
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolve an exact targeted run without following a run-directory symlink or
|
|
138
|
+
* accepting a path whose canonical location escaped the async root.
|
|
139
|
+
*/
|
|
140
|
+
export function resolveTargetedAsyncRun(asyncDirRoot: string, id: string, sessionId?: string): TargetedAsyncRunResolution {
|
|
141
|
+
if (!id || id === "." || id === ".." || path.basename(id) !== id) return { kind: "reject" };
|
|
142
|
+
const asyncDir = path.join(asyncDirRoot, id);
|
|
143
|
+
let entryStat: fs.Stats;
|
|
144
|
+
try {
|
|
145
|
+
entryStat = fs.lstatSync(asyncDir);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (isNotFoundError(error)) return { kind: "scan" };
|
|
148
|
+
throw new Error(`Failed to inspect async run path '${asyncDir}': ${getErrorMessage(error)}`, {
|
|
149
|
+
cause: error instanceof Error ? error : undefined,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (!entryStat.isDirectory() || entryStat.isSymbolicLink()) return { kind: "reject" };
|
|
153
|
+
try {
|
|
154
|
+
const canonicalRoot = fs.realpathSync(asyncDirRoot);
|
|
155
|
+
const canonicalDir = fs.realpathSync(asyncDir);
|
|
156
|
+
if (canonicalDir !== canonicalRoot && !canonicalDir.startsWith(`${canonicalRoot}${path.sep}`)) return { kind: "reject" };
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (isNotFoundError(error)) return { kind: "reject" };
|
|
159
|
+
throw new Error(`Failed to resolve async run path '${asyncDir}': ${getErrorMessage(error)}`, {
|
|
160
|
+
cause: error instanceof Error ? error : undefined,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (sessionId !== undefined) {
|
|
164
|
+
const status = readStatus(asyncDir);
|
|
165
|
+
if (status?.sessionId !== sessionId) return { kind: "scan" };
|
|
166
|
+
}
|
|
167
|
+
return { kind: "exact", id };
|
|
168
|
+
}
|
|
169
|
+
|
|
120
170
|
function outputFileMtime(outputFile: string | undefined): number | undefined {
|
|
121
171
|
if (!outputFile) return undefined;
|
|
122
172
|
try {
|
|
@@ -163,6 +213,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
163
213
|
return {
|
|
164
214
|
index,
|
|
165
215
|
agent: step.agent,
|
|
216
|
+
...(step.context ? { context: step.context } : {}),
|
|
166
217
|
...(step.label ? { label: step.label } : {}),
|
|
167
218
|
...(step.phase ? { phase: step.phase } : {}),
|
|
168
219
|
...(step.outputName ? { outputName: step.outputName } : {}),
|
|
@@ -186,12 +237,19 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
186
237
|
...(step.model ? { model: step.model } : {}),
|
|
187
238
|
...(step.thinking ? { thinking: step.thinking } : {}),
|
|
188
239
|
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
240
|
+
...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
|
|
241
|
+
...(step.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
|
|
189
242
|
...(step.error ? { error: step.error } : {}),
|
|
190
243
|
...(step.timedOut !== undefined ? { timedOut: step.timedOut } : {}),
|
|
191
244
|
...(step.stopped !== undefined ? { stopped: step.stopped } : {}),
|
|
192
245
|
...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
|
|
193
246
|
...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
|
|
194
247
|
...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
|
|
248
|
+
...(step.acceptance ? { acceptance: step.acceptance } : {}),
|
|
249
|
+
...(step.agentContract ? { agentContract: step.agentContract } : {}),
|
|
250
|
+
...(step.execution ? { execution: step.execution } : {}),
|
|
251
|
+
...(step.review ? { review: step.review } : {}),
|
|
252
|
+
...(step.effects ? { effects: step.effects } : {}),
|
|
195
253
|
...(step.children?.length ? { children: step.children } : {}),
|
|
196
254
|
};
|
|
197
255
|
});
|
|
@@ -211,6 +269,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
211
269
|
toolCount: status.toolCount,
|
|
212
270
|
steering: status.steering,
|
|
213
271
|
mode: status.mode,
|
|
272
|
+
...(summarizeContextModes(summarizedSteps.map((step) => step.context)) ? { context: summarizeContextModes(summarizedSteps.map((step) => step.context)) } : {}),
|
|
214
273
|
cwd: status.cwd,
|
|
215
274
|
startedAt: status.startedAt,
|
|
216
275
|
lastUpdate: status.lastUpdate,
|
|
@@ -264,7 +323,19 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
|
|
|
264
323
|
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
|
|
265
324
|
let entries: string[];
|
|
266
325
|
try {
|
|
267
|
-
|
|
326
|
+
if (options.runId !== undefined) {
|
|
327
|
+
const resolution = resolveTargetedAsyncRun(asyncDirRoot, options.runId, options.sessionId);
|
|
328
|
+
entries = resolution.kind === "exact"
|
|
329
|
+
? [resolution.id]
|
|
330
|
+
: resolution.kind === "scan"
|
|
331
|
+
? fs.readdirSync(asyncDirRoot).filter((entry) =>
|
|
332
|
+
(entry === options.runId || entry.startsWith(options.runId!))
|
|
333
|
+
&& resolveTargetedAsyncRun(asyncDirRoot, entry, options.sessionId).kind === "exact"
|
|
334
|
+
)
|
|
335
|
+
: [];
|
|
336
|
+
} else {
|
|
337
|
+
entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
|
|
338
|
+
}
|
|
268
339
|
} catch (error) {
|
|
269
340
|
if (isNotFoundError(error)) return [];
|
|
270
341
|
throw new Error(`Failed to list async runs in '${asyncDirRoot}': ${getErrorMessage(error)}`, {
|
|
@@ -333,8 +404,9 @@ function formatActivityFacts(input: { activityState?: ActivityState; lastActivit
|
|
|
333
404
|
|
|
334
405
|
function formatStepLine(step: AsyncRunStepSummary): string {
|
|
335
406
|
const display = step.label ? `${step.label} (${step.agent})` : step.agent;
|
|
407
|
+
const context = contextModeLabel(step.context);
|
|
336
408
|
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
337
|
-
const parts = [`${step.index + 1}. ${phase}${display}`, step.status];
|
|
409
|
+
const parts = [`${step.index + 1}. ${phase}${display}${context ? ` ${context}` : ""}`, step.status];
|
|
338
410
|
const activity = formatActivityFacts(step);
|
|
339
411
|
if (activity) parts.push(activity);
|
|
340
412
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
@@ -375,7 +447,8 @@ function formatRunHeader(run: AsyncRunSummary): string {
|
|
|
375
447
|
const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
|
|
376
448
|
const activity = formatActivityFacts(run);
|
|
377
449
|
const pending = run.pendingAppends ? ` | ${run.pendingAppends} pending append${run.pendingAppends === 1 ? "" : "s"}` : "";
|
|
378
|
-
|
|
450
|
+
const context = contextModeLabel(run.context);
|
|
451
|
+
return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode}${context ? ` ${context}` : ""} | ${stepLabel}${pending} | ${cwd}`;
|
|
379
452
|
}
|
|
380
453
|
|
|
381
454
|
export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {
|
|
@@ -131,6 +131,7 @@ export function consumeChainAppendRequests(asyncDir: string): ChainAppendRequest
|
|
|
131
131
|
function statusStepForTask(task: RunnerSubagentStep): StatusStep {
|
|
132
132
|
return {
|
|
133
133
|
agent: task.agent,
|
|
134
|
+
...(task.context ? { context: task.context } : {}),
|
|
134
135
|
phase: task.phase,
|
|
135
136
|
label: task.label,
|
|
136
137
|
outputName: task.outputName,
|
|
@@ -151,6 +152,7 @@ function statusStepsForRunnerStep(step: RunnerStep): StatusStep[] {
|
|
|
151
152
|
if (isDynamicRunnerGroup(step)) {
|
|
152
153
|
return [{
|
|
153
154
|
agent: `expand:${step.parallel.agent}`,
|
|
155
|
+
...(step.parallel.context ? { context: step.parallel.context } : {}),
|
|
154
156
|
phase: step.phase ?? step.parallel.phase,
|
|
155
157
|
label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
|
|
156
158
|
outputName: step.collect.as,
|
|
@@ -89,8 +89,8 @@ export interface CompletionBatcher<T> {
|
|
|
89
89
|
push(item: T): void;
|
|
90
90
|
/** Emit any held items immediately as a single group. */
|
|
91
91
|
flush(): void;
|
|
92
|
-
/** Clear timers
|
|
93
|
-
dispose():
|
|
92
|
+
/** Clear timers and return items that were never emitted. */
|
|
93
|
+
dispose(): T[];
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
@@ -109,7 +109,7 @@ export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>)
|
|
|
109
109
|
options.emit([item]);
|
|
110
110
|
},
|
|
111
111
|
flush() {},
|
|
112
|
-
dispose() {},
|
|
112
|
+
dispose() { return []; },
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -160,7 +160,9 @@ export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>)
|
|
|
160
160
|
flush: emitGroup,
|
|
161
161
|
dispose() {
|
|
162
162
|
clearTimers();
|
|
163
|
+
const abandoned = pending;
|
|
163
164
|
pending = [];
|
|
164
|
-
|
|
165
|
+
return abandoned;
|
|
166
|
+
}
|
|
165
167
|
};
|
|
166
168
|
}
|
|
@@ -20,9 +20,9 @@ function asFiniteNumber(value: unknown): number | undefined {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function buildCompletionKey(data: CompletionDataLike, fallback: string): string {
|
|
23
|
-
const id = asNonEmptyString(data.id);
|
|
24
|
-
if (id) return `id:${id}`;
|
|
25
23
|
const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
|
|
24
|
+
const id = asNonEmptyString(data.id);
|
|
25
|
+
if (id) return `session:${sessionId}:id:${id}`;
|
|
26
26
|
const agent = asNonEmptyString(data.agent) ?? "unknown";
|
|
27
27
|
const timestamp = asFiniteNumber(data.timestamp);
|
|
28
28
|
const taskIndex = asFiniteNumber(data.taskIndex);
|
|
@@ -52,12 +52,3 @@ export function markSeenWithTtl(seen: Map<string, number>, key: string, now: num
|
|
|
52
52
|
seen.set(key, now);
|
|
53
53
|
return false;
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
export function getGlobalSeenMap(storeKey: string): Map<string, number> {
|
|
57
|
-
const globalStore = globalThis as Record<string, unknown>;
|
|
58
|
-
const existing = globalStore[storeKey];
|
|
59
|
-
if (existing instanceof Map) return existing as Map<string, number>;
|
|
60
|
-
const map = new Map<string, number>();
|
|
61
|
-
globalStore[storeKey] = map;
|
|
62
|
-
return map;
|
|
63
|
-
}
|