pi-subagents 0.58.0 → 0.59.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +89 -4
  8. package/docs/workflows.md +93 -2
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +5 -1
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +24 -3
  16. package/src/agents/agent-management.ts +5 -17
  17. package/src/agents/agent-serializer.ts +4 -2
  18. package/src/agents/agents.ts +67 -26
  19. package/src/agents/runtime-agent-registry.ts +9 -16
  20. package/src/api/background-work.ts +5 -1
  21. package/src/api/delegation.ts +0 -7
  22. package/src/api/preflight.ts +6 -9
  23. package/src/extension/fanout-child.ts +5 -3
  24. package/src/extension/index.ts +51 -23
  25. package/src/extension/public-execution.ts +15 -2
  26. package/src/extension/schemas.ts +35 -10
  27. package/src/extension/tool-description.ts +18 -3
  28. package/src/intercom/result-intercom.ts +2 -0
  29. package/src/profiles/profiles.ts +5 -6
  30. package/src/runs/background/active-async-capacity.ts +2 -2
  31. package/src/runs/background/async-execution.ts +50 -35
  32. package/src/runs/background/async-job-tracker.ts +14 -12
  33. package/src/runs/background/async-resume.ts +23 -25
  34. package/src/runs/background/async-status-snapshot.ts +23 -261
  35. package/src/runs/background/async-status.ts +66 -7
  36. package/src/runs/background/chain-append.ts +6 -3
  37. package/src/runs/background/chain-root-attachment.ts +60 -8
  38. package/src/runs/background/fleet-view.ts +21 -11
  39. package/src/runs/background/notify.ts +158 -6
  40. package/src/runs/background/result-files.ts +2 -1
  41. package/src/runs/background/result-watcher.ts +2 -0
  42. package/src/runs/background/resume-guidance.ts +1 -1
  43. package/src/runs/background/retained-children.ts +1 -1
  44. package/src/runs/background/run-status.ts +18 -8
  45. package/src/runs/background/scheduled-runs.ts +86 -7
  46. package/src/runs/background/stale-run-reconciler.ts +10 -4
  47. package/src/runs/background/steering.ts +4 -14
  48. package/src/runs/background/subagent-runner.ts +346 -357
  49. package/src/runs/background/subagent-wait.ts +58 -10
  50. package/src/runs/background/terminal-run-index.ts +1 -1
  51. package/src/runs/background/wait-completions.ts +22 -1
  52. package/src/runs/background/wait-config.ts +23 -9
  53. package/src/runs/background/wait-tool.ts +9 -2
  54. package/src/runs/foreground/async-steering-action.ts +2 -2
  55. package/src/runs/foreground/execution.ts +114 -120
  56. package/src/runs/foreground/foreground-control.ts +3 -0
  57. package/src/runs/foreground/foreground-history.ts +1 -0
  58. package/src/runs/foreground/subagent-executor.ts +478 -239
  59. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  60. package/src/runs/shared/abort-recovery.ts +119 -0
  61. package/src/runs/shared/async-status-projection.ts +463 -0
  62. package/src/runs/shared/child-identity.ts +19 -4
  63. package/src/runs/shared/child-launch-plan.ts +151 -0
  64. package/src/runs/shared/completion-evidence.ts +89 -0
  65. package/src/runs/shared/completion-guard.ts +1 -1
  66. package/src/runs/shared/dynamic-fanout.ts +2 -2
  67. package/src/runs/shared/host-step-status.ts +230 -0
  68. package/src/runs/shared/lane-metadata.ts +105 -0
  69. package/src/runs/shared/mcp-config-sources.ts +42 -6
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  71. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  72. package/src/runs/shared/model-fallback.ts +9 -2
  73. package/src/runs/shared/nested-events.ts +6 -2
  74. package/src/runs/shared/nested-render.ts +7 -3
  75. package/src/runs/shared/parallel-handoff.ts +419 -7
  76. package/src/runs/shared/parallel-utils.ts +7 -0
  77. package/src/runs/shared/pi-args.ts +20 -2
  78. package/src/runs/shared/single-output.ts +27 -4
  79. package/src/runs/shared/subagent-prompt-runtime.ts +13 -4
  80. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  81. package/src/runs/shared/worktree.ts +18 -0
  82. package/src/shared/child-session-name.ts +46 -0
  83. package/src/shared/extension-context.ts +24 -0
  84. package/src/shared/formatters.ts +5 -2
  85. package/src/shared/launch-contract.ts +1 -1
  86. package/src/shared/settings.ts +9 -103
  87. package/src/shared/types.ts +198 -29
  88. package/src/shared/utils.ts +35 -55
  89. package/src/slash/delegation-adapters.ts +1 -8
  90. package/src/slash/delegation-request.ts +0 -4
  91. package/src/slash/slash-bridge.ts +1 -2
  92. package/src/slash/slash-commands.ts +369 -90
  93. package/src/slash/slash-live-state.ts +22 -11
  94. package/src/tui/fleet-status.ts +125 -74
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +316 -27
  97. package/src/watchdog/turn-delta.ts +1 -1
  98. package/src/workflows/chat-progress.ts +6 -3
  99. package/src/workflows/host-command.ts +230 -0
  100. package/src/workflows/scripted-workflow.ts +452 -38
  101. package/src/workflows/workflow-child-summary.ts +9 -5
  102. package/src/workflows/workflow-preflight.ts +270 -0
  103. package/src/workflows/workflow-receipt.ts +43 -4
  104. package/src/workflows/workflow-settlement.ts +246 -0
  105. package/src/runs/shared/turn-budget.ts +0 -98
