pi-subagents 0.65.1 → 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 +55 -0
- package/README.md +1 -1
- package/agents/researcher.md +23 -13
- package/docs/agents.md +16 -2
- package/docs/configuration.md +18 -0
- package/docs/extension-api.md +91 -0
- package/docs/models.md +58 -1
- package/docs/observability.md +42 -2
- package/docs/tool-reference.md +3 -3
- package/docs/workflows.md +14 -7
- package/package.json +2 -1
- package/skills/pi-subagents/references/execution-controls.md +14 -1
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- 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/shared-types.ts +1 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/index.ts +40 -2
- package/src/extension/public-execution.ts +0 -1
- package/src/extension/rpc.ts +4 -21
- package/src/extension/schemas.ts +8 -6
- package/src/extension/tool-description.ts +6 -5
- package/src/intercom/native-supervisor-channel.ts +82 -54
- package/src/runs/background/active-async-capacity.ts +18 -18
- package/src/runs/background/async-job-tracker.ts +35 -3
- 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 +86 -12
- package/src/runs/background/owned-process-tree.ts +6 -6
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +60 -32
- package/src/runs/background/run-status.ts +75 -5
- package/src/runs/background/runner-aliases.ts +18 -7
- 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 +412 -209
- 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 +101 -25
- package/src/runs/foreground/subagent-executor.ts +474 -197
- 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 +2 -2
- 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 +42 -6
- 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 +10 -9
- package/src/runs/shared/model-fallback.ts +10 -6
- package/src/runs/shared/nested-events.ts +5 -5
- 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 +2 -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-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +332 -204
- package/src/shared/types.ts +81 -60
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +23 -5
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +38 -10
- package/src/workflows/workflow-checklist.ts +11 -15
- 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
|
@@ -6,10 +6,11 @@ import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../runs/shared/para
|
|
|
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
8
|
import { describeGateAcceptanceConflict } from "../runs/shared/acceptance.ts";
|
|
9
|
-
import type { AcceptanceRecoveryMetadata,
|
|
9
|
+
import type { AcceptanceRecoveryMetadata, HostStepNode, SingleResult } from "../shared/types.ts";
|
|
10
10
|
import { normalizeWorkflowHostCommandParams, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "./host-command.ts";
|
|
11
11
|
|
|
12
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.";
|
|
13
14
|
function validGitRef(ref: unknown): ref is string {
|
|
14
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;
|
|
15
16
|
if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(ref)) return false;
|
|
@@ -644,7 +645,7 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
644
645
|
}
|
|
645
646
|
if (Object.prototype.hasOwnProperty.call(params, "clarify")) throw new Error(label + " does not support clarify UI.");
|
|
646
647
|
if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
|
|
647
|
-
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.");
|
|
648
649
|
validateLaneMetadata(params.lane, label + " lane", key);
|
|
649
650
|
if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
|
|
650
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));
|
|
@@ -1110,11 +1111,11 @@ export interface RunWorkflowScriptOptions {
|
|
|
1110
1111
|
globalConcurrencyLimit?: number;
|
|
1111
1112
|
admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
|
|
1112
1113
|
launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean; batch: boolean }) => Promise<WorkflowScriptChildResult>;
|
|
1113
|
-
resolveResume?: (reference: WorkflowReceiptResumeReference, signal: AbortSignal) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
|
|
1114
|
+
resolveResume?: (reference: WorkflowReceiptResumeReference | string, signal: AbortSignal, index?: number) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
|
|
1114
1115
|
status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
|
|
1115
1116
|
steer?: (key: string, message: string, options: WorkflowSteerOptions, signal: AbortSignal) => Promise<WorkflowSteerResult>;
|
|
1116
1117
|
host?: (key: string, params: WorkflowHostCommandParams, signal: AbortSignal) => Promise<WorkflowHostCommandResult>;
|
|
1117
|
-
onHostStep?: (hostStep:
|
|
1118
|
+
onHostStep?: (hostStep: HostStepNode) => void;
|
|
1118
1119
|
state?: {
|
|
1119
1120
|
get: (key: string) => unknown | Promise<unknown>;
|
|
1120
1121
|
set: (key: string, value: unknown) => void | Promise<void>;
|
|
@@ -1502,7 +1503,7 @@ function definitelyNonJson(node: AstNode, normalizeUndefined = false): string |
|
|
|
1502
1503
|
if (node.type === "ObjectExpression" && Array.isArray(node.properties)) {
|
|
1503
1504
|
const values = new Map<string, AstNode>();
|
|
1504
1505
|
for (const property of node.properties) {
|
|
1505
|
-
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;
|
|
1506
1507
|
const key = staticPropertyKey(property);
|
|
1507
1508
|
if (key === undefined) return undefined;
|
|
1508
1509
|
values.set(key, property.value);
|
|
@@ -1531,6 +1532,26 @@ function directObjectPropertyValue(node: AstNode, name: string): AstNode | undef
|
|
|
1531
1532
|
return value;
|
|
1532
1533
|
}
|
|
1533
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
|
+
|
|
1534
1555
|
function directRunsAllKeys(call: AstNode): Array<{ key: string; node: AstNode }> {
|
|
1535
1556
|
const args = Array.isArray(call.arguments) ? call.arguments : [];
|
|
1536
1557
|
const items = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0].elements : [];
|
|
@@ -1572,6 +1593,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
|
|
|
1572
1593
|
const key = literalString(keyNode);
|
|
1573
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) });
|
|
1574
1595
|
if (astNode(args[1])) {
|
|
1596
|
+
errors.push(...validateStaticBaseRef(args[1], "runs.run"));
|
|
1575
1597
|
const message = definitelyNonJson(args[1]);
|
|
1576
1598
|
if (message) errors.push({ message: `runs.run params are invalid: ${message}.`, ...nodeLocation(args[1]) });
|
|
1577
1599
|
}
|
|
@@ -1581,6 +1603,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
|
|
|
1581
1603
|
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
1582
1604
|
if (astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements)) {
|
|
1583
1605
|
for (const item of args[0].elements) if (astNode(item)) {
|
|
1606
|
+
errors.push(...validateStaticBaseRef(item, "runs.all item"));
|
|
1584
1607
|
const message = definitelyNonJson(item);
|
|
1585
1608
|
if (message) errors.push({ message: `runs.all item params are invalid: ${message}.`, ...nodeLocation(item) });
|
|
1586
1609
|
}
|
|
@@ -1751,7 +1774,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1751
1774
|
console.error("Workflow onLanePlan callback failed:", error);
|
|
1752
1775
|
}
|
|
1753
1776
|
};
|
|
1754
|
-
const hostStepChanged = (hostStep:
|
|
1777
|
+
const hostStepChanged = (hostStep: HostStepNode) => {
|
|
1755
1778
|
try {
|
|
1756
1779
|
options.onHostStep?.(hostStep);
|
|
1757
1780
|
} catch (error) {
|
|
@@ -2047,7 +2070,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2047
2070
|
if (!options.host) return respond(Promise.reject(new Error("runs.host is unavailable in this host context.")));
|
|
2048
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.`)));
|
|
2049
2072
|
const startedAt = Date.now();
|
|
2050
|
-
const startedStep:
|
|
2073
|
+
const startedStep: HostStepNode = {
|
|
2051
2074
|
version: 1,
|
|
2052
2075
|
kind: "host-step",
|
|
2053
2076
|
monitorKind: "command",
|
|
@@ -2130,7 +2153,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2130
2153
|
return respond(Promise.reject(new Error(`runs.run('${key}') worktree must be true or false.`)));
|
|
2131
2154
|
}
|
|
2132
2155
|
if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) {
|
|
2133
|
-
return respond(Promise.reject(new Error(`runs.run('${key}')
|
|
2156
|
+
return respond(Promise.reject(new Error(`runs.run('${key}') ${BASE_REF_VALIDATION_ERROR}`)));
|
|
2134
2157
|
}
|
|
2135
2158
|
if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) {
|
|
2136
2159
|
return respond(Promise.reject(new Error(`runs.run('${key}') gate must be a non-empty command string.`)));
|
|
@@ -2183,10 +2206,11 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2183
2206
|
const childStopController = new AbortController();
|
|
2184
2207
|
childStopControllers.set(key, childStopController);
|
|
2185
2208
|
const childSignal = combinedAbortSignal([childController.signal, childStopController.signal]);
|
|
2186
|
-
const
|
|
2209
|
+
const resumeInput = resumeReference ?? (typeof params.resume === "string" && options.resolveResume ? params.resume : undefined);
|
|
2210
|
+
const resolvedResumeValue = resumeInput
|
|
2187
2211
|
? await Promise.resolve().then(() => {
|
|
2188
2212
|
if (!options.resolveResume) throw new Error("Keyed workflow receipt resume is unavailable in this host.");
|
|
2189
|
-
return options.resolveResume(
|
|
2213
|
+
return options.resolveResume(resumeInput, childSignal, typeof params.index === "number" ? params.index : undefined);
|
|
2190
2214
|
})
|
|
2191
2215
|
: undefined;
|
|
2192
2216
|
const resolvedResume = typeof resolvedResumeValue === "string"
|
|
@@ -2202,6 +2226,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2202
2226
|
: [];
|
|
2203
2227
|
resolvedResumeLineage = [...new Set(lineage.length ? lineage : [resolvedResumeId!])];
|
|
2204
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
|
+
}
|
|
2205
2233
|
}
|
|
2206
2234
|
const launchParams = resolvedResumeId ? { ...params, resume: resolvedResumeId } : params;
|
|
2207
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)
|
|
@@ -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.");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { WorkflowPreflightLane, WorkflowPreflight } from "../shared/types.ts";
|
|
2
2
|
|
|
3
3
|
export const WORKFLOW_PREFLIGHT_VERSION = 1 as const;
|
|
4
4
|
export const WORKFLOW_PREFLIGHT_MAX_LANES = 64;
|
|
@@ -11,7 +11,7 @@ export const WORKFLOW_PREFLIGHT_MAX_WARNINGS = 16;
|
|
|
11
11
|
const WORKFLOW_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
12
12
|
const PREFLIGHT_FIELDS = new Set(["version", "coverage", "lanes"]);
|
|
13
13
|
const LANE_FIELDS = new Set(["key", "mode", "decision", "claims", "expectedOutput", "independence"]);
|
|
14
|
-
const PREFLIGHT_MODES = new Set<
|
|
14
|
+
const PREFLIGHT_MODES = new Set<WorkflowPreflightLane["mode"]>(["mutation", "review", "scout", "gate"]);
|
|
15
15
|
|
|
16
16
|
type WorkflowTraceLike = {
|
|
17
17
|
operation: string;
|
|
@@ -23,7 +23,7 @@ type WorkflowTraceLike = {
|
|
|
23
23
|
|
|
24
24
|
export interface WorkflowPreflightValidationResult {
|
|
25
25
|
ok: boolean;
|
|
26
|
-
preflight?:
|
|
26
|
+
preflight?: WorkflowPreflight;
|
|
27
27
|
error?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -60,18 +60,18 @@ function normalizeDisplayString(value: unknown, path: string): string {
|
|
|
60
60
|
return normalized;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function normalizeLane(value: unknown, index: number):
|
|
63
|
+
function normalizeLane(value: unknown, index: number): WorkflowPreflightLane {
|
|
64
64
|
const path = `preflight.lanes[${index}]`;
|
|
65
65
|
assertPlainObject(value, path);
|
|
66
66
|
assertKnownFields(value, LANE_FIELDS, path);
|
|
67
67
|
const key = normalizeDisplayString(value.key, `${path}.key`);
|
|
68
68
|
if (!WORKFLOW_KEY_PATTERN.test(key)) throw new Error(`${path}.key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.`);
|
|
69
|
-
let mode:
|
|
69
|
+
let mode: WorkflowPreflightLane["mode"];
|
|
70
70
|
if (value.mode !== undefined) {
|
|
71
|
-
if (typeof value.mode !== "string" || !PREFLIGHT_MODES.has(value.mode as
|
|
71
|
+
if (typeof value.mode !== "string" || !PREFLIGHT_MODES.has(value.mode as WorkflowPreflightLane["mode"])) {
|
|
72
72
|
throw new Error(`${path}.mode must be one of: mutation, review, scout, gate.`);
|
|
73
73
|
}
|
|
74
|
-
mode = value.mode as
|
|
74
|
+
mode = value.mode as WorkflowPreflightLane["mode"];
|
|
75
75
|
}
|
|
76
76
|
let claims: string[] | undefined;
|
|
77
77
|
if (value.claims !== undefined) {
|
|
@@ -97,7 +97,7 @@ function normalizeLane(value: unknown, index: number): WorkflowPreflightLaneV1 {
|
|
|
97
97
|
* independent of workflowScript parsing: callers may describe dynamic fanout
|
|
98
98
|
* without making the metadata a second execution graph.
|
|
99
99
|
*/
|
|
100
|
-
export function normalizeWorkflowPreflight(input: unknown):
|
|
100
|
+
export function normalizeWorkflowPreflight(input: unknown): WorkflowPreflight | undefined {
|
|
101
101
|
if (input === undefined) return undefined;
|
|
102
102
|
assertPlainObject(input, "preflight");
|
|
103
103
|
assertKnownFields(input, PREFLIGHT_FIELDS, "preflight");
|
|
@@ -115,7 +115,7 @@ export function normalizeWorkflowPreflight(input: unknown): WorkflowPreflightV1
|
|
|
115
115
|
if (seenKeys.has(lane.key)) throw new Error(`preflight.lanes contains duplicate key '${lane.key}'.`);
|
|
116
116
|
seenKeys.add(lane.key);
|
|
117
117
|
}
|
|
118
|
-
const normalized = { version: WORKFLOW_PREFLIGHT_VERSION, coverage, lanes } satisfies
|
|
118
|
+
const normalized = { version: WORKFLOW_PREFLIGHT_VERSION, coverage, lanes } satisfies WorkflowPreflight;
|
|
119
119
|
const depth = 1 + (lanes.length > 0 ? 1 : 0) + (lanes.some((lane) => lane.claims !== undefined) ? 1 : 0);
|
|
120
120
|
if (depth > WORKFLOW_PREFLIGHT_MAX_DEPTH) throw new Error(`preflight exceeds the maximum nesting depth of ${WORKFLOW_PREFLIGHT_MAX_DEPTH}.`);
|
|
121
121
|
const bytes = Buffer.byteLength(JSON.stringify(normalized), "utf8");
|
|
@@ -138,7 +138,7 @@ function warningLimit(messages: string[]): string[] {
|
|
|
138
138
|
return [...messages.slice(0, WORKFLOW_PREFLIGHT_MAX_WARNINGS - 1), `Preflight advisory: ${omitted} additional mismatch warning(s) omitted.`];
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
function declaredKeys(preflight:
|
|
141
|
+
function declaredKeys(preflight: WorkflowPreflight): Set<string> {
|
|
142
142
|
return new Set(preflight.lanes.map((lane) => lane.key));
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -152,15 +152,15 @@ export function workflowKeyMatchesPreflightLane(key: string, laneKey: string, ge
|
|
|
152
152
|
|
|
153
153
|
/** Select the most specific advisory lane without allowing declaration order to override an exact runtime key. */
|
|
154
154
|
export function workflowPreflightLaneForRuntimeKey(
|
|
155
|
-
preflight:
|
|
155
|
+
preflight: WorkflowPreflight | undefined,
|
|
156
156
|
key: string,
|
|
157
157
|
preferredKeys: readonly (string | undefined)[] = [],
|
|
158
|
-
):
|
|
158
|
+
): WorkflowPreflightLane | undefined {
|
|
159
159
|
const lanes = preflight?.lanes;
|
|
160
160
|
if (!lanes) return undefined;
|
|
161
161
|
const exact = lanes.find((lane) => lane.key === key);
|
|
162
162
|
if (exact) return exact;
|
|
163
|
-
let closestRoot:
|
|
163
|
+
let closestRoot: WorkflowPreflightLane | undefined;
|
|
164
164
|
for (const lane of lanes) {
|
|
165
165
|
if (key.startsWith(`${lane.key}.`) && (!closestRoot || lane.key.length > closestRoot.key.length)) closestRoot = lane;
|
|
166
166
|
}
|
|
@@ -203,7 +203,7 @@ function unlaunchedWarning(key: string): string {
|
|
|
203
203
|
|
|
204
204
|
/** Return bounded advisory mismatch warnings; these never reject a child launch. */
|
|
205
205
|
export function workflowPreflightWarnings(
|
|
206
|
-
preflight:
|
|
206
|
+
preflight: WorkflowPreflight | undefined,
|
|
207
207
|
trace: readonly WorkflowTraceLike[],
|
|
208
208
|
options: { settled?: boolean } = {},
|
|
209
209
|
): string[] {
|
|
@@ -218,7 +218,7 @@ export function workflowPreflightWarnings(
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
/** Attach the first undeclared-key warning to its first trace row for status/debug views. */
|
|
221
|
-
export function annotateWorkflowPreflightTrace<T extends WorkflowTraceLike>(trace: readonly T[], preflight:
|
|
221
|
+
export function annotateWorkflowPreflightTrace<T extends WorkflowTraceLike>(trace: readonly T[], preflight: WorkflowPreflight | undefined): T[] {
|
|
222
222
|
if (!preflight) return [...trace];
|
|
223
223
|
const declared = declaredKeys(preflight);
|
|
224
224
|
const warned = new Set<string>();
|
|
@@ -235,14 +235,14 @@ function laneCell(value: string | undefined): string {
|
|
|
235
235
|
|
|
236
236
|
const WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH = 96;
|
|
237
237
|
|
|
238
|
-
function planLaneLabel(lane:
|
|
238
|
+
function planLaneLabel(lane: WorkflowPreflightLane): string {
|
|
239
239
|
const value = lane.decision?.trim() || lane.key;
|
|
240
240
|
if (value.length <= WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH) return value;
|
|
241
241
|
return `${value.slice(0, WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH - 1).trimEnd()}…`;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/** Render the detailed bounded table reserved for tool output and expanded/debug views. */
|
|
245
|
-
export function formatWorkflowPreflight(preflight:
|
|
245
|
+
export function formatWorkflowPreflight(preflight: WorkflowPreflight | undefined, options: { indent?: string } = {}): string {
|
|
246
246
|
if (!preflight) return "";
|
|
247
247
|
const indent = options.indent ?? "";
|
|
248
248
|
const count = preflight.lanes.length;
|
|
@@ -263,7 +263,7 @@ export function formatWorkflowPreflight(preflight: WorkflowPreflightV1 | undefin
|
|
|
263
263
|
return lines.join("\n");
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
export function formatWorkflowPreflightSummary(preflight:
|
|
266
|
+
export function formatWorkflowPreflightSummary(preflight: WorkflowPreflight | undefined): string {
|
|
267
267
|
if (!preflight) return "";
|
|
268
268
|
const keys = preflight.lanes.slice(0, 4).map((lane) => lane.key).join(", ");
|
|
269
269
|
const remainder = preflight.lanes.length > 4 ? `, +${preflight.lanes.length - 4}` : "";
|
|
@@ -271,7 +271,7 @@ export function formatWorkflowPreflightSummary(preflight: WorkflowPreflightV1 |
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
/** Render the operator-facing one-line plan shown in routine status views. */
|
|
274
|
-
export function formatWorkflowPreflightPlanSummary(preflight:
|
|
274
|
+
export function formatWorkflowPreflightPlanSummary(preflight: WorkflowPreflight | undefined, options: { indent?: string } = {}): string {
|
|
275
275
|
if (!preflight) return "";
|
|
276
276
|
const indent = options.indent ?? "";
|
|
277
277
|
const count = preflight.lanes.length;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
4
|
-
import type { AcceptanceRecoveryMetadata, ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState, WorkflowRecoveryAction,
|
|
4
|
+
import type { AcceptanceRecoveryMetadata, ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState, WorkflowRecoveryAction, WorkflowResourceProvenance, WorkflowTerminalOutcome, WorkflowTerminalResolution } from "../shared/types.ts";
|
|
5
5
|
import type { WorkflowReceiptResumeReference, WorkflowScriptChildResult } from "./scripted-workflow.ts";
|
|
6
6
|
import { parseWorkflowChildSummary } from "./workflow-child-summary.ts";
|
|
7
7
|
import { HOST_STEP_MAX_COUNT, assertUniqueHostStepIds, parseHostStepNode } from "../runs/shared/host-step-status.ts";
|
|
@@ -39,7 +39,7 @@ export function buildWorkflowReceipt(input: {
|
|
|
39
39
|
children: WorkflowScriptChildResult[];
|
|
40
40
|
hostSteps?: WorkflowReceipt["hostSteps"];
|
|
41
41
|
workflowChildren?: WorkflowReceipt["workflowChildren"];
|
|
42
|
-
resource?:
|
|
42
|
+
resource?: WorkflowResourceProvenance;
|
|
43
43
|
terminalOutcome?: WorkflowTerminalOutcome;
|
|
44
44
|
createdAt?: number;
|
|
45
45
|
}): WorkflowReceipt {
|
|
@@ -262,7 +262,7 @@ function parseWorkflowResolution(value: unknown, source: string): WorkflowTermin
|
|
|
262
262
|
return value;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
function parseWorkflowResource(value: unknown, source: string):
|
|
265
|
+
function parseWorkflowResource(value: unknown, source: string): WorkflowResourceProvenance | undefined {
|
|
266
266
|
if (value === undefined) return undefined;
|
|
267
267
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid workflow receipt '${source}': resource must be an object.`);
|
|
268
268
|
const resource = value as Record<string, unknown>;
|