pi-subagents 0.35.1 → 0.37.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 +71 -0
- package/README.md +224 -31
- package/agents/advisor.md +73 -0
- package/agents/delegate.md +2 -0
- package/agents/worker.md +2 -0
- package/package.json +11 -13
- package/skills/pi-subagents/SKILL.md +42 -13
- package/src/agents/agent-management.ts +7 -1
- package/src/agents/agents.ts +121 -17
- package/src/api/capability-ceiling.ts +17 -0
- package/src/api/delegation.ts +127 -0
- package/src/api/preflight.ts +399 -0
- package/src/extension/config.ts +7 -1
- package/src/extension/index.ts +52 -38
- package/src/extension/rpc.ts +31 -2
- package/src/extension/schemas.ts +29 -3
- package/src/extension/tool-description.ts +4 -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 +158 -17
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +47 -8
- package/src/runs/background/async-status.ts +94 -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/process-terminal.ts +280 -0
- package/src/runs/background/result-watcher.ts +138 -78
- package/src/runs/background/run-status.ts +10 -2
- package/src/runs/background/scheduled-runs.ts +6 -1
- package/src/runs/background/stale-run-reconciler.ts +6 -0
- package/src/runs/background/subagent-runner.ts +406 -54
- 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 +181 -111
- package/src/runs/foreground/execution.ts +147 -47
- package/src/runs/foreground/foreground-control.ts +90 -0
- package/src/runs/foreground/subagent-executor.ts +426 -165
- package/src/runs/shared/acceptance.ts +94 -41
- package/src/runs/shared/agent-contract.ts +38 -0
- package/src/runs/shared/capability-ceiling.ts +177 -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 +5 -5
- package/src/runs/shared/long-running-guard.ts +4 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
- package/src/runs/shared/nested-events.ts +35 -3
- package/src/runs/shared/parallel-handoff.ts +154 -0
- package/src/runs/shared/parallel-utils.ts +11 -0
- package/src/runs/shared/pi-args.ts +148 -56
- package/src/runs/shared/run-history.ts +90 -5
- package/src/runs/shared/session-lease.ts +25 -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 +31 -19
- package/src/runs/shared/task-intent.ts +10 -5
- package/src/runs/shared/tool-availability.ts +21 -3
- 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/launch-contract.ts +123 -0
- package/src/shared/settings.ts +9 -1
- package/src/shared/types.ts +314 -28
- package/src/shared/utils.ts +17 -42
- package/src/slash/delegation-adapters.ts +153 -6
- 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 +15 -6
- 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 +17 -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
|
@@ -1,9 +1,10 @@
|
|
|
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";
|
|
7
|
+
import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
7
8
|
import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
|
|
8
9
|
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
9
10
|
|
|
@@ -38,6 +39,8 @@ export type AsyncResumeTarget = {
|
|
|
38
39
|
model?: string;
|
|
39
40
|
thinking?: string;
|
|
40
41
|
recoveryDescriptor?: SteeringRecoveryDescriptor;
|
|
42
|
+
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
43
|
+
launchContractDigest?: string;
|
|
41
44
|
};
|
|
42
45
|
|
|
43
46
|
interface AsyncResultFile {
|
|
@@ -52,7 +55,9 @@ interface AsyncResultFile {
|
|
|
52
55
|
sessionFile?: string;
|
|
53
56
|
model?: string;
|
|
54
57
|
thinking?: string;
|
|
55
|
-
|
|
58
|
+
launchContractDigest?: string;
|
|
59
|
+
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
60
|
+
results?: Array<{ agent?: string; success?: boolean; sessionFile?: string; intercomTarget?: string; model?: string; thinking?: string; launchContractDigest?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling }>;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
export interface AsyncRunLocation {
|
|
@@ -92,9 +97,11 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
|
|
|
92
97
|
const intercomTarget = validateOptionalString(child, "intercomTarget", resultPath, `results[${index}].intercomTarget`);
|
|
93
98
|
const model = validateOptionalString(child, "model", resultPath, `results[${index}].model`);
|
|
94
99
|
const thinking = validateOptionalString(child, "thinking", resultPath, `results[${index}].thinking`);
|
|
100
|
+
const launchContractDigest = validateOptionalString(child, "launchContractDigest", resultPath, `results[${index}].launchContractDigest`);
|
|
101
|
+
const capabilityCeiling = child.capabilityCeiling === undefined ? undefined : parseSubagentCapabilityCeiling(child.capabilityCeiling, `async result file '${resultPath}' results[${index}].capabilityCeiling`);
|
|
95
102
|
const success = child.success;
|
|
96
103
|
if (success !== undefined && typeof success !== "boolean") throw new Error(`Invalid async result file '${resultPath}': results[${index}].success must be a boolean.`);
|
|
97
|
-
return { agent, sessionFile, intercomTarget, model, thinking, ...(typeof success === "boolean" ? { success } : {}) };
|
|
104
|
+
return { agent, sessionFile, intercomTarget, model, thinking, launchContractDigest, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(typeof success === "boolean" ? { success } : {}) };
|
|
98
105
|
});
|
|
99
106
|
}
|
|
100
107
|
const success = data.success;
|
|
@@ -110,6 +117,8 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
|
|
|
110
117
|
sessionFile: validateOptionalString(data, "sessionFile", resultPath),
|
|
111
118
|
model: validateOptionalString(data, "model", resultPath),
|
|
112
119
|
thinking: validateOptionalString(data, "thinking", resultPath),
|
|
120
|
+
launchContractDigest: validateOptionalString(data, "launchContractDigest", resultPath),
|
|
121
|
+
...(data.capabilityCeiling === undefined ? {} : { capabilityCeiling: parseSubagentCapabilityCeiling(data.capabilityCeiling, `async result file '${resultPath}' capabilityCeiling`) }),
|
|
113
122
|
...(typeof success === "boolean" ? { success } : {}),
|
|
114
123
|
...(results ? { results } : {}),
|
|
115
124
|
};
|
|
@@ -237,6 +246,7 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
|
|
|
237
246
|
if (status.sessionId !== undefined && typeof status.sessionId !== "string") throw new Error(`Invalid async status '${source}': sessionId must be a string.`);
|
|
238
247
|
if (status.cwd !== undefined && typeof status.cwd !== "string") throw new Error(`Invalid async status '${source}': cwd must be a string.`);
|
|
239
248
|
if (status.sessionFile !== undefined && typeof status.sessionFile !== "string") throw new Error(`Invalid async status '${source}': sessionFile must be a string.`);
|
|
249
|
+
if (status.capabilityCeiling !== undefined) status.capabilityCeiling = parseSubagentCapabilityCeiling(status.capabilityCeiling, `async status '${source}' capabilityCeiling`);
|
|
240
250
|
if (status.steps !== undefined) {
|
|
241
251
|
if (!Array.isArray(status.steps)) throw new Error(`Invalid async status '${source}': steps must be an array.`);
|
|
242
252
|
status.steps.forEach((step, index) => {
|
|
@@ -246,10 +256,22 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
|
|
|
246
256
|
if (stepRecord.sessionFile !== undefined && typeof stepRecord.sessionFile !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionFile must be a string.`);
|
|
247
257
|
if (stepRecord.model !== undefined && typeof stepRecord.model !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].model must be a string.`);
|
|
248
258
|
if (stepRecord.thinking !== undefined && typeof stepRecord.thinking !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].thinking must be a string.`);
|
|
259
|
+
if (stepRecord.capabilityCeiling !== undefined) stepRecord.capabilityCeiling = parseSubagentCapabilityCeiling(stepRecord.capabilityCeiling, `async status '${source}' steps[${index}].capabilityCeiling`);
|
|
249
260
|
});
|
|
250
261
|
}
|
|
251
262
|
}
|
|
252
263
|
|
|
264
|
+
function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): AcceptanceInput | undefined {
|
|
265
|
+
if (value && typeof value === "object" && !Array.isArray(value) && ("explicit" in value || "inferredReason" in value)) {
|
|
266
|
+
const { explicit, inferredReason: _inferredReason, ...publicAcceptance } = value as Record<string, unknown>;
|
|
267
|
+
if (explicit === false) return undefined;
|
|
268
|
+
value = publicAcceptance;
|
|
269
|
+
}
|
|
270
|
+
const errors = validateAcceptanceInput(value, "recoveryDescriptor.acceptance");
|
|
271
|
+
if (errors.length) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${errors.join(" ")}`);
|
|
272
|
+
return value as AcceptanceInput;
|
|
273
|
+
}
|
|
274
|
+
|
|
253
275
|
export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): SteeringRecoveryDescriptor | undefined {
|
|
254
276
|
if (!asyncDir) return undefined;
|
|
255
277
|
const descriptorPath = path.join(asyncDir, "recovery-descriptor.json");
|
|
@@ -263,10 +285,10 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
263
285
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
|
|
264
286
|
const parsed = value as Record<string, unknown>;
|
|
265
287
|
const allowedFields = new Set([
|
|
266
|
-
"version", "sourceRunId", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
|
|
288
|
+
"version", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
|
|
267
289
|
"subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
|
|
268
|
-
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "acceptance", "sessionDir", "artifactConfig",
|
|
269
|
-
"artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share",
|
|
290
|
+
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
291
|
+
"artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
270
292
|
]);
|
|
271
293
|
for (const field of Object.keys(parsed)) {
|
|
272
294
|
if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
|
|
@@ -276,6 +298,12 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
276
298
|
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
299
|
}
|
|
278
300
|
if (parsed.version !== 1) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': version must be 1.`);
|
|
301
|
+
if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
|
|
302
|
+
if (parsed.agentContract !== undefined) {
|
|
303
|
+
if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
|
|
304
|
+
const contract = parsed.agentContract as Record<string, unknown>;
|
|
305
|
+
if (contract.version !== 1 || Object.keys(contract).some((key) => key !== "version")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be { version: 1 }.`);
|
|
306
|
+
}
|
|
279
307
|
if (parsed.systemPromptMode !== "append" && parsed.systemPromptMode !== "replace") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPromptMode is invalid.`);
|
|
280
308
|
if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
|
|
281
309
|
for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
|
|
@@ -291,6 +319,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
291
319
|
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
320
|
}
|
|
293
321
|
if (parsed.completionGuard !== undefined && typeof parsed.completionGuard !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': completionGuard must be a boolean.`);
|
|
322
|
+
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
323
|
if (parsed.memory !== undefined) {
|
|
295
324
|
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
325
|
const memory = parsed.memory as Record<string, unknown>;
|
|
@@ -335,8 +364,9 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
335
364
|
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
365
|
}
|
|
337
366
|
if (parsed.acceptance !== undefined) {
|
|
338
|
-
const
|
|
339
|
-
if (
|
|
367
|
+
const acceptance = normalizeRecoveryAcceptance(parsed.acceptance, descriptorPath);
|
|
368
|
+
if (acceptance === undefined) delete parsed.acceptance;
|
|
369
|
+
else parsed.acceptance = acceptance;
|
|
340
370
|
}
|
|
341
371
|
return parsed as unknown as SteeringRecoveryDescriptor;
|
|
342
372
|
}
|
|
@@ -385,6 +415,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
385
415
|
if (requestedIndex < 0 || requestedIndex >= stepCount) throw new Error(`Async run '${runId}' has ${stepCount} children. Index ${requestedIndex} is out of range.`);
|
|
386
416
|
const selectedStep = statusSteps[requestedIndex];
|
|
387
417
|
if (selectedStep?.status === "running") {
|
|
418
|
+
const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, selectedStep.capabilityCeiling);
|
|
388
419
|
return {
|
|
389
420
|
kind: "live",
|
|
390
421
|
runId,
|
|
@@ -396,6 +427,8 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
396
427
|
sessionFile: selectedStep.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
|
|
397
428
|
model: selectedStep.model,
|
|
398
429
|
thinking: selectedStep.thinking,
|
|
430
|
+
launchContractDigest: selectedStep.launchContractDigest ?? result?.results?.[requestedIndex]?.launchContractDigest ?? result?.launchContractDigest,
|
|
431
|
+
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
399
432
|
...(recoveryDescriptor ? { recoveryDescriptor } : {}),
|
|
400
433
|
};
|
|
401
434
|
}
|
|
@@ -409,6 +442,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
409
442
|
if (!selected) {
|
|
410
443
|
throw new Error(`Async run '${runId}' has ${running.length} running children. Provide index to choose one.`);
|
|
411
444
|
}
|
|
445
|
+
const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, selected.step.capabilityCeiling);
|
|
412
446
|
return {
|
|
413
447
|
kind: "live",
|
|
414
448
|
runId,
|
|
@@ -420,6 +454,8 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
420
454
|
sessionFile: selected.step.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
|
|
421
455
|
model: selected.step.model,
|
|
422
456
|
thinking: selected.step.thinking,
|
|
457
|
+
launchContractDigest: selected.step.launchContractDigest ?? result?.results?.[selected.index]?.launchContractDigest ?? result?.launchContractDigest,
|
|
458
|
+
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
423
459
|
...(recoveryDescriptor ? { recoveryDescriptor } : {}),
|
|
424
460
|
};
|
|
425
461
|
}
|
|
@@ -441,6 +477,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
441
477
|
const resolvedSessionFile = sessionFile ? validateResumeSessionFile(runId, sessionFile) : undefined;
|
|
442
478
|
const stepModel = statusSteps[index]?.model ?? resultSteps[index]?.model ?? (stepCount === 1 ? result?.model : undefined);
|
|
443
479
|
const stepThinking = statusSteps[index]?.thinking ?? resultSteps[index]?.thinking ?? (stepCount === 1 ? result?.thinking : undefined);
|
|
480
|
+
const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, statusSteps[index]?.capabilityCeiling, result?.capabilityCeiling, resultSteps[index]?.capabilityCeiling);
|
|
444
481
|
|
|
445
482
|
return {
|
|
446
483
|
kind: "revive",
|
|
@@ -453,6 +490,8 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
453
490
|
...(resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {}),
|
|
454
491
|
...(stepModel ? { model: stepModel } : {}),
|
|
455
492
|
...(stepThinking ? { thinking: stepThinking } : {}),
|
|
493
|
+
launchContractDigest: statusSteps[index]?.launchContractDigest ?? resultSteps[index]?.launchContractDigest ?? result?.launchContractDigest,
|
|
494
|
+
...(capabilityCeiling ? { capabilityCeiling } : {}),
|
|
456
495
|
...(recoveryDescriptor ? { recoveryDescriptor } : {}),
|
|
457
496
|
};
|
|
458
497
|
}
|
|
@@ -3,15 +3,19 @@ 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
5
|
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState } from "../../shared/types.ts";
|
|
6
|
+
import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
|
|
6
7
|
import { readStatus } from "../../shared/utils.ts";
|
|
7
8
|
import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
8
9
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
9
10
|
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
11
|
+
import { contextModeLabel, summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
|
|
10
12
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
13
|
+
import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
|
|
11
14
|
|
|
12
15
|
interface AsyncRunStepSummary {
|
|
13
16
|
index: number;
|
|
14
17
|
agent: string;
|
|
18
|
+
context?: ContextMode;
|
|
15
19
|
label?: string;
|
|
16
20
|
phase?: string;
|
|
17
21
|
outputName?: string;
|
|
@@ -35,12 +39,22 @@ interface AsyncRunStepSummary {
|
|
|
35
39
|
model?: string;
|
|
36
40
|
thinking?: string;
|
|
37
41
|
attemptedModels?: string[];
|
|
42
|
+
sessionFile?: string;
|
|
43
|
+
transcriptPath?: string;
|
|
38
44
|
error?: string;
|
|
39
45
|
timedOut?: boolean;
|
|
40
46
|
stopped?: boolean;
|
|
41
47
|
turnBudget?: TurnBudgetState;
|
|
42
48
|
turnBudgetExceeded?: boolean;
|
|
43
49
|
wrapUpRequested?: boolean;
|
|
50
|
+
acceptance?: AsyncJobStep["acceptance"];
|
|
51
|
+
agentContract?: AsyncJobStep["agentContract"];
|
|
52
|
+
execution?: AsyncJobStep["execution"];
|
|
53
|
+
review?: AsyncJobStep["review"];
|
|
54
|
+
effects?: AsyncJobStep["effects"];
|
|
55
|
+
processTerminal?: AsyncJobStep["processTerminal"];
|
|
56
|
+
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
57
|
+
capabilityAudit?: SubagentCapabilityAudit;
|
|
44
58
|
children?: NestedRunSummary[];
|
|
45
59
|
}
|
|
46
60
|
|
|
@@ -59,6 +73,7 @@ export interface AsyncRunSummary {
|
|
|
59
73
|
toolCount?: number;
|
|
60
74
|
steering?: SteeringStatus;
|
|
61
75
|
mode: SubagentRunMode;
|
|
76
|
+
context?: ContextSummary;
|
|
62
77
|
cwd?: string;
|
|
63
78
|
startedAt: number;
|
|
64
79
|
lastUpdate?: number;
|
|
@@ -82,6 +97,9 @@ export interface AsyncRunSummary {
|
|
|
82
97
|
sessionFile?: string;
|
|
83
98
|
nestedChildren?: NestedRunSummary[];
|
|
84
99
|
nestedWarnings?: string[];
|
|
100
|
+
processTerminal?: AsyncStatus["processTerminal"];
|
|
101
|
+
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
102
|
+
capabilityAudit?: SubagentCapabilityAudit;
|
|
85
103
|
}
|
|
86
104
|
|
|
87
105
|
interface AsyncRunListOptions {
|
|
@@ -92,6 +110,7 @@ interface AsyncRunListOptions {
|
|
|
92
110
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
93
111
|
now?: () => number;
|
|
94
112
|
reconcile?: boolean;
|
|
113
|
+
runId?: string;
|
|
95
114
|
}
|
|
96
115
|
|
|
97
116
|
function getErrorMessage(error: unknown): string {
|
|
@@ -117,6 +136,45 @@ function isAsyncRunDir(root: string, entry: string): boolean {
|
|
|
117
136
|
}
|
|
118
137
|
}
|
|
119
138
|
|
|
139
|
+
type TargetedAsyncRunResolution =
|
|
140
|
+
| { kind: "exact"; id: string }
|
|
141
|
+
| { kind: "scan" }
|
|
142
|
+
| { kind: "reject" };
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolve an exact targeted run without following a run-directory symlink or
|
|
146
|
+
* accepting a path whose canonical location escaped the async root.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveTargetedAsyncRun(asyncDirRoot: string, id: string, sessionId?: string): TargetedAsyncRunResolution {
|
|
149
|
+
if (!id || id === "." || id === ".." || path.basename(id) !== id) return { kind: "reject" };
|
|
150
|
+
const asyncDir = path.join(asyncDirRoot, id);
|
|
151
|
+
let entryStat: fs.Stats;
|
|
152
|
+
try {
|
|
153
|
+
entryStat = fs.lstatSync(asyncDir);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (isNotFoundError(error)) return { kind: "scan" };
|
|
156
|
+
throw new Error(`Failed to inspect async run path '${asyncDir}': ${getErrorMessage(error)}`, {
|
|
157
|
+
cause: error instanceof Error ? error : undefined,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (!entryStat.isDirectory() || entryStat.isSymbolicLink()) return { kind: "reject" };
|
|
161
|
+
try {
|
|
162
|
+
const canonicalRoot = fs.realpathSync(asyncDirRoot);
|
|
163
|
+
const canonicalDir = fs.realpathSync(asyncDir);
|
|
164
|
+
if (canonicalDir !== canonicalRoot && !canonicalDir.startsWith(`${canonicalRoot}${path.sep}`)) return { kind: "reject" };
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (isNotFoundError(error)) return { kind: "reject" };
|
|
167
|
+
throw new Error(`Failed to resolve async run path '${asyncDir}': ${getErrorMessage(error)}`, {
|
|
168
|
+
cause: error instanceof Error ? error : undefined,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (sessionId !== undefined) {
|
|
172
|
+
const status = readStatus(asyncDir);
|
|
173
|
+
if (status?.sessionId !== sessionId) return { kind: "scan" };
|
|
174
|
+
}
|
|
175
|
+
return { kind: "exact", id };
|
|
176
|
+
}
|
|
177
|
+
|
|
120
178
|
function outputFileMtime(outputFile: string | undefined): number | undefined {
|
|
121
179
|
if (!outputFile) return undefined;
|
|
122
180
|
try {
|
|
@@ -144,6 +202,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
144
202
|
throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
|
|
145
203
|
}
|
|
146
204
|
const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
|
|
205
|
+
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
|
|
206
|
+
?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
|
|
147
207
|
const steps = status.steps ?? [];
|
|
148
208
|
const chainStepCount = status.chainStepCount ?? steps.length;
|
|
149
209
|
const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
|
|
@@ -163,6 +223,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
163
223
|
return {
|
|
164
224
|
index,
|
|
165
225
|
agent: step.agent,
|
|
226
|
+
...(step.context ? { context: step.context } : {}),
|
|
166
227
|
...(step.label ? { label: step.label } : {}),
|
|
167
228
|
...(step.phase ? { phase: step.phase } : {}),
|
|
168
229
|
...(step.outputName ? { outputName: step.outputName } : {}),
|
|
@@ -186,12 +247,23 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
186
247
|
...(step.model ? { model: step.model } : {}),
|
|
187
248
|
...(step.thinking ? { thinking: step.thinking } : {}),
|
|
188
249
|
...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
250
|
+
...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
|
|
251
|
+
...(step.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
|
|
189
252
|
...(step.error ? { error: step.error } : {}),
|
|
190
253
|
...(step.timedOut !== undefined ? { timedOut: step.timedOut } : {}),
|
|
191
254
|
...(step.stopped !== undefined ? { stopped: step.stopped } : {}),
|
|
192
255
|
...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
|
|
193
256
|
...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
|
|
194
257
|
...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
|
|
258
|
+
...(step.acceptance ? { acceptance: step.acceptance } : {}),
|
|
259
|
+
...(step.agentContract ? { agentContract: step.agentContract } : {}),
|
|
260
|
+
...(step.launchContractDigest ? { launchContractDigest: step.launchContractDigest } : {}),
|
|
261
|
+
...(step.execution ? { execution: step.execution } : {}),
|
|
262
|
+
...(step.review ? { review: step.review } : {}),
|
|
263
|
+
...(step.effects ? { effects: step.effects } : {}),
|
|
264
|
+
...(step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${path.join(asyncDir, "status.json")} step ${index}`) } : {}),
|
|
265
|
+
...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
|
|
266
|
+
...(step.capabilityAudit ? { capabilityAudit: step.capabilityAudit } : {}),
|
|
195
267
|
...(step.children?.length ? { children: step.children } : {}),
|
|
196
268
|
};
|
|
197
269
|
});
|
|
@@ -211,6 +283,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
211
283
|
toolCount: status.toolCount,
|
|
212
284
|
steering: status.steering,
|
|
213
285
|
mode: status.mode,
|
|
286
|
+
...(summarizeContextModes(summarizedSteps.map((step) => step.context)) ? { context: summarizeContextModes(summarizedSteps.map((step) => step.context)) } : {}),
|
|
214
287
|
cwd: status.cwd,
|
|
215
288
|
startedAt: status.startedAt,
|
|
216
289
|
lastUpdate: status.lastUpdate,
|
|
@@ -229,6 +302,10 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
229
302
|
steps: summarizedSteps,
|
|
230
303
|
...(nestedChildren.length ? { nestedChildren } : {}),
|
|
231
304
|
...(nestedWarnings.length ? { nestedWarnings } : {}),
|
|
305
|
+
...(processTerminal ? { processTerminal } : {}),
|
|
306
|
+
...(status.launchContractDigest ? { launchContractDigest: status.launchContractDigest } : {}),
|
|
307
|
+
...(status.capabilityCeiling ? { capabilityCeiling: status.capabilityCeiling } : {}),
|
|
308
|
+
...(status.capabilityAudit ? { capabilityAudit: status.capabilityAudit } : {}),
|
|
232
309
|
...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
|
|
233
310
|
...(status.outputFile ? { outputFile: status.outputFile } : {}),
|
|
234
311
|
...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
|
|
@@ -264,7 +341,19 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
|
|
|
264
341
|
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
|
|
265
342
|
let entries: string[];
|
|
266
343
|
try {
|
|
267
|
-
|
|
344
|
+
if (options.runId !== undefined) {
|
|
345
|
+
const resolution = resolveTargetedAsyncRun(asyncDirRoot, options.runId, options.sessionId);
|
|
346
|
+
entries = resolution.kind === "exact"
|
|
347
|
+
? [resolution.id]
|
|
348
|
+
: resolution.kind === "scan"
|
|
349
|
+
? fs.readdirSync(asyncDirRoot).filter((entry) =>
|
|
350
|
+
(entry === options.runId || entry.startsWith(options.runId!))
|
|
351
|
+
&& resolveTargetedAsyncRun(asyncDirRoot, entry, options.sessionId).kind === "exact"
|
|
352
|
+
)
|
|
353
|
+
: [];
|
|
354
|
+
} else {
|
|
355
|
+
entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
|
|
356
|
+
}
|
|
268
357
|
} catch (error) {
|
|
269
358
|
if (isNotFoundError(error)) return [];
|
|
270
359
|
throw new Error(`Failed to list async runs in '${asyncDirRoot}': ${getErrorMessage(error)}`, {
|
|
@@ -333,8 +422,9 @@ function formatActivityFacts(input: { activityState?: ActivityState; lastActivit
|
|
|
333
422
|
|
|
334
423
|
function formatStepLine(step: AsyncRunStepSummary): string {
|
|
335
424
|
const display = step.label ? `${step.label} (${step.agent})` : step.agent;
|
|
425
|
+
const context = contextModeLabel(step.context);
|
|
336
426
|
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
337
|
-
const parts = [`${step.index + 1}. ${phase}${display}`, step.status];
|
|
427
|
+
const parts = [`${step.index + 1}. ${phase}${display}${context ? ` ${context}` : ""}`, step.status];
|
|
338
428
|
const activity = formatActivityFacts(step);
|
|
339
429
|
if (activity) parts.push(activity);
|
|
340
430
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
@@ -375,7 +465,8 @@ function formatRunHeader(run: AsyncRunSummary): string {
|
|
|
375
465
|
const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
|
|
376
466
|
const activity = formatActivityFacts(run);
|
|
377
467
|
const pending = run.pendingAppends ? ` | ${run.pendingAppends} pending append${run.pendingAppends === 1 ? "" : "s"}` : "";
|
|
378
|
-
|
|
468
|
+
const context = contextModeLabel(run.context);
|
|
469
|
+
return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode}${context ? ` ${context}` : ""} | ${stepLabel}${pending} | ${cwd}`;
|
|
379
470
|
}
|
|
380
471
|
|
|
381
472
|
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
|
-
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "../../shared/types.ts";
|
|
17
17
|
import { readStatus } from "../../shared/utils.ts";
|
|
18
18
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
19
|
+
import { contextModeLabel, summarizeContextModes } from "../shared/context-mode.ts";
|
|
19
20
|
import { formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
|
|
20
21
|
|
|
21
22
|
const DEFAULT_TRANSCRIPT_LINES = 80;
|
|
@@ -277,15 +278,17 @@ function formatAsyncFleetLines(runs: AsyncRunSummary[]): string[] {
|
|
|
277
278
|
const activity = formatActivityFacts(run);
|
|
278
279
|
const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
|
|
279
280
|
const pending = run.pendingAppends ? ` | ${run.pendingAppends} pending append${run.pendingAppends === 1 ? "" : "s"}` : "";
|
|
280
|
-
|
|
281
|
+
const runContext = contextModeLabel(run.context);
|
|
282
|
+
lines.push(`- ${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode}${runContext ? ` ${runContext}` : ""} | ${progress}${pending} | ${cwd}`);
|
|
281
283
|
lines.push(` status: subagent({ action: "status", id: "${run.id}" })`);
|
|
282
284
|
lines.push(` transcript: subagent({ action: "status", id: "${run.id}", view: "transcript" })`);
|
|
283
285
|
for (const step of run.steps) {
|
|
284
286
|
const display = step.label ? `${step.label} (${step.agent})` : step.agent;
|
|
287
|
+
const stepContext = contextModeLabel(step.context);
|
|
285
288
|
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
286
289
|
const stepActivity = formatActivityFacts(step);
|
|
287
290
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
288
|
-
const parts = [`${step.index}. ${phase}${display}`, step.status, stepActivity, modelThinking].filter(Boolean);
|
|
291
|
+
const parts = [`${step.index}. ${phase}${display}${stepContext ? ` ${stepContext}` : ""}`, step.status, stepActivity, modelThinking].filter(Boolean);
|
|
289
292
|
lines.push(` ${parts.join(" | ")}`);
|
|
290
293
|
const output = path.join(run.asyncDir, `output-${step.index}.log`);
|
|
291
294
|
if (fs.existsSync(output)) lines.push(` output: ${shortenPath(output)}`);
|
|
@@ -385,8 +388,9 @@ function selectTranscriptStep(status: AsyncStatus, options: TranscriptOptions):
|
|
|
385
388
|
function stepStateLine(mode: SubagentRunMode, index: number | undefined, step: AsyncJobStep | undefined): string | undefined {
|
|
386
389
|
if (index === undefined || !step) return undefined;
|
|
387
390
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
391
|
+
const context = contextModeLabel(step.context);
|
|
388
392
|
const parts = [
|
|
389
|
-
`${mode === "parallel" ? "Agent" : "Step"}: ${index} (${step.agent})`,
|
|
393
|
+
`${mode === "parallel" ? "Agent" : "Step"}: ${index} (${step.agent})${context ? ` ${context}` : ""}`,
|
|
390
394
|
step.status,
|
|
391
395
|
formatActivityFacts(step),
|
|
392
396
|
modelThinking,
|
|
@@ -428,10 +432,11 @@ export function formatAsyncRunTranscript(status: AsyncStatus, asyncDir: string,
|
|
|
428
432
|
const sessionFile = selected.index !== undefined ? selected.step?.sessionFile : status.sessionFile;
|
|
429
433
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
430
434
|
|
|
435
|
+
const context = contextModeLabel(status.context ?? summarizeContextModes((status.steps ?? []).map((step) => step.context)));
|
|
431
436
|
const lines = [
|
|
432
437
|
`Run: ${status.runId}`,
|
|
433
438
|
`State: ${status.state}`,
|
|
434
|
-
`Mode: ${status.mode}`,
|
|
439
|
+
`Mode: ${status.mode}${context ? ` ${context}` : ""}`,
|
|
435
440
|
stepStateLine(status.mode, selected.index, selected.step),
|
|
436
441
|
selected.hint,
|
|
437
442
|
].filter((line): line is string => Boolean(line));
|