@@ -0,0 +1,463 @@
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
+
6
+ export const ASYNC_STATUS_SNAPSHOT_KIND = "pi-subagents.async-status-snapshot";
7
+ export const ASYNC_STATUS_SNAPSHOT_VERSION = 1;
8
+
9
+ const DEFAULT_MAX_RUNS = 20;
10
+ const DEFAULT_MAX_CHILDREN_PER_NODE = 8;
11
+ const DEFAULT_MAX_DEPTH = 3;
12
+ const DEFAULT_MAX_STRING_LENGTH = 160;
13
+ const DEFAULT_MAX_SERIALIZED_BYTES = 32 * 1024;
14
+
15
+ export type AsyncStatusSnapshotState = "queued" | "running" | "complete" | "failed" | "partial" | "paused" | "stopped" | "rejected";
16
+ export type AsyncStatusSnapshotKind = "subagent" | "workflow" | "step";
17
+
18
+ export interface AsyncStatusSnapshotActivityV1 {
19
+ state?: string;
20
+ currentTool?: string;
21
+ lastActivityAt?: number;
22
+ currentToolStartedAt?: number;
23
+ turnCount?: number;
24
+ toolCount?: number;
25
+ }
26
+
27
+ export interface AsyncStatusSnapshotHostStepV1 {
28
+ kind: HostStepMonitorKind;
29
+ provider?: string;
30
+ role?: string;
31
+ state: HostStepState;
32
+ verdict?: HostStepVerdict;
33
+ reasonCode?: string;
34
+ detail?: string;
35
+ target?: string;
36
+ stale?: boolean;
37
+ report?: string;
38
+ }
39
+
40
+ export interface AsyncStatusSnapshotNodeV1 {
41
+ id: string;
42
+ kind: AsyncStatusSnapshotKind | "host-step";
43
+ label: string;
44
+ state: AsyncStatusSnapshotState;
45
+ startedAt?: number;
46
+ updatedAt?: number;
47
+ endedAt?: number;
48
+ activity?: AsyncStatusSnapshotActivityV1;
49
+ hostStep?: AsyncStatusSnapshotHostStepV1;
50
+ children?: AsyncStatusSnapshotNodeV1[];
51
+ }
52
+
53
+ export interface AsyncStatusSnapshotCapsV1 {
54
+ maxRuns: number;
55
+ maxChildrenPerNode: number;
56
+ maxDepth: number;
57
+ maxStringLength: number;
58
+ maxSerializedBytes: number;
59
+ }
60
+
61
+ export interface AsyncStatusSnapshotOmittedV1 {
62
+ runs: number;
63
+ children: number;
64
+ byteLimitExceeded: boolean;
65
+ }
66
+
67
+ export interface AsyncStatusSnapshotV1 {
68
+ kind: typeof ASYNC_STATUS_SNAPSHOT_KIND;
69
+ version: typeof ASYNC_STATUS_SNAPSHOT_VERSION;
70
+ generatedAt: number;
71
+ caps: AsyncStatusSnapshotCapsV1;
72
+ omitted: AsyncStatusSnapshotOmittedV1;
73
+ runs: AsyncStatusSnapshotNodeV1[];
74
+ }
75
+
76
+ export interface AsyncStatusSnapshotOptions {
77
+ generatedAt?: number;
78
+ maxRuns?: number;
79
+ maxChildrenPerNode?: number;
80
+ maxDepth?: number;
81
+ maxStringLength?: number;
82
+ maxSerializedBytes?: number;
83
+ }
84
+
85
+ export interface AsyncStatusWorkflowRow {
86
+ name: string;
87
+ state: AsyncJobStep["status"] | HostStepState | "planned";
88
+ /** Present only for typed host-owned monitor rows; legacy child rows omit it. */
89
+ kind?: HostStepMonitorKind;
90
+ modelThinking?: string;
91
+ activity?: string;
92
+ startedAt?: number;
93
+ tokens?: number;
94
+ window?: number;
95
+ overflow?: number;
96
+ provider?: string;
97
+ role?: string;
98
+ verdict?: HostStepVerdict;
99
+ reasonCode?: string;
100
+ detail?: string;
101
+ target?: string;
102
+ freshness?: HostStepFreshnessV1;
103
+ reportPath?: string;
104
+ preflight?: WorkflowPreflightLaneV1;
105
+ }
106
+
107
+ interface ProjectionContext {
108
+ caps: AsyncStatusSnapshotCapsV1;
109
+ omitted: AsyncStatusSnapshotOmittedV1;
110
+ }
111
+
112
+ function validHostStepList(source: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | undefined): HostStepNodeV1[] {
113
+ if (source && "nodes" in source) return validHostStepNodes(source);
114
+ const hostSteps: HostStepNodeV1[] = [];
115
+ for (const [index, value] of (source ?? []).slice(0, HOST_STEP_MAX_COUNT).entries()) {
116
+ try {
117
+ hostSteps.push(parseHostStepNode(value, `hostSteps[${index}]`));
118
+ } catch {
119
+ // Renderers must not turn malformed host data into a fake child row.
120
+ }
121
+ }
122
+ return hostSteps;
123
+ }
124
+
125
+ function resolveCaps(options: AsyncStatusSnapshotOptions): AsyncStatusSnapshotCapsV1 {
126
+ return {
127
+ maxRuns: Math.max(0, Math.floor(options.maxRuns ?? DEFAULT_MAX_RUNS)),
128
+ maxChildrenPerNode: Math.max(0, Math.floor(options.maxChildrenPerNode ?? DEFAULT_MAX_CHILDREN_PER_NODE)),
129
+ maxDepth: Math.max(0, Math.floor(options.maxDepth ?? DEFAULT_MAX_DEPTH)),
130
+ maxStringLength: Math.max(0, Math.floor(options.maxStringLength ?? DEFAULT_MAX_STRING_LENGTH)),
131
+ maxSerializedBytes: Math.max(256, Math.floor(options.maxSerializedBytes ?? DEFAULT_MAX_SERIALIZED_BYTES)),
132
+ };
133
+ }
134
+
135
+ function publicText(value: unknown, fallback: string, maxLength: number): string {
136
+ if (typeof value !== "string") return fallback;
137
+ const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
138
+ return truncateDisplayText(normalized || fallback, maxLength);
139
+ }
140
+
141
+ function publicOptionalText(value: unknown, maxLength: number): string | undefined {
142
+ if (typeof value !== "string") return undefined;
143
+ const normalized = sanitizeDisplayText(value.slice(0, Math.max(0, maxLength * 4)));
144
+ return normalized ? truncateDisplayText(normalized, maxLength) : undefined;
145
+ }
146
+
147
+ function publicTime(value: unknown): number | undefined {
148
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
149
+ }
150
+
151
+ function publicCount(value: unknown): number | undefined {
152
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
153
+ }
154
+
155
+ function normalizeState(value: string): AsyncStatusSnapshotState {
156
+ if (value === "completed") return "complete";
157
+ if (value === "pending") return "queued";
158
+ return value as AsyncStatusSnapshotState;
159
+ }
160
+
161
+ function terminalState(state: AsyncStatusSnapshotState): boolean {
162
+ return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped" || state === "rejected";
163
+ }
164
+
165
+ function kindForMode(mode: SubagentRunMode | undefined): AsyncStatusSnapshotKind {
166
+ return mode === "workflow" ? "workflow" : "subagent";
167
+ }
168
+
169
+ function labelForAgents(agents: readonly string[] | undefined, fallback: string, maxLength: number): string {
170
+ if (!agents?.length) return publicText(fallback, fallback, maxLength);
171
+ const visible = agents.slice(0, 3).map((agent) => publicText(agent, "agent", maxLength)).join(", ");
172
+ const suffix = agents.length > 3 ? `, +${agents.length - 3} more` : "";
173
+ return truncateDisplayText(`${visible}${suffix}`, maxLength);
174
+ }
175
+
176
+ function activityFor(source: {
177
+ activityState?: unknown;
178
+ currentTool?: unknown;
179
+ lastActivityAt?: unknown;
180
+ currentToolStartedAt?: unknown;
181
+ turnCount?: unknown;
182
+ toolCount?: unknown;
183
+ }, ctx: ProjectionContext): AsyncStatusSnapshotActivityV1 | undefined {
184
+ const currentTool = publicOptionalText(source.currentTool, ctx.caps.maxStringLength);
185
+ const lastActivityAt = publicTime(source.lastActivityAt);
186
+ const currentToolStartedAt = publicTime(source.currentToolStartedAt);
187
+ const turnCount = publicCount(source.turnCount);
188
+ const toolCount = publicCount(source.toolCount);
189
+ const activity: AsyncStatusSnapshotActivityV1 = {
190
+ ...(typeof source.activityState === "string" ? { state: publicText(source.activityState, "unknown", ctx.caps.maxStringLength) } : {}),
191
+ ...(currentTool ? { currentTool } : {}),
192
+ ...(lastActivityAt !== undefined ? { lastActivityAt } : {}),
193
+ ...(currentToolStartedAt !== undefined ? { currentToolStartedAt } : {}),
194
+ ...(turnCount !== undefined ? { turnCount } : {}),
195
+ ...(toolCount !== undefined ? { toolCount } : {}),
196
+ };
197
+ return Object.keys(activity).length ? activity : undefined;
198
+ }
199
+
200
+ function appendBoundedChildren(children: AsyncStatusSnapshotNodeV1[], source: readonly AsyncStatusSnapshotNodeV1[], ctx: ProjectionContext): void {
201
+ const remaining = Math.max(0, ctx.caps.maxChildrenPerNode - children.length);
202
+ children.push(...source.slice(0, remaining));
203
+ ctx.omitted.children += Math.max(0, source.length - remaining);
204
+ }
205
+
206
+ function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
207
+ const state = normalizeState(step.status);
208
+ const startedAt = publicTime(step.startedAt);
209
+ const endedAt = publicTime(step.endedAt);
210
+ const updatedAt = endedAt ?? publicTime(step.lastActivityAt) ?? startedAt;
211
+ const activity = activityFor(step, ctx);
212
+ const node: AsyncStatusSnapshotNodeV1 = {
213
+ id: publicText("workflowKey" in step && step.workflowKey ? step.workflowKey : "runId" in step && step.runId ? step.runId : `step:${index}`, `step:${index}`, ctx.caps.maxStringLength),
214
+ kind: "step",
215
+ label: publicText("label" in step && step.label ? step.label : step.agent, "step", ctx.caps.maxStringLength),
216
+ state,
217
+ ...(startedAt !== undefined ? { startedAt } : {}),
218
+ ...(updatedAt !== undefined ? { updatedAt } : {}),
219
+ ...(terminalState(state) && endedAt !== undefined ? { endedAt } : {}),
220
+ ...(activity ? { activity } : {}),
221
+ };
222
+ if (depth < ctx.caps.maxDepth && step.children?.length) {
223
+ const nested = step.children.map((child, childIndex) => projectNestedRun(child, childIndex, depth + 1, ctx));
224
+ const bounded: AsyncStatusSnapshotNodeV1[] = [];
225
+ appendBoundedChildren(bounded, nested, ctx);
226
+ if (bounded.length) node.children = bounded;
227
+ } else if (step.children?.length) {
228
+ ctx.omitted.children += step.children.length;
229
+ }
230
+ return node;
231
+ }
232
+
233
+ function projectNestedRun(child: NestedRunSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
234
+ const state = normalizeState(child.state);
235
+ const startedAt = publicTime(child.startedAt);
236
+ const endedAt = publicTime(child.endedAt);
237
+ const updatedAt = publicTime(child.lastUpdate) ?? endedAt ?? publicTime(child.lastActivityAt) ?? startedAt;
238
+ const activity = activityFor(child, ctx);
239
+ const node: AsyncStatusSnapshotNodeV1 = {
240
+ id: publicText(child.id, `nested:${index}`, ctx.caps.maxStringLength),
241
+ kind: kindForMode(child.mode),
242
+ label: child.agent ? publicText(child.agent, "subagent", ctx.caps.maxStringLength) : labelForAgents(child.agents, child.mode ?? "subagent", ctx.caps.maxStringLength),
243
+ state,
244
+ ...(startedAt !== undefined ? { startedAt } : {}),
245
+ ...(updatedAt !== undefined ? { updatedAt } : {}),
246
+ ...(terminalState(state) && endedAt !== undefined ? { endedAt } : {}),
247
+ ...(activity ? { activity } : {}),
248
+ };
249
+ if (depth < ctx.caps.maxDepth) {
250
+ const nestedSteps = child.steps?.map((step, stepIndex) => projectStep(step, stepIndex, depth + 1, ctx)) ?? [];
251
+ const nestedChildren = child.children?.map((nested, childIndex) => projectNestedRun(nested, childIndex, depth + 1, ctx)) ?? [];
252
+ const bounded: AsyncStatusSnapshotNodeV1[] = [];
253
+ appendBoundedChildren(bounded, [...nestedSteps, ...nestedChildren], ctx);
254
+ if (bounded.length) node.children = bounded;
255
+ } else {
256
+ ctx.omitted.children += (child.steps?.length ?? 0) + (child.children?.length ?? 0);
257
+ }
258
+ return node;
259
+ }
260
+
261
+ function hostStepSnapshotState(state: HostStepState, verdict: HostStepVerdict | undefined): AsyncStatusSnapshotState {
262
+ if (state === "pending") return "queued";
263
+ if (state === "running") return "running";
264
+ if (state === "cancelled") return "stopped";
265
+ if (state === "error" || verdict === "fail") return "failed";
266
+ return verdict === undefined || verdict === "inconclusive" ? "partial" : "complete";
267
+ }
268
+
269
+ function projectHostStep(hostStep: HostStepNodeV1, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
270
+ const state = hostStepSnapshotState(hostStep.state, hostStep.verdict);
271
+ const detail = publicOptionalText(hostStep.detail, ctx.caps.maxStringLength);
272
+ const report = publicOptionalText(hostStepReportName(hostStep.reportPath), ctx.caps.maxStringLength);
273
+ const hostMetadata: AsyncStatusSnapshotHostStepV1 = {
274
+ kind: hostStep.monitorKind,
275
+ state: hostStep.state,
276
+ ...(hostStep.provider ? { provider: publicText(hostStep.provider, "provider", ctx.caps.maxStringLength) } : {}),
277
+ ...(hostStep.role ? { role: publicText(hostStep.role, "role", ctx.caps.maxStringLength) } : {}),
278
+ ...(hostStep.verdict ? { verdict: hostStep.verdict } : {}),
279
+ ...(hostStep.reasonCode ? { reasonCode: publicText(hostStep.reasonCode, "reason", ctx.caps.maxStringLength) } : {}),
280
+ ...(detail ? { detail } : {}),
281
+ ...(hostStep.target ? { target: publicText(hostStep.target, "target", ctx.caps.maxStringLength) } : {}),
282
+ ...(hostStep.freshness?.stale !== undefined ? { stale: hostStep.freshness.stale } : {}),
283
+ ...(report ? { report } : {}),
284
+ };
285
+ return {
286
+ id: publicText(hostStep.id, "host-step", ctx.caps.maxStringLength),
287
+ kind: "host-step",
288
+ label: publicText(hostStep.label, "host step", ctx.caps.maxStringLength),
289
+ state,
290
+ updatedAt: hostStep.updatedAt,
291
+ ...(terminalState(state) ? { endedAt: hostStep.updatedAt } : {}),
292
+ hostStep: hostMetadata,
293
+ };
294
+ }
295
+
296
+ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
297
+ const state = normalizeState(job.status);
298
+ const startedAt = publicTime(job.startedAt);
299
+ const updatedAt = publicTime(job.updatedAt) ?? startedAt;
300
+ const activity = activityFor(job, ctx);
301
+ const node: AsyncStatusSnapshotNodeV1 = {
302
+ id: publicText(job.asyncId, "async", ctx.caps.maxStringLength),
303
+ kind: kindForMode(job.mode),
304
+ label: labelForAgents(job.agents, job.mode ?? "subagent", ctx.caps.maxStringLength),
305
+ state,
306
+ ...(startedAt !== undefined ? { startedAt } : {}),
307
+ ...(updatedAt !== undefined ? { updatedAt } : {}),
308
+ ...(terminalState(state) && updatedAt !== undefined ? { endedAt: updatedAt } : {}),
309
+ ...(activity ? { activity } : {}),
310
+ };
311
+ if (ctx.caps.maxDepth > 0) {
312
+ const stepChildren = job.steps?.map((step, index) => projectStep(step, step.index ?? index, 1, ctx)) ?? [];
313
+ const nestedChildren = job.nestedChildren?.map((child, index) => projectNestedRun(child, index, 1, ctx)) ?? [];
314
+ const hostStepChildren = validHostStepList(job.hostSteps).map((hostStep) => projectHostStep(hostStep, ctx));
315
+ const ordinaryChildren = [...stepChildren, ...nestedChildren];
316
+ const retainedHostSteps = hostStepChildren.slice(0, ctx.caps.maxChildrenPerNode);
317
+ const retainedOrdinaryChildren = ordinaryChildren.slice(0, ctx.caps.maxChildrenPerNode - retainedHostSteps.length);
318
+ const bounded: AsyncStatusSnapshotNodeV1[] = [];
319
+ bounded.push(...retainedOrdinaryChildren, ...retainedHostSteps);
320
+ ctx.omitted.children += ordinaryChildren.length - retainedOrdinaryChildren.length + hostStepChildren.length - retainedHostSteps.length;
321
+ if (bounded.length) node.children = bounded;
322
+ } else {
323
+ ctx.omitted.children += (job.steps?.length ?? 0) + (job.nestedChildren?.length ?? 0) + validHostStepList(job.hostSteps).length;
324
+ }
325
+ return node;
326
+ }
327
+
328
+ function snapshotBytes(snapshot: AsyncStatusSnapshotV1): number {
329
+ return Buffer.byteLength(JSON.stringify(snapshot), "utf8");
330
+ }
331
+
332
+ function enforceByteLimit(snapshot: AsyncStatusSnapshotV1): void {
333
+ if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) return;
334
+ snapshot.omitted.byteLimitExceeded = true;
335
+ const runs = snapshot.runs;
336
+ const initialOmittedRuns = snapshot.omitted.runs;
337
+ let lower = 0;
338
+ let upper = Math.max(0, runs.length - 1);
339
+ while (lower < upper) {
340
+ const retained = Math.ceil((lower + upper) / 2);
341
+ snapshot.runs = runs.slice(0, retained);
342
+ snapshot.omitted.runs = initialOmittedRuns + runs.length - retained;
343
+ if (snapshotBytes(snapshot) <= snapshot.caps.maxSerializedBytes) lower = retained;
344
+ else upper = retained - 1;
345
+ }
346
+ snapshot.runs = runs.slice(0, lower);
347
+ snapshot.omitted.runs = initialOmittedRuns + runs.length - lower;
348
+ }
349
+
350
+ function workflowStepActivity(step: AsyncJobStep): string | undefined {
351
+ if (step.currentTool) return `tool ${step.currentTool}`;
352
+ if (step.currentPath) return step.currentPath.split(/[\\/]/).at(-1);
353
+ if (step.activityState === "needs_attention") return "needs attention";
354
+ if (step.activityState === "active_long_running") return "long-running";
355
+ if (step.turnCount !== undefined) return `${step.turnCount} turns`;
356
+ if (step.toolCount !== undefined) return `${step.toolCount} tools`;
357
+ return undefined;
358
+ }
359
+
360
+ function workflowStepName(step: AsyncJobStep, index: number): string {
361
+ const key = step.workflowKey ?? `step ${index + 1}`;
362
+ const label = step.label && step.label !== key ? ` · ${step.label}` : "";
363
+ const phase = step.phase ? `${step.phase}: ` : "";
364
+ return `${phase}${key}${label} (${step.agent})`;
365
+ }
366
+
367
+ function hostStepRow(hostStep: HostStepNodeV1): AsyncStatusWorkflowRow {
368
+ const freshness = hostStep.freshness
369
+ ? {
370
+ expectedRef: publicText(hostStep.freshness.expectedRef, "ref", HOST_STEP_MAX_REF_CHARS),
371
+ ...(hostStep.freshness.observedRef ? { observedRef: publicText(hostStep.freshness.observedRef, "ref", HOST_STEP_MAX_REF_CHARS) } : {}),
372
+ ...(hostStep.freshness.stale !== undefined ? { stale: hostStep.freshness.stale } : {}),
373
+ }
374
+ : undefined;
375
+ return {
376
+ name: publicText(hostStep.label, "host step", HOST_STEP_MAX_LABEL_CHARS),
377
+ kind: hostStep.monitorKind,
378
+ state: hostStep.state,
379
+ ...(hostStep.role ? { role: publicText(hostStep.role, "role", HOST_STEP_MAX_ROLE_CHARS) } : {}),
380
+ ...(hostStep.provider ? { provider: publicText(hostStep.provider, "provider", HOST_STEP_MAX_PROVIDER_CHARS) } : {}),
381
+ ...(hostStep.verdict ? { verdict: hostStep.verdict } : {}),
382
+ ...(hostStep.reasonCode ? { reasonCode: publicText(hostStep.reasonCode, "reason", HOST_STEP_MAX_REASON_CHARS) } : {}),
383
+ ...(hostStep.detail ? { detail: publicText(hostStep.detail, "detail", HOST_STEP_MAX_DETAIL_CHARS) } : {}),
384
+ ...(hostStep.target ? { target: publicText(hostStep.target, "target", HOST_STEP_MAX_TARGET_CHARS) } : {}),
385
+ ...(freshness ? { freshness } : {}),
386
+ ...(hostStep.reportPath ? { reportPath: hostStepReportName(hostStep.reportPath) } : {}),
387
+ };
388
+ }
389
+
390
+
391
+ function isWorkflowPreflight(value: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1 | undefined): value is WorkflowPreflightV1 {
392
+ return value !== undefined && !Array.isArray(value) && "lanes" in value;
393
+ }
394
+
395
+ /** Project loaded workflow child facts plus stored preflight hints into compact rows. */
396
+ export function projectAsyncWorkflowRows(
397
+ steps: readonly AsyncJobStep[] | undefined,
398
+ hostStepsOrPreflight?: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1,
399
+ preflightOverride?: WorkflowPreflightV1,
400
+ ): AsyncStatusWorkflowRow[] {
401
+ const preflight = preflightOverride ?? (isWorkflowPreflight(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined);
402
+ const hostSteps = isWorkflowPreflight(hostStepsOrPreflight) ? undefined : hostStepsOrPreflight;
403
+ const loaded = steps ?? [];
404
+ const declared = new Map<string, WorkflowPreflightLaneV1>();
405
+ const loadedIndexByKey = new Map<string, number>();
406
+ for (const [index, step] of loaded.entries()) {
407
+ if (step.workflowKey !== undefined && !loadedIndexByKey.has(step.workflowKey)) loadedIndexByKey.set(step.workflowKey, index);
408
+ }
409
+ const consumed = new Set<number>();
410
+ const childRows: AsyncStatusWorkflowRow[] = [];
411
+ for (const lane of preflight?.lanes ?? []) {
412
+ declared.set(lane.key, lane);
413
+ const index = loadedIndexByKey.get(lane.key);
414
+ if (index === undefined) {
415
+ childRows.push({ name: lane.key, state: "planned", preflight: lane });
416
+ continue;
417
+ }
418
+ consumed.add(index);
419
+ childRows.push(projectLoadedWorkflowRow(loaded[index]!, index, lane));
420
+ }
421
+ for (const [index, step] of loaded.entries()) {
422
+ if (consumed.has(index)) continue;
423
+ childRows.push(projectLoadedWorkflowRow(step, index, step.workflowKey ? declared.get(step.workflowKey) : undefined));
424
+ }
425
+ return [...childRows, ...validHostStepList(hostSteps).map(hostStepRow)];
426
+ }
427
+
428
+ function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?: WorkflowPreflightLaneV1): AsyncStatusWorkflowRow {
429
+ const modelThinking = formatModelThinking(step.model, step.thinking) || undefined;
430
+ const activity = workflowStepActivity(step);
431
+ return {
432
+ name: workflowStepName(step, index),
433
+ state: step.status,
434
+ ...(modelThinking ? { modelThinking } : {}),
435
+ ...(activity ? { activity } : {}),
436
+ ...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
437
+ ...(step.tokens?.total !== undefined ? { tokens: step.tokens.total } : {}),
438
+ ...(step.tokens?.window !== undefined ? { window: step.tokens.window } : {}),
439
+ ...(preflight ? { preflight } : {}),
440
+ };
441
+ }
442
+
443
+ /** Project already-loaded async status facts into the bounded public snapshot shape. */
444
+ export function projectAsyncStatusSnapshot(jobs: Iterable<AsyncJobState>, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshotV1 {
445
+ const caps = resolveCaps(options);
446
+ const ctx: ProjectionContext = { caps, omitted: { runs: 0, children: 0, byteLimitExceeded: false } };
447
+ const sorted = [...jobs].sort((left, right) => {
448
+ const leftUpdated = left.updatedAt ?? left.startedAt ?? 0;
449
+ const rightUpdated = right.updatedAt ?? right.startedAt ?? 0;
450
+ return rightUpdated - leftUpdated || left.asyncId.localeCompare(right.asyncId);
451
+ });
452
+ ctx.omitted.runs += Math.max(0, sorted.length - caps.maxRuns);
453
+ const snapshot: AsyncStatusSnapshotV1 = {
454
+ kind: ASYNC_STATUS_SNAPSHOT_KIND,
455
+ version: ASYNC_STATUS_SNAPSHOT_VERSION,
456
+ generatedAt: options.generatedAt ?? Date.now(),
457
+ caps,
458
+ omitted: ctx.omitted,
459
+ runs: sorted.slice(0, caps.maxRuns).map((job) => projectRun(job, ctx)),
460
+ };
461
+ enforceByteLimit(snapshot);
462
+ return snapshot;
463
+ }
@@ -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(status: Pick<AsyncStatus, "runId" | "steps">, childId: string): AsyncStatusChildResolution {
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 (!asyncStatusChildIdentityCandidates(step, index).includes(childId)) continue;
27
- matches.push({ index, step, id: asyncStatusChildIdentity(step, index) });
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}'.` };
@@ -0,0 +1,151 @@
1
+ import * as path from "node:path";
2
+ import type { AgentConfig } from "../../agents/agents.ts";
3
+ import { resolveChildCwd } from "../../shared/utils.ts";
4
+ import type { OutputMode } from "../../shared/types.ts";
5
+ import { resolveSingleOutputPath } from "./single-output.ts";
6
+
7
+ export interface ResolvedStepBehavior {
8
+ output: string | false;
9
+ outputMode: OutputMode;
10
+ reads: string[] | false;
11
+ progress: boolean;
12
+ skills: string[] | false;
13
+ model?: string;
14
+ fast?: boolean;
15
+ }
16
+
17
+ export type OutputOverrideInput = string | boolean;
18
+
19
+ export interface StepOverrides {
20
+ output?: OutputOverrideInput;
21
+ outputMode?: OutputMode;
22
+ reads?: string[] | false;
23
+ progress?: boolean;
24
+ skills?: string[] | false;
25
+ model?: string;
26
+ fast?: boolean;
27
+ }
28
+
29
+ export interface ChildLaunchPlanInput {
30
+ agentConfig: AgentConfig;
31
+ stepOverrides: StepOverrides;
32
+ task?: string;
33
+ originalTask?: string;
34
+ runnerCwd: string;
35
+ runtimeCwd: string;
36
+ stepCwdInput?: string;
37
+ behaviorCwd?: string;
38
+ chainSkills?: string[];
39
+ outputBaseDir?: string;
40
+ parallelOutputNamespace?: { stepIndex: number; taskIndex?: number };
41
+ resolvedBehavior?: ResolvedStepBehavior;
42
+ }
43
+
44
+ export interface ChildLaunchPlan {
45
+ stepCwd: string;
46
+ instructionCwd: string;
47
+ readExistenceCwd: string;
48
+ behavior: ResolvedStepBehavior;
49
+ inheritedRelativeParallelOutput: boolean;
50
+ namespaceOutputPath: boolean;
51
+ outputPath?: string;
52
+ skillNames: string[];
53
+ }
54
+
55
+ export function normalizeOutputOverride(output: unknown): string | false | undefined {
56
+ if (output === false || output === "false") return false;
57
+ if (output === true || output === "true") return undefined;
58
+ return typeof output === "string" && output.length > 0 ? output : undefined;
59
+ }
60
+
61
+ export function resolveStepBehavior(
62
+ agentConfig: AgentConfig,
63
+ stepOverrides: StepOverrides,
64
+ chainSkills?: string[],
65
+ ): ResolvedStepBehavior {
66
+ const stepOutput = normalizeOutputOverride(stepOverrides.output);
67
+ const output = stepOutput !== undefined
68
+ ? stepOutput
69
+ : normalizeOutputOverride(agentConfig.output) ?? false;
70
+
71
+ const reads = stepOverrides.reads !== undefined
72
+ ? stepOverrides.reads
73
+ : agentConfig.defaultReads ?? false;
74
+
75
+ const progress = stepOverrides.progress !== undefined
76
+ ? stepOverrides.progress
77
+ : agentConfig.defaultProgress ?? false;
78
+
79
+ let skills: string[] | false;
80
+ if (stepOverrides.skills === false) {
81
+ skills = false;
82
+ } else if (stepOverrides.skills !== undefined) {
83
+ skills = [...stepOverrides.skills];
84
+ if (chainSkills && chainSkills.length > 0) {
85
+ skills = [...new Set([...skills, ...chainSkills])];
86
+ }
87
+ } else {
88
+ skills = agentConfig.skills ? [...agentConfig.skills] : [];
89
+ if (chainSkills && chainSkills.length > 0) {
90
+ skills = [...new Set([...skills, ...chainSkills])];
91
+ }
92
+ }
93
+
94
+ const outputMode = stepOverrides.outputMode ?? agentConfig.outputMode ?? "inline";
95
+ const model = stepOverrides.model ?? agentConfig.model;
96
+ const fast = stepOverrides.fast ?? agentConfig.fast;
97
+ return { output, outputMode, reads, progress, skills, model, fast };
98
+ }
99
+
100
+ export function resolveTaskTextForFileUpdatePolicy(task: string | undefined, originalTask?: string): string | undefined {
101
+ if (!task) return originalTask;
102
+ return originalTask ? task.replaceAll("{task}", originalTask) : task;
103
+ }
104
+
105
+ export function taskDisallowsFileUpdates(task: string | undefined): boolean {
106
+ if (!task) return false;
107
+ return /\breview[- ]only\b/i.test(task)
108
+ || /\bread[- ]only\s+(?:review|audit|inspection|pass)\b/i.test(task)
109
+ || /\b(?:no|without)\s+(?:file\s+)?edits?\b/i.test(task)
110
+ || /\b(?:do not|don't|must not)\s+(?:edit|modify|write|touch)\b/i.test(task)
111
+ || /\bleave\s+files?\s+unchanged\b/i.test(task);
112
+ }
113
+
114
+ export function suppressProgressForReadOnlyTask(behavior: ResolvedStepBehavior, task: string | undefined, originalTask?: string): ResolvedStepBehavior {
115
+ const policyTask = resolveTaskTextForFileUpdatePolicy(task, originalTask);
116
+ return behavior.progress && taskDisallowsFileUpdates(policyTask) ? { ...behavior, progress: false } : behavior;
117
+ }
118
+
119
+ export function planChildLaunch(input: ChildLaunchPlanInput): ChildLaunchPlan {
120
+ const stepCwd = resolveChildCwd(input.runnerCwd, input.stepCwdInput);
121
+ const instructionCwd = input.behaviorCwd ?? stepCwd;
122
+ const readExistenceCwd = input.behaviorCwd ? stepCwd : instructionCwd;
123
+ let behavior = suppressProgressForReadOnlyTask(
124
+ input.resolvedBehavior ?? resolveStepBehavior(input.agentConfig, input.stepOverrides, input.chainSkills),
125
+ input.task,
126
+ input.originalTask,
127
+ );
128
+
129
+ const inheritedRelativeParallelOutput = Boolean(
130
+ input.parallelOutputNamespace
131
+ && input.stepOverrides.output === undefined
132
+ && typeof behavior.output === "string"
133
+ && !path.isAbsolute(behavior.output),
134
+ );
135
+ if (inheritedRelativeParallelOutput && input.parallelOutputNamespace?.taskIndex !== undefined) {
136
+ behavior = {
137
+ ...behavior,
138
+ output: path.join(
139
+ `parallel-${input.parallelOutputNamespace.stepIndex}`,
140
+ `${input.parallelOutputNamespace.taskIndex}-${input.agentConfig.name}`,
141
+ behavior.output as string,
142
+ ),
143
+ };
144
+ }
145
+
146
+ const namespaceOutputPath = inheritedRelativeParallelOutput && input.parallelOutputNamespace?.taskIndex === undefined;
147
+ const skillNames = behavior.skills === false ? [] : behavior.skills;
148
+ const outputPath = resolveSingleOutputPath(behavior.output, input.runtimeCwd, instructionCwd, input.outputBaseDir);
149
+
150
+ return { stepCwd, instructionCwd, readExistenceCwd, behavior, inheritedRelativeParallelOutput, namespaceOutputPath, outputPath, skillNames };
151
+ }