pi-subagents 0.65.0 → 0.66.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 +87 -0
- package/README.md +1 -1
- package/agents/researcher.md +23 -13
- package/docs/agents.md +17 -3
- package/docs/configuration.md +34 -0
- package/docs/extension-api.md +94 -0
- package/docs/models.md +58 -1
- package/docs/observability.md +42 -2
- package/docs/tool-reference.md +11 -5
- package/docs/workflows.md +22 -7
- package/package.json +4 -1
- package/runner-server-preload.mjs +13 -0
- package/skills/pi-subagents/SKILL.md +2 -1
- package/skills/pi-subagents/references/execution-controls.md +19 -2
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- package/src/agents/advertised-agent-prompt.ts +63 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +8 -0
- package/src/api/preflight.ts +5 -1
- package/src/api/shared-types.ts +1 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/config.ts +4 -2
- package/src/extension/index.ts +71 -4
- package/src/extension/public-execution.ts +0 -1
- package/src/extension/rpc.ts +4 -21
- package/src/extension/schemas.ts +9 -7
- package/src/extension/tool-description.ts +10 -5
- package/src/integrations/pi-web-session-liveness.ts +73 -0
- package/src/intercom/native-supervisor-channel.ts +102 -88
- package/src/intercom/supervisor-ui.ts +3 -2
- package/src/missions/workflow-state.ts +37 -16
- package/src/runs/background/active-async-capacity.ts +18 -18
- package/src/runs/background/async-execution.ts +8 -1
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-retention.ts +9 -0
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +40 -29
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/notify.ts +88 -12
- package/src/runs/background/owned-process-tree.ts +6 -6
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/src/runs/background/run-child-session.ts +62 -33
- package/src/runs/background/run-status.ts +75 -5
- package/src/runs/background/runner-aliases.ts +46 -8
- package/src/runs/background/runner-child-launch.ts +86 -0
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/subagent-runner.ts +430 -208
- package/src/runs/background/subagent-wait.ts +3 -0
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/async-steering-action.ts +19 -0
- package/src/runs/foreground/execution.ts +116 -26
- package/src/runs/foreground/foreground-history.ts +3 -1
- package/src/runs/foreground/prompt-audit.ts +9 -5
- package/src/runs/foreground/subagent-executor.ts +531 -227
- package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
- package/src/runs/shared/acceptance.ts +16 -3
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +47 -47
- package/src/runs/shared/child-hooks.ts +151 -2
- package/src/runs/shared/child-launch.ts +18 -13
- package/src/runs/shared/child-session.ts +55 -24
- package/src/runs/shared/child-tool-plan.ts +2 -2
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +1 -0
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +30 -20
- package/src/runs/shared/model-exclusions.ts +2 -1
- package/src/runs/shared/model-fallback.ts +41 -8
- package/src/runs/shared/nested-events.ts +8 -8
- package/src/runs/shared/orca-progress-tabs.ts +6 -0
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +3 -2
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
- package/src/runs/shared/worktree-cleanup-plan.ts +6 -3
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +403 -210
- package/src/shared/model-response-aliases.ts +13 -0
- package/src/shared/types.ts +89 -60
- package/src/shared/utils.ts +10 -2
- package/src/shared/watch-strategy.ts +2 -0
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/tui/fleet-status.ts +1 -1
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +44 -15
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +70 -16
- package/src/workflows/workflow-checklist.ts +15 -18
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
package/src/tui/render.ts
CHANGED
|
@@ -1284,7 +1284,7 @@ function workflowChecklistItemPriority(state: WorkflowChecklistState): number {
|
|
|
1284
1284
|
return 6;
|
|
1285
1285
|
}
|
|
1286
1286
|
|
|
1287
|
-
function workflowChecklistItemLine(item: WorkflowChecklistItem, theme: Theme, indent: string, frame
|
|
1287
|
+
function workflowChecklistItemLine(item: WorkflowChecklistItem, theme: Theme, indent: string, frame: number | undefined, includeError: boolean): string {
|
|
1288
1288
|
const identity = [item.label, item.agent && item.agent !== item.label ? item.agent : undefined].filter(Boolean).join(" · ");
|
|
1289
1289
|
const context = contextModeBadge(theme, item.context);
|
|
1290
1290
|
const state = item.state === "complete" ? "" : ` ${theme.fg("dim", `· ${workflowChecklistStateLabel(item.state)}`)}`;
|
|
@@ -1294,15 +1294,26 @@ function workflowChecklistItemLine(item: WorkflowChecklistItem, theme: Theme, in
|
|
|
1294
1294
|
!item.currentTool && item.durationMs !== undefined ? formatDuration(item.durationMs) : undefined,
|
|
1295
1295
|
item.toolCount !== undefined ? `${item.toolCount} tools` : undefined,
|
|
1296
1296
|
item.outputName ? `out:${item.outputName}` : undefined,
|
|
1297
|
-
item.error ? `error:${oneLine(item.error)}` : undefined,
|
|
1297
|
+
includeError && item.error ? `error:${oneLine(item.error)}` : undefined,
|
|
1298
1298
|
].filter(Boolean).join(" · ");
|
|
1299
1299
|
return `${indent}${workflowChecklistGlyph(item, theme, frame)} ${theme.bold(identity || item.key)}${context}${state}${details ? ` ${theme.fg("dim", `· ${details}`)}` : ""}`;
|
|
1300
1300
|
}
|
|
1301
1301
|
|
|
1302
1302
|
const COLLAPSED_WORKFLOW_PHASE_LIMIT = 4;
|
|
1303
1303
|
|
|
1304
|
-
|
|
1304
|
+
interface WorkflowChecklistWidgetOptions {
|
|
1305
|
+
includeSummary?: boolean;
|
|
1306
|
+
limitPhases?: boolean;
|
|
1307
|
+
includeBottleneckOutput?: boolean;
|
|
1308
|
+
includeItemErrors?: boolean;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function workflowChecklistWidgetLines(checklist: WorkflowChecklistProjection | undefined, theme: Theme, indent: string, expanded: boolean, frame?: number, options: WorkflowChecklistWidgetOptions = {}): string[] {
|
|
1305
1312
|
if (!checklist?.total) return [];
|
|
1313
|
+
const includeSummary = options.includeSummary ?? true;
|
|
1314
|
+
const limitPhases = options.limitPhases ?? !expanded;
|
|
1315
|
+
const includeBottleneckOutput = options.includeBottleneckOutput ?? expanded;
|
|
1316
|
+
const includeItemErrors = options.includeItemErrors ?? true;
|
|
1306
1317
|
const lines = includeSummary ? [`${indent}${theme.fg("dim", `Checklist ${formatWorkflowChecklistSummary(checklist)}`)}`] : [];
|
|
1307
1318
|
const phases = !limitPhases || checklist.phases.length <= COLLAPSED_WORKFLOW_PHASE_LIMIT
|
|
1308
1319
|
? checklist.phases
|
|
@@ -1326,11 +1337,11 @@ function workflowChecklistWidgetLines(checklist: WorkflowChecklistProjection | u
|
|
|
1326
1337
|
lines.push(`${indent}${glyph} ${theme.bold(formatWorkflowChecklistPhase(phase))}`);
|
|
1327
1338
|
if (expanded) {
|
|
1328
1339
|
for (const item of [...phase.items].sort((left, right) => workflowChecklistItemPriority(left.state) - workflowChecklistItemPriority(right.state))) {
|
|
1329
|
-
lines.push(workflowChecklistItemLine(item, theme, `${indent} `, frame));
|
|
1340
|
+
lines.push(workflowChecklistItemLine(item, theme, `${indent} `, frame, includeItemErrors || item.kind === "host"));
|
|
1330
1341
|
}
|
|
1331
1342
|
}
|
|
1332
1343
|
}
|
|
1333
|
-
const bottleneck = formatWorkflowChecklistBottleneck(checklist.bottleneck, { includeOutput: includeBottleneckOutput });
|
|
1344
|
+
const bottleneck = formatWorkflowChecklistBottleneck(checklist.bottleneck, { includeOutput: includeBottleneckOutput, includeError: !expanded });
|
|
1334
1345
|
if (bottleneck) {
|
|
1335
1346
|
const tone = checklist.bottleneck?.state === "blocked" || checklist.bottleneck?.state === "failed" ? "error" : checklist.bottleneck?.state === "running" ? "accent" : "warning";
|
|
1336
1347
|
lines.push(`${indent}${theme.fg(tone, `bottleneck · ${bottleneck}`)}`);
|
|
@@ -2371,7 +2382,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
2371
2382
|
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
|
|
2372
2383
|
const lines: string[] = [
|
|
2373
2384
|
...(expanded ? workflowPreflightLines(job) : []),
|
|
2374
|
-
...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame),
|
|
2385
|
+
...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame, { includeItemErrors: !expanded }),
|
|
2375
2386
|
];
|
|
2376
2387
|
const group = activeParallelWidgetGroup(job);
|
|
2377
2388
|
if (group) {
|
|
@@ -2711,16 +2722,29 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[
|
|
|
2711
2722
|
return rendered.lines;
|
|
2712
2723
|
}
|
|
2713
2724
|
|
|
2714
|
-
|
|
2715
|
-
|
|
2725
|
+
const asyncWidgetUpdates = new WeakMap<ExtensionContext["ui"], (jobs: AsyncJobState[]) => void>();
|
|
2726
|
+
|
|
2727
|
+
function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"]): (tui: { requestRender(): void }, theme: Theme) => Component {
|
|
2728
|
+
return (tui, theme) => {
|
|
2716
2729
|
const container = new Container();
|
|
2717
2730
|
let cachedRenderWidth: number | undefined;
|
|
2718
2731
|
let cachedFrame: number | undefined;
|
|
2719
2732
|
let cachedExpanded: boolean | undefined;
|
|
2720
2733
|
let cachedLines: string[] | undefined;
|
|
2734
|
+
const update = (nextJobs: AsyncJobState[]): void => {
|
|
2735
|
+
jobs = nextJobs;
|
|
2736
|
+
cachedLines = undefined;
|
|
2737
|
+
tui.requestRender();
|
|
2738
|
+
};
|
|
2739
|
+
asyncWidgetUpdates.set(ui, update);
|
|
2740
|
+
const component = Object.assign(container, {
|
|
2741
|
+
dispose(): void {
|
|
2742
|
+
if (asyncWidgetUpdates.get(ui) === update) asyncWidgetUpdates.delete(ui);
|
|
2743
|
+
},
|
|
2744
|
+
});
|
|
2721
2745
|
container.render = (renderWidth: number): string[] => {
|
|
2722
2746
|
const frame = Math.floor(Date.now() / WIDGET_ANIMATION_INTERVAL_MS);
|
|
2723
|
-
const expanded =
|
|
2747
|
+
const expanded = ui.getToolsExpanded?.() ?? false;
|
|
2724
2748
|
if (cachedLines && cachedRenderWidth === renderWidth && cachedFrame === frame && cachedExpanded === expanded) return cachedLines;
|
|
2725
2749
|
const width = Math.max(0, renderWidth - 2);
|
|
2726
2750
|
const projectionFor = workflowWidgetProjectionLookup();
|
|
@@ -2735,7 +2759,7 @@ function buildWidgetComponent(jobs: AsyncJobState[], isExpanded: () => boolean):
|
|
|
2735
2759
|
cachedLines = fitAdaptiveWidgetLines(jobs, buildLines, theme, width, expanded, frame, projectionFor).map((line) => paddedWidgetLine(line, renderWidth));
|
|
2736
2760
|
return cachedLines;
|
|
2737
2761
|
};
|
|
2738
|
-
return
|
|
2762
|
+
return component;
|
|
2739
2763
|
};
|
|
2740
2764
|
}
|
|
2741
2765
|
|
|
@@ -2767,7 +2791,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
|
|
|
2767
2791
|
: [
|
|
2768
2792
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
2769
2793
|
...widgetLaneDetailLines(job, theme, projection),
|
|
2770
|
-
...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame),
|
|
2794
|
+
...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame, { includeItemErrors: !expanded || !job.steps?.length }),
|
|
2771
2795
|
...widgetParallelAgentDetails(job, theme, expanded, width, frame),
|
|
2772
2796
|
];
|
|
2773
2797
|
items.push([
|
|
@@ -2830,6 +2854,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
2830
2854
|
export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
|
|
2831
2855
|
if (jobs.length === 0) {
|
|
2832
2856
|
resetWidgetLayoutSession();
|
|
2857
|
+
asyncWidgetUpdates.delete(ctx.ui);
|
|
2833
2858
|
if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
2834
2859
|
return;
|
|
2835
2860
|
}
|
|
@@ -2838,7 +2863,11 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
|
|
|
2838
2863
|
ctx.ui.setWidget(WIDGET_KEY, encodeAsyncStatusSnapshotWidget(jobs));
|
|
2839
2864
|
return;
|
|
2840
2865
|
}
|
|
2841
|
-
|
|
2866
|
+
// Pi replaces a widget by deleting and reinserting its key. Update the mounted
|
|
2867
|
+
// component instead so progress cannot move it past other extensions' widgets.
|
|
2868
|
+
const update = asyncWidgetUpdates.get(ctx.ui);
|
|
2869
|
+
if (update) update(jobs);
|
|
2870
|
+
else ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui));
|
|
2842
2871
|
}
|
|
2843
2872
|
|
|
2844
2873
|
function renderSingleCompact(
|
|
@@ -2991,7 +3020,7 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
|
|
|
2991
3020
|
if (d.preflight) c.addChild(new Text(truncLine(theme.fg("dim", formatWorkflowPreflightPlanSummary(d.preflight, { indent: rowIndent })), width), 0, 0));
|
|
2992
3021
|
if (phase) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Phase ${phase}`), width), 0, 0));
|
|
2993
3022
|
const checklist = projectWorkflowChecklist({ hostSteps: workflow?.receipt?.hostSteps, preflight: d.preflight, trace: workflow?.trace, now: Date.now() });
|
|
2994
|
-
for (const line of workflowChecklistWidgetLines(checklist, theme, rowIndent, false, frame,
|
|
3023
|
+
for (const line of workflowChecklistWidgetLines(checklist, theme, rowIndent, false, frame, { limitPhases: !expanded, includeBottleneckOutput: expanded })) {
|
|
2995
3024
|
c.addChild(new Text(truncLine(line, width), 0, 0));
|
|
2996
3025
|
}
|
|
2997
3026
|
if (rows.length === 0) {
|
|
@@ -3073,7 +3102,7 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
|
|
|
3073
3102
|
const detailIndent = mainWindowIndent(layout, 2);
|
|
3074
3103
|
c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold(d.mode))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
|
|
3075
3104
|
if (checklistPrimary) {
|
|
3076
|
-
for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, rowIndent, false, frame, false)) {
|
|
3105
|
+
for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, rowIndent, false, frame, { includeSummary: false })) {
|
|
3077
3106
|
c.addChild(new Text(truncLine(line, width), 0, 0));
|
|
3078
3107
|
}
|
|
3079
3108
|
if (hasRunning || workflowChecklist.running > 0) c.addChild(new Text(truncLine(theme.fg("accent", `${rowIndent}${liveDetailHintText()}`), width), 0, 0));
|
|
@@ -3424,7 +3453,7 @@ export function renderSubagentResult(
|
|
|
3424
3453
|
c.addChild(new Text(fit(` ${chainVis}`), 0, 0));
|
|
3425
3454
|
}
|
|
3426
3455
|
const workflowChecklist = foregroundWorkflowChecklist(d);
|
|
3427
|
-
for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, " ", true, frame)) {
|
|
3456
|
+
for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, " ", true, frame, { includeItemErrors: false })) {
|
|
3428
3457
|
c.addChild(new Text(fit(line), 0, 0));
|
|
3429
3458
|
}
|
|
3430
3459
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import type { Details,
|
|
4
|
+
import type { Details, WorkflowPreflightLane, WorkflowPreflight } from "../shared/types.ts";
|
|
5
5
|
import { workflowPreflightLaneForRuntimeKey } from "./workflow-preflight.ts";
|
|
6
6
|
|
|
7
7
|
export const WORKFLOW_CHAT_PROGRESS_MODES = ["auto", "off", "live-card"] as const;
|
|
@@ -103,14 +103,14 @@ export interface WorkflowChatProgressRow {
|
|
|
103
103
|
runId?: string;
|
|
104
104
|
durationMs?: number;
|
|
105
105
|
error?: string;
|
|
106
|
-
preflight?:
|
|
106
|
+
preflight?: WorkflowPreflightLane;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
function cleanLabel(value: unknown): string | undefined {
|
|
110
110
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
export function buildWorkflowChatProgressRows(trace: NonNullable<Details["workflow"]>["trace"], preflight?:
|
|
113
|
+
export function buildWorkflowChatProgressRows(trace: NonNullable<Details["workflow"]>["trace"], preflight?: WorkflowPreflight): WorkflowChatProgressRow[] {
|
|
114
114
|
const rows = new Map<string, WorkflowChatProgressRow>();
|
|
115
115
|
for (const entry of trace) {
|
|
116
116
|
if (entry.operation !== "run") continue;
|
|
@@ -5,10 +5,12 @@ import { Worker } from "node:worker_threads";
|
|
|
5
5
|
import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../runs/shared/parallel-utils.ts";
|
|
6
6
|
import { HOST_STEP_MAX_COUNT } from "../runs/shared/host-step-status.ts";
|
|
7
7
|
import { classifyTaskMutationIntent } from "../runs/shared/task-intent.ts";
|
|
8
|
-
import
|
|
8
|
+
import { describeGateAcceptanceConflict } from "../runs/shared/acceptance.ts";
|
|
9
|
+
import type { AcceptanceRecoveryMetadata, HostStepNode, SingleResult } from "../shared/types.ts";
|
|
9
10
|
import { normalizeWorkflowHostCommandParams, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "./host-command.ts";
|
|
10
11
|
|
|
11
12
|
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
13
|
+
const BASE_REF_VALIDATION_ERROR = "baseRef must be a valid Git ref: use HEAD or a supported named ref (for example, refs/heads/main). Full 40/64-character commit IDs and revision expressions are unsupported.";
|
|
12
14
|
function validGitRef(ref: unknown): ref is string {
|
|
13
15
|
if (typeof ref !== "string" || !ref || ref === "@" || Buffer.byteLength(ref, "utf-8") > 1024 || ref.startsWith("/") || ref.endsWith("/") || ref.includes("//") || ref.includes("..") || ref.includes("@{")) return false;
|
|
14
16
|
if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(ref)) return false;
|
|
@@ -74,6 +76,12 @@ function stableRunJson(value) {
|
|
|
74
76
|
return JSON.stringify(value) ?? "undefined";
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
function canonicalRunParams(params) {
|
|
80
|
+
if (params.gate === undefined || params.acceptance !== false) return params;
|
|
81
|
+
const { acceptance: _acceptance, ...withoutAcceptance } = params;
|
|
82
|
+
return withoutAcceptance;
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
function isDirectWorkflowScriptPromiseHandlerCall() {
|
|
78
86
|
const stack = new Error().stack;
|
|
79
87
|
if (typeof stack !== "string") return false;
|
|
@@ -445,7 +453,7 @@ function validateLaneSpecs(laneSpecs) {
|
|
|
445
453
|
validateLaneStageBounds(validationParams, stageLabel);
|
|
446
454
|
validateRunCall(generatedKey, validationParams, stageLabel, validationFingerprints);
|
|
447
455
|
const existingFingerprint = runFingerprints.get(generatedKey);
|
|
448
|
-
if (existingFingerprint !== undefined && (resume === "previous" || existingFingerprint !== stableRunJson(params))) {
|
|
456
|
+
if (existingFingerprint !== undefined && (resume === "previous" || existingFingerprint !== stableRunJson(canonicalRunParams(params)))) {
|
|
449
457
|
throw new Error("runs.lanes generated child key '" + generatedKey + "' is already used with incompatible launch params.");
|
|
450
458
|
}
|
|
451
459
|
stages.push({ key: stageKey, generatedKey, resume, params });
|
|
@@ -614,6 +622,19 @@ function validateLaneMetadata(value, label, workflowKey) {
|
|
|
614
622
|
}
|
|
615
623
|
}
|
|
616
624
|
|
|
625
|
+
function describeGateAcceptanceConflict(gate, acceptance) {
|
|
626
|
+
const render = (value) => {
|
|
627
|
+
let encoded;
|
|
628
|
+
try {
|
|
629
|
+
encoded = JSON.stringify(value) ?? String(value);
|
|
630
|
+
} catch {
|
|
631
|
+
encoded = String(value);
|
|
632
|
+
}
|
|
633
|
+
return encoded.length > 120 ? encoded.slice(0, 120) + "..." : encoded;
|
|
634
|
+
};
|
|
635
|
+
return " Both fields were present: gate=" + render(gate) + " acceptance=" + render(acceptance) + ".";
|
|
636
|
+
}
|
|
637
|
+
|
|
617
638
|
function validateRunCall(key, params, label, fingerprints) {
|
|
618
639
|
if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error(label + " has an invalid key.");
|
|
619
640
|
if (hostKeys.has(key)) throw new Error("Workflow key '" + key + "' is already used by runs.host.");
|
|
@@ -624,10 +645,10 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
624
645
|
}
|
|
625
646
|
if (Object.prototype.hasOwnProperty.call(params, "clarify")) throw new Error(label + " does not support clarify UI.");
|
|
626
647
|
if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
|
|
627
|
-
if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) throw new Error(label + " baseRef must be a valid Git ref.");
|
|
648
|
+
if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) throw new Error(label + " baseRef must be a valid Git ref: use HEAD or a supported named ref (for example, refs/heads/main). Full 40/64-character commit IDs and revision expressions are unsupported.");
|
|
628
649
|
validateLaneMetadata(params.lane, label + " lane", key);
|
|
629
650
|
if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
|
|
630
|
-
if (params.gate !== undefined && params.acceptance !== undefined) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify.");
|
|
651
|
+
if (params.gate !== undefined && params.acceptance !== undefined && params.acceptance !== false) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify." + describeGateAcceptanceConflict(params.gate, params.acceptance));
|
|
631
652
|
if (params.gate !== undefined && params.resume !== undefined) throw new Error(label + " gate is not supported with retained resume.");
|
|
632
653
|
if (params.extensionBindings !== undefined && params.resume !== undefined) throw new Error(label + " extensionBindings is not supported with retained resume; resume uses the original retained child binding.");
|
|
633
654
|
if (params.resume !== undefined && typeof params.resume !== "string") {
|
|
@@ -644,7 +665,7 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
644
665
|
if (params.resume !== undefined && (typeof params.task !== "string" || !params.task.trim())) throw new Error(label + " resume requires a non-empty task follow-up.");
|
|
645
666
|
validateExtensionBindings(params.extensionBindings, label);
|
|
646
667
|
assertJsonValue(params, label + " params");
|
|
647
|
-
const fingerprint = stableRunJson(params);
|
|
668
|
+
const fingerprint = stableRunJson(canonicalRunParams(params));
|
|
648
669
|
const existing = fingerprints.get(key);
|
|
649
670
|
if (existing !== undefined && existing !== fingerprint) throw new Error("Duplicate workflow key '" + key + "' used with incompatible launch params.");
|
|
650
671
|
fingerprints.set(key, fingerprint);
|
|
@@ -1090,11 +1111,11 @@ export interface RunWorkflowScriptOptions {
|
|
|
1090
1111
|
globalConcurrencyLimit?: number;
|
|
1091
1112
|
admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
|
|
1092
1113
|
launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean; batch: boolean }) => Promise<WorkflowScriptChildResult>;
|
|
1093
|
-
resolveResume?: (reference: WorkflowReceiptResumeReference, signal: AbortSignal) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
|
|
1114
|
+
resolveResume?: (reference: WorkflowReceiptResumeReference | string, signal: AbortSignal, index?: number) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
|
|
1094
1115
|
status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
|
|
1095
1116
|
steer?: (key: string, message: string, options: WorkflowSteerOptions, signal: AbortSignal) => Promise<WorkflowSteerResult>;
|
|
1096
1117
|
host?: (key: string, params: WorkflowHostCommandParams, signal: AbortSignal) => Promise<WorkflowHostCommandResult>;
|
|
1097
|
-
onHostStep?: (hostStep:
|
|
1118
|
+
onHostStep?: (hostStep: HostStepNode) => void;
|
|
1098
1119
|
state?: {
|
|
1099
1120
|
get: (key: string) => unknown | Promise<unknown>;
|
|
1100
1121
|
set: (key: string, value: unknown) => void | Promise<void>;
|
|
@@ -1370,6 +1391,12 @@ function stableJson(value: unknown): string {
|
|
|
1370
1391
|
return JSON.stringify(value) ?? "undefined";
|
|
1371
1392
|
}
|
|
1372
1393
|
|
|
1394
|
+
function canonicalRunParams(params: Record<string, unknown>): Record<string, unknown> {
|
|
1395
|
+
if (params.gate === undefined || params.acceptance !== false) return params;
|
|
1396
|
+
const { acceptance: _acceptance, ...withoutAcceptance } = params;
|
|
1397
|
+
return withoutAcceptance;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1373
1400
|
function validateKey(value: unknown, owner = "runs.run"): string {
|
|
1374
1401
|
if (typeof value !== "string" || !KEY_PATTERN.test(value)) {
|
|
1375
1402
|
throw new Error(`${owner} key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.`);
|
|
@@ -1476,7 +1503,7 @@ function definitelyNonJson(node: AstNode, normalizeUndefined = false): string |
|
|
|
1476
1503
|
if (node.type === "ObjectExpression" && Array.isArray(node.properties)) {
|
|
1477
1504
|
const values = new Map<string, AstNode>();
|
|
1478
1505
|
for (const property of node.properties) {
|
|
1479
|
-
if (!astNode(property) || property.type !== "Property" || !astNode(property.value)) return undefined;
|
|
1506
|
+
if (!astNode(property) || property.type !== "Property" || !astNode(property.value) || property.kind !== "init") return undefined;
|
|
1480
1507
|
const key = staticPropertyKey(property);
|
|
1481
1508
|
if (key === undefined) return undefined;
|
|
1482
1509
|
values.set(key, property.value);
|
|
@@ -1505,6 +1532,26 @@ function directObjectPropertyValue(node: AstNode, name: string): AstNode | undef
|
|
|
1505
1532
|
return value;
|
|
1506
1533
|
}
|
|
1507
1534
|
|
|
1535
|
+
function validateStaticBaseRef(params: AstNode, owner: string): WorkflowScriptValidationError[] {
|
|
1536
|
+
if (params.type !== "ObjectExpression" || !Array.isArray(params.properties)) return [];
|
|
1537
|
+
// Inspect the final definition only. A later spread or unknown key may overwrite it.
|
|
1538
|
+
for (let index = params.properties.length - 1; index >= 0; index--) {
|
|
1539
|
+
const property = params.properties[index];
|
|
1540
|
+
if (!astNode(property) || property.type !== "Property") return [];
|
|
1541
|
+
const key = staticPropertyKey(property);
|
|
1542
|
+
if (key === undefined) return [];
|
|
1543
|
+
if (key !== "baseRef") continue;
|
|
1544
|
+
if (property.kind !== "init" || !astNode(property.value)) return [];
|
|
1545
|
+
const valueNode = property.value;
|
|
1546
|
+
const value = literalString(valueNode);
|
|
1547
|
+
if ((value !== undefined || valueNode.type === "Literal") && !validGitRef(value)) {
|
|
1548
|
+
return [{ message: `${owner} ${BASE_REF_VALIDATION_ERROR}`, ...nodeLocation(valueNode) }];
|
|
1549
|
+
}
|
|
1550
|
+
return [];
|
|
1551
|
+
}
|
|
1552
|
+
return [];
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1508
1555
|
function directRunsAllKeys(call: AstNode): Array<{ key: string; node: AstNode }> {
|
|
1509
1556
|
const args = Array.isArray(call.arguments) ? call.arguments : [];
|
|
1510
1557
|
const items = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0].elements : [];
|
|
@@ -1546,6 +1593,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
|
|
|
1546
1593
|
const key = literalString(keyNode);
|
|
1547
1594
|
if (keyNode && key !== undefined && !KEY_PATTERN.test(key)) errors.push({ message: "runs.run key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.", ...nodeLocation(keyNode) });
|
|
1548
1595
|
if (astNode(args[1])) {
|
|
1596
|
+
errors.push(...validateStaticBaseRef(args[1], "runs.run"));
|
|
1549
1597
|
const message = definitelyNonJson(args[1]);
|
|
1550
1598
|
if (message) errors.push({ message: `runs.run params are invalid: ${message}.`, ...nodeLocation(args[1]) });
|
|
1551
1599
|
}
|
|
@@ -1555,6 +1603,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
|
|
|
1555
1603
|
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
1556
1604
|
if (astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements)) {
|
|
1557
1605
|
for (const item of args[0].elements) if (astNode(item)) {
|
|
1606
|
+
errors.push(...validateStaticBaseRef(item, "runs.all item"));
|
|
1558
1607
|
const message = definitelyNonJson(item);
|
|
1559
1608
|
if (message) errors.push({ message: `runs.all item params are invalid: ${message}.`, ...nodeLocation(item) });
|
|
1560
1609
|
}
|
|
@@ -1725,7 +1774,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1725
1774
|
console.error("Workflow onLanePlan callback failed:", error);
|
|
1726
1775
|
}
|
|
1727
1776
|
};
|
|
1728
|
-
const hostStepChanged = (hostStep:
|
|
1777
|
+
const hostStepChanged = (hostStep: HostStepNode) => {
|
|
1729
1778
|
try {
|
|
1730
1779
|
options.onHostStep?.(hostStep);
|
|
1731
1780
|
} catch (error) {
|
|
@@ -2021,7 +2070,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2021
2070
|
if (!options.host) return respond(Promise.reject(new Error("runs.host is unavailable in this host context.")));
|
|
2022
2071
|
if (hostCalls.size >= HOST_STEP_MAX_COUNT) return respond(Promise.reject(new Error(`workflowScript supports at most ${HOST_STEP_MAX_COUNT} runs.host calls.`)));
|
|
2023
2072
|
const startedAt = Date.now();
|
|
2024
|
-
const startedStep:
|
|
2073
|
+
const startedStep: HostStepNode = {
|
|
2025
2074
|
version: 1,
|
|
2026
2075
|
kind: "host-step",
|
|
2027
2076
|
monitorKind: "command",
|
|
@@ -2082,7 +2131,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2082
2131
|
}
|
|
2083
2132
|
return result;
|
|
2084
2133
|
});
|
|
2085
|
-
const fingerprint = stableJson(params);
|
|
2134
|
+
const fingerprint = stableJson(canonicalRunParams(params));
|
|
2086
2135
|
const existing = launches.get(key);
|
|
2087
2136
|
if (existing) {
|
|
2088
2137
|
if (existing.fingerprint !== fingerprint) return respond(Promise.reject(new Error(`Duplicate workflow key '${key}' used with incompatible launch params.`)));
|
|
@@ -2104,13 +2153,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2104
2153
|
return respond(Promise.reject(new Error(`runs.run('${key}') worktree must be true or false.`)));
|
|
2105
2154
|
}
|
|
2106
2155
|
if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) {
|
|
2107
|
-
return respond(Promise.reject(new Error(`runs.run('${key}')
|
|
2156
|
+
return respond(Promise.reject(new Error(`runs.run('${key}') ${BASE_REF_VALIDATION_ERROR}`)));
|
|
2108
2157
|
}
|
|
2109
2158
|
if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) {
|
|
2110
2159
|
return respond(Promise.reject(new Error(`runs.run('${key}') gate must be a non-empty command string.`)));
|
|
2111
2160
|
}
|
|
2112
|
-
if (params.gate !== undefined && params.acceptance !== undefined) {
|
|
2113
|
-
return respond(Promise.reject(new Error(`runs.run('${key}') gate cannot be combined with acceptance; use one gate command or acceptance.verify.`)));
|
|
2161
|
+
if (params.gate !== undefined && params.acceptance !== undefined && params.acceptance !== false) {
|
|
2162
|
+
return respond(Promise.reject(new Error(`runs.run('${key}') gate cannot be combined with acceptance; use one gate command or acceptance.verify.` + describeGateAcceptanceConflict(params.gate, params.acceptance))));
|
|
2114
2163
|
}
|
|
2115
2164
|
if (params.gate !== undefined && params.resume !== undefined) {
|
|
2116
2165
|
return respond(Promise.reject(new Error(`runs.run('${key}') gate is not supported with retained resume.`)));
|
|
@@ -2157,10 +2206,11 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2157
2206
|
const childStopController = new AbortController();
|
|
2158
2207
|
childStopControllers.set(key, childStopController);
|
|
2159
2208
|
const childSignal = combinedAbortSignal([childController.signal, childStopController.signal]);
|
|
2160
|
-
const
|
|
2209
|
+
const resumeInput = resumeReference ?? (typeof params.resume === "string" && options.resolveResume ? params.resume : undefined);
|
|
2210
|
+
const resolvedResumeValue = resumeInput
|
|
2161
2211
|
? await Promise.resolve().then(() => {
|
|
2162
2212
|
if (!options.resolveResume) throw new Error("Keyed workflow receipt resume is unavailable in this host.");
|
|
2163
|
-
return options.resolveResume(
|
|
2213
|
+
return options.resolveResume(resumeInput, childSignal, typeof params.index === "number" ? params.index : undefined);
|
|
2164
2214
|
})
|
|
2165
2215
|
: undefined;
|
|
2166
2216
|
const resolvedResume = typeof resolvedResumeValue === "string"
|
|
@@ -2176,6 +2226,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2176
2226
|
: [];
|
|
2177
2227
|
resolvedResumeLineage = [...new Set(lineage.length ? lineage : [resolvedResumeId!])];
|
|
2178
2228
|
if (resolvedResumeLineage.at(-1) !== resolvedResumeId) resolvedResumeLineage.push(resolvedResumeId!);
|
|
2229
|
+
if (typeof resumeInput === "string") {
|
|
2230
|
+
const predecessor = [...children.values()].find((child) => child.runId === resolvedResumeId);
|
|
2231
|
+
if (predecessor?.continuation && predecessor.continuation.runIds.at(-1) === resolvedResumeId) resolvedResumeLineage = predecessor.continuation.runIds;
|
|
2232
|
+
}
|
|
2179
2233
|
}
|
|
2180
2234
|
const launchParams = resolvedResumeId ? { ...params, resume: resolvedResumeId } : params;
|
|
2181
2235
|
await launchSemaphore.acquire();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { AsyncJobStep,
|
|
1
|
+
import type { AsyncJobStep, HostStepNode, WorkflowGraphNode, WorkflowGraphSnapshot, WorkflowPreflightLane, WorkflowPreflight } from "../shared/types.ts";
|
|
2
2
|
import { sanitizeDisplayText } from "../shared/display-text.ts";
|
|
3
|
-
import { workflowPreflightLaneForRuntimeKey } from "./workflow-preflight.ts";
|
|
3
|
+
import { workflowPreflightLaneForRuntimeKey as laneFor } from "./workflow-preflight.ts";
|
|
4
4
|
|
|
5
5
|
export type WorkflowChecklistState = "complete" | "running" | "queued" | "blocked" | "failed" | "paused" | "stopped";
|
|
6
6
|
|
|
@@ -62,12 +62,12 @@ export interface WorkflowChecklistItem {
|
|
|
62
62
|
toolCount?: number;
|
|
63
63
|
outputName?: string;
|
|
64
64
|
error?: string;
|
|
65
|
-
preflight?:
|
|
65
|
+
preflight?: WorkflowPreflightLane;
|
|
66
66
|
kind?: "child" | "host";
|
|
67
|
-
monitorKind?:
|
|
67
|
+
monitorKind?: HostStepNode["monitorKind"];
|
|
68
68
|
provider?: string;
|
|
69
69
|
role?: string;
|
|
70
|
-
verdict?:
|
|
70
|
+
verdict?: HostStepNode["verdict"];
|
|
71
71
|
target?: string;
|
|
72
72
|
reasonCode?: string;
|
|
73
73
|
stale?: boolean;
|
|
@@ -106,8 +106,8 @@ export interface WorkflowChecklistProjection {
|
|
|
106
106
|
export interface WorkflowChecklistInput {
|
|
107
107
|
graph?: WorkflowGraphSnapshot;
|
|
108
108
|
steps?: readonly WorkflowChecklistStep[] | readonly AsyncJobStep[];
|
|
109
|
-
hostSteps?: readonly
|
|
110
|
-
preflight?:
|
|
109
|
+
hostSteps?: readonly HostStepNode[];
|
|
110
|
+
preflight?: WorkflowPreflight;
|
|
111
111
|
trace?: readonly WorkflowChecklistTraceEntry[];
|
|
112
112
|
now?: number;
|
|
113
113
|
}
|
|
@@ -190,15 +190,11 @@ function duration(step: Pick<WorkflowChecklistStep, "durationMs" | "startedAt" |
|
|
|
190
190
|
return end === undefined ? undefined : Math.max(0, end - startedAt);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
function laneFor(preflight: WorkflowPreflightV1 | undefined, key: string, preferredKeys: readonly (string | undefined)[] = []): WorkflowPreflightLaneV1 | undefined {
|
|
194
|
-
return workflowPreflightLaneForRuntimeKey(preflight, key, preferredKeys);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
193
|
function stepKey(step: WorkflowChecklistStep): string | undefined {
|
|
198
194
|
return step.key ?? step.workflowKey ?? step.runId;
|
|
199
195
|
}
|
|
200
196
|
|
|
201
|
-
function stepItem(step: WorkflowChecklistStep, index: number, phase: string, key = stepKey(step) ?? `step-${index + 1}`, label = step.label ?? step.description ?? stepKey(step) ?? step.agent ?? key, preflight?:
|
|
197
|
+
function stepItem(step: WorkflowChecklistStep, index: number, phase: string, key = stepKey(step) ?? `step-${index + 1}`, label = step.label ?? step.description ?? stepKey(step) ?? step.agent ?? key, preflight?: WorkflowPreflightLane): WorkflowChecklistItem {
|
|
202
198
|
const state = checklistState(step);
|
|
203
199
|
return {
|
|
204
200
|
key: keyText(key, `step-${index + 1}`),
|
|
@@ -219,7 +215,7 @@ function stepItem(step: WorkflowChecklistStep, index: number, phase: string, key
|
|
|
219
215
|
};
|
|
220
216
|
}
|
|
221
217
|
|
|
222
|
-
function hostItem(host:
|
|
218
|
+
function hostItem(host: HostStepNode, phase: string, key = host.id): WorkflowChecklistItem {
|
|
223
219
|
const state = checklistState({ status: host.state, verdict: host.verdict, stale: host.freshness?.stale });
|
|
224
220
|
return {
|
|
225
221
|
key: keyText(key, "host-step"),
|
|
@@ -264,7 +260,7 @@ function traceSources(trace: readonly WorkflowChecklistTraceEntry[] | undefined)
|
|
|
264
260
|
return [...latest.values()];
|
|
265
261
|
}
|
|
266
262
|
|
|
267
|
-
function traceItem(entry: WorkflowChecklistTraceEntry, index: number, preflight:
|
|
263
|
+
function traceItem(entry: WorkflowChecklistTraceEntry, index: number, preflight: WorkflowPreflightLane | undefined): WorkflowChecklistItem {
|
|
268
264
|
const phase = keyText(preflight?.key ?? entry.generatedLaneKey ?? entry.phase, "Workflow");
|
|
269
265
|
const item = stepItem({ key: entry.key, label: entry.label, phase, agent: entry.agent, status: entry.state === "started" ? "running" : entry.state, durationMs: entry.durationMs, error: entry.error }, index, phase, entry.key, entry.label ?? entry.key, preflight);
|
|
270
266
|
if (entry.operation === "host") item.kind = "host";
|
|
@@ -284,7 +280,7 @@ function add(phases: Map<string, WorkflowChecklistPhase>, phase: string, item: W
|
|
|
284
280
|
phaseFor(phases, phase).items.push(item);
|
|
285
281
|
}
|
|
286
282
|
|
|
287
|
-
function mergeNodeStep(node: WorkflowGraphNode, step: WorkflowChecklistStep, phase: string, trace: WorkflowChecklistTraceEntry | undefined, preflight:
|
|
283
|
+
function mergeNodeStep(node: WorkflowGraphNode, step: WorkflowChecklistStep, phase: string, trace: WorkflowChecklistTraceEntry | undefined, preflight: WorkflowPreflightLane | undefined): WorkflowChecklistItem {
|
|
288
284
|
const state = checklistState(step);
|
|
289
285
|
const nodeState = checklistState({ status: node.status, acceptance: node.acceptanceStatus ? { status: node.acceptanceStatus } : undefined });
|
|
290
286
|
const status = TERMINAL_STATES.has(nodeState) && !TERMINAL_STATES.has(state)
|
|
@@ -402,11 +398,12 @@ export function formatWorkflowChecklistPhase(phase: WorkflowChecklistPhase): str
|
|
|
402
398
|
return counts.length ? `${phase.label} ${counts.join(" · ")}` : phase.label;
|
|
403
399
|
}
|
|
404
400
|
|
|
405
|
-
export function formatWorkflowChecklistBottleneck(item: WorkflowChecklistItem | undefined, options: { includeOutput?: boolean } = {}): string | undefined {
|
|
401
|
+
export function formatWorkflowChecklistBottleneck(item: WorkflowChecklistItem | undefined, options: { includeOutput?: boolean; includeError?: boolean } = {}): string | undefined {
|
|
406
402
|
if (!item) return undefined;
|
|
407
403
|
const identity = [item.label, item.agent && item.agent !== item.label ? item.agent : undefined].filter((value): value is string => Boolean(value)).join(" · ") || item.key;
|
|
408
404
|
const includeOutput = options.includeOutput ?? true;
|
|
409
|
-
const
|
|
405
|
+
const includeError = options.includeError ?? true;
|
|
406
|
+
const details = [item.context ? `(${item.context})` : undefined, item.currentTool ? `${item.currentTool}${item.durationMs !== undefined ? ` ${formatDurationText(item.durationMs)}` : ""}` : undefined, !item.currentTool && item.currentPath ? item.currentPath : undefined, !item.currentTool && item.durationMs !== undefined ? formatDurationText(item.durationMs) : undefined, item.toolCount !== undefined ? `${item.toolCount} tools` : undefined, includeOutput && item.outputName ? `out:${item.outputName}` : undefined, includeError && item.error ? `error:${item.error.replace(/\bOutput:/g, "output:")}` : undefined].filter((value): value is string => Boolean(value));
|
|
410
407
|
return [identity, ...details].join(" · ");
|
|
411
408
|
}
|
|
412
409
|
|
|
@@ -433,7 +430,7 @@ export function formatWorkflowChecklistText(projection: WorkflowChecklistProject
|
|
|
433
430
|
lines.push(`${indent} ${marker} ${formatWorkflowChecklistItem(item)}`);
|
|
434
431
|
}
|
|
435
432
|
}
|
|
436
|
-
const bottleneck = formatWorkflowChecklistBottleneck(projection.bottleneck);
|
|
433
|
+
const bottleneck = formatWorkflowChecklistBottleneck(projection.bottleneck, { includeError: options.includeItems === false });
|
|
437
434
|
if (bottleneck) lines.push(`${indent} bottleneck · ${bottleneck}`);
|
|
438
435
|
return lines;
|
|
439
436
|
}
|
|
@@ -1,9 +1,42 @@
|
|
|
1
|
-
import type { AsyncStatus,
|
|
1
|
+
import type { AgentProgress, AsyncStatus, WorkflowChildActivity, WorkflowChildSummary } from "../shared/types.ts";
|
|
2
2
|
import type { WorkflowScriptChildResult, WorkflowScriptTraceEntry } from "./scripted-workflow.ts";
|
|
3
3
|
|
|
4
4
|
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
5
5
|
const TERMINAL_STATES = new Set(["completed", "failed", "paused", "stopped", "rejected", "detached"]);
|
|
6
6
|
const MAX_REQUIRED_ID_BYTES = 4_096;
|
|
7
|
+
const ACTIVITY_COUNTERS = ["currentToolStartedAt", "lastActivityAt", "durationMs", "toolCount", "turnCount", "tokens", "inputTokens", "outputTokens"] as const;
|
|
8
|
+
type WorkflowChildLiveProgress = WorkflowChildActivity & Pick<AgentProgress, "agent" | "sessionName" | "model" | "thinking">;
|
|
9
|
+
|
|
10
|
+
export function workflowChildProgress(progress: AgentProgress): WorkflowChildLiveProgress {
|
|
11
|
+
return {
|
|
12
|
+
...workflowChildActivity(progress),
|
|
13
|
+
agent: bounded(progress.agent, 256) ?? "",
|
|
14
|
+
...(bounded(progress.sessionName, 256) ? { sessionName: progress.sessionName } : {}),
|
|
15
|
+
...(bounded(progress.model, 256) ? { model: progress.model } : {}),
|
|
16
|
+
...(bounded(progress.thinking, 32) ? { thinking: progress.thinking } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Fixed field count and a 256 UTF-8 byte tool name keep JSON activity below 2 KiB, including escaping. */
|
|
21
|
+
export function workflowChildActivity(progress: WorkflowChildActivity): WorkflowChildActivity {
|
|
22
|
+
const activity: WorkflowChildActivity = {};
|
|
23
|
+
if (bounded(progress.currentTool, 256)) activity.currentTool = progress.currentTool;
|
|
24
|
+
for (const field of ACTIVITY_COUNTERS) {
|
|
25
|
+
const value = progress[field];
|
|
26
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) activity[field] = value;
|
|
27
|
+
}
|
|
28
|
+
return activity;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseActivity(value: unknown): WorkflowChildActivity | undefined {
|
|
32
|
+
if (value === undefined) return undefined;
|
|
33
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("workflowChildren child activity is invalid.");
|
|
34
|
+
const input = value as Record<string, unknown>;
|
|
35
|
+
for (const [key, value] of Object.entries(input)) {
|
|
36
|
+
if (key === "currentTool" ? !bounded(value, 256) : !ACTIVITY_COUNTERS.includes(key as typeof ACTIVITY_COUNTERS[number]) || typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error("workflowChildren child activity is invalid.");
|
|
37
|
+
}
|
|
38
|
+
return { ...input } as WorkflowChildActivity;
|
|
39
|
+
}
|
|
7
40
|
|
|
8
41
|
function bounded(value: unknown, maxBytes: number): string | undefined {
|
|
9
42
|
if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value, "utf8") > maxBytes) return undefined;
|
|
@@ -19,13 +52,15 @@ function requiredId(value: string, label: string): string {
|
|
|
19
52
|
export function workflowChildSummary(input: {
|
|
20
53
|
parentToolCallId: string;
|
|
21
54
|
workflowRunId: string;
|
|
22
|
-
workflowState:
|
|
55
|
+
workflowState: WorkflowChildSummary["workflowState"];
|
|
23
56
|
inventoryComplete: boolean;
|
|
24
57
|
trace?: WorkflowScriptTraceEntry[];
|
|
25
58
|
children?: WorkflowScriptChildResult[];
|
|
26
59
|
steps?: NonNullable<AsyncStatus["steps"]>;
|
|
27
|
-
|
|
28
|
-
|
|
60
|
+
/** In-memory snapshots from synchronous child callbacks, keyed by workflow key. */
|
|
61
|
+
progress?: ReadonlyMap<string, WorkflowChildLiveProgress>;
|
|
62
|
+
}): WorkflowChildSummary {
|
|
63
|
+
const rows = new Map<string, WorkflowChildSummary["children"][number]>();
|
|
29
64
|
for (const entry of input.trace ?? []) {
|
|
30
65
|
if (entry.operation !== "run" || !KEY_PATTERN.test(entry.key)) continue;
|
|
31
66
|
const previous = rows.get(entry.key);
|
|
@@ -83,6 +118,18 @@ export function workflowChildSummary(input: {
|
|
|
83
118
|
if (!TERMINAL_STATES.has(row.state)) rows.set(key, { ...row, state: input.workflowState === "stopped" ? "stopped" : "failed" });
|
|
84
119
|
}
|
|
85
120
|
}
|
|
121
|
+
for (const [key, progress] of input.progress ?? []) {
|
|
122
|
+
const row = rows.get(key);
|
|
123
|
+
if (!row || row.state !== "running") continue;
|
|
124
|
+
rows.set(key, {
|
|
125
|
+
...row,
|
|
126
|
+
...(bounded(progress.agent, 256) ? { agent: progress.agent } : {}),
|
|
127
|
+
...(bounded(progress.sessionName, 256) ? { sessionName: progress.sessionName } : {}),
|
|
128
|
+
...(bounded(progress.model, 256) ? { model: progress.model } : {}),
|
|
129
|
+
...(bounded(progress.thinking, 32) ? { thinking: progress.thinking } : {}),
|
|
130
|
+
activity: workflowChildActivity(progress),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
86
133
|
return {
|
|
87
134
|
version: 1,
|
|
88
135
|
parentToolCallId: requiredId(input.parentToolCallId, "parentToolCallId"),
|
|
@@ -93,7 +140,7 @@ export function workflowChildSummary(input: {
|
|
|
93
140
|
};
|
|
94
141
|
}
|
|
95
142
|
|
|
96
|
-
export function parseWorkflowChildSummary(value: unknown):
|
|
143
|
+
export function parseWorkflowChildSummary(value: unknown): WorkflowChildSummary | undefined {
|
|
97
144
|
if (value === undefined) return undefined;
|
|
98
145
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("workflowChildren must be an object.");
|
|
99
146
|
const input = value as Record<string, unknown>;
|
|
@@ -102,17 +149,19 @@ export function parseWorkflowChildSummary(value: unknown): WorkflowChildSummaryV
|
|
|
102
149
|
if (input.version !== 1 || typeof input.inventoryComplete !== "boolean" || !Array.isArray(input.children)) throw new Error("workflowChildren is invalid.");
|
|
103
150
|
const workflowState = input.workflowState;
|
|
104
151
|
if (workflowState !== "queued" && workflowState !== "running" && workflowState !== "completed" && workflowState !== "failed" && workflowState !== "paused" && workflowState !== "stopped") throw new Error("workflowChildren.workflowState is invalid.");
|
|
105
|
-
const children = input.children.map((row):
|
|
152
|
+
const children = input.children.map((row): WorkflowChildSummary["children"][number] => {
|
|
106
153
|
if (!row || typeof row !== "object" || Array.isArray(row)) throw new Error("workflowChildren child row is invalid.");
|
|
107
154
|
const child = row as Record<string, unknown>;
|
|
108
|
-
if (Object.keys(child).some((key) => !["childId", "runId", "agent", "sessionName", "model", "thinking", "state"].includes(key))) throw new Error("workflowChildren child row has unsupported fields.");
|
|
155
|
+
if (Object.keys(child).some((key) => !["childId", "runId", "agent", "sessionName", "model", "thinking", "state", "activity"].includes(key))) throw new Error("workflowChildren child row has unsupported fields.");
|
|
109
156
|
if (typeof child.childId !== "string" || !KEY_PATTERN.test(child.childId)) throw new Error("workflowChildren childId is invalid.");
|
|
110
157
|
const state = child.state;
|
|
111
158
|
if (state !== "pending" && state !== "running" && state !== "completed" && state !== "failed" && state !== "paused" && state !== "stopped" && state !== "rejected" && state !== "detached") throw new Error("workflowChildren child state is invalid.");
|
|
112
159
|
for (const [field, maxBytes] of [["runId", 256], ["agent", 256], ["sessionName", 256], ["model", 256], ["thinking", 32]] as const) {
|
|
113
160
|
if (child[field] !== undefined && bounded(child[field], maxBytes) === undefined) throw new Error(`workflowChildren child ${field} is invalid.`);
|
|
114
161
|
}
|
|
115
|
-
|
|
162
|
+
const activity = parseActivity(child.activity);
|
|
163
|
+
if (activity && state !== "running") throw new Error("workflowChildren child activity requires running state.");
|
|
164
|
+
return { childId: child.childId, state, ...(activity ? { activity } : {}), ...(child.runId ? { runId: child.runId as string } : {}), ...(child.agent ? { agent: child.agent as string } : {}), ...(child.sessionName ? { sessionName: child.sessionName as string } : {}), ...(child.model ? { model: child.model as string } : {}), ...(child.thinking ? { thinking: child.thinking as string } : {}) };
|
|
116
165
|
});
|
|
117
166
|
if (new Set(children.map((child) => child.childId)).size !== children.length) throw new Error("workflowChildren has duplicate childId values.");
|
|
118
167
|
if (typeof input.parentToolCallId !== "string") throw new Error("workflowChildren.parentToolCallId is invalid.");
|