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.
Files changed (108) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/agents/claude-code-writer.md +15 -0
  3. package/agents/claude-code.md +15 -0
  4. package/agents/codex-exec-writer.md +15 -0
  5. package/agents/codex-exec.md +15 -0
  6. package/agents/cursor-agent-writer.md +14 -0
  7. package/agents/cursor-agent.md +14 -0
  8. package/docs/agents.md +124 -21
  9. package/docs/configuration.md +37 -0
  10. package/docs/extension-api.md +41 -2
  11. package/docs/models.md +3 -3
  12. package/docs/observability.md +8 -7
  13. package/docs/tool-reference.md +14 -3
  14. package/docs/workflows.md +44 -0
  15. package/package.json +1 -1
  16. package/skills/pi-subagents/SKILL.md +2 -0
  17. package/skills/pi-subagents/references/execution-controls.md +2 -2
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
  19. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  20. package/src/agents/agent-management.ts +36 -5
  21. package/src/agents/agent-refinements.ts +4 -4
  22. package/src/agents/agent-serializer.ts +5 -0
  23. package/src/agents/agents.ts +257 -51
  24. package/src/agents/builtin-names.ts +6 -0
  25. package/src/agents/runtime-agent-events.ts +70 -0
  26. package/src/agents/runtime-agent-registry.ts +18 -4
  27. package/src/api/agents.ts +10 -5
  28. package/src/api/preflight.ts +28 -3
  29. package/src/extension/config.ts +70 -0
  30. package/src/extension/doctor.ts +3 -3
  31. package/src/extension/index.ts +33 -8
  32. package/src/extension/public-execution.ts +29 -13
  33. package/src/extension/rpc.ts +55 -19
  34. package/src/extension/schemas.ts +6 -5
  35. package/src/extension/tool-description.ts +18 -12
  36. package/src/inspectors/herdr/actions.ts +2 -1
  37. package/src/inspectors/herdr/inspector-runner.ts +2 -10
  38. package/src/inspectors/herdr/session-roots-codec.ts +42 -0
  39. package/src/integrations/herdr-status.ts +51 -3
  40. package/src/runs/background/active-async-capacity.ts +77 -10
  41. package/src/runs/background/async-execution.ts +127 -19
  42. package/src/runs/background/async-job-tracker.ts +5 -0
  43. package/src/runs/background/async-resume.ts +6 -2
  44. package/src/runs/background/async-retention.ts +20 -3
  45. package/src/runs/background/async-status.ts +7 -0
  46. package/src/runs/background/chain-append.ts +2 -0
  47. package/src/runs/background/chain-root-attachment.ts +15 -1
  48. package/src/runs/background/fleet-view.ts +16 -10
  49. package/src/runs/background/inspect-rpc.ts +8 -8
  50. package/src/runs/background/notify.ts +26 -3
  51. package/src/runs/background/result-delivery-ownership.ts +45 -0
  52. package/src/runs/background/result-files.ts +27 -14
  53. package/src/runs/background/result-watcher.ts +36 -15
  54. package/src/runs/background/run-status.ts +33 -6
  55. package/src/runs/background/scheduled-runs.ts +7 -1
  56. package/src/runs/background/subagent-runner.ts +239 -56
  57. package/src/runs/background/wait-completions.ts +4 -0
  58. package/src/runs/foreground/execution.ts +92 -11
  59. package/src/runs/foreground/foreground-control.ts +6 -0
  60. package/src/runs/foreground/foreground-history.ts +22 -1
  61. package/src/runs/foreground/subagent-executor.ts +322 -102
  62. package/src/runs/foreground/workflow-detach-reconcile.ts +144 -18
  63. package/src/runs/shared/child-protocol.ts +21 -7
  64. package/src/runs/shared/claude-code-adapter.ts +129 -0
  65. package/src/runs/shared/codex-exec-adapter.ts +129 -0
  66. package/src/runs/shared/completion-guard.ts +4 -3
  67. package/src/runs/shared/cursor-agent-adapter.ts +114 -0
  68. package/src/runs/shared/dynamic-fanout.ts +3 -3
  69. package/src/runs/shared/external-cli-contract.ts +167 -0
  70. package/src/runs/shared/external-cli-preflight.ts +122 -0
  71. package/src/runs/shared/external-cli-runner.ts +348 -55
  72. package/src/runs/shared/fast-mode-extension.ts +5 -5
  73. package/src/runs/shared/launch-cwd.ts +16 -0
  74. package/src/runs/shared/long-running-guard.ts +2 -1
  75. package/src/runs/shared/mcp-config-sources.ts +386 -0
  76. package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
  77. package/src/runs/shared/model-exclusions.ts +69 -7
  78. package/src/runs/shared/model-fallback.ts +39 -5
  79. package/src/runs/shared/mutation-evidence.ts +7 -2
  80. package/src/runs/shared/nested-events.ts +3 -1
  81. package/src/runs/shared/nested-render.ts +2 -2
  82. package/src/runs/shared/parallel-utils.ts +8 -1
  83. package/src/runs/shared/pi-args.ts +61 -6
  84. package/src/runs/shared/process-signal.ts +13 -0
  85. package/src/runs/shared/run-history.ts +21 -1
  86. package/src/runs/shared/single-output.ts +17 -0
  87. package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
  88. package/src/shared/fork-context.ts +21 -0
  89. package/src/shared/formatters.ts +13 -1
  90. package/src/shared/launch-contract.ts +4 -0
  91. package/src/shared/pruned-fork.ts +450 -0
  92. package/src/shared/session-file-trust.ts +19 -0
  93. package/src/shared/session-tokens.ts +14 -3
  94. package/src/shared/settings.ts +10 -2
  95. package/src/shared/shortcuts.ts +17 -0
  96. package/src/shared/types.ts +160 -10
  97. package/src/shared/utils.ts +6 -29
  98. package/src/shared/workflow-child-permit.ts +116 -0
  99. package/src/slash/delegation-adapters.ts +0 -1
  100. package/src/slash/slash-commands.ts +8 -6
  101. package/src/slash/subagents-admin.ts +3 -0
  102. package/src/tui/fleet-status.ts +27 -10
  103. package/src/tui/fleet-transcript.ts +11 -5
  104. package/src/tui/fleet.ts +28 -13
  105. package/src/tui/render.ts +55 -21
  106. package/src/workflows/scripted-workflow.ts +299 -31
  107. package/src/workflows/workflow-child-summary.ts +117 -0
  108. 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
- assertJsonValue(persistedValue, "return");
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" ? "detached-child" : undefined));
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(() => finish({ error: new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) }), options.timeoutMs);
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 workflowError = new Error(typeof message.error === "string" ? message.error : "Workflow script failed.") as Error & { workflowErrorKind?: "detached-child" };
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
- return options.launch(key, launchParams, childSignal, { admitted: true });
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
+ }