pi-extensible-workflows 1.0.0 → 2.0.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/README.md +7 -2
- package/dist/src/agent-execution.d.ts +13 -0
- package/dist/src/agent-execution.js +177 -40
- package/dist/src/ambient-workflow-evals.js +14 -33
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +530 -6
- package/dist/src/doctor.d.ts +4 -4
- package/dist/src/doctor.js +9 -31
- package/dist/src/herdr.d.ts +12 -0
- package/dist/src/herdr.js +74 -0
- package/dist/src/index.d.ts +107 -27
- package/dist/src/index.js +838 -322
- package/dist/src/persistence.d.ts +28 -0
- package/dist/src/persistence.js +213 -16
- package/dist/src/session-inspector.d.ts +1 -0
- package/dist/src/session-inspector.js +3 -0
- package/dist/src/workflow-evals.d.ts +1 -0
- package/dist/src/workflow-evals.js +25 -26
- package/package.json +8 -4
- package/skills/pi-extensible-workflows/SKILL.md +48 -60
- package/src/agent-execution.ts +140 -27
- package/src/ambient-workflow-evals.ts +18 -28
- package/src/cli.ts +418 -6
- package/src/doctor.ts +13 -35
- package/src/herdr.ts +73 -0
- package/src/index.ts +734 -266
- package/src/persistence.ts +192 -16
- package/src/session-inspector.ts +4 -0
- package/src/workflow-evals.ts +15 -17
package/dist/src/index.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import { fork } from "node:child_process";
|
|
2
|
+
import { fork, spawn } from "node:child_process";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync,
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
6
6
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { StringDecoder } from "node:string_decoder";
|
|
8
9
|
import * as acorn from "acorn";
|
|
9
10
|
import { Script } from "node:vm";
|
|
10
11
|
import { Type } from "@earendil-works/pi-ai";
|
|
11
12
|
import { Value } from "typebox/value";
|
|
12
|
-
import { copyToClipboard, getAgentDir, parseFrontmatter, highlightCode,
|
|
13
|
+
import { copyToClipboard, getAgentDir, parseFrontmatter, highlightCode, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
|
|
13
14
|
import { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
|
|
14
|
-
import {
|
|
15
|
-
import { acquireSessionLease, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
|
|
15
|
+
import { herdrPaneId, openHerdrPane } from "./herdr.js";
|
|
16
|
+
import { acquireSessionLease, atomicWriteFile, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
|
|
16
17
|
export const RUN_STATES = ["queued", "running", "pausing", "paused", "awaiting_input", "completed", "failed", "stopped", "interrupted", "budget_exhausted"];
|
|
17
18
|
export const AGENT_STATES = ["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"];
|
|
18
|
-
export const
|
|
19
|
-
export const WORKFLOW_ASYNC_COMPLETE_EVENT = "workflow:async-complete";
|
|
19
|
+
export const WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree", "shell"];
|
|
20
20
|
export const WORKFLOW_RUN_STARTED_EVENT = "workflow:run-started";
|
|
21
21
|
export const WORKFLOW_RUN_RESUMED_EVENT = "workflow:run-resumed";
|
|
22
22
|
export const WORKFLOW_RUN_STATE_CHANGED_EVENT = "workflow:run-state-changed";
|
|
@@ -30,10 +30,10 @@ export const WORKFLOW_WORKTREE_CREATED_EVENT = "workflow:worktree-created";
|
|
|
30
30
|
const SETTLED_AGENT_STATES = new Set(["completed", "failed", "cancelled"]);
|
|
31
31
|
export const ERROR_CODES = [
|
|
32
32
|
"CONFIG_ERROR", "INVALID_SETTINGS", "INVALID_SYNTAX", "INVALID_METADATA", "DUPLICATE_NAME", "INVALID_SCHEMA", "UNKNOWN_MODEL", "UNKNOWN_TOOL", "UNKNOWN_AGENT_TYPE",
|
|
33
|
-
"RUN_OWNED", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID",
|
|
33
|
+
"RUN_OWNED", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "SHELL_FAILED", "AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID",
|
|
34
34
|
"CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
|
|
35
35
|
];
|
|
36
|
-
export const LAUNCH_SNAPSHOT_IDENTITY_VERSION =
|
|
36
|
+
export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 4;
|
|
37
37
|
export class WorkflowError extends Error {
|
|
38
38
|
code;
|
|
39
39
|
constructor(code, message) {
|
|
@@ -42,6 +42,7 @@ export class WorkflowError extends Error {
|
|
|
42
42
|
this.name = "WorkflowError";
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
const WORKFLOW_FAILURE_DIAGNOSTICS = Symbol("workflowFailureDiagnostics");
|
|
45
46
|
const WORKFLOW_AUTHORED_ERROR = Symbol("workflowAuthoredError");
|
|
46
47
|
function errorText(error) { return error && typeof error === "object" && typeof error.message === "string" ? error.message : error instanceof Error ? error.message : String(error); }
|
|
47
48
|
function errorCode(error) {
|
|
@@ -71,12 +72,13 @@ const WORKFLOW_ERROR_PROSE = {
|
|
|
71
72
|
INVALID_SCHEMA: (detail) => `The workflow schema is invalid: ${detail}.`,
|
|
72
73
|
REGISTRY_FROZEN: (detail) => `Workflow extension registration is closed: ${detail}.`,
|
|
73
74
|
GLOBAL_COLLISION: (detail) => `The workflow global name is already in use: ${detail}.`,
|
|
74
|
-
MISSING_WORKFLOW: (detail) => `The workflow
|
|
75
|
+
MISSING_WORKFLOW: (detail) => `The registered workflow function is unavailable: ${detail}.`,
|
|
75
76
|
UNKNOWN_MODEL: (detail) => `The workflow requested the unavailable model ${detail.replace(/^(?:Unknown model(?: for role [^:]+)?|Invalid model spec):\s*/, "")}.`,
|
|
76
77
|
UNKNOWN_TOOL: (detail) => `The workflow requested the unavailable tool ${detail.replace(/^Unknown tool:\s*/, "")}.`,
|
|
77
78
|
UNKNOWN_AGENT_TYPE: (detail) => `The workflow requested the unavailable agent role ${detail.replace(/^Unknown agent role:\s*/, "")}.`,
|
|
78
79
|
RUN_OWNED: (detail) => /already owned|active ownership/.test(detail) ? "The workflow session is already in use." : `The workflow session is already in use: ${detail}.`,
|
|
79
80
|
RPC_LIMIT_EXCEEDED: (detail) => `The workflow communication data exceeded its size limit: ${detail}.`,
|
|
81
|
+
SHELL_FAILED: (detail) => `The workflow shell command failed: ${detail}.`,
|
|
80
82
|
AGENT_TIMEOUT: (detail) => `The workflow agent timed out: ${detail}.`,
|
|
81
83
|
AGENT_FAILED: (detail) => `The workflow agent failed: ${detail}.`,
|
|
82
84
|
RESULT_INVALID: (detail) => `The workflow produced an invalid result: ${detail}.`,
|
|
@@ -188,6 +190,7 @@ export class RunLifecycle {
|
|
|
188
190
|
export const DEFAULT_SETTINGS = Object.freeze({ concurrency: 8 });
|
|
189
191
|
function fail(code, message) { throw new WorkflowError(code, message); }
|
|
190
192
|
function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
193
|
+
export { object as isObject };
|
|
191
194
|
function nonNegativeInteger(value) { return Number.isInteger(value) && value >= 0; }
|
|
192
195
|
function nonNegativeFinite(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
|
|
193
196
|
export function validateBudget(value) {
|
|
@@ -562,9 +565,7 @@ export function saveModelAliases(path = workflowSettingsPath(), aliases = {}) {
|
|
|
562
565
|
throw error;
|
|
563
566
|
}
|
|
564
567
|
mkdirSync(dirname(path), { recursive: true });
|
|
565
|
-
|
|
566
|
-
writeFileSync(temporary, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, { mode: 0o600 });
|
|
567
|
-
renameSync(temporary, path);
|
|
568
|
+
atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
|
|
568
569
|
}
|
|
569
570
|
export function parseRoleMarkdown(content, strict = false, rolePath) {
|
|
570
571
|
if (!strict) {
|
|
@@ -700,20 +701,32 @@ function parseWorkflow(script) {
|
|
|
700
701
|
function astNode(value) {
|
|
701
702
|
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
702
703
|
}
|
|
704
|
+
function astChildren(node) {
|
|
705
|
+
const children = [];
|
|
706
|
+
for (const value of Object.values(node)) {
|
|
707
|
+
if (Array.isArray(value)) {
|
|
708
|
+
for (const child of value)
|
|
709
|
+
if (astNode(child))
|
|
710
|
+
children.push(child);
|
|
711
|
+
}
|
|
712
|
+
else if (astNode(value))
|
|
713
|
+
children.push(value);
|
|
714
|
+
}
|
|
715
|
+
return children;
|
|
716
|
+
}
|
|
717
|
+
function workflowCallKind(node) {
|
|
718
|
+
if (node.type !== "CallExpression" || node.callee.type !== "Identifier")
|
|
719
|
+
return undefined;
|
|
720
|
+
const kind = node.callee.name;
|
|
721
|
+
return WORKFLOW_CALL_KINDS.includes(kind) ? kind : undefined;
|
|
722
|
+
}
|
|
703
723
|
function workflowCalls(program) {
|
|
704
724
|
const calls = [];
|
|
705
725
|
const visit = (node) => {
|
|
706
|
-
if (
|
|
726
|
+
if (workflowCallKind(node))
|
|
707
727
|
calls.push(node);
|
|
708
|
-
for (const
|
|
709
|
-
|
|
710
|
-
for (const child of value)
|
|
711
|
-
if (astNode(child))
|
|
712
|
-
visit(child);
|
|
713
|
-
}
|
|
714
|
-
else if (astNode(value))
|
|
715
|
-
visit(value);
|
|
716
|
-
}
|
|
728
|
+
for (const child of astChildren(node))
|
|
729
|
+
visit(child);
|
|
717
730
|
};
|
|
718
731
|
visit(program);
|
|
719
732
|
return calls.sort((left, right) => left.start - right.start);
|
|
@@ -728,9 +741,9 @@ function workflowCallsWithStructure(program) {
|
|
|
728
741
|
if (scope?.key === null && key)
|
|
729
742
|
current = { ...current, structure: [...current.structure.slice(0, -1), { ...scope, key }] };
|
|
730
743
|
}
|
|
731
|
-
|
|
744
|
+
const operation = workflowCallKind(node);
|
|
745
|
+
if (operation) {
|
|
732
746
|
const call = node;
|
|
733
|
-
const operation = call.callee.name;
|
|
734
747
|
const execution = operation === "parallel" ? "parallel" : operation === "pipeline" ? "sequential" : current.execution;
|
|
735
748
|
calls.push({ call, execution, structure: current.structure });
|
|
736
749
|
for (const [index, argument] of call.arguments.entries()) {
|
|
@@ -741,15 +754,8 @@ function workflowCallsWithStructure(program) {
|
|
|
741
754
|
}
|
|
742
755
|
return;
|
|
743
756
|
}
|
|
744
|
-
for (const
|
|
745
|
-
|
|
746
|
-
for (const child of value)
|
|
747
|
-
if (astNode(child))
|
|
748
|
-
visit(child, current);
|
|
749
|
-
}
|
|
750
|
-
else if (astNode(value))
|
|
751
|
-
visit(value, current);
|
|
752
|
-
}
|
|
757
|
+
for (const child of astChildren(node))
|
|
758
|
+
visit(child, current);
|
|
753
759
|
};
|
|
754
760
|
visit(program, { execution: "sequential", structure: [] });
|
|
755
761
|
return calls.sort((left, right) => left.call.start - right.call.start);
|
|
@@ -762,26 +768,20 @@ function validateDirectPrimitiveReferences(program, name) {
|
|
|
762
768
|
if (!directCall && !propertyKey)
|
|
763
769
|
fail("INVALID_METADATA", `${name} calls must use a direct ${name}(...) call; aliases and indirect calls are unsupported`);
|
|
764
770
|
}
|
|
765
|
-
for (const
|
|
766
|
-
|
|
767
|
-
for (const child of value)
|
|
768
|
-
if (astNode(child))
|
|
769
|
-
visit(child, node);
|
|
770
|
-
}
|
|
771
|
-
else if (astNode(value))
|
|
772
|
-
visit(value, node);
|
|
773
|
-
}
|
|
771
|
+
for (const child of astChildren(node))
|
|
772
|
+
visit(child, node);
|
|
774
773
|
};
|
|
775
774
|
visit(program);
|
|
776
775
|
}
|
|
777
776
|
function hasIdentifier(node, name) {
|
|
778
777
|
if (node.type === "Identifier" && node.name === name)
|
|
779
778
|
return true;
|
|
780
|
-
return
|
|
779
|
+
return astChildren(node).some((child) => hasIdentifier(child, name));
|
|
781
780
|
}
|
|
782
781
|
const INTERNAL_AGENT_NAME = "__pi_extensible_workflows_agent";
|
|
783
782
|
const INTERNAL_CONVERSATION_NAME = "__pi_extensible_workflows_conversation";
|
|
784
783
|
const INTERNAL_WORKTREE_NAME = "__pi_extensible_workflows_withWorktree";
|
|
784
|
+
const INTERNAL_SHELL_NAME = "__pi_extensible_workflows_shell";
|
|
785
785
|
function callHasTrailingComma(source, call) {
|
|
786
786
|
let previous;
|
|
787
787
|
let current;
|
|
@@ -802,12 +802,14 @@ function instrumentWorkflow(script) {
|
|
|
802
802
|
fail("INVALID_METADATA", `${INTERNAL_CONVERSATION_NAME} is reserved for workflow conversation instrumentation`);
|
|
803
803
|
if (hasIdentifier(program, INTERNAL_WORKTREE_NAME))
|
|
804
804
|
fail("INVALID_METADATA", `${INTERNAL_WORKTREE_NAME} is reserved for workflow withWorktree instrumentation`);
|
|
805
|
-
|
|
805
|
+
if (hasIdentifier(program, INTERNAL_SHELL_NAME))
|
|
806
|
+
fail("INVALID_METADATA", `${INTERNAL_SHELL_NAME} is reserved for workflow shell instrumentation`);
|
|
807
|
+
const calls = workflowCalls(program).filter((call) => ["agent", "conversation", "withWorktree", "shell"].includes(call.callee.name));
|
|
806
808
|
const edits = calls.flatMap((call) => {
|
|
807
809
|
const callSite = `${String(call.start)}:${String(call.end)}`;
|
|
808
810
|
const hiddenArgument = call.arguments.length === 0 || callHasTrailingComma(body, call) ? "" : ", ";
|
|
809
811
|
return [
|
|
810
|
-
{ start: call.callee.start, end: call.callee.end, text: call.callee.name === "agent" ? INTERNAL_AGENT_NAME : call.callee.name === "conversation" ? INTERNAL_CONVERSATION_NAME : INTERNAL_WORKTREE_NAME },
|
|
812
|
+
{ start: call.callee.start, end: call.callee.end, text: call.callee.name === "agent" ? INTERNAL_AGENT_NAME : call.callee.name === "conversation" ? INTERNAL_CONVERSATION_NAME : call.callee.name === "withWorktree" ? INTERNAL_WORKTREE_NAME : INTERNAL_SHELL_NAME },
|
|
811
813
|
{ start: call.end - 1, end: call.end - 1, text: `${hiddenArgument}${JSON.stringify(callSite)}` },
|
|
812
814
|
];
|
|
813
815
|
}).sort((left, right) => right.start - left.start);
|
|
@@ -946,6 +948,23 @@ function validateAgentOptions(value) {
|
|
|
946
948
|
fail("INVALID_METADATA", "Role agents must not specify model, thinking, or tools");
|
|
947
949
|
return value;
|
|
948
950
|
}
|
|
951
|
+
const SHELL_OPTION_KEYS = new Set(["timeoutMs", "env"]);
|
|
952
|
+
function validateShellOptions(value) {
|
|
953
|
+
if (value === undefined)
|
|
954
|
+
return {};
|
|
955
|
+
if (!object(value) || !jsonValue(value) || Object.keys(value).some((key) => !SHELL_OPTION_KEYS.has(key)))
|
|
956
|
+
fail("INVALID_METADATA", "shell options must contain only timeoutMs and env");
|
|
957
|
+
if (value.timeoutMs !== undefined && !positiveInteger(value.timeoutMs))
|
|
958
|
+
fail("INVALID_METADATA", "shell timeoutMs must be a positive integer");
|
|
959
|
+
if (value.env !== undefined && (!object(value.env) || Object.values(value.env).some((entry) => typeof entry !== "string")))
|
|
960
|
+
fail("INVALID_METADATA", "shell env must be an object of strings");
|
|
961
|
+
return { ...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }), ...(value.env === undefined ? {} : { env: value.env }) };
|
|
962
|
+
}
|
|
963
|
+
function validateShellCommand(value) {
|
|
964
|
+
if (typeof value !== "string")
|
|
965
|
+
fail("INVALID_METADATA", "shell command must be a string");
|
|
966
|
+
return value;
|
|
967
|
+
}
|
|
949
968
|
function staticValue(node) {
|
|
950
969
|
if (!node)
|
|
951
970
|
return { known: false };
|
|
@@ -1011,6 +1030,8 @@ export function inspectWorkflowScript(script) {
|
|
|
1011
1030
|
}
|
|
1012
1031
|
if (kind === "checkpoint")
|
|
1013
1032
|
return { ...placement, kind, start: call.start, end: call.end, name: staticString(propertyNode(first, "name")), prompt: staticString(propertyNode(first, "prompt")), model: null, role: null };
|
|
1033
|
+
if (kind === "shell")
|
|
1034
|
+
return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
|
|
1014
1035
|
return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
|
|
1015
1036
|
});
|
|
1016
1037
|
}
|
|
@@ -1026,6 +1047,18 @@ function validateStaticAgentOptions(node, aliases = {}, knownModels, settingsPat
|
|
|
1026
1047
|
validateAgentOption(key, value.value, aliases, knownModels, settingsPath);
|
|
1027
1048
|
}
|
|
1028
1049
|
}
|
|
1050
|
+
function validateStaticShellOptions(call) {
|
|
1051
|
+
if (call.arguments.some((argument) => argument.type === "SpreadElement"))
|
|
1052
|
+
return;
|
|
1053
|
+
if (call.arguments.length !== 1 && call.arguments.length !== 2)
|
|
1054
|
+
fail("INVALID_METADATA", "shell requires a command string and optional options");
|
|
1055
|
+
const command = staticValue(callArgument(call, 0));
|
|
1056
|
+
if (command.known)
|
|
1057
|
+
validateShellCommand(command.value);
|
|
1058
|
+
const options = staticValue(callArgument(call, 1));
|
|
1059
|
+
if (options.known)
|
|
1060
|
+
validateShellOptions(options.value);
|
|
1061
|
+
}
|
|
1029
1062
|
function validateStaticWithWorktree(call) {
|
|
1030
1063
|
if (call.arguments.some((argument) => argument.type === "SpreadElement"))
|
|
1031
1064
|
return;
|
|
@@ -1040,13 +1073,12 @@ function validateStaticWithWorktree(call) {
|
|
|
1040
1073
|
fail("INVALID_METADATA", "withWorktree name must be a non-empty string");
|
|
1041
1074
|
}
|
|
1042
1075
|
}
|
|
1043
|
-
const RESERVED_GLOBALS = new Set(["agent", "conversation", "prompt", "checkpoint", "parallel", "pipeline", "phase", "withWorktree", "log", "args", "Promise", "JSON", "Math", "Date", "eval", "Function", "WebAssembly", "process", "require", "module", "exports", "console", "fetch", "XMLHttpRequest", "WebSocket", "performance", "crypto", "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "Intl", "SharedArrayBuffer", "Atomics", "globalThis", "global", "undefined", "NaN", "Infinity", "extensions", "workflow_catalog"]);
|
|
1076
|
+
const RESERVED_GLOBALS = new Set(["agent", "conversation", "shell", "prompt", "checkpoint", "parallel", "pipeline", "phase", "withWorktree", "log", "args", "Promise", "JSON", "Math", "Date", "eval", "Function", "WebAssembly", "process", "require", "module", "exports", "console", "fetch", "XMLHttpRequest", "WebSocket", "performance", "crypto", "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "Intl", "SharedArrayBuffer", "Atomics", "globalThis", "global", "undefined", "NaN", "Infinity", "extensions", "workflow_catalog"]);
|
|
1044
1077
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
1045
1078
|
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
1046
1079
|
export class WorkflowRegistry {
|
|
1047
1080
|
#extensions = new Set();
|
|
1048
1081
|
#globals = new Map();
|
|
1049
|
-
#workflows = new Map();
|
|
1050
1082
|
#hooks = new Map();
|
|
1051
1083
|
#frozen = false;
|
|
1052
1084
|
get frozen() { return this.#frozen; }
|
|
@@ -1054,14 +1086,15 @@ export class WorkflowRegistry {
|
|
|
1054
1086
|
register(extension) {
|
|
1055
1087
|
if (this.#frozen)
|
|
1056
1088
|
fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
|
|
1057
|
-
if (
|
|
1089
|
+
if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows"))
|
|
1090
|
+
fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
|
|
1091
|
+
if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "agentSetupHooks"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim())
|
|
1058
1092
|
fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
|
|
1059
1093
|
const functions = extension.functions ?? {};
|
|
1060
1094
|
const variables = extension.variables ?? {};
|
|
1061
|
-
const workflows = extension.workflows ?? {};
|
|
1062
1095
|
const agentSetupHooks = extension.agentSetupHooks ?? {};
|
|
1063
|
-
if (!object(functions) || !object(variables) || !object(
|
|
1064
|
-
fail("INVALID_METADATA", "Workflow extensions require functions, variables,
|
|
1096
|
+
if (!object(functions) || !object(variables) || !object(agentSetupHooks) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(agentSetupHooks).length === 0))
|
|
1097
|
+
fail("INVALID_METADATA", "Workflow extensions require functions, variables, or agent setup hooks");
|
|
1065
1098
|
const names = [...Object.keys(functions), ...Object.keys(variables)];
|
|
1066
1099
|
if (new Set(names).size !== names.length)
|
|
1067
1100
|
fail("GLOBAL_COLLISION", "Global name collision inside extension");
|
|
@@ -1086,50 +1119,38 @@ export class WorkflowRegistry {
|
|
|
1086
1119
|
fail("INVALID_METADATA", `Invalid workflow variable: ${name}`);
|
|
1087
1120
|
validateSchema(variable.schema, `${name} schema`);
|
|
1088
1121
|
}
|
|
1089
|
-
for (const [name, workflow] of Object.entries(workflows)) {
|
|
1090
|
-
if (!IDENTIFIER.test(name) || !object(workflow) || Object.keys(workflow).some((key) => !["description", "script"].includes(key)) || typeof workflow.description !== "string" || !workflow.description.trim() || typeof workflow.script !== "string" || !workflow.script.trim())
|
|
1091
|
-
fail("INVALID_METADATA", `Invalid workflow script: ${name}`);
|
|
1092
|
-
if (this.#workflows.has(name))
|
|
1093
|
-
fail("DUPLICATE_NAME", `Reusable workflow already registered: ${name}`);
|
|
1094
|
-
parseWorkflow(workflow.script);
|
|
1095
|
-
}
|
|
1096
1122
|
for (const [name, hook] of Object.entries(agentSetupHooks)) {
|
|
1097
1123
|
if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "setup"].includes(key)) || typeof hook.setup !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority)))
|
|
1098
1124
|
fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
|
|
1099
1125
|
if (this.#hooks.has(name))
|
|
1100
1126
|
fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
|
|
1101
1127
|
}
|
|
1102
|
-
const stored = deepFreeze({ ...extension, functions, variables,
|
|
1128
|
+
const stored = deepFreeze({ ...extension, functions, variables, agentSetupHooks });
|
|
1103
1129
|
this.#extensions.add(stored);
|
|
1104
1130
|
for (const name of names)
|
|
1105
1131
|
this.#globals.set(name, name);
|
|
1106
|
-
for (const [name, workflow] of Object.entries(workflows))
|
|
1107
|
-
this.#workflows.set(name, workflow);
|
|
1108
1132
|
for (const [name, hook] of Object.entries(agentSetupHooks))
|
|
1109
1133
|
this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
|
|
1110
1134
|
}
|
|
1111
|
-
|
|
1135
|
+
function(name) {
|
|
1112
1136
|
if (!IDENTIFIER.test(name))
|
|
1113
|
-
fail("MISSING_WORKFLOW", `Registered
|
|
1114
|
-
const
|
|
1115
|
-
if (!
|
|
1116
|
-
fail("MISSING_WORKFLOW", `
|
|
1117
|
-
return
|
|
1137
|
+
fail("MISSING_WORKFLOW", `Registered functions require an unqualified name: ${name}`);
|
|
1138
|
+
const fn = [...this.#extensions].find((extension) => extension.functions?.[name])?.functions?.[name];
|
|
1139
|
+
if (!fn)
|
|
1140
|
+
fail("MISSING_WORKFLOW", `Registered function is unavailable: ${name}; the separate registered-workflow format was removed`);
|
|
1141
|
+
return fn;
|
|
1118
1142
|
}
|
|
1119
|
-
|
|
1120
|
-
return Object.freeze(Object.fromEntries(this.#
|
|
1143
|
+
functions() {
|
|
1144
|
+
return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.entries(extension.functions ?? {}))));
|
|
1121
1145
|
}
|
|
1122
1146
|
catalog() {
|
|
1123
1147
|
const functions = [];
|
|
1124
1148
|
const variables = [];
|
|
1125
|
-
const workflows = [];
|
|
1126
1149
|
for (const extension of this.#extensions) {
|
|
1127
1150
|
for (const [name, fn] of Object.entries(extension.functions ?? {}))
|
|
1128
1151
|
functions.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: fn.description, input: structuredClone(fn.input), output: structuredClone(fn.output) });
|
|
1129
1152
|
for (const [name, variable] of Object.entries(extension.variables ?? {}))
|
|
1130
1153
|
variables.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: variable.description, schema: structuredClone(variable.schema) });
|
|
1131
|
-
for (const [name, workflow] of Object.entries(extension.workflows ?? {}))
|
|
1132
|
-
workflows.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: workflow.description });
|
|
1133
1154
|
}
|
|
1134
1155
|
let aliases;
|
|
1135
1156
|
try {
|
|
@@ -1139,15 +1160,28 @@ export class WorkflowRegistry {
|
|
|
1139
1160
|
aliases = undefined;
|
|
1140
1161
|
}
|
|
1141
1162
|
const sort = (left, right) => left.name.localeCompare(right.name);
|
|
1142
|
-
return deepFreeze({ functions: functions.sort(sort), variables: variables.sort(sort),
|
|
1163
|
+
return deepFreeze({ functions: functions.sort(sort), variables: variables.sort(sort), ...(aliases ? { modelAliases: structuredClone(aliases) } : {}) });
|
|
1164
|
+
}
|
|
1165
|
+
catalogIndex() {
|
|
1166
|
+
const catalog = this.catalog();
|
|
1167
|
+
return deepFreeze({
|
|
1168
|
+
functions: catalog.functions.map(({ name, description, input }) => ({ name, description, input: structuredClone(input) })),
|
|
1169
|
+
variables: catalog.variables.map(({ name, description, schema }) => ({ name, description, schema: structuredClone(schema) })),
|
|
1170
|
+
...(catalog.modelAliases ? { modelAliases: structuredClone(catalog.modelAliases) } : {}),
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
catalogDetail(name) {
|
|
1174
|
+
const catalog = this.catalog();
|
|
1175
|
+
const entry = catalog.functions.find((candidate) => candidate.name === name) ?? catalog.variables.find((candidate) => candidate.name === name);
|
|
1176
|
+
if (entry)
|
|
1177
|
+
return entry;
|
|
1178
|
+
return deepFreeze({ error: { code: "NOT_FOUND", name, message: `No registered workflow function or variable is available: ${name}` } });
|
|
1143
1179
|
}
|
|
1144
1180
|
globals() {
|
|
1145
1181
|
return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.keys(extension.functions ?? {}).map((name) => [name, { name }]))));
|
|
1146
1182
|
}
|
|
1147
1183
|
async invokeFunction(name, input, context, path, journal) {
|
|
1148
|
-
const fn =
|
|
1149
|
-
if (!fn)
|
|
1150
|
-
fail("MISSING_WORKFLOW", `Workflow function is unavailable: ${name}`);
|
|
1184
|
+
const fn = this.function(name);
|
|
1151
1185
|
if (!object(input) || !jsonValue(input) || !Value.Check(fn.input, input))
|
|
1152
1186
|
fail("RESULT_INVALID", `Invalid input for ${name}`);
|
|
1153
1187
|
const replayed = journal.get(path);
|
|
@@ -1156,7 +1190,7 @@ export class WorkflowRegistry {
|
|
|
1156
1190
|
fail("RESULT_INVALID", `Invalid replay for ${name}`);
|
|
1157
1191
|
return structuredClone(replayed);
|
|
1158
1192
|
}
|
|
1159
|
-
const result = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, agent: context.agent, prompt: context.prompt, parallel: context.parallel, pipeline: context.pipeline, withWorktree: context.withWorktree, checkpoint: context.checkpoint, phase: context.phase, log: context.log }));
|
|
1193
|
+
const result = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, invoke: context.invoke, agent: context.agent, shell: context.shell, prompt: context.prompt, parallel: context.parallel, pipeline: context.pipeline, withWorktree: context.withWorktree, checkpoint: context.checkpoint, phase: context.phase, log: context.log }));
|
|
1160
1194
|
if (!jsonValue(result) || !Value.Check(fn.output, result))
|
|
1161
1195
|
fail("RESULT_INVALID", `Invalid output from ${name}`);
|
|
1162
1196
|
const stored = structuredClone(result);
|
|
@@ -1177,9 +1211,11 @@ function createWorkflowRegistryApi(registry) {
|
|
|
1177
1211
|
get frozen() { return registry.frozen; },
|
|
1178
1212
|
freeze: () => { registry.freeze(); },
|
|
1179
1213
|
register: (extension) => { registry.register(extension); },
|
|
1180
|
-
|
|
1181
|
-
|
|
1214
|
+
function: (name) => registry.function(name),
|
|
1215
|
+
functions: () => registry.functions(),
|
|
1182
1216
|
catalog: () => registry.catalog(),
|
|
1217
|
+
catalogIndex: () => registry.catalogIndex(),
|
|
1218
|
+
catalogDetail: (name) => registry.catalogDetail(name),
|
|
1183
1219
|
globals: () => registry.globals(),
|
|
1184
1220
|
invokeFunction: (...args) => registry.invokeFunction(...args),
|
|
1185
1221
|
variables: () => registry.variables(),
|
|
@@ -1200,25 +1236,28 @@ function loadingRegistry() { return workflowRegistryHost().api; }
|
|
|
1200
1236
|
beginWorkflowExtensionLoading();
|
|
1201
1237
|
export function registerWorkflowExtension(extension) { loadingRegistry().register(extension); }
|
|
1202
1238
|
export function workflowCatalog() { return loadingRegistry().catalog(); }
|
|
1203
|
-
export function
|
|
1239
|
+
export function workflowCatalogIndex() { return loadingRegistry().catalogIndex(); }
|
|
1240
|
+
export function workflowCatalogDetail(name) { return loadingRegistry().catalogDetail(name); }
|
|
1241
|
+
export function registeredWorkflowFunctions() { return loadingRegistry().functions(); }
|
|
1204
1242
|
export function formatWorkflowPreview(args) {
|
|
1205
1243
|
const name = typeof args.name === "string" && args.name.trim() ? args.name.trim() : typeof args.workflow === "string" && args.workflow.trim() ? args.workflow : "workflow";
|
|
1206
1244
|
if (typeof args.script !== "string" || !args.script.trim())
|
|
1207
|
-
return `workflow ${name}${typeof args.workflow === "string" ? "\nRegistered
|
|
1245
|
+
return `workflow ${name}${typeof args.workflow === "string" ? "\nRegistered function" : ""}`;
|
|
1208
1246
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
1209
1247
|
}
|
|
1210
1248
|
export const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
1211
1249
|
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow";
|
|
1212
|
-
export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow that orchestrates subagents. Inline scripts require a name; registered
|
|
1250
|
+
export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow that orchestrates subagents. Inline scripts require a name; registered functions use their function name. Runs in the background by default; completion arrives as a follow-up message. Foreground results include the completed run ID, and parentRunId reuses matching named worktrees from a terminal run.";
|
|
1213
1251
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
1214
1252
|
name: Type.Optional(Type.String({ description: "Workflow name for inline scripts" })),
|
|
1215
1253
|
description: Type.Optional(Type.String({ description: "Optional human-readable workflow description" })),
|
|
1216
1254
|
script: Type.Optional(Type.String({ description: "Immutable workflow source without metadata" })),
|
|
1217
|
-
workflow: Type.Optional(Type.String({ description: "Registered reusable
|
|
1255
|
+
workflow: Type.Optional(Type.String({ description: "Registered reusable function as an unqualified name" })),
|
|
1218
1256
|
args: Type.Optional(Type.Unknown({ description: "JSON-compatible workflow arguments" })),
|
|
1219
1257
|
foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of running in the background" })),
|
|
1220
1258
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16 })),
|
|
1221
1259
|
budget: Type.Optional(Type.Unknown({ description: "Optional aggregate soft and hard run budgets" })),
|
|
1260
|
+
parentRunId: Type.Optional(Type.String({ description: "Terminal run whose named worktrees may be reused" })),
|
|
1222
1261
|
});
|
|
1223
1262
|
function hasDynamicAgentRole(node) {
|
|
1224
1263
|
if (!node)
|
|
@@ -1244,8 +1283,11 @@ export function preflight(script, capabilities, schemas = [], metadata = { name:
|
|
|
1244
1283
|
fail("INVALID_METADATA", `${INTERNAL_CONVERSATION_NAME} is reserved for workflow conversation instrumentation`);
|
|
1245
1284
|
if (hasIdentifier(program, INTERNAL_WORKTREE_NAME))
|
|
1246
1285
|
fail("INVALID_METADATA", `${INTERNAL_WORKTREE_NAME} is reserved for workflow withWorktree instrumentation`);
|
|
1286
|
+
if (hasIdentifier(program, INTERNAL_SHELL_NAME))
|
|
1287
|
+
fail("INVALID_METADATA", `${INTERNAL_SHELL_NAME} is reserved for workflow shell instrumentation`);
|
|
1247
1288
|
validateDirectPrimitiveReferences(program, "withWorktree");
|
|
1248
1289
|
validateDirectPrimitiveReferences(program, "conversation");
|
|
1290
|
+
validateDirectPrimitiveReferences(program, "shell");
|
|
1249
1291
|
for (const [index, schema] of schemas.entries())
|
|
1250
1292
|
validateSchema(schema, `schema[${String(index)}]`);
|
|
1251
1293
|
const calls = workflowCalls(program);
|
|
@@ -1259,6 +1301,8 @@ export function preflight(script, capabilities, schemas = [], metadata = { name:
|
|
|
1259
1301
|
}
|
|
1260
1302
|
if (operation === "withWorktree")
|
|
1261
1303
|
validateStaticWithWorktree(call);
|
|
1304
|
+
if (operation === "shell")
|
|
1305
|
+
validateStaticShellOptions(call);
|
|
1262
1306
|
if ((operation === "parallel" || operation === "pipeline") && call.arguments.some((argument) => argument.type === "SpreadElement"))
|
|
1263
1307
|
continue;
|
|
1264
1308
|
if (operation === "checkpoint" && stableName(call.arguments[0]) === false)
|
|
@@ -1295,6 +1339,7 @@ export function preflight(script, capabilities, schemas = [], metadata = { name:
|
|
|
1295
1339
|
fail("UNKNOWN_AGENT_TYPE", `Unknown agent type: ${missingType}`);
|
|
1296
1340
|
return Object.freeze({ metadata: deepFreeze(checkedMetadata), referenced: deepFreeze({ phases, models, tools, agentTypes }), schemas: deepFreeze(checkedSchemas), dynamicAgentRoles });
|
|
1297
1341
|
}
|
|
1342
|
+
function functionLaunchScript(name) { return `return await ${name}(args);`; }
|
|
1298
1343
|
export function validateWorkflowLaunch(params, context) {
|
|
1299
1344
|
return validateWorkflowLaunchWithRegistry(params, context, loadingRegistry());
|
|
1300
1345
|
}
|
|
@@ -1303,15 +1348,19 @@ function validateWorkflowLaunchWithRegistry(params, context, registry) {
|
|
|
1303
1348
|
fail("INVALID_METADATA", "maxAgentLaunches has been removed; use budget.agentLaunches");
|
|
1304
1349
|
if (params.script !== undefined && params.workflow !== undefined)
|
|
1305
1350
|
fail("INVALID_METADATA", "Provide either script or workflow, not both");
|
|
1306
|
-
const
|
|
1307
|
-
const
|
|
1351
|
+
const functionName = typeof params.workflow === "string" ? params.workflow : undefined;
|
|
1352
|
+
const fn = functionName === undefined ? undefined : registry.function(functionName);
|
|
1353
|
+
const args = params.args === undefined ? null : params.args;
|
|
1354
|
+
if (functionName !== undefined && fn && (!object(args) || !jsonValue(args) || !Value.Check(fn.input, args)))
|
|
1355
|
+
fail("RESULT_INVALID", `Invalid input for ${functionName}`);
|
|
1356
|
+
const script = functionName !== undefined && fn ? functionLaunchScript(functionName) : typeof params.script === "string" && params.script.trim() ? params.script : "";
|
|
1308
1357
|
if (!script)
|
|
1309
|
-
fail("INVALID_SYNTAX", "Provide script or registered
|
|
1310
|
-
const workflowName = typeof params.name === "string" && params.name.trim() ? params.name.trim() :
|
|
1358
|
+
fail("INVALID_SYNTAX", "Provide script or registered function");
|
|
1359
|
+
const workflowName = functionName ?? (typeof params.name === "string" && params.name.trim() ? params.name.trim() : "");
|
|
1311
1360
|
if (!workflowName)
|
|
1312
1361
|
fail("INVALID_METADATA", "Inline workflows require name");
|
|
1313
|
-
const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } :
|
|
1314
|
-
const globalAgentDefinitions = loadAgentDefinitions(context.cwd,
|
|
1362
|
+
const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } : fn?.description ? { description: fn.description } : {}) });
|
|
1363
|
+
const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false);
|
|
1315
1364
|
const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};
|
|
1316
1365
|
const agentDefinitions = deepFreeze({ ...globalAgentDefinitions, ...projectAgentDefinitions });
|
|
1317
1366
|
const aliases = context.modelAliases ?? {};
|
|
@@ -1319,7 +1368,7 @@ function validateWorkflowLaunchWithRegistry(params, context, registry) {
|
|
|
1319
1368
|
const checked = preflight(script, { models: context.availableModels, tools: context.rootTools, agentTypes: new Set(Object.keys(agentDefinitions)), modelAliases: aliases, knownModels, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}) }, [], metadata);
|
|
1320
1369
|
const roleNames = checked.dynamicAgentRoles ? Object.keys(agentDefinitions) : checked.referenced.agentTypes;
|
|
1321
1370
|
validateRolePolicies(agentDefinitions, roleNames, context.availableModels, context.rootTools, aliases, knownModels, context.settingsPath);
|
|
1322
|
-
return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames };
|
|
1371
|
+
return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, ...(functionName ? { functionName } : {}) };
|
|
1323
1372
|
}
|
|
1324
1373
|
function deepFreeze(value) {
|
|
1325
1374
|
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
|
|
@@ -1330,13 +1379,29 @@ function deepFreeze(value) {
|
|
|
1330
1379
|
return value;
|
|
1331
1380
|
}
|
|
1332
1381
|
export function createLaunchSnapshot(input) {
|
|
1333
|
-
return deepFreeze(structuredClone({ ...input, identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION }));
|
|
1382
|
+
return deepFreeze(structuredClone({ ...input, launchKind: input.launchKind ?? (input.functionName ? "function" : "inline"), identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION }));
|
|
1334
1383
|
}
|
|
1335
1384
|
export function loadLaunchSnapshot(input) {
|
|
1336
|
-
if (object(input.settings) && Object.prototype.hasOwnProperty.call(input.settings, "maxAgentLaunches"))
|
|
1337
|
-
fail("RESUME_INCOMPATIBLE", "Persisted workflow settings contain removed maxAgentLaunches");
|
|
1338
1385
|
return deepFreeze(structuredClone(input));
|
|
1339
1386
|
}
|
|
1387
|
+
function launchScriptForSnapshot(snapshot, registry) {
|
|
1388
|
+
if (snapshot.launchKind === "function") {
|
|
1389
|
+
if (!snapshot.functionName)
|
|
1390
|
+
fail("RESUME_INCOMPATIBLE", "Persisted registered function launch is missing its function name");
|
|
1391
|
+
try {
|
|
1392
|
+
registry.function(snapshot.functionName);
|
|
1393
|
+
}
|
|
1394
|
+
catch (error) {
|
|
1395
|
+
if (error instanceof WorkflowError && error.code === "MISSING_WORKFLOW")
|
|
1396
|
+
throw new WorkflowError("RESUME_INCOMPATIBLE", `Persisted registered function is unavailable: ${snapshot.functionName}`);
|
|
1397
|
+
throw error;
|
|
1398
|
+
}
|
|
1399
|
+
return functionLaunchScript(snapshot.functionName);
|
|
1400
|
+
}
|
|
1401
|
+
if (snapshot.launchKind === "inline")
|
|
1402
|
+
return snapshot.script;
|
|
1403
|
+
fail("RESUME_INCOMPATIBLE", "This persisted run uses the removed registered-workflow format; launch it again as a registered function or inline script");
|
|
1404
|
+
}
|
|
1340
1405
|
export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
1341
1406
|
export const HEARTBEAT_TIMEOUT_MS = 5000;
|
|
1342
1407
|
const OUTCOME_ERRORS = new Set(["AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID"]);
|
|
@@ -1402,10 +1467,12 @@ const named = (value, kind) => { if (typeof value !== "string" || !value.trim())
|
|
|
1402
1467
|
const path = (...names) => names.map(encodeURIComponent).join("/");
|
|
1403
1468
|
const inheritedAgentPath = new AsyncLocalStorage();
|
|
1404
1469
|
const agentOccurrences = new Map();
|
|
1470
|
+
const shellOccurrences = new Map();
|
|
1405
1471
|
const conversationOccurrences = new Map();
|
|
1406
1472
|
const worktreeOwners = new AsyncLocalStorage();
|
|
1407
1473
|
const worktreeOccurrences = new Map();
|
|
1408
1474
|
const rejectAgent = () => { throw workError("INVALID_METADATA", "Workflow agent calls must use a direct agent(...) call; aliases and indirect calls are unsupported"); };
|
|
1475
|
+
const rejectShell = () => { throw workError("INVALID_METADATA", "Workflow shell calls must use a direct shell(...) call; aliases and indirect calls are unsupported"); };
|
|
1409
1476
|
const rejectWorktree = () => { throw workError("INVALID_METADATA", "withWorktree calls must use a direct withWorktree(...) call; aliases and indirect calls are unsupported"); };
|
|
1410
1477
|
const internalWithWorktree = async (...values) => {
|
|
1411
1478
|
const callSite = values.pop();
|
|
@@ -1424,7 +1491,9 @@ const internalWithWorktree = async (...values) => {
|
|
|
1424
1491
|
worktreeOccurrences.set(occurrenceKey, occurrence);
|
|
1425
1492
|
owner = path("worktree", "unnamed", ...inherited, "callsite:" + callSite, "occurrence:" + String(occurrence));
|
|
1426
1493
|
}
|
|
1427
|
-
|
|
1494
|
+
const reference = await rpc("worktree", [owner]);
|
|
1495
|
+
if (!reference || typeof reference !== "object" || typeof reference.path !== "string" || typeof reference.branch !== "string") throw workError("WORKTREE_FAILED", "Worktree reference is invalid");
|
|
1496
|
+
return await worktreeOwners.run(owner, () => callback(Object.freeze({ path: reference.path, branch: reference.branch })));
|
|
1428
1497
|
};
|
|
1429
1498
|
const internalConversation = (...values) => {
|
|
1430
1499
|
const callSite = values.pop();
|
|
@@ -1483,6 +1552,31 @@ const internalAgent = (...values) => {
|
|
|
1483
1552
|
});
|
|
1484
1553
|
return result;
|
|
1485
1554
|
};
|
|
1555
|
+
const internalShell = (...values) => {
|
|
1556
|
+
const callSite = values.pop();
|
|
1557
|
+
if (typeof callSite !== "string") throw workError("INTERNAL_ERROR", "Missing workflow shell call-site identity");
|
|
1558
|
+
if (values.length !== 1 && values.length !== 2) throw workError("INVALID_METADATA", "shell requires a command string and optional options");
|
|
1559
|
+
const command = values[0];
|
|
1560
|
+
if (typeof command !== "string") throw workError("INVALID_METADATA", "shell command must be a string");
|
|
1561
|
+
const options = values.length < 2 || values[1] === undefined ? {} : values[1];
|
|
1562
|
+
if (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some(key => key !== "timeoutMs" && key !== "env")) throw workError("INVALID_METADATA", "shell options must contain only timeoutMs and env");
|
|
1563
|
+
if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0)) throw workError("INVALID_METADATA", "shell timeoutMs must be a positive integer");
|
|
1564
|
+
if (options.env !== undefined && (!options.env || typeof options.env !== "object" || Array.isArray(options.env) || Object.values(options.env).some(value => typeof value !== "string"))) throw workError("INVALID_METADATA", "shell env must be an object of strings");
|
|
1565
|
+
const inherited = inheritedAgentPath.getStore() || [];
|
|
1566
|
+
const worktreeOwner = worktreeOwners.getStore();
|
|
1567
|
+
const occurrenceKey = JSON.stringify([inherited, callSite, worktreeOwner || null]);
|
|
1568
|
+
const occurrence = (shellOccurrences.get(occurrenceKey) || 0) + 1;
|
|
1569
|
+
shellOccurrences.set(occurrenceKey, occurrence);
|
|
1570
|
+
const identity = { structuralPath: [...inherited], callSite, occurrence, ...(worktreeOwner ? { worktreeOwner } : {}) };
|
|
1571
|
+
const result = rpc("shell", [command, options, identity]);
|
|
1572
|
+
Object.defineProperties(result, {
|
|
1573
|
+
toJSON: { value() { throw workError("INVALID_METADATA", "Workflow shell result is a Promise; await it before serialization"); } },
|
|
1574
|
+
toString: { value() { throw workError("INVALID_METADATA", "Workflow shell result is a Promise; await it before interpolation"); } },
|
|
1575
|
+
[Symbol.toPrimitive]: { value() { throw workError("INVALID_METADATA", "Workflow shell result is a Promise; await it before interpolation"); } },
|
|
1576
|
+
});
|
|
1577
|
+
return result;
|
|
1578
|
+
};
|
|
1579
|
+
const shell = rejectShell;
|
|
1486
1580
|
const agent = rejectAgent;
|
|
1487
1581
|
const promptPath = (at, key) => /^[A-Za-z_$][\w$]*$/.test(key) ? at + "." + key : at + "[" + JSON.stringify(key) + "]";
|
|
1488
1582
|
const plainPromptObject = value => {
|
|
@@ -1604,13 +1698,13 @@ const pipeline = async (operationName, items, stages) => {
|
|
|
1604
1698
|
return Object.fromEntries(results.map(result => [result.name, result.value]));
|
|
1605
1699
|
};
|
|
1606
1700
|
const safeMath = Object.fromEntries(Object.getOwnPropertyNames(Math).filter(name => name !== "random").map(name => [name, Math[name]]));
|
|
1607
|
-
const sandbox = { agent, conversation: internalConversation, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
|
|
1701
|
+
const sandbox = { agent, conversation: internalConversation, shell, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
|
|
1608
1702
|
for (const [name, fn] of Object.entries(functions)) Object.defineProperty(sandbox, name, { value: fn, writable: false, configurable: false });
|
|
1609
1703
|
for (const [name, value] of Object.entries(config.variables || {})) Object.defineProperty(sandbox, name, { value: freeze(value), writable: false, configurable: false });
|
|
1610
1704
|
for (const name of ["Date","eval","Function","WebAssembly","process","require","module","exports","console","fetch","XMLHttpRequest","WebSocket","performance","crypto","setTimeout","setInterval","setImmediate","queueMicrotask","Intl","SharedArrayBuffer","Atomics"]) sandbox[name] = undefined;
|
|
1611
1705
|
const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
|
|
1612
1706
|
const body = config.script;
|
|
1613
|
-
Promise.resolve().then(() => new vm.Script("(async(__pi_extensible_workflows_agent,__pi_extensible_workflows_conversation,__pi_extensible_workflows_withWorktree)=>{" + body + "\n})", { filename: "workflow.js" }).runInContext(context)(internalAgent, internalConversation, internalWithWorktree))
|
|
1707
|
+
Promise.resolve().then(() => new vm.Script("(async(__pi_extensible_workflows_agent,__pi_extensible_workflows_conversation,__pi_extensible_workflows_withWorktree,__pi_extensible_workflows_shell)=>{" + body + "\n})", { filename: "workflow.js" }).runInContext(context)(internalAgent, internalConversation, internalWithWorktree, internalShell))
|
|
1614
1708
|
.then(async value => { await Promise.all(inflight); send({ type: "result", value: value === undefined ? null : value }); })
|
|
1615
1709
|
.catch(error => send({ type: "error", error: workerError(error) }))
|
|
1616
1710
|
.finally(() => clearInterval(heartbeat));
|
|
@@ -1623,6 +1717,9 @@ function encoded(value) {
|
|
|
1623
1717
|
fail("RPC_LIMIT_EXCEEDED", "RPC value exceeds the 10 MB JSON boundary");
|
|
1624
1718
|
return json;
|
|
1625
1719
|
}
|
|
1720
|
+
function encodedRpcResult(id, value) {
|
|
1721
|
+
return encoded({ type: "rpcResult", id, ok: true, value });
|
|
1722
|
+
}
|
|
1626
1723
|
function readAgentIdentity(value) {
|
|
1627
1724
|
if (!object(value))
|
|
1628
1725
|
fail("INTERNAL_ERROR", "Invalid workflow agent identity");
|
|
@@ -1637,9 +1734,18 @@ function readAgentIdentity(value) {
|
|
|
1637
1734
|
fail("INTERNAL_ERROR", "Invalid workflow agent identity");
|
|
1638
1735
|
return { structuralPath: [...structuralPath], callSite, occurrence, ...(typeof parentBreadcrumb === "string" ? { parentBreadcrumb } : {}), ...(typeof worktreeOwner === "string" ? { worktreeOwner } : {}), ...(parsedConversation ? { conversation: parsedConversation } : {}) };
|
|
1639
1736
|
}
|
|
1737
|
+
function readShellIdentity(value) {
|
|
1738
|
+
const identity = readAgentIdentity(value);
|
|
1739
|
+
if (identity.conversation)
|
|
1740
|
+
fail("INTERNAL_ERROR", "Shell identity cannot include a conversation");
|
|
1741
|
+
return { structuralPath: identity.structuralPath, callSite: identity.callSite, occurrence: identity.occurrence, ...(identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {}) };
|
|
1742
|
+
}
|
|
1640
1743
|
function agentIdentityPath(identity) {
|
|
1641
1744
|
return operationPath("agent", ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
|
|
1642
1745
|
}
|
|
1746
|
+
function shellIdentityPath(identity) {
|
|
1747
|
+
return operationPath("shell", ...(identity.worktreeOwner ? ["worktree", identity.worktreeOwner] : []), ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
|
|
1748
|
+
}
|
|
1643
1749
|
function conversationIdentityPath(identity) {
|
|
1644
1750
|
if (!identity.conversation)
|
|
1645
1751
|
throw new WorkflowError("INTERNAL_ERROR", "Missing conversation identity");
|
|
@@ -1650,9 +1756,122 @@ function conversationTurnPath(identity) {
|
|
|
1650
1756
|
throw new WorkflowError("INTERNAL_ERROR", "Missing conversation identity");
|
|
1651
1757
|
return operationPath(conversationIdentityPath(identity), `turn:${String(identity.conversation.turn)}`);
|
|
1652
1758
|
}
|
|
1759
|
+
function readShellResult(value) {
|
|
1760
|
+
if (!object(value) || (value.exitCode !== null && !Number.isInteger(value.exitCode)) || typeof value.stdout !== "string" || typeof value.stderr !== "string")
|
|
1761
|
+
fail("SHELL_FAILED", "Shell bridge returned an invalid result");
|
|
1762
|
+
return { exitCode: value.exitCode, stdout: value.stdout, stderr: value.stderr };
|
|
1763
|
+
}
|
|
1653
1764
|
function agentWorktree(identity) {
|
|
1654
1765
|
return identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {};
|
|
1655
1766
|
}
|
|
1767
|
+
function shellProcessKill(child) {
|
|
1768
|
+
let forceKill;
|
|
1769
|
+
const killProcessTree = (signal) => {
|
|
1770
|
+
try {
|
|
1771
|
+
if (child.pid && process.platform !== "win32")
|
|
1772
|
+
process.kill(-child.pid, signal);
|
|
1773
|
+
else if (child.pid && process.platform === "win32") {
|
|
1774
|
+
const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore", windowsHide: true });
|
|
1775
|
+
killer.unref();
|
|
1776
|
+
}
|
|
1777
|
+
else
|
|
1778
|
+
child.kill(signal);
|
|
1779
|
+
}
|
|
1780
|
+
catch {
|
|
1781
|
+
try {
|
|
1782
|
+
child.kill(signal);
|
|
1783
|
+
}
|
|
1784
|
+
catch { /* The process may already have exited. */ }
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
child.once("close", () => { if (forceKill)
|
|
1788
|
+
clearTimeout(forceKill); });
|
|
1789
|
+
killProcessTree("SIGTERM");
|
|
1790
|
+
forceKill = setTimeout(() => { forceKill = undefined; killProcessTree("SIGKILL"); }, 1000);
|
|
1791
|
+
forceKill.unref();
|
|
1792
|
+
}
|
|
1793
|
+
function executeShellCommand(command, options, signal, cwd = process.cwd()) {
|
|
1794
|
+
return new Promise((resolve, reject) => {
|
|
1795
|
+
let child;
|
|
1796
|
+
try {
|
|
1797
|
+
child = spawn(command, { shell: true, cwd, env: { ...process.env, ...(options.env ?? {}) }, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
|
|
1798
|
+
}
|
|
1799
|
+
catch (error) {
|
|
1800
|
+
reject(new WorkflowError("SHELL_FAILED", errorText(error)));
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
let settled = false;
|
|
1804
|
+
let timedOut = false;
|
|
1805
|
+
let outputBytes = 0;
|
|
1806
|
+
let timeout;
|
|
1807
|
+
const stdoutDecoder = new StringDecoder("utf8");
|
|
1808
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
1809
|
+
let stdout = "";
|
|
1810
|
+
let stderr = "";
|
|
1811
|
+
const cleanup = () => {
|
|
1812
|
+
if (timeout)
|
|
1813
|
+
clearTimeout(timeout);
|
|
1814
|
+
signal.removeEventListener("abort", onAbort);
|
|
1815
|
+
};
|
|
1816
|
+
const failShell = (error) => {
|
|
1817
|
+
if (settled)
|
|
1818
|
+
return;
|
|
1819
|
+
settled = true;
|
|
1820
|
+
cleanup();
|
|
1821
|
+
shellProcessKill(child);
|
|
1822
|
+
reject(error);
|
|
1823
|
+
};
|
|
1824
|
+
const onAbort = () => { failShell(new WorkflowError("CANCELLED", "Workflow cancelled")); };
|
|
1825
|
+
const capture = (target, chunk) => {
|
|
1826
|
+
if (settled)
|
|
1827
|
+
return;
|
|
1828
|
+
outputBytes += chunk.byteLength;
|
|
1829
|
+
if (outputBytes > RPC_LIMIT_BYTES) {
|
|
1830
|
+
failShell(new WorkflowError("RPC_LIMIT_EXCEEDED", "Shell result exceeds the 10 MB JSON boundary"));
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
if (target === "stdout")
|
|
1834
|
+
stdout += stdoutDecoder.write(chunk);
|
|
1835
|
+
else
|
|
1836
|
+
stderr += stderrDecoder.write(chunk);
|
|
1837
|
+
};
|
|
1838
|
+
child.stdout?.on("data", (chunk) => { capture("stdout", chunk); });
|
|
1839
|
+
child.stderr?.on("data", (chunk) => { capture("stderr", chunk); });
|
|
1840
|
+
child.once("error", (error) => { failShell(new WorkflowError("SHELL_FAILED", errorText(error))); });
|
|
1841
|
+
child.once("close", (exitCode) => {
|
|
1842
|
+
if (settled)
|
|
1843
|
+
return;
|
|
1844
|
+
settled = true;
|
|
1845
|
+
cleanup();
|
|
1846
|
+
stdout += stdoutDecoder.end();
|
|
1847
|
+
stderr += stderrDecoder.end();
|
|
1848
|
+
if (signal.aborted) {
|
|
1849
|
+
reject(new WorkflowError("CANCELLED", "Workflow cancelled"));
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
if (timedOut) {
|
|
1853
|
+
reject(new WorkflowError("SHELL_FAILED", `Shell command timed out after ${String(options.timeoutMs)}ms`));
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
const result = { exitCode: exitCode === null ? null : exitCode, stdout, stderr };
|
|
1857
|
+
try {
|
|
1858
|
+
encodedRpcResult(Number.MAX_SAFE_INTEGER, result);
|
|
1859
|
+
}
|
|
1860
|
+
catch (error) {
|
|
1861
|
+
reject(error instanceof WorkflowError ? error : new WorkflowError("RPC_LIMIT_EXCEEDED", errorText(error)));
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
resolve(result);
|
|
1865
|
+
});
|
|
1866
|
+
if (signal.aborted) {
|
|
1867
|
+
onAbort();
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1871
|
+
if (options.timeoutMs !== undefined)
|
|
1872
|
+
timeout = setTimeout(() => { timedOut = true; shellProcessKill(child); }, options.timeoutMs);
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1656
1875
|
export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
1657
1876
|
encoded(args);
|
|
1658
1877
|
const config = JSON.stringify({ script: instrumentWorkflow(script), args: structuredClone(args), functions: bridge.functions ?? {}, variables: bridge.variables ?? {} });
|
|
@@ -1756,6 +1975,14 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
|
1756
1975
|
value = branded({ name: label, ok: false, failedAt: path, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } });
|
|
1757
1976
|
}
|
|
1758
1977
|
}
|
|
1978
|
+
else if (method === "shell") {
|
|
1979
|
+
if (!bridge.shell)
|
|
1980
|
+
fail("SHELL_FAILED", "No shell bridge is available");
|
|
1981
|
+
const command = validateShellCommand(values[0]);
|
|
1982
|
+
const options = validateShellOptions(values[1]);
|
|
1983
|
+
const identity = readShellIdentity(values[2]);
|
|
1984
|
+
value = readShellResult(await bridge.shell(command, options, controller.signal, identity));
|
|
1985
|
+
}
|
|
1759
1986
|
else if (method === "checkpoint") {
|
|
1760
1987
|
if (!bridge.checkpoint || !object(values[0]))
|
|
1761
1988
|
fail("INTERNAL_ERROR", "checkpoint requires an available bridge and object input");
|
|
@@ -1792,6 +2019,11 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
|
1792
2019
|
value = branded({ name, ok: false, failedAt: name, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } });
|
|
1793
2020
|
}
|
|
1794
2021
|
}
|
|
2022
|
+
else if (method === "worktree") {
|
|
2023
|
+
if (!bridge.worktree || typeof values[0] !== "string" || !values[0])
|
|
2024
|
+
fail("INTERNAL_ERROR", "worktree requires an active host bridge and scope");
|
|
2025
|
+
value = await bridge.worktree(values[0], controller.signal);
|
|
2026
|
+
}
|
|
1795
2027
|
else if (method === "phase") {
|
|
1796
2028
|
if (typeof values[0] !== "string")
|
|
1797
2029
|
fail("INTERNAL_ERROR", "phase name must be a string");
|
|
@@ -1805,7 +2037,7 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
|
1805
2037
|
else
|
|
1806
2038
|
fail("INTERNAL_ERROR", `Unknown worker RPC method: ${method}`);
|
|
1807
2039
|
encoded(value);
|
|
1808
|
-
child.send(
|
|
2040
|
+
child.send(encodedRpcResult(id, value));
|
|
1809
2041
|
}
|
|
1810
2042
|
catch (error) {
|
|
1811
2043
|
const typed = asWorkflowError(error);
|
|
@@ -2005,13 +2237,9 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
2005
2237
|
const render = ({ agent, depth }, grouped) => {
|
|
2006
2238
|
const icon = agent.state === "completed" ? "✓" : agent.state === "failed" || agent.state === "cancelled" ? "✗" : agent.state === "running" ? "⠦" : "○";
|
|
2007
2239
|
const breadcrumb = grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId);
|
|
2008
|
-
const setup = agent.attemptDetails?.at(-1)?.setup;
|
|
2009
|
-
const thinking = setup?.model.thinking ?? agent.model.thinking;
|
|
2010
|
-
const model = `${setup?.model.provider ?? agent.model.provider}/${setup?.model.model ?? agent.model.model}${thinking ? `:${thinking}` : ""}`;
|
|
2011
|
-
const policy = [`model=${model}`, agent.requestedModel ? `requested=${agent.requestedModel}` : "", `tools=${(setup?.tools ?? agent.tools).join(",") || "(none)"}`, agent.role ? `role=${agent.role}` : ""];
|
|
2012
2240
|
const tokens = agent.accounting ? formatAccounting(agent.accounting) : "";
|
|
2013
2241
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
2014
|
-
const result = [`${indent}${icon} ${breadcrumb} · ${agent.state}
|
|
2242
|
+
const result = [`${indent}${icon} ${breadcrumb} · ${agent.state}${tokens ? ` · ${tokens}` : ""}`];
|
|
2015
2243
|
if (agent.state === "failed" && agent.attemptDetails?.length) {
|
|
2016
2244
|
const last = agent.attemptDetails[agent.attemptDetails.length - 1];
|
|
2017
2245
|
if (last?.error)
|
|
@@ -2075,7 +2303,8 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
|
|
|
2075
2303
|
return lines.join("\n");
|
|
2076
2304
|
}
|
|
2077
2305
|
function formatCheckpointReview(checkpoint) {
|
|
2078
|
-
|
|
2306
|
+
const context = JSON.stringify(checkpoint.context, null, 2);
|
|
2307
|
+
return [`Name: ${checkpoint.name}`, "Prompt:", checkpoint.prompt, context === "null" ? "Context: null" : "Context:", ...(context === "null" ? [] : [context])].join("\n");
|
|
2079
2308
|
}
|
|
2080
2309
|
const DELIVERY_LIMIT_BYTES = 4 * 1024;
|
|
2081
2310
|
const WORKFLOW_LOG_ENTRY = "workflow-log";
|
|
@@ -2097,11 +2326,144 @@ function utf8Prefix(value, maxBytes) {
|
|
|
2097
2326
|
end -= 1;
|
|
2098
2327
|
return bytes.subarray(0, end).toString("utf8");
|
|
2099
2328
|
}
|
|
2329
|
+
const DIAGNOSTIC_LIMIT_BYTES = DELIVERY_LIMIT_BYTES - 512;
|
|
2330
|
+
function failureDiagnosticsFrom(error) {
|
|
2331
|
+
if (!error || typeof error !== "object")
|
|
2332
|
+
return undefined;
|
|
2333
|
+
return error[WORKFLOW_FAILURE_DIAGNOSTICS];
|
|
2334
|
+
}
|
|
2335
|
+
function boundedWorkflowFailureDiagnostics(value) {
|
|
2336
|
+
let bounded = {
|
|
2337
|
+
runId: utf8Prefix(value.runId, 128),
|
|
2338
|
+
workflowName: utf8Prefix(value.workflowName, 256),
|
|
2339
|
+
state: value.state,
|
|
2340
|
+
failedAt: value.failedAt === null ? null : utf8Prefix(value.failedAt, 1024),
|
|
2341
|
+
error: { code: value.error.code, message: utf8Prefix(value.error.message, 1024) },
|
|
2342
|
+
...(value.failedAgent ? { failedAgent: {
|
|
2343
|
+
id: utf8Prefix(value.failedAgent.id, 128),
|
|
2344
|
+
...(value.failedAgent.label ? { label: utf8Prefix(value.failedAgent.label, 128) } : {}),
|
|
2345
|
+
...(value.failedAgent.role ? { role: utf8Prefix(value.failedAgent.role, 128) } : {}),
|
|
2346
|
+
structuralPath: value.failedAgent.structuralPath.slice(0, 8).map((part) => utf8Prefix(part, 128)),
|
|
2347
|
+
attempt: value.failedAgent.attempt,
|
|
2348
|
+
...(value.failedAgent.sessionId ? { sessionId: utf8Prefix(value.failedAgent.sessionId, 256) } : {}),
|
|
2349
|
+
...(value.failedAgent.sessionFile ? { sessionFile: utf8Prefix(value.failedAgent.sessionFile, 1024) } : {}),
|
|
2350
|
+
} } : {}),
|
|
2351
|
+
completedSiblingAgents: (value.completedSiblingAgents ?? []).slice(0, 16).map((agent) => ({
|
|
2352
|
+
id: utf8Prefix(agent.id, 128),
|
|
2353
|
+
...(agent.label ? { label: utf8Prefix(agent.label, 128) } : {}),
|
|
2354
|
+
...(agent.role ? { role: utf8Prefix(agent.role, 128) } : {}),
|
|
2355
|
+
structuralPath: agent.structuralPath.slice(0, 8).map((part) => utf8Prefix(part, 128)),
|
|
2356
|
+
})),
|
|
2357
|
+
completedSiblingPaths: value.completedSiblingPaths.slice(0, 16).map((path) => path.slice(0, 8).map((part) => utf8Prefix(part, 128))),
|
|
2358
|
+
artifacts: { runDirectory: utf8Prefix(value.artifacts.runDirectory, 1024), statePath: utf8Prefix(value.artifacts.statePath, 1024), journalPath: utf8Prefix(value.artifacts.journalPath, 1024) },
|
|
2359
|
+
};
|
|
2360
|
+
const size = () => Buffer.byteLength(JSON.stringify(bounded));
|
|
2361
|
+
while (size() > DIAGNOSTIC_LIMIT_BYTES) {
|
|
2362
|
+
if (bounded.completedSiblingAgents?.length || bounded.completedSiblingPaths.length) {
|
|
2363
|
+
bounded = { ...bounded, completedSiblingAgents: bounded.completedSiblingAgents?.slice(0, -1) ?? [], completedSiblingPaths: bounded.completedSiblingPaths.slice(0, -1) };
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2366
|
+
if (bounded.failedAgent?.sessionFile) {
|
|
2367
|
+
const failedAgent = { ...bounded.failedAgent };
|
|
2368
|
+
delete failedAgent.sessionFile;
|
|
2369
|
+
bounded = { ...bounded, failedAgent };
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
if (bounded.failedAgent?.sessionId) {
|
|
2373
|
+
const failedAgent = { ...bounded.failedAgent };
|
|
2374
|
+
delete failedAgent.sessionId;
|
|
2375
|
+
bounded = { ...bounded, failedAgent };
|
|
2376
|
+
continue;
|
|
2377
|
+
}
|
|
2378
|
+
if (Buffer.byteLength(bounded.artifacts.runDirectory) > 256) {
|
|
2379
|
+
bounded = { ...bounded, artifacts: { ...bounded.artifacts, runDirectory: utf8Prefix(bounded.artifacts.runDirectory, 256) } };
|
|
2380
|
+
continue;
|
|
2381
|
+
}
|
|
2382
|
+
if (Buffer.byteLength(bounded.error.message) > 256) {
|
|
2383
|
+
bounded = { ...bounded, error: { ...bounded.error, message: utf8Prefix(bounded.error.message, 256) } };
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
if (bounded.failedAt !== null && Buffer.byteLength(bounded.failedAt) > 256) {
|
|
2387
|
+
bounded = { ...bounded, failedAt: utf8Prefix(bounded.failedAt, 256) };
|
|
2388
|
+
continue;
|
|
2389
|
+
}
|
|
2390
|
+
if (bounded.failedAgent && bounded.failedAgent.structuralPath.length > 4) {
|
|
2391
|
+
bounded = { ...bounded, failedAgent: { ...bounded.failedAgent, structuralPath: bounded.failedAgent.structuralPath.slice(0, 4) } };
|
|
2392
|
+
continue;
|
|
2393
|
+
}
|
|
2394
|
+
if (bounded.failedAgent?.structuralPath.some((part) => Buffer.byteLength(part) > 64)) {
|
|
2395
|
+
bounded = { ...bounded, failedAgent: { ...bounded.failedAgent, structuralPath: bounded.failedAgent.structuralPath.map((part) => utf8Prefix(part, 64)) } };
|
|
2396
|
+
continue;
|
|
2397
|
+
}
|
|
2398
|
+
if (Buffer.byteLength(bounded.artifacts.statePath) > 512 || Buffer.byteLength(bounded.artifacts.journalPath) > 512) {
|
|
2399
|
+
bounded = { ...bounded, artifacts: { ...bounded.artifacts, statePath: utf8Prefix(bounded.artifacts.statePath, 512), journalPath: utf8Prefix(bounded.artifacts.journalPath, 512) } };
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
if (Buffer.byteLength(bounded.workflowName) > 128) {
|
|
2403
|
+
bounded = { ...bounded, workflowName: utf8Prefix(bounded.workflowName, 128) };
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
break;
|
|
2407
|
+
}
|
|
2408
|
+
return bounded;
|
|
2409
|
+
}
|
|
2410
|
+
function createWorkflowFailureDiagnostics(store, metadata, error, run) {
|
|
2411
|
+
const rawFailedAt = error && typeof error === "object" ? error.failedAt : undefined;
|
|
2412
|
+
const failedAt = typeof rawFailedAt === "string" && rawFailedAt ? rawFailedAt : null;
|
|
2413
|
+
const failedAgents = run.agents.filter((agent) => agent.state === "failed");
|
|
2414
|
+
const failedAgentRecord = failedAgents.find((agent) => {
|
|
2415
|
+
if (failedAt === null)
|
|
2416
|
+
return false;
|
|
2417
|
+
try {
|
|
2418
|
+
return failedAt.includes(`${operationPath("agent", ...(agent.structuralPath ?? []))}/`);
|
|
2419
|
+
}
|
|
2420
|
+
catch {
|
|
2421
|
+
return false;
|
|
2422
|
+
}
|
|
2423
|
+
}) ?? failedAgents.at(-1);
|
|
2424
|
+
const failedAttempt = failedAgentRecord ? [...(failedAgentRecord.attemptDetails ?? [])].reverse().find((attempt) => attempt.error) ?? failedAgentRecord.attemptDetails?.at(-1) : undefined;
|
|
2425
|
+
const failedAgent = failedAgentRecord ? {
|
|
2426
|
+
id: failedAgentRecord.id,
|
|
2427
|
+
...(failedAgentRecord.label ?? failedAgentRecord.name ? { label: failedAgentRecord.label ?? failedAgentRecord.name } : {}),
|
|
2428
|
+
...(failedAgentRecord.role ? { role: failedAgentRecord.role } : {}),
|
|
2429
|
+
structuralPath: [...(failedAgentRecord.structuralPath ?? [])],
|
|
2430
|
+
attempt: Math.max(1, failedAttempt?.attempt ?? failedAgentRecord.attempts),
|
|
2431
|
+
...(failedAttempt?.sessionId ? { sessionId: failedAttempt.sessionId } : {}),
|
|
2432
|
+
...(failedAttempt?.sessionFile ? { sessionFile: failedAttempt.sessionFile } : {}),
|
|
2433
|
+
} : undefined;
|
|
2434
|
+
const completedSiblingAgents = run.agents.filter((agent) => {
|
|
2435
|
+
if (agent.state !== "completed" || agent.id === failedAgentRecord?.id)
|
|
2436
|
+
return false;
|
|
2437
|
+
return failedAgentRecord?.parentId === undefined ? agent.parentId === undefined : agent.parentId === failedAgentRecord.parentId;
|
|
2438
|
+
}).map((agent) => ({
|
|
2439
|
+
id: agent.id,
|
|
2440
|
+
...(agent.label ?? agent.name ? { label: agent.label ?? agent.name } : {}),
|
|
2441
|
+
...(agent.role ? { role: agent.role } : {}),
|
|
2442
|
+
structuralPath: [...(agent.structuralPath ?? [])],
|
|
2443
|
+
}));
|
|
2444
|
+
const completedSiblingPaths = completedSiblingAgents.map((agent) => [...agent.structuralPath]);
|
|
2445
|
+
return boundedWorkflowFailureDiagnostics({
|
|
2446
|
+
runId: run.id, workflowName: metadata.name, state: run.state, failedAt,
|
|
2447
|
+
error: { code: errorCode(error) ?? "INTERNAL_ERROR", message: errorText(error) || "The workflow failed without an error message." },
|
|
2448
|
+
...(failedAgent ? { failedAgent } : {}), completedSiblingAgents, completedSiblingPaths,
|
|
2449
|
+
artifacts: { runDirectory: store.directory, statePath: join(store.directory, "state.json"), journalPath: join(store.directory, "journal.json") },
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
export function formatWorkflowFailureDiagnostics(diagnostic) {
|
|
2453
|
+
const failedAgent = diagnostic.failedAgent ? `${diagnostic.failedAgent.label ?? diagnostic.failedAgent.id}${diagnostic.failedAgent.role ? ` role=${diagnostic.failedAgent.role}` : ""} attempt=${String(diagnostic.failedAgent.attempt)} path=${diagnostic.failedAgent.structuralPath.join(" > ") || "(root)"}${diagnostic.failedAgent.sessionFile ? ` session=${diagnostic.failedAgent.sessionFile}` : ""}` : "(not persisted)";
|
|
2454
|
+
const siblingAgents = diagnostic.completedSiblingAgents;
|
|
2455
|
+
const siblings = siblingAgents ? siblingAgents.map((agent) => `${agent.label ?? agent.id}${agent.role ? ` role=${agent.role}` : ""} path=${agent.structuralPath.join(" > ") || "(root)"}`).join(", ") || "(none)" : diagnostic.completedSiblingPaths.map((path) => path.join(" > ") || "(root)").join(", ") || "(none)";
|
|
2456
|
+
return [`✗ Workflow: ${diagnostic.workflowName}`, ` Run: ${diagnostic.runId}`, ` State: ${diagnostic.state}`, ` Error: ${diagnostic.error.code}: ${diagnostic.error.message}`, ` Failed at: ${diagnostic.failedAt ?? "(unknown)"}`, ` Failed agent: ${failedAgent}`, ` Completed sibling ${siblingAgents ? "agents" : "paths"}: ${siblings}`, ` Artifacts: state=${diagnostic.artifacts.statePath} journal=${diagnostic.artifacts.journalPath}`].join("\n");
|
|
2457
|
+
}
|
|
2458
|
+
function serializeWorkflowFailureDiagnostics(diagnostic) { return JSON.stringify(diagnostic); }
|
|
2459
|
+
function isWorkflowFailureDiagnostics(value) {
|
|
2460
|
+
return object(value) && typeof value.runId === "string" && typeof value.workflowName === "string" && typeof value.state === "string" && "failedAt" in value && object(value.error) && object(value.artifacts);
|
|
2461
|
+
}
|
|
2100
2462
|
function deliver(pi, content) {
|
|
2101
2463
|
pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
2102
2464
|
}
|
|
2103
|
-
function deliverFailure(pi,
|
|
2104
|
-
deliver(pi, `Workflow ${
|
|
2465
|
+
function deliverFailure(pi, diagnostic) {
|
|
2466
|
+
deliver(pi, `Workflow ${utf8Prefix(diagnostic.workflowName, 128)} failure diagnostics: ${serializeWorkflowFailureDiagnostics(diagnostic)}`);
|
|
2105
2467
|
}
|
|
2106
2468
|
function safeEventError(error) {
|
|
2107
2469
|
const code = errorCode(error) ?? "INTERNAL_ERROR";
|
|
@@ -2115,36 +2477,28 @@ class WorkflowEventPublisher {
|
|
|
2115
2477
|
constructor(sink) {
|
|
2116
2478
|
this.sink = sink;
|
|
2117
2479
|
}
|
|
2480
|
+
removeRun(runId) {
|
|
2481
|
+
this.#queues.delete(runId);
|
|
2482
|
+
this.#budgetEvents.delete(runId);
|
|
2483
|
+
this.#worktrees.delete(runId);
|
|
2484
|
+
}
|
|
2118
2485
|
seedBudget(runId, events) {
|
|
2119
2486
|
const seen = this.#budgetEvents.get(runId) ?? new Set();
|
|
2120
2487
|
for (const event of events ?? [])
|
|
2121
2488
|
seen.add(this.budgetKey(event));
|
|
2122
2489
|
this.#budgetEvents.set(runId, seen);
|
|
2123
2490
|
}
|
|
2124
|
-
async runStarted(store, metadata,
|
|
2125
|
-
await this.#publish(store, metadata, WORKFLOW_RUN_STARTED_EVENT, {});
|
|
2126
|
-
if (background)
|
|
2127
|
-
await this.legacyStarted(store, metadata);
|
|
2128
|
-
}
|
|
2129
|
-
async legacyStarted(store, metadata) { await this.#publish(store, metadata, WORKFLOW_ASYNC_STARTED_EVENT, { id: store.runId, runId: store.runId, pid: process.pid, sessionId: store.sessionId, asyncDir: store.directory, agent: metadata.name }); }
|
|
2491
|
+
async runStarted(store, metadata) { await this.#publish(store, metadata, WORKFLOW_RUN_STARTED_EVENT, {}); }
|
|
2130
2492
|
async runResumed(store, metadata) { await this.#publish(store, metadata, WORKFLOW_RUN_RESUMED_EVENT, {}); }
|
|
2131
2493
|
async runState(store, metadata, previousState, state, reason) {
|
|
2132
2494
|
await this.#publish(store, metadata, WORKFLOW_RUN_STATE_CHANGED_EVENT, { previousState, state, ...(reason ? { reason } : {}), ...(ERROR_CODES.includes(reason) ? { errorCode: reason } : {}) });
|
|
2133
2495
|
if ((previousState === "paused" || previousState === "interrupted" || previousState === "budget_exhausted") && state === "running")
|
|
2134
2496
|
await this.runResumed(store, metadata);
|
|
2135
2497
|
}
|
|
2136
|
-
async runCompleted(store, metadata, resultPath,
|
|
2137
|
-
|
|
2138
|
-
if (background)
|
|
2139
|
-
await this.#publish(store, metadata, WORKFLOW_ASYNC_COMPLETE_EVENT, { id: store.runId, runId: store.runId, sessionId: store.sessionId, asyncDir: store.directory, success: true, state: "complete" });
|
|
2140
|
-
}
|
|
2141
|
-
async runFailed(store, metadata, error, background, state) {
|
|
2498
|
+
async runCompleted(store, metadata, resultPath) { await this.#publish(store, metadata, WORKFLOW_RUN_COMPLETED_EVENT, { resultPath }); }
|
|
2499
|
+
async runFailed(store, metadata, error, state) {
|
|
2142
2500
|
if (state === "failed")
|
|
2143
2501
|
await this.#publish(store, metadata, WORKFLOW_RUN_FAILED_EVENT, { error: safeEventError(error) });
|
|
2144
|
-
if (background) {
|
|
2145
|
-
const legacyState = state === "interrupted" ? "failed" : state;
|
|
2146
|
-
await this.#publish(store, metadata, WORKFLOW_ASYNC_COMPLETE_EVENT, { id: store.runId, runId: store.runId, sessionId: store.sessionId, asyncDir: store.directory, success: false, state: legacyState, ...(state === "stopped" ? { stopped: true } : {}), ...(legacyState === "budget_exhausted" || legacyState === "failed" ? { error: safeEventError(error).message } : {}) });
|
|
2147
|
-
}
|
|
2148
2502
|
}
|
|
2149
2503
|
async agentState(store, metadata, previous, agent) {
|
|
2150
2504
|
await this.#publish(store, metadata, WORKFLOW_AGENT_STATE_CHANGED_EVENT, { agentId: agent.id, displayLabel: agent.label ?? agent.name, ...(agent.role ? { role: agent.role } : {}), structuralPath: [...(agent.structuralPath ?? [])], ...(agent.parentId ? { parentId: agent.parentId } : {}), ...(agent.parentBreadcrumb ? { parentBreadcrumb: agent.parentBreadcrumb } : {}), ...(agent.worktreeOwner ? { worktreeOwner: agent.worktreeOwner } : {}), ...(previous ? { previousState: previous.state } : {}), state: agent.state, attempt: agent.attempts });
|
|
@@ -2202,7 +2556,12 @@ function namedRecord(value, kind) {
|
|
|
2202
2556
|
fail("INVALID_METADATA", `${kind} must be a record`);
|
|
2203
2557
|
return Object.entries(value);
|
|
2204
2558
|
}
|
|
2205
|
-
function
|
|
2559
|
+
function publicWorktreeReference(reference) {
|
|
2560
|
+
if (!object(reference) || typeof reference.path !== "string" || typeof reference.branch !== "string")
|
|
2561
|
+
fail("WORKTREE_FAILED", "Worktree reference is invalid");
|
|
2562
|
+
return Object.freeze({ path: reference.path, branch: reference.branch });
|
|
2563
|
+
}
|
|
2564
|
+
async function hostWithWorktree(args, identity, occurrences, resolveWorktree, signal) {
|
|
2206
2565
|
if (args.length !== 1 && args.length !== 2)
|
|
2207
2566
|
fail("INVALID_METADATA", "withWorktree requires a callback or a name and callback");
|
|
2208
2567
|
const callback = args[args.length - 1];
|
|
@@ -2221,7 +2580,10 @@ function hostWithWorktree(args, identity, occurrences) {
|
|
|
2221
2580
|
occurrences.set(key, occurrence);
|
|
2222
2581
|
owner = operationPath("worktree", "unnamed", "function", identity, ...structuralPath, `occurrence:${String(occurrence)}`);
|
|
2223
2582
|
}
|
|
2224
|
-
|
|
2583
|
+
if (!resolveWorktree)
|
|
2584
|
+
fail("WORKTREE_FAILED", "No worktree bridge is available");
|
|
2585
|
+
const reference = publicWorktreeReference(await resolveWorktree(owner, signal));
|
|
2586
|
+
return inheritedHostWorktreeOwner.run(owner, async () => await callback(reference));
|
|
2225
2587
|
}
|
|
2226
2588
|
function workflowRunContext(cwd, sessionId, runId, workflow, args, signal) {
|
|
2227
2589
|
return Object.freeze({ cwd, sessionId, runId, workflow: deepFreeze(structuredClone(workflow)), args: deepFreeze(structuredClone(args)), signal });
|
|
@@ -2320,47 +2682,125 @@ function nextNamedOccurrence(counters, label) {
|
|
|
2320
2682
|
}
|
|
2321
2683
|
function withWorkflowFunctions(bridge, store, runContext, variables, registry) {
|
|
2322
2684
|
const functionAgentOccurrences = new Map();
|
|
2685
|
+
const functionShellOccurrences = new Map();
|
|
2323
2686
|
const functionWorktreeOccurrences = new Map();
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2687
|
+
const functionInvokeOccurrences = new Map();
|
|
2688
|
+
const invokeFunction = async (name, input, path, signal, worktreeOwner, structuralPath = [], breadcrumb) => {
|
|
2689
|
+
const replayed = await store.replay(path);
|
|
2690
|
+
let stored;
|
|
2691
|
+
const sideEffects = [];
|
|
2692
|
+
const functionBreadcrumb = breadcrumb ?? name;
|
|
2693
|
+
const context = {
|
|
2694
|
+
run: runContext,
|
|
2695
|
+
invoke: async (targetName, targetInput) => {
|
|
2696
|
+
const inherited = inheritedHostAgentPath.getStore() ?? structuralPath;
|
|
2697
|
+
const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
|
|
2698
|
+
const key = JSON.stringify([path, inherited, targetName]);
|
|
2699
|
+
const occurrence = (functionInvokeOccurrences.get(key) ?? 0) + 1;
|
|
2700
|
+
functionInvokeOccurrences.set(key, occurrence);
|
|
2701
|
+
const nestedPath = operationPath("function", "nested", path, ...inherited, targetName, `occurrence:${String(occurrence)}`);
|
|
2702
|
+
return invokeFunction(targetName, targetInput, nestedPath, signal, scopedWorktreeOwner, inherited, `${functionBreadcrumb} > ${targetName}`);
|
|
2703
|
+
},
|
|
2704
|
+
agent: async (...args) => {
|
|
2705
|
+
if (!bridge.agent || typeof args[0] !== "string")
|
|
2706
|
+
fail("AGENT_FAILED", "No agent bridge is available");
|
|
2707
|
+
const options = validateAgentOptions(args[1] === undefined ? {} : args[1]);
|
|
2708
|
+
const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
|
|
2709
|
+
const inherited = inheritedHostAgentPath.getStore() ?? [];
|
|
2710
|
+
const key = `${path}\0${JSON.stringify(inherited)}`;
|
|
2711
|
+
const occurrence = (functionAgentOccurrences.get(key) ?? 0) + 1;
|
|
2712
|
+
functionAgentOccurrences.set(key, occurrence);
|
|
2713
|
+
return bridge.agent(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, parentBreadcrumb: functionBreadcrumb, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
|
|
2714
|
+
},
|
|
2715
|
+
shell: async (...args) => {
|
|
2716
|
+
if (!bridge.shell)
|
|
2717
|
+
fail("SHELL_FAILED", "No shell bridge is available");
|
|
2718
|
+
if (typeof args[0] !== "string")
|
|
2719
|
+
fail("INVALID_METADATA", "shell command must be a string");
|
|
2720
|
+
const options = validateShellOptions(args[1] === undefined ? {} : args[1]);
|
|
2721
|
+
const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
|
|
2722
|
+
const inherited = inheritedHostAgentPath.getStore() ?? [];
|
|
2723
|
+
const key = `${path}\0${JSON.stringify([inherited, scopedWorktreeOwner ?? null])}`;
|
|
2724
|
+
const occurrence = (functionShellOccurrences.get(key) ?? 0) + 1;
|
|
2725
|
+
functionShellOccurrences.set(key, occurrence);
|
|
2726
|
+
return bridge.shell(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
|
|
2727
|
+
},
|
|
2728
|
+
prompt: workflowPrompt,
|
|
2729
|
+
parallel: (...args) => hostParallel(args[0], args[1]),
|
|
2730
|
+
pipeline: (...args) => hostPipeline(args[0], args[1], args[2]),
|
|
2731
|
+
withWorktree: (...args) => hostWithWorktree(args, path, functionWorktreeOccurrences, bridge.worktree, signal),
|
|
2732
|
+
checkpoint: async (...args) => {
|
|
2733
|
+
if (!bridge.checkpoint || !object(args[0]) || !jsonValue(args[0]))
|
|
2734
|
+
fail("INTERNAL_ERROR", "No checkpoint bridge is available");
|
|
2735
|
+
return bridge.checkpoint(args[0], signal);
|
|
2736
|
+
},
|
|
2737
|
+
phase: (name) => { sideEffects.push(Promise.resolve(bridge.phase?.(name))); },
|
|
2738
|
+
log: (message) => { sideEffects.push(Promise.resolve(bridge.log?.(message))); },
|
|
2739
|
+
};
|
|
2740
|
+
const result = await inheritedHostAgentPath.run([...structuralPath], async () => registry.invokeFunction(name, input, context, path, { get: () => replayed?.value, put: (_path, value) => { stored = value; } }));
|
|
2741
|
+
await Promise.all(sideEffects);
|
|
2742
|
+
if (!replayed)
|
|
2743
|
+
await store.complete(path, stored ?? result);
|
|
2744
|
+
return result;
|
|
2745
|
+
};
|
|
2746
|
+
return { ...bridge, functions: registry.globals(), variables, function: invokeFunction };
|
|
2359
2747
|
}
|
|
2360
2748
|
function projectTrusted(ctx) {
|
|
2361
2749
|
const check = object(ctx) ? ctx.isProjectTrusted : undefined;
|
|
2362
2750
|
return typeof check === "function" ? Boolean(Reflect.apply(check, ctx, [])) : true;
|
|
2363
2751
|
}
|
|
2752
|
+
function isEntryRenderer(value) { return typeof value === "function"; }
|
|
2753
|
+
function isWorkflowEventSink(value) { return object(value) && typeof value.emit === "function"; }
|
|
2754
|
+
function piHostCapabilities(pi) {
|
|
2755
|
+
if (!object(pi))
|
|
2756
|
+
return {};
|
|
2757
|
+
const registerEntryRenderer = pi.registerEntryRenderer;
|
|
2758
|
+
const events = pi.events;
|
|
2759
|
+
return { ...(isEntryRenderer(registerEntryRenderer) ? { registerEntryRenderer } : {}), ...(isWorkflowEventSink(events) ? { events } : {}) };
|
|
2760
|
+
}
|
|
2761
|
+
function isModelRegistryGetter(value) { return typeof value === "function"; }
|
|
2762
|
+
function contextHostCapabilities(ctx) {
|
|
2763
|
+
if (!object(ctx) || !object(ctx.modelRegistry))
|
|
2764
|
+
return {};
|
|
2765
|
+
const registry = ctx.modelRegistry;
|
|
2766
|
+
const getAll = registry.getAll;
|
|
2767
|
+
const getAvailable = registry.getAvailable;
|
|
2768
|
+
return { modelRegistry: { ...(isModelRegistryGetter(getAll) ? { getAll: () => getAll.call(registry) } : {}), ...(isModelRegistryGetter(getAvailable) ? { getAvailable: () => getAvailable.call(registry) } : {}) } };
|
|
2769
|
+
}
|
|
2770
|
+
function isUiSelect(value) { return typeof value === "function"; }
|
|
2771
|
+
function isUiInput(value) { return typeof value === "function"; }
|
|
2772
|
+
function isUiSetStatus(value) { return typeof value === "function"; }
|
|
2773
|
+
function uiHostCapabilities(ui) {
|
|
2774
|
+
if (!object(ui))
|
|
2775
|
+
return undefined;
|
|
2776
|
+
const select = ui.select;
|
|
2777
|
+
const input = ui.input;
|
|
2778
|
+
const setStatus = ui.setStatus;
|
|
2779
|
+
return { ...(isUiSelect(select) ? { select } : {}), ...(isUiInput(input) ? { input } : {}), ...(isUiSetStatus(setStatus) ? { setStatus } : {}) };
|
|
2780
|
+
}
|
|
2781
|
+
function tuiHostCapabilities(tui) {
|
|
2782
|
+
if (!object(tui) || !object(tui.terminal))
|
|
2783
|
+
return {};
|
|
2784
|
+
return { terminal: { ...(tui.terminal.rows === undefined ? {} : { rows: tui.terminal.rows }) } };
|
|
2785
|
+
}
|
|
2786
|
+
function tuiRows(tui) { const rows = tuiHostCapabilities(tui).terminal?.rows; return typeof rows === "number" && Number.isFinite(rows) ? rows : 24; }
|
|
2787
|
+
const WORKFLOW_OVERLAY_BORDER_ROWS = 2;
|
|
2788
|
+
function borderWorkflowOverlay(component, theme) {
|
|
2789
|
+
return {
|
|
2790
|
+
...component,
|
|
2791
|
+
render(width) {
|
|
2792
|
+
const border = theme.fg("border", "─".repeat(Math.max(1, width)));
|
|
2793
|
+
return [border, ...component.render(width), border];
|
|
2794
|
+
},
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
function isKeybindingGetter(value) { return typeof value === "function"; }
|
|
2798
|
+
function keybindingsHostCapabilities(keybindings) {
|
|
2799
|
+
if (!object(keybindings) || !isKeybindingGetter(keybindings.getKeys))
|
|
2800
|
+
return {};
|
|
2801
|
+
return { getKeys: keybindings.getKeys };
|
|
2802
|
+
}
|
|
2803
|
+
function keybindingKeys(keybindings, name) { const getKeys = keybindingsHostCapabilities(keybindings).getKeys; return typeof getKeys === "function" ? getKeys.call(keybindings, name) : undefined; }
|
|
2364
2804
|
function parseThinking(value) {
|
|
2365
2805
|
switch (value) {
|
|
2366
2806
|
case "off":
|
|
@@ -2373,10 +2813,11 @@ function parseThinking(value) {
|
|
|
2373
2813
|
default: return undefined;
|
|
2374
2814
|
}
|
|
2375
2815
|
}
|
|
2376
|
-
export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession) {
|
|
2816
|
+
export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession, agentDir) {
|
|
2377
2817
|
beginWorkflowExtensionLoading();
|
|
2378
2818
|
const registry = loadingRegistry();
|
|
2379
|
-
const
|
|
2819
|
+
const extensionAgentDir = agentDir ?? getAgentDir();
|
|
2820
|
+
const registerEntryRenderer = piHostCapabilities(pi).registerEntryRenderer;
|
|
2380
2821
|
registerEntryRenderer?.(WORKFLOW_LOG_ENTRY, (entry) => {
|
|
2381
2822
|
const data = entry.data;
|
|
2382
2823
|
return textBlock(data ? `Workflow ${data.workflowName}: ${data.message}` : "");
|
|
@@ -2390,7 +2831,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2390
2831
|
await lifecycle.leave();
|
|
2391
2832
|
}
|
|
2392
2833
|
};
|
|
2393
|
-
const eventPublisher = new WorkflowEventPublisher(pi.events);
|
|
2834
|
+
const eventPublisher = new WorkflowEventPublisher(piHostCapabilities(pi).events);
|
|
2394
2835
|
pi.on("resources_discover", () => {
|
|
2395
2836
|
if (!pi.getActiveTools().includes("workflow"))
|
|
2396
2837
|
return;
|
|
@@ -2399,6 +2840,63 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2399
2840
|
return skillPath ? { skillPaths: [skillPath] } : undefined;
|
|
2400
2841
|
});
|
|
2401
2842
|
const runs = new Map();
|
|
2843
|
+
let providerRecoveryQueue = Promise.resolve();
|
|
2844
|
+
const enqueueProviderRecovery = (task) => { const next = providerRecoveryQueue.then(task, task); providerRecoveryQueue = next.then(() => undefined, () => undefined); return next; };
|
|
2845
|
+
const createProviderErrorRecovery = (host, fallbackModels, abort) => {
|
|
2846
|
+
if (!object(host) || host.mode !== "tui" || host.hasUI !== true)
|
|
2847
|
+
return undefined;
|
|
2848
|
+
const ui = object(host.ui) ? host.ui : undefined;
|
|
2849
|
+
const select = uiHostCapabilities(ui)?.select;
|
|
2850
|
+
if (!select)
|
|
2851
|
+
return undefined;
|
|
2852
|
+
const hostModels = contextHostCapabilities(host).modelRegistry;
|
|
2853
|
+
const choose = (title, options) => select.call(ui, title, options);
|
|
2854
|
+
return (failure) => enqueueProviderRecovery(async () => {
|
|
2855
|
+
const action = await choose(`Subagent "${failure.label}" failed\nCurrent provider/model: ${failure.provider}/${failure.model}\nProvider error: ${failure.error}\nChoose what to do`, ["Retry", "Change model", "Abort workflow"]);
|
|
2856
|
+
if (action === "Retry")
|
|
2857
|
+
return "retry";
|
|
2858
|
+
if (action === "Change model") {
|
|
2859
|
+
const available = hostModels?.getAvailable?.().map((model) => `${model.provider}/${model.id}`) ?? [...fallbackModels];
|
|
2860
|
+
const selected = await choose(`Available models for subagent "${failure.label}"`, [...new Set(available)].sort());
|
|
2861
|
+
if (selected)
|
|
2862
|
+
return { model: selected };
|
|
2863
|
+
}
|
|
2864
|
+
abort();
|
|
2865
|
+
return "abort";
|
|
2866
|
+
});
|
|
2867
|
+
};
|
|
2868
|
+
const pendingFailureDiagnostics = new Map();
|
|
2869
|
+
pi.on("tool_result", (event) => {
|
|
2870
|
+
if (event.toolName !== "workflow" || !event.isError)
|
|
2871
|
+
return;
|
|
2872
|
+
const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
|
|
2873
|
+
if (!diagnostic)
|
|
2874
|
+
return;
|
|
2875
|
+
pendingFailureDiagnostics.delete(event.toolCallId);
|
|
2876
|
+
return { content: [{ type: "text", text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
|
|
2877
|
+
});
|
|
2878
|
+
const liveActivities = new Map();
|
|
2879
|
+
const setLiveActivity = (runId, agentId, activity) => {
|
|
2880
|
+
const activities = liveActivities.get(runId);
|
|
2881
|
+
if (activity) {
|
|
2882
|
+
if (activities)
|
|
2883
|
+
activities.set(agentId, activity);
|
|
2884
|
+
else
|
|
2885
|
+
liveActivities.set(runId, new Map([[agentId, activity]]));
|
|
2886
|
+
}
|
|
2887
|
+
else {
|
|
2888
|
+
activities?.delete(agentId);
|
|
2889
|
+
if (activities?.size === 0)
|
|
2890
|
+
liveActivities.delete(runId);
|
|
2891
|
+
}
|
|
2892
|
+
};
|
|
2893
|
+
const withLiveActivities = (run) => {
|
|
2894
|
+
const activities = liveActivities.get(run.id);
|
|
2895
|
+
return activities?.size ? { ...run, agents: run.agents.map((agent) => {
|
|
2896
|
+
const activity = activities.get(agent.id);
|
|
2897
|
+
return activity ? { ...agent, activity } : agent;
|
|
2898
|
+
}) } : run;
|
|
2899
|
+
};
|
|
2402
2900
|
const conversationLocks = new Set();
|
|
2403
2901
|
const terminalRunStates = new Map();
|
|
2404
2902
|
let sessionLease;
|
|
@@ -2429,10 +2927,39 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2429
2927
|
const persistWorktree = async (store, metadata, owner) => {
|
|
2430
2928
|
const existing = (await store.worktrees()).some((worktree) => worktree.owner === owner);
|
|
2431
2929
|
const worktree = await store.worktree(owner);
|
|
2432
|
-
if (!existing)
|
|
2930
|
+
if (!existing && await store.ownsWorktree(owner))
|
|
2433
2931
|
await eventPublisher.worktree(store, metadata, worktree);
|
|
2434
2932
|
return worktree;
|
|
2435
2933
|
};
|
|
2934
|
+
const resolveWorktree = async (store, metadata, owner) => {
|
|
2935
|
+
const run = runs.get(store.runId);
|
|
2936
|
+
if (!run)
|
|
2937
|
+
fail("INTERNAL_ERROR", `Unknown production run: ${store.runId}`);
|
|
2938
|
+
await run.lifecycle.enter();
|
|
2939
|
+
try {
|
|
2940
|
+
const worktree = await persistWorktree(store, metadata, owner);
|
|
2941
|
+
return { path: worktree.path, branch: worktree.branch };
|
|
2942
|
+
}
|
|
2943
|
+
finally {
|
|
2944
|
+
await run.lifecycle.leave();
|
|
2945
|
+
}
|
|
2946
|
+
};
|
|
2947
|
+
const shellForRun = async (store, metadata, lifecycle, command, options, signal, identity) => {
|
|
2948
|
+
await lifecycle.enter();
|
|
2949
|
+
try {
|
|
2950
|
+
const path = shellIdentityPath(identity);
|
|
2951
|
+
const replayed = await store.replay(path);
|
|
2952
|
+
if (replayed)
|
|
2953
|
+
return readShellResult(replayed.value);
|
|
2954
|
+
const cwd = identity.worktreeOwner ? (await persistWorktree(store, metadata, identity.worktreeOwner)).cwd : store.cwd;
|
|
2955
|
+
const result = await executeShellCommand(command, options, signal, cwd);
|
|
2956
|
+
await store.complete(path, result);
|
|
2957
|
+
return result;
|
|
2958
|
+
}
|
|
2959
|
+
finally {
|
|
2960
|
+
await lifecycle.leave();
|
|
2961
|
+
}
|
|
2962
|
+
};
|
|
2436
2963
|
const lifecycleFor = (store, state, budget, metadata) => new RunLifecycle(state, async (next, previous, reason) => {
|
|
2437
2964
|
if (next !== "pausing")
|
|
2438
2965
|
budget.transition(next);
|
|
@@ -2443,7 +2970,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2443
2970
|
return nextRun;
|
|
2444
2971
|
});
|
|
2445
2972
|
await eventPublisher.runState(store, metadata, previous, next, reason);
|
|
2446
|
-
runs.get(store.runId)?.update?.(workflowToolUpdate(persisted));
|
|
2973
|
+
runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2447
2974
|
});
|
|
2448
2975
|
const scheduler = new FairAgentScheduler(async ({ id, runId, parentId, prompt, options, signal, setSteer }) => {
|
|
2449
2976
|
const run = runs.get(runId);
|
|
@@ -2464,7 +2991,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2464
2991
|
}
|
|
2465
2992
|
if (!runState.agents.some((agent) => agent.id === id))
|
|
2466
2993
|
return;
|
|
2467
|
-
|
|
2994
|
+
setLiveActivity(runId, id, progress.activity);
|
|
2995
|
+
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
2468
2996
|
};
|
|
2469
2997
|
const onAttempt = async (attempt) => {
|
|
2470
2998
|
await scheduler.flush();
|
|
@@ -2475,15 +3003,16 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2475
3003
|
const active = (await run.store.load()).run;
|
|
2476
3004
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, active.agents);
|
|
2477
3005
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
2478
|
-
run.update?.(workflowToolUpdate(persisted));
|
|
3006
|
+
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2479
3007
|
};
|
|
2480
|
-
const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, onProgress, onAttempt, budget, ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.thinking ? { thinking: options.thinking } : {}), ...(options.role ? { role: options.role } : {}), ...(options.role ? {} : { tools: options.tools }), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}), ...(options.conversation ? { conversation: options.conversation } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, thinking) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
|
|
3008
|
+
const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.thinking ? { thinking: options.thinking } : {}), ...(options.role ? { role: options.role } : {}), ...(options.role ? {} : { tools: options.tools }), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}), ...(options.conversation ? { conversation: options.conversation } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, thinking) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
|
|
2481
3009
|
const before = (await run.store.load()).run;
|
|
2482
3010
|
await persistAgentAttempts(run.store, id, result.attempts);
|
|
2483
3011
|
const completed = (await run.store.load()).run;
|
|
2484
3012
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, completed.agents);
|
|
2485
3013
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
2486
|
-
|
|
3014
|
+
setLiveActivity(runId, id);
|
|
3015
|
+
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2487
3016
|
return result.value;
|
|
2488
3017
|
}
|
|
2489
3018
|
catch (error) {
|
|
@@ -2495,7 +3024,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2495
3024
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, failed.agents);
|
|
2496
3025
|
}
|
|
2497
3026
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
2498
|
-
|
|
3027
|
+
setLiveActivity(runId, id);
|
|
3028
|
+
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2499
3029
|
throw error;
|
|
2500
3030
|
}
|
|
2501
3031
|
}, 16, async (runId, ownership) => {
|
|
@@ -2522,8 +3052,23 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2522
3052
|
return { ...current, agents };
|
|
2523
3053
|
});
|
|
2524
3054
|
await eventPublisher.agentStates(run.store, run.metadata, previousAgents, runState.agents);
|
|
2525
|
-
run.update?.(workflowToolUpdate(runState));
|
|
3055
|
+
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
2526
3056
|
});
|
|
3057
|
+
const cleanupTerminalRun = async (runId) => {
|
|
3058
|
+
const run = runs.get(runId);
|
|
3059
|
+
if (!run || !["completed", "failed", "stopped"].includes(run.lifecycle.state))
|
|
3060
|
+
return;
|
|
3061
|
+
await scheduler.cancelRun(runId);
|
|
3062
|
+
await scheduler.flush();
|
|
3063
|
+
if (runs.get(runId) !== run)
|
|
3064
|
+
return;
|
|
3065
|
+
scheduler.removeRun(runId);
|
|
3066
|
+
terminalRunStates.set(runId, run.lifecycle.state);
|
|
3067
|
+
run.checkpointResolvers.clear();
|
|
3068
|
+
liveActivities.delete(runId);
|
|
3069
|
+
eventPublisher.removeRun(runId);
|
|
3070
|
+
runs.delete(runId);
|
|
3071
|
+
};
|
|
2527
3072
|
const stopWorkflowRun = async (runId) => {
|
|
2528
3073
|
const run = runs.get(runId);
|
|
2529
3074
|
const terminalState = terminalRunStates.get(runId);
|
|
@@ -2537,6 +3082,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2537
3082
|
run.execution?.cancel();
|
|
2538
3083
|
await scheduler.cancelRun(run.store.runId);
|
|
2539
3084
|
await scheduler.flush();
|
|
3085
|
+
await cleanupTerminalRun(runId);
|
|
2540
3086
|
return { runId, state: "stopped", stopped: true };
|
|
2541
3087
|
};
|
|
2542
3088
|
const answerCheckpoint = async (runId, name, approved, silent = false) => {
|
|
@@ -2569,18 +3115,18 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2569
3115
|
return undefined;
|
|
2570
3116
|
await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
|
|
2571
3117
|
const result = await applyBudgetDecision(request, approved);
|
|
2572
|
-
run.budgetResolvers.get(proposalId)?.(result);
|
|
2573
|
-
run.budgetResolvers.delete(proposalId);
|
|
2574
3118
|
if (!silent)
|
|
2575
3119
|
deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
|
|
2576
3120
|
return result;
|
|
2577
3121
|
};
|
|
2578
|
-
const checkpointBridge = (runId, store, metadata, foreground, ui) => {
|
|
3122
|
+
const checkpointBridge = (runId, store, metadata, foreground, ui, headless = false) => {
|
|
2579
3123
|
const checkpointCounters = new Map();
|
|
2580
3124
|
return async (raw, signal) => {
|
|
2581
3125
|
const input = validateCheckpoint(raw);
|
|
2582
3126
|
const label = nextNamedOccurrence(checkpointCounters, input.name);
|
|
2583
3127
|
const path = operationPath("checkpoint", label);
|
|
3128
|
+
if (headless)
|
|
3129
|
+
fail("RESUME_INCOMPATIBLE", "Headless CLI checkpoints are unsupported");
|
|
2584
3130
|
if (foreground && !ui?.select)
|
|
2585
3131
|
fail("RESUME_INCOMPATIBLE", "Foreground checkpoints require UI");
|
|
2586
3132
|
const alreadyAwaiting = (await store.awaitingCheckpoints()).some((checkpoint) => checkpoint.path === path);
|
|
@@ -2671,14 +3217,17 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2671
3217
|
return;
|
|
2672
3218
|
const catalog = registry.catalog();
|
|
2673
3219
|
const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0;
|
|
2674
|
-
if (!catalog.functions.length && !catalog.variables.length && !
|
|
3220
|
+
if (!catalog.functions.length && !catalog.variables.length && !hasAliases)
|
|
2675
3221
|
return;
|
|
2676
3222
|
pi.registerTool({
|
|
2677
3223
|
name: "workflow_catalog",
|
|
2678
3224
|
label: "Workflow Catalog",
|
|
2679
|
-
description: "List
|
|
2680
|
-
parameters: Type.Object({}, { additionalProperties: false }),
|
|
2681
|
-
async execute(
|
|
3225
|
+
description: "List reusable workflow functions, variables, and model aliases, or load one entry in full",
|
|
3226
|
+
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function or variable name for full detail" })) }, { additionalProperties: false }),
|
|
3227
|
+
async execute(_id, params = {}) {
|
|
3228
|
+
const result = params.name === undefined ? registry.catalogIndex() : registry.catalogDetail(params.name);
|
|
3229
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
3230
|
+
}
|
|
2682
3231
|
});
|
|
2683
3232
|
catalogRegistered = true;
|
|
2684
3233
|
};
|
|
@@ -2688,7 +3237,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2688
3237
|
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
2689
3238
|
if (missing)
|
|
2690
3239
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
2691
|
-
const settingsPath = workflowSettingsPath();
|
|
3240
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2692
3241
|
const currentSettings = loadSettings(settingsPath);
|
|
2693
3242
|
resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath);
|
|
2694
3243
|
const currentAliases = currentSettings.modelAliases ?? {};
|
|
@@ -2702,7 +3251,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2702
3251
|
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2703
3252
|
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
|
|
2704
3253
|
await run.store.saveSnapshot(snapshot);
|
|
2705
|
-
run.executor = new WorkflowAgentExecutor({ cwd: run.store.cwd, model: run.model, tools: new Set(snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: resumeModels, knownModels: resumeModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath) }, createSession);
|
|
3254
|
+
run.executor = new WorkflowAgentExecutor({ cwd: run.store.cwd, agentDir: extensionAgentDir, model: run.model, tools: new Set(snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: resumeModels, knownModels: resumeModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath) }, createSession);
|
|
2706
3255
|
run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
|
|
2707
3256
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
2708
3257
|
if (drift.length)
|
|
@@ -2710,6 +3259,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2710
3259
|
};
|
|
2711
3260
|
const coldResumeRun = async (run, hasUI, ui, trustedProject, context) => {
|
|
2712
3261
|
const loaded = await run.store.load();
|
|
3262
|
+
await run.store.validateBorrowedWorktrees();
|
|
2713
3263
|
if (loaded.snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION)
|
|
2714
3264
|
throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
|
|
2715
3265
|
if (loaded.snapshot.roles === undefined)
|
|
@@ -2723,7 +3273,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2723
3273
|
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
2724
3274
|
if (missing)
|
|
2725
3275
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
2726
|
-
const settingsPath = workflowSettingsPath();
|
|
3276
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2727
3277
|
const currentSettings = loadSettings(settingsPath);
|
|
2728
3278
|
resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
|
|
2729
3279
|
const currentAliases = currentSettings.modelAliases ?? {};
|
|
@@ -2736,10 +3286,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2736
3286
|
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
2737
3287
|
const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2738
3288
|
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2739
|
-
|
|
3289
|
+
const script = launchScriptForSnapshot(loaded.snapshot, registry);
|
|
3290
|
+
preflight(script, { models: resumeModels, tools: active, agentTypes: new Set(loaded.snapshot.agentTypes), modelAliases: resumeAliases, knownModels: resumeModels, settingsPath, skipModelAvailability: true }, loaded.snapshot.schemas, loaded.snapshot.metadata);
|
|
2740
3291
|
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
|
|
2741
3292
|
await run.store.saveSnapshot(snapshot);
|
|
2742
|
-
run.executor = new WorkflowAgentExecutor({ cwd: run.store.cwd, model: run.model, tools: new Set(snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: resumeModels, knownModels: resumeModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath) }, createSession);
|
|
3293
|
+
run.executor = new WorkflowAgentExecutor({ cwd: run.store.cwd, agentDir: extensionAgentDir, model: run.model, tools: new Set(snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: resumeModels, knownModels: resumeModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath) }, createSession);
|
|
2743
3294
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
2744
3295
|
if (drift.length)
|
|
2745
3296
|
await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
@@ -2756,14 +3307,15 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2756
3307
|
if (!["completed", "failed", "stopped"].includes(run.lifecycle.state)) {
|
|
2757
3308
|
await run.lifecycle.terminal("failed", typed.code).catch(() => undefined);
|
|
2758
3309
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, error: { code: typed.code, message: typed.message } }));
|
|
2759
|
-
await eventPublisher.runFailed(run.store, run.metadata, typed,
|
|
3310
|
+
await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
|
|
2760
3311
|
run.update?.(workflowToolUpdate(persisted));
|
|
2761
3312
|
}
|
|
3313
|
+
await cleanupTerminalRun(run.store.runId);
|
|
2762
3314
|
throw typed;
|
|
2763
3315
|
}
|
|
2764
3316
|
await scheduler.cancelRun(run.store.runId);
|
|
2765
3317
|
await run.lifecycle.resume();
|
|
2766
|
-
const execution = runWorkflow(
|
|
3318
|
+
const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: async (prompt, options, signal, identity) => {
|
|
2767
3319
|
await run.lifecycle.enter();
|
|
2768
3320
|
const conversationId = identity.conversation ? conversationIdentityPath(identity) : undefined;
|
|
2769
3321
|
const conversationLock = conversationId ? `${run.store.runId}:${conversationId}` : "";
|
|
@@ -2810,7 +3362,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2810
3362
|
conversationLocks.delete(conversationLock);
|
|
2811
3363
|
await run.lifecycle.leave();
|
|
2812
3364
|
}
|
|
2813
|
-
}, checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, false, hasUI ? ui : undefined), phase: async (phase) => { await run.lifecycle.enter(); try {
|
|
3365
|
+
}, worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, false, hasUI ? ui : undefined), phase: async (phase) => { await run.lifecycle.enter(); try {
|
|
2814
3366
|
let previousPhase;
|
|
2815
3367
|
const persisted = await persistRunState(run.store, run.metadata, (current) => { previousPhase = current.phase; return { ...current, phase }; });
|
|
2816
3368
|
await eventPublisher.phase(run.store, run.metadata, previousPhase, phase);
|
|
@@ -2826,7 +3378,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2826
3378
|
throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
|
|
2827
3379
|
const resultPath = await run.store.saveResult(value);
|
|
2828
3380
|
await run.lifecycle.terminal("completed", "completed");
|
|
2829
|
-
await eventPublisher.runCompleted(run.store, run.metadata, resultPath
|
|
3381
|
+
await eventPublisher.runCompleted(run.store, run.metadata, resultPath);
|
|
2830
3382
|
return { value, resultPath };
|
|
2831
3383
|
}).catch(async (error) => {
|
|
2832
3384
|
await scheduler.flush();
|
|
@@ -2835,11 +3387,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2835
3387
|
await run.lifecycle.terminal(typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
2836
3388
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), error: { code: typed.code, message: typed.message } }));
|
|
2837
3389
|
const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
|
|
2838
|
-
await eventPublisher.runFailed(run.store, run.metadata, typed,
|
|
3390
|
+
await eventPublisher.runFailed(run.store, run.metadata, typed, state);
|
|
2839
3391
|
run.update?.(workflowToolUpdate(persisted));
|
|
2840
3392
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
|
|
2841
|
-
deliverFailure(pi, run.metadata
|
|
2842
|
-
});
|
|
3393
|
+
deliverFailure(pi, createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted));
|
|
3394
|
+
}).finally(() => cleanupTerminalRun(run.store.runId));
|
|
3395
|
+
run.completion = completion;
|
|
2843
3396
|
void completion;
|
|
2844
3397
|
};
|
|
2845
3398
|
const applyBudgetDecision = async (request, approved) => {
|
|
@@ -2875,18 +3428,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2875
3428
|
if (budgetRelaxed(currentBudget, nextBudget)) {
|
|
2876
3429
|
const proposalId = randomUUID();
|
|
2877
3430
|
const request = { kind: "budget", proposalId, runId, consumed: usage, previous: currentBudget ?? {}, proposed: nextBudget ?? {}, budgetVersion: loaded.run.budgetVersion ?? 1 };
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
await run.store.requestWorkflowDecision(request);
|
|
2881
|
-
await appendBudgetDecisionEvent(run, request, "adjustment_requested");
|
|
2882
|
-
}
|
|
2883
|
-
catch (error) {
|
|
2884
|
-
run.budgetResolvers.delete(proposalId);
|
|
2885
|
-
throw error;
|
|
2886
|
-
}
|
|
3431
|
+
await run.store.requestWorkflowDecision(request);
|
|
3432
|
+
await appendBudgetDecisionEvent(run, request, "adjustment_requested");
|
|
2887
3433
|
deliver(pi, budgetDecisionDelivery(run.metadata, request));
|
|
2888
|
-
|
|
2889
|
-
return { state: decisionResult.state };
|
|
3434
|
+
return { state: "awaiting_approval", proposalId };
|
|
2890
3435
|
}
|
|
2891
3436
|
const changed = JSON.stringify(currentBudget ?? {}) !== JSON.stringify(nextBudget ?? {});
|
|
2892
3437
|
if (changed) {
|
|
@@ -2955,14 +3500,16 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2955
3500
|
const lifecycle = lifecycleFor(store, loaded.run.state, budgetRuntime, loaded.snapshot.metadata);
|
|
2956
3501
|
const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
2957
3502
|
const roleDefinitions = loaded.snapshot.roles ?? {};
|
|
2958
|
-
|
|
3503
|
+
const abortController = new AbortController();
|
|
3504
|
+
const providerErrorRecovery = createProviderErrorRecovery(ctx, new Set(loaded.snapshot.models), () => { abortController.abort(); });
|
|
3505
|
+
runs.set(runId, { executor: new WorkflowAgentExecutor({ cwd: ctx.cwd, agentDir: extensionAgentDir, model, tools: new Set(loaded.snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: new Set(loaded.snapshot.models), knownModels: new Set(loaded.snapshot.models), ...(loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ? { modelAliases: loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases } : {}), ...(loaded.snapshot.settingsPath ? { settingsPath: loaded.snapshot.settingsPath } : {}), agentDefinitions: roleDefinitions, runStore: store, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(store.cwd, projectTrusted(ctx), loaded.snapshot.settingsPath ?? workflowSettingsPath(extensionAgentDir)) }, createSession), store, metadata: loaded.snapshot.metadata, model, lifecycle, budget: budgetRuntime, abortController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) });
|
|
2959
3506
|
for (const checkpoint of await store.awaitingCheckpoints())
|
|
2960
3507
|
deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
|
|
2961
3508
|
for (const decision of await store.pendingWorkflowDecisions())
|
|
2962
3509
|
deliver(pi, budgetDecisionDelivery(loaded.snapshot.metadata, decision));
|
|
2963
3510
|
scheduler.restoreRun(runId, loaded.snapshot.settings.concurrency, loaded.snapshot.identityVersion === LAUNCH_SNAPSHOT_IDENTITY_VERSION ? await store.loadOwnership() : [], () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
2964
3511
|
}
|
|
2965
|
-
const resumeSelect = ctx.ui?.select;
|
|
3512
|
+
const resumeSelect = uiHostCapabilities(ctx.ui)?.select;
|
|
2966
3513
|
if (ctx.hasUI && resumeSelect) {
|
|
2967
3514
|
const interrupted = [...runs.values()].filter((r) => r.lifecycle.state === "interrupted");
|
|
2968
3515
|
if (interrupted.length > 0) {
|
|
@@ -2992,7 +3539,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2992
3539
|
pi.on("before_agent_start", (event, ctx) => {
|
|
2993
3540
|
if (!pi.getActiveTools().includes("workflow"))
|
|
2994
3541
|
return;
|
|
2995
|
-
const roles = Object.entries(loadAgentDefinitions(ctx.cwd,
|
|
3542
|
+
const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx))).filter(([, definition]) => definition.description);
|
|
2996
3543
|
if (!roles.length)
|
|
2997
3544
|
return;
|
|
2998
3545
|
const content = `Workflow role descriptions:\n${roles.map(([name, definition]) => `- \`${name}\`: ${String(definition.description)}`).join("\n")}`;
|
|
@@ -3004,9 +3551,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3004
3551
|
description: WORKFLOW_TOOL_DESCRIPTION,
|
|
3005
3552
|
promptSnippet: WORKFLOW_TOOL_PROMPT_SNIPPET,
|
|
3006
3553
|
parameters: WORKFLOW_TOOL_PARAMETERS,
|
|
3007
|
-
async execute(
|
|
3554
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
3008
3555
|
try {
|
|
3009
|
-
const
|
|
3556
|
+
const headless = object(ctx) && ctx.headless === true;
|
|
3557
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
3010
3558
|
const defaults = loadSettings(settingsPath);
|
|
3011
3559
|
if (!ctx.model)
|
|
3012
3560
|
throw new WorkflowError("UNKNOWN_MODEL", "A launching model is required");
|
|
@@ -3014,7 +3562,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3014
3562
|
const budget = validateBudget(params.budget);
|
|
3015
3563
|
const rootModel = { provider: ctx.model.provider, model: ctx.model.id, thinking: pi.getThinkingLevel() };
|
|
3016
3564
|
const rootModelName = `${rootModel.provider}/${rootModel.model}`;
|
|
3017
|
-
const modelRegistry = ctx.modelRegistry;
|
|
3565
|
+
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
3018
3566
|
const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? [ctx.model]).map((model) => `${model.provider}/${model.id}`));
|
|
3019
3567
|
knownModels.add(rootModelName);
|
|
3020
3568
|
const availableModels = knownModels;
|
|
@@ -3022,8 +3570,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3022
3570
|
const trustedProject = projectTrusted(ctx);
|
|
3023
3571
|
if (typeof ctx.cwd === "string")
|
|
3024
3572
|
resolveAgentResourcePolicy(ctx.cwd, trustedProject, settingsPath);
|
|
3025
|
-
const validated = validateWorkflowLaunchWithRegistry(params, { cwd: ctx.cwd, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases: defaults.modelAliases ?? {}, knownModels, settingsPath }, registry);
|
|
3026
|
-
const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames } = validated;
|
|
3573
|
+
const validated = validateWorkflowLaunchWithRegistry({ ...params, args: params.args }, { cwd: ctx.cwd, agentDir: extensionAgentDir, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases: defaults.modelAliases ?? {}, knownModels, settingsPath }, registry);
|
|
3574
|
+
const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, functionName } = validated;
|
|
3027
3575
|
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
3028
3576
|
const runId = randomUUID();
|
|
3029
3577
|
const args = (params.args ?? null);
|
|
@@ -3036,24 +3584,28 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3036
3584
|
const runContext = workflowRunContext(ctx.cwd, ctx.sessionManager.getSessionId(), runId, checked.metadata, args, runController.signal);
|
|
3037
3585
|
const variables = await resolveWorkflowVariables(runContext, runController, registry);
|
|
3038
3586
|
const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
|
|
3587
|
+
const parentRunId = params.parentRunId;
|
|
3588
|
+
if (parentRunId !== undefined)
|
|
3589
|
+
await store.validateParentRun(parentRunId);
|
|
3039
3590
|
const roles = Object.fromEntries(roleNames.map((role) => [role, agentDefinitions[role]]));
|
|
3040
3591
|
const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
|
|
3041
3592
|
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, defaults.modelAliases, knownModels, settingsPath)] : []; });
|
|
3042
3593
|
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
|
|
3043
|
-
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, ...(defaults.modelAliases ? { modelAliases: defaults.modelAliases } : {}), ...(budget ? { budget } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
|
|
3594
|
+
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, ...(functionName ? { launchKind: "function", functionName } : {}), ...(defaults.modelAliases ? { modelAliases: defaults.modelAliases } : {}), ...(budget ? { budget } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
|
|
3044
3595
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
3045
3596
|
const initialBudget = budgetRuntime.snapshot();
|
|
3046
|
-
await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", agents: [], nativeSessions: [], ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
|
|
3597
|
+
await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], nativeSessions: [], ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
|
|
3047
3598
|
const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
|
|
3048
3599
|
const background = !params.foreground;
|
|
3049
3600
|
const providerPause = async () => { if (background)
|
|
3050
3601
|
deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
3051
|
-
const
|
|
3052
|
-
|
|
3602
|
+
const providerErrorRecovery = createProviderErrorRecovery(ctx, availableModels, () => { runController.abort(); });
|
|
3603
|
+
const executor = new WorkflowAgentExecutor({ cwd: ctx.cwd, agentDir: extensionAgentDir, model: rootModel, tools: new Set(rootTools), availableModels, knownModels, modelAliases: defaults.modelAliases ?? {}, settingsPath, agentDefinitions, runStore: store, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(ctx.cwd, projectTrusted(ctx), settingsPath), runContext }, createSession);
|
|
3604
|
+
runs.set(runId, { executor, store, metadata: checked.metadata, model: rootModel, lifecycle, budget: budgetRuntime, abortController: runController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}), ...(params.foreground && onUpdate ? { update: onUpdate } : {}) });
|
|
3053
3605
|
if (params.foreground && onUpdate)
|
|
3054
3606
|
onUpdate(workflowToolUpdate((await store.load()).run));
|
|
3055
3607
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
3056
|
-
const execution = runWorkflow(script, args, withWorkflowFunctions({ agent: async (prompt, options, agentSignal, identity) => {
|
|
3608
|
+
const execution = runWorkflow(script, args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: async (prompt, options, agentSignal, identity) => {
|
|
3057
3609
|
await lifecycle.enter();
|
|
3058
3610
|
const conversationId = identity.conversation ? conversationIdentityPath(identity) : undefined;
|
|
3059
3611
|
const conversationLock = conversationId ? `${runId}:${conversationId}` : "";
|
|
@@ -3103,7 +3655,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3103
3655
|
conversationLocks.delete(conversationLock);
|
|
3104
3656
|
await lifecycle.leave();
|
|
3105
3657
|
}
|
|
3106
|
-
}, checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined), phase: async (phase) => {
|
|
3658
|
+
}, worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined, headless), phase: async (phase) => {
|
|
3107
3659
|
await lifecycle.enter();
|
|
3108
3660
|
try {
|
|
3109
3661
|
let previousPhase;
|
|
@@ -3116,35 +3668,46 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3116
3668
|
}
|
|
3117
3669
|
}, log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
|
|
3118
3670
|
runs.get(runId).execution = execution;
|
|
3119
|
-
await eventPublisher.runStarted(store, checked.metadata
|
|
3671
|
+
await eventPublisher.runStarted(store, checked.metadata);
|
|
3120
3672
|
const finish = execution.result.then(async (value) => {
|
|
3121
3673
|
await scheduler.flush();
|
|
3122
3674
|
if (budgetRuntime.hardExhausted)
|
|
3123
3675
|
throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
|
|
3124
3676
|
const resultPath = await store.saveResult(value);
|
|
3125
3677
|
await lifecycle.terminal("completed", "completed");
|
|
3126
|
-
await eventPublisher.runCompleted(store, checked.metadata, resultPath
|
|
3678
|
+
await eventPublisher.runCompleted(store, checked.metadata, resultPath);
|
|
3127
3679
|
return { value, resultPath };
|
|
3128
3680
|
}).catch(async (error) => {
|
|
3129
3681
|
await scheduler.flush();
|
|
3130
3682
|
const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
|
|
3131
3683
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(lifecycle.state))
|
|
3132
3684
|
await lifecycle.terminal(typed.code === "CANCELLED" ? "stopped" : typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
3133
|
-
await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot(), error: { code: typed.code, message: typed.message } }));
|
|
3685
|
+
const persisted = await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot(), error: { code: typed.code, message: typed.message } }));
|
|
3134
3686
|
const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
|
|
3135
|
-
await eventPublisher.runFailed(store, checked.metadata, typed,
|
|
3687
|
+
await eventPublisher.runFailed(store, checked.metadata, typed, state);
|
|
3688
|
+
const diagnostic = createWorkflowFailureDiagnostics(store, checked.metadata, typed, persisted);
|
|
3689
|
+
Object.defineProperty(typed, WORKFLOW_FAILURE_DIAGNOSTICS, { value: diagnostic });
|
|
3690
|
+
if (params.foreground)
|
|
3691
|
+
pendingFailureDiagnostics.set(toolCallId, diagnostic);
|
|
3136
3692
|
throw typed;
|
|
3137
3693
|
});
|
|
3138
|
-
|
|
3694
|
+
const completion = finish.finally(() => cleanupTerminalRun(runId));
|
|
3695
|
+
runs.get(runId).completion = completion;
|
|
3139
3696
|
if (background) {
|
|
3140
|
-
void
|
|
3697
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
3141
3698
|
deliver(pi, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
3142
|
-
}, (error) => {
|
|
3699
|
+
}, (error) => {
|
|
3700
|
+
const diagnostic = failureDiagnosticsFrom(error);
|
|
3701
|
+
if (diagnostic)
|
|
3702
|
+
deliverFailure(pi, diagnostic);
|
|
3703
|
+
else
|
|
3704
|
+
deliver(pi, `Workflow ${checked.metadata.name} failed: ${formatWorkflowFailure(error)}`);
|
|
3705
|
+
});
|
|
3143
3706
|
return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
3144
3707
|
}
|
|
3145
|
-
const { value } = await
|
|
3708
|
+
const { value } = await completion;
|
|
3146
3709
|
const run = (await store.load()).run;
|
|
3147
|
-
return { content: [{ type: "text", text: JSON.stringify(value) }], details: { runId, value, run } };
|
|
3710
|
+
return { content: [{ type: "text", text: JSON.stringify(value) }, { type: "text", text: `Workflow run ID: ${runId}` }], details: { runId, value, run } };
|
|
3148
3711
|
}
|
|
3149
3712
|
catch (error) {
|
|
3150
3713
|
throw mainAgentError(error);
|
|
@@ -3155,19 +3718,22 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3155
3718
|
},
|
|
3156
3719
|
renderResult(result, { isPartial }, _theme, context) {
|
|
3157
3720
|
const details = result.details;
|
|
3721
|
+
if (isWorkflowFailureDiagnostics(details))
|
|
3722
|
+
return textBlock(formatWorkflowFailureDiagnostics(details));
|
|
3723
|
+
const runDetails = details;
|
|
3158
3724
|
const state = context.state;
|
|
3159
|
-
if (
|
|
3725
|
+
if (runDetails?.run && isPartial && runDetails.run.state === "running" && !state.workflowSpinner) {
|
|
3160
3726
|
state.workflowSpinner = setInterval(context.invalidate, 80);
|
|
3161
3727
|
state.workflowSpinner.unref();
|
|
3162
3728
|
}
|
|
3163
|
-
else if ((!isPartial ||
|
|
3729
|
+
else if ((!isPartial || runDetails?.run?.state !== "running") && state.workflowSpinner) {
|
|
3164
3730
|
clearInterval(state.workflowSpinner);
|
|
3165
3731
|
delete state.workflowSpinner;
|
|
3166
3732
|
}
|
|
3167
|
-
if (
|
|
3168
|
-
return workflowProgressBlock(
|
|
3733
|
+
if (runDetails?.run)
|
|
3734
|
+
return workflowProgressBlock(runDetails.run);
|
|
3169
3735
|
const content = result.content[0];
|
|
3170
|
-
return textBlock(isPartial ? "Workflow starting..." :
|
|
3736
|
+
return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
|
|
3171
3737
|
},
|
|
3172
3738
|
});
|
|
3173
3739
|
pi.registerCommand("workflow", {
|
|
@@ -3198,7 +3764,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3198
3764
|
let stores = await loadStores();
|
|
3199
3765
|
const usage = "Usage: /workflow [doctor|model-aliases], or /workflow pause|resume|stop|approve|reject|delete <run-id> [checkpoint or proposal-id]. Use workflow_resume for budget patches.";
|
|
3200
3766
|
const setWorkflowStatus = (text) => {
|
|
3201
|
-
const setStatus = ctx.ui
|
|
3767
|
+
const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
|
|
3202
3768
|
setStatus?.call(ctx.ui, "workflow-stop", text);
|
|
3203
3769
|
};
|
|
3204
3770
|
const runAction = async (actionCommand, keepContext, status = setWorkflowStatus) => {
|
|
@@ -3254,7 +3820,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3254
3820
|
return keepContext ? "dashboard" : "done";
|
|
3255
3821
|
}
|
|
3256
3822
|
if (action === "adjust" && run?.lifecycle.state === "budget_exhausted") {
|
|
3257
|
-
const input = await ctx.ui
|
|
3823
|
+
const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
|
|
3258
3824
|
if (input === undefined)
|
|
3259
3825
|
return keepContext ? "dashboard" : "done";
|
|
3260
3826
|
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input));
|
|
@@ -3291,8 +3857,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3291
3857
|
}
|
|
3292
3858
|
};
|
|
3293
3859
|
const manageAliases = async () => {
|
|
3294
|
-
const settingsPath = workflowSettingsPath();
|
|
3295
|
-
const modelRegistry = ctx.modelRegistry;
|
|
3860
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
3861
|
+
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
3296
3862
|
const available = () => [...new Set((modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`))].sort();
|
|
3297
3863
|
const selectTarget = async () => {
|
|
3298
3864
|
const models = available();
|
|
@@ -3409,10 +3975,20 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3409
3975
|
const labels = navigatorRunLabels(sorted);
|
|
3410
3976
|
const terminalStates = new Set(["completed", "failed", "stopped"]);
|
|
3411
3977
|
const hasCompleted = sorted.some(({ loaded: { run } }) => run.state === "completed");
|
|
3412
|
-
const pickerOptions = [...labels, "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : [])];
|
|
3978
|
+
const pickerOptions = [...labels, ...(herdrPaneId() ? ["Inspect session in pane"] : []), "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : [])];
|
|
3413
3979
|
const runChoice = await ctx.ui.select("Workflows\n", pickerOptions);
|
|
3414
3980
|
if (!runChoice || runChoice === "Close")
|
|
3415
3981
|
return;
|
|
3982
|
+
if (runChoice === "Inspect session in pane") {
|
|
3983
|
+
try {
|
|
3984
|
+
await openHerdrPane({ action: "inspect", cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId() });
|
|
3985
|
+
ctx.ui.notify("Opened session inspector in pane.", "info");
|
|
3986
|
+
}
|
|
3987
|
+
catch (error) {
|
|
3988
|
+
ctx.ui.notify(`Cannot open session inspector in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
3989
|
+
}
|
|
3990
|
+
continue;
|
|
3991
|
+
}
|
|
3416
3992
|
if (runChoice === "Model aliases") {
|
|
3417
3993
|
await manageAliases();
|
|
3418
3994
|
stores = await loadStores();
|
|
@@ -3448,49 +4024,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3448
4024
|
ctx.ui.notify(`Failed to copy ${artifact}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3449
4025
|
}
|
|
3450
4026
|
};
|
|
3451
|
-
const openTranscript = async (transcript) => {
|
|
3452
|
-
try {
|
|
3453
|
-
const entries = SessionManager.open(transcript).buildContextEntries();
|
|
3454
|
-
if (ctx.mode !== "tui") {
|
|
3455
|
-
ctx.ui.notify(`Transcript: ${transcript}`, "info");
|
|
3456
|
-
return;
|
|
3457
|
-
}
|
|
3458
|
-
await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
3459
|
-
let offset = 0;
|
|
3460
|
-
let renderedLines = [];
|
|
3461
|
-
const viewport = () => Math.max(1, ((tui.terminal?.rows) ?? 24) - 3);
|
|
3462
|
-
const move = (delta) => { offset = Math.max(0, Math.min(Math.max(0, renderedLines.length - viewport()), offset + delta)); };
|
|
3463
|
-
return {
|
|
3464
|
-
render(width) {
|
|
3465
|
-
renderedLines = transcriptLines(entries).flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
|
|
3466
|
-
offset = Math.min(offset, Math.max(0, renderedLines.length - viewport()));
|
|
3467
|
-
return [theme.fg("accent", "Native Pi transcript"), ...renderedLines.slice(offset, offset + viewport()), "", theme.fg("dim", "↑↓/pgup/pgdn scroll · esc close")];
|
|
3468
|
-
},
|
|
3469
|
-
invalidate() { },
|
|
3470
|
-
handleInput(data) {
|
|
3471
|
-
if (keybindings.matches(data, "tui.select.up"))
|
|
3472
|
-
move(-1);
|
|
3473
|
-
else if (keybindings.matches(data, "tui.select.down"))
|
|
3474
|
-
move(1);
|
|
3475
|
-
else if (keybindings.matches(data, "tui.select.pageUp"))
|
|
3476
|
-
move(-viewport());
|
|
3477
|
-
else if (keybindings.matches(data, "tui.select.pageDown"))
|
|
3478
|
-
move(viewport());
|
|
3479
|
-
else if (keybindings.matches(data, "tui.editor.cursorLineStart"))
|
|
3480
|
-
offset = 0;
|
|
3481
|
-
else if (keybindings.matches(data, "tui.editor.cursorLineEnd"))
|
|
3482
|
-
offset = Math.max(0, renderedLines.length - viewport());
|
|
3483
|
-
else if (keybindings.matches(data, "tui.select.cancel"))
|
|
3484
|
-
done(undefined);
|
|
3485
|
-
tui.requestRender();
|
|
3486
|
-
},
|
|
3487
|
-
};
|
|
3488
|
-
}, { overlay: true });
|
|
3489
|
-
}
|
|
3490
|
-
catch (error) {
|
|
3491
|
-
ctx.ui.notify(`Cannot open transcript: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
3492
|
-
}
|
|
3493
|
-
};
|
|
3494
4027
|
const loadDashboard = async () => {
|
|
3495
4028
|
const loaded = await store.load();
|
|
3496
4029
|
const checkpoints = await store.awaitingCheckpoints();
|
|
@@ -3530,20 +4063,15 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3530
4063
|
actions.set("Refresh", "refresh");
|
|
3531
4064
|
else
|
|
3532
4065
|
actions.set("View script", "view-script");
|
|
3533
|
-
const transcripts = [...new Set([...loaded.run.agents.flatMap((agent) => (agent.attemptDetails ?? []).map((attempt) => attempt.sessionFile)), ...loaded.run.nativeSessions.map(({ sessionFile }) => sessionFile)])];
|
|
3534
4066
|
if (loaded.run.agents.length)
|
|
3535
4067
|
actions.set("Agents...", "agents");
|
|
3536
|
-
if (!loaded.run.agents.length && ctx.mode === "tui" && transcripts.length)
|
|
3537
|
-
actions.set("View transcript", "view-transcript");
|
|
3538
|
-
if (!loaded.run.agents.length && transcripts.length)
|
|
3539
|
-
actions.set("Transcript paths", "transcripts");
|
|
3540
4068
|
if (terminalStates.has(loaded.run.state))
|
|
3541
4069
|
add("Delete", "delete");
|
|
3542
4070
|
if (ctx.mode === "tui") {
|
|
3543
4071
|
addCopy("Copy run path", store.directory, "run path");
|
|
3544
4072
|
addCopy("Copy run ID", store.runId, "run ID");
|
|
3545
4073
|
}
|
|
3546
|
-
return { dashboard: formatNavigatorDashboard(loaded.run, checkpoints, worktrees), actions, copies, reviews,
|
|
4074
|
+
return { dashboard: formatNavigatorDashboard(loaded.run, checkpoints, worktrees), actions, copies, reviews, script: loaded.snapshot.script, agents: loaded.run.agents, worktrees, cwd: loaded.run.cwd };
|
|
3547
4075
|
};
|
|
3548
4076
|
const selectAgent = async (dashboard) => {
|
|
3549
4077
|
const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
|
|
@@ -3566,14 +4094,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3566
4094
|
const attempts = [...(selected.attemptDetails ?? [])].sort((left, right) => left.attempt - right.attempt);
|
|
3567
4095
|
const worktree = selected.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === selected.worktreeOwner) : undefined;
|
|
3568
4096
|
const actions = [
|
|
3569
|
-
...(attempts.length ? ["
|
|
4097
|
+
...(attempts.length && herdrPaneId() ? ["Fork as Pi session in pane"] : []),
|
|
3570
4098
|
...(worktree ? ["Copy branch", "Copy worktree path"] : []),
|
|
3571
4099
|
"Copy agent ID",
|
|
3572
4100
|
"Back",
|
|
3573
4101
|
];
|
|
3574
4102
|
const chooseAttempt = async () => {
|
|
3575
4103
|
const choices = attempts.map((attempt) => `Attempt ${String(attempt.attempt)}`);
|
|
3576
|
-
const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("
|
|
4104
|
+
const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("Fork attempts", [...choices, "Back"]);
|
|
3577
4105
|
const index = choice ? choices.indexOf(choice) : -1;
|
|
3578
4106
|
return index >= 0 ? attempts[index] : undefined;
|
|
3579
4107
|
};
|
|
@@ -3593,14 +4121,20 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3593
4121
|
await copyArtifact(worktree.path, "worktree path");
|
|
3594
4122
|
continue;
|
|
3595
4123
|
}
|
|
3596
|
-
if (action === "
|
|
4124
|
+
if (action === "Fork as Pi session in pane") {
|
|
3597
4125
|
const attempt = await chooseAttempt();
|
|
3598
4126
|
if (!attempt)
|
|
3599
4127
|
continue;
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
4128
|
+
const running = !SETTLED_AGENT_STATES.has(selected.state) && attempt.attempt === attempts.at(-1)?.attempt && !attempt.error;
|
|
4129
|
+
if (running && !await ctx.ui.confirm("Fork running attempt?", "This attempt is still running. The snapshot may end mid-turn and will not receive later updates. It opens read-only to avoid concurrent changes to the workflow agent's working directory. Continue?"))
|
|
4130
|
+
continue;
|
|
4131
|
+
try {
|
|
4132
|
+
await openHerdrPane({ action: "fork", cwd: worktree?.cwd ?? attempt.setup?.cwd ?? dashboard.cwd, original: attempt.sessionFile, ...(running ? { readOnly: true } : {}) });
|
|
4133
|
+
ctx.ui.notify("Forked Pi session in pane.", "info");
|
|
4134
|
+
}
|
|
4135
|
+
catch (error) {
|
|
4136
|
+
ctx.ui.notify(`Cannot fork Pi session in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
4137
|
+
}
|
|
3604
4138
|
}
|
|
3605
4139
|
}
|
|
3606
4140
|
};
|
|
@@ -3615,19 +4149,18 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3615
4149
|
let disposed = false;
|
|
3616
4150
|
let stopRequested = false;
|
|
3617
4151
|
let stopStatus;
|
|
3618
|
-
const terminalRows = () => Math.max(1, (tui
|
|
4152
|
+
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
3619
4153
|
const keyLabels = { up: "↑", down: "↓", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
|
|
3620
4154
|
const keyLabel = (binding, fallback) => {
|
|
3621
|
-
const
|
|
3622
|
-
const keys = getKeys?.call(keybindings, binding);
|
|
4155
|
+
const keys = keybindingKeys(keybindings, binding);
|
|
3623
4156
|
return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
|
|
3624
4157
|
};
|
|
3625
4158
|
const dashboardLayout = () => {
|
|
3626
4159
|
const rows = terminalRows();
|
|
3627
4160
|
const hintRows = rows >= 4 ? 1 : 0;
|
|
3628
|
-
const separatorRows = rows >=
|
|
4161
|
+
const separatorRows = rows >= 8 ? 1 : 0;
|
|
3629
4162
|
const available = Math.max(1, rows - hintRows - separatorRows);
|
|
3630
|
-
const actionViewport = Math.min(options.length, Math.max(1, Math.
|
|
4163
|
+
const actionViewport = Math.min(options.length, Math.max(1, Math.ceil(available / 2)));
|
|
3631
4164
|
return { rows, hintRows, separatorRows, actionViewport, dashboardViewport: available - actionViewport };
|
|
3632
4165
|
};
|
|
3633
4166
|
const updateDashboard = async (selectedOption) => {
|
|
@@ -3670,7 +4203,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3670
4203
|
void updateDashboard(selectedOption).catch(() => undefined).finally(() => { refreshing = false; });
|
|
3671
4204
|
}, 1000);
|
|
3672
4205
|
timer.unref();
|
|
3673
|
-
return {
|
|
4206
|
+
return borderWorkflowOverlay({
|
|
3674
4207
|
render(width) {
|
|
3675
4208
|
const dashboard = stopStatus ? `${view.dashboard}\n\n${stopStatus}` : view.dashboard;
|
|
3676
4209
|
const dashboardLines = truncateToVisualLines(theme.fg("accent", dashboard), Number.MAX_SAFE_INTEGER, width, 1).visualLines;
|
|
@@ -3717,7 +4250,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3717
4250
|
tui.requestRender();
|
|
3718
4251
|
},
|
|
3719
4252
|
dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
|
|
3720
|
-
};
|
|
4253
|
+
}, theme);
|
|
3721
4254
|
}, { overlay: true })
|
|
3722
4255
|
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Close"]);
|
|
3723
4256
|
if (!actionChoice || actionChoice === "Close")
|
|
@@ -3733,12 +4266,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3733
4266
|
const highlighted = highlightCode(view.script, "javascript");
|
|
3734
4267
|
let offset = 0;
|
|
3735
4268
|
let renderedLines = [];
|
|
3736
|
-
const viewport = () => Math.max(1, (
|
|
4269
|
+
const viewport = () => Math.max(1, tuiRows(tui) - 3 - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
3737
4270
|
const move = (delta) => {
|
|
3738
4271
|
const maxOffset = Math.max(0, renderedLines.length - viewport());
|
|
3739
4272
|
offset = Math.max(0, Math.min(maxOffset, offset + delta));
|
|
3740
4273
|
};
|
|
3741
|
-
return {
|
|
4274
|
+
return borderWorkflowOverlay({
|
|
3742
4275
|
render(width) {
|
|
3743
4276
|
renderedLines = highlighted.flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
|
|
3744
4277
|
const maxOffset = Math.max(0, renderedLines.length - viewport());
|
|
@@ -3764,24 +4297,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3764
4297
|
done(undefined);
|
|
3765
4298
|
tui.requestRender();
|
|
3766
4299
|
},
|
|
3767
|
-
};
|
|
3768
|
-
});
|
|
3769
|
-
continue;
|
|
3770
|
-
}
|
|
3771
|
-
if (actionChoice === "View transcript") {
|
|
3772
|
-
const transcript = await ctx.ui.select("Native Pi transcripts", [...view.transcripts, "Back"]);
|
|
3773
|
-
if (transcript && transcript !== "Back")
|
|
3774
|
-
await openTranscript(transcript);
|
|
3775
|
-
continue;
|
|
3776
|
-
}
|
|
3777
|
-
if (actionChoice === "Transcript paths") {
|
|
3778
|
-
const transcript = await ctx.ui.select("Native Pi transcript paths", [...view.transcripts, "Back"]);
|
|
3779
|
-
if (transcript && transcript !== "Back") {
|
|
3780
|
-
if (ctx.mode === "tui")
|
|
3781
|
-
await copyArtifact(transcript, "transcript path");
|
|
3782
|
-
else
|
|
3783
|
-
ctx.ui.notify(transcript, "info");
|
|
3784
|
-
}
|
|
4300
|
+
}, theme);
|
|
4301
|
+
}, { overlay: true, overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" } });
|
|
3785
4302
|
continue;
|
|
3786
4303
|
}
|
|
3787
4304
|
const copy = view.copies.get(actionChoice);
|
|
@@ -3799,11 +4316,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3799
4316
|
let offset = 0;
|
|
3800
4317
|
let renderedLines = [];
|
|
3801
4318
|
const layout = () => {
|
|
3802
|
-
const rows = Math.max(1, (
|
|
4319
|
+
const rows = Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
3803
4320
|
const compactControls = rows < 4;
|
|
3804
4321
|
const titleRows = rows >= 5 ? 1 : 0;
|
|
3805
4322
|
const hintRows = rows >= 8 ? 1 : 0;
|
|
3806
|
-
const separatorRows = rows >= 8 ?
|
|
4323
|
+
const separatorRows = rows >= 8 ? 1 : 0;
|
|
3807
4324
|
const controlRows = compactControls ? 1 : options.length;
|
|
3808
4325
|
const contentViewport = Math.max(0, rows - titleRows - hintRows - separatorRows - controlRows);
|
|
3809
4326
|
return { rows, compactControls, titleRows, hintRows, separatorRows, contentViewport };
|
|
@@ -3812,7 +4329,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3812
4329
|
const maxOffset = Math.max(0, renderedLines.length - layout().contentViewport);
|
|
3813
4330
|
offset = Math.max(0, Math.min(maxOffset, offset + delta));
|
|
3814
4331
|
};
|
|
3815
|
-
return {
|
|
4332
|
+
return borderWorkflowOverlay({
|
|
3816
4333
|
render(width) {
|
|
3817
4334
|
renderedLines = truncateToVisualLines(formatCheckpointReview(checkpoint), Number.MAX_SAFE_INTEGER, width, 0).visualLines;
|
|
3818
4335
|
const currentLayout = layout();
|
|
@@ -3827,7 +4344,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3827
4344
|
...renderedLines.slice(offset, offset + currentLayout.contentViewport),
|
|
3828
4345
|
...(currentLayout.separatorRows ? [""] : []),
|
|
3829
4346
|
...controls,
|
|
3830
|
-
...(currentLayout.separatorRows ? [""] : []),
|
|
3831
4347
|
...(currentLayout.hintRows ? [hint] : []),
|
|
3832
4348
|
];
|
|
3833
4349
|
},
|
|
@@ -3847,8 +4363,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3847
4363
|
done(undefined);
|
|
3848
4364
|
tui.requestRender();
|
|
3849
4365
|
},
|
|
3850
|
-
};
|
|
3851
|
-
});
|
|
4366
|
+
}, theme);
|
|
4367
|
+
}, { overlay: true, overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" } });
|
|
3852
4368
|
if (decision) {
|
|
3853
4369
|
const accepted = await answerCheckpoint(store.runId, checkpoint.name, decision === "Approve", true);
|
|
3854
4370
|
if (!accepted)
|