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
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { sanitizeDisplayText, truncateDisplayText } from "../../shared/display-text.ts";
|
|
2
|
+
import { formatModelThinking } from "../../shared/formatters.ts";
|
|
3
|
+
import type { AsyncJobState, AsyncJobStep, HostStepFreshnessV1, HostStepMonitorKind, HostStepNodeV1, HostStepState, HostStepVerdict, NestedRunSummary, NestedStepSummary, SubagentRunMode, WorkflowGraphSnapshot, WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../../shared/types.ts";
|
|
4
|
+
import { HOST_STEP_MAX_COUNT, HOST_STEP_MAX_DETAIL_CHARS, HOST_STEP_MAX_LABEL_CHARS, HOST_STEP_MAX_PROVIDER_CHARS, HOST_STEP_MAX_REASON_CHARS, HOST_STEP_MAX_REF_CHARS, HOST_STEP_MAX_ROLE_CHARS, HOST_STEP_MAX_TARGET_CHARS, hostStepReportName, parseHostStepNode, validHostStepNodes } from "./host-step-status.ts";
|
|
5
|
+
import { workflowGraphStageNodes } from "./workflow-graph.ts";
|
|
6
|
+
|
|
7
|
+
export const ASYNC_STATUS_SNAPSHOT_KIND = "pi-subagents.async-status-snapshot";
|
|
8
|
+
export const ASYNC_STATUS_SNAPSHOT_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MAX_RUNS = 20;
|
|
11
|
+
const DEFAULT_MAX_CHILDREN_PER_NODE = 8;
|
|
12
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
13
|
+
const DEFAULT_MAX_STRING_LENGTH = 160;
|
|
14
|
+
const DEFAULT_MAX_SERIALIZED_BYTES = 32 * 1024;
|
|
15
|
+
|
|
16
|
+
export type AsyncStatusSnapshotState = "queued" | "running" | "complete" | "failed" | "partial" | "paused" | "stopped" | "rejected";
|
|
17
|
+
export type AsyncStatusSnapshotKind = "subagent" | "workflow" | "step";
|
|
18
|
+
|
|
19
|
+
const ASYNC_STATUS_SNAPSHOT_STATES: Record<AsyncStatusSnapshotState, true> = {
|
|
20
|
+
queued: true,
|
|
21
|
+
running: true,
|
|
22
|
+
complete: true,
|
|
23
|
+
failed: true,
|
|
24
|
+
partial: true,
|
|
25
|
+
paused: true,
|
|
26
|
+
stopped: true,
|
|
27
|
+
rejected: true,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function isAsyncStatusSnapshotState(value: string): value is AsyncStatusSnapshotState {
|
|
31
|
+
return Object.hasOwn(ASYNC_STATUS_SNAPSHOT_STATES, value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AsyncStatusSnapshotActivityV1 {
|
|
35
|
+
state?: string;
|
|
36
|
+
currentTool?: string;
|
|
37
|
+
lastActivityAt?: number;
|
|
38
|
+
currentToolStartedAt?: number;
|
|
39
|
+
turnCount?: number;
|
|
40
|
+
toolCount?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AsyncStatusSnapshotHostStepV1 {
|
|
44
|
+
kind: HostStepMonitorKind;
|
|
45
|
+
provider?: string;
|
|
46
|
+
role?: string;
|
|
47
|
+
state: HostStepState;
|
|
48
|
+
verdict?: HostStepVerdict;
|
|
49
|
+
reasonCode?: string;
|
|
50
|
+
detail?: string;
|
|
51
|
+
target?: string;
|
|
52
|
+
stale?: boolean;
|
|
53
|
+
report?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AsyncStatusSnapshotNodeV1 {
|
|
57
|
+
id: string;
|
|
58
|
+
kind: AsyncStatusSnapshotKind | "host-step";
|
|
59
|
+
label: string;
|
|
60
|
+
state: AsyncStatusSnapshotState;
|
|
61
|
+
startedAt?: number;
|
|
62
|
+
updatedAt?: number;
|
|
63
|
+
endedAt?: number;
|
|
64
|
+
activity?: AsyncStatusSnapshotActivityV1;
|
|
65
|
+
hostStep?: AsyncStatusSnapshotHostStepV1;
|
|
66
|
+
children?: AsyncStatusSnapshotNodeV1[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AsyncStatusSnapshotCapsV1 {
|
|
70
|
+
maxRuns: number;
|
|
71
|
+
maxChildrenPerNode: number;
|
|
72
|
+
maxDepth: number;
|
|
73
|
+
maxStringLength: number;
|
|
74
|
+
maxSerializedBytes: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface AsyncStatusSnapshotOmittedV1 {
|
|
78
|
+
runs: number;
|
|
79
|
+
children: number;
|
|
80
|
+
byteLimitExceeded: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface AsyncStatusSnapshotV1 {
|
|
84
|
+
kind: typeof ASYNC_STATUS_SNAPSHOT_KIND;
|
|
85
|
+
version: typeof ASYNC_STATUS_SNAPSHOT_VERSION;
|
|
86
|
+
generatedAt: number;
|
|
87
|
+
caps: AsyncStatusSnapshotCapsV1;
|
|
88
|
+
omitted: AsyncStatusSnapshotOmittedV1;
|
|
89
|
+
runs: AsyncStatusSnapshotNodeV1[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface AsyncStatusSnapshotOptions {
|
|
93
|
+
generatedAt?: number;
|
|
94
|
+
maxRuns?: number;
|
|
95
|
+
maxChildrenPerNode?: number;
|
|
96
|
+
maxDepth?: number;
|
|
97
|
+
maxStringLength?: number;
|
|
98
|
+
maxSerializedBytes?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface AsyncStatusWorkflowRow {
|
|
102
|
+
name: string;
|
|
103
|
+
state: AsyncJobStep["status"] | HostStepState | "planned";
|
|
104
|
+
/** Present only for typed host-owned monitor rows; legacy child rows omit it. */
|
|
105
|
+
kind?: HostStepMonitorKind;
|
|
106
|
+
context?: AsyncJobStep["context"];
|
|
107
|
+
modelThinking?: string;
|
|
108
|
+
activity?: string;
|
|
109
|
+
startedAt?: number;
|
|
110
|
+
tokens?: number;
|
|
111
|
+
window?: number;
|
|
112
|
+
overflow?: number;
|
|
113
|
+
provider?: string;
|
|
114
|
+
role?: string;
|
|
115
|
+
verdict?: HostStepVerdict;
|
|
116
|
+
reasonCode?: string;
|
|
117
|
+
detail?: string;
|
|
118
|
+
target?: string;
|
|
119
|
+
freshness?: HostStepFreshnessV1;
|
|
120
|
+
reportPath?: string;
|
|
121
|
+
preflight?: WorkflowPreflightLaneV1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface ProjectionContext {
|
|
125
|
+
caps: AsyncStatusSnapshotCapsV1;
|
|
126
|
+
omitted: AsyncStatusSnapshotOmittedV1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function validHostStepList(source: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | undefined): HostStepNodeV1[] {
|
|
130
|
+
if (source && "nodes" in source) return validHostStepNodes(source);
|
|
131
|
+
const hostSteps: HostStepNodeV1[] = [];
|
|
132
|
+
for (const [index, value] of (source ?? []).slice(0, HOST_STEP_MAX_COUNT).entries()) {
|
|
133
|
+
try {
|
|
134
|
+
hostSteps.push(parseHostStepNode(value, `hostSteps[${index}]`));
|
|
135
|
+
} catch {
|
|
136
|
+
// Renderers must not turn malformed host data into a fake child row.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return hostSteps;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolveCaps(options: AsyncStatusSnapshotOptions): AsyncStatusSnapshotCapsV1 {
|
|
143
|
+
return {
|
|
144
|
+
maxRuns: Math.max(0, Math.floor(options.maxRuns ?? DEFAULT_MAX_RUNS)),
|
|
145
|
+
maxChildrenPerNode: Math.max(0, Math.floor(options.maxChildrenPerNode ?? DEFAULT_MAX_CHILDREN_PER_NODE)),
|
|
146
|
+
maxDepth: Math.max(0, Math.floor(options.maxDepth ?? DEFAULT_MAX_DEPTH)),
|
|
147
|
+
maxStringLength: Math.max(0, Math.floor(options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH)),
|
|
148
|
+
maxSerializedBytes: Math.max(256, Math.floor(options.maxSerializedBytes ?? DEFAULT_MAX_SERIALIZED_BYTES)),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function publicText(value: unknown, fallback: string, maxLength: number): string {
|
|
153
|
+
if (typeof value !== "string") return fallback;
|
|
154
|
+
const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
|
|
155
|
+
return truncateDisplayText(normalized || fallback, maxLength);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function publicOptionalText(value: unknown, maxLength: number): string | undefined {
|
|
159
|
+
if (typeof value !== "string") return undefined;
|
|
160
|
+
const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
|
|
161
|
+
return normalized ? truncateDisplayText(normalized, maxLength) : undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function publicTime(value: unknown): number | undefined {
|
|
165
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function publicCount(value: unknown): number | undefined {
|
|
169
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeState(value: string): AsyncStatusSnapshotState {
|
|
173
|
+
if (value === "completed") return "complete";
|
|
174
|
+
if (value === "pending") return "queued";
|
|
175
|
+
if (isAsyncStatusSnapshotState(value)) return value;
|
|
176
|
+
return "partial";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function terminalState(state: AsyncStatusSnapshotState): boolean {
|
|
180
|
+
return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped" || state === "rejected";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function kindForMode(mode: SubagentRunMode | undefined): AsyncStatusSnapshotKind {
|
|
184
|
+
return mode === "workflow" ? "workflow" : "subagent";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function labelForAgents(agents: readonly string[] | undefined, fallback: string, maxLength: number): string {
|
|
188
|
+
if (!agents?.length) return publicText(fallback, fallback, maxLength);
|
|
189
|
+
const visible = agents.slice(0, 3).map((agent) => publicText(agent, "agent", maxLength)).join(", ");
|
|
190
|
+
const suffix = agents.length > 3 ? `, +${agents.length - 3} more` : "";
|
|
191
|
+
return truncateDisplayText(`${visible}${suffix}`, maxLength);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function activityFor(source: {
|
|
195
|
+
activityState?: unknown;
|
|
196
|
+
currentTool?: unknown;
|
|
197
|
+
lastActivityAt?: unknown;
|
|
198
|
+
currentToolStartedAt?: unknown;
|
|
199
|
+
turnCount?: unknown;
|
|
200
|
+
toolCount?: unknown;
|
|
201
|
+
}, ctx: ProjectionContext): AsyncStatusSnapshotActivityV1 | undefined {
|
|
202
|
+
const currentTool = publicOptionalText(source.currentTool, ctx.caps.maxStringLength);
|
|
203
|
+
const lastActivityAt = publicTime(source.lastActivityAt);
|
|
204
|
+
const currentToolStartedAt = publicTime(source.currentToolStartedAt);
|
|
205
|
+
const turnCount = publicCount(source.turnCount);
|
|
206
|
+
const toolCount = publicCount(source.toolCount);
|
|
207
|
+
const activity: AsyncStatusSnapshotActivityV1 = {
|
|
208
|
+
...(typeof source.activityState === "string" ? { state: publicText(source.activityState, "unknown", ctx.caps.maxStringLength) } : {}),
|
|
209
|
+
...(currentTool ? { currentTool } : {}),
|
|
210
|
+
...(lastActivityAt !== undefined ? { lastActivityAt } : {}),
|
|
211
|
+
...(currentToolStartedAt !== undefined ? { currentToolStartedAt } : {}),
|
|
212
|
+
...(turnCount !== undefined ? { turnCount } : {}),
|
|
213
|
+
...(toolCount !== undefined ? { toolCount } : {}),
|
|
214
|
+
};
|
|
215
|
+
return Object.keys(activity).length ? activity : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function appendBoundedChildren(children: AsyncStatusSnapshotNodeV1[], source: readonly AsyncStatusSnapshotNodeV1[], ctx: ProjectionContext): void {
|
|
219
|
+
const remaining = Math.max(0, ctx.caps.maxChildrenPerNode - children.length);
|
|
220
|
+
children.push(...source.slice(0, remaining));
|
|
221
|
+
ctx.omitted.children += Math.max(0, source.length - remaining);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
|
|
225
|
+
const state = normalizeState(step.status);
|
|
226
|
+
const startedAt = publicTime(step.startedAt);
|
|
227
|
+
const endedAt = publicTime(step.endedAt);
|
|
228
|
+
const updatedAt = endedAt ?? publicTime(step.lastActivityAt) ?? startedAt;
|
|
229
|
+
const activity = activityFor(step, ctx);
|
|
230
|
+
const node: AsyncStatusSnapshotNodeV1 = {
|
|
231
|
+
id: publicText("workflowKey" in step && step.workflowKey ? step.workflowKey : "runId" in step && step.runId ? step.runId : `step:${index}`, `step:${index}`, ctx.caps.maxStringLength),
|
|
232
|
+
kind: "step",
|
|
233
|
+
label: publicText("label" in step && step.label ? step.label : step.agent, "step", ctx.caps.maxStringLength),
|
|
234
|
+
state,
|
|
235
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
236
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
237
|
+
...(terminalState(state) && endedAt !== undefined ? { endedAt } : {}),
|
|
238
|
+
...(activity ? { activity } : {}),
|
|
239
|
+
};
|
|
240
|
+
if (depth < ctx.caps.maxDepth && step.children?.length) {
|
|
241
|
+
const nested = step.children.map((child, childIndex) => projectNestedRun(child, childIndex, depth + 1, ctx));
|
|
242
|
+
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
243
|
+
appendBoundedChildren(bounded, nested, ctx);
|
|
244
|
+
if (bounded.length) node.children = bounded;
|
|
245
|
+
} else if (step.children?.length) {
|
|
246
|
+
ctx.omitted.children += step.children.length;
|
|
247
|
+
}
|
|
248
|
+
return node;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function projectWorkflowGraphNode(node: WorkflowGraphSnapshot["nodes"][number], index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
|
|
252
|
+
const step: AsyncJobStep = {
|
|
253
|
+
agent: node.agent ?? node.label,
|
|
254
|
+
status: workflowGraphStepStatus(node.status),
|
|
255
|
+
workflowKey: node.id,
|
|
256
|
+
label: node.label,
|
|
257
|
+
};
|
|
258
|
+
return projectStep(step, node.flatIndex ?? index, depth, ctx);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function projectNestedRun(child: NestedRunSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
|
|
262
|
+
const state = normalizeState(child.state);
|
|
263
|
+
const startedAt = publicTime(child.startedAt);
|
|
264
|
+
const endedAt = publicTime(child.endedAt);
|
|
265
|
+
const updatedAt = publicTime(child.lastUpdate) ?? endedAt ?? publicTime(child.lastActivityAt) ?? startedAt;
|
|
266
|
+
const activity = activityFor(child, ctx);
|
|
267
|
+
const node: AsyncStatusSnapshotNodeV1 = {
|
|
268
|
+
id: publicText(child.id, `nested:${index}`, ctx.caps.maxStringLength),
|
|
269
|
+
kind: kindForMode(child.mode),
|
|
270
|
+
label: child.agent ? publicText(child.agent, "subagent", ctx.caps.maxStringLength) : labelForAgents(child.agents, child.mode ?? "subagent", ctx.caps.maxStringLength),
|
|
271
|
+
state,
|
|
272
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
273
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
274
|
+
...(terminalState(state) && endedAt !== undefined ? { endedAt } : {}),
|
|
275
|
+
...(activity ? { activity } : {}),
|
|
276
|
+
};
|
|
277
|
+
if (depth < ctx.caps.maxDepth) {
|
|
278
|
+
const nestedSteps = child.steps?.map((step, stepIndex) => projectStep(step, stepIndex, depth + 1, ctx)) ?? [];
|
|
279
|
+
const nestedChildren = child.children?.map((nested, childIndex) => projectNestedRun(nested, childIndex, depth + 1, ctx)) ?? [];
|
|
280
|
+
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
281
|
+
appendBoundedChildren(bounded, [...nestedSteps, ...nestedChildren], ctx);
|
|
282
|
+
if (bounded.length) node.children = bounded;
|
|
283
|
+
} else {
|
|
284
|
+
ctx.omitted.children += (child.steps?.length ?? 0) + (child.children?.length ?? 0);
|
|
285
|
+
}
|
|
286
|
+
return node;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function hostStepSnapshotState(state: HostStepState, verdict: HostStepVerdict | undefined): AsyncStatusSnapshotState {
|
|
290
|
+
if (state === "pending") return "queued";
|
|
291
|
+
if (state === "running") return "running";
|
|
292
|
+
if (state === "cancelled") return "stopped";
|
|
293
|
+
if (state === "error" || verdict === "fail") return "failed";
|
|
294
|
+
return verdict === undefined || verdict === "inconclusive" ? "partial" : "complete";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function projectHostStep(hostStep: HostStepNodeV1, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
|
|
298
|
+
const state = hostStepSnapshotState(hostStep.state, hostStep.verdict);
|
|
299
|
+
const detail = publicOptionalText(hostStep.detail, ctx.caps.maxStringLength);
|
|
300
|
+
const report = publicOptionalText(hostStepReportName(hostStep.reportPath), ctx.caps.maxStringLength);
|
|
301
|
+
const hostMetadata: AsyncStatusSnapshotHostStepV1 = {
|
|
302
|
+
kind: hostStep.monitorKind,
|
|
303
|
+
state: hostStep.state,
|
|
304
|
+
...(hostStep.provider ? { provider: publicText(hostStep.provider, "provider", ctx.caps.maxStringLength) } : {}),
|
|
305
|
+
...(hostStep.role ? { role: publicText(hostStep.role, "role", ctx.caps.maxStringLength) } : {}),
|
|
306
|
+
...(hostStep.verdict ? { verdict: hostStep.verdict } : {}),
|
|
307
|
+
...(hostStep.reasonCode ? { reasonCode: publicText(hostStep.reasonCode, "reason", ctx.caps.maxStringLength) } : {}),
|
|
308
|
+
...(detail ? { detail } : {}),
|
|
309
|
+
...(hostStep.target ? { target: publicText(hostStep.target, "target", ctx.caps.maxStringLength) } : {}),
|
|
310
|
+
...(hostStep.freshness?.stale !== undefined ? { stale: hostStep.freshness.stale } : {}),
|
|
311
|
+
...(report ? { report } : {}),
|
|
312
|
+
};
|
|
313
|
+
return {
|
|
314
|
+
id: publicText(hostStep.id, "host-step", ctx.caps.maxStringLength),
|
|
315
|
+
kind: "host-step",
|
|
316
|
+
label: publicText(hostStep.label, "host step", ctx.caps.maxStringLength),
|
|
317
|
+
state,
|
|
318
|
+
updatedAt: hostStep.updatedAt,
|
|
319
|
+
...(terminalState(state) ? { endedAt: hostStep.updatedAt } : {}),
|
|
320
|
+
hostStep: hostMetadata,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
|
|
325
|
+
const state = normalizeState(job.status);
|
|
326
|
+
const startedAt = publicTime(job.startedAt);
|
|
327
|
+
const updatedAt = publicTime(job.updatedAt) ?? startedAt;
|
|
328
|
+
const activity = activityFor(job, ctx);
|
|
329
|
+
const node: AsyncStatusSnapshotNodeV1 = {
|
|
330
|
+
id: publicText(job.asyncId, "async", ctx.caps.maxStringLength),
|
|
331
|
+
kind: kindForMode(job.mode),
|
|
332
|
+
label: labelForAgents(job.agents, job.mode ?? "subagent", ctx.caps.maxStringLength),
|
|
333
|
+
state,
|
|
334
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
335
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
336
|
+
...(terminalState(state) && updatedAt !== undefined ? { endedAt: updatedAt } : {}),
|
|
337
|
+
...(activity ? { activity } : {}),
|
|
338
|
+
};
|
|
339
|
+
if (ctx.caps.maxDepth > 0) {
|
|
340
|
+
const stepChildren = job.steps?.map((step, index) => projectStep(step, step.index ?? index, 1, ctx)) ?? [];
|
|
341
|
+
const loadedKeys = new Set(job.steps?.flatMap((step) => step.workflowKey ? [step.workflowKey] : []) ?? []);
|
|
342
|
+
const graphStages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
|
|
343
|
+
const graphChildren = graphStages
|
|
344
|
+
.filter((graphNode) => !loadedKeys.has(graphNode.id))
|
|
345
|
+
.map((graphNode, index) => projectWorkflowGraphNode(graphNode, index, 1, ctx));
|
|
346
|
+
const nestedChildren = job.nestedChildren?.map((child, index) => projectNestedRun(child, index, 1, ctx)) ?? [];
|
|
347
|
+
const hostStepChildren = validHostStepList(job.hostSteps).map((hostStep) => projectHostStep(hostStep, ctx));
|
|
348
|
+
const ordinaryChildren = [...stepChildren, ...graphChildren, ...nestedChildren];
|
|
349
|
+
const retainedHostSteps = hostStepChildren.slice(0, ctx.caps.maxChildrenPerNode);
|
|
350
|
+
const retainedOrdinaryChildren = ordinaryChildren.slice(0, ctx.caps.maxChildrenPerNode - retainedHostSteps.length);
|
|
351
|
+
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
352
|
+
bounded.push(...retainedOrdinaryChildren, ...retainedHostSteps);
|
|
353
|
+
ctx.omitted.children += ordinaryChildren.length - retainedOrdinaryChildren.length + hostStepChildren.length - retainedHostSteps.length;
|
|
354
|
+
if (bounded.length) node.children = bounded;
|
|
355
|
+
} else {
|
|
356
|
+
const loadedKeys = new Set(job.steps?.flatMap((step) => step.workflowKey ? [step.workflowKey] : []) ?? []);
|
|
357
|
+
const graphStages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
|
|
358
|
+
const graphCount = graphStages.filter((graphNode) => !loadedKeys.has(graphNode.id)).length;
|
|
359
|
+
ctx.omitted.children += (job.steps?.length ?? 0) + graphCount + (job.nestedChildren?.length ?? 0) + validHostStepList(job.hostSteps).length;
|
|
360
|
+
}
|
|
361
|
+
return node;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function snapshotBytes(snapshot: AsyncStatusSnapshotV1): number {
|
|
365
|
+
return Buffer.byteLength(JSON.stringify(snapshot), "utf8");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function enforceByteLimit(snapshot: AsyncStatusSnapshotV1): void {
|
|
369
|
+
if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) return;
|
|
370
|
+
snapshot.omitted.byteLimitExceeded = true;
|
|
371
|
+
const runs = snapshot.runs;
|
|
372
|
+
const initialOmittedRuns = snapshot.omitted.runs;
|
|
373
|
+
let lower = 0;
|
|
374
|
+
let upper = Math.max(0, runs.length - 1);
|
|
375
|
+
while (lower < upper) {
|
|
376
|
+
const retained = Math.ceil((lower + upper) / 2);
|
|
377
|
+
snapshot.runs = runs.slice(0, retained);
|
|
378
|
+
snapshot.omitted.runs = initialOmittedRuns + runs.length - retained;
|
|
379
|
+
if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) lower = retained;
|
|
380
|
+
else upper = retained - 1;
|
|
381
|
+
}
|
|
382
|
+
snapshot.runs = runs.slice(0, lower);
|
|
383
|
+
snapshot.omitted.runs = initialOmittedRuns + runs.length - lower;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function workflowStepActivity(step: AsyncJobStep): string | undefined {
|
|
387
|
+
if (step.currentTool) return `tool ${step.currentTool}`;
|
|
388
|
+
if (step.currentPath) return step.currentPath.split(/[\\/]/).at(-1);
|
|
389
|
+
if (step.activityState === "needs_attention") return "needs attention";
|
|
390
|
+
if (step.activityState === "active_long_running") return "long-running";
|
|
391
|
+
if (step.turnCount !== undefined) return `${step.turnCount} turns`;
|
|
392
|
+
if (step.toolCount !== undefined) return `${step.toolCount} tools`;
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function workflowStepName(step: AsyncJobStep, index: number): string {
|
|
397
|
+
const key = step.workflowKey ?? `step ${index + 1}`;
|
|
398
|
+
const label = step.label && step.label !== key ? ` · ${step.label}` : "";
|
|
399
|
+
const phase = step.phase ? `${step.phase}: ` : "";
|
|
400
|
+
return `${phase}${key}${label} (${step.agent})`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function hostStepRow(hostStep: HostStepNodeV1): AsyncStatusWorkflowRow {
|
|
404
|
+
const freshness = hostStep.freshness
|
|
405
|
+
? {
|
|
406
|
+
expectedRef: publicText(hostStep.freshness.expectedRef, "ref", HOST_STEP_MAX_REF_CHARS),
|
|
407
|
+
...(hostStep.freshness.observedRef ? { observedRef: publicText(hostStep.freshness.observedRef, "ref", HOST_STEP_MAX_REF_CHARS) } : {}),
|
|
408
|
+
...(hostStep.freshness.stale !== undefined ? { stale: hostStep.freshness.stale } : {}),
|
|
409
|
+
}
|
|
410
|
+
: undefined;
|
|
411
|
+
return {
|
|
412
|
+
name: publicText(hostStep.label, "host step", HOST_STEP_MAX_LABEL_CHARS),
|
|
413
|
+
kind: hostStep.monitorKind,
|
|
414
|
+
state: hostStep.state,
|
|
415
|
+
...(hostStep.role ? { role: publicText(hostStep.role, "role", HOST_STEP_MAX_ROLE_CHARS) } : {}),
|
|
416
|
+
...(hostStep.provider ? { provider: publicText(hostStep.provider, "provider", HOST_STEP_MAX_PROVIDER_CHARS) } : {}),
|
|
417
|
+
...(hostStep.verdict ? { verdict: hostStep.verdict } : {}),
|
|
418
|
+
...(hostStep.reasonCode ? { reasonCode: publicText(hostStep.reasonCode, "reason", HOST_STEP_MAX_REASON_CHARS) } : {}),
|
|
419
|
+
...(hostStep.detail ? { detail: publicText(hostStep.detail, "detail", HOST_STEP_MAX_DETAIL_CHARS) } : {}),
|
|
420
|
+
...(hostStep.target ? { target: publicText(hostStep.target, "target", HOST_STEP_MAX_TARGET_CHARS) } : {}),
|
|
421
|
+
...(freshness ? { freshness } : {}),
|
|
422
|
+
...(hostStep.reportPath ? { reportPath: hostStepReportName(hostStep.reportPath) } : {}),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
function isWorkflowPreflight(value: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1 | undefined): value is WorkflowPreflightV1 {
|
|
428
|
+
return value !== undefined && !Array.isArray(value) && "lanes" in value;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function isWorkflowGraph(value: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1 | undefined): value is WorkflowGraphSnapshot {
|
|
432
|
+
return value !== undefined && !Array.isArray(value) && "nodes" in value;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function workflowGraphRowState(status: WorkflowGraphSnapshot["nodes"][number]["status"]): AsyncStatusWorkflowRow["state"] {
|
|
436
|
+
switch (status) {
|
|
437
|
+
case "pending":
|
|
438
|
+
return "planned";
|
|
439
|
+
case "completed":
|
|
440
|
+
return "complete";
|
|
441
|
+
case "detached":
|
|
442
|
+
return "paused";
|
|
443
|
+
default:
|
|
444
|
+
return status;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function workflowGraphStepStatus(status: WorkflowGraphSnapshot["nodes"][number]["status"]): AsyncJobStep["status"] {
|
|
449
|
+
switch (status) {
|
|
450
|
+
case "completed":
|
|
451
|
+
return "complete";
|
|
452
|
+
case "detached":
|
|
453
|
+
return "paused";
|
|
454
|
+
default:
|
|
455
|
+
return status;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function workflowGraphRowName(node: WorkflowGraphSnapshot["nodes"][number]): string {
|
|
460
|
+
const key = publicText(node.id, "stage", HOST_STEP_MAX_LABEL_CHARS);
|
|
461
|
+
const label = publicOptionalText(node.label, HOST_STEP_MAX_LABEL_CHARS);
|
|
462
|
+
const phase = publicOptionalText(node.phase, HOST_STEP_MAX_LABEL_CHARS);
|
|
463
|
+
const agent = publicOptionalText(node.agent, HOST_STEP_MAX_LABEL_CHARS);
|
|
464
|
+
return `${phase ? `${phase}: ` : ""}${key}${label && label !== node.id ? ` · ${label}` : ""}${agent ? ` (${agent})` : ""}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], preflight?: WorkflowPreflightLaneV1): AsyncStatusWorkflowRow {
|
|
468
|
+
return {
|
|
469
|
+
name: workflowGraphRowName(node),
|
|
470
|
+
state: workflowGraphRowState(node.status),
|
|
471
|
+
...(preflight ? { preflight } : {}),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Project loaded workflow child facts plus stored preflight hints into compact rows. */
|
|
476
|
+
export function projectAsyncWorkflowRows(
|
|
477
|
+
steps: readonly AsyncJobStep[] | undefined,
|
|
478
|
+
hostStepsOrPreflight?: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1,
|
|
479
|
+
preflightOverride?: WorkflowPreflightV1,
|
|
480
|
+
): AsyncStatusWorkflowRow[] {
|
|
481
|
+
const preflight = preflightOverride ?? (isWorkflowPreflight(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined);
|
|
482
|
+
const graph = isWorkflowGraph(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined;
|
|
483
|
+
const hostSteps = isWorkflowPreflight(hostStepsOrPreflight) || graph ? undefined : hostStepsOrPreflight;
|
|
484
|
+
const loaded = steps ?? [];
|
|
485
|
+
const declared = new Map<string, WorkflowPreflightLaneV1>();
|
|
486
|
+
if (graph) {
|
|
487
|
+
const loadedIndexesByKey = new Map<string, number[]>();
|
|
488
|
+
for (const [index, step] of loaded.entries()) {
|
|
489
|
+
if (!step.workflowKey) continue;
|
|
490
|
+
const indexes = loadedIndexesByKey.get(step.workflowKey) ?? [];
|
|
491
|
+
indexes.push(index);
|
|
492
|
+
loadedIndexesByKey.set(step.workflowKey, indexes);
|
|
493
|
+
}
|
|
494
|
+
const consumed = new Set<number>();
|
|
495
|
+
const childRows: AsyncStatusWorkflowRow[] = [];
|
|
496
|
+
for (const lane of preflight?.lanes ?? []) declared.set(lane.key, lane);
|
|
497
|
+
const graphStages = workflowGraphStageNodes(graph);
|
|
498
|
+
const graphKeys = new Set(graphStages.map((node) => node.id));
|
|
499
|
+
const graphPhaseByNodeId = new Map<string, string>();
|
|
500
|
+
for (const phase of graph.phases) {
|
|
501
|
+
for (const nodeId of phase.nodeIds) {
|
|
502
|
+
if (graphKeys.has(nodeId) && !graphPhaseByNodeId.has(nodeId)) graphPhaseByNodeId.set(nodeId, phase.title);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const graphLaneKeys = new Set(graphPhaseByNodeId.values());
|
|
506
|
+
const preflightForNode = (node: WorkflowGraphSnapshot["nodes"][number]): WorkflowPreflightLaneV1 | undefined => {
|
|
507
|
+
const phaseTitle = graphPhaseByNodeId.get(node.id);
|
|
508
|
+
return declared.get(node.id) ?? (node.phase ? declared.get(node.phase) : undefined) ?? (phaseTitle ? declared.get(phaseTitle) : undefined);
|
|
509
|
+
};
|
|
510
|
+
for (const node of graphStages) {
|
|
511
|
+
const indexes = (loadedIndexesByKey.get(node.id) ?? []).filter((index) => !consumed.has(index));
|
|
512
|
+
if (indexes.length > 0) {
|
|
513
|
+
for (const index of indexes) {
|
|
514
|
+
consumed.add(index);
|
|
515
|
+
childRows.push(projectLoadedWorkflowRow(loaded[index]!, index, preflightForNode(node)));
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
childRows.push(projectWorkflowGraphRow(node, preflightForNode(node)));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
for (const lane of preflight?.lanes ?? []) {
|
|
522
|
+
if (graphKeys.has(lane.key) || graphLaneKeys.has(lane.key)) continue;
|
|
523
|
+
const indexes = (loadedIndexesByKey.get(lane.key) ?? []).filter((index) => !consumed.has(index));
|
|
524
|
+
if (indexes.length > 0) {
|
|
525
|
+
for (const index of indexes) {
|
|
526
|
+
consumed.add(index);
|
|
527
|
+
childRows.push(projectLoadedWorkflowRow(loaded[index]!, index, lane));
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
childRows.push({ name: lane.key, state: "planned", preflight: lane });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
for (const [index, step] of loaded.entries()) {
|
|
534
|
+
if (!consumed.has(index)) childRows.push(projectLoadedWorkflowRow(step, index, step.workflowKey ? declared.get(step.workflowKey) : undefined));
|
|
535
|
+
}
|
|
536
|
+
return [...childRows, ...validHostStepList(graph).map(hostStepRow)];
|
|
537
|
+
}
|
|
538
|
+
const loadedIndexByKey = new Map<string, number>();
|
|
539
|
+
for (const [index, step] of loaded.entries()) {
|
|
540
|
+
if (step.workflowKey !== undefined && !loadedIndexByKey.has(step.workflowKey)) loadedIndexByKey.set(step.workflowKey, index);
|
|
541
|
+
}
|
|
542
|
+
const consumed = new Set<number>();
|
|
543
|
+
const childRows: AsyncStatusWorkflowRow[] = [];
|
|
544
|
+
for (const lane of preflight?.lanes ?? []) {
|
|
545
|
+
declared.set(lane.key, lane);
|
|
546
|
+
const index = loadedIndexByKey.get(lane.key);
|
|
547
|
+
if (index === undefined) {
|
|
548
|
+
childRows.push({ name: lane.key, state: "planned", preflight: lane });
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
consumed.add(index);
|
|
552
|
+
childRows.push(projectLoadedWorkflowRow(loaded[index]!, index, lane));
|
|
553
|
+
}
|
|
554
|
+
for (const [index, step] of loaded.entries()) {
|
|
555
|
+
if (consumed.has(index)) continue;
|
|
556
|
+
childRows.push(projectLoadedWorkflowRow(step, index, step.workflowKey ? declared.get(step.workflowKey) : undefined));
|
|
557
|
+
}
|
|
558
|
+
return [...childRows, ...validHostStepList(hostSteps).map(hostStepRow)];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?: WorkflowPreflightLaneV1): AsyncStatusWorkflowRow {
|
|
562
|
+
const modelThinking = formatModelThinking(step.model, step.thinking) || undefined;
|
|
563
|
+
const activity = workflowStepActivity(step);
|
|
564
|
+
return {
|
|
565
|
+
name: workflowStepName(step, index),
|
|
566
|
+
state: step.status,
|
|
567
|
+
...(step.context ? { context: step.context } : {}),
|
|
568
|
+
...(modelThinking ? { modelThinking } : {}),
|
|
569
|
+
...(activity ? { activity } : {}),
|
|
570
|
+
...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
|
|
571
|
+
...(step.tokens?.total !== undefined ? { tokens: step.tokens.total } : {}),
|
|
572
|
+
...(step.tokens?.window !== undefined ? { window: step.tokens.window } : {}),
|
|
573
|
+
...(preflight ? { preflight } : {}),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Project already-loaded async status facts into the bounded public snapshot shape. */
|
|
578
|
+
export function projectAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
|
|
579
|
+
const caps = resolveCaps(options);
|
|
580
|
+
const ctx: ProjectionContext = { caps, omitted: { runs: 0, children: 0, byteLimitExceeded: false } };
|
|
581
|
+
const sorted = [...jobs].sort((left, right) => {
|
|
582
|
+
const leftUpdated = left.updatedAt ?? left.startedAt ?? 0;
|
|
583
|
+
const rightUpdated = right.updatedAt ?? right.startedAt ?? 0;
|
|
584
|
+
return rightUpdated - leftUpdated || left.asyncId.localeCompare(right.asyncId);
|
|
585
|
+
});
|
|
586
|
+
ctx.omitted.runs += Math.max(0, sorted.length - caps.maxRuns);
|
|
587
|
+
const snapshot: AsyncStatusSnapshotV1 = {
|
|
588
|
+
kind: ASYNC_STATUS_SNAPSHOT_KIND,
|
|
589
|
+
version: ASYNC_STATUS_SNAPSHOT_VERSION,
|
|
590
|
+
generatedAt: options.generatedAt ?? Date.now(),
|
|
591
|
+
caps,
|
|
592
|
+
omitted: ctx.omitted,
|
|
593
|
+
runs: sorted.slice(0, caps.maxRuns).map((job) => projectRun(job, ctx)),
|
|
594
|
+
};
|
|
595
|
+
enforceByteLimit(snapshot);
|
|
596
|
+
return snapshot;
|
|
597
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AsyncStatus } from "../../shared/types.ts";
|
|
1
|
+
import type { AsyncStatus, NestedRunSummary } from "../../shared/types.ts";
|
|
2
2
|
|
|
3
3
|
export type AsyncStatusStep = NonNullable<AsyncStatus["steps"]>[number];
|
|
4
4
|
|
|
@@ -6,6 +6,7 @@ export interface ResolvedAsyncStatusChild {
|
|
|
6
6
|
index: number;
|
|
7
7
|
step: AsyncStatusStep;
|
|
8
8
|
id: string;
|
|
9
|
+
nested?: NestedRunSummary;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export type AsyncStatusChildResolution =
|
|
@@ -20,11 +21,25 @@ export function asyncStatusChildIdentityCandidates(step: AsyncStatusStep, index:
|
|
|
20
21
|
return [...new Set([step.workflowKey, step.runId, `step:${index}`].filter((value): value is string => typeof value === "string" && value.length > 0))];
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export function resolveAsyncStatusChild(
|
|
24
|
+
export function resolveAsyncStatusChild(
|
|
25
|
+
status: Pick<AsyncStatus, "runId" | "steps">,
|
|
26
|
+
childId: string,
|
|
27
|
+
options: { includeNested?: boolean } = {},
|
|
28
|
+
): AsyncStatusChildResolution {
|
|
24
29
|
const matches: ResolvedAsyncStatusChild[] = [];
|
|
25
30
|
for (const [index, step] of (status.steps ?? []).entries()) {
|
|
26
|
-
if (
|
|
27
|
-
|
|
31
|
+
if (asyncStatusChildIdentityCandidates(step, index).includes(childId)) {
|
|
32
|
+
matches.push({ index, step, id: asyncStatusChildIdentity(step, index) });
|
|
33
|
+
}
|
|
34
|
+
if (options.includeNested) {
|
|
35
|
+
const findNested = (children: readonly NestedRunSummary[] | undefined): void => {
|
|
36
|
+
for (const nested of children ?? []) {
|
|
37
|
+
if (nested.id === childId) matches.push({ index, step, id: nested.id, nested });
|
|
38
|
+
findNested(nested.children);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
findNested(step.children);
|
|
42
|
+
}
|
|
28
43
|
}
|
|
29
44
|
if (matches.length === 1) return { ok: true, child: matches[0]! };
|
|
30
45
|
if (matches.length > 1) return { ok: false, code: "ambiguous", message: `Child '${childId}' is ambiguous under async run '${status.runId}'.` };
|