pi-subagents 0.58.0 → 0.60.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 +102 -0
- package/docs/agents.md +5 -3
- package/docs/configuration.md +4 -4
- package/docs/extension-api.md +1 -1
- package/docs/models.md +24 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +93 -5
- package/docs/workflows.md +94 -3
- package/package.json +3 -1
- package/prompts/review-loop.md +2 -2
- package/skills/council-mode/SKILL.md +48 -243
- package/skills/council-mode/references/pass-contracts.md +150 -0
- package/skills/pi-subagents/SKILL.md +87 -37
- package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
- package/skills/pi-subagents/references/execution-controls.md +52 -7
- package/skills/pi-subagents/references/management-authoring-rpc.md +3 -4
- package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +54 -26
- package/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/src/agents/agent-management.ts +159 -42
- package/src/agents/agent-serializer.ts +4 -2
- package/src/agents/agents.ts +67 -26
- package/src/agents/runtime-agent-registry.ts +9 -16
- package/src/api/background-work.ts +5 -1
- package/src/api/delegation.ts +0 -7
- package/src/api/preflight.ts +6 -9
- package/src/api/shared-types.ts +2 -0
- package/src/extension/fanout-child.ts +5 -3
- package/src/extension/index.ts +51 -23
- package/src/extension/public-execution.ts +16 -2
- package/src/extension/schemas.ts +36 -10
- package/src/extension/tool-description.ts +24 -5
- package/src/intercom/result-intercom.ts +2 -0
- package/src/profiles/profiles.ts +5 -6
- package/src/runs/background/active-async-capacity.ts +2 -2
- package/src/runs/background/async-execution.ts +52 -37
- package/src/runs/background/async-job-tracker.ts +17 -12
- package/src/runs/background/async-resume.ts +23 -25
- package/src/runs/background/async-status-snapshot.ts +23 -261
- package/src/runs/background/async-status.ts +110 -8
- package/src/runs/background/chain-append.ts +6 -3
- package/src/runs/background/chain-root-attachment.ts +60 -8
- package/src/runs/background/control-channel.ts +3 -2
- package/src/runs/background/fleet-view.ts +21 -11
- package/src/runs/background/notify.ts +158 -6
- package/src/runs/background/result-files.ts +2 -1
- package/src/runs/background/result-watcher.ts +2 -0
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/retained-children.ts +1 -1
- package/src/runs/background/run-status.ts +30 -9
- package/src/runs/background/scheduled-runs.ts +86 -7
- package/src/runs/background/stale-run-reconciler.ts +10 -4
- package/src/runs/background/steering.ts +4 -14
- package/src/runs/background/subagent-runner.ts +351 -358
- package/src/runs/background/subagent-wait.ts +68 -12
- package/src/runs/background/terminal-run-index.ts +1 -1
- package/src/runs/background/wait-completions.ts +25 -1
- package/src/runs/background/wait-config.ts +23 -9
- package/src/runs/background/wait-tool.ts +9 -2
- package/src/runs/foreground/async-steering-action.ts +2 -2
- package/src/runs/foreground/execution.ts +125 -122
- package/src/runs/foreground/foreground-control.ts +3 -0
- package/src/runs/foreground/foreground-history.ts +1 -0
- package/src/runs/foreground/subagent-executor.ts +576 -240
- package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
- package/src/runs/shared/abort-recovery.ts +119 -0
- package/src/runs/shared/async-status-projection.ts +597 -0
- package/src/runs/shared/background-process-options.ts +9 -0
- package/src/runs/shared/child-identity.ts +19 -4
- package/src/runs/shared/child-launch-plan.ts +151 -0
- package/src/runs/shared/completion-evidence.ts +89 -0
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/host-step-status.ts +230 -0
- package/src/runs/shared/lane-metadata.ts +105 -0
- package/src/runs/shared/mcp-config-sources.ts +42 -6
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
- package/src/runs/shared/mcp-direct-tool-grant.ts +194 -0
- package/src/runs/shared/model-fallback.ts +9 -2
- package/src/runs/shared/mutation-evidence.ts +52 -3
- package/src/runs/shared/nested-events.ts +6 -2
- package/src/runs/shared/nested-render.ts +7 -3
- package/src/runs/shared/parallel-handoff.ts +419 -7
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +67 -3
- package/src/runs/shared/single-output.ts +72 -22
- package/src/runs/shared/subagent-prompt-runtime.ts +33 -6
- package/src/runs/shared/workflow-graph.ts +15 -0
- package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/src/runs/shared/worktree.ts +18 -0
- package/src/shared/child-session-name.ts +46 -0
- package/src/shared/extension-context.ts +24 -0
- package/src/shared/formatters.ts +5 -2
- package/src/shared/launch-contract.ts +1 -1
- package/src/shared/settings.ts +9 -103
- package/src/shared/types.ts +232 -30
- package/src/shared/utils.ts +35 -55
- package/src/slash/delegation-adapters.ts +1 -8
- package/src/slash/delegation-request.ts +0 -4
- package/src/slash/slash-bridge.ts +1 -2
- package/src/slash/slash-commands.ts +369 -90
- package/src/slash/slash-live-state.ts +22 -11
- package/src/tui/fleet-status.ts +134 -75
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render-helpers.ts +31 -0
- package/src/tui/render.ts +895 -121
- package/src/watchdog/change-signature.ts +40 -1
- package/src/watchdog/turn-delta.ts +1 -1
- package/src/workflows/chat-progress.ts +6 -3
- package/src/workflows/host-command.ts +235 -0
- package/src/workflows/scripted-workflow.ts +503 -38
- package/src/workflows/workflow-child-summary.ts +9 -5
- package/src/workflows/workflow-preflight.ts +270 -0
- package/src/workflows/workflow-receipt.ts +43 -4
- package/src/workflows/workflow-settlement.ts +246 -0
- package/src/runs/shared/turn-budget.ts +0 -98
|
@@ -25,6 +25,8 @@ import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
|
|
|
25
25
|
import { EXTERNAL_JOB_BRIDGE_REQUEST_DIR, serviceExternalJobBridgeRequests } from "../shared/external-job-bridge.ts";
|
|
26
26
|
import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
|
|
27
27
|
import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
|
|
28
|
+
import { validHostStepNodes } from "../shared/host-step-status.ts";
|
|
29
|
+
import { withCachedUiContext } from "../../shared/extension-context.ts";
|
|
28
30
|
|
|
29
31
|
interface AsyncJobTrackerOptions {
|
|
30
32
|
completionRetentionMs?: number;
|
|
@@ -78,23 +80,19 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
78
80
|
const watch = options.watch ?? fs.watch;
|
|
79
81
|
const useNativeWatcher = () => shouldUseNativeFsWatch("async-job-tracker", options.platform);
|
|
80
82
|
const terminalStatus = (status: string) => status === "complete" || status === "failed" || status === "paused" || status === "stopped";
|
|
83
|
+
const withLastUiContext = <T>(run: (ctx: ExtensionContext) => T): T | undefined => {
|
|
84
|
+
const cached = state.lastUiContext;
|
|
85
|
+
return withCachedUiContext(cached, () => {
|
|
86
|
+
if (state.lastUiContext === cached) state.lastUiContext = null;
|
|
87
|
+
}, run);
|
|
88
|
+
};
|
|
81
89
|
const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
|
|
82
90
|
if (state.widgetsSuspended) return;
|
|
83
91
|
renderWidget(ctx, options.widgetEnabled === false ? [] : jobs);
|
|
84
92
|
(ctx.ui as { requestRender?: () => void }).requestRender?.();
|
|
85
93
|
};
|
|
86
94
|
const rerenderLastWidget = (jobs = Array.from(state.asyncJobs.values())) => {
|
|
87
|
-
|
|
88
|
-
if (!ctx) return;
|
|
89
|
-
try {
|
|
90
|
-
if (ctx.hasUI) rerenderWidget(ctx, jobs);
|
|
91
|
-
} catch (error) {
|
|
92
|
-
if (error instanceof Error && error.message.includes("extension ctx is stale")) {
|
|
93
|
-
state.lastUiContext = null;
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
throw error;
|
|
97
|
-
}
|
|
95
|
+
withLastUiContext((ctx) => rerenderWidget(ctx, jobs));
|
|
98
96
|
};
|
|
99
97
|
const requestLastWidgetRender = () => {
|
|
100
98
|
if (options.widgetEnabled === false) return;
|
|
@@ -147,6 +145,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
147
145
|
currentStep: run.currentStep,
|
|
148
146
|
chainStepCount: run.chainStepCount,
|
|
149
147
|
parallelGroups: groups,
|
|
148
|
+
hostSteps: run.hostSteps,
|
|
149
|
+
...(run.mode === "workflow" && run.workflowGraph ? { workflowGraph: run.workflowGraph } : {}),
|
|
150
|
+
preflight: run.preflight,
|
|
150
151
|
steps: visibleSteps,
|
|
151
152
|
stepsTotal: visibleSteps.length,
|
|
152
153
|
runningSteps: visibleSteps.filter((step) => step.status === "running").length,
|
|
@@ -342,7 +343,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
342
343
|
};
|
|
343
344
|
|
|
344
345
|
const refreshJob = (job: AsyncJobState): boolean => {
|
|
345
|
-
const widgetExpanded =
|
|
346
|
+
const widgetExpanded = withLastUiContext((ctx) => ctx.ui.getToolsExpanded?.() ?? false) ?? false;
|
|
346
347
|
const widgetStateBefore = widgetRenderKey(job, widgetExpanded);
|
|
347
348
|
let nestedRefreshFailed = false;
|
|
348
349
|
const refreshNestedProjection = () => {
|
|
@@ -399,7 +400,10 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
399
400
|
job.mode = status.mode;
|
|
400
401
|
job.parentWorkflowRunId = status.parentWorkflowRunId ?? job.parentWorkflowRunId;
|
|
401
402
|
job.workflowKey = status.workflowKey ?? job.workflowKey;
|
|
403
|
+
job.lane = status.lane ?? job.lane;
|
|
402
404
|
job.workflow = status.workflow ?? job.workflow;
|
|
405
|
+
if (status.mode === "workflow") job.workflowGraph = status.workflowGraph ?? job.workflowGraph;
|
|
406
|
+
job.hostSteps = validHostStepNodes(status.workflowGraph);
|
|
403
407
|
const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
|
|
404
408
|
if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error("workflowChildren.workflowRunId does not match async status runId.");
|
|
405
409
|
job.workflowChildren = workflowChildren ?? job.workflowChildren;
|
|
@@ -625,6 +629,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
625
629
|
turnBudget: info.turnBudget,
|
|
626
630
|
parentWorkflowRunId: info.parentWorkflowRunId,
|
|
627
631
|
workflowKey: info.workflowKey,
|
|
632
|
+
...(info.mode === "workflow" && info.workflowGraph ? { workflowGraph: info.workflowGraph } : {}),
|
|
628
633
|
controlEventCursor: 0,
|
|
629
634
|
});
|
|
630
635
|
const job = state.asyncJobs.get(info.id)!;
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { DIRS, type AcceptanceInput, type AsyncStatus, type
|
|
3
|
+
import { DIRS, type AcceptanceInput, type AsyncStatus, type SteeringRecoveryDescriptor, type SubagentRunMode } from "../../shared/types.ts";
|
|
4
4
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
5
5
|
import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
|
|
6
|
+
import { normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
|
|
6
7
|
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
7
8
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
8
9
|
import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
9
10
|
import { validateRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
10
|
-
import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
|
|
11
11
|
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
12
12
|
import { resultFilePath, resultPayloadPathForIndexedRun } from "./result-files.ts";
|
|
13
13
|
import { canScanAsyncRunPrefix, MIN_SAFE_ASYNC_RUN_PREFIX_LENGTH } from "./run-id-query.ts";
|
|
14
14
|
import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/parallel-handoff.ts";
|
|
15
15
|
import { intersectThinkingCeilings, parseThinkingLevel, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
|
|
16
|
+
import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
|
|
16
17
|
|
|
17
18
|
export interface AsyncResumeParams {
|
|
18
19
|
id?: string;
|
|
@@ -40,6 +41,8 @@ export type AsyncResumeTarget = {
|
|
|
40
41
|
state: AsyncStatus["state"];
|
|
41
42
|
mode?: SubagentRunMode;
|
|
42
43
|
agent: string;
|
|
44
|
+
/** Human-readable display name for the child session, when derived at launch. */
|
|
45
|
+
sessionName?: string;
|
|
43
46
|
index: number;
|
|
44
47
|
cwd?: string;
|
|
45
48
|
sessionFile?: string;
|
|
@@ -63,11 +66,12 @@ interface AsyncResultFile {
|
|
|
63
66
|
cwd?: string;
|
|
64
67
|
sessionId?: string;
|
|
65
68
|
sessionFile?: string;
|
|
69
|
+
sessionName?: string;
|
|
66
70
|
model?: string;
|
|
67
71
|
thinking?: string;
|
|
68
72
|
launchContractDigest?: string;
|
|
69
73
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
70
|
-
results?: Array<{ agent?: string; success?: boolean; sessionFile?: string; intercomTarget?: string; model?: string; thinking?: string; launchContractDigest?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling }>;
|
|
74
|
+
results?: Array<{ agent?: string; sessionName?: string; success?: boolean; sessionFile?: string; intercomTarget?: string; model?: string; thinking?: string; launchContractDigest?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling }>;
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
export interface AsyncRunLocation {
|
|
@@ -104,6 +108,7 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
|
|
|
104
108
|
const child = ensureObject(entry, `${resultPath} results[${index}]`);
|
|
105
109
|
const agent = validateOptionalString(child, "agent", resultPath, `results[${index}].agent`);
|
|
106
110
|
const sessionFile = validateOptionalString(child, "sessionFile", resultPath, `results[${index}].sessionFile`);
|
|
111
|
+
const sessionName = validateOptionalString(child, "sessionName", resultPath, `results[${index}].sessionName`);
|
|
107
112
|
const intercomTarget = validateOptionalString(child, "intercomTarget", resultPath, `results[${index}].intercomTarget`);
|
|
108
113
|
const model = validateOptionalString(child, "model", resultPath, `results[${index}].model`);
|
|
109
114
|
const thinking = validateOptionalString(child, "thinking", resultPath, `results[${index}].thinking`);
|
|
@@ -111,7 +116,7 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
|
|
|
111
116
|
const capabilityCeiling = child.capabilityCeiling === undefined ? undefined : parseSubagentCapabilityCeiling(child.capabilityCeiling, `async result file '${resultPath}' results[${index}].capabilityCeiling`);
|
|
112
117
|
const success = child.success;
|
|
113
118
|
if (success !== undefined && typeof success !== "boolean") throw new Error(`Invalid async result file '${resultPath}': results[${index}].success must be a boolean.`);
|
|
114
|
-
return { agent, sessionFile, intercomTarget, model, thinking, launchContractDigest, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(typeof success === "boolean" ? { success } : {}) };
|
|
119
|
+
return { agent, sessionName, sessionFile, intercomTarget, model, thinking, launchContractDigest, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(typeof success === "boolean" ? { success } : {}) };
|
|
115
120
|
});
|
|
116
121
|
}
|
|
117
122
|
const success = data.success;
|
|
@@ -248,7 +253,7 @@ export function resolveAsyncRunLocation(params: AsyncResumeParams, asyncDirRoot:
|
|
|
248
253
|
}
|
|
249
254
|
|
|
250
255
|
function resultState(result: AsyncResultFile): AsyncStatus["state"] {
|
|
251
|
-
if (result.state === "complete" || result.state === "failed" || result.state === "paused" || result.state === "stopped" || result.state === "running" || result.state === "queued") {
|
|
256
|
+
if (result.state === "complete" || result.state === "failed" || result.state === "partial" || result.state === "paused" || result.state === "stopped" || result.state === "running" || result.state === "queued") {
|
|
252
257
|
return result.state;
|
|
253
258
|
}
|
|
254
259
|
return result.success ? "complete" : "failed";
|
|
@@ -257,6 +262,7 @@ function resultState(result: AsyncResultFile): AsyncStatus["state"] {
|
|
|
257
262
|
function validateStatusForResume(status: AsyncStatus | null, source: string): void {
|
|
258
263
|
if (!status) return;
|
|
259
264
|
if (typeof status.runId !== "string") throw new Error(`Invalid async status '${source}': runId must be a string.`);
|
|
265
|
+
assertWorkflowGraphHostSteps(status.workflowGraph, source, status.runId);
|
|
260
266
|
if (status.sessionId !== undefined && typeof status.sessionId !== "string") throw new Error(`Invalid async status '${source}': sessionId must be a string.`);
|
|
261
267
|
if (status.cwd !== undefined && typeof status.cwd !== "string") throw new Error(`Invalid async status '${source}': cwd must be a string.`);
|
|
262
268
|
if (status.sessionFile !== undefined && typeof status.sessionFile !== "string") throw new Error(`Invalid async status '${source}': sessionFile must be a string.`);
|
|
@@ -268,6 +274,7 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
|
|
|
268
274
|
const stepRecord = step as Record<string, unknown>;
|
|
269
275
|
if (typeof stepRecord.agent !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].agent must be a string.`);
|
|
270
276
|
if (stepRecord.sessionFile !== undefined && typeof stepRecord.sessionFile !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionFile must be a string.`);
|
|
277
|
+
if (stepRecord.sessionName !== undefined && typeof stepRecord.sessionName !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionName must be a string.`);
|
|
271
278
|
if (stepRecord.model !== undefined && typeof stepRecord.model !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].model must be a string.`);
|
|
272
279
|
if (stepRecord.thinking !== undefined && typeof stepRecord.thinking !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].thinking must be a string.`);
|
|
273
280
|
if (stepRecord.thinkingCeiling !== undefined) stepRecord.thinkingCeiling = parseThinkingLevel(stepRecord.thinkingCeiling, `async status '${source}' steps[${index}].thinkingCeiling`);
|
|
@@ -283,23 +290,6 @@ function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): Ac
|
|
|
283
290
|
return value as AcceptanceInput;
|
|
284
291
|
}
|
|
285
292
|
|
|
286
|
-
function normalizeRecoveryTurnBudget(value: unknown, descriptorPath: string): ResolvedTurnBudget {
|
|
287
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
288
|
-
const {
|
|
289
|
-
outcome: _outcome,
|
|
290
|
-
turnCount: _turnCount,
|
|
291
|
-
wrapUpRequestedAtTurn: _wrapUpRequestedAtTurn,
|
|
292
|
-
terminationDeferredAtTurn: _terminationDeferredAtTurn,
|
|
293
|
-
exceededAtTurn: _exceededAtTurn,
|
|
294
|
-
...publicTurnBudget
|
|
295
|
-
} = value as Record<string, unknown>;
|
|
296
|
-
value = publicTurnBudget;
|
|
297
|
-
}
|
|
298
|
-
const result = resolveTurnBudgetConfig(value, "recoveryDescriptor.initialTurnBudget");
|
|
299
|
-
if (result.error || !result.turnBudget) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${result.error ?? "recoveryDescriptor.initialTurnBudget is invalid."}`);
|
|
300
|
-
return result.turnBudget;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
293
|
export function asyncReviveRequiresRecoveryDescriptor(target: Pick<AsyncResumeTarget, "recoveryDescriptor" | "mode" | "sessionFile">): boolean {
|
|
304
294
|
if (target.recoveryDescriptor) return false;
|
|
305
295
|
return !(target.mode === "workflow" && Boolean(target.sessionFile));
|
|
@@ -324,11 +314,11 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
324
314
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
|
|
325
315
|
const parsed = value as Record<string, unknown>;
|
|
326
316
|
const allowedFields = new Set([
|
|
327
|
-
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "fallbackModels", "thinking", "thinkingCeiling", "tools", "extensions",
|
|
317
|
+
"version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "fallbackModels", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
|
|
328
318
|
"subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
|
|
329
319
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
330
320
|
"artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
331
|
-
"launchResolvedExtensions", "runFanoutBudget",
|
|
321
|
+
"launchResolvedExtensions", "runFanoutBudget", "lane",
|
|
332
322
|
"extensionBindings",
|
|
333
323
|
]);
|
|
334
324
|
for (const field of Object.keys(parsed)) {
|
|
@@ -347,6 +337,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
347
337
|
if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
|
|
348
338
|
if (parsed.thinkingCeiling !== undefined) parsed.thinkingCeiling = parseThinkingLevel(parsed.thinkingCeiling, `async recovery descriptor '${descriptorPath}' thinkingCeiling`);
|
|
349
339
|
if (parsed.extensionBindings !== undefined) parsed.extensionBindings = normalizeExtensionBindings(parsed.extensionBindings)!.value;
|
|
340
|
+
if (parsed.lane !== undefined) parsed.lane = normalizeWorkflowLaneMetadata(parsed.lane, `Invalid async recovery descriptor '${descriptorPath}': lane`);
|
|
350
341
|
if (parsed.agentContract !== undefined) {
|
|
351
342
|
if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
|
|
352
343
|
const contract = parsed.agentContract as Record<string, unknown>;
|
|
@@ -361,6 +352,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
361
352
|
}
|
|
362
353
|
if (parsed.inheritGlobalContext === undefined) parsed.inheritGlobalContext = parsed.inheritProjectContext;
|
|
363
354
|
else if (typeof parsed.inheritGlobalContext !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': inheritGlobalContext must be a boolean.`);
|
|
355
|
+
if (parsed.allowNestedSubagents !== undefined && typeof parsed.allowNestedSubagents !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': allowNestedSubagents must be a boolean.`);
|
|
364
356
|
if (!Number.isInteger(parsed.maxSubagentDepth) || (parsed.maxSubagentDepth as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxSubagentDepth must be a non-negative integer.`);
|
|
365
357
|
for (const field of ["fallbackModels", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "mutationTools", "skills", "skillPath"] as const) {
|
|
366
358
|
const item = parsed[field];
|
|
@@ -379,7 +371,9 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
379
371
|
}
|
|
380
372
|
if (parsed.absoluteDeadlineAt !== undefined && (!Number.isFinite(parsed.absoluteDeadlineAt) || (parsed.absoluteDeadlineAt as number) <= 0)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': absoluteDeadlineAt must be a positive timestamp.`);
|
|
381
373
|
if (parsed.initialTurnBudget !== undefined) {
|
|
382
|
-
|
|
374
|
+
// Older descriptors may contain the removed turn-budget setting. Accept it
|
|
375
|
+
// for recovery compatibility, but do not restore or enforce it.
|
|
376
|
+
delete parsed.initialTurnBudget;
|
|
383
377
|
}
|
|
384
378
|
if (parsed.initialToolBudget !== undefined) {
|
|
385
379
|
const result = validateToolBudgetConfig(parsed.initialToolBudget, "recoveryDescriptor.initialToolBudget");
|
|
@@ -492,6 +486,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
492
486
|
state,
|
|
493
487
|
...(mode ? { mode } : {}),
|
|
494
488
|
agent: selectedStep.agent,
|
|
489
|
+
...(selectedStep.sessionName ?? result?.results?.[requestedIndex]?.sessionName ? { sessionName: selectedStep.sessionName ?? result?.results?.[requestedIndex]?.sessionName } : {}),
|
|
495
490
|
index: requestedIndex,
|
|
496
491
|
cwd: status?.cwd ?? result?.cwd,
|
|
497
492
|
sessionFile: selectedStep.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
|
|
@@ -523,6 +518,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
523
518
|
state,
|
|
524
519
|
...(mode ? { mode } : {}),
|
|
525
520
|
agent: selected.step.agent,
|
|
521
|
+
...(selected.step.sessionName ?? result?.results?.[selected.index]?.sessionName ? { sessionName: selected.step.sessionName ?? result?.results?.[selected.index]?.sessionName } : {}),
|
|
526
522
|
index: selected.index,
|
|
527
523
|
cwd: status?.cwd ?? result?.cwd,
|
|
528
524
|
sessionFile: selected.step.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
|
|
@@ -568,6 +564,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
|
|
|
568
564
|
state,
|
|
569
565
|
...(mode ? { mode } : {}),
|
|
570
566
|
agent,
|
|
567
|
+
...(statusSteps[index]?.sessionName ?? resultSteps[index]?.sessionName ? { sessionName: statusSteps[index]?.sessionName ?? resultSteps[index]?.sessionName } : {}),
|
|
571
568
|
index,
|
|
572
569
|
...(resumeCwd ? { cwd: resumeCwd } : {}),
|
|
573
570
|
...(resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {}),
|
|
@@ -591,6 +588,7 @@ export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descr
|
|
|
591
588
|
thinking: descriptor.thinking,
|
|
592
589
|
maxThinking: intersectThinkingCeilings(descriptor.thinkingCeiling, agentConfig.maxThinking),
|
|
593
590
|
tools: descriptor.tools ? [...descriptor.tools] : undefined,
|
|
591
|
+
allowNestedSubagents: descriptor.allowNestedSubagents,
|
|
594
592
|
extensions: descriptor.extensions ? [...descriptor.extensions] : undefined,
|
|
595
593
|
subagentOnlyExtensions: descriptor.subagentOnlyExtensions ? [...descriptor.subagentOnlyExtensions] : undefined,
|
|
596
594
|
mcpDirectTools: descriptor.mcpDirectTools ? [...descriptor.mcpDirectTools] : undefined,
|
|
@@ -1,268 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { AsyncJobState, SubagentState } from "../../shared/types.ts";
|
|
2
|
+
import {
|
|
3
|
+
projectAsyncStatusSnapshot,
|
|
4
|
+
type AsyncStatusSnapshotOptions,
|
|
5
|
+
type AsyncStatusSnapshotV1,
|
|
6
|
+
} from "../shared/async-status-projection.ts";
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
ASYNC_STATUS_SNAPSHOT_KIND,
|
|
10
|
+
ASYNC_STATUS_SNAPSHOT_VERSION,
|
|
11
|
+
} from "../shared/async-status-projection.ts";
|
|
12
|
+
export type {
|
|
13
|
+
AsyncStatusSnapshotActivityV1,
|
|
14
|
+
AsyncStatusSnapshotCapsV1,
|
|
15
|
+
AsyncStatusSnapshotHostStepV1,
|
|
16
|
+
AsyncStatusSnapshotKind,
|
|
17
|
+
AsyncStatusSnapshotNodeV1,
|
|
18
|
+
AsyncStatusSnapshotOmittedV1,
|
|
19
|
+
AsyncStatusSnapshotOptions,
|
|
20
|
+
AsyncStatusSnapshotState,
|
|
21
|
+
AsyncStatusSnapshotV1,
|
|
22
|
+
} from "../shared/async-status-projection.ts";
|
|
3
23
|
|
|
4
|
-
export const ASYNC_STATUS_SNAPSHOT_KIND = "pi-subagents.async-status-snapshot";
|
|
5
|
-
export const ASYNC_STATUS_SNAPSHOT_VERSION = 1;
|
|
6
24
|
export const ASYNC_STATUS_SNAPSHOT_WIDGET_PREFIX = "PI_SUBAGENT_ASYNC_JSON:";
|
|
7
25
|
|
|
8
|
-
const DEFAULT_MAX_RUNS = 20;
|
|
9
|
-
const DEFAULT_MAX_CHILDREN_PER_NODE = 8;
|
|
10
|
-
const DEFAULT_MAX_DEPTH = 3;
|
|
11
|
-
const DEFAULT_MAX_STRING_LENGTH = 160;
|
|
12
|
-
const DEFAULT_MAX_SERIALIZED_BYTES = 32 * 1024;
|
|
13
|
-
|
|
14
|
-
export type AsyncStatusSnapshotState = "queued" | "running" | "complete" | "failed" | "paused" | "stopped" | "rejected";
|
|
15
|
-
export type AsyncStatusSnapshotKind = "subagent" | "workflow" | "step";
|
|
16
|
-
|
|
17
|
-
export interface AsyncStatusSnapshotActivityV1 {
|
|
18
|
-
state?: string;
|
|
19
|
-
currentTool?: string;
|
|
20
|
-
lastActivityAt?: number;
|
|
21
|
-
currentToolStartedAt?: number;
|
|
22
|
-
turnCount?: number;
|
|
23
|
-
toolCount?: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface AsyncStatusSnapshotNodeV1 {
|
|
27
|
-
id: string;
|
|
28
|
-
kind: AsyncStatusSnapshotKind;
|
|
29
|
-
label: string;
|
|
30
|
-
state: AsyncStatusSnapshotState;
|
|
31
|
-
startedAt?: number;
|
|
32
|
-
updatedAt?: number;
|
|
33
|
-
endedAt?: number;
|
|
34
|
-
activity?: AsyncStatusSnapshotActivityV1;
|
|
35
|
-
children?: AsyncStatusSnapshotNodeV1[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface AsyncStatusSnapshotCapsV1 {
|
|
39
|
-
maxRuns: number;
|
|
40
|
-
maxChildrenPerNode: number;
|
|
41
|
-
maxDepth: number;
|
|
42
|
-
maxStringLength: number;
|
|
43
|
-
maxSerializedBytes: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface AsyncStatusSnapshotOmittedV1 {
|
|
47
|
-
runs: number;
|
|
48
|
-
children: number;
|
|
49
|
-
byteLimitExceeded: boolean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface AsyncStatusSnapshotV1 {
|
|
53
|
-
kind: typeof ASYNC_STATUS_SNAPSHOT_KIND;
|
|
54
|
-
version: typeof ASYNC_STATUS_SNAPSHOT_VERSION;
|
|
55
|
-
generatedAt: number;
|
|
56
|
-
caps: AsyncStatusSnapshotCapsV1;
|
|
57
|
-
omitted: AsyncStatusSnapshotOmittedV1;
|
|
58
|
-
runs: AsyncStatusSnapshotNodeV1[];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface AsyncStatusSnapshotOptions {
|
|
62
|
-
generatedAt?: number;
|
|
63
|
-
maxRuns?: number;
|
|
64
|
-
maxChildrenPerNode?: number;
|
|
65
|
-
maxDepth?: number;
|
|
66
|
-
maxStringLength?: number;
|
|
67
|
-
maxSerializedBytes?: number;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
interface BuildContext {
|
|
71
|
-
caps: AsyncStatusSnapshotCapsV1;
|
|
72
|
-
omitted: AsyncStatusSnapshotOmittedV1;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function resolveCaps(options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotCapsV1 {
|
|
76
|
-
return {
|
|
77
|
-
maxRuns: Math.max(0, Math.floor(options.maxRuns ?? DEFAULT_MAX_RUNS)),
|
|
78
|
-
maxChildrenPerNode: Math.max(0, Math.floor(options.maxChildrenPerNode ?? DEFAULT_MAX_CHILDREN_PER_NODE)),
|
|
79
|
-
maxDepth: Math.max(0, Math.floor(options.maxDepth ?? DEFAULT_MAX_DEPTH)),
|
|
80
|
-
maxStringLength: Math.max(0, Math.floor(options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH)),
|
|
81
|
-
maxSerializedBytes: Math.max(256, Math.floor(options.maxSerializedBytes ?? DEFAULT_MAX_SERIALIZED_BYTES)),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function publicText(value: unknown, fallback: string, maxLength: number): string {
|
|
86
|
-
if (typeof value !== "string") return fallback;
|
|
87
|
-
const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
|
|
88
|
-
return truncateDisplayText(normalized || fallback, maxLength);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function publicOptionalText(value: unknown, maxLength: number): string | undefined {
|
|
92
|
-
if (typeof value !== "string") return undefined;
|
|
93
|
-
const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
|
|
94
|
-
return normalized ? truncateDisplayText(normalized, maxLength) : undefined;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function publicTime(value: unknown): number | undefined {
|
|
98
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function publicCount(value: unknown): number | undefined {
|
|
102
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function normalizeState(value: string): AsyncStatusSnapshotState {
|
|
106
|
-
if (value === "completed") return "complete";
|
|
107
|
-
if (value === "pending") return "queued";
|
|
108
|
-
return value as AsyncStatusSnapshotState;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function terminalState(state: AsyncStatusSnapshotState): boolean {
|
|
112
|
-
return state === "complete" || state === "failed" || state === "paused" || state === "stopped" || state === "rejected";
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function kindForMode(mode: SubagentRunMode | undefined): AsyncStatusSnapshotKind {
|
|
116
|
-
return mode === "workflow" ? "workflow" : "subagent";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function labelForAgents(agents: readonly string[] | undefined, fallback: string, maxLength: number): string {
|
|
120
|
-
if (!agents?.length) return publicText(fallback, fallback, maxLength);
|
|
121
|
-
const visible = agents.slice(0, 3).map((agent) => publicText(agent, "agent", maxLength)).join(", ");
|
|
122
|
-
const suffix = agents.length > 3 ? `, +${agents.length - 3} more` : "";
|
|
123
|
-
return truncateDisplayText(`${visible}${suffix}`, maxLength);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function activityFor(source: {
|
|
127
|
-
activityState?: unknown;
|
|
128
|
-
currentTool?: unknown;
|
|
129
|
-
lastActivityAt?: unknown;
|
|
130
|
-
currentToolStartedAt?: unknown;
|
|
131
|
-
turnCount?: unknown;
|
|
132
|
-
toolCount?: unknown;
|
|
133
|
-
}, ctx: BuildContext): AsyncStatusSnapshotActivityV1 | undefined {
|
|
134
|
-
const activity: AsyncStatusSnapshotActivityV1 = {
|
|
135
|
-
...(typeof source.activityState === "string" ? { state: publicText(source.activityState, "unknown", ctx.caps.maxStringLength) } : {}),
|
|
136
|
-
...(publicOptionalText(source.currentTool, ctx.caps.maxStringLength) ? { currentTool: publicOptionalText(source.currentTool, ctx.caps.maxStringLength) } : {}),
|
|
137
|
-
...(publicTime(source.lastActivityAt) !== undefined ? { lastActivityAt: publicTime(source.lastActivityAt) } : {}),
|
|
138
|
-
...(publicTime(source.currentToolStartedAt) !== undefined ? { currentToolStartedAt: publicTime(source.currentToolStartedAt) } : {}),
|
|
139
|
-
...(publicCount(source.turnCount) !== undefined ? { turnCount: publicCount(source.turnCount) } : {}),
|
|
140
|
-
...(publicCount(source.toolCount) !== undefined ? { toolCount: publicCount(source.toolCount) } : {}),
|
|
141
|
-
};
|
|
142
|
-
return Object.keys(activity).length ? activity : undefined;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function appendBoundedChildren(children: AsyncStatusSnapshotNodeV1[], source: readonly AsyncStatusSnapshotNodeV1[], ctx: BuildContext): void {
|
|
146
|
-
const remaining = Math.max(0, ctx.caps.maxChildrenPerNode - children.length);
|
|
147
|
-
children.push(...source.slice(0, remaining));
|
|
148
|
-
ctx.omitted.children += Math.max(0, source.length - remaining);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function buildStepNode(step: AsyncJobStep | NestedStepSummary, index: number, depth: number, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
|
|
152
|
-
const state = normalizeState(step.status);
|
|
153
|
-
const updatedAt = publicTime(step.endedAt) ?? publicTime(step.lastActivityAt) ?? publicTime(step.startedAt);
|
|
154
|
-
const node: AsyncStatusSnapshotNodeV1 = {
|
|
155
|
-
id: publicText("workflowKey" in step && step.workflowKey ? step.workflowKey : "runId" in step && step.runId ? step.runId : `step:${index}`, `step:${index}`, ctx.caps.maxStringLength),
|
|
156
|
-
kind: "step",
|
|
157
|
-
label: publicText("label" in step && step.label ? step.label : step.agent, "step", ctx.caps.maxStringLength),
|
|
158
|
-
state,
|
|
159
|
-
...(publicTime(step.startedAt) !== undefined ? { startedAt: publicTime(step.startedAt) } : {}),
|
|
160
|
-
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
161
|
-
...(terminalState(state) && publicTime(step.endedAt) !== undefined ? { endedAt: publicTime(step.endedAt) } : {}),
|
|
162
|
-
...(activityFor(step, ctx) ? { activity: activityFor(step, ctx) } : {}),
|
|
163
|
-
};
|
|
164
|
-
if (depth < ctx.caps.maxDepth && step.children?.length) {
|
|
165
|
-
const nested = step.children.map((child, childIndex) => buildNestedNode(child, childIndex, depth + 1, ctx));
|
|
166
|
-
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
167
|
-
appendBoundedChildren(bounded, nested, ctx);
|
|
168
|
-
if (bounded.length) node.children = bounded;
|
|
169
|
-
} else if (step.children?.length) {
|
|
170
|
-
ctx.omitted.children += step.children.length;
|
|
171
|
-
}
|
|
172
|
-
return node;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function buildNestedNode(child: NestedRunSummary, index: number, depth: number, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
|
|
176
|
-
const state = normalizeState(child.state);
|
|
177
|
-
const updatedAt = publicTime(child.lastUpdate) ?? publicTime(child.endedAt) ?? publicTime(child.lastActivityAt) ?? publicTime(child.startedAt);
|
|
178
|
-
const node: AsyncStatusSnapshotNodeV1 = {
|
|
179
|
-
id: publicText(child.id, `nested:${index}`, ctx.caps.maxStringLength),
|
|
180
|
-
kind: kindForMode(child.mode),
|
|
181
|
-
label: child.agent ? publicText(child.agent, "subagent", ctx.caps.maxStringLength) : labelForAgents(child.agents, child.mode ?? "subagent", ctx.caps.maxStringLength),
|
|
182
|
-
state,
|
|
183
|
-
...(publicTime(child.startedAt) !== undefined ? { startedAt: publicTime(child.startedAt) } : {}),
|
|
184
|
-
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
185
|
-
...(terminalState(state) && publicTime(child.endedAt) !== undefined ? { endedAt: publicTime(child.endedAt) } : {}),
|
|
186
|
-
...(activityFor(child, ctx) ? { activity: activityFor(child, ctx) } : {}),
|
|
187
|
-
};
|
|
188
|
-
if (depth < ctx.caps.maxDepth) {
|
|
189
|
-
const nestedSteps = child.steps?.map((step, stepIndex) => buildStepNode(step, stepIndex, depth + 1, ctx)) ?? [];
|
|
190
|
-
const nestedChildren = child.children?.map((nested, childIndex) => buildNestedNode(nested, childIndex, depth + 1, ctx)) ?? [];
|
|
191
|
-
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
192
|
-
appendBoundedChildren(bounded, [...nestedSteps, ...nestedChildren], ctx);
|
|
193
|
-
if (bounded.length) node.children = bounded;
|
|
194
|
-
} else {
|
|
195
|
-
ctx.omitted.children += (child.steps?.length ?? 0) + (child.children?.length ?? 0);
|
|
196
|
-
}
|
|
197
|
-
return node;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function buildRunNode(job: AsyncJobState, ctx: BuildContext): AsyncStatusSnapshotNodeV1 {
|
|
201
|
-
const state = normalizeState(job.status);
|
|
202
|
-
const updatedAt = publicTime(job.updatedAt) ?? publicTime(job.startedAt);
|
|
203
|
-
const node: AsyncStatusSnapshotNodeV1 = {
|
|
204
|
-
id: publicText(job.asyncId, "async", ctx.caps.maxStringLength),
|
|
205
|
-
kind: kindForMode(job.mode),
|
|
206
|
-
label: labelForAgents(job.agents, job.mode ?? "subagent", ctx.caps.maxStringLength),
|
|
207
|
-
state,
|
|
208
|
-
...(publicTime(job.startedAt) !== undefined ? { startedAt: publicTime(job.startedAt) } : {}),
|
|
209
|
-
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
210
|
-
...(terminalState(state) && updatedAt !== undefined ? { endedAt: updatedAt } : {}),
|
|
211
|
-
...(activityFor(job, ctx) ? { activity: activityFor(job, ctx) } : {}),
|
|
212
|
-
};
|
|
213
|
-
if (ctx.caps.maxDepth > 0) {
|
|
214
|
-
const stepChildren = job.steps?.map((step, index) => buildStepNode(step, step.index ?? index, 1, ctx)) ?? [];
|
|
215
|
-
const nestedChildren = job.nestedChildren?.map((child, index) => buildNestedNode(child, index, 1, ctx)) ?? [];
|
|
216
|
-
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
217
|
-
appendBoundedChildren(bounded, [...stepChildren, ...nestedChildren], ctx);
|
|
218
|
-
if (bounded.length) node.children = bounded;
|
|
219
|
-
} else {
|
|
220
|
-
ctx.omitted.children += (job.steps?.length ?? 0) + (job.nestedChildren?.length ?? 0);
|
|
221
|
-
}
|
|
222
|
-
return node;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function snapshotBytes(snapshot: AsyncStatusSnapshotV1): number {
|
|
226
|
-
return Buffer.byteLength(JSON.stringify(snapshot), "utf8");
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function enforceByteLimit(snapshot: AsyncStatusSnapshotV1): void {
|
|
230
|
-
if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) return;
|
|
231
|
-
snapshot.omitted.byteLimitExceeded = true;
|
|
232
|
-
const runs = snapshot.runs;
|
|
233
|
-
const initialOmittedRuns = snapshot.omitted.runs;
|
|
234
|
-
let lower = 0;
|
|
235
|
-
let upper = Math.max(0, runs.length - 1);
|
|
236
|
-
while (lower < upper) {
|
|
237
|
-
const retained = Math.ceil((lower + upper) / 2);
|
|
238
|
-
snapshot.runs = runs.slice(0, retained);
|
|
239
|
-
snapshot.omitted.runs = initialOmittedRuns + runs.length - retained;
|
|
240
|
-
if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) lower = retained;
|
|
241
|
-
else upper = retained - 1;
|
|
242
|
-
}
|
|
243
|
-
snapshot.runs = runs.slice(0, lower);
|
|
244
|
-
snapshot.omitted.runs = initialOmittedRuns + runs.length - lower;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
26
|
export function buildAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
|
|
248
|
-
|
|
249
|
-
const ctx: BuildContext = { caps, omitted: { runs: 0, children: 0, byteLimitExceeded: false } };
|
|
250
|
-
const sorted = [...jobs].sort((left, right) => {
|
|
251
|
-
const leftUpdated = left.updatedAt ?? left.startedAt ?? 0;
|
|
252
|
-
const rightUpdated = right.updatedAt ?? right.startedAt ?? 0;
|
|
253
|
-
return rightUpdated - leftUpdated || left.asyncId.localeCompare(right.asyncId);
|
|
254
|
-
});
|
|
255
|
-
ctx.omitted.runs += Math.max(0, sorted.length - caps.maxRuns);
|
|
256
|
-
const snapshot: AsyncStatusSnapshotV1 = {
|
|
257
|
-
kind: ASYNC_STATUS_SNAPSHOT_KIND,
|
|
258
|
-
version: ASYNC_STATUS_SNAPSHOT_VERSION,
|
|
259
|
-
generatedAt: options.generatedAt ?? Date.now(),
|
|
260
|
-
caps,
|
|
261
|
-
omitted: ctx.omitted,
|
|
262
|
-
runs: sorted.slice(0, caps.maxRuns).map((job) => buildRunNode(job, ctx)),
|
|
263
|
-
};
|
|
264
|
-
enforceByteLimit(snapshot);
|
|
265
|
-
return snapshot;
|
|
27
|
+
return projectAsyncStatusSnapshot(jobs, options);
|
|
266
28
|
}
|
|
267
29
|
|
|
268
30
|
export function asyncStatusSnapshotJobsForState(state: SubagentState | undefined, sessionId: string | null | undefined): AsyncJobState[] {
|