pi-extensible-workflows 1.0.1 → 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 +130 -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 -29
- package/dist/src/herdr.d.ts +12 -0
- package/dist/src/herdr.js +74 -0
- package/dist/src/index.d.ts +101 -24
- package/dist/src/index.js +669 -212
- package/dist/src/persistence.d.ts +26 -0
- package/dist/src/persistence.js +186 -12
- package/dist/src/session-inspector.d.ts +1 -0
- package/dist/src/session-inspector.js +3 -0
- package/dist/src/workflow-evals.js +2 -1
- package/package.json +4 -3
- package/skills/pi-extensible-workflows/SKILL.md +48 -64
- package/src/agent-execution.ts +95 -20
- package/src/cli.ts +418 -6
- package/src/doctor.ts +13 -32
- package/src/herdr.ts +73 -0
- package/src/index.ts +595 -193
- package/src/persistence.ts +169 -12
- package/src/session-inspector.ts +4 -0
- package/src/workflow-evals.ts +2 -1
package/dist/src/index.js
CHANGED
|
@@ -1,21 +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
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 { herdrPaneId, openHerdrPane } from "./herdr.js";
|
|
15
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 WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"];
|
|
19
|
+
export const WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree", "shell"];
|
|
19
20
|
export const WORKFLOW_RUN_STARTED_EVENT = "workflow:run-started";
|
|
20
21
|
export const WORKFLOW_RUN_RESUMED_EVENT = "workflow:run-resumed";
|
|
21
22
|
export const WORKFLOW_RUN_STATE_CHANGED_EVENT = "workflow:run-state-changed";
|
|
@@ -29,10 +30,10 @@ export const WORKFLOW_WORKTREE_CREATED_EVENT = "workflow:worktree-created";
|
|
|
29
30
|
const SETTLED_AGENT_STATES = new Set(["completed", "failed", "cancelled"]);
|
|
30
31
|
export const ERROR_CODES = [
|
|
31
32
|
"CONFIG_ERROR", "INVALID_SETTINGS", "INVALID_SYNTAX", "INVALID_METADATA", "DUPLICATE_NAME", "INVALID_SCHEMA", "UNKNOWN_MODEL", "UNKNOWN_TOOL", "UNKNOWN_AGENT_TYPE",
|
|
32
|
-
"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",
|
|
33
34
|
"CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
|
|
34
35
|
];
|
|
35
|
-
export const LAUNCH_SNAPSHOT_IDENTITY_VERSION =
|
|
36
|
+
export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 4;
|
|
36
37
|
export class WorkflowError extends Error {
|
|
37
38
|
code;
|
|
38
39
|
constructor(code, message) {
|
|
@@ -41,6 +42,7 @@ export class WorkflowError extends Error {
|
|
|
41
42
|
this.name = "WorkflowError";
|
|
42
43
|
}
|
|
43
44
|
}
|
|
45
|
+
const WORKFLOW_FAILURE_DIAGNOSTICS = Symbol("workflowFailureDiagnostics");
|
|
44
46
|
const WORKFLOW_AUTHORED_ERROR = Symbol("workflowAuthoredError");
|
|
45
47
|
function errorText(error) { return error && typeof error === "object" && typeof error.message === "string" ? error.message : error instanceof Error ? error.message : String(error); }
|
|
46
48
|
function errorCode(error) {
|
|
@@ -70,12 +72,13 @@ const WORKFLOW_ERROR_PROSE = {
|
|
|
70
72
|
INVALID_SCHEMA: (detail) => `The workflow schema is invalid: ${detail}.`,
|
|
71
73
|
REGISTRY_FROZEN: (detail) => `Workflow extension registration is closed: ${detail}.`,
|
|
72
74
|
GLOBAL_COLLISION: (detail) => `The workflow global name is already in use: ${detail}.`,
|
|
73
|
-
MISSING_WORKFLOW: (detail) => `The workflow
|
|
75
|
+
MISSING_WORKFLOW: (detail) => `The registered workflow function is unavailable: ${detail}.`,
|
|
74
76
|
UNKNOWN_MODEL: (detail) => `The workflow requested the unavailable model ${detail.replace(/^(?:Unknown model(?: for role [^:]+)?|Invalid model spec):\s*/, "")}.`,
|
|
75
77
|
UNKNOWN_TOOL: (detail) => `The workflow requested the unavailable tool ${detail.replace(/^Unknown tool:\s*/, "")}.`,
|
|
76
78
|
UNKNOWN_AGENT_TYPE: (detail) => `The workflow requested the unavailable agent role ${detail.replace(/^Unknown agent role:\s*/, "")}.`,
|
|
77
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}.`,
|
|
78
80
|
RPC_LIMIT_EXCEEDED: (detail) => `The workflow communication data exceeded its size limit: ${detail}.`,
|
|
81
|
+
SHELL_FAILED: (detail) => `The workflow shell command failed: ${detail}.`,
|
|
79
82
|
AGENT_TIMEOUT: (detail) => `The workflow agent timed out: ${detail}.`,
|
|
80
83
|
AGENT_FAILED: (detail) => `The workflow agent failed: ${detail}.`,
|
|
81
84
|
RESULT_INVALID: (detail) => `The workflow produced an invalid result: ${detail}.`,
|
|
@@ -778,6 +781,7 @@ function hasIdentifier(node, name) {
|
|
|
778
781
|
const INTERNAL_AGENT_NAME = "__pi_extensible_workflows_agent";
|
|
779
782
|
const INTERNAL_CONVERSATION_NAME = "__pi_extensible_workflows_conversation";
|
|
780
783
|
const INTERNAL_WORKTREE_NAME = "__pi_extensible_workflows_withWorktree";
|
|
784
|
+
const INTERNAL_SHELL_NAME = "__pi_extensible_workflows_shell";
|
|
781
785
|
function callHasTrailingComma(source, call) {
|
|
782
786
|
let previous;
|
|
783
787
|
let current;
|
|
@@ -798,12 +802,14 @@ function instrumentWorkflow(script) {
|
|
|
798
802
|
fail("INVALID_METADATA", `${INTERNAL_CONVERSATION_NAME} is reserved for workflow conversation instrumentation`);
|
|
799
803
|
if (hasIdentifier(program, INTERNAL_WORKTREE_NAME))
|
|
800
804
|
fail("INVALID_METADATA", `${INTERNAL_WORKTREE_NAME} is reserved for workflow withWorktree instrumentation`);
|
|
801
|
-
|
|
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));
|
|
802
808
|
const edits = calls.flatMap((call) => {
|
|
803
809
|
const callSite = `${String(call.start)}:${String(call.end)}`;
|
|
804
810
|
const hiddenArgument = call.arguments.length === 0 || callHasTrailingComma(body, call) ? "" : ", ";
|
|
805
811
|
return [
|
|
806
|
-
{ 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 },
|
|
807
813
|
{ start: call.end - 1, end: call.end - 1, text: `${hiddenArgument}${JSON.stringify(callSite)}` },
|
|
808
814
|
];
|
|
809
815
|
}).sort((left, right) => right.start - left.start);
|
|
@@ -942,6 +948,23 @@ function validateAgentOptions(value) {
|
|
|
942
948
|
fail("INVALID_METADATA", "Role agents must not specify model, thinking, or tools");
|
|
943
949
|
return value;
|
|
944
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
|
+
}
|
|
945
968
|
function staticValue(node) {
|
|
946
969
|
if (!node)
|
|
947
970
|
return { known: false };
|
|
@@ -1007,6 +1030,8 @@ export function inspectWorkflowScript(script) {
|
|
|
1007
1030
|
}
|
|
1008
1031
|
if (kind === "checkpoint")
|
|
1009
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 };
|
|
1010
1035
|
return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
|
|
1011
1036
|
});
|
|
1012
1037
|
}
|
|
@@ -1022,6 +1047,18 @@ function validateStaticAgentOptions(node, aliases = {}, knownModels, settingsPat
|
|
|
1022
1047
|
validateAgentOption(key, value.value, aliases, knownModels, settingsPath);
|
|
1023
1048
|
}
|
|
1024
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
|
+
}
|
|
1025
1062
|
function validateStaticWithWorktree(call) {
|
|
1026
1063
|
if (call.arguments.some((argument) => argument.type === "SpreadElement"))
|
|
1027
1064
|
return;
|
|
@@ -1036,13 +1073,12 @@ function validateStaticWithWorktree(call) {
|
|
|
1036
1073
|
fail("INVALID_METADATA", "withWorktree name must be a non-empty string");
|
|
1037
1074
|
}
|
|
1038
1075
|
}
|
|
1039
|
-
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"]);
|
|
1040
1077
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
1041
1078
|
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
1042
1079
|
export class WorkflowRegistry {
|
|
1043
1080
|
#extensions = new Set();
|
|
1044
1081
|
#globals = new Map();
|
|
1045
|
-
#workflows = new Map();
|
|
1046
1082
|
#hooks = new Map();
|
|
1047
1083
|
#frozen = false;
|
|
1048
1084
|
get frozen() { return this.#frozen; }
|
|
@@ -1050,14 +1086,15 @@ export class WorkflowRegistry {
|
|
|
1050
1086
|
register(extension) {
|
|
1051
1087
|
if (this.#frozen)
|
|
1052
1088
|
fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
|
|
1053
|
-
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())
|
|
1054
1092
|
fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
|
|
1055
1093
|
const functions = extension.functions ?? {};
|
|
1056
1094
|
const variables = extension.variables ?? {};
|
|
1057
|
-
const workflows = extension.workflows ?? {};
|
|
1058
1095
|
const agentSetupHooks = extension.agentSetupHooks ?? {};
|
|
1059
|
-
if (!object(functions) || !object(variables) || !object(
|
|
1060
|
-
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");
|
|
1061
1098
|
const names = [...Object.keys(functions), ...Object.keys(variables)];
|
|
1062
1099
|
if (new Set(names).size !== names.length)
|
|
1063
1100
|
fail("GLOBAL_COLLISION", "Global name collision inside extension");
|
|
@@ -1082,50 +1119,38 @@ export class WorkflowRegistry {
|
|
|
1082
1119
|
fail("INVALID_METADATA", `Invalid workflow variable: ${name}`);
|
|
1083
1120
|
validateSchema(variable.schema, `${name} schema`);
|
|
1084
1121
|
}
|
|
1085
|
-
for (const [name, workflow] of Object.entries(workflows)) {
|
|
1086
|
-
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())
|
|
1087
|
-
fail("INVALID_METADATA", `Invalid workflow script: ${name}`);
|
|
1088
|
-
if (this.#workflows.has(name))
|
|
1089
|
-
fail("DUPLICATE_NAME", `Reusable workflow already registered: ${name}`);
|
|
1090
|
-
parseWorkflow(workflow.script);
|
|
1091
|
-
}
|
|
1092
1122
|
for (const [name, hook] of Object.entries(agentSetupHooks)) {
|
|
1093
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)))
|
|
1094
1124
|
fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
|
|
1095
1125
|
if (this.#hooks.has(name))
|
|
1096
1126
|
fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
|
|
1097
1127
|
}
|
|
1098
|
-
const stored = deepFreeze({ ...extension, functions, variables,
|
|
1128
|
+
const stored = deepFreeze({ ...extension, functions, variables, agentSetupHooks });
|
|
1099
1129
|
this.#extensions.add(stored);
|
|
1100
1130
|
for (const name of names)
|
|
1101
1131
|
this.#globals.set(name, name);
|
|
1102
|
-
for (const [name, workflow] of Object.entries(workflows))
|
|
1103
|
-
this.#workflows.set(name, workflow);
|
|
1104
1132
|
for (const [name, hook] of Object.entries(agentSetupHooks))
|
|
1105
1133
|
this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
|
|
1106
1134
|
}
|
|
1107
|
-
|
|
1135
|
+
function(name) {
|
|
1108
1136
|
if (!IDENTIFIER.test(name))
|
|
1109
|
-
fail("MISSING_WORKFLOW", `Registered
|
|
1110
|
-
const
|
|
1111
|
-
if (!
|
|
1112
|
-
fail("MISSING_WORKFLOW", `
|
|
1113
|
-
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;
|
|
1114
1142
|
}
|
|
1115
|
-
|
|
1116
|
-
return Object.freeze(Object.fromEntries(this.#
|
|
1143
|
+
functions() {
|
|
1144
|
+
return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.entries(extension.functions ?? {}))));
|
|
1117
1145
|
}
|
|
1118
1146
|
catalog() {
|
|
1119
1147
|
const functions = [];
|
|
1120
1148
|
const variables = [];
|
|
1121
|
-
const workflows = [];
|
|
1122
1149
|
for (const extension of this.#extensions) {
|
|
1123
1150
|
for (const [name, fn] of Object.entries(extension.functions ?? {}))
|
|
1124
1151
|
functions.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: fn.description, input: structuredClone(fn.input), output: structuredClone(fn.output) });
|
|
1125
1152
|
for (const [name, variable] of Object.entries(extension.variables ?? {}))
|
|
1126
1153
|
variables.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: variable.description, schema: structuredClone(variable.schema) });
|
|
1127
|
-
for (const [name, workflow] of Object.entries(extension.workflows ?? {}))
|
|
1128
|
-
workflows.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: workflow.description });
|
|
1129
1154
|
}
|
|
1130
1155
|
let aliases;
|
|
1131
1156
|
try {
|
|
@@ -1135,15 +1160,28 @@ export class WorkflowRegistry {
|
|
|
1135
1160
|
aliases = undefined;
|
|
1136
1161
|
}
|
|
1137
1162
|
const sort = (left, right) => left.name.localeCompare(right.name);
|
|
1138
|
-
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}` } });
|
|
1139
1179
|
}
|
|
1140
1180
|
globals() {
|
|
1141
1181
|
return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.keys(extension.functions ?? {}).map((name) => [name, { name }]))));
|
|
1142
1182
|
}
|
|
1143
1183
|
async invokeFunction(name, input, context, path, journal) {
|
|
1144
|
-
const fn =
|
|
1145
|
-
if (!fn)
|
|
1146
|
-
fail("MISSING_WORKFLOW", `Workflow function is unavailable: ${name}`);
|
|
1184
|
+
const fn = this.function(name);
|
|
1147
1185
|
if (!object(input) || !jsonValue(input) || !Value.Check(fn.input, input))
|
|
1148
1186
|
fail("RESULT_INVALID", `Invalid input for ${name}`);
|
|
1149
1187
|
const replayed = journal.get(path);
|
|
@@ -1152,7 +1190,7 @@ export class WorkflowRegistry {
|
|
|
1152
1190
|
fail("RESULT_INVALID", `Invalid replay for ${name}`);
|
|
1153
1191
|
return structuredClone(replayed);
|
|
1154
1192
|
}
|
|
1155
|
-
const result = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, invoke: context.invoke, 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 }));
|
|
1156
1194
|
if (!jsonValue(result) || !Value.Check(fn.output, result))
|
|
1157
1195
|
fail("RESULT_INVALID", `Invalid output from ${name}`);
|
|
1158
1196
|
const stored = structuredClone(result);
|
|
@@ -1173,9 +1211,11 @@ function createWorkflowRegistryApi(registry) {
|
|
|
1173
1211
|
get frozen() { return registry.frozen; },
|
|
1174
1212
|
freeze: () => { registry.freeze(); },
|
|
1175
1213
|
register: (extension) => { registry.register(extension); },
|
|
1176
|
-
|
|
1177
|
-
|
|
1214
|
+
function: (name) => registry.function(name),
|
|
1215
|
+
functions: () => registry.functions(),
|
|
1178
1216
|
catalog: () => registry.catalog(),
|
|
1217
|
+
catalogIndex: () => registry.catalogIndex(),
|
|
1218
|
+
catalogDetail: (name) => registry.catalogDetail(name),
|
|
1179
1219
|
globals: () => registry.globals(),
|
|
1180
1220
|
invokeFunction: (...args) => registry.invokeFunction(...args),
|
|
1181
1221
|
variables: () => registry.variables(),
|
|
@@ -1196,25 +1236,28 @@ function loadingRegistry() { return workflowRegistryHost().api; }
|
|
|
1196
1236
|
beginWorkflowExtensionLoading();
|
|
1197
1237
|
export function registerWorkflowExtension(extension) { loadingRegistry().register(extension); }
|
|
1198
1238
|
export function workflowCatalog() { return loadingRegistry().catalog(); }
|
|
1199
|
-
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(); }
|
|
1200
1242
|
export function formatWorkflowPreview(args) {
|
|
1201
1243
|
const name = typeof args.name === "string" && args.name.trim() ? args.name.trim() : typeof args.workflow === "string" && args.workflow.trim() ? args.workflow : "workflow";
|
|
1202
1244
|
if (typeof args.script !== "string" || !args.script.trim())
|
|
1203
|
-
return `workflow ${name}${typeof args.workflow === "string" ? "\nRegistered
|
|
1245
|
+
return `workflow ${name}${typeof args.workflow === "string" ? "\nRegistered function" : ""}`;
|
|
1204
1246
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
1205
1247
|
}
|
|
1206
1248
|
export const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
1207
1249
|
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow";
|
|
1208
|
-
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.";
|
|
1209
1251
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
1210
1252
|
name: Type.Optional(Type.String({ description: "Workflow name for inline scripts" })),
|
|
1211
1253
|
description: Type.Optional(Type.String({ description: "Optional human-readable workflow description" })),
|
|
1212
1254
|
script: Type.Optional(Type.String({ description: "Immutable workflow source without metadata" })),
|
|
1213
|
-
workflow: Type.Optional(Type.String({ description: "Registered reusable
|
|
1255
|
+
workflow: Type.Optional(Type.String({ description: "Registered reusable function as an unqualified name" })),
|
|
1214
1256
|
args: Type.Optional(Type.Unknown({ description: "JSON-compatible workflow arguments" })),
|
|
1215
1257
|
foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of running in the background" })),
|
|
1216
1258
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16 })),
|
|
1217
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" })),
|
|
1218
1261
|
});
|
|
1219
1262
|
function hasDynamicAgentRole(node) {
|
|
1220
1263
|
if (!node)
|
|
@@ -1240,8 +1283,11 @@ export function preflight(script, capabilities, schemas = [], metadata = { name:
|
|
|
1240
1283
|
fail("INVALID_METADATA", `${INTERNAL_CONVERSATION_NAME} is reserved for workflow conversation instrumentation`);
|
|
1241
1284
|
if (hasIdentifier(program, INTERNAL_WORKTREE_NAME))
|
|
1242
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`);
|
|
1243
1288
|
validateDirectPrimitiveReferences(program, "withWorktree");
|
|
1244
1289
|
validateDirectPrimitiveReferences(program, "conversation");
|
|
1290
|
+
validateDirectPrimitiveReferences(program, "shell");
|
|
1245
1291
|
for (const [index, schema] of schemas.entries())
|
|
1246
1292
|
validateSchema(schema, `schema[${String(index)}]`);
|
|
1247
1293
|
const calls = workflowCalls(program);
|
|
@@ -1255,6 +1301,8 @@ export function preflight(script, capabilities, schemas = [], metadata = { name:
|
|
|
1255
1301
|
}
|
|
1256
1302
|
if (operation === "withWorktree")
|
|
1257
1303
|
validateStaticWithWorktree(call);
|
|
1304
|
+
if (operation === "shell")
|
|
1305
|
+
validateStaticShellOptions(call);
|
|
1258
1306
|
if ((operation === "parallel" || operation === "pipeline") && call.arguments.some((argument) => argument.type === "SpreadElement"))
|
|
1259
1307
|
continue;
|
|
1260
1308
|
if (operation === "checkpoint" && stableName(call.arguments[0]) === false)
|
|
@@ -1291,6 +1339,7 @@ export function preflight(script, capabilities, schemas = [], metadata = { name:
|
|
|
1291
1339
|
fail("UNKNOWN_AGENT_TYPE", `Unknown agent type: ${missingType}`);
|
|
1292
1340
|
return Object.freeze({ metadata: deepFreeze(checkedMetadata), referenced: deepFreeze({ phases, models, tools, agentTypes }), schemas: deepFreeze(checkedSchemas), dynamicAgentRoles });
|
|
1293
1341
|
}
|
|
1342
|
+
function functionLaunchScript(name) { return `return await ${name}(args);`; }
|
|
1294
1343
|
export function validateWorkflowLaunch(params, context) {
|
|
1295
1344
|
return validateWorkflowLaunchWithRegistry(params, context, loadingRegistry());
|
|
1296
1345
|
}
|
|
@@ -1299,15 +1348,19 @@ function validateWorkflowLaunchWithRegistry(params, context, registry) {
|
|
|
1299
1348
|
fail("INVALID_METADATA", "maxAgentLaunches has been removed; use budget.agentLaunches");
|
|
1300
1349
|
if (params.script !== undefined && params.workflow !== undefined)
|
|
1301
1350
|
fail("INVALID_METADATA", "Provide either script or workflow, not both");
|
|
1302
|
-
const
|
|
1303
|
-
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 : "";
|
|
1304
1357
|
if (!script)
|
|
1305
|
-
fail("INVALID_SYNTAX", "Provide script or registered
|
|
1306
|
-
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() : "");
|
|
1307
1360
|
if (!workflowName)
|
|
1308
1361
|
fail("INVALID_METADATA", "Inline workflows require name");
|
|
1309
|
-
const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } :
|
|
1310
|
-
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);
|
|
1311
1364
|
const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};
|
|
1312
1365
|
const agentDefinitions = deepFreeze({ ...globalAgentDefinitions, ...projectAgentDefinitions });
|
|
1313
1366
|
const aliases = context.modelAliases ?? {};
|
|
@@ -1315,7 +1368,7 @@ function validateWorkflowLaunchWithRegistry(params, context, registry) {
|
|
|
1315
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);
|
|
1316
1369
|
const roleNames = checked.dynamicAgentRoles ? Object.keys(agentDefinitions) : checked.referenced.agentTypes;
|
|
1317
1370
|
validateRolePolicies(agentDefinitions, roleNames, context.availableModels, context.rootTools, aliases, knownModels, context.settingsPath);
|
|
1318
|
-
return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames };
|
|
1371
|
+
return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, ...(functionName ? { functionName } : {}) };
|
|
1319
1372
|
}
|
|
1320
1373
|
function deepFreeze(value) {
|
|
1321
1374
|
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
|
|
@@ -1326,11 +1379,29 @@ function deepFreeze(value) {
|
|
|
1326
1379
|
return value;
|
|
1327
1380
|
}
|
|
1328
1381
|
export function createLaunchSnapshot(input) {
|
|
1329
|
-
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 }));
|
|
1330
1383
|
}
|
|
1331
1384
|
export function loadLaunchSnapshot(input) {
|
|
1332
1385
|
return deepFreeze(structuredClone(input));
|
|
1333
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
|
+
}
|
|
1334
1405
|
export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
1335
1406
|
export const HEARTBEAT_TIMEOUT_MS = 5000;
|
|
1336
1407
|
const OUTCOME_ERRORS = new Set(["AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID"]);
|
|
@@ -1396,10 +1467,12 @@ const named = (value, kind) => { if (typeof value !== "string" || !value.trim())
|
|
|
1396
1467
|
const path = (...names) => names.map(encodeURIComponent).join("/");
|
|
1397
1468
|
const inheritedAgentPath = new AsyncLocalStorage();
|
|
1398
1469
|
const agentOccurrences = new Map();
|
|
1470
|
+
const shellOccurrences = new Map();
|
|
1399
1471
|
const conversationOccurrences = new Map();
|
|
1400
1472
|
const worktreeOwners = new AsyncLocalStorage();
|
|
1401
1473
|
const worktreeOccurrences = new Map();
|
|
1402
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"); };
|
|
1403
1476
|
const rejectWorktree = () => { throw workError("INVALID_METADATA", "withWorktree calls must use a direct withWorktree(...) call; aliases and indirect calls are unsupported"); };
|
|
1404
1477
|
const internalWithWorktree = async (...values) => {
|
|
1405
1478
|
const callSite = values.pop();
|
|
@@ -1418,7 +1491,9 @@ const internalWithWorktree = async (...values) => {
|
|
|
1418
1491
|
worktreeOccurrences.set(occurrenceKey, occurrence);
|
|
1419
1492
|
owner = path("worktree", "unnamed", ...inherited, "callsite:" + callSite, "occurrence:" + String(occurrence));
|
|
1420
1493
|
}
|
|
1421
|
-
|
|
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 })));
|
|
1422
1497
|
};
|
|
1423
1498
|
const internalConversation = (...values) => {
|
|
1424
1499
|
const callSite = values.pop();
|
|
@@ -1477,6 +1552,31 @@ const internalAgent = (...values) => {
|
|
|
1477
1552
|
});
|
|
1478
1553
|
return result;
|
|
1479
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;
|
|
1480
1580
|
const agent = rejectAgent;
|
|
1481
1581
|
const promptPath = (at, key) => /^[A-Za-z_$][\w$]*$/.test(key) ? at + "." + key : at + "[" + JSON.stringify(key) + "]";
|
|
1482
1582
|
const plainPromptObject = value => {
|
|
@@ -1598,13 +1698,13 @@ const pipeline = async (operationName, items, stages) => {
|
|
|
1598
1698
|
return Object.fromEntries(results.map(result => [result.name, result.value]));
|
|
1599
1699
|
};
|
|
1600
1700
|
const safeMath = Object.fromEntries(Object.getOwnPropertyNames(Math).filter(name => name !== "random").map(name => [name, Math[name]]));
|
|
1601
|
-
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) };
|
|
1602
1702
|
for (const [name, fn] of Object.entries(functions)) Object.defineProperty(sandbox, name, { value: fn, writable: false, configurable: false });
|
|
1603
1703
|
for (const [name, value] of Object.entries(config.variables || {})) Object.defineProperty(sandbox, name, { value: freeze(value), writable: false, configurable: false });
|
|
1604
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;
|
|
1605
1705
|
const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
|
|
1606
1706
|
const body = config.script;
|
|
1607
|
-
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))
|
|
1608
1708
|
.then(async value => { await Promise.all(inflight); send({ type: "result", value: value === undefined ? null : value }); })
|
|
1609
1709
|
.catch(error => send({ type: "error", error: workerError(error) }))
|
|
1610
1710
|
.finally(() => clearInterval(heartbeat));
|
|
@@ -1617,6 +1717,9 @@ function encoded(value) {
|
|
|
1617
1717
|
fail("RPC_LIMIT_EXCEEDED", "RPC value exceeds the 10 MB JSON boundary");
|
|
1618
1718
|
return json;
|
|
1619
1719
|
}
|
|
1720
|
+
function encodedRpcResult(id, value) {
|
|
1721
|
+
return encoded({ type: "rpcResult", id, ok: true, value });
|
|
1722
|
+
}
|
|
1620
1723
|
function readAgentIdentity(value) {
|
|
1621
1724
|
if (!object(value))
|
|
1622
1725
|
fail("INTERNAL_ERROR", "Invalid workflow agent identity");
|
|
@@ -1631,9 +1734,18 @@ function readAgentIdentity(value) {
|
|
|
1631
1734
|
fail("INTERNAL_ERROR", "Invalid workflow agent identity");
|
|
1632
1735
|
return { structuralPath: [...structuralPath], callSite, occurrence, ...(typeof parentBreadcrumb === "string" ? { parentBreadcrumb } : {}), ...(typeof worktreeOwner === "string" ? { worktreeOwner } : {}), ...(parsedConversation ? { conversation: parsedConversation } : {}) };
|
|
1633
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
|
+
}
|
|
1634
1743
|
function agentIdentityPath(identity) {
|
|
1635
1744
|
return operationPath("agent", ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
|
|
1636
1745
|
}
|
|
1746
|
+
function shellIdentityPath(identity) {
|
|
1747
|
+
return operationPath("shell", ...(identity.worktreeOwner ? ["worktree", identity.worktreeOwner] : []), ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
|
|
1748
|
+
}
|
|
1637
1749
|
function conversationIdentityPath(identity) {
|
|
1638
1750
|
if (!identity.conversation)
|
|
1639
1751
|
throw new WorkflowError("INTERNAL_ERROR", "Missing conversation identity");
|
|
@@ -1644,9 +1756,122 @@ function conversationTurnPath(identity) {
|
|
|
1644
1756
|
throw new WorkflowError("INTERNAL_ERROR", "Missing conversation identity");
|
|
1645
1757
|
return operationPath(conversationIdentityPath(identity), `turn:${String(identity.conversation.turn)}`);
|
|
1646
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
|
+
}
|
|
1647
1764
|
function agentWorktree(identity) {
|
|
1648
1765
|
return identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {};
|
|
1649
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
|
+
}
|
|
1650
1875
|
export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
1651
1876
|
encoded(args);
|
|
1652
1877
|
const config = JSON.stringify({ script: instrumentWorkflow(script), args: structuredClone(args), functions: bridge.functions ?? {}, variables: bridge.variables ?? {} });
|
|
@@ -1750,6 +1975,14 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
|
1750
1975
|
value = branded({ name: label, ok: false, failedAt: path, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } });
|
|
1751
1976
|
}
|
|
1752
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
|
+
}
|
|
1753
1986
|
else if (method === "checkpoint") {
|
|
1754
1987
|
if (!bridge.checkpoint || !object(values[0]))
|
|
1755
1988
|
fail("INTERNAL_ERROR", "checkpoint requires an available bridge and object input");
|
|
@@ -1786,6 +2019,11 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
|
1786
2019
|
value = branded({ name, ok: false, failedAt: name, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } });
|
|
1787
2020
|
}
|
|
1788
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
|
+
}
|
|
1789
2027
|
else if (method === "phase") {
|
|
1790
2028
|
if (typeof values[0] !== "string")
|
|
1791
2029
|
fail("INTERNAL_ERROR", "phase name must be a string");
|
|
@@ -1799,7 +2037,7 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
|
1799
2037
|
else
|
|
1800
2038
|
fail("INTERNAL_ERROR", `Unknown worker RPC method: ${method}`);
|
|
1801
2039
|
encoded(value);
|
|
1802
|
-
child.send(
|
|
2040
|
+
child.send(encodedRpcResult(id, value));
|
|
1803
2041
|
}
|
|
1804
2042
|
catch (error) {
|
|
1805
2043
|
const typed = asWorkflowError(error);
|
|
@@ -1999,13 +2237,9 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
|
|
|
1999
2237
|
const render = ({ agent, depth }, grouped) => {
|
|
2000
2238
|
const icon = agent.state === "completed" ? "✓" : agent.state === "failed" || agent.state === "cancelled" ? "✗" : agent.state === "running" ? "⠦" : "○";
|
|
2001
2239
|
const breadcrumb = grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId);
|
|
2002
|
-
const setup = agent.attemptDetails?.at(-1)?.setup;
|
|
2003
|
-
const thinking = setup?.model.thinking ?? agent.model.thinking;
|
|
2004
|
-
const model = `${setup?.model.provider ?? agent.model.provider}/${setup?.model.model ?? agent.model.model}${thinking ? `:${thinking}` : ""}`;
|
|
2005
|
-
const policy = [`model=${model}`, agent.requestedModel ? `requested=${agent.requestedModel}` : "", `tools=${(setup?.tools ?? agent.tools).join(",") || "(none)"}`, agent.role ? `role=${agent.role}` : ""];
|
|
2006
2240
|
const tokens = agent.accounting ? formatAccounting(agent.accounting) : "";
|
|
2007
2241
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
2008
|
-
const result = [`${indent}${icon} ${breadcrumb} · ${agent.state}
|
|
2242
|
+
const result = [`${indent}${icon} ${breadcrumb} · ${agent.state}${tokens ? ` · ${tokens}` : ""}`];
|
|
2009
2243
|
if (agent.state === "failed" && agent.attemptDetails?.length) {
|
|
2010
2244
|
const last = agent.attemptDetails[agent.attemptDetails.length - 1];
|
|
2011
2245
|
if (last?.error)
|
|
@@ -2069,7 +2303,8 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
|
|
|
2069
2303
|
return lines.join("\n");
|
|
2070
2304
|
}
|
|
2071
2305
|
function formatCheckpointReview(checkpoint) {
|
|
2072
|
-
|
|
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");
|
|
2073
2308
|
}
|
|
2074
2309
|
const DELIVERY_LIMIT_BYTES = 4 * 1024;
|
|
2075
2310
|
const WORKFLOW_LOG_ENTRY = "workflow-log";
|
|
@@ -2091,11 +2326,144 @@ function utf8Prefix(value, maxBytes) {
|
|
|
2091
2326
|
end -= 1;
|
|
2092
2327
|
return bytes.subarray(0, end).toString("utf8");
|
|
2093
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
|
+
}
|
|
2094
2462
|
function deliver(pi, content) {
|
|
2095
2463
|
pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
2096
2464
|
}
|
|
2097
|
-
function deliverFailure(pi,
|
|
2098
|
-
deliver(pi, `Workflow ${
|
|
2465
|
+
function deliverFailure(pi, diagnostic) {
|
|
2466
|
+
deliver(pi, `Workflow ${utf8Prefix(diagnostic.workflowName, 128)} failure diagnostics: ${serializeWorkflowFailureDiagnostics(diagnostic)}`);
|
|
2099
2467
|
}
|
|
2100
2468
|
function safeEventError(error) {
|
|
2101
2469
|
const code = errorCode(error) ?? "INTERNAL_ERROR";
|
|
@@ -2109,6 +2477,11 @@ class WorkflowEventPublisher {
|
|
|
2109
2477
|
constructor(sink) {
|
|
2110
2478
|
this.sink = sink;
|
|
2111
2479
|
}
|
|
2480
|
+
removeRun(runId) {
|
|
2481
|
+
this.#queues.delete(runId);
|
|
2482
|
+
this.#budgetEvents.delete(runId);
|
|
2483
|
+
this.#worktrees.delete(runId);
|
|
2484
|
+
}
|
|
2112
2485
|
seedBudget(runId, events) {
|
|
2113
2486
|
const seen = this.#budgetEvents.get(runId) ?? new Set();
|
|
2114
2487
|
for (const event of events ?? [])
|
|
@@ -2183,7 +2556,12 @@ function namedRecord(value, kind) {
|
|
|
2183
2556
|
fail("INVALID_METADATA", `${kind} must be a record`);
|
|
2184
2557
|
return Object.entries(value);
|
|
2185
2558
|
}
|
|
2186
|
-
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) {
|
|
2187
2565
|
if (args.length !== 1 && args.length !== 2)
|
|
2188
2566
|
fail("INVALID_METADATA", "withWorktree requires a callback or a name and callback");
|
|
2189
2567
|
const callback = args[args.length - 1];
|
|
@@ -2202,7 +2580,10 @@ function hostWithWorktree(args, identity, occurrences) {
|
|
|
2202
2580
|
occurrences.set(key, occurrence);
|
|
2203
2581
|
owner = operationPath("worktree", "unnamed", "function", identity, ...structuralPath, `occurrence:${String(occurrence)}`);
|
|
2204
2582
|
}
|
|
2205
|
-
|
|
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));
|
|
2206
2587
|
}
|
|
2207
2588
|
function workflowRunContext(cwd, sessionId, runId, workflow, args, signal) {
|
|
2208
2589
|
return Object.freeze({ cwd, sessionId, runId, workflow: deepFreeze(structuredClone(workflow)), args: deepFreeze(structuredClone(args)), signal });
|
|
@@ -2301,6 +2682,7 @@ function nextNamedOccurrence(counters, label) {
|
|
|
2301
2682
|
}
|
|
2302
2683
|
function withWorkflowFunctions(bridge, store, runContext, variables, registry) {
|
|
2303
2684
|
const functionAgentOccurrences = new Map();
|
|
2685
|
+
const functionShellOccurrences = new Map();
|
|
2304
2686
|
const functionWorktreeOccurrences = new Map();
|
|
2305
2687
|
const functionInvokeOccurrences = new Map();
|
|
2306
2688
|
const invokeFunction = async (name, input, path, signal, worktreeOwner, structuralPath = [], breadcrumb) => {
|
|
@@ -2330,10 +2712,23 @@ function withWorkflowFunctions(bridge, store, runContext, variables, registry) {
|
|
|
2330
2712
|
functionAgentOccurrences.set(key, occurrence);
|
|
2331
2713
|
return bridge.agent(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, parentBreadcrumb: functionBreadcrumb, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
|
|
2332
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
|
+
},
|
|
2333
2728
|
prompt: workflowPrompt,
|
|
2334
2729
|
parallel: (...args) => hostParallel(args[0], args[1]),
|
|
2335
2730
|
pipeline: (...args) => hostPipeline(args[0], args[1], args[2]),
|
|
2336
|
-
withWorktree: (...args) => hostWithWorktree(args, path, functionWorktreeOccurrences),
|
|
2731
|
+
withWorktree: (...args) => hostWithWorktree(args, path, functionWorktreeOccurrences, bridge.worktree, signal),
|
|
2337
2732
|
checkpoint: async (...args) => {
|
|
2338
2733
|
if (!bridge.checkpoint || !object(args[0]) || !jsonValue(args[0]))
|
|
2339
2734
|
fail("INTERNAL_ERROR", "No checkpoint bridge is available");
|
|
@@ -2389,6 +2784,16 @@ function tuiHostCapabilities(tui) {
|
|
|
2389
2784
|
return { terminal: { ...(tui.terminal.rows === undefined ? {} : { rows: tui.terminal.rows }) } };
|
|
2390
2785
|
}
|
|
2391
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
|
+
}
|
|
2392
2797
|
function isKeybindingGetter(value) { return typeof value === "function"; }
|
|
2393
2798
|
function keybindingsHostCapabilities(keybindings) {
|
|
2394
2799
|
if (!object(keybindings) || !isKeybindingGetter(keybindings.getKeys))
|
|
@@ -2408,9 +2813,10 @@ function parseThinking(value) {
|
|
|
2408
2813
|
default: return undefined;
|
|
2409
2814
|
}
|
|
2410
2815
|
}
|
|
2411
|
-
export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession) {
|
|
2816
|
+
export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession, agentDir) {
|
|
2412
2817
|
beginWorkflowExtensionLoading();
|
|
2413
2818
|
const registry = loadingRegistry();
|
|
2819
|
+
const extensionAgentDir = agentDir ?? getAgentDir();
|
|
2414
2820
|
const registerEntryRenderer = piHostCapabilities(pi).registerEntryRenderer;
|
|
2415
2821
|
registerEntryRenderer?.(WORKFLOW_LOG_ENTRY, (entry) => {
|
|
2416
2822
|
const data = entry.data;
|
|
@@ -2434,6 +2840,41 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2434
2840
|
return skillPath ? { skillPaths: [skillPath] } : undefined;
|
|
2435
2841
|
});
|
|
2436
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
|
+
});
|
|
2437
2878
|
const liveActivities = new Map();
|
|
2438
2879
|
const setLiveActivity = (runId, agentId, activity) => {
|
|
2439
2880
|
const activities = liveActivities.get(runId);
|
|
@@ -2486,10 +2927,39 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2486
2927
|
const persistWorktree = async (store, metadata, owner) => {
|
|
2487
2928
|
const existing = (await store.worktrees()).some((worktree) => worktree.owner === owner);
|
|
2488
2929
|
const worktree = await store.worktree(owner);
|
|
2489
|
-
if (!existing)
|
|
2930
|
+
if (!existing && await store.ownsWorktree(owner))
|
|
2490
2931
|
await eventPublisher.worktree(store, metadata, worktree);
|
|
2491
2932
|
return worktree;
|
|
2492
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
|
+
};
|
|
2493
2963
|
const lifecycleFor = (store, state, budget, metadata) => new RunLifecycle(state, async (next, previous, reason) => {
|
|
2494
2964
|
if (next !== "pausing")
|
|
2495
2965
|
budget.transition(next);
|
|
@@ -2535,7 +3005,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2535
3005
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
2536
3006
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2537
3007
|
};
|
|
2538
|
-
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); });
|
|
2539
3009
|
const before = (await run.store.load()).run;
|
|
2540
3010
|
await persistAgentAttempts(run.store, id, result.attempts);
|
|
2541
3011
|
const completed = (await run.store.load()).run;
|
|
@@ -2584,6 +3054,21 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2584
3054
|
await eventPublisher.agentStates(run.store, run.metadata, previousAgents, runState.agents);
|
|
2585
3055
|
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
2586
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
|
+
};
|
|
2587
3072
|
const stopWorkflowRun = async (runId) => {
|
|
2588
3073
|
const run = runs.get(runId);
|
|
2589
3074
|
const terminalState = terminalRunStates.get(runId);
|
|
@@ -2597,6 +3082,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2597
3082
|
run.execution?.cancel();
|
|
2598
3083
|
await scheduler.cancelRun(run.store.runId);
|
|
2599
3084
|
await scheduler.flush();
|
|
3085
|
+
await cleanupTerminalRun(runId);
|
|
2600
3086
|
return { runId, state: "stopped", stopped: true };
|
|
2601
3087
|
};
|
|
2602
3088
|
const answerCheckpoint = async (runId, name, approved, silent = false) => {
|
|
@@ -2629,18 +3115,18 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2629
3115
|
return undefined;
|
|
2630
3116
|
await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
|
|
2631
3117
|
const result = await applyBudgetDecision(request, approved);
|
|
2632
|
-
run.budgetResolvers.get(proposalId)?.(result);
|
|
2633
|
-
run.budgetResolvers.delete(proposalId);
|
|
2634
3118
|
if (!silent)
|
|
2635
3119
|
deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
|
|
2636
3120
|
return result;
|
|
2637
3121
|
};
|
|
2638
|
-
const checkpointBridge = (runId, store, metadata, foreground, ui) => {
|
|
3122
|
+
const checkpointBridge = (runId, store, metadata, foreground, ui, headless = false) => {
|
|
2639
3123
|
const checkpointCounters = new Map();
|
|
2640
3124
|
return async (raw, signal) => {
|
|
2641
3125
|
const input = validateCheckpoint(raw);
|
|
2642
3126
|
const label = nextNamedOccurrence(checkpointCounters, input.name);
|
|
2643
3127
|
const path = operationPath("checkpoint", label);
|
|
3128
|
+
if (headless)
|
|
3129
|
+
fail("RESUME_INCOMPATIBLE", "Headless CLI checkpoints are unsupported");
|
|
2644
3130
|
if (foreground && !ui?.select)
|
|
2645
3131
|
fail("RESUME_INCOMPATIBLE", "Foreground checkpoints require UI");
|
|
2646
3132
|
const alreadyAwaiting = (await store.awaitingCheckpoints()).some((checkpoint) => checkpoint.path === path);
|
|
@@ -2731,14 +3217,17 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2731
3217
|
return;
|
|
2732
3218
|
const catalog = registry.catalog();
|
|
2733
3219
|
const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0;
|
|
2734
|
-
if (!catalog.functions.length && !catalog.variables.length && !
|
|
3220
|
+
if (!catalog.functions.length && !catalog.variables.length && !hasAliases)
|
|
2735
3221
|
return;
|
|
2736
3222
|
pi.registerTool({
|
|
2737
3223
|
name: "workflow_catalog",
|
|
2738
3224
|
label: "Workflow Catalog",
|
|
2739
|
-
description: "List
|
|
2740
|
-
parameters: Type.Object({}, { additionalProperties: false }),
|
|
2741
|
-
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
|
+
}
|
|
2742
3231
|
});
|
|
2743
3232
|
catalogRegistered = true;
|
|
2744
3233
|
};
|
|
@@ -2748,7 +3237,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2748
3237
|
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
2749
3238
|
if (missing)
|
|
2750
3239
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
2751
|
-
const settingsPath = workflowSettingsPath();
|
|
3240
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2752
3241
|
const currentSettings = loadSettings(settingsPath);
|
|
2753
3242
|
resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath);
|
|
2754
3243
|
const currentAliases = currentSettings.modelAliases ?? {};
|
|
@@ -2762,7 +3251,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2762
3251
|
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2763
3252
|
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
|
|
2764
3253
|
await run.store.saveSnapshot(snapshot);
|
|
2765
|
-
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);
|
|
2766
3255
|
run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
|
|
2767
3256
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
2768
3257
|
if (drift.length)
|
|
@@ -2770,6 +3259,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2770
3259
|
};
|
|
2771
3260
|
const coldResumeRun = async (run, hasUI, ui, trustedProject, context) => {
|
|
2772
3261
|
const loaded = await run.store.load();
|
|
3262
|
+
await run.store.validateBorrowedWorktrees();
|
|
2773
3263
|
if (loaded.snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION)
|
|
2774
3264
|
throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
|
|
2775
3265
|
if (loaded.snapshot.roles === undefined)
|
|
@@ -2783,7 +3273,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2783
3273
|
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
2784
3274
|
if (missing)
|
|
2785
3275
|
throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
2786
|
-
const settingsPath = workflowSettingsPath();
|
|
3276
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2787
3277
|
const currentSettings = loadSettings(settingsPath);
|
|
2788
3278
|
resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
|
|
2789
3279
|
const currentAliases = currentSettings.modelAliases ?? {};
|
|
@@ -2796,10 +3286,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2796
3286
|
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
2797
3287
|
const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2798
3288
|
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2799
|
-
|
|
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);
|
|
2800
3291
|
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
|
|
2801
3292
|
await run.store.saveSnapshot(snapshot);
|
|
2802
|
-
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);
|
|
2803
3294
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
2804
3295
|
if (drift.length)
|
|
2805
3296
|
await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
@@ -2819,11 +3310,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2819
3310
|
await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
|
|
2820
3311
|
run.update?.(workflowToolUpdate(persisted));
|
|
2821
3312
|
}
|
|
3313
|
+
await cleanupTerminalRun(run.store.runId);
|
|
2822
3314
|
throw typed;
|
|
2823
3315
|
}
|
|
2824
3316
|
await scheduler.cancelRun(run.store.runId);
|
|
2825
3317
|
await run.lifecycle.resume();
|
|
2826
|
-
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) => {
|
|
2827
3319
|
await run.lifecycle.enter();
|
|
2828
3320
|
const conversationId = identity.conversation ? conversationIdentityPath(identity) : undefined;
|
|
2829
3321
|
const conversationLock = conversationId ? `${run.store.runId}:${conversationId}` : "";
|
|
@@ -2870,7 +3362,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2870
3362
|
conversationLocks.delete(conversationLock);
|
|
2871
3363
|
await run.lifecycle.leave();
|
|
2872
3364
|
}
|
|
2873
|
-
}, 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 {
|
|
2874
3366
|
let previousPhase;
|
|
2875
3367
|
const persisted = await persistRunState(run.store, run.metadata, (current) => { previousPhase = current.phase; return { ...current, phase }; });
|
|
2876
3368
|
await eventPublisher.phase(run.store, run.metadata, previousPhase, phase);
|
|
@@ -2898,8 +3390,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2898
3390
|
await eventPublisher.runFailed(run.store, run.metadata, typed, state);
|
|
2899
3391
|
run.update?.(workflowToolUpdate(persisted));
|
|
2900
3392
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
|
|
2901
|
-
deliverFailure(pi, run.metadata
|
|
2902
|
-
});
|
|
3393
|
+
deliverFailure(pi, createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted));
|
|
3394
|
+
}).finally(() => cleanupTerminalRun(run.store.runId));
|
|
3395
|
+
run.completion = completion;
|
|
2903
3396
|
void completion;
|
|
2904
3397
|
};
|
|
2905
3398
|
const applyBudgetDecision = async (request, approved) => {
|
|
@@ -2935,18 +3428,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
2935
3428
|
if (budgetRelaxed(currentBudget, nextBudget)) {
|
|
2936
3429
|
const proposalId = randomUUID();
|
|
2937
3430
|
const request = { kind: "budget", proposalId, runId, consumed: usage, previous: currentBudget ?? {}, proposed: nextBudget ?? {}, budgetVersion: loaded.run.budgetVersion ?? 1 };
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
await run.store.requestWorkflowDecision(request);
|
|
2941
|
-
await appendBudgetDecisionEvent(run, request, "adjustment_requested");
|
|
2942
|
-
}
|
|
2943
|
-
catch (error) {
|
|
2944
|
-
run.budgetResolvers.delete(proposalId);
|
|
2945
|
-
throw error;
|
|
2946
|
-
}
|
|
3431
|
+
await run.store.requestWorkflowDecision(request);
|
|
3432
|
+
await appendBudgetDecisionEvent(run, request, "adjustment_requested");
|
|
2947
3433
|
deliver(pi, budgetDecisionDelivery(run.metadata, request));
|
|
2948
|
-
|
|
2949
|
-
return { state: decisionResult.state };
|
|
3434
|
+
return { state: "awaiting_approval", proposalId };
|
|
2950
3435
|
}
|
|
2951
3436
|
const changed = JSON.stringify(currentBudget ?? {}) !== JSON.stringify(nextBudget ?? {});
|
|
2952
3437
|
if (changed) {
|
|
@@ -3015,7 +3500,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3015
3500
|
const lifecycle = lifecycleFor(store, loaded.run.state, budgetRuntime, loaded.snapshot.metadata);
|
|
3016
3501
|
const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
3017
3502
|
const roleDefinitions = loaded.snapshot.roles ?? {};
|
|
3018
|
-
|
|
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 } : {}) });
|
|
3019
3506
|
for (const checkpoint of await store.awaitingCheckpoints())
|
|
3020
3507
|
deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
|
|
3021
3508
|
for (const decision of await store.pendingWorkflowDecisions())
|
|
@@ -3052,7 +3539,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3052
3539
|
pi.on("before_agent_start", (event, ctx) => {
|
|
3053
3540
|
if (!pi.getActiveTools().includes("workflow"))
|
|
3054
3541
|
return;
|
|
3055
|
-
const roles = Object.entries(loadAgentDefinitions(ctx.cwd,
|
|
3542
|
+
const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx))).filter(([, definition]) => definition.description);
|
|
3056
3543
|
if (!roles.length)
|
|
3057
3544
|
return;
|
|
3058
3545
|
const content = `Workflow role descriptions:\n${roles.map(([name, definition]) => `- \`${name}\`: ${String(definition.description)}`).join("\n")}`;
|
|
@@ -3064,9 +3551,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3064
3551
|
description: WORKFLOW_TOOL_DESCRIPTION,
|
|
3065
3552
|
promptSnippet: WORKFLOW_TOOL_PROMPT_SNIPPET,
|
|
3066
3553
|
parameters: WORKFLOW_TOOL_PARAMETERS,
|
|
3067
|
-
async execute(
|
|
3554
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
3068
3555
|
try {
|
|
3069
|
-
const
|
|
3556
|
+
const headless = object(ctx) && ctx.headless === true;
|
|
3557
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
3070
3558
|
const defaults = loadSettings(settingsPath);
|
|
3071
3559
|
if (!ctx.model)
|
|
3072
3560
|
throw new WorkflowError("UNKNOWN_MODEL", "A launching model is required");
|
|
@@ -3082,8 +3570,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3082
3570
|
const trustedProject = projectTrusted(ctx);
|
|
3083
3571
|
if (typeof ctx.cwd === "string")
|
|
3084
3572
|
resolveAgentResourcePolicy(ctx.cwd, trustedProject, settingsPath);
|
|
3085
|
-
const validated = validateWorkflowLaunchWithRegistry(params, { cwd: ctx.cwd, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases: defaults.modelAliases ?? {}, knownModels, settingsPath }, registry);
|
|
3086
|
-
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;
|
|
3087
3575
|
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
3088
3576
|
const runId = randomUUID();
|
|
3089
3577
|
const args = (params.args ?? null);
|
|
@@ -3096,24 +3584,28 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3096
3584
|
const runContext = workflowRunContext(ctx.cwd, ctx.sessionManager.getSessionId(), runId, checked.metadata, args, runController.signal);
|
|
3097
3585
|
const variables = await resolveWorkflowVariables(runContext, runController, registry);
|
|
3098
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);
|
|
3099
3590
|
const roles = Object.fromEntries(roleNames.map((role) => [role, agentDefinitions[role]]));
|
|
3100
3591
|
const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
|
|
3101
3592
|
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, defaults.modelAliases, knownModels, settingsPath)] : []; });
|
|
3102
3593
|
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
|
|
3103
|
-
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 });
|
|
3104
3595
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
3105
3596
|
const initialBudget = budgetRuntime.snapshot();
|
|
3106
|
-
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);
|
|
3107
3598
|
const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
|
|
3108
3599
|
const background = !params.foreground;
|
|
3109
3600
|
const providerPause = async () => { if (background)
|
|
3110
3601
|
deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
3111
|
-
const
|
|
3112
|
-
|
|
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 } : {}) });
|
|
3113
3605
|
if (params.foreground && onUpdate)
|
|
3114
3606
|
onUpdate(workflowToolUpdate((await store.load()).run));
|
|
3115
3607
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
3116
|
-
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) => {
|
|
3117
3609
|
await lifecycle.enter();
|
|
3118
3610
|
const conversationId = identity.conversation ? conversationIdentityPath(identity) : undefined;
|
|
3119
3611
|
const conversationLock = conversationId ? `${runId}:${conversationId}` : "";
|
|
@@ -3163,7 +3655,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3163
3655
|
conversationLocks.delete(conversationLock);
|
|
3164
3656
|
await lifecycle.leave();
|
|
3165
3657
|
}
|
|
3166
|
-
}, 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) => {
|
|
3167
3659
|
await lifecycle.enter();
|
|
3168
3660
|
try {
|
|
3169
3661
|
let previousPhase;
|
|
@@ -3190,21 +3682,32 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3190
3682
|
const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
|
|
3191
3683
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(lifecycle.state))
|
|
3192
3684
|
await lifecycle.terminal(typed.code === "CANCELLED" ? "stopped" : typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
3193
|
-
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 } }));
|
|
3194
3686
|
const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
|
|
3195
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);
|
|
3196
3692
|
throw typed;
|
|
3197
3693
|
});
|
|
3198
|
-
|
|
3694
|
+
const completion = finish.finally(() => cleanupTerminalRun(runId));
|
|
3695
|
+
runs.get(runId).completion = completion;
|
|
3199
3696
|
if (background) {
|
|
3200
|
-
void
|
|
3697
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
3201
3698
|
deliver(pi, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
3202
|
-
}, (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
|
+
});
|
|
3203
3706
|
return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
3204
3707
|
}
|
|
3205
|
-
const { value } = await
|
|
3708
|
+
const { value } = await completion;
|
|
3206
3709
|
const run = (await store.load()).run;
|
|
3207
|
-
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 } };
|
|
3208
3711
|
}
|
|
3209
3712
|
catch (error) {
|
|
3210
3713
|
throw mainAgentError(error);
|
|
@@ -3215,19 +3718,22 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3215
3718
|
},
|
|
3216
3719
|
renderResult(result, { isPartial }, _theme, context) {
|
|
3217
3720
|
const details = result.details;
|
|
3721
|
+
if (isWorkflowFailureDiagnostics(details))
|
|
3722
|
+
return textBlock(formatWorkflowFailureDiagnostics(details));
|
|
3723
|
+
const runDetails = details;
|
|
3218
3724
|
const state = context.state;
|
|
3219
|
-
if (
|
|
3725
|
+
if (runDetails?.run && isPartial && runDetails.run.state === "running" && !state.workflowSpinner) {
|
|
3220
3726
|
state.workflowSpinner = setInterval(context.invalidate, 80);
|
|
3221
3727
|
state.workflowSpinner.unref();
|
|
3222
3728
|
}
|
|
3223
|
-
else if ((!isPartial ||
|
|
3729
|
+
else if ((!isPartial || runDetails?.run?.state !== "running") && state.workflowSpinner) {
|
|
3224
3730
|
clearInterval(state.workflowSpinner);
|
|
3225
3731
|
delete state.workflowSpinner;
|
|
3226
3732
|
}
|
|
3227
|
-
if (
|
|
3228
|
-
return workflowProgressBlock(
|
|
3733
|
+
if (runDetails?.run)
|
|
3734
|
+
return workflowProgressBlock(runDetails.run);
|
|
3229
3735
|
const content = result.content[0];
|
|
3230
|
-
return textBlock(isPartial ? "Workflow starting..." :
|
|
3736
|
+
return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
|
|
3231
3737
|
},
|
|
3232
3738
|
});
|
|
3233
3739
|
pi.registerCommand("workflow", {
|
|
@@ -3351,7 +3857,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3351
3857
|
}
|
|
3352
3858
|
};
|
|
3353
3859
|
const manageAliases = async () => {
|
|
3354
|
-
const settingsPath = workflowSettingsPath();
|
|
3860
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
3355
3861
|
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
3356
3862
|
const available = () => [...new Set((modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`))].sort();
|
|
3357
3863
|
const selectTarget = async () => {
|
|
@@ -3469,10 +3975,20 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3469
3975
|
const labels = navigatorRunLabels(sorted);
|
|
3470
3976
|
const terminalStates = new Set(["completed", "failed", "stopped"]);
|
|
3471
3977
|
const hasCompleted = sorted.some(({ loaded: { run } }) => run.state === "completed");
|
|
3472
|
-
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"] : [])];
|
|
3473
3979
|
const runChoice = await ctx.ui.select("Workflows\n", pickerOptions);
|
|
3474
3980
|
if (!runChoice || runChoice === "Close")
|
|
3475
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
|
+
}
|
|
3476
3992
|
if (runChoice === "Model aliases") {
|
|
3477
3993
|
await manageAliases();
|
|
3478
3994
|
stores = await loadStores();
|
|
@@ -3508,49 +4024,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3508
4024
|
ctx.ui.notify(`Failed to copy ${artifact}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3509
4025
|
}
|
|
3510
4026
|
};
|
|
3511
|
-
const openTranscript = async (transcript) => {
|
|
3512
|
-
try {
|
|
3513
|
-
const entries = SessionManager.open(transcript).buildContextEntries();
|
|
3514
|
-
if (ctx.mode !== "tui") {
|
|
3515
|
-
ctx.ui.notify(`Transcript: ${transcript}`, "info");
|
|
3516
|
-
return;
|
|
3517
|
-
}
|
|
3518
|
-
await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
3519
|
-
let offset = 0;
|
|
3520
|
-
let renderedLines = [];
|
|
3521
|
-
const viewport = () => Math.max(1, tuiRows(tui) - 3);
|
|
3522
|
-
const move = (delta) => { offset = Math.max(0, Math.min(Math.max(0, renderedLines.length - viewport()), offset + delta)); };
|
|
3523
|
-
return {
|
|
3524
|
-
render(width) {
|
|
3525
|
-
renderedLines = transcriptLines(entries).flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
|
|
3526
|
-
offset = Math.min(offset, Math.max(0, renderedLines.length - viewport()));
|
|
3527
|
-
return [theme.fg("accent", "Native Pi transcript"), ...renderedLines.slice(offset, offset + viewport()), "", theme.fg("dim", "↑↓/pgup/pgdn scroll · esc close")];
|
|
3528
|
-
},
|
|
3529
|
-
invalidate() { },
|
|
3530
|
-
handleInput(data) {
|
|
3531
|
-
if (keybindings.matches(data, "tui.select.up"))
|
|
3532
|
-
move(-1);
|
|
3533
|
-
else if (keybindings.matches(data, "tui.select.down"))
|
|
3534
|
-
move(1);
|
|
3535
|
-
else if (keybindings.matches(data, "tui.select.pageUp"))
|
|
3536
|
-
move(-viewport());
|
|
3537
|
-
else if (keybindings.matches(data, "tui.select.pageDown"))
|
|
3538
|
-
move(viewport());
|
|
3539
|
-
else if (keybindings.matches(data, "tui.editor.cursorLineStart"))
|
|
3540
|
-
offset = 0;
|
|
3541
|
-
else if (keybindings.matches(data, "tui.editor.cursorLineEnd"))
|
|
3542
|
-
offset = Math.max(0, renderedLines.length - viewport());
|
|
3543
|
-
else if (keybindings.matches(data, "tui.select.cancel"))
|
|
3544
|
-
done(undefined);
|
|
3545
|
-
tui.requestRender();
|
|
3546
|
-
},
|
|
3547
|
-
};
|
|
3548
|
-
}, { overlay: true });
|
|
3549
|
-
}
|
|
3550
|
-
catch (error) {
|
|
3551
|
-
ctx.ui.notify(`Cannot open transcript: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
3552
|
-
}
|
|
3553
|
-
};
|
|
3554
4027
|
const loadDashboard = async () => {
|
|
3555
4028
|
const loaded = await store.load();
|
|
3556
4029
|
const checkpoints = await store.awaitingCheckpoints();
|
|
@@ -3590,20 +4063,15 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3590
4063
|
actions.set("Refresh", "refresh");
|
|
3591
4064
|
else
|
|
3592
4065
|
actions.set("View script", "view-script");
|
|
3593
|
-
const transcripts = [...new Set([...loaded.run.agents.flatMap((agent) => (agent.attemptDetails ?? []).map((attempt) => attempt.sessionFile)), ...loaded.run.nativeSessions.map(({ sessionFile }) => sessionFile)])];
|
|
3594
4066
|
if (loaded.run.agents.length)
|
|
3595
4067
|
actions.set("Agents...", "agents");
|
|
3596
|
-
if (!loaded.run.agents.length && ctx.mode === "tui" && transcripts.length)
|
|
3597
|
-
actions.set("View transcript", "view-transcript");
|
|
3598
|
-
if (!loaded.run.agents.length && transcripts.length)
|
|
3599
|
-
actions.set("Transcript paths", "transcripts");
|
|
3600
4068
|
if (terminalStates.has(loaded.run.state))
|
|
3601
4069
|
add("Delete", "delete");
|
|
3602
4070
|
if (ctx.mode === "tui") {
|
|
3603
4071
|
addCopy("Copy run path", store.directory, "run path");
|
|
3604
4072
|
addCopy("Copy run ID", store.runId, "run ID");
|
|
3605
4073
|
}
|
|
3606
|
-
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 };
|
|
3607
4075
|
};
|
|
3608
4076
|
const selectAgent = async (dashboard) => {
|
|
3609
4077
|
const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
|
|
@@ -3626,14 +4094,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3626
4094
|
const attempts = [...(selected.attemptDetails ?? [])].sort((left, right) => left.attempt - right.attempt);
|
|
3627
4095
|
const worktree = selected.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === selected.worktreeOwner) : undefined;
|
|
3628
4096
|
const actions = [
|
|
3629
|
-
...(attempts.length ? ["
|
|
4097
|
+
...(attempts.length && herdrPaneId() ? ["Fork as Pi session in pane"] : []),
|
|
3630
4098
|
...(worktree ? ["Copy branch", "Copy worktree path"] : []),
|
|
3631
4099
|
"Copy agent ID",
|
|
3632
4100
|
"Back",
|
|
3633
4101
|
];
|
|
3634
4102
|
const chooseAttempt = async () => {
|
|
3635
4103
|
const choices = attempts.map((attempt) => `Attempt ${String(attempt.attempt)}`);
|
|
3636
|
-
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"]);
|
|
3637
4105
|
const index = choice ? choices.indexOf(choice) : -1;
|
|
3638
4106
|
return index >= 0 ? attempts[index] : undefined;
|
|
3639
4107
|
};
|
|
@@ -3653,14 +4121,20 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3653
4121
|
await copyArtifact(worktree.path, "worktree path");
|
|
3654
4122
|
continue;
|
|
3655
4123
|
}
|
|
3656
|
-
if (action === "
|
|
4124
|
+
if (action === "Fork as Pi session in pane") {
|
|
3657
4125
|
const attempt = await chooseAttempt();
|
|
3658
4126
|
if (!attempt)
|
|
3659
4127
|
continue;
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
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
|
+
}
|
|
3664
4138
|
}
|
|
3665
4139
|
}
|
|
3666
4140
|
};
|
|
@@ -3675,7 +4149,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3675
4149
|
let disposed = false;
|
|
3676
4150
|
let stopRequested = false;
|
|
3677
4151
|
let stopStatus;
|
|
3678
|
-
const terminalRows = () => Math.max(1, tuiRows(tui));
|
|
4152
|
+
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
3679
4153
|
const keyLabels = { up: "↑", down: "↓", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
|
|
3680
4154
|
const keyLabel = (binding, fallback) => {
|
|
3681
4155
|
const keys = keybindingKeys(keybindings, binding);
|
|
@@ -3684,9 +4158,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3684
4158
|
const dashboardLayout = () => {
|
|
3685
4159
|
const rows = terminalRows();
|
|
3686
4160
|
const hintRows = rows >= 4 ? 1 : 0;
|
|
3687
|
-
const separatorRows = rows >=
|
|
4161
|
+
const separatorRows = rows >= 8 ? 1 : 0;
|
|
3688
4162
|
const available = Math.max(1, rows - hintRows - separatorRows);
|
|
3689
|
-
const actionViewport = Math.min(options.length, Math.max(1, Math.
|
|
4163
|
+
const actionViewport = Math.min(options.length, Math.max(1, Math.ceil(available / 2)));
|
|
3690
4164
|
return { rows, hintRows, separatorRows, actionViewport, dashboardViewport: available - actionViewport };
|
|
3691
4165
|
};
|
|
3692
4166
|
const updateDashboard = async (selectedOption) => {
|
|
@@ -3729,7 +4203,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3729
4203
|
void updateDashboard(selectedOption).catch(() => undefined).finally(() => { refreshing = false; });
|
|
3730
4204
|
}, 1000);
|
|
3731
4205
|
timer.unref();
|
|
3732
|
-
return {
|
|
4206
|
+
return borderWorkflowOverlay({
|
|
3733
4207
|
render(width) {
|
|
3734
4208
|
const dashboard = stopStatus ? `${view.dashboard}\n\n${stopStatus}` : view.dashboard;
|
|
3735
4209
|
const dashboardLines = truncateToVisualLines(theme.fg("accent", dashboard), Number.MAX_SAFE_INTEGER, width, 1).visualLines;
|
|
@@ -3776,7 +4250,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3776
4250
|
tui.requestRender();
|
|
3777
4251
|
},
|
|
3778
4252
|
dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
|
|
3779
|
-
};
|
|
4253
|
+
}, theme);
|
|
3780
4254
|
}, { overlay: true })
|
|
3781
4255
|
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Close"]);
|
|
3782
4256
|
if (!actionChoice || actionChoice === "Close")
|
|
@@ -3792,12 +4266,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3792
4266
|
const highlighted = highlightCode(view.script, "javascript");
|
|
3793
4267
|
let offset = 0;
|
|
3794
4268
|
let renderedLines = [];
|
|
3795
|
-
const viewport = () => Math.max(1, tuiRows(tui) - 3);
|
|
4269
|
+
const viewport = () => Math.max(1, tuiRows(tui) - 3 - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
3796
4270
|
const move = (delta) => {
|
|
3797
4271
|
const maxOffset = Math.max(0, renderedLines.length - viewport());
|
|
3798
4272
|
offset = Math.max(0, Math.min(maxOffset, offset + delta));
|
|
3799
4273
|
};
|
|
3800
|
-
return {
|
|
4274
|
+
return borderWorkflowOverlay({
|
|
3801
4275
|
render(width) {
|
|
3802
4276
|
renderedLines = highlighted.flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
|
|
3803
4277
|
const maxOffset = Math.max(0, renderedLines.length - viewport());
|
|
@@ -3823,24 +4297,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3823
4297
|
done(undefined);
|
|
3824
4298
|
tui.requestRender();
|
|
3825
4299
|
},
|
|
3826
|
-
};
|
|
3827
|
-
});
|
|
3828
|
-
continue;
|
|
3829
|
-
}
|
|
3830
|
-
if (actionChoice === "View transcript") {
|
|
3831
|
-
const transcript = await ctx.ui.select("Native Pi transcripts", [...view.transcripts, "Back"]);
|
|
3832
|
-
if (transcript && transcript !== "Back")
|
|
3833
|
-
await openTranscript(transcript);
|
|
3834
|
-
continue;
|
|
3835
|
-
}
|
|
3836
|
-
if (actionChoice === "Transcript paths") {
|
|
3837
|
-
const transcript = await ctx.ui.select("Native Pi transcript paths", [...view.transcripts, "Back"]);
|
|
3838
|
-
if (transcript && transcript !== "Back") {
|
|
3839
|
-
if (ctx.mode === "tui")
|
|
3840
|
-
await copyArtifact(transcript, "transcript path");
|
|
3841
|
-
else
|
|
3842
|
-
ctx.ui.notify(transcript, "info");
|
|
3843
|
-
}
|
|
4300
|
+
}, theme);
|
|
4301
|
+
}, { overlay: true, overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" } });
|
|
3844
4302
|
continue;
|
|
3845
4303
|
}
|
|
3846
4304
|
const copy = view.copies.get(actionChoice);
|
|
@@ -3858,11 +4316,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3858
4316
|
let offset = 0;
|
|
3859
4317
|
let renderedLines = [];
|
|
3860
4318
|
const layout = () => {
|
|
3861
|
-
const rows = Math.max(1, tuiRows(tui));
|
|
4319
|
+
const rows = Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
3862
4320
|
const compactControls = rows < 4;
|
|
3863
4321
|
const titleRows = rows >= 5 ? 1 : 0;
|
|
3864
4322
|
const hintRows = rows >= 8 ? 1 : 0;
|
|
3865
|
-
const separatorRows = rows >= 8 ?
|
|
4323
|
+
const separatorRows = rows >= 8 ? 1 : 0;
|
|
3866
4324
|
const controlRows = compactControls ? 1 : options.length;
|
|
3867
4325
|
const contentViewport = Math.max(0, rows - titleRows - hintRows - separatorRows - controlRows);
|
|
3868
4326
|
return { rows, compactControls, titleRows, hintRows, separatorRows, contentViewport };
|
|
@@ -3871,7 +4329,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3871
4329
|
const maxOffset = Math.max(0, renderedLines.length - layout().contentViewport);
|
|
3872
4330
|
offset = Math.max(0, Math.min(maxOffset, offset + delta));
|
|
3873
4331
|
};
|
|
3874
|
-
return {
|
|
4332
|
+
return borderWorkflowOverlay({
|
|
3875
4333
|
render(width) {
|
|
3876
4334
|
renderedLines = truncateToVisualLines(formatCheckpointReview(checkpoint), Number.MAX_SAFE_INTEGER, width, 0).visualLines;
|
|
3877
4335
|
const currentLayout = layout();
|
|
@@ -3886,7 +4344,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3886
4344
|
...renderedLines.slice(offset, offset + currentLayout.contentViewport),
|
|
3887
4345
|
...(currentLayout.separatorRows ? [""] : []),
|
|
3888
4346
|
...controls,
|
|
3889
|
-
...(currentLayout.separatorRows ? [""] : []),
|
|
3890
4347
|
...(currentLayout.hintRows ? [hint] : []),
|
|
3891
4348
|
];
|
|
3892
4349
|
},
|
|
@@ -3906,8 +4363,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
|
|
|
3906
4363
|
done(undefined);
|
|
3907
4364
|
tui.requestRender();
|
|
3908
4365
|
},
|
|
3909
|
-
};
|
|
3910
|
-
});
|
|
4366
|
+
}, theme);
|
|
4367
|
+
}, { overlay: true, overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" } });
|
|
3911
4368
|
if (decision) {
|
|
3912
4369
|
const accepted = await answerCheckpoint(store.runId, checkpoint.name, decision === "Approve", true);
|
|
3913
4370
|
if (!accepted)
|