pi-subagents 0.55.0 → 0.57.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 +81 -3
- package/agents/claude-code-writer.md +15 -0
- package/agents/claude-code.md +15 -0
- package/agents/codex-exec-writer.md +15 -0
- package/agents/codex-exec.md +15 -0
- package/agents/cursor-agent-writer.md +14 -0
- package/agents/cursor-agent.md +14 -0
- package/docs/agents.md +111 -15
- package/docs/configuration.md +37 -0
- package/docs/extension-api.md +2 -2
- package/docs/models.md +6 -0
- package/docs/observability.md +8 -7
- package/docs/tool-reference.md +11 -1
- package/docs/workflows.md +20 -0
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +1 -1
- package/src/agents/agent-management.ts +13 -5
- package/src/agents/agent-refinements.ts +4 -4
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +242 -49
- package/src/agents/builtin-names.ts +6 -0
- package/src/agents/runtime-agent-registry.ts +8 -2
- package/src/api/preflight.ts +22 -3
- package/src/extension/config.ts +70 -0
- package/src/extension/doctor.ts +3 -3
- package/src/extension/index.ts +18 -4
- package/src/extension/public-execution.ts +29 -13
- package/src/extension/rpc.ts +20 -5
- package/src/extension/schemas.ts +9 -3
- package/src/extension/tool-description.ts +11 -9
- package/src/inspectors/herdr/actions.ts +2 -1
- package/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/src/integrations/herdr-status.ts +46 -3
- package/src/runs/background/active-async-capacity.ts +77 -10
- package/src/runs/background/async-execution.ts +148 -27
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-resume.ts +3 -0
- package/src/runs/background/async-retention.ts +20 -3
- package/src/runs/background/async-status.ts +5 -0
- package/src/runs/background/chain-root-attachment.ts +15 -1
- package/src/runs/background/fleet-view.ts +16 -10
- package/src/runs/background/inspect-rpc.ts +8 -8
- package/src/runs/background/result-files.ts +27 -14
- package/src/runs/background/result-watcher.ts +9 -5
- package/src/runs/background/run-status.ts +33 -6
- package/src/runs/background/scheduled-runs.ts +7 -1
- package/src/runs/background/subagent-runner.ts +254 -49
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/execution.ts +107 -16
- package/src/runs/foreground/foreground-control.ts +6 -0
- package/src/runs/foreground/foreground-history.ts +25 -3
- package/src/runs/foreground/subagent-executor.ts +324 -84
- package/src/runs/foreground/workflow-detach-reconcile.ts +31 -7
- package/src/runs/shared/acceptance.ts +10 -5
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/child-protocol.ts +21 -7
- package/src/runs/shared/claude-code-adapter.ts +129 -0
- package/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/src/runs/shared/completion-guard.ts +3 -2
- package/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/extension-bindings.ts +78 -0
- package/src/runs/shared/external-cli-contract.ts +167 -0
- package/src/runs/shared/external-cli-preflight.ts +122 -0
- package/src/runs/shared/external-cli-runner.ts +349 -54
- package/src/runs/shared/fast-mode-extension.ts +10 -0
- package/src/runs/shared/model-exclusions.ts +59 -8
- package/src/runs/shared/model-fallback.ts +25 -8
- package/src/runs/shared/mutation-evidence.ts +150 -0
- package/src/runs/shared/nested-events.ts +3 -1
- package/src/runs/shared/nested-render.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +3 -0
- package/src/runs/shared/pi-args.ts +47 -0
- package/src/runs/shared/process-signal.ts +13 -0
- package/src/runs/shared/run-history.ts +21 -1
- package/src/runs/shared/structured-output.ts +18 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +9 -5
- package/src/shared/fork-context.ts +21 -0
- package/src/shared/formatters.ts +7 -1
- package/src/shared/launch-contract.ts +6 -0
- package/src/shared/pruned-fork.ts +450 -0
- package/src/shared/session-file-trust.ts +19 -0
- package/src/shared/session-tokens.ts +14 -3
- package/src/shared/settings.ts +16 -3
- package/src/shared/shortcuts.ts +17 -0
- package/src/shared/types.ts +166 -9
- package/src/shared/workflow-child-permit.ts +116 -0
- package/src/slash/delegation-adapters.ts +0 -1
- package/src/slash/slash-commands.ts +8 -6
- package/src/tui/fleet-status.ts +27 -10
- package/src/tui/fleet-transcript.ts +11 -5
- package/src/tui/fleet.ts +28 -13
- package/src/tui/render.ts +47 -21
- package/src/workflows/scripted-workflow.ts +298 -30
- package/src/workflows/workflow-child-summary.ts +117 -0
- package/src/workflows/workflow-receipt.ts +119 -4
|
@@ -6,6 +6,17 @@ import { Worker } from "node:worker_threads";
|
|
|
6
6
|
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
7
7
|
const requireFromPackage = createRequire(import.meta.url);
|
|
8
8
|
|
|
9
|
+
export interface WorkflowScriptValidationError {
|
|
10
|
+
message: string;
|
|
11
|
+
line?: number;
|
|
12
|
+
column?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WorkflowScriptValidationResult {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
errors: WorkflowScriptValidationError[];
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
const WORKER_SOURCE = String.raw`
|
|
10
21
|
const { parentPort, workerData } = require("node:worker_threads");
|
|
11
22
|
const vm = require("node:vm");
|
|
@@ -150,7 +161,15 @@ function trackObservationTracker(tracker, promise, allowFutureObservations = fal
|
|
|
150
161
|
get(promiseTarget, prop) {
|
|
151
162
|
if (prop === "then") return function promiseThen(onFulfilled, onRejected) {
|
|
152
163
|
consumeTrackedObservations(tracker);
|
|
153
|
-
|
|
164
|
+
const chainTracker = { observations: tracker.observations, consumed: false, dependencies: [tracker] };
|
|
165
|
+
const wrapHandler = (handler) => typeof handler === "function"
|
|
166
|
+
? function trackedThenHandler(...args) {
|
|
167
|
+
const value = handler.apply(this, args);
|
|
168
|
+
addTrackerDependency(chainTracker, promiseObservationTracker(value));
|
|
169
|
+
return trackedPromiseTarget(value);
|
|
170
|
+
}
|
|
171
|
+
: handler;
|
|
172
|
+
return trackObservationTracker(chainTracker, promiseTarget.then(wrapHandler(onFulfilled), wrapHandler(onRejected)), true);
|
|
154
173
|
};
|
|
155
174
|
if (prop === "catch") return function promiseCatch(onRejected) {
|
|
156
175
|
consumeTrackedObservations(tracker);
|
|
@@ -276,8 +295,43 @@ function formatRef(result) {
|
|
|
276
295
|
return "[" + parts.join("; ") + "]";
|
|
277
296
|
}
|
|
278
297
|
|
|
298
|
+
function formatChildResultString(result) {
|
|
299
|
+
const output = typeof result?.output === "string" ? result.output.trim() : "";
|
|
300
|
+
return output || formatRef(result);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function decorateWorkflowChildResult(result) {
|
|
304
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) return result;
|
|
305
|
+
Object.defineProperties(result, {
|
|
306
|
+
toString: { value() { return formatChildResultString(this); }, enumerable: false, configurable: true },
|
|
307
|
+
});
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
|
|
279
311
|
let runFingerprints = new Map();
|
|
280
312
|
|
|
313
|
+
function validateExtensionBindings(value, label) {
|
|
314
|
+
if (value === undefined) return;
|
|
315
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(label + " extensionBindings must be a plain JSON object.");
|
|
316
|
+
const keys = Object.keys(value);
|
|
317
|
+
if (keys.length > 16) throw new Error(label + " extensionBindings supports at most 16 namespaces.");
|
|
318
|
+
for (const key of keys) if (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62})\/[1-9][0-9]{0,8}$/.test(key)) throw new Error(label + " extensionBindings namespace '" + key + "' must use a package-like name followed by '/<positive-version>'.");
|
|
319
|
+
assertJsonValue(value, label + " extensionBindings");
|
|
320
|
+
let propertyCount = 0;
|
|
321
|
+
function visit(entry, depth) {
|
|
322
|
+
if (!entry || typeof entry !== "object") return;
|
|
323
|
+
if (depth > 16) throw new Error(label + " extensionBindings exceeds the maximum nesting depth of 16.");
|
|
324
|
+
if (Array.isArray(entry)) { for (const item of entry) visit(item, depth + 1); return; }
|
|
325
|
+
for (const child of Object.values(entry)) {
|
|
326
|
+
propertyCount++;
|
|
327
|
+
if (propertyCount > 256) throw new Error(label + " extensionBindings exceeds 256 total properties.");
|
|
328
|
+
visit(child, depth + 1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
visit(value, 0);
|
|
332
|
+
if (new TextEncoder().encode(stableRunJson(value)).byteLength > 16384) throw new Error(label + " extensionBindings canonical JSON exceeds 16384 bytes.");
|
|
333
|
+
}
|
|
334
|
+
|
|
281
335
|
function validateRunCall(key, params, label, fingerprints) {
|
|
282
336
|
if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error(label + " has an invalid key.");
|
|
283
337
|
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error(label + " requires a params object.");
|
|
@@ -290,6 +344,7 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
290
344
|
if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
|
|
291
345
|
if (params.gate !== undefined && params.acceptance !== undefined) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify.");
|
|
292
346
|
if (params.gate !== undefined && params.resume !== undefined) throw new Error(label + " gate is not supported with retained resume.");
|
|
347
|
+
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.");
|
|
293
348
|
if (params.resume !== undefined && typeof params.resume !== "string") {
|
|
294
349
|
const reference = params.resume;
|
|
295
350
|
if (!reference || typeof reference !== "object" || Array.isArray(reference)) throw new Error(label + " resume must be a retained run id or keyed workflow receipt reference.");
|
|
@@ -302,6 +357,7 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
302
357
|
if (typeof params.resume === "string" && !params.resume.trim()) throw new Error(label + " resume must be a non-empty retained run id.");
|
|
303
358
|
if (params.resume !== undefined && params.agent !== undefined) throw new Error(label + " resume and agent are mutually exclusive.");
|
|
304
359
|
if (params.resume !== undefined && (typeof params.task !== "string" || !params.task.trim())) throw new Error(label + " resume requires a non-empty task follow-up.");
|
|
360
|
+
validateExtensionBindings(params.extensionBindings, label);
|
|
305
361
|
assertJsonValue(params, label + " params");
|
|
306
362
|
const fingerprint = stableRunJson(params);
|
|
307
363
|
const existing = fingerprints.get(key);
|
|
@@ -312,7 +368,8 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
312
368
|
const runs = Object.freeze({
|
|
313
369
|
run(key, params) {
|
|
314
370
|
validateRunCall(key, params, "runs.run", runFingerprints);
|
|
315
|
-
|
|
371
|
+
const launched = runHostCall(key, params, false);
|
|
372
|
+
return trackRunObservation([{ key, operation: "run", callId: launched.callId }], launched.promise.then(decorateWorkflowChildResult));
|
|
316
373
|
},
|
|
317
374
|
all(items) {
|
|
318
375
|
if (!Array.isArray(items)) throw new Error("runs.all(items) requires an array.");
|
|
@@ -329,7 +386,7 @@ const runs = Object.freeze({
|
|
|
329
386
|
runFingerprints = fingerprints;
|
|
330
387
|
const batch = { id: "batch-" + (++nextCallId), calls };
|
|
331
388
|
const launched = calls.map(({ key, params }) => runHostCall(key, params, true, batch));
|
|
332
|
-
return trackRunObservation(launched.map(({ key, callId }) => ({ key, operation: "run", callId })), Promise.all(launched.map(({ promise }) => promise)).then((results) => wrapRunsAllResults(results, calls.map(({ key }) => key))));
|
|
389
|
+
return trackRunObservation(launched.map(({ key, callId }) => ({ key, operation: "run", callId })), Promise.all(launched.map(({ promise }) => promise)).then((results) => wrapRunsAllResults(results.map(decorateWorkflowChildResult), calls.map(({ key }) => key))));
|
|
333
390
|
},
|
|
334
391
|
steer(key, message, options = {}) {
|
|
335
392
|
if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error("runs.steer has an invalid key.");
|
|
@@ -394,7 +451,7 @@ function isSyntaxError(error) {
|
|
|
394
451
|
return error instanceof SyntaxError || error?.name === "SyntaxError";
|
|
395
452
|
}
|
|
396
453
|
|
|
397
|
-
const NESTED_ASYNC_WORKFLOW_ERROR = "workflowScript does not support nested async functions. Use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains so workflows stay portable across Node and Bun.";
|
|
454
|
+
const NESTED_ASYNC_WORKFLOW_ERROR = "workflowScript validation failed before child launch; no children launched. workflowScript does not support nested async functions. Use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains so workflows stay portable across Node and Bun. Parallel plus sequential rewrite: const a = runs.run(\"a\", { agent: \"worker\", task: \"A\" }); const writer = await runs.run(\"writer\", { agent: \"worker\", task: \"Write\" }); const review = await runs.run(\"review\", { agent: \"reviewer\", task: writer.output }); const [aResult] = await Promise.all([a]); return { a: aResult.output, issue: { writerRunId: writer.runId, reviewRunId: review.runId } };";
|
|
398
455
|
const AST_SCALAR_KEYS = new Set(["type", "start", "end"]);
|
|
399
456
|
|
|
400
457
|
function assertPortableWorkflowScript(source) {
|
|
@@ -581,7 +638,12 @@ parentPort.on("message", async (message) => {
|
|
|
581
638
|
}
|
|
582
639
|
}
|
|
583
640
|
const persistedValue = value === undefined ? null : omitUndefinedWorkflowValues(value);
|
|
584
|
-
|
|
641
|
+
try {
|
|
642
|
+
assertJsonValue(persistedValue, "return");
|
|
643
|
+
} catch (error) {
|
|
644
|
+
parentPort.postMessage({ type: "error", errorPhase: "return-serialization", error: formatWorkflowScriptError(error) });
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
585
647
|
parentPort.postMessage({ type: "complete", value: persistedValue });
|
|
586
648
|
} catch (error) {
|
|
587
649
|
parentPort.postMessage({ type: "error", error: isSyntaxError(error) ? formatWorkflowScriptSyntaxError(error) : formatWorkflowScriptError(error), ...(error && error.workflowErrorKind === "detached-child" ? { errorKind: "detached-child" } : {}) });
|
|
@@ -599,10 +661,12 @@ export interface WorkflowScriptChildResult {
|
|
|
599
661
|
output: string;
|
|
600
662
|
error?: string;
|
|
601
663
|
detached?: boolean;
|
|
664
|
+
interrupted?: boolean;
|
|
602
665
|
structuredOutput?: unknown;
|
|
603
666
|
requestedContext?: "fresh" | "fork";
|
|
604
667
|
resolvedContext?: "fresh" | "fork" | "mixed";
|
|
605
668
|
outputReference?: string;
|
|
669
|
+
externalAdapter?: import("../shared/types.ts").ExternalCliReceiptMetadata;
|
|
606
670
|
resumability?: { state: "resumable" } | { state: "not-resumable"; reason: string };
|
|
607
671
|
continuation?: { runIds: string[] };
|
|
608
672
|
artifactPaths: string[];
|
|
@@ -670,10 +734,12 @@ export class WorkflowScriptError extends Error {
|
|
|
670
734
|
|
|
671
735
|
export interface RunWorkflowScriptOptions {
|
|
672
736
|
script: string;
|
|
737
|
+
/** Host-only first-slice admission context. It is never sent to the workflow worker. */
|
|
738
|
+
oneUsePermit?: { claim: (key: string) => string | undefined };
|
|
673
739
|
timeoutMs?: number;
|
|
674
740
|
signal?: AbortSignal;
|
|
675
741
|
admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
|
|
676
|
-
launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean }) => Promise<WorkflowScriptChildResult>;
|
|
742
|
+
launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean; batch: boolean }) => Promise<WorkflowScriptChildResult>;
|
|
677
743
|
resolveResume?: (reference: WorkflowReceiptResumeReference, signal: AbortSignal) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
|
|
678
744
|
status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
|
|
679
745
|
steer?: (key: string, message: string, options: WorkflowSteerOptions, signal: AbortSignal) => Promise<WorkflowSteerResult>;
|
|
@@ -780,6 +846,15 @@ export function formatWorkflowJsonPreview(value: unknown, maxLength: number): st
|
|
|
780
846
|
}
|
|
781
847
|
}
|
|
782
848
|
|
|
849
|
+
function workflowReturnRecoveryHint(children: WorkflowScriptChildResult[]): string {
|
|
850
|
+
if (children.length === 0) return " Return only plain JSON data. For a child result, select fields such as { runId: child.runId, ok: child.ok, outputReference: child.outputReference }.";
|
|
851
|
+
const references = children.slice(0, 10).map((child) => {
|
|
852
|
+
const fields = [child.runId ? `runId=${child.runId.slice(0, 500)}` : undefined, child.outputReference ? `outputReference=${child.outputReference.slice(0, 500)}` : undefined, child.artifactPaths[0] ? `artifact=${child.artifactPaths[0].slice(0, 500)}` : undefined].filter((field): field is string => field !== undefined);
|
|
853
|
+
return `'${child.key}'${fields.length > 0 ? ` (${fields.join(", ")})` : ""}`;
|
|
854
|
+
});
|
|
855
|
+
return ` Child work completed before return serialization failed. Recover outputs from: ${references.join(", ")}${children.length > references.length ? `, and ${children.length - references.length} more` : ""}. Return a plain projection such as { runId: child.runId, ok: child.ok, outputReference: child.outputReference }.`;
|
|
856
|
+
}
|
|
857
|
+
|
|
783
858
|
export interface SimpleWorkflowRunPreview {
|
|
784
859
|
agent?: string;
|
|
785
860
|
task?: string;
|
|
@@ -817,6 +892,194 @@ function validateKey(value: unknown, owner = "runs.run"): string {
|
|
|
817
892
|
return value;
|
|
818
893
|
}
|
|
819
894
|
|
|
895
|
+
type AstNode = {
|
|
896
|
+
type: string;
|
|
897
|
+
loc?: { start: { line: number; column: number } };
|
|
898
|
+
[key: string]: unknown;
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
const AST_LOCATION_KEYS = new Set(["type", "start", "end", "loc", "range"]);
|
|
902
|
+
|
|
903
|
+
function astNode(value: unknown): value is AstNode {
|
|
904
|
+
return Boolean(value) && typeof value === "object" && typeof (value as { type?: unknown }).type === "string";
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function literalString(node: unknown): string | undefined {
|
|
908
|
+
if (!astNode(node)) return undefined;
|
|
909
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
910
|
+
if (node.type === "TemplateLiteral" && Array.isArray(node.expressions) && node.expressions.length === 0 && Array.isArray(node.quasis)) {
|
|
911
|
+
const first = node.quasis[0] as { value?: { cooked?: unknown } } | undefined;
|
|
912
|
+
if (typeof first?.value?.cooked === "string") return first.value.cooked;
|
|
913
|
+
}
|
|
914
|
+
return undefined;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function directRunsCall(node: unknown, method: "run" | "all"): node is AstNode {
|
|
918
|
+
if (!astNode(node) || node.type !== "CallExpression" || !astNode(node.callee) || node.callee.type !== "MemberExpression") return false;
|
|
919
|
+
const property = node.callee.computed === true ? literalString(node.callee.property) : astNode(node.callee.property) && node.callee.property.type === "Identifier" ? node.callee.property.name : undefined;
|
|
920
|
+
return property === method && astNode(node.callee.object) && node.callee.object.type === "Identifier" && node.callee.object.name === "runs";
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function nodeLocation(node: AstNode): Pick<WorkflowScriptValidationError, "line" | "column"> {
|
|
924
|
+
return node.loc ? { line: Math.max(1, node.loc.start.line - 1), column: node.loc.start.column + 1 } : {};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function walkAst(node: unknown, visit: (node: AstNode) => void, includeNestedFunctions = true): void {
|
|
928
|
+
if (Array.isArray(node)) {
|
|
929
|
+
for (const item of node) walkAst(item, visit, includeNestedFunctions);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (!astNode(node)) return;
|
|
933
|
+
visit(node);
|
|
934
|
+
if (!includeNestedFunctions && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression")) return;
|
|
935
|
+
for (const [key, child] of Object.entries(node)) {
|
|
936
|
+
if (!AST_LOCATION_KEYS.has(key)) walkAst(child, visit, includeNestedFunctions);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function definitelyNonJson(node: AstNode, normalizeUndefined = false): string | undefined {
|
|
941
|
+
if (node.type === "Literal") {
|
|
942
|
+
if (typeof node.bigint === "string") return "BigInt values are not JSON-representable";
|
|
943
|
+
if (node.regex !== undefined) return "regular expressions are not JSON-representable";
|
|
944
|
+
return undefined;
|
|
945
|
+
}
|
|
946
|
+
if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") return "functions are not JSON-representable";
|
|
947
|
+
if (node.type === "Identifier" && node.name === "undefined") return normalizeUndefined ? undefined : "undefined is not JSON-representable";
|
|
948
|
+
if (node.type === "UnaryExpression" && node.operator === "void") return normalizeUndefined ? undefined : "undefined is not JSON-representable";
|
|
949
|
+
if (node.type === "ArrayExpression" && Array.isArray(node.elements)) {
|
|
950
|
+
if (node.elements.some((entry) => entry === null)) return "sparse arrays are not JSON-representable";
|
|
951
|
+
for (const entry of node.elements) if (astNode(entry)) {
|
|
952
|
+
const error = definitelyNonJson(entry, normalizeUndefined);
|
|
953
|
+
if (error) return error;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (node.type === "ObjectExpression" && Array.isArray(node.properties)) {
|
|
957
|
+
const values = new Map<string, AstNode>();
|
|
958
|
+
for (const property of node.properties) {
|
|
959
|
+
if (!astNode(property) || property.type !== "Property" || !astNode(property.value)) return undefined;
|
|
960
|
+
const key = staticPropertyKey(property);
|
|
961
|
+
if (key === undefined) return undefined;
|
|
962
|
+
values.set(key, property.value);
|
|
963
|
+
}
|
|
964
|
+
for (const value of values.values()) {
|
|
965
|
+
const error = definitelyNonJson(value, normalizeUndefined);
|
|
966
|
+
if (error) return error;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return undefined;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function staticPropertyKey(property: AstNode): string | undefined {
|
|
973
|
+
return property.computed === true
|
|
974
|
+
? literalString(property.key)
|
|
975
|
+
: literalString(property.key) ?? (astNode(property.key) && property.key.type === "Identifier" ? property.key.name as string : undefined);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function directObjectPropertyValue(node: AstNode, name: string): AstNode | undefined {
|
|
979
|
+
if (node.type !== "ObjectExpression" || !Array.isArray(node.properties)) return undefined;
|
|
980
|
+
let value: AstNode | undefined;
|
|
981
|
+
for (const property of node.properties) {
|
|
982
|
+
if (!astNode(property) || property.type !== "Property" || !astNode(property.value)) continue;
|
|
983
|
+
if (staticPropertyKey(property) === name) value = property.value;
|
|
984
|
+
}
|
|
985
|
+
return value;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function directRunsAllKeys(call: AstNode): Array<{ key: string; node: AstNode }> {
|
|
989
|
+
const args = Array.isArray(call.arguments) ? call.arguments : [];
|
|
990
|
+
const items = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0].elements : [];
|
|
991
|
+
return items.flatMap((item) => {
|
|
992
|
+
if (!astNode(item)) return [];
|
|
993
|
+
const keyNode = directObjectPropertyValue(item, "key");
|
|
994
|
+
const key = literalString(keyNode);
|
|
995
|
+
return keyNode && key !== undefined ? [{ key, node: keyNode }] : [];
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/** Parse a workflowScript and apply only rules that are decidable from its local syntax. */
|
|
1000
|
+
export function validateWorkflowScript(script: string): WorkflowScriptValidationResult {
|
|
1001
|
+
const errors: WorkflowScriptValidationError[] = [];
|
|
1002
|
+
if (!script.trim()) return { ok: false, errors: [{ message: "workflowScript must not be empty." }] };
|
|
1003
|
+
let root: AstNode;
|
|
1004
|
+
try {
|
|
1005
|
+
const parser = requireFromPackage(resolveWorkflowParserEntry()) as { parse(source: string, options: Record<string, unknown>): unknown };
|
|
1006
|
+
root = parser.parse(`(async () => {\n${script}\n})()`, { ecmaVersion: "latest", sourceType: "script", locations: true }) as AstNode;
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
const location = error && typeof error === "object" && "loc" in error && error.loc && typeof error.loc === "object"
|
|
1009
|
+
? error.loc as { line?: unknown; column?: unknown }
|
|
1010
|
+
: undefined;
|
|
1011
|
+
const message = (error instanceof Error ? error.message : String(error)).replace(/\s+\(\d+:\d+\)$/, "");
|
|
1012
|
+
return { ok: false, errors: [{ message, ...(typeof location?.line === "number" ? { line: Math.max(1, location.line - 1) } : {}), ...(typeof location?.column === "number" ? { column: location.column + 1 } : {}) }] };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const wrapper = astNode(root.body) ? undefined : Array.isArray(root.body) && astNode(root.body[0]) && astNode(root.body[0].expression) && astNode(root.body[0].expression.callee)
|
|
1016
|
+
? root.body[0].expression.callee
|
|
1017
|
+
: undefined;
|
|
1018
|
+
const workflowBody = wrapper && astNode(wrapper.body) ? wrapper.body : root;
|
|
1019
|
+
walkAst(workflowBody, (node) => {
|
|
1020
|
+
if (node !== wrapper && node.async === true && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression")) {
|
|
1021
|
+
errors.push({ message: "workflowScript does not support nested async functions. Use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains.", ...nodeLocation(node) });
|
|
1022
|
+
}
|
|
1023
|
+
if (directRunsCall(node, "run")) {
|
|
1024
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
1025
|
+
const keyNode = astNode(args[0]) ? args[0] : undefined;
|
|
1026
|
+
const key = literalString(keyNode);
|
|
1027
|
+
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) });
|
|
1028
|
+
if (astNode(args[1])) {
|
|
1029
|
+
const message = definitelyNonJson(args[1]);
|
|
1030
|
+
if (message) errors.push({ message: `runs.run params are invalid: ${message}.`, ...nodeLocation(args[1]) });
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (directRunsCall(node, "all")) {
|
|
1034
|
+
for (const entry of directRunsAllKeys(node)) if (!KEY_PATTERN.test(entry.key)) errors.push({ message: "runs.all item key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.", ...nodeLocation(entry.node) });
|
|
1035
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
1036
|
+
if (astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements)) {
|
|
1037
|
+
for (const item of args[0].elements) if (astNode(item)) {
|
|
1038
|
+
const message = definitelyNonJson(item);
|
|
1039
|
+
if (message) errors.push({ message: `runs.all item params are invalid: ${message}.`, ...nodeLocation(item) });
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const boundaryValue = node.type === "CallExpression" && astNode(node.callee) && node.callee.type === "Identifier" && node.callee.name === "emit" && Array.isArray(node.arguments) && astNode(node.arguments[0])
|
|
1044
|
+
? node.arguments[0]
|
|
1045
|
+
: node.type === "CallExpression" && astNode(node.callee) && node.callee.type === "MemberExpression" && astNode(node.callee.object) && node.callee.object.type === "Identifier" && node.callee.object.name === "state" && astNode(node.callee.property) && node.callee.property.type === "Identifier" && node.callee.property.name === "set" && Array.isArray(node.arguments) && astNode(node.arguments[1])
|
|
1046
|
+
? node.arguments[1]
|
|
1047
|
+
: undefined;
|
|
1048
|
+
if (boundaryValue) {
|
|
1049
|
+
const message = definitelyNonJson(boundaryValue);
|
|
1050
|
+
if (message) errors.push({ message: `workflowScript boundary value is invalid: ${message}.`, ...nodeLocation(boundaryValue) });
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
walkAst(workflowBody, (node) => {
|
|
1054
|
+
if (node.type !== "ReturnStatement" || !astNode(node.argument)) return;
|
|
1055
|
+
const message = definitelyNonJson(node.argument, true);
|
|
1056
|
+
if (message) errors.push({ message: `workflowScript boundary value is invalid: ${message}.`, ...nodeLocation(node.argument) });
|
|
1057
|
+
}, false);
|
|
1058
|
+
|
|
1059
|
+
if (workflowBody.type === "BlockStatement" && Array.isArray(workflowBody.body)) {
|
|
1060
|
+
for (let statementIndex = 0; statementIndex < workflowBody.body.length; statementIndex++) {
|
|
1061
|
+
const statement = workflowBody.body[statementIndex];
|
|
1062
|
+
if (!astNode(statement) || statement.type !== "VariableDeclaration" || !Array.isArray(statement.declarations)) continue;
|
|
1063
|
+
for (const declaration of statement.declarations) {
|
|
1064
|
+
if (!astNode(declaration) || !astNode(declaration.id) || declaration.id.type !== "Identifier" || !astNode(declaration.init) || declaration.init.type !== "AwaitExpression" || !directRunsCall(declaration.init.argument, "all")) continue;
|
|
1065
|
+
const name = declaration.id.name as string;
|
|
1066
|
+
const keys = new Set(directRunsAllKeys(declaration.init.argument).map((entry) => entry.key));
|
|
1067
|
+
if (keys.size === 0) continue;
|
|
1068
|
+
const args = Array.isArray(declaration.init.argument.arguments) ? declaration.init.argument.arguments : [];
|
|
1069
|
+
const itemCount = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0].elements.length : 0;
|
|
1070
|
+
const arrayResultShape = Array.from({ length: itemCount });
|
|
1071
|
+
for (const later of workflowBody.body.slice(statementIndex + 1)) walkAst(later, (node) => {
|
|
1072
|
+
if (node.type !== "MemberExpression" || !astNode(node.object) || node.object.type !== "Identifier" || node.object.name !== name) return;
|
|
1073
|
+
const property = node.computed === true ? literalString(node.property) : astNode(node.property) && node.property.type === "Identifier" ? node.property.name as string : undefined;
|
|
1074
|
+
if (property && keys.has(property) && !(property in arrayResultShape)) errors.push({ message: `runs.all returns an ordered array; '${name}.${property}' is keyed access. Use an index, destructuring, or map(...).`, ...nodeLocation(node) });
|
|
1075
|
+
}, false);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const unique = errors.filter((error, index) => errors.findIndex((candidate) => candidate.message === error.message && candidate.line === error.line && candidate.column === error.column) === index);
|
|
1081
|
+
return { ok: unique.length === 0, errors: unique };
|
|
1082
|
+
}
|
|
820
1083
|
function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label" | "agent"> {
|
|
821
1084
|
return {
|
|
822
1085
|
...(typeof params.phase === "string" && params.phase.trim() ? { phase: params.phase.trim() } : {}),
|
|
@@ -1002,7 +1265,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1002
1265
|
return finish({ value: message.value });
|
|
1003
1266
|
}
|
|
1004
1267
|
if (message.type === "error") {
|
|
1005
|
-
const
|
|
1268
|
+
const rawError = typeof message.error === "string" ? message.error : "Workflow script failed.";
|
|
1269
|
+
const text = message.errorPhase === "return-serialization" ? `${rawError}${workflowReturnRecoveryHint(partial().children)}` : rawError;
|
|
1270
|
+
const workflowError = new Error(text) as Error & { workflowErrorKind?: "detached-child" };
|
|
1006
1271
|
if (message.errorKind === "detached-child") workflowError.workflowErrorKind = "detached-child";
|
|
1007
1272
|
return finish({ error: workflowError });
|
|
1008
1273
|
}
|
|
@@ -1118,6 +1383,31 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1118
1383
|
}
|
|
1119
1384
|
const params = message.args.params;
|
|
1120
1385
|
if (!isRecord(params)) return respond(Promise.reject(new Error(`runs.run('${key}', params) requires a params object.`)));
|
|
1386
|
+
const collectFailure = message.args.collectFailure === true;
|
|
1387
|
+
const callObserved = observedRunCalls.delete(message.callId);
|
|
1388
|
+
const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
|
|
1389
|
+
? promise
|
|
1390
|
+
: promise.then((result) => {
|
|
1391
|
+
if (!result.ok && !result.stopped) {
|
|
1392
|
+
const childError = new Error(result.detached ? `Run '${key}' detached: ${result.error ?? result.output}` : `Run '${key}' failed: ${result.error ?? result.output}`) as Error & { workflowErrorKind?: "detached-child" };
|
|
1393
|
+
if (result.detached) childError.workflowErrorKind = "detached-child";
|
|
1394
|
+
throw childError;
|
|
1395
|
+
}
|
|
1396
|
+
return result;
|
|
1397
|
+
});
|
|
1398
|
+
const fingerprint = stableJson(params);
|
|
1399
|
+
const existing = launches.get(key);
|
|
1400
|
+
if (existing) {
|
|
1401
|
+
if (existing.fingerprint !== fingerprint) return respond(Promise.reject(new Error(`Duplicate workflow key '${key}' used with incompatible launch params.`)));
|
|
1402
|
+
if (callObserved) existing.observed = true;
|
|
1403
|
+
trace.push({ operation: "run", key, state: "reused", ...workflowStringMetadata(params) });
|
|
1404
|
+
traceChanged();
|
|
1405
|
+
return respond(deliver(existing.promise), `runs.run('${key}') result`);
|
|
1406
|
+
}
|
|
1407
|
+
const permitError = options.oneUsePermit?.claim(key);
|
|
1408
|
+
if (permitError) return respond(Promise.reject(new Error(permitError)));
|
|
1409
|
+
if (options.oneUsePermit && message.args.batch !== undefined) return respond(Promise.reject(new Error("Workflow child permit does not support runs.all.")));
|
|
1410
|
+
if (options.oneUsePermit && params.resume !== undefined) return respond(Promise.reject(new Error("Workflow child permit does not support retained resume.")));
|
|
1121
1411
|
if (params.action !== undefined) return respond(Promise.reject(new Error(`runs.run('${key}') accepts execution params only; management action is not allowed.`)));
|
|
1122
1412
|
if (params.workflowScript !== undefined) return respond(Promise.reject(new Error(`runs.run('${key}') cannot start a nested workflow script.`)));
|
|
1123
1413
|
if (params.tasks !== undefined || params.chain !== undefined || params.parallel !== undefined || params.concurrency !== undefined || params.chainDir !== undefined) {
|
|
@@ -1148,28 +1438,6 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1148
1438
|
if (params.resume !== undefined && (typeof params.task !== "string" || !params.task.trim())) {
|
|
1149
1439
|
return respond(Promise.reject(new Error(`runs.run('${key}') resume requires a non-empty task follow-up.`)));
|
|
1150
1440
|
}
|
|
1151
|
-
const collectFailure = message.args.collectFailure === true;
|
|
1152
|
-
const callObserved = observedRunCalls.delete(message.callId);
|
|
1153
|
-
const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
|
|
1154
|
-
? promise
|
|
1155
|
-
: promise.then((result) => {
|
|
1156
|
-
if (!result.ok && !result.stopped) {
|
|
1157
|
-
const childError = new Error(result.detached ? `Run '${key}' detached: ${result.error ?? result.output}` : `Run '${key}' failed: ${result.error ?? result.output}`) as Error & { workflowErrorKind?: "detached-child" };
|
|
1158
|
-
if (result.detached) childError.workflowErrorKind = "detached-child";
|
|
1159
|
-
throw childError;
|
|
1160
|
-
}
|
|
1161
|
-
return result;
|
|
1162
|
-
});
|
|
1163
|
-
const fingerprint = stableJson(params);
|
|
1164
|
-
const existing = launches.get(key);
|
|
1165
|
-
if (existing) {
|
|
1166
|
-
if (existing.fingerprint !== fingerprint) return respond(Promise.reject(new Error(`Duplicate workflow key '${key}' used with incompatible launch params.`)));
|
|
1167
|
-
if (callObserved) existing.observed = true;
|
|
1168
|
-
trace.push({ operation: "run", key, state: "reused", ...workflowStringMetadata(params) });
|
|
1169
|
-
traceChanged();
|
|
1170
|
-
return respond(deliver(existing.promise), `runs.run('${key}') result`);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
1441
|
const startedAt = Date.now();
|
|
1174
1442
|
const batch = isRecord(message.args.batch) && typeof message.args.batch.id === "string" && Array.isArray(message.args.batch.calls)
|
|
1175
1443
|
? { id: message.args.batch.id, calls: message.args.batch.calls.filter((call): call is { key: string; params: Record<string, unknown> } => isRecord(call) && typeof call.key === "string" && isRecord(call.params)) }
|
|
@@ -1219,7 +1487,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1219
1487
|
if (resolvedResumeLineage.at(-1) !== resolvedResumeId) resolvedResumeLineage.push(resolvedResumeId!);
|
|
1220
1488
|
}
|
|
1221
1489
|
const launchParams = resolvedResumeId ? { ...params, resume: resolvedResumeId } : params;
|
|
1222
|
-
return options.launch(key, launchParams, childSignal, { admitted: true });
|
|
1490
|
+
return options.launch(key, launchParams, childSignal, { admitted: true, batch: batch !== undefined });
|
|
1223
1491
|
}).then((result) => {
|
|
1224
1492
|
let normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
|
|
1225
1493
|
if (resolvedResumeLineage?.length && normalized.runId) {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { AsyncStatus, WorkflowChildSummaryV1 } from "../shared/types.ts";
|
|
2
|
+
import type { WorkflowScriptChildResult, WorkflowScriptTraceEntry } from "./scripted-workflow.ts";
|
|
3
|
+
|
|
4
|
+
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
5
|
+
const TERMINAL_STATES = new Set(["completed", "failed", "paused", "stopped", "rejected", "detached"]);
|
|
6
|
+
|
|
7
|
+
function bounded(value: unknown, maxBytes: number): string | undefined {
|
|
8
|
+
if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value, "utf8") > maxBytes) return undefined;
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function requiredId(value: string, label: string): string {
|
|
13
|
+
const id = bounded(value, 256);
|
|
14
|
+
if (!id) throw new Error(`${label} must be a non-empty identifier of at most 256 UTF-8 bytes.`);
|
|
15
|
+
return id;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function workflowChildSummary(input: {
|
|
19
|
+
parentToolCallId: string;
|
|
20
|
+
workflowRunId: string;
|
|
21
|
+
workflowState: WorkflowChildSummaryV1["workflowState"];
|
|
22
|
+
inventoryComplete: boolean;
|
|
23
|
+
trace?: WorkflowScriptTraceEntry[];
|
|
24
|
+
children?: WorkflowScriptChildResult[];
|
|
25
|
+
steps?: NonNullable<AsyncStatus["steps"]>;
|
|
26
|
+
}): WorkflowChildSummaryV1 {
|
|
27
|
+
const rows = new Map<string, WorkflowChildSummaryV1["children"][number]>();
|
|
28
|
+
for (const entry of input.trace ?? []) {
|
|
29
|
+
if (entry.operation !== "run" || !KEY_PATTERN.test(entry.key)) continue;
|
|
30
|
+
const previous = rows.get(entry.key);
|
|
31
|
+
const state = entry.state === "completed" ? "completed"
|
|
32
|
+
: entry.state === "failed" ? "failed"
|
|
33
|
+
: entry.state === "stopped" ? "stopped"
|
|
34
|
+
: entry.state === "detached" ? "detached"
|
|
35
|
+
: previous?.state ?? "running";
|
|
36
|
+
rows.set(entry.key, {
|
|
37
|
+
childId: entry.key,
|
|
38
|
+
state,
|
|
39
|
+
...(bounded(entry.runId, 256) ? { runId: bounded(entry.runId, 256) } : {}),
|
|
40
|
+
...(previous?.agent ? { agent: previous.agent } : {}),
|
|
41
|
+
...(previous?.model ? { model: previous.model } : {}),
|
|
42
|
+
...(previous?.thinking ? { thinking: previous.thinking } : {}),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
for (const step of input.steps ?? []) {
|
|
46
|
+
const key = step.workflowKey;
|
|
47
|
+
if (!key || !KEY_PATTERN.test(key)) continue;
|
|
48
|
+
const state = step.status === "complete" || step.status === "completed" ? "completed"
|
|
49
|
+
: step.status === "failed" ? "failed"
|
|
50
|
+
: step.status === "paused" ? "paused"
|
|
51
|
+
: step.status === "stopped" ? "stopped"
|
|
52
|
+
: step.status === "rejected" ? "rejected"
|
|
53
|
+
: step.status === "pending" ? "pending" : "running";
|
|
54
|
+
const launchResolved = step.async !== undefined || step.sessionFile !== undefined || step.runId !== undefined || step.model !== undefined;
|
|
55
|
+
rows.set(key, {
|
|
56
|
+
childId: key,
|
|
57
|
+
state,
|
|
58
|
+
...(bounded(step.runId, 256) ? { runId: bounded(step.runId, 256) } : {}),
|
|
59
|
+
...(launchResolved && bounded(step.agent, 256) ? { agent: bounded(step.agent, 256) } : {}),
|
|
60
|
+
...(bounded(step.model, 256) ? { model: bounded(step.model, 256) } : {}),
|
|
61
|
+
...(bounded(step.thinking, 32) ? { thinking: bounded(step.thinking, 32) } : {}),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
for (const child of input.children ?? []) {
|
|
65
|
+
if (!KEY_PATTERN.test(child.key)) continue;
|
|
66
|
+
const result = Array.isArray(child.results) ? child.results.find((value) => value && typeof value === "object") as Record<string, unknown> | undefined : undefined;
|
|
67
|
+
const state = child.detached ? "detached" : child.stopped ? "stopped" : child.interrupted ? "paused" : child.ok ? "completed" : result?.acceptance && typeof result.acceptance === "object" && (result.acceptance as { status?: unknown }).status === "rejected" ? "rejected" : "failed";
|
|
68
|
+
rows.set(child.key, {
|
|
69
|
+
childId: child.key,
|
|
70
|
+
state,
|
|
71
|
+
...(bounded(child.runId, 256) ? { runId: bounded(child.runId, 256) } : {}),
|
|
72
|
+
...(bounded(child.agent, 256) ? { agent: bounded(child.agent, 256) } : {}),
|
|
73
|
+
...(bounded(result?.model, 256) ? { model: bounded(result?.model, 256) } : {}),
|
|
74
|
+
...(bounded(result?.thinking, 32) ? { thinking: bounded(result?.thinking, 32) } : {}),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (input.inventoryComplete) {
|
|
78
|
+
for (const [key, row] of rows) {
|
|
79
|
+
if (!TERMINAL_STATES.has(row.state)) rows.set(key, { ...row, state: input.workflowState === "stopped" ? "stopped" : "failed" });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
version: 1,
|
|
84
|
+
parentToolCallId: requiredId(input.parentToolCallId, "parentToolCallId"),
|
|
85
|
+
workflowRunId: requiredId(input.workflowRunId, "workflowRunId"),
|
|
86
|
+
inventoryComplete: input.inventoryComplete,
|
|
87
|
+
workflowState: input.workflowState,
|
|
88
|
+
children: [...rows.values()],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parseWorkflowChildSummary(value: unknown): WorkflowChildSummaryV1 | undefined {
|
|
93
|
+
if (value === undefined) return undefined;
|
|
94
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("workflowChildren must be an object.");
|
|
95
|
+
const input = value as Record<string, unknown>;
|
|
96
|
+
const allowed = new Set(["version", "parentToolCallId", "workflowRunId", "inventoryComplete", "workflowState", "children"]);
|
|
97
|
+
if (Object.keys(input).some((key) => !allowed.has(key))) throw new Error("workflowChildren has unsupported fields.");
|
|
98
|
+
if (input.version !== 1 || typeof input.inventoryComplete !== "boolean" || !Array.isArray(input.children)) throw new Error("workflowChildren is invalid.");
|
|
99
|
+
const workflowState = input.workflowState;
|
|
100
|
+
if (workflowState !== "queued" && workflowState !== "running" && workflowState !== "completed" && workflowState !== "failed" && workflowState !== "paused" && workflowState !== "stopped") throw new Error("workflowChildren.workflowState is invalid.");
|
|
101
|
+
const children = input.children.map((row): WorkflowChildSummaryV1["children"][number] => {
|
|
102
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) throw new Error("workflowChildren child row is invalid.");
|
|
103
|
+
const child = row as Record<string, unknown>;
|
|
104
|
+
if (Object.keys(child).some((key) => !["childId", "runId", "agent", "model", "thinking", "state"].includes(key))) throw new Error("workflowChildren child row has unsupported fields.");
|
|
105
|
+
if (typeof child.childId !== "string" || !KEY_PATTERN.test(child.childId)) throw new Error("workflowChildren childId is invalid.");
|
|
106
|
+
const state = child.state;
|
|
107
|
+
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.");
|
|
108
|
+
for (const [field, maxBytes] of [["runId", 256], ["agent", 256], ["model", 256], ["thinking", 32]] as const) {
|
|
109
|
+
if (child[field] !== undefined && bounded(child[field], maxBytes) === undefined) throw new Error(`workflowChildren child ${field} is invalid.`);
|
|
110
|
+
}
|
|
111
|
+
return { childId: child.childId, state, ...(child.runId ? { runId: child.runId as string } : {}), ...(child.agent ? { agent: child.agent as string } : {}), ...(child.model ? { model: child.model as string } : {}), ...(child.thinking ? { thinking: child.thinking as string } : {}) };
|
|
112
|
+
});
|
|
113
|
+
if (new Set(children.map((child) => child.childId)).size !== children.length) throw new Error("workflowChildren has duplicate childId values.");
|
|
114
|
+
if (typeof input.parentToolCallId !== "string") throw new Error("workflowChildren.parentToolCallId is invalid.");
|
|
115
|
+
if (typeof input.workflowRunId !== "string") throw new Error("workflowChildren.workflowRunId is invalid.");
|
|
116
|
+
return { version: 1, parentToolCallId: requiredId(input.parentToolCallId, "parentToolCallId"), workflowRunId: requiredId(input.workflowRunId, "workflowRunId"), inventoryComplete: input.inventoryComplete, workflowState, children };
|
|
117
|
+
}
|