pi-subagents 0.56.0 → 0.58.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 +92 -0
- 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 +124 -21
- package/docs/configuration.md +37 -0
- package/docs/extension-api.md +41 -2
- package/docs/models.md +3 -3
- package/docs/observability.md +8 -7
- package/docs/tool-reference.md +14 -3
- package/docs/workflows.md +44 -0
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +2 -0
- package/skills/pi-subagents/references/execution-controls.md +2 -2
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/agent-management.ts +36 -5
- package/src/agents/agent-refinements.ts +4 -4
- package/src/agents/agent-serializer.ts +5 -0
- package/src/agents/agents.ts +257 -51
- package/src/agents/builtin-names.ts +6 -0
- package/src/agents/runtime-agent-events.ts +70 -0
- package/src/agents/runtime-agent-registry.ts +18 -4
- package/src/api/agents.ts +10 -5
- package/src/api/preflight.ts +28 -3
- package/src/extension/config.ts +70 -0
- package/src/extension/doctor.ts +3 -3
- package/src/extension/index.ts +33 -8
- package/src/extension/public-execution.ts +29 -13
- package/src/extension/rpc.ts +55 -19
- package/src/extension/schemas.ts +6 -5
- package/src/extension/tool-description.ts +18 -12
- 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 +51 -3
- package/src/runs/background/active-async-capacity.ts +77 -10
- package/src/runs/background/async-execution.ts +127 -19
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-resume.ts +6 -2
- package/src/runs/background/async-retention.ts +20 -3
- package/src/runs/background/async-status.ts +7 -0
- package/src/runs/background/chain-append.ts +2 -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/notify.ts +26 -3
- package/src/runs/background/result-delivery-ownership.ts +45 -0
- package/src/runs/background/result-files.ts +27 -14
- package/src/runs/background/result-watcher.ts +36 -15
- 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 +239 -56
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/execution.ts +92 -11
- package/src/runs/foreground/foreground-control.ts +6 -0
- package/src/runs/foreground/foreground-history.ts +22 -1
- package/src/runs/foreground/subagent-executor.ts +322 -102
- package/src/runs/foreground/workflow-detach-reconcile.ts +144 -18
- 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 +4 -3
- package/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/src/runs/shared/dynamic-fanout.ts +3 -3
- 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 +348 -55
- package/src/runs/shared/fast-mode-extension.ts +5 -5
- package/src/runs/shared/launch-cwd.ts +16 -0
- package/src/runs/shared/long-running-guard.ts +2 -1
- package/src/runs/shared/mcp-config-sources.ts +386 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
- package/src/runs/shared/model-exclusions.ts +69 -7
- package/src/runs/shared/model-fallback.ts +39 -5
- package/src/runs/shared/mutation-evidence.ts +7 -2
- 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 +8 -1
- package/src/runs/shared/pi-args.ts +61 -6
- package/src/runs/shared/process-signal.ts +13 -0
- package/src/runs/shared/run-history.ts +21 -1
- package/src/runs/shared/single-output.ts +17 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
- package/src/shared/fork-context.ts +21 -0
- package/src/shared/formatters.ts +13 -1
- package/src/shared/launch-contract.ts +4 -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 +10 -2
- package/src/shared/shortcuts.ts +17 -0
- package/src/shared/types.ts +160 -10
- package/src/shared/utils.ts +6 -29
- 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/slash/subagents-admin.ts +3 -0
- 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 +55 -21
- package/src/workflows/scripted-workflow.ts +299 -31
- package/src/workflows/workflow-child-summary.ts +117 -0
- package/src/workflows/workflow-receipt.ts +155 -5
|
@@ -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");
|
|
@@ -440,7 +451,7 @@ function isSyntaxError(error) {
|
|
|
440
451
|
return error instanceof SyntaxError || error?.name === "SyntaxError";
|
|
441
452
|
}
|
|
442
453
|
|
|
443
|
-
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 } };";
|
|
444
455
|
const AST_SCALAR_KEYS = new Set(["type", "start", "end"]);
|
|
445
456
|
|
|
446
457
|
function assertPortableWorkflowScript(source) {
|
|
@@ -627,7 +638,12 @@ parentPort.on("message", async (message) => {
|
|
|
627
638
|
}
|
|
628
639
|
}
|
|
629
640
|
const persistedValue = value === undefined ? null : omitUndefinedWorkflowValues(value);
|
|
630
|
-
|
|
641
|
+
try {
|
|
642
|
+
assertJsonValue(persistedValue, "return");
|
|
643
|
+
} catch (error) {
|
|
644
|
+
parentPort.postMessage({ type: "error", errorPhase: "return-serialization", error: formatWorkflowScriptError(error) });
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
631
647
|
parentPort.postMessage({ type: "complete", value: persistedValue });
|
|
632
648
|
} catch (error) {
|
|
633
649
|
parentPort.postMessage({ type: "error", error: isSyntaxError(error) ? formatWorkflowScriptSyntaxError(error) : formatWorkflowScriptError(error), ...(error && error.workflowErrorKind === "detached-child" ? { errorKind: "detached-child" } : {}) });
|
|
@@ -638,6 +654,7 @@ parentPort.on("message", async (message) => {
|
|
|
638
654
|
export interface WorkflowScriptChildResult {
|
|
639
655
|
key: string;
|
|
640
656
|
ok: boolean;
|
|
657
|
+
terminalOutcome?: import("../shared/types.ts").WorkflowTerminalOutcome;
|
|
641
658
|
stopped?: boolean;
|
|
642
659
|
/** Canonical child agent name when launch resolution produced one. */
|
|
643
660
|
agent?: string;
|
|
@@ -650,6 +667,8 @@ export interface WorkflowScriptChildResult {
|
|
|
650
667
|
requestedContext?: "fresh" | "fork";
|
|
651
668
|
resolvedContext?: "fresh" | "fork" | "mixed";
|
|
652
669
|
outputReference?: string;
|
|
670
|
+
outputPathMapping?: { requestedPath: string; savedPath: string };
|
|
671
|
+
externalAdapter?: import("../shared/types.ts").ExternalCliReceiptMetadata;
|
|
653
672
|
resumability?: { state: "resumable" } | { state: "not-resumable"; reason: string };
|
|
654
673
|
continuation?: { runIds: string[] };
|
|
655
674
|
artifactPaths: string[];
|
|
@@ -705,9 +724,9 @@ export interface WorkflowScriptResult {
|
|
|
705
724
|
|
|
706
725
|
export class WorkflowScriptError extends Error {
|
|
707
726
|
readonly partial: Omit<WorkflowScriptResult, "value">;
|
|
708
|
-
readonly errorKind?: "detached-child";
|
|
727
|
+
readonly errorKind?: "detached-child" | "timeout";
|
|
709
728
|
|
|
710
|
-
constructor(message: string, partial: Omit<WorkflowScriptResult, "value">, errorKind?: "detached-child") {
|
|
729
|
+
constructor(message: string, partial: Omit<WorkflowScriptResult, "value">, errorKind?: "detached-child" | "timeout") {
|
|
711
730
|
super(message);
|
|
712
731
|
this.name = "WorkflowScriptError";
|
|
713
732
|
this.partial = partial;
|
|
@@ -717,10 +736,12 @@ export class WorkflowScriptError extends Error {
|
|
|
717
736
|
|
|
718
737
|
export interface RunWorkflowScriptOptions {
|
|
719
738
|
script: string;
|
|
739
|
+
/** Host-only first-slice admission context. It is never sent to the workflow worker. */
|
|
740
|
+
oneUsePermit?: { claim: (key: string) => string | undefined };
|
|
720
741
|
timeoutMs?: number;
|
|
721
742
|
signal?: AbortSignal;
|
|
722
743
|
admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
|
|
723
|
-
launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean }) => Promise<WorkflowScriptChildResult>;
|
|
744
|
+
launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean; batch: boolean }) => Promise<WorkflowScriptChildResult>;
|
|
724
745
|
resolveResume?: (reference: WorkflowReceiptResumeReference, signal: AbortSignal) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
|
|
725
746
|
status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
|
|
726
747
|
steer?: (key: string, message: string, options: WorkflowSteerOptions, signal: AbortSignal) => Promise<WorkflowSteerResult>;
|
|
@@ -827,6 +848,15 @@ export function formatWorkflowJsonPreview(value: unknown, maxLength: number): st
|
|
|
827
848
|
}
|
|
828
849
|
}
|
|
829
850
|
|
|
851
|
+
function workflowReturnRecoveryHint(children: WorkflowScriptChildResult[]): string {
|
|
852
|
+
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 }.";
|
|
853
|
+
const references = children.slice(0, 10).map((child) => {
|
|
854
|
+
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);
|
|
855
|
+
return `'${child.key}'${fields.length > 0 ? ` (${fields.join(", ")})` : ""}`;
|
|
856
|
+
});
|
|
857
|
+
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 }.`;
|
|
858
|
+
}
|
|
859
|
+
|
|
830
860
|
export interface SimpleWorkflowRunPreview {
|
|
831
861
|
agent?: string;
|
|
832
862
|
task?: string;
|
|
@@ -864,6 +894,194 @@ function validateKey(value: unknown, owner = "runs.run"): string {
|
|
|
864
894
|
return value;
|
|
865
895
|
}
|
|
866
896
|
|
|
897
|
+
type AstNode = {
|
|
898
|
+
type: string;
|
|
899
|
+
loc?: { start: { line: number; column: number } };
|
|
900
|
+
[key: string]: unknown;
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
const AST_LOCATION_KEYS = new Set(["type", "start", "end", "loc", "range"]);
|
|
904
|
+
|
|
905
|
+
function astNode(value: unknown): value is AstNode {
|
|
906
|
+
return Boolean(value) && typeof value === "object" && typeof (value as { type?: unknown }).type === "string";
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function literalString(node: unknown): string | undefined {
|
|
910
|
+
if (!astNode(node)) return undefined;
|
|
911
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
912
|
+
if (node.type === "TemplateLiteral" && Array.isArray(node.expressions) && node.expressions.length === 0 && Array.isArray(node.quasis)) {
|
|
913
|
+
const first = node.quasis[0] as { value?: { cooked?: unknown } } | undefined;
|
|
914
|
+
if (typeof first?.value?.cooked === "string") return first.value.cooked;
|
|
915
|
+
}
|
|
916
|
+
return undefined;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function directRunsCall(node: unknown, method: "run" | "all"): node is AstNode {
|
|
920
|
+
if (!astNode(node) || node.type !== "CallExpression" || !astNode(node.callee) || node.callee.type !== "MemberExpression") return false;
|
|
921
|
+
const property = node.callee.computed === true ? literalString(node.callee.property) : astNode(node.callee.property) && node.callee.property.type === "Identifier" ? node.callee.property.name : undefined;
|
|
922
|
+
return property === method && astNode(node.callee.object) && node.callee.object.type === "Identifier" && node.callee.object.name === "runs";
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function nodeLocation(node: AstNode): Pick<WorkflowScriptValidationError, "line" | "column"> {
|
|
926
|
+
return node.loc ? { line: Math.max(1, node.loc.start.line - 1), column: node.loc.start.column + 1 } : {};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function walkAst(node: unknown, visit: (node: AstNode) => void, includeNestedFunctions = true): void {
|
|
930
|
+
if (Array.isArray(node)) {
|
|
931
|
+
for (const item of node) walkAst(item, visit, includeNestedFunctions);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (!astNode(node)) return;
|
|
935
|
+
visit(node);
|
|
936
|
+
if (!includeNestedFunctions && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression")) return;
|
|
937
|
+
for (const [key, child] of Object.entries(node)) {
|
|
938
|
+
if (!AST_LOCATION_KEYS.has(key)) walkAst(child, visit, includeNestedFunctions);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function definitelyNonJson(node: AstNode, normalizeUndefined = false): string | undefined {
|
|
943
|
+
if (node.type === "Literal") {
|
|
944
|
+
if (typeof node.bigint === "string") return "BigInt values are not JSON-representable";
|
|
945
|
+
if (node.regex !== undefined) return "regular expressions are not JSON-representable";
|
|
946
|
+
return undefined;
|
|
947
|
+
}
|
|
948
|
+
if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") return "functions are not JSON-representable";
|
|
949
|
+
if (node.type === "Identifier" && node.name === "undefined") return normalizeUndefined ? undefined : "undefined is not JSON-representable";
|
|
950
|
+
if (node.type === "UnaryExpression" && node.operator === "void") return normalizeUndefined ? undefined : "undefined is not JSON-representable";
|
|
951
|
+
if (node.type === "ArrayExpression" && Array.isArray(node.elements)) {
|
|
952
|
+
if (node.elements.some((entry) => entry === null)) return "sparse arrays are not JSON-representable";
|
|
953
|
+
for (const entry of node.elements) if (astNode(entry)) {
|
|
954
|
+
const error = definitelyNonJson(entry, normalizeUndefined);
|
|
955
|
+
if (error) return error;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (node.type === "ObjectExpression" && Array.isArray(node.properties)) {
|
|
959
|
+
const values = new Map<string, AstNode>();
|
|
960
|
+
for (const property of node.properties) {
|
|
961
|
+
if (!astNode(property) || property.type !== "Property" || !astNode(property.value)) return undefined;
|
|
962
|
+
const key = staticPropertyKey(property);
|
|
963
|
+
if (key === undefined) return undefined;
|
|
964
|
+
values.set(key, property.value);
|
|
965
|
+
}
|
|
966
|
+
for (const value of values.values()) {
|
|
967
|
+
const error = definitelyNonJson(value, normalizeUndefined);
|
|
968
|
+
if (error) return error;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return undefined;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function staticPropertyKey(property: AstNode): string | undefined {
|
|
975
|
+
return property.computed === true
|
|
976
|
+
? literalString(property.key)
|
|
977
|
+
: literalString(property.key) ?? (astNode(property.key) && property.key.type === "Identifier" ? property.key.name as string : undefined);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function directObjectPropertyValue(node: AstNode, name: string): AstNode | undefined {
|
|
981
|
+
if (node.type !== "ObjectExpression" || !Array.isArray(node.properties)) return undefined;
|
|
982
|
+
let value: AstNode | undefined;
|
|
983
|
+
for (const property of node.properties) {
|
|
984
|
+
if (!astNode(property) || property.type !== "Property" || !astNode(property.value)) continue;
|
|
985
|
+
if (staticPropertyKey(property) === name) value = property.value;
|
|
986
|
+
}
|
|
987
|
+
return value;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function directRunsAllKeys(call: AstNode): Array<{ key: string; node: AstNode }> {
|
|
991
|
+
const args = Array.isArray(call.arguments) ? call.arguments : [];
|
|
992
|
+
const items = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0].elements : [];
|
|
993
|
+
return items.flatMap((item) => {
|
|
994
|
+
if (!astNode(item)) return [];
|
|
995
|
+
const keyNode = directObjectPropertyValue(item, "key");
|
|
996
|
+
const key = literalString(keyNode);
|
|
997
|
+
return keyNode && key !== undefined ? [{ key, node: keyNode }] : [];
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/** Parse a workflowScript and apply only rules that are decidable from its local syntax. */
|
|
1002
|
+
export function validateWorkflowScript(script: string): WorkflowScriptValidationResult {
|
|
1003
|
+
const errors: WorkflowScriptValidationError[] = [];
|
|
1004
|
+
if (!script.trim()) return { ok: false, errors: [{ message: "workflowScript must not be empty." }] };
|
|
1005
|
+
let root: AstNode;
|
|
1006
|
+
try {
|
|
1007
|
+
const parser = requireFromPackage(resolveWorkflowParserEntry()) as { parse(source: string, options: Record<string, unknown>): unknown };
|
|
1008
|
+
root = parser.parse(`(async () => {\n${script}\n})()`, { ecmaVersion: "latest", sourceType: "script", locations: true }) as AstNode;
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
const location = error && typeof error === "object" && "loc" in error && error.loc && typeof error.loc === "object"
|
|
1011
|
+
? error.loc as { line?: unknown; column?: unknown }
|
|
1012
|
+
: undefined;
|
|
1013
|
+
const message = (error instanceof Error ? error.message : String(error)).replace(/\s+\(\d+:\d+\)$/, "");
|
|
1014
|
+
return { ok: false, errors: [{ message, ...(typeof location?.line === "number" ? { line: Math.max(1, location.line - 1) } : {}), ...(typeof location?.column === "number" ? { column: location.column + 1 } : {}) }] };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
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)
|
|
1018
|
+
? root.body[0].expression.callee
|
|
1019
|
+
: undefined;
|
|
1020
|
+
const workflowBody = wrapper && astNode(wrapper.body) ? wrapper.body : root;
|
|
1021
|
+
walkAst(workflowBody, (node) => {
|
|
1022
|
+
if (node !== wrapper && node.async === true && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression")) {
|
|
1023
|
+
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) });
|
|
1024
|
+
}
|
|
1025
|
+
if (directRunsCall(node, "run")) {
|
|
1026
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
1027
|
+
const keyNode = astNode(args[0]) ? args[0] : undefined;
|
|
1028
|
+
const key = literalString(keyNode);
|
|
1029
|
+
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) });
|
|
1030
|
+
if (astNode(args[1])) {
|
|
1031
|
+
const message = definitelyNonJson(args[1]);
|
|
1032
|
+
if (message) errors.push({ message: `runs.run params are invalid: ${message}.`, ...nodeLocation(args[1]) });
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (directRunsCall(node, "all")) {
|
|
1036
|
+
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) });
|
|
1037
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
1038
|
+
if (astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements)) {
|
|
1039
|
+
for (const item of args[0].elements) if (astNode(item)) {
|
|
1040
|
+
const message = definitelyNonJson(item);
|
|
1041
|
+
if (message) errors.push({ message: `runs.all item params are invalid: ${message}.`, ...nodeLocation(item) });
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const boundaryValue = node.type === "CallExpression" && astNode(node.callee) && node.callee.type === "Identifier" && node.callee.name === "emit" && Array.isArray(node.arguments) && astNode(node.arguments[0])
|
|
1046
|
+
? node.arguments[0]
|
|
1047
|
+
: 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])
|
|
1048
|
+
? node.arguments[1]
|
|
1049
|
+
: undefined;
|
|
1050
|
+
if (boundaryValue) {
|
|
1051
|
+
const message = definitelyNonJson(boundaryValue);
|
|
1052
|
+
if (message) errors.push({ message: `workflowScript boundary value is invalid: ${message}.`, ...nodeLocation(boundaryValue) });
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
walkAst(workflowBody, (node) => {
|
|
1056
|
+
if (node.type !== "ReturnStatement" || !astNode(node.argument)) return;
|
|
1057
|
+
const message = definitelyNonJson(node.argument, true);
|
|
1058
|
+
if (message) errors.push({ message: `workflowScript boundary value is invalid: ${message}.`, ...nodeLocation(node.argument) });
|
|
1059
|
+
}, false);
|
|
1060
|
+
|
|
1061
|
+
if (workflowBody.type === "BlockStatement" && Array.isArray(workflowBody.body)) {
|
|
1062
|
+
for (let statementIndex = 0; statementIndex < workflowBody.body.length; statementIndex++) {
|
|
1063
|
+
const statement = workflowBody.body[statementIndex];
|
|
1064
|
+
if (!astNode(statement) || statement.type !== "VariableDeclaration" || !Array.isArray(statement.declarations)) continue;
|
|
1065
|
+
for (const declaration of statement.declarations) {
|
|
1066
|
+
if (!astNode(declaration) || !astNode(declaration.id) || declaration.id.type !== "Identifier" || !astNode(declaration.init) || declaration.init.type !== "AwaitExpression" || !directRunsCall(declaration.init.argument, "all")) continue;
|
|
1067
|
+
const name = declaration.id.name as string;
|
|
1068
|
+
const keys = new Set(directRunsAllKeys(declaration.init.argument).map((entry) => entry.key));
|
|
1069
|
+
if (keys.size === 0) continue;
|
|
1070
|
+
const args = Array.isArray(declaration.init.argument.arguments) ? declaration.init.argument.arguments : [];
|
|
1071
|
+
const itemCount = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0].elements.length : 0;
|
|
1072
|
+
const arrayResultShape = Array.from({ length: itemCount });
|
|
1073
|
+
for (const later of workflowBody.body.slice(statementIndex + 1)) walkAst(later, (node) => {
|
|
1074
|
+
if (node.type !== "MemberExpression" || !astNode(node.object) || node.object.type !== "Identifier" || node.object.name !== name) return;
|
|
1075
|
+
const property = node.computed === true ? literalString(node.property) : astNode(node.property) && node.property.type === "Identifier" ? node.property.name as string : undefined;
|
|
1076
|
+
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) });
|
|
1077
|
+
}, false);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const unique = errors.filter((error, index) => errors.findIndex((candidate) => candidate.message === error.message && candidate.line === error.line && candidate.column === error.column) === index);
|
|
1083
|
+
return { ok: unique.length === 0, errors: unique };
|
|
1084
|
+
}
|
|
867
1085
|
function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label" | "agent"> {
|
|
868
1086
|
return {
|
|
869
1087
|
...(typeof params.phase === "string" && params.phase.trim() ? { phase: params.phase.trim() } : {}),
|
|
@@ -893,6 +1111,41 @@ function resolveWorkflowParserEntry(): string {
|
|
|
893
1111
|
}
|
|
894
1112
|
}
|
|
895
1113
|
|
|
1114
|
+
const AUTO_RESUME_PARAM_KEYS = ["acceptance", "agentContract", "index", "intercomBridge", "label", "maxRuntimeMs", "output", "outputMode", "outputSchema", "phase", "skill", "skills", "task", "timeoutMs", "toolBudget", "turnBudget", "worktree"] as const;
|
|
1115
|
+
|
|
1116
|
+
function isZeroUsage(usage: unknown): boolean {
|
|
1117
|
+
if (!isRecord(usage)) return false;
|
|
1118
|
+
const cost = usage.cost;
|
|
1119
|
+
return (usage.input ?? 0) === 0
|
|
1120
|
+
&& (usage.output ?? 0) === 0
|
|
1121
|
+
&& (usage.cacheRead ?? 0) === 0
|
|
1122
|
+
&& (usage.cacheWrite ?? 0) === 0
|
|
1123
|
+
&& (!isRecord(cost) || (cost.total ?? 0) === 0);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function setupAbortResumeParams(params: Record<string, unknown>, result: WorkflowScriptChildResult, signal: AbortSignal): Record<string, unknown> | undefined {
|
|
1127
|
+
if (signal.aborted || result.ok || result.stopped || result.interrupted || !result.runId) return undefined;
|
|
1128
|
+
const childResult = Array.isArray(result.results) && result.results.length === 1 && isRecord(result.results[0]) ? result.results[0] : undefined;
|
|
1129
|
+
const error = typeof childResult?.error === "string" ? childResult.error : result.error;
|
|
1130
|
+
if (error !== "This operation was aborted" || !isZeroUsage(childResult?.usage)) return undefined;
|
|
1131
|
+
const messages = Array.isArray(childResult?.messages) ? childResult.messages : [];
|
|
1132
|
+
const message = messages.findLast((entry) => isRecord(entry) && entry.role === "assistant");
|
|
1133
|
+
if (message !== undefined) {
|
|
1134
|
+
if (!isRecord(message)) return undefined;
|
|
1135
|
+
if (message.stopReason !== "error" || message.errorMessage !== error) return undefined;
|
|
1136
|
+
if (!Array.isArray(message.content) || message.content.length > 0 || !isZeroUsage(message.usage)) return undefined;
|
|
1137
|
+
if (Object.hasOwn(message, "diagnostics") || Object.hasOwn(message, "responseId")) return undefined;
|
|
1138
|
+
}
|
|
1139
|
+
const task = typeof params.task === "string" && params.task.trim() ? params.task.trim() : "Continue after the setup abort.";
|
|
1140
|
+
const resumeParams: Record<string, unknown> = { resume: result.runId, task };
|
|
1141
|
+
for (const key of AUTO_RESUME_PARAM_KEYS) {
|
|
1142
|
+
if (Object.hasOwn(params, key)) resumeParams[key] = params[key];
|
|
1143
|
+
}
|
|
1144
|
+
resumeParams.resume = result.runId;
|
|
1145
|
+
resumeParams.task = task;
|
|
1146
|
+
return resumeParams;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
896
1149
|
export async function runWorkflowScript(options: RunWorkflowScriptOptions): Promise<WorkflowScriptResult> {
|
|
897
1150
|
if (!options.script.trim()) throw new Error("workflowScript must not be empty.");
|
|
898
1151
|
if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1)) throw new Error("workflow script timeout must be a positive integer.");
|
|
@@ -978,7 +1231,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
978
1231
|
return unobservedSteers.length > 0 ? new Error(`workflowScript completed with unawaited runs.steer call(s): ${unobservedSteers.map((key) => `'${key}'`).join(", ")}. Await or return each call.`) : undefined;
|
|
979
1232
|
})()
|
|
980
1233
|
: undefined;
|
|
981
|
-
if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child"
|
|
1234
|
+
if ("error" in outcome) reject(new WorkflowScriptError(outcome.error.message, partial(), outcome.error.workflowErrorKind === "detached-child" || outcome.error.workflowErrorKind === "timeout" ? outcome.error.workflowErrorKind : undefined));
|
|
982
1235
|
else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
|
|
983
1236
|
else resolve({ value: outcome.value, ...partial() });
|
|
984
1237
|
});
|
|
@@ -1009,7 +1262,11 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1009
1262
|
};
|
|
1010
1263
|
const timer = options.timeoutMs === undefined
|
|
1011
1264
|
? undefined
|
|
1012
|
-
: setTimeout(() =>
|
|
1265
|
+
: setTimeout(() => {
|
|
1266
|
+
const error = new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) as Error & { workflowErrorKind: "timeout" };
|
|
1267
|
+
error.workflowErrorKind = "timeout";
|
|
1268
|
+
finish({ error });
|
|
1269
|
+
}, options.timeoutMs);
|
|
1013
1270
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1014
1271
|
if (options.signal?.aborted) return onAbort();
|
|
1015
1272
|
|
|
@@ -1049,7 +1306,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1049
1306
|
return finish({ value: message.value });
|
|
1050
1307
|
}
|
|
1051
1308
|
if (message.type === "error") {
|
|
1052
|
-
const
|
|
1309
|
+
const rawError = typeof message.error === "string" ? message.error : "Workflow script failed.";
|
|
1310
|
+
const text = message.errorPhase === "return-serialization" ? `${rawError}${workflowReturnRecoveryHint(partial().children)}` : rawError;
|
|
1311
|
+
const workflowError = new Error(text) as Error & { workflowErrorKind?: "detached-child" };
|
|
1053
1312
|
if (message.errorKind === "detached-child") workflowError.workflowErrorKind = "detached-child";
|
|
1054
1313
|
return finish({ error: workflowError });
|
|
1055
1314
|
}
|
|
@@ -1165,6 +1424,31 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1165
1424
|
}
|
|
1166
1425
|
const params = message.args.params;
|
|
1167
1426
|
if (!isRecord(params)) return respond(Promise.reject(new Error(`runs.run('${key}', params) requires a params object.`)));
|
|
1427
|
+
const collectFailure = message.args.collectFailure === true;
|
|
1428
|
+
const callObserved = observedRunCalls.delete(message.callId);
|
|
1429
|
+
const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
|
|
1430
|
+
? promise
|
|
1431
|
+
: promise.then((result) => {
|
|
1432
|
+
if (!result.ok && !result.stopped) {
|
|
1433
|
+
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" };
|
|
1434
|
+
if (result.detached) childError.workflowErrorKind = "detached-child";
|
|
1435
|
+
throw childError;
|
|
1436
|
+
}
|
|
1437
|
+
return result;
|
|
1438
|
+
});
|
|
1439
|
+
const fingerprint = stableJson(params);
|
|
1440
|
+
const existing = launches.get(key);
|
|
1441
|
+
if (existing) {
|
|
1442
|
+
if (existing.fingerprint !== fingerprint) return respond(Promise.reject(new Error(`Duplicate workflow key '${key}' used with incompatible launch params.`)));
|
|
1443
|
+
if (callObserved) existing.observed = true;
|
|
1444
|
+
trace.push({ operation: "run", key, state: "reused", ...workflowStringMetadata(params) });
|
|
1445
|
+
traceChanged();
|
|
1446
|
+
return respond(deliver(existing.promise), `runs.run('${key}') result`);
|
|
1447
|
+
}
|
|
1448
|
+
const permitError = options.oneUsePermit?.claim(key);
|
|
1449
|
+
if (permitError) return respond(Promise.reject(new Error(permitError)));
|
|
1450
|
+
if (options.oneUsePermit && message.args.batch !== undefined) return respond(Promise.reject(new Error("Workflow child permit does not support runs.all.")));
|
|
1451
|
+
if (options.oneUsePermit && params.resume !== undefined) return respond(Promise.reject(new Error("Workflow child permit does not support retained resume.")));
|
|
1168
1452
|
if (params.action !== undefined) return respond(Promise.reject(new Error(`runs.run('${key}') accepts execution params only; management action is not allowed.`)));
|
|
1169
1453
|
if (params.workflowScript !== undefined) return respond(Promise.reject(new Error(`runs.run('${key}') cannot start a nested workflow script.`)));
|
|
1170
1454
|
if (params.tasks !== undefined || params.chain !== undefined || params.parallel !== undefined || params.concurrency !== undefined || params.chainDir !== undefined) {
|
|
@@ -1195,28 +1479,6 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1195
1479
|
if (params.resume !== undefined && (typeof params.task !== "string" || !params.task.trim())) {
|
|
1196
1480
|
return respond(Promise.reject(new Error(`runs.run('${key}') resume requires a non-empty task follow-up.`)));
|
|
1197
1481
|
}
|
|
1198
|
-
const collectFailure = message.args.collectFailure === true;
|
|
1199
|
-
const callObserved = observedRunCalls.delete(message.callId);
|
|
1200
|
-
const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
|
|
1201
|
-
? promise
|
|
1202
|
-
: promise.then((result) => {
|
|
1203
|
-
if (!result.ok && !result.stopped) {
|
|
1204
|
-
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" };
|
|
1205
|
-
if (result.detached) childError.workflowErrorKind = "detached-child";
|
|
1206
|
-
throw childError;
|
|
1207
|
-
}
|
|
1208
|
-
return result;
|
|
1209
|
-
});
|
|
1210
|
-
const fingerprint = stableJson(params);
|
|
1211
|
-
const existing = launches.get(key);
|
|
1212
|
-
if (existing) {
|
|
1213
|
-
if (existing.fingerprint !== fingerprint) return respond(Promise.reject(new Error(`Duplicate workflow key '${key}' used with incompatible launch params.`)));
|
|
1214
|
-
if (callObserved) existing.observed = true;
|
|
1215
|
-
trace.push({ operation: "run", key, state: "reused", ...workflowStringMetadata(params) });
|
|
1216
|
-
traceChanged();
|
|
1217
|
-
return respond(deliver(existing.promise), `runs.run('${key}') result`);
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
1482
|
const startedAt = Date.now();
|
|
1221
1483
|
const batch = isRecord(message.args.batch) && typeof message.args.batch.id === "string" && Array.isArray(message.args.batch.calls)
|
|
1222
1484
|
? { 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)) }
|
|
@@ -1266,7 +1528,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1266
1528
|
if (resolvedResumeLineage.at(-1) !== resolvedResumeId) resolvedResumeLineage.push(resolvedResumeId!);
|
|
1267
1529
|
}
|
|
1268
1530
|
const launchParams = resolvedResumeId ? { ...params, resume: resolvedResumeId } : params;
|
|
1269
|
-
|
|
1531
|
+
const result = await options.launch(key, launchParams, childSignal, { admitted: true, batch: batch !== undefined });
|
|
1532
|
+
const autoResumeParams = setupAbortResumeParams(params, result, childSignal);
|
|
1533
|
+
if (!autoResumeParams) return result;
|
|
1534
|
+
resolvedResumeLineage = [...new Set([...(resolvedResumeLineage ?? []), result.runId!])];
|
|
1535
|
+
trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(autoResumeParams), phase: "auto-resume", runId: result.runId });
|
|
1536
|
+
traceChanged();
|
|
1537
|
+
return options.launch(key, autoResumeParams, childSignal, { admitted: true, batch: batch !== undefined });
|
|
1270
1538
|
}).then((result) => {
|
|
1271
1539
|
let normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
|
|
1272
1540
|
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
|
+
}
|