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/src/index.ts
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import { fork, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { fork, spawn, type ChildProcess } 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 { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor, type AgentActivity, type AgentAccounting, type AgentAttempt, type AgentBudgetHooks, type AgentDefinition, type AgentProgress, type AgentSetupHook, type RegisteredAgentSetupHook, type SessionFactory } from "./agent-execution.js";
|
|
14
|
-
import {
|
|
13
|
+
import { copyToClipboard, getAgentDir, parseFrontmatter, highlightCode, truncateToVisualLines, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor, type AgentActivity, type AgentAccounting, type AgentAttempt, type AgentBudgetHooks, type AgentDefinition, type AgentProgress, type AgentProviderFailure, type AgentProviderRecovery, type AgentSetupHook, type RegisteredAgentSetupHook, type SessionFactory } from "./agent-execution.js";
|
|
15
|
+
import { herdrPaneId, openHerdrPane } from "./herdr.js";
|
|
15
16
|
import { acquireSessionLease, atomicWriteFile, listRunIds, RunStore, SessionLease, structuralPath as operationPath } from "./persistence.js";
|
|
16
17
|
import type { AwaitingCheckpoint, PersistedRun, WorktreeReference } from "./persistence.js";
|
|
17
18
|
|
|
18
19
|
export const RUN_STATES = ["queued", "running", "pausing", "paused", "awaiting_input", "completed", "failed", "stopped", "interrupted", "budget_exhausted"] as const;
|
|
19
20
|
export const AGENT_STATES = ["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"] as const;
|
|
20
|
-
export const WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"] as const;
|
|
21
|
+
export const WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree", "shell"] as const;
|
|
21
22
|
export type WorkflowCallKind = (typeof WORKFLOW_CALL_KINDS)[number];
|
|
22
23
|
export const WORKFLOW_RUN_STARTED_EVENT = "workflow:run-started";
|
|
23
24
|
export const WORKFLOW_RUN_RESUMED_EVENT = "workflow:run-resumed";
|
|
@@ -32,7 +33,7 @@ export const WORKFLOW_WORKTREE_CREATED_EVENT = "workflow:worktree-created";
|
|
|
32
33
|
const SETTLED_AGENT_STATES: ReadonlySet<AgentState> = new Set(["completed", "failed", "cancelled"]);
|
|
33
34
|
export const ERROR_CODES = [
|
|
34
35
|
"CONFIG_ERROR", "INVALID_SETTINGS", "INVALID_SYNTAX", "INVALID_METADATA", "DUPLICATE_NAME", "INVALID_SCHEMA", "UNKNOWN_MODEL", "UNKNOWN_TOOL", "UNKNOWN_AGENT_TYPE",
|
|
35
|
-
"RUN_OWNED", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID",
|
|
36
|
+
"RUN_OWNED", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "SHELL_FAILED", "AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID",
|
|
36
37
|
"CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
|
|
37
38
|
] as const;
|
|
38
39
|
|
|
@@ -41,6 +42,8 @@ export type AgentState = (typeof AGENT_STATES)[number];
|
|
|
41
42
|
export type WorkflowErrorCode = (typeof ERROR_CODES)[number];
|
|
42
43
|
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
43
44
|
export type JsonSchema = { [key: string]: JsonValue };
|
|
45
|
+
export interface ShellOptions { timeoutMs?: number; env?: Record<string, string> }
|
|
46
|
+
export interface ShellResult { exitCode: number | null; stdout: string; stderr: string }
|
|
44
47
|
export type BudgetDimension = "tokens" | "costUsd" | "durationMs" | "agentLaunches";
|
|
45
48
|
export interface BudgetLimits { soft?: number; hard?: number }
|
|
46
49
|
export type WorkflowBudget = Partial<Record<BudgetDimension, BudgetLimits>>;
|
|
@@ -69,19 +72,24 @@ export interface AgentResourcePolicy { globalSettingsPath: string; projectSettin
|
|
|
69
72
|
export interface AgentSetupSummary { hookNames: readonly string[]; model: ModelSpec; tools: readonly string[]; cwd: string; disabledAgentResources?: { skills: readonly string[]; extensions: readonly string[]; unmatchedSkills: readonly string[]; unmatchedExtensions: readonly string[] } }
|
|
70
73
|
export interface AgentAttemptSummary { attempt: number; sessionId: string; sessionFile: string; error?: { code: string; message: string }; accounting: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; setup?: AgentSetupSummary }
|
|
71
74
|
export interface WorkflowWorktreeCreatedEvent extends WorkflowEventBase { owner: string; branch: string; path: string; base: string }
|
|
75
|
+
export interface WorkflowWorktreeReference { readonly path: string; readonly branch: string }
|
|
72
76
|
export interface AgentRecord { id: string; name: string; label?: string; path: string; state: AgentState; parentId?: string; structuralPath?: readonly string[]; parentBreadcrumb?: string; worktreeOwner?: string; role?: string; requestedModel?: string; model: ModelSpec; tools: readonly string[]; attempts: number; attemptDetails?: readonly AgentAttemptSummary[]; accounting?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; toolCalls?: readonly { id: string; name: string; state: "running" | "completed" | "failed" }[]; activity?: AgentActivity | undefined }
|
|
73
77
|
export interface WorkflowRunEvent { type: string; message: string }
|
|
74
|
-
export interface RunRecord { id: string; workflowName: string; cwd: string; sessionId: string; state: RunState; phase?: string; agents: readonly AgentRecord[]; error?: WorkflowErrorShape; budget?: WorkflowBudget; budgetVersion?: number; usage?: WorkflowBudgetUsage; budgetEvents?: readonly BudgetEvent[]; events?: readonly WorkflowRunEvent[] }
|
|
75
|
-
export const LAUNCH_SNAPSHOT_IDENTITY_VERSION =
|
|
76
|
-
export interface LaunchSnapshot { identityVersion?: number; script: string; args: JsonValue; metadata: WorkflowMetadata; settings: WorkflowSettings; budget?: WorkflowBudget; settingsPath?: string; modelAliases?: Readonly<Record<string, string>>; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[]; roles?: Readonly<Record<string, AgentDefinition>>; projectRoles?: readonly string[]; schemas: readonly JsonSchema[] }
|
|
78
|
+
export interface RunRecord { id: string; workflowName: string; cwd: string; sessionId: string; state: RunState; parentRunId?: string; phase?: string; agents: readonly AgentRecord[]; error?: WorkflowErrorShape; budget?: WorkflowBudget; budgetVersion?: number; usage?: WorkflowBudgetUsage; budgetEvents?: readonly BudgetEvent[]; events?: readonly WorkflowRunEvent[] }
|
|
79
|
+
export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 4;
|
|
80
|
+
export interface LaunchSnapshot { identityVersion?: number; launchKind?: "inline" | "function"; functionName?: string; script: string; args: JsonValue; metadata: WorkflowMetadata; settings: WorkflowSettings; budget?: WorkflowBudget; settingsPath?: string; modelAliases?: Readonly<Record<string, string>>; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[]; roles?: Readonly<Record<string, AgentDefinition>>; projectRoles?: readonly string[]; schemas: readonly JsonSchema[] }
|
|
77
81
|
export interface PreflightCapabilities { models: ReadonlySet<string>; tools: ReadonlySet<string>; agentTypes: ReadonlySet<string>; modelAliases?: Readonly<Record<string, string>>; knownModels?: ReadonlySet<string>; settingsPath?: string; skipModelAvailability?: boolean }
|
|
78
82
|
export interface PreflightResult { metadata: WorkflowMetadata; referenced: { phases: readonly string[]; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[] }; schemas: readonly JsonSchema[]; dynamicAgentRoles: boolean }
|
|
79
83
|
export interface WorkflowOrchestrationContext {
|
|
80
84
|
agent: (...args: readonly unknown[]) => Promise<JsonValue>;
|
|
85
|
+
shell: (command: string, options?: ShellOptions) => Promise<ShellResult>;
|
|
81
86
|
prompt: (template: string, values: Readonly<Record<string, JsonValue>>) => string;
|
|
82
87
|
parallel: (...args: readonly unknown[]) => Promise<JsonValue>;
|
|
83
88
|
pipeline: (...args: readonly unknown[]) => Promise<JsonValue>;
|
|
84
|
-
withWorktree:
|
|
89
|
+
withWorktree: {
|
|
90
|
+
(callback: WorkflowWorktreeCallback): Promise<JsonValue>;
|
|
91
|
+
(name: string, callback: WorkflowWorktreeCallback): Promise<JsonValue>;
|
|
92
|
+
};
|
|
85
93
|
checkpoint: (...args: readonly unknown[]) => Promise<boolean>;
|
|
86
94
|
phase: (name: string) => void;
|
|
87
95
|
log: (message: string) => void;
|
|
@@ -91,16 +99,45 @@ export interface WorkflowFunctionContext extends WorkflowOrchestrationContext {
|
|
|
91
99
|
run: Readonly<WorkflowRunContext>;
|
|
92
100
|
invoke: (name: string, input: Readonly<Record<string, JsonValue>>) => Promise<JsonValue>;
|
|
93
101
|
}
|
|
102
|
+
export type WorkflowWorktreeCallback = (reference: Readonly<WorkflowWorktreeReference>) => JsonValue | Promise<JsonValue>;
|
|
94
103
|
export interface WorkflowFunction { description: string; input: JsonSchema; output: JsonSchema; run: (input: Readonly<Record<string, JsonValue>>, context: Readonly<WorkflowFunctionContext>) => Promise<JsonValue> | JsonValue }
|
|
95
104
|
export interface WorkflowVariable { description: string; schema: JsonSchema; resolve: (run: Readonly<WorkflowRunContext>) => Promise<JsonValue> | JsonValue }
|
|
96
|
-
export interface
|
|
97
|
-
export interface WorkflowExtension { version: string; headline: string; description: string; functions?: Readonly<Record<string, WorkflowFunction>>; variables?: Readonly<Record<string, WorkflowVariable>>; workflows?: Readonly<Record<string, WorkflowScriptDefinition>>; agentSetupHooks?: Readonly<Record<string, AgentSetupHook>> }
|
|
105
|
+
export interface WorkflowExtension { version: string; headline: string; description: string; functions?: Readonly<Record<string, WorkflowFunction>>; variables?: Readonly<Record<string, WorkflowVariable>>; agentSetupHooks?: Readonly<Record<string, AgentSetupHook>> }
|
|
98
106
|
export interface WorkflowJournal { get(path: string): JsonValue | undefined; put(path: string, value: JsonValue): void }
|
|
99
107
|
|
|
100
108
|
export class WorkflowError extends Error {
|
|
101
109
|
constructor(public readonly code: WorkflowErrorCode, message: string) { super(message); this.name = "WorkflowError"; }
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
export interface WorkflowFailureAgent {
|
|
113
|
+
id: string;
|
|
114
|
+
label?: string;
|
|
115
|
+
role?: string;
|
|
116
|
+
structuralPath: readonly string[];
|
|
117
|
+
attempt: number;
|
|
118
|
+
sessionId?: string;
|
|
119
|
+
sessionFile?: string;
|
|
120
|
+
}
|
|
121
|
+
export interface WorkflowSiblingAgent {
|
|
122
|
+
id: string;
|
|
123
|
+
label?: string;
|
|
124
|
+
role?: string;
|
|
125
|
+
structuralPath: readonly string[];
|
|
126
|
+
}
|
|
127
|
+
export interface WorkflowFailureDiagnostics {
|
|
128
|
+
runId: string;
|
|
129
|
+
workflowName: string;
|
|
130
|
+
state: RunState;
|
|
131
|
+
failedAt: string | null;
|
|
132
|
+
error: WorkflowErrorShape;
|
|
133
|
+
failedAgent?: WorkflowFailureAgent;
|
|
134
|
+
completedSiblingAgents?: readonly WorkflowSiblingAgent[];
|
|
135
|
+
completedSiblingPaths: readonly (readonly string[])[];
|
|
136
|
+
artifacts: { runDirectory: string; statePath: string; journalPath: string };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const WORKFLOW_FAILURE_DIAGNOSTICS = Symbol("workflowFailureDiagnostics");
|
|
140
|
+
|
|
104
141
|
const WORKFLOW_AUTHORED_ERROR = Symbol("workflowAuthoredError");
|
|
105
142
|
type WorkerErrorShape = WorkflowErrorShape & { authored?: boolean; failedAt?: string };
|
|
106
143
|
|
|
@@ -130,12 +167,13 @@ const WORKFLOW_ERROR_PROSE: Record<WorkflowErrorCode, (detail: string) => string
|
|
|
130
167
|
INVALID_SCHEMA: (detail) => `The workflow schema is invalid: ${detail}.`,
|
|
131
168
|
REGISTRY_FROZEN: (detail) => `Workflow extension registration is closed: ${detail}.`,
|
|
132
169
|
GLOBAL_COLLISION: (detail) => `The workflow global name is already in use: ${detail}.`,
|
|
133
|
-
MISSING_WORKFLOW: (detail) => `The workflow
|
|
170
|
+
MISSING_WORKFLOW: (detail) => `The registered workflow function is unavailable: ${detail}.`,
|
|
134
171
|
UNKNOWN_MODEL: (detail) => `The workflow requested the unavailable model ${detail.replace(/^(?:Unknown model(?: for role [^:]+)?|Invalid model spec):\s*/, "")}.`,
|
|
135
172
|
UNKNOWN_TOOL: (detail) => `The workflow requested the unavailable tool ${detail.replace(/^Unknown tool:\s*/, "")}.`,
|
|
136
173
|
UNKNOWN_AGENT_TYPE: (detail) => `The workflow requested the unavailable agent role ${detail.replace(/^Unknown agent role:\s*/, "")}.`,
|
|
137
174
|
RUN_OWNED: (detail) => /already owned|active ownership/.test(detail) ? "The workflow session is already in use." : `The workflow session is already in use: ${detail}.`,
|
|
138
175
|
RPC_LIMIT_EXCEEDED: (detail) => `The workflow communication data exceeded its size limit: ${detail}.`,
|
|
176
|
+
SHELL_FAILED: (detail) => `The workflow shell command failed: ${detail}.`,
|
|
139
177
|
AGENT_TIMEOUT: (detail) => `The workflow agent timed out: ${detail}.`,
|
|
140
178
|
AGENT_FAILED: (detail) => `The workflow agent failed: ${detail}.`,
|
|
141
179
|
RESULT_INVALID: (detail) => `The workflow produced an invalid result: ${detail}.`,
|
|
@@ -686,6 +724,7 @@ function hasIdentifier(node: acorn.AnyNode, name: string): boolean {
|
|
|
686
724
|
const INTERNAL_AGENT_NAME = "__pi_extensible_workflows_agent";
|
|
687
725
|
const INTERNAL_CONVERSATION_NAME = "__pi_extensible_workflows_conversation";
|
|
688
726
|
const INTERNAL_WORKTREE_NAME = "__pi_extensible_workflows_withWorktree";
|
|
727
|
+
const INTERNAL_SHELL_NAME = "__pi_extensible_workflows_shell";
|
|
689
728
|
|
|
690
729
|
function callHasTrailingComma(source: string, call: WorkflowCall): boolean {
|
|
691
730
|
let previous: acorn.Token | undefined;
|
|
@@ -704,12 +743,13 @@ function instrumentWorkflow(script: string): string {
|
|
|
704
743
|
if (hasIdentifier(program, INTERNAL_AGENT_NAME)) fail("INVALID_METADATA", `${INTERNAL_AGENT_NAME} is reserved for workflow agent instrumentation`);
|
|
705
744
|
if (hasIdentifier(program, INTERNAL_CONVERSATION_NAME)) fail("INVALID_METADATA", `${INTERNAL_CONVERSATION_NAME} is reserved for workflow conversation instrumentation`);
|
|
706
745
|
if (hasIdentifier(program, INTERNAL_WORKTREE_NAME)) fail("INVALID_METADATA", `${INTERNAL_WORKTREE_NAME} is reserved for workflow withWorktree instrumentation`);
|
|
707
|
-
|
|
746
|
+
if (hasIdentifier(program, INTERNAL_SHELL_NAME)) fail("INVALID_METADATA", `${INTERNAL_SHELL_NAME} is reserved for workflow shell instrumentation`);
|
|
747
|
+
const calls = workflowCalls(program).filter((call) => ["agent", "conversation", "withWorktree", "shell"].includes(call.callee.name));
|
|
708
748
|
const edits = calls.flatMap((call) => {
|
|
709
749
|
const callSite = `${String(call.start)}:${String(call.end)}`;
|
|
710
750
|
const hiddenArgument = call.arguments.length === 0 || callHasTrailingComma(body, call) ? "" : ", ";
|
|
711
751
|
return [
|
|
712
|
-
{ 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 },
|
|
752
|
+
{ 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 },
|
|
713
753
|
{ start: call.end - 1, end: call.end - 1, text: `${hiddenArgument}${JSON.stringify(callSite)}` },
|
|
714
754
|
];
|
|
715
755
|
}).sort((left, right) => right.start - left.start);
|
|
@@ -821,6 +861,18 @@ function validateAgentOptions(value: unknown): Readonly<Record<string, JsonValue
|
|
|
821
861
|
if (typeof value.role === "string" && ["model", "thinking", "tools"].some((key) => Object.prototype.hasOwnProperty.call(value, key))) fail("INVALID_METADATA", "Role agents must not specify model, thinking, or tools");
|
|
822
862
|
return value;
|
|
823
863
|
}
|
|
864
|
+
const SHELL_OPTION_KEYS = new Set(["timeoutMs", "env"]);
|
|
865
|
+
function validateShellOptions(value: unknown): ShellOptions {
|
|
866
|
+
if (value === undefined) return {};
|
|
867
|
+
if (!object(value) || !jsonValue(value) || Object.keys(value).some((key) => !SHELL_OPTION_KEYS.has(key))) fail("INVALID_METADATA", "shell options must contain only timeoutMs and env");
|
|
868
|
+
if (value.timeoutMs !== undefined && !positiveInteger(value.timeoutMs)) fail("INVALID_METADATA", "shell timeoutMs must be a positive integer");
|
|
869
|
+
if (value.env !== undefined && (!object(value.env) || Object.values(value.env).some((entry) => typeof entry !== "string"))) fail("INVALID_METADATA", "shell env must be an object of strings");
|
|
870
|
+
return { ...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }), ...(value.env === undefined ? {} : { env: value.env as Record<string, string> }) };
|
|
871
|
+
}
|
|
872
|
+
function validateShellCommand(value: unknown): string {
|
|
873
|
+
if (typeof value !== "string") fail("INVALID_METADATA", "shell command must be a string");
|
|
874
|
+
return value;
|
|
875
|
+
}
|
|
824
876
|
|
|
825
877
|
type StaticValue = { known: true; value: unknown } | { known: false };
|
|
826
878
|
|
|
@@ -901,6 +953,7 @@ export function inspectWorkflowScript(script: string): StaticWorkflowCall[] {
|
|
|
901
953
|
return { ...base, ...(retries.known && typeof retries.value === "number" ? { retries: retries.value } : {}), ...(outputSchema.known && object(outputSchema.value) ? { outputSchema: outputSchema.value as JsonSchema } : {}), ...(optionKeys.length ? { options: knownOptions, optionKeys } : {}) };
|
|
902
954
|
}
|
|
903
955
|
if (kind === "checkpoint") return { ...placement, kind, start: call.start, end: call.end, name: staticString(propertyNode(first, "name")), prompt: staticString(propertyNode(first, "prompt")), model: null, role: null };
|
|
956
|
+
if (kind === "shell") return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
|
|
904
957
|
return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
|
|
905
958
|
});
|
|
906
959
|
}
|
|
@@ -914,6 +967,14 @@ function validateStaticAgentOptions(node: acorn.AnyNode | undefined, aliases: Re
|
|
|
914
967
|
if (value.known) validateAgentOption(key, value.value, aliases, knownModels, settingsPath);
|
|
915
968
|
}
|
|
916
969
|
}
|
|
970
|
+
function validateStaticShellOptions(call: WorkflowCall): void {
|
|
971
|
+
if (call.arguments.some((argument) => argument.type === "SpreadElement")) return;
|
|
972
|
+
if (call.arguments.length !== 1 && call.arguments.length !== 2) fail("INVALID_METADATA", "shell requires a command string and optional options");
|
|
973
|
+
const command = staticValue(callArgument(call, 0));
|
|
974
|
+
if (command.known) validateShellCommand(command.value);
|
|
975
|
+
const options = staticValue(callArgument(call, 1));
|
|
976
|
+
if (options.known) validateShellOptions(options.value);
|
|
977
|
+
}
|
|
917
978
|
|
|
918
979
|
function validateStaticWithWorktree(call: WorkflowCall): void {
|
|
919
980
|
if (call.arguments.some((argument) => argument.type === "SpreadElement")) return;
|
|
@@ -928,16 +989,18 @@ function validateStaticWithWorktree(call: WorkflowCall): void {
|
|
|
928
989
|
|
|
929
990
|
export interface WorkflowCatalogFunction { name: string; version: string; headline: string; extensionDescription: string; description: string; input: JsonSchema; output: JsonSchema }
|
|
930
991
|
export interface WorkflowCatalogVariable { name: string; version: string; headline: string; extensionDescription: string; description: string; schema: JsonSchema }
|
|
931
|
-
export interface
|
|
932
|
-
export interface
|
|
933
|
-
|
|
992
|
+
export interface WorkflowCatalog { functions: readonly WorkflowCatalogFunction[]; variables: readonly WorkflowCatalogVariable[]; modelAliases?: Readonly<Record<string, string>> }
|
|
993
|
+
export interface WorkflowCatalogIndexFunction { name: string; description: string; input: JsonSchema }
|
|
994
|
+
export interface WorkflowCatalogIndexVariable { name: string; description: string; schema: JsonSchema }
|
|
995
|
+
export interface WorkflowCatalogIndex { functions: readonly WorkflowCatalogIndexFunction[]; variables: readonly WorkflowCatalogIndexVariable[]; modelAliases?: Readonly<Record<string, string>> }
|
|
996
|
+
export interface WorkflowCatalogError { error: { code: "NOT_FOUND"; name: string; message: string } }
|
|
997
|
+
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"]);
|
|
934
998
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
935
999
|
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
936
1000
|
|
|
937
1001
|
export class WorkflowRegistry {
|
|
938
1002
|
readonly #extensions = new Set<Readonly<WorkflowExtension>>();
|
|
939
1003
|
readonly #globals = new Map<string, string>();
|
|
940
|
-
readonly #workflows = new Map<string, WorkflowScriptDefinition>();
|
|
941
1004
|
readonly #hooks = new Map<string, RegisteredAgentSetupHook>();
|
|
942
1005
|
#frozen = false;
|
|
943
1006
|
|
|
@@ -946,12 +1009,12 @@ export class WorkflowRegistry {
|
|
|
946
1009
|
|
|
947
1010
|
register(extension: WorkflowExtension): void {
|
|
948
1011
|
if (this.#frozen) fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
|
|
949
|
-
if (
|
|
1012
|
+
if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows")) fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
|
|
1013
|
+
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()) fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
|
|
950
1014
|
const functions = extension.functions ?? {};
|
|
951
1015
|
const variables = extension.variables ?? {};
|
|
952
|
-
const workflows = extension.workflows ?? {};
|
|
953
1016
|
const agentSetupHooks = extension.agentSetupHooks ?? {};
|
|
954
|
-
if (!object(functions) || !object(variables) || !object(
|
|
1017
|
+
if (!object(functions) || !object(variables) || !object(agentSetupHooks) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(agentSetupHooks).length === 0)) fail("INVALID_METADATA", "Workflow extensions require functions, variables, or agent setup hooks");
|
|
955
1018
|
const names = [...Object.keys(functions), ...Object.keys(variables)];
|
|
956
1019
|
if (new Set(names).size !== names.length) fail("GLOBAL_COLLISION", "Global name collision inside extension");
|
|
957
1020
|
for (const name of names) {
|
|
@@ -969,46 +1032,54 @@ export class WorkflowRegistry {
|
|
|
969
1032
|
if (!object(variable) || Object.keys(variable).some((key) => !["description", "schema", "resolve"].includes(key)) || typeof variable.description !== "string" || !variable.description.trim() || typeof variable.resolve !== "function") fail("INVALID_METADATA", `Invalid workflow variable: ${name}`);
|
|
970
1033
|
validateSchema(variable.schema, `${name} schema`);
|
|
971
1034
|
}
|
|
972
|
-
for (const [name, workflow] of Object.entries(workflows)) {
|
|
973
|
-
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()) fail("INVALID_METADATA", `Invalid workflow script: ${name}`);
|
|
974
|
-
if (this.#workflows.has(name)) fail("DUPLICATE_NAME", `Reusable workflow already registered: ${name}`);
|
|
975
|
-
parseWorkflow(workflow.script);
|
|
976
|
-
}
|
|
977
1035
|
for (const [name, hook] of Object.entries(agentSetupHooks)) {
|
|
978
1036
|
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))) fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
|
|
979
1037
|
if (this.#hooks.has(name)) fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
|
|
980
1038
|
}
|
|
981
|
-
const stored = deepFreeze({ ...extension, functions, variables,
|
|
1039
|
+
const stored = deepFreeze({ ...extension, functions, variables, agentSetupHooks });
|
|
982
1040
|
this.#extensions.add(stored);
|
|
983
1041
|
for (const name of names) this.#globals.set(name, name);
|
|
984
|
-
for (const [name, workflow] of Object.entries(workflows)) this.#workflows.set(name, workflow);
|
|
985
1042
|
for (const [name, hook] of Object.entries(agentSetupHooks)) this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
|
|
986
1043
|
}
|
|
987
1044
|
|
|
988
|
-
|
|
989
|
-
if (!IDENTIFIER.test(name)) fail("MISSING_WORKFLOW", `Registered
|
|
990
|
-
const
|
|
991
|
-
if (!
|
|
992
|
-
return
|
|
1045
|
+
function(name: string): WorkflowFunction {
|
|
1046
|
+
if (!IDENTIFIER.test(name)) fail("MISSING_WORKFLOW", `Registered functions require an unqualified name: ${name}`);
|
|
1047
|
+
const fn = [...this.#extensions].find((extension) => extension.functions?.[name])?.functions?.[name];
|
|
1048
|
+
if (!fn) fail("MISSING_WORKFLOW", `Registered function is unavailable: ${name}; the separate registered-workflow format was removed`);
|
|
1049
|
+
return fn;
|
|
993
1050
|
}
|
|
994
1051
|
|
|
995
|
-
|
|
996
|
-
return Object.freeze(Object.fromEntries(this.#
|
|
1052
|
+
functions(): Readonly<Record<string, WorkflowFunction>> {
|
|
1053
|
+
return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.entries(extension.functions ?? {}))));
|
|
997
1054
|
}
|
|
998
1055
|
|
|
999
1056
|
catalog(): WorkflowCatalog {
|
|
1000
1057
|
const functions: WorkflowCatalogFunction[] = [];
|
|
1001
1058
|
const variables: WorkflowCatalogVariable[] = [];
|
|
1002
|
-
const workflows: WorkflowCatalogWorkflow[] = [];
|
|
1003
1059
|
for (const extension of this.#extensions) {
|
|
1004
1060
|
for (const [name, fn] of Object.entries(extension.functions ?? {})) functions.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: fn.description, input: structuredClone(fn.input), output: structuredClone(fn.output) });
|
|
1005
1061
|
for (const [name, variable] of Object.entries(extension.variables ?? {})) variables.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: variable.description, schema: structuredClone(variable.schema) });
|
|
1006
|
-
for (const [name, workflow] of Object.entries(extension.workflows ?? {})) workflows.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: workflow.description });
|
|
1007
1062
|
}
|
|
1008
1063
|
let aliases: Readonly<Record<string, string>> | undefined;
|
|
1009
1064
|
try { aliases = loadSettings().modelAliases; } catch { aliases = undefined; }
|
|
1010
1065
|
const sort = (left: { name: string }, right: { name: string }) => left.name.localeCompare(right.name);
|
|
1011
|
-
return deepFreeze({ functions: functions.sort(sort), variables: variables.sort(sort),
|
|
1066
|
+
return deepFreeze({ functions: functions.sort(sort), variables: variables.sort(sort), ...(aliases ? { modelAliases: structuredClone(aliases) } : {}) });
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
catalogIndex(): WorkflowCatalogIndex {
|
|
1070
|
+
const catalog = this.catalog();
|
|
1071
|
+
return deepFreeze({
|
|
1072
|
+
functions: catalog.functions.map(({ name, description, input }) => ({ name, description, input: structuredClone(input) })),
|
|
1073
|
+
variables: catalog.variables.map(({ name, description, schema }) => ({ name, description, schema: structuredClone(schema) })),
|
|
1074
|
+
...(catalog.modelAliases ? { modelAliases: structuredClone(catalog.modelAliases) } : {}),
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
catalogDetail(name: string): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogError {
|
|
1079
|
+
const catalog = this.catalog();
|
|
1080
|
+
const entry = catalog.functions.find((candidate) => candidate.name === name) ?? catalog.variables.find((candidate) => candidate.name === name);
|
|
1081
|
+
if (entry) return entry;
|
|
1082
|
+
return deepFreeze({ error: { code: "NOT_FOUND", name, message: `No registered workflow function or variable is available: ${name}` } });
|
|
1012
1083
|
}
|
|
1013
1084
|
|
|
1014
1085
|
globals(): Readonly<Record<string, { name: string }>> {
|
|
@@ -1016,15 +1087,14 @@ export class WorkflowRegistry {
|
|
|
1016
1087
|
}
|
|
1017
1088
|
|
|
1018
1089
|
async invokeFunction(name: string, input: unknown, context: Readonly<WorkflowFunctionContext>, path: string, journal: WorkflowJournal): Promise<JsonValue> {
|
|
1019
|
-
const fn =
|
|
1020
|
-
if (!fn) fail("MISSING_WORKFLOW", `Workflow function is unavailable: ${name}`);
|
|
1090
|
+
const fn = this.function(name);
|
|
1021
1091
|
if (!object(input) || !jsonValue(input) || !Value.Check(fn.input, input)) fail("RESULT_INVALID", `Invalid input for ${name}`);
|
|
1022
1092
|
const replayed = journal.get(path);
|
|
1023
1093
|
if (replayed !== undefined) {
|
|
1024
1094
|
if (!jsonValue(replayed) || !Value.Check(fn.output, replayed)) fail("RESULT_INVALID", `Invalid replay for ${name}`);
|
|
1025
1095
|
return structuredClone(replayed);
|
|
1026
1096
|
}
|
|
1027
|
-
const result: unknown = 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 }));
|
|
1097
|
+
const result: unknown = 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 }));
|
|
1028
1098
|
if (!jsonValue(result) || !Value.Check(fn.output, result)) fail("RESULT_INVALID", `Invalid output from ${name}`);
|
|
1029
1099
|
const stored = structuredClone(result);
|
|
1030
1100
|
journal.put(path, stored);
|
|
@@ -1038,7 +1108,7 @@ export class WorkflowRegistry {
|
|
|
1038
1108
|
return [...this.#hooks.values()].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
1039
1109
|
}
|
|
1040
1110
|
}
|
|
1041
|
-
type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "
|
|
1111
|
+
type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "agentSetupHooks">;
|
|
1042
1112
|
interface WorkflowRegistryHost { api: WorkflowRegistryApi }
|
|
1043
1113
|
const WORKFLOW_REGISTRY_KEY = Symbol.for("pi-extensible-workflows.workflow-registry");
|
|
1044
1114
|
const globalRegistry = globalThis as typeof globalThis & Record<symbol, WorkflowRegistryHost | undefined>;
|
|
@@ -1047,9 +1117,11 @@ function createWorkflowRegistryApi(registry: WorkflowRegistry): WorkflowRegistry
|
|
|
1047
1117
|
get frozen() { return registry.frozen; },
|
|
1048
1118
|
freeze: () => { registry.freeze(); },
|
|
1049
1119
|
register: (extension) => { registry.register(extension); },
|
|
1050
|
-
|
|
1051
|
-
|
|
1120
|
+
function: (name) => registry.function(name),
|
|
1121
|
+
functions: () => registry.functions(),
|
|
1052
1122
|
catalog: () => registry.catalog(),
|
|
1123
|
+
catalogIndex: () => registry.catalogIndex(),
|
|
1124
|
+
catalogDetail: (name) => registry.catalogDetail(name),
|
|
1053
1125
|
globals: () => registry.globals(),
|
|
1054
1126
|
invokeFunction: (...args) => registry.invokeFunction(...args),
|
|
1055
1127
|
variables: () => registry.variables(),
|
|
@@ -1069,26 +1141,29 @@ function loadingRegistry(): WorkflowRegistryApi { return workflowRegistryHost().
|
|
|
1069
1141
|
beginWorkflowExtensionLoading();
|
|
1070
1142
|
export function registerWorkflowExtension(extension: WorkflowExtension): void { loadingRegistry().register(extension); }
|
|
1071
1143
|
export function workflowCatalog(): WorkflowCatalog { return loadingRegistry().catalog(); }
|
|
1072
|
-
export function
|
|
1144
|
+
export function workflowCatalogIndex(): WorkflowCatalogIndex { return loadingRegistry().catalogIndex(); }
|
|
1145
|
+
export function workflowCatalogDetail(name: string): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogError { return loadingRegistry().catalogDetail(name); }
|
|
1146
|
+
export function registeredWorkflowFunctions(): Readonly<Record<string, WorkflowFunction>> { return loadingRegistry().functions(); }
|
|
1073
1147
|
|
|
1074
1148
|
|
|
1075
1149
|
export function formatWorkflowPreview(args: { script?: unknown; workflow?: unknown; name?: unknown; description?: unknown }): string {
|
|
1076
1150
|
const name = typeof args.name === "string" && args.name.trim() ? args.name.trim() : typeof args.workflow === "string" && args.workflow.trim() ? args.workflow : "workflow";
|
|
1077
|
-
if (typeof args.script !== "string" || !args.script.trim()) return `workflow ${name}${typeof args.workflow === "string" ? "\nRegistered
|
|
1151
|
+
if (typeof args.script !== "string" || !args.script.trim()) return `workflow ${name}${typeof args.workflow === "string" ? "\nRegistered function" : ""}`;
|
|
1078
1152
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
1079
1153
|
}
|
|
1080
1154
|
export const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
1081
1155
|
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow";
|
|
1082
|
-
export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow that orchestrates subagents. Inline scripts require a name; registered
|
|
1156
|
+
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.";
|
|
1083
1157
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
1084
1158
|
name: Type.Optional(Type.String({ description: "Workflow name for inline scripts" })),
|
|
1085
1159
|
description: Type.Optional(Type.String({ description: "Optional human-readable workflow description" })),
|
|
1086
1160
|
script: Type.Optional(Type.String({ description: "Immutable workflow source without metadata" })),
|
|
1087
|
-
workflow: Type.Optional(Type.String({ description: "Registered reusable
|
|
1161
|
+
workflow: Type.Optional(Type.String({ description: "Registered reusable function as an unqualified name" })),
|
|
1088
1162
|
args: Type.Optional(Type.Unknown({ description: "JSON-compatible workflow arguments" })),
|
|
1089
1163
|
foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of running in the background" })),
|
|
1090
1164
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16 })),
|
|
1091
1165
|
budget: Type.Optional(Type.Unknown({ description: "Optional aggregate soft and hard run budgets" })),
|
|
1166
|
+
parentRunId: Type.Optional(Type.String({ description: "Terminal run whose named worktrees may be reused" })),
|
|
1092
1167
|
});
|
|
1093
1168
|
|
|
1094
1169
|
function hasDynamicAgentRole(node: acorn.AnyNode | undefined): boolean {
|
|
@@ -1109,8 +1184,10 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
|
|
|
1109
1184
|
if (hasIdentifier(program, INTERNAL_AGENT_NAME)) fail("INVALID_METADATA", `${INTERNAL_AGENT_NAME} is reserved for workflow agent instrumentation`);
|
|
1110
1185
|
if (hasIdentifier(program, INTERNAL_CONVERSATION_NAME)) fail("INVALID_METADATA", `${INTERNAL_CONVERSATION_NAME} is reserved for workflow conversation instrumentation`);
|
|
1111
1186
|
if (hasIdentifier(program, INTERNAL_WORKTREE_NAME)) fail("INVALID_METADATA", `${INTERNAL_WORKTREE_NAME} is reserved for workflow withWorktree instrumentation`);
|
|
1187
|
+
if (hasIdentifier(program, INTERNAL_SHELL_NAME)) fail("INVALID_METADATA", `${INTERNAL_SHELL_NAME} is reserved for workflow shell instrumentation`);
|
|
1112
1188
|
validateDirectPrimitiveReferences(program, "withWorktree");
|
|
1113
1189
|
validateDirectPrimitiveReferences(program, "conversation");
|
|
1190
|
+
validateDirectPrimitiveReferences(program, "shell");
|
|
1114
1191
|
for (const [index, schema] of schemas.entries()) validateSchema(schema, `schema[${String(index)}]`);
|
|
1115
1192
|
const calls = workflowCalls(program);
|
|
1116
1193
|
const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase): phase is string => phase !== undefined);
|
|
@@ -1121,6 +1198,7 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
|
|
|
1121
1198
|
validateStaticAgentOptions(call.arguments[1], capabilities.modelAliases ?? {}, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath);
|
|
1122
1199
|
}
|
|
1123
1200
|
if (operation === "withWorktree") validateStaticWithWorktree(call);
|
|
1201
|
+
if (operation === "shell") validateStaticShellOptions(call);
|
|
1124
1202
|
if ((operation === "parallel" || operation === "pipeline") && call.arguments.some((argument) => argument.type === "SpreadElement")) continue;
|
|
1125
1203
|
if (operation === "checkpoint" && stableName(call.arguments[0]) === false) fail("INVALID_METADATA", `${operation} requires a stable explicit name`);
|
|
1126
1204
|
if (operation === "parallel" && (call.arguments.length !== 2 || !literalString(call.arguments[0])?.trim() || call.arguments[1]?.type !== "ObjectExpression")) fail("INVALID_METADATA", "parallel requires an operation name string and tasks record");
|
|
@@ -1155,6 +1233,7 @@ export interface WorkflowValidationParameters {
|
|
|
1155
1233
|
description?: string;
|
|
1156
1234
|
script?: string;
|
|
1157
1235
|
workflow?: string;
|
|
1236
|
+
args?: unknown;
|
|
1158
1237
|
}
|
|
1159
1238
|
|
|
1160
1239
|
export interface WorkflowValidationContext {
|
|
@@ -1165,6 +1244,7 @@ export interface WorkflowValidationContext {
|
|
|
1165
1244
|
modelAliases?: Readonly<Record<string, string>>;
|
|
1166
1245
|
knownModels?: ReadonlySet<string>;
|
|
1167
1246
|
settingsPath?: string;
|
|
1247
|
+
agentDir?: string;
|
|
1168
1248
|
}
|
|
1169
1249
|
|
|
1170
1250
|
|
|
@@ -1174,21 +1254,27 @@ export interface ValidatedWorkflowLaunch {
|
|
|
1174
1254
|
agentDefinitions: Readonly<Record<string, AgentDefinition>>;
|
|
1175
1255
|
projectAgentDefinitions: Readonly<Record<string, AgentDefinition>>;
|
|
1176
1256
|
roleNames: readonly string[];
|
|
1257
|
+
functionName?: string;
|
|
1177
1258
|
}
|
|
1178
1259
|
|
|
1260
|
+
function functionLaunchScript(name: string): string { return `return await ${name}(args);`; }
|
|
1261
|
+
|
|
1179
1262
|
export function validateWorkflowLaunch(params: WorkflowValidationParameters, context: WorkflowValidationContext): ValidatedWorkflowLaunch {
|
|
1180
1263
|
return validateWorkflowLaunchWithRegistry(params, context, loadingRegistry());
|
|
1181
1264
|
}
|
|
1182
1265
|
function validateWorkflowLaunchWithRegistry(params: WorkflowValidationParameters, context: WorkflowValidationContext, registry: WorkflowRegistryApi): ValidatedWorkflowLaunch {
|
|
1183
1266
|
if (Object.prototype.hasOwnProperty.call(params, "maxAgentLaunches")) fail("INVALID_METADATA", "maxAgentLaunches has been removed; use budget.agentLaunches");
|
|
1184
1267
|
if (params.script !== undefined && params.workflow !== undefined) fail("INVALID_METADATA", "Provide either script or workflow, not both");
|
|
1185
|
-
const
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
|
|
1268
|
+
const functionName = typeof params.workflow === "string" ? params.workflow : undefined;
|
|
1269
|
+
const fn = functionName === undefined ? undefined : registry.function(functionName);
|
|
1270
|
+
const args = params.args === undefined ? null : params.args;
|
|
1271
|
+
if (functionName !== undefined && fn && (!object(args) || !jsonValue(args) || !Value.Check(fn.input, args))) fail("RESULT_INVALID", `Invalid input for ${functionName}`);
|
|
1272
|
+
const script = functionName !== undefined && fn ? functionLaunchScript(functionName) : typeof params.script === "string" && params.script.trim() ? params.script : "";
|
|
1273
|
+
if (!script) fail("INVALID_SYNTAX", "Provide script or registered function");
|
|
1274
|
+
const workflowName = functionName ?? (typeof params.name === "string" && params.name.trim() ? params.name.trim() : "");
|
|
1189
1275
|
if (!workflowName) fail("INVALID_METADATA", "Inline workflows require name");
|
|
1190
|
-
const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } :
|
|
1191
|
-
const globalAgentDefinitions = loadAgentDefinitions(context.cwd,
|
|
1276
|
+
const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } : fn?.description ? { description: fn.description } : {}) });
|
|
1277
|
+
const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false);
|
|
1192
1278
|
const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};
|
|
1193
1279
|
const agentDefinitions = deepFreeze({ ...globalAgentDefinitions, ...projectAgentDefinitions });
|
|
1194
1280
|
const aliases = context.modelAliases ?? {};
|
|
@@ -1196,7 +1282,7 @@ function validateWorkflowLaunchWithRegistry(params: WorkflowValidationParameters
|
|
|
1196
1282
|
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);
|
|
1197
1283
|
const roleNames = checked.dynamicAgentRoles ? Object.keys(agentDefinitions) : checked.referenced.agentTypes;
|
|
1198
1284
|
validateRolePolicies(agentDefinitions, roleNames, context.availableModels, context.rootTools, aliases, knownModels, context.settingsPath);
|
|
1199
|
-
return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames };
|
|
1285
|
+
return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, ...(functionName ? { functionName } : {}) };
|
|
1200
1286
|
}
|
|
1201
1287
|
|
|
1202
1288
|
function deepFreeze<T>(value: T): T {
|
|
@@ -1210,21 +1296,34 @@ function deepFreeze<T>(value: T): T {
|
|
|
1210
1296
|
type LaunchSnapshotInput = Omit<LaunchSnapshot, "identityVersion"> & { identityVersion?: number };
|
|
1211
1297
|
|
|
1212
1298
|
export function createLaunchSnapshot(input: LaunchSnapshotInput): Readonly<LaunchSnapshot> {
|
|
1213
|
-
return deepFreeze(structuredClone({ ...input, identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION }));
|
|
1299
|
+
return deepFreeze(structuredClone({ ...input, launchKind: input.launchKind ?? (input.functionName ? "function" : "inline"), identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION }));
|
|
1214
1300
|
}
|
|
1215
1301
|
|
|
1216
1302
|
export function loadLaunchSnapshot(input: LaunchSnapshot): Readonly<LaunchSnapshot> {
|
|
1217
1303
|
return deepFreeze(structuredClone(input));
|
|
1218
1304
|
}
|
|
1219
1305
|
|
|
1306
|
+
function launchScriptForSnapshot(snapshot: Readonly<LaunchSnapshot>, registry: WorkflowRegistryApi): string {
|
|
1307
|
+
if (snapshot.launchKind === "function") {
|
|
1308
|
+
if (!snapshot.functionName) fail("RESUME_INCOMPATIBLE", "Persisted registered function launch is missing its function name");
|
|
1309
|
+
try { registry.function(snapshot.functionName); } catch (error) { if (error instanceof WorkflowError && error.code === "MISSING_WORKFLOW") throw new WorkflowError("RESUME_INCOMPATIBLE", `Persisted registered function is unavailable: ${snapshot.functionName}`); throw error; }
|
|
1310
|
+
return functionLaunchScript(snapshot.functionName);
|
|
1311
|
+
}
|
|
1312
|
+
if (snapshot.launchKind === "inline") return snapshot.script;
|
|
1313
|
+
fail("RESUME_INCOMPATIBLE", "This persisted run uses the removed registered-workflow format; launch it again as a registered function or inline script");
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1220
1316
|
export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
1221
1317
|
export const HEARTBEAT_TIMEOUT_MS = 5000;
|
|
1222
1318
|
|
|
1223
1319
|
export interface AgentIdentity { structuralPath: readonly string[]; callSite: string; occurrence: number; parentBreadcrumb?: string; worktreeOwner?: string; conversation?: { name: string; turn: number } }
|
|
1320
|
+
export interface ShellIdentity { structuralPath: readonly string[]; callSite: string; occurrence: number; worktreeOwner?: string }
|
|
1224
1321
|
export interface WorkflowBridge {
|
|
1225
1322
|
agent?: (prompt: string, options: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: AgentIdentity) => Promise<JsonValue>;
|
|
1323
|
+
shell?: (command: string, options: ShellOptions, signal: AbortSignal, identity: ShellIdentity) => Promise<ShellResult>;
|
|
1226
1324
|
checkpoint?: (input: Readonly<Record<string, JsonValue>>, signal: AbortSignal) => boolean | Promise<boolean>;
|
|
1227
1325
|
function?: (name: string, input: Readonly<Record<string, JsonValue>>, path: string, signal: AbortSignal, worktreeOwner?: string, structuralPath?: readonly string[]) => Promise<JsonValue>;
|
|
1326
|
+
worktree?: (owner: string, signal: AbortSignal) => Promise<Readonly<WorkflowWorktreeReference>>;
|
|
1228
1327
|
functions?: Readonly<Record<string, { name: string }>>;
|
|
1229
1328
|
variables?: Readonly<Record<string, JsonValue>>;
|
|
1230
1329
|
phase?: (name: string) => void | Promise<void>;
|
|
@@ -1297,10 +1396,12 @@ const named = (value, kind) => { if (typeof value !== "string" || !value.trim())
|
|
|
1297
1396
|
const path = (...names) => names.map(encodeURIComponent).join("/");
|
|
1298
1397
|
const inheritedAgentPath = new AsyncLocalStorage();
|
|
1299
1398
|
const agentOccurrences = new Map();
|
|
1399
|
+
const shellOccurrences = new Map();
|
|
1300
1400
|
const conversationOccurrences = new Map();
|
|
1301
1401
|
const worktreeOwners = new AsyncLocalStorage();
|
|
1302
1402
|
const worktreeOccurrences = new Map();
|
|
1303
1403
|
const rejectAgent = () => { throw workError("INVALID_METADATA", "Workflow agent calls must use a direct agent(...) call; aliases and indirect calls are unsupported"); };
|
|
1404
|
+
const rejectShell = () => { throw workError("INVALID_METADATA", "Workflow shell calls must use a direct shell(...) call; aliases and indirect calls are unsupported"); };
|
|
1304
1405
|
const rejectWorktree = () => { throw workError("INVALID_METADATA", "withWorktree calls must use a direct withWorktree(...) call; aliases and indirect calls are unsupported"); };
|
|
1305
1406
|
const internalWithWorktree = async (...values) => {
|
|
1306
1407
|
const callSite = values.pop();
|
|
@@ -1319,7 +1420,9 @@ const internalWithWorktree = async (...values) => {
|
|
|
1319
1420
|
worktreeOccurrences.set(occurrenceKey, occurrence);
|
|
1320
1421
|
owner = path("worktree", "unnamed", ...inherited, "callsite:" + callSite, "occurrence:" + String(occurrence));
|
|
1321
1422
|
}
|
|
1322
|
-
|
|
1423
|
+
const reference = await rpc("worktree", [owner]);
|
|
1424
|
+
if (!reference || typeof reference !== "object" || typeof reference.path !== "string" || typeof reference.branch !== "string") throw workError("WORKTREE_FAILED", "Worktree reference is invalid");
|
|
1425
|
+
return await worktreeOwners.run(owner, () => callback(Object.freeze({ path: reference.path, branch: reference.branch })));
|
|
1323
1426
|
};
|
|
1324
1427
|
const internalConversation = (...values) => {
|
|
1325
1428
|
const callSite = values.pop();
|
|
@@ -1378,6 +1481,31 @@ const internalAgent = (...values) => {
|
|
|
1378
1481
|
});
|
|
1379
1482
|
return result;
|
|
1380
1483
|
};
|
|
1484
|
+
const internalShell = (...values) => {
|
|
1485
|
+
const callSite = values.pop();
|
|
1486
|
+
if (typeof callSite !== "string") throw workError("INTERNAL_ERROR", "Missing workflow shell call-site identity");
|
|
1487
|
+
if (values.length !== 1 && values.length !== 2) throw workError("INVALID_METADATA", "shell requires a command string and optional options");
|
|
1488
|
+
const command = values[0];
|
|
1489
|
+
if (typeof command !== "string") throw workError("INVALID_METADATA", "shell command must be a string");
|
|
1490
|
+
const options = values.length < 2 || values[1] === undefined ? {} : values[1];
|
|
1491
|
+
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");
|
|
1492
|
+
if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0)) throw workError("INVALID_METADATA", "shell timeoutMs must be a positive integer");
|
|
1493
|
+
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");
|
|
1494
|
+
const inherited = inheritedAgentPath.getStore() || [];
|
|
1495
|
+
const worktreeOwner = worktreeOwners.getStore();
|
|
1496
|
+
const occurrenceKey = JSON.stringify([inherited, callSite, worktreeOwner || null]);
|
|
1497
|
+
const occurrence = (shellOccurrences.get(occurrenceKey) || 0) + 1;
|
|
1498
|
+
shellOccurrences.set(occurrenceKey, occurrence);
|
|
1499
|
+
const identity = { structuralPath: [...inherited], callSite, occurrence, ...(worktreeOwner ? { worktreeOwner } : {}) };
|
|
1500
|
+
const result = rpc("shell", [command, options, identity]);
|
|
1501
|
+
Object.defineProperties(result, {
|
|
1502
|
+
toJSON: { value() { throw workError("INVALID_METADATA", "Workflow shell result is a Promise; await it before serialization"); } },
|
|
1503
|
+
toString: { value() { throw workError("INVALID_METADATA", "Workflow shell result is a Promise; await it before interpolation"); } },
|
|
1504
|
+
[Symbol.toPrimitive]: { value() { throw workError("INVALID_METADATA", "Workflow shell result is a Promise; await it before interpolation"); } },
|
|
1505
|
+
});
|
|
1506
|
+
return result;
|
|
1507
|
+
};
|
|
1508
|
+
const shell = rejectShell;
|
|
1381
1509
|
const agent = rejectAgent;
|
|
1382
1510
|
const promptPath = (at, key) => /^[A-Za-z_$][\w$]*$/.test(key) ? at + "." + key : at + "[" + JSON.stringify(key) + "]";
|
|
1383
1511
|
const plainPromptObject = value => {
|
|
@@ -1499,13 +1627,13 @@ const pipeline = async (operationName, items, stages) => {
|
|
|
1499
1627
|
return Object.fromEntries(results.map(result => [result.name, result.value]));
|
|
1500
1628
|
};
|
|
1501
1629
|
const safeMath = Object.fromEntries(Object.getOwnPropertyNames(Math).filter(name => name !== "random").map(name => [name, Math[name]]));
|
|
1502
|
-
const sandbox = { agent, conversation: internalConversation, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
|
|
1630
|
+
const sandbox = { agent, conversation: internalConversation, shell, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
|
|
1503
1631
|
for (const [name, fn] of Object.entries(functions)) Object.defineProperty(sandbox, name, { value: fn, writable: false, configurable: false });
|
|
1504
1632
|
for (const [name, value] of Object.entries(config.variables || {})) Object.defineProperty(sandbox, name, { value: freeze(value), writable: false, configurable: false });
|
|
1505
1633
|
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;
|
|
1506
1634
|
const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
|
|
1507
1635
|
const body = config.script;
|
|
1508
|
-
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))
|
|
1636
|
+
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))
|
|
1509
1637
|
.then(async value => { await Promise.all(inflight); send({ type: "result", value: value === undefined ? null : value }); })
|
|
1510
1638
|
.catch(error => send({ type: "error", error: workerError(error) }))
|
|
1511
1639
|
.finally(() => clearInterval(heartbeat));
|
|
@@ -1518,6 +1646,10 @@ function encoded(value: unknown): string {
|
|
|
1518
1646
|
return json;
|
|
1519
1647
|
}
|
|
1520
1648
|
|
|
1649
|
+
function encodedRpcResult(id: number, value: JsonValue): string {
|
|
1650
|
+
return encoded({ type: "rpcResult", id, ok: true, value });
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1521
1653
|
function readAgentIdentity(value: unknown): AgentIdentity {
|
|
1522
1654
|
if (!object(value)) fail("INTERNAL_ERROR", "Invalid workflow agent identity");
|
|
1523
1655
|
const structuralPath = value.structuralPath;
|
|
@@ -1530,10 +1662,17 @@ function readAgentIdentity(value: unknown): AgentIdentity {
|
|
|
1530
1662
|
if (!Array.isArray(structuralPath) || !structuralPath.every((part): part is string => typeof part === "string" && Boolean(part.trim())) || typeof callSite !== "string" || !callSite || !positiveInteger(occurrence) || parentBreadcrumb !== undefined && (typeof parentBreadcrumb !== "string" || !parentBreadcrumb.trim()) || worktreeOwner !== undefined && (typeof worktreeOwner !== "string" || !worktreeOwner) || conversation !== undefined && !parsedConversation) fail("INTERNAL_ERROR", "Invalid workflow agent identity");
|
|
1531
1663
|
return { structuralPath: [...structuralPath], callSite, occurrence, ...(typeof parentBreadcrumb === "string" ? { parentBreadcrumb } : {}), ...(typeof worktreeOwner === "string" ? { worktreeOwner } : {}), ...(parsedConversation ? { conversation: parsedConversation } : {}) };
|
|
1532
1664
|
}
|
|
1533
|
-
|
|
1665
|
+
function readShellIdentity(value: unknown): ShellIdentity {
|
|
1666
|
+
const identity = readAgentIdentity(value);
|
|
1667
|
+
if (identity.conversation) fail("INTERNAL_ERROR", "Shell identity cannot include a conversation");
|
|
1668
|
+
return { structuralPath: identity.structuralPath, callSite: identity.callSite, occurrence: identity.occurrence, ...(identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {}) };
|
|
1669
|
+
}
|
|
1534
1670
|
function agentIdentityPath(identity: AgentIdentity): string {
|
|
1535
1671
|
return operationPath("agent", ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
|
|
1536
1672
|
}
|
|
1673
|
+
function shellIdentityPath(identity: ShellIdentity): string {
|
|
1674
|
+
return operationPath("shell", ...(identity.worktreeOwner ? ["worktree", identity.worktreeOwner] : []), ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
|
|
1675
|
+
}
|
|
1537
1676
|
function conversationIdentityPath(identity: AgentIdentity): string {
|
|
1538
1677
|
if (!identity.conversation) throw new WorkflowError("INTERNAL_ERROR", "Missing conversation identity");
|
|
1539
1678
|
return operationPath("conversation", identity.conversation.name, ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
|
|
@@ -1542,9 +1681,82 @@ function conversationTurnPath(identity: AgentIdentity): string {
|
|
|
1542
1681
|
if (!identity.conversation) throw new WorkflowError("INTERNAL_ERROR", "Missing conversation identity");
|
|
1543
1682
|
return operationPath(conversationIdentityPath(identity), `turn:${String(identity.conversation.turn)}`);
|
|
1544
1683
|
}
|
|
1684
|
+
function readShellResult(value: unknown): ShellResult {
|
|
1685
|
+
if (!object(value) || (value.exitCode !== null && !Number.isInteger(value.exitCode)) || typeof value.stdout !== "string" || typeof value.stderr !== "string") fail("SHELL_FAILED", "Shell bridge returned an invalid result");
|
|
1686
|
+
return { exitCode: value.exitCode as number | null, stdout: value.stdout, stderr: value.stderr };
|
|
1687
|
+
}
|
|
1545
1688
|
function agentWorktree(identity: AgentIdentity): { worktreeOwner?: string } {
|
|
1546
1689
|
return identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {};
|
|
1547
1690
|
}
|
|
1691
|
+
function shellProcessKill(child: ChildProcess): void {
|
|
1692
|
+
let forceKill: ReturnType<typeof setTimeout> | undefined;
|
|
1693
|
+
const killProcessTree = (signal: "SIGTERM" | "SIGKILL") => {
|
|
1694
|
+
try {
|
|
1695
|
+
if (child.pid && process.platform !== "win32") process.kill(-child.pid, signal);
|
|
1696
|
+
else if (child.pid && process.platform === "win32") {
|
|
1697
|
+
const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore", windowsHide: true });
|
|
1698
|
+
killer.unref();
|
|
1699
|
+
} else child.kill(signal);
|
|
1700
|
+
} catch {
|
|
1701
|
+
try { child.kill(signal); } catch { /* The process may already have exited. */ }
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
child.once("close", () => { if (forceKill) clearTimeout(forceKill); });
|
|
1705
|
+
killProcessTree("SIGTERM");
|
|
1706
|
+
forceKill = setTimeout(() => { forceKill = undefined; killProcessTree("SIGKILL"); }, 1000);
|
|
1707
|
+
forceKill.unref();
|
|
1708
|
+
}
|
|
1709
|
+
function executeShellCommand(command: string, options: ShellOptions, signal: AbortSignal, cwd = process.cwd()): Promise<ShellResult> {
|
|
1710
|
+
return new Promise((resolve, reject) => {
|
|
1711
|
+
let child: ChildProcess;
|
|
1712
|
+
try { child = spawn(command, { shell: true, cwd, env: { ...process.env, ...(options.env ?? {}) }, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] }); }
|
|
1713
|
+
catch (error) { reject(new WorkflowError("SHELL_FAILED", errorText(error))); return; }
|
|
1714
|
+
let settled = false;
|
|
1715
|
+
let timedOut = false;
|
|
1716
|
+
let outputBytes = 0;
|
|
1717
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1718
|
+
const stdoutDecoder = new StringDecoder("utf8");
|
|
1719
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
1720
|
+
let stdout = "";
|
|
1721
|
+
let stderr = "";
|
|
1722
|
+
const cleanup = () => {
|
|
1723
|
+
if (timeout) clearTimeout(timeout);
|
|
1724
|
+
signal.removeEventListener("abort", onAbort);
|
|
1725
|
+
};
|
|
1726
|
+
const failShell = (error: WorkflowError) => {
|
|
1727
|
+
if (settled) return;
|
|
1728
|
+
settled = true;
|
|
1729
|
+
cleanup();
|
|
1730
|
+
shellProcessKill(child);
|
|
1731
|
+
reject(error);
|
|
1732
|
+
};
|
|
1733
|
+
const onAbort = () => { failShell(new WorkflowError("CANCELLED", "Workflow cancelled")); };
|
|
1734
|
+
const capture = (target: "stdout" | "stderr", chunk: Buffer) => {
|
|
1735
|
+
if (settled) return;
|
|
1736
|
+
outputBytes += chunk.byteLength;
|
|
1737
|
+
if (outputBytes > RPC_LIMIT_BYTES) { failShell(new WorkflowError("RPC_LIMIT_EXCEEDED", "Shell result exceeds the 10 MB JSON boundary")); return; }
|
|
1738
|
+
if (target === "stdout") stdout += stdoutDecoder.write(chunk); else stderr += stderrDecoder.write(chunk);
|
|
1739
|
+
};
|
|
1740
|
+
child.stdout?.on("data", (chunk: Buffer) => { capture("stdout", chunk); });
|
|
1741
|
+
child.stderr?.on("data", (chunk: Buffer) => { capture("stderr", chunk); });
|
|
1742
|
+
child.once("error", (error) => { failShell(new WorkflowError("SHELL_FAILED", errorText(error))); });
|
|
1743
|
+
child.once("close", (exitCode) => {
|
|
1744
|
+
if (settled) return;
|
|
1745
|
+
settled = true;
|
|
1746
|
+
cleanup();
|
|
1747
|
+
stdout += stdoutDecoder.end();
|
|
1748
|
+
stderr += stderrDecoder.end();
|
|
1749
|
+
if (signal.aborted) { reject(new WorkflowError("CANCELLED", "Workflow cancelled")); return; }
|
|
1750
|
+
if (timedOut) { reject(new WorkflowError("SHELL_FAILED", `Shell command timed out after ${String(options.timeoutMs)}ms`)); return; }
|
|
1751
|
+
const result = { exitCode: exitCode === null ? null : exitCode, stdout, stderr };
|
|
1752
|
+
try { encodedRpcResult(Number.MAX_SAFE_INTEGER, result); } catch (error) { reject(error instanceof WorkflowError ? error : new WorkflowError("RPC_LIMIT_EXCEEDED", errorText(error))); return; }
|
|
1753
|
+
resolve(result);
|
|
1754
|
+
});
|
|
1755
|
+
if (signal.aborted) { onAbort(); return; }
|
|
1756
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1757
|
+
if (options.timeoutMs !== undefined) timeout = setTimeout(() => { timedOut = true; shellProcessKill(child); }, options.timeoutMs);
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1548
1760
|
export function runWorkflow(script: string, args: JsonValue = null, bridge: WorkflowBridge = {}, signal?: AbortSignal): WorkflowExecution {
|
|
1549
1761
|
encoded(args);
|
|
1550
1762
|
const config = JSON.stringify({ script: instrumentWorkflow(script), args: structuredClone(args), functions: bridge.functions ?? {}, variables: bridge.variables ?? {} });
|
|
@@ -1614,6 +1826,12 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
1614
1826
|
if (!OUTCOME_ERRORS.has(typed.code)) throw typed;
|
|
1615
1827
|
value = branded({ name: label, ok: false, failedAt: path, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } });
|
|
1616
1828
|
}
|
|
1829
|
+
} else if (method === "shell") {
|
|
1830
|
+
if (!bridge.shell) fail("SHELL_FAILED", "No shell bridge is available");
|
|
1831
|
+
const command = validateShellCommand(values[0]);
|
|
1832
|
+
const options = validateShellOptions(values[1]);
|
|
1833
|
+
const identity = readShellIdentity(values[2]);
|
|
1834
|
+
value = readShellResult(await bridge.shell(command, options, controller.signal, identity)) as unknown as JsonValue;
|
|
1617
1835
|
} else if (method === "checkpoint") {
|
|
1618
1836
|
if (!bridge.checkpoint || !object(values[0])) fail("INTERNAL_ERROR", "checkpoint requires an available bridge and object input");
|
|
1619
1837
|
const name = typeof values[0].name === "string" ? values[0].name : "checkpoint";
|
|
@@ -1640,6 +1858,9 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
1640
1858
|
if (!OUTCOME_ERRORS.has(typed.code)) throw typed;
|
|
1641
1859
|
value = branded({ name, ok: false, failedAt: name, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } });
|
|
1642
1860
|
}
|
|
1861
|
+
} else if (method === "worktree") {
|
|
1862
|
+
if (!bridge.worktree || typeof values[0] !== "string" || !values[0]) fail("INTERNAL_ERROR", "worktree requires an active host bridge and scope");
|
|
1863
|
+
value = await bridge.worktree(values[0], controller.signal);
|
|
1643
1864
|
} else if (method === "phase") {
|
|
1644
1865
|
if (typeof values[0] !== "string") fail("INTERNAL_ERROR", "phase name must be a string");
|
|
1645
1866
|
await bridge.phase?.(values[0]);
|
|
@@ -1649,7 +1870,7 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
1649
1870
|
}
|
|
1650
1871
|
else fail("INTERNAL_ERROR", `Unknown worker RPC method: ${method}`);
|
|
1651
1872
|
encoded(value);
|
|
1652
|
-
child.send(
|
|
1873
|
+
child.send(encodedRpcResult(id, value));
|
|
1653
1874
|
} catch (error) {
|
|
1654
1875
|
const typed = asWorkflowError(error);
|
|
1655
1876
|
child.send(encoded({ type: "rpcResult", id, ok: false, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } }));
|
|
@@ -1846,13 +2067,9 @@ export function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonl
|
|
|
1846
2067
|
const render = ({ agent, depth }: { agent: AgentRecord; index: number; depth: number }, grouped: boolean) => {
|
|
1847
2068
|
const icon = agent.state === "completed" ? "✓" : agent.state === "failed" || agent.state === "cancelled" ? "✗" : agent.state === "running" ? "⠦" : "○";
|
|
1848
2069
|
const breadcrumb = grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId);
|
|
1849
|
-
const setup = agent.attemptDetails?.at(-1)?.setup;
|
|
1850
|
-
const thinking = setup?.model.thinking ?? agent.model.thinking;
|
|
1851
|
-
const model = `${setup?.model.provider ?? agent.model.provider}/${setup?.model.model ?? agent.model.model}${thinking ? `:${thinking}` : ""}`;
|
|
1852
|
-
const policy = [`model=${model}`, agent.requestedModel ? `requested=${agent.requestedModel}` : "", `tools=${(setup?.tools ?? agent.tools).join(",") || "(none)"}`, agent.role ? `role=${agent.role}` : ""];
|
|
1853
2070
|
const tokens = agent.accounting ? formatAccounting(agent.accounting) : "";
|
|
1854
2071
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
1855
|
-
const result = [`${indent}${icon} ${breadcrumb} · ${agent.state}
|
|
2072
|
+
const result = [`${indent}${icon} ${breadcrumb} · ${agent.state}${tokens ? ` · ${tokens}` : ""}`];
|
|
1856
2073
|
if (agent.state === "failed" && agent.attemptDetails?.length) {
|
|
1857
2074
|
const last = agent.attemptDetails[agent.attemptDetails.length - 1];
|
|
1858
2075
|
if (last?.error) result.push(`${indent} error: ${last.error.code}: ${last.error.message}`);
|
|
@@ -1903,7 +2120,8 @@ export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readon
|
|
|
1903
2120
|
return lines.join("\n");
|
|
1904
2121
|
}
|
|
1905
2122
|
function formatCheckpointReview(checkpoint: AwaitingCheckpoint): string {
|
|
1906
|
-
|
|
2123
|
+
const context = JSON.stringify(checkpoint.context, null, 2);
|
|
2124
|
+
return [`Name: ${checkpoint.name}`, "Prompt:", checkpoint.prompt, context === "null" ? "Context: null" : "Context:", ...(context === "null" ? [] : [context])].join("\n");
|
|
1907
2125
|
}
|
|
1908
2126
|
|
|
1909
2127
|
const DELIVERY_LIMIT_BYTES = 4 * 1024;
|
|
@@ -1926,12 +2144,109 @@ function utf8Prefix(value: string, maxBytes: number): string {
|
|
|
1926
2144
|
while (end < bytes.length && end > 0 && ((bytes[end] ?? 0) & 0xc0) === 0x80) end -= 1;
|
|
1927
2145
|
return bytes.subarray(0, end).toString("utf8");
|
|
1928
2146
|
}
|
|
2147
|
+
const DIAGNOSTIC_LIMIT_BYTES = DELIVERY_LIMIT_BYTES - 512;
|
|
2148
|
+
function failureDiagnosticsFrom(error: unknown): WorkflowFailureDiagnostics | undefined {
|
|
2149
|
+
if (!error || typeof error !== "object") return undefined;
|
|
2150
|
+
return (error as { [WORKFLOW_FAILURE_DIAGNOSTICS]?: WorkflowFailureDiagnostics })[WORKFLOW_FAILURE_DIAGNOSTICS];
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
function boundedWorkflowFailureDiagnostics(value: WorkflowFailureDiagnostics): WorkflowFailureDiagnostics {
|
|
2154
|
+
let bounded: WorkflowFailureDiagnostics = {
|
|
2155
|
+
runId: utf8Prefix(value.runId, 128),
|
|
2156
|
+
workflowName: utf8Prefix(value.workflowName, 256),
|
|
2157
|
+
state: value.state,
|
|
2158
|
+
failedAt: value.failedAt === null ? null : utf8Prefix(value.failedAt, 1024),
|
|
2159
|
+
error: { code: value.error.code, message: utf8Prefix(value.error.message, 1024) },
|
|
2160
|
+
...(value.failedAgent ? { failedAgent: {
|
|
2161
|
+
id: utf8Prefix(value.failedAgent.id, 128),
|
|
2162
|
+
...(value.failedAgent.label ? { label: utf8Prefix(value.failedAgent.label, 128) } : {}),
|
|
2163
|
+
...(value.failedAgent.role ? { role: utf8Prefix(value.failedAgent.role, 128) } : {}),
|
|
2164
|
+
structuralPath: value.failedAgent.structuralPath.slice(0, 8).map((part) => utf8Prefix(part, 128)),
|
|
2165
|
+
attempt: value.failedAgent.attempt,
|
|
2166
|
+
...(value.failedAgent.sessionId ? { sessionId: utf8Prefix(value.failedAgent.sessionId, 256) } : {}),
|
|
2167
|
+
...(value.failedAgent.sessionFile ? { sessionFile: utf8Prefix(value.failedAgent.sessionFile, 1024) } : {}),
|
|
2168
|
+
} } : {}),
|
|
2169
|
+
completedSiblingAgents: (value.completedSiblingAgents ?? []).slice(0, 16).map((agent) => ({
|
|
2170
|
+
id: utf8Prefix(agent.id, 128),
|
|
2171
|
+
...(agent.label ? { label: utf8Prefix(agent.label, 128) } : {}),
|
|
2172
|
+
...(agent.role ? { role: utf8Prefix(agent.role, 128) } : {}),
|
|
2173
|
+
structuralPath: agent.structuralPath.slice(0, 8).map((part) => utf8Prefix(part, 128)),
|
|
2174
|
+
})),
|
|
2175
|
+
completedSiblingPaths: value.completedSiblingPaths.slice(0, 16).map((path) => path.slice(0, 8).map((part) => utf8Prefix(part, 128))),
|
|
2176
|
+
artifacts: { runDirectory: utf8Prefix(value.artifacts.runDirectory, 1024), statePath: utf8Prefix(value.artifacts.statePath, 1024), journalPath: utf8Prefix(value.artifacts.journalPath, 1024) },
|
|
2177
|
+
};
|
|
2178
|
+
const size = () => Buffer.byteLength(JSON.stringify(bounded));
|
|
2179
|
+
while (size() > DIAGNOSTIC_LIMIT_BYTES) {
|
|
2180
|
+
if (bounded.completedSiblingAgents?.length || bounded.completedSiblingPaths.length) {
|
|
2181
|
+
bounded = { ...bounded, completedSiblingAgents: bounded.completedSiblingAgents?.slice(0, -1) ?? [], completedSiblingPaths: bounded.completedSiblingPaths.slice(0, -1) };
|
|
2182
|
+
continue;
|
|
2183
|
+
}
|
|
2184
|
+
if (bounded.failedAgent?.sessionFile) { const failedAgent = { ...bounded.failedAgent }; delete failedAgent.sessionFile; bounded = { ...bounded, failedAgent }; continue; }
|
|
2185
|
+
if (bounded.failedAgent?.sessionId) { const failedAgent = { ...bounded.failedAgent }; delete failedAgent.sessionId; bounded = { ...bounded, failedAgent }; continue; }
|
|
2186
|
+
if (Buffer.byteLength(bounded.artifacts.runDirectory) > 256) { bounded = { ...bounded, artifacts: { ...bounded.artifacts, runDirectory: utf8Prefix(bounded.artifacts.runDirectory, 256) } }; continue; }
|
|
2187
|
+
if (Buffer.byteLength(bounded.error.message) > 256) { bounded = { ...bounded, error: { ...bounded.error, message: utf8Prefix(bounded.error.message, 256) } }; continue; }
|
|
2188
|
+
if (bounded.failedAt !== null && Buffer.byteLength(bounded.failedAt) > 256) { bounded = { ...bounded, failedAt: utf8Prefix(bounded.failedAt, 256) }; continue; }
|
|
2189
|
+
if (bounded.failedAgent && bounded.failedAgent.structuralPath.length > 4) { bounded = { ...bounded, failedAgent: { ...bounded.failedAgent, structuralPath: bounded.failedAgent.structuralPath.slice(0, 4) } }; continue; }
|
|
2190
|
+
if (bounded.failedAgent?.structuralPath.some((part) => Buffer.byteLength(part) > 64)) { bounded = { ...bounded, failedAgent: { ...bounded.failedAgent, structuralPath: bounded.failedAgent.structuralPath.map((part) => utf8Prefix(part, 64)) } }; continue; }
|
|
2191
|
+
if (Buffer.byteLength(bounded.artifacts.statePath) > 512 || Buffer.byteLength(bounded.artifacts.journalPath) > 512) { bounded = { ...bounded, artifacts: { ...bounded.artifacts, statePath: utf8Prefix(bounded.artifacts.statePath, 512), journalPath: utf8Prefix(bounded.artifacts.journalPath, 512) } }; continue; }
|
|
2192
|
+
if (Buffer.byteLength(bounded.workflowName) > 128) { bounded = { ...bounded, workflowName: utf8Prefix(bounded.workflowName, 128) }; continue; }
|
|
2193
|
+
break;
|
|
2194
|
+
}
|
|
2195
|
+
return bounded;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
function createWorkflowFailureDiagnostics(store: RunStore, metadata: WorkflowMetadata, error: unknown, run: PersistedRun): WorkflowFailureDiagnostics {
|
|
2199
|
+
const rawFailedAt = error && typeof error === "object" ? (error as { failedAt?: unknown }).failedAt : undefined;
|
|
2200
|
+
const failedAt = typeof rawFailedAt === "string" && rawFailedAt ? rawFailedAt : null;
|
|
2201
|
+
const failedAgents = run.agents.filter((agent) => agent.state === "failed");
|
|
2202
|
+
const failedAgentRecord = failedAgents.find((agent) => {
|
|
2203
|
+
if (failedAt === null) return false;
|
|
2204
|
+
try { return failedAt.includes(`${operationPath("agent", ...(agent.structuralPath ?? []))}/`); } catch { return false; }
|
|
2205
|
+
}) ?? failedAgents.at(-1);
|
|
2206
|
+
const failedAttempt = failedAgentRecord ? [...(failedAgentRecord.attemptDetails ?? [])].reverse().find((attempt) => attempt.error) ?? failedAgentRecord.attemptDetails?.at(-1) : undefined;
|
|
2207
|
+
const failedAgent = failedAgentRecord ? {
|
|
2208
|
+
id: failedAgentRecord.id,
|
|
2209
|
+
...(failedAgentRecord.label ?? failedAgentRecord.name ? { label: failedAgentRecord.label ?? failedAgentRecord.name } : {}),
|
|
2210
|
+
...(failedAgentRecord.role ? { role: failedAgentRecord.role } : {}),
|
|
2211
|
+
structuralPath: [...(failedAgentRecord.structuralPath ?? [])],
|
|
2212
|
+
attempt: Math.max(1, failedAttempt?.attempt ?? failedAgentRecord.attempts),
|
|
2213
|
+
...(failedAttempt?.sessionId ? { sessionId: failedAttempt.sessionId } : {}),
|
|
2214
|
+
...(failedAttempt?.sessionFile ? { sessionFile: failedAttempt.sessionFile } : {}),
|
|
2215
|
+
} satisfies WorkflowFailureAgent : undefined;
|
|
2216
|
+
const completedSiblingAgents = run.agents.filter((agent) => {
|
|
2217
|
+
if (agent.state !== "completed" || agent.id === failedAgentRecord?.id) return false;
|
|
2218
|
+
return failedAgentRecord?.parentId === undefined ? agent.parentId === undefined : agent.parentId === failedAgentRecord.parentId;
|
|
2219
|
+
}).map((agent) => ({
|
|
2220
|
+
id: agent.id,
|
|
2221
|
+
...(agent.label ?? agent.name ? { label: agent.label ?? agent.name } : {}),
|
|
2222
|
+
...(agent.role ? { role: agent.role } : {}),
|
|
2223
|
+
structuralPath: [...(agent.structuralPath ?? [])],
|
|
2224
|
+
} satisfies WorkflowSiblingAgent));
|
|
2225
|
+
const completedSiblingPaths = completedSiblingAgents.map((agent) => [...agent.structuralPath]);
|
|
2226
|
+
return boundedWorkflowFailureDiagnostics({
|
|
2227
|
+
runId: run.id, workflowName: metadata.name, state: run.state, failedAt,
|
|
2228
|
+
error: { code: errorCode(error) ?? "INTERNAL_ERROR", message: errorText(error) || "The workflow failed without an error message." },
|
|
2229
|
+
...(failedAgent ? { failedAgent } : {}), completedSiblingAgents, completedSiblingPaths,
|
|
2230
|
+
artifacts: { runDirectory: store.directory, statePath: join(store.directory, "state.json"), journalPath: join(store.directory, "journal.json") },
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
export function formatWorkflowFailureDiagnostics(diagnostic: WorkflowFailureDiagnostics): string {
|
|
2235
|
+
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)";
|
|
2236
|
+
const siblingAgents = diagnostic.completedSiblingAgents;
|
|
2237
|
+
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)";
|
|
2238
|
+
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");
|
|
2239
|
+
}
|
|
1929
2240
|
|
|
2241
|
+
function serializeWorkflowFailureDiagnostics(diagnostic: WorkflowFailureDiagnostics): string { return JSON.stringify(diagnostic); }
|
|
2242
|
+
function isWorkflowFailureDiagnostics(value: unknown): value is WorkflowFailureDiagnostics {
|
|
2243
|
+
return object(value) && typeof value.runId === "string" && typeof value.workflowName === "string" && typeof value.state === "string" && "failedAt" in value && object(value.error) && object(value.artifacts);
|
|
2244
|
+
}
|
|
1930
2245
|
function deliver(pi: ExtensionAPI, content: string): void {
|
|
1931
2246
|
pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
1932
2247
|
}
|
|
1933
|
-
function deliverFailure(pi: ExtensionAPI,
|
|
1934
|
-
deliver(pi, `Workflow ${
|
|
2248
|
+
function deliverFailure(pi: ExtensionAPI, diagnostic: WorkflowFailureDiagnostics): void {
|
|
2249
|
+
deliver(pi, `Workflow ${utf8Prefix(diagnostic.workflowName, 128)} failure diagnostics: ${serializeWorkflowFailureDiagnostics(diagnostic)}`);
|
|
1935
2250
|
}
|
|
1936
2251
|
|
|
1937
2252
|
type WorkflowEventSink = { emit: (name: string, payload: unknown) => unknown };
|
|
@@ -1948,6 +2263,12 @@ class WorkflowEventPublisher {
|
|
|
1948
2263
|
|
|
1949
2264
|
constructor(private readonly sink: WorkflowEventSink | undefined) {}
|
|
1950
2265
|
|
|
2266
|
+
removeRun(runId: string): void {
|
|
2267
|
+
this.#queues.delete(runId);
|
|
2268
|
+
this.#budgetEvents.delete(runId);
|
|
2269
|
+
this.#worktrees.delete(runId);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
1951
2272
|
seedBudget(runId: string, events: readonly BudgetEvent[] | undefined): void {
|
|
1952
2273
|
const seen = this.#budgetEvents.get(runId) ?? new Set<string>();
|
|
1953
2274
|
for (const event of events ?? []) seen.add(this.budgetKey(event));
|
|
@@ -2025,7 +2346,11 @@ function namedRecord(value: unknown, kind: string): Array<[string, unknown]> {
|
|
|
2025
2346
|
if (!object(value)) fail("INVALID_METADATA", `${kind} must be a record`);
|
|
2026
2347
|
return Object.entries(value);
|
|
2027
2348
|
}
|
|
2028
|
-
function
|
|
2349
|
+
function publicWorktreeReference(reference: WorkflowWorktreeReference): Readonly<WorkflowWorktreeReference> {
|
|
2350
|
+
if (!object(reference) || typeof reference.path !== "string" || typeof reference.branch !== "string") fail("WORKTREE_FAILED", "Worktree reference is invalid");
|
|
2351
|
+
return Object.freeze({ path: reference.path, branch: reference.branch });
|
|
2352
|
+
}
|
|
2353
|
+
async function hostWithWorktree(args: readonly unknown[], identity: string, occurrences: Map<string, number>, resolveWorktree: ((owner: string, signal: AbortSignal) => Promise<Readonly<WorkflowWorktreeReference>>) | undefined, signal: AbortSignal): Promise<JsonValue> {
|
|
2029
2354
|
if (args.length !== 1 && args.length !== 2) fail("INVALID_METADATA", "withWorktree requires a callback or a name and callback");
|
|
2030
2355
|
const callback = args[args.length - 1];
|
|
2031
2356
|
if (typeof callback !== "function") fail("INVALID_METADATA", "withWorktree callback must be a function");
|
|
@@ -2040,7 +2365,9 @@ function hostWithWorktree(args: readonly unknown[], identity: string, occurrence
|
|
|
2040
2365
|
occurrences.set(key, occurrence);
|
|
2041
2366
|
owner = operationPath("worktree", "unnamed", "function", identity, ...structuralPath, `occurrence:${String(occurrence)}`);
|
|
2042
2367
|
}
|
|
2043
|
-
|
|
2368
|
+
if (!resolveWorktree) fail("WORKTREE_FAILED", "No worktree bridge is available");
|
|
2369
|
+
const reference = publicWorktreeReference(await resolveWorktree(owner, signal));
|
|
2370
|
+
return inheritedHostWorktreeOwner.run(owner, async () => await (callback as (reference: Readonly<WorkflowWorktreeReference>) => unknown)(reference)) as Promise<JsonValue>;
|
|
2044
2371
|
}
|
|
2045
2372
|
function workflowRunContext(cwd: string, sessionId: string, runId: string, workflow: WorkflowMetadata, args: JsonValue, signal: AbortSignal): Readonly<WorkflowRunContext> {
|
|
2046
2373
|
return Object.freeze({ cwd, sessionId, runId, workflow: deepFreeze(structuredClone(workflow)), args: deepFreeze(structuredClone(args)), signal });
|
|
@@ -2123,6 +2450,7 @@ function nextNamedOccurrence(counters: Map<string, number>, label: string): stri
|
|
|
2123
2450
|
|
|
2124
2451
|
function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, runContext: Readonly<WorkflowRunContext>, variables: Readonly<Record<string, JsonValue>>, registry: WorkflowRegistryApi): WorkflowBridge {
|
|
2125
2452
|
const functionAgentOccurrences = new Map<string, number>();
|
|
2453
|
+
const functionShellOccurrences = new Map<string, number>();
|
|
2126
2454
|
const functionWorktreeOccurrences = new Map<string, number>();
|
|
2127
2455
|
const functionInvokeOccurrences = new Map<string, number>();
|
|
2128
2456
|
const invokeFunction = async (name: string, input: Readonly<Record<string, JsonValue>>, path: string, signal: AbortSignal, worktreeOwner?: string, structuralPath: readonly string[] = [], breadcrumb?: string): Promise<JsonValue> => {
|
|
@@ -2151,10 +2479,21 @@ function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, runConte
|
|
|
2151
2479
|
functionAgentOccurrences.set(key, occurrence);
|
|
2152
2480
|
return bridge.agent(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, parentBreadcrumb: functionBreadcrumb, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
|
|
2153
2481
|
},
|
|
2482
|
+
shell: async (...args: readonly unknown[]) => {
|
|
2483
|
+
if (!bridge.shell) fail("SHELL_FAILED", "No shell bridge is available");
|
|
2484
|
+
if (typeof args[0] !== "string") fail("INVALID_METADATA", "shell command must be a string");
|
|
2485
|
+
const options = validateShellOptions(args[1] === undefined ? {} : args[1]);
|
|
2486
|
+
const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
|
|
2487
|
+
const inherited = inheritedHostAgentPath.getStore() ?? [];
|
|
2488
|
+
const key = `${path}\0${JSON.stringify([inherited, scopedWorktreeOwner ?? null])}`;
|
|
2489
|
+
const occurrence = (functionShellOccurrences.get(key) ?? 0) + 1;
|
|
2490
|
+
functionShellOccurrences.set(key, occurrence);
|
|
2491
|
+
return bridge.shell(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
|
|
2492
|
+
},
|
|
2154
2493
|
prompt: workflowPrompt,
|
|
2155
2494
|
parallel: (...args: readonly unknown[]) => hostParallel(args[0], args[1]),
|
|
2156
2495
|
pipeline: (...args: readonly unknown[]) => hostPipeline(args[0], args[1], args[2]),
|
|
2157
|
-
withWorktree: (...args: readonly unknown[]) => hostWithWorktree(args, path, functionWorktreeOccurrences),
|
|
2496
|
+
withWorktree: (...args: readonly unknown[]) => hostWithWorktree(args, path, functionWorktreeOccurrences, bridge.worktree, signal),
|
|
2158
2497
|
checkpoint: async (...args: readonly unknown[]) => {
|
|
2159
2498
|
if (!bridge.checkpoint || !object(args[0]) || !jsonValue(args[0])) fail("INTERNAL_ERROR", "No checkpoint bridge is available");
|
|
2160
2499
|
return bridge.checkpoint(args[0], signal);
|
|
@@ -2215,6 +2554,17 @@ function tuiHostCapabilities(tui: unknown): TuiHostCapabilities {
|
|
|
2215
2554
|
return { terminal: { ...(tui.terminal.rows === undefined ? {} : { rows: tui.terminal.rows }) } };
|
|
2216
2555
|
}
|
|
2217
2556
|
function tuiRows(tui: unknown): number { const rows = tuiHostCapabilities(tui).terminal?.rows; return typeof rows === "number" && Number.isFinite(rows) ? rows : 24; }
|
|
2557
|
+
const WORKFLOW_OVERLAY_BORDER_ROWS = 2;
|
|
2558
|
+
type WorkflowOverlayComponent = { render(width: number): string[]; invalidate(): void; handleInput?(data: string): void; dispose?(): void };
|
|
2559
|
+
function borderWorkflowOverlay(component: WorkflowOverlayComponent, theme: { fg(color: "border", text: string): string }): WorkflowOverlayComponent {
|
|
2560
|
+
return {
|
|
2561
|
+
...component,
|
|
2562
|
+
render(width: number) {
|
|
2563
|
+
const border = theme.fg("border", "─".repeat(Math.max(1, width)));
|
|
2564
|
+
return [border, ...component.render(width), border];
|
|
2565
|
+
},
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2218
2568
|
type KeybindingsHostCapabilities = { getKeys?: (name: string) => readonly string[] };
|
|
2219
2569
|
function isKeybindingGetter(value: unknown): value is NonNullable<KeybindingsHostCapabilities["getKeys"]> { return typeof value === "function"; }
|
|
2220
2570
|
function keybindingsHostCapabilities(keybindings: unknown): KeybindingsHostCapabilities {
|
|
@@ -2229,9 +2579,10 @@ function parseThinking(value: unknown): ModelSpec["thinking"] | undefined {
|
|
|
2229
2579
|
}
|
|
2230
2580
|
}
|
|
2231
2581
|
|
|
2232
|
-
export default function workflowExtension(pi: ExtensionAPI, home?: string, clipboard = copyToClipboard, createSession: SessionFactory = createNativeAgentSession) {
|
|
2582
|
+
export default function workflowExtension(pi: ExtensionAPI, home?: string, clipboard = copyToClipboard, createSession: SessionFactory = createNativeAgentSession, agentDir?: string) {
|
|
2233
2583
|
beginWorkflowExtensionLoading();
|
|
2234
2584
|
const registry = loadingRegistry();
|
|
2585
|
+
const extensionAgentDir = agentDir ?? getAgentDir();
|
|
2235
2586
|
const registerEntryRenderer = piHostCapabilities(pi).registerEntryRenderer;
|
|
2236
2587
|
registerEntryRenderer?.<WorkflowLogEntry>(WORKFLOW_LOG_ENTRY, (entry) => {
|
|
2237
2588
|
const data = entry.data;
|
|
@@ -2250,7 +2601,36 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2250
2601
|
return skillPath ? { skillPaths: [skillPath] } : undefined;
|
|
2251
2602
|
});
|
|
2252
2603
|
type BudgetDecisionResult = { state: "running" | "budget_exhausted"; approved: boolean };
|
|
2253
|
-
const runs = new Map<string, { executor: WorkflowAgentExecutor; store: RunStore; metadata: WorkflowMetadata; model: ModelSpec; lifecycle: RunLifecycle; budget: WorkflowBudgetRuntime; abortController: AbortController; projectTrusted: () => boolean; execution?: WorkflowExecution; completion?: Promise<unknown>; checkpointResolvers: Map<string, (value: boolean) => void>;
|
|
2604
|
+
const runs = new Map<string, { executor: WorkflowAgentExecutor; store: RunStore; metadata: WorkflowMetadata; model: ModelSpec; lifecycle: RunLifecycle; budget: WorkflowBudgetRuntime; abortController: AbortController; projectTrusted: () => boolean; providerErrorRecovery?: (failure: AgentProviderFailure) => Promise<AgentProviderRecovery>; execution?: WorkflowExecution; completion?: Promise<unknown>; checkpointResolvers: Map<string, (value: boolean) => void>; update?: (result: WorkflowToolUpdate) => void }>();
|
|
2605
|
+
let providerRecoveryQueue = Promise.resolve();
|
|
2606
|
+
const enqueueProviderRecovery = <T>(task: () => Promise<T>): Promise<T> => { const next = providerRecoveryQueue.then(task, task); providerRecoveryQueue = next.then(() => undefined, () => undefined); return next; };
|
|
2607
|
+
const createProviderErrorRecovery = (host: unknown, fallbackModels: ReadonlySet<string>, abort: () => void) => {
|
|
2608
|
+
if (!object(host) || host.mode !== "tui" || host.hasUI !== true) return undefined;
|
|
2609
|
+
const ui = object(host.ui) ? host.ui : undefined;
|
|
2610
|
+
const select = uiHostCapabilities(ui)?.select;
|
|
2611
|
+
if (!select) return undefined;
|
|
2612
|
+
const hostModels = contextHostCapabilities(host).modelRegistry;
|
|
2613
|
+
const choose = (title: string, options: string[]) => select.call(ui, title, options);
|
|
2614
|
+
return (failure: AgentProviderFailure): Promise<AgentProviderRecovery> => enqueueProviderRecovery(async () => {
|
|
2615
|
+
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"]);
|
|
2616
|
+
if (action === "Retry") return "retry";
|
|
2617
|
+
if (action === "Change model") {
|
|
2618
|
+
const available = hostModels?.getAvailable?.().map((model) => `${model.provider}/${model.id}`) ?? [...fallbackModels];
|
|
2619
|
+
const selected = await choose(`Available models for subagent "${failure.label}"`, [...new Set(available)].sort());
|
|
2620
|
+
if (selected) return { model: selected };
|
|
2621
|
+
}
|
|
2622
|
+
abort();
|
|
2623
|
+
return "abort";
|
|
2624
|
+
});
|
|
2625
|
+
};
|
|
2626
|
+
const pendingFailureDiagnostics = new Map<string, WorkflowFailureDiagnostics>();
|
|
2627
|
+
pi.on("tool_result", (event) => {
|
|
2628
|
+
if (event.toolName !== "workflow" || !event.isError) return;
|
|
2629
|
+
const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
|
|
2630
|
+
if (!diagnostic) return;
|
|
2631
|
+
pendingFailureDiagnostics.delete(event.toolCallId);
|
|
2632
|
+
return { content: [{ type: "text" as const, text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
|
|
2633
|
+
});
|
|
2254
2634
|
const liveActivities = new Map<string, Map<string, AgentActivity>>();
|
|
2255
2635
|
const setLiveActivity = (runId: string, agentId: string, activity?: AgentActivity) => {
|
|
2256
2636
|
const activities = liveActivities.get(runId);
|
|
@@ -2293,9 +2673,30 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2293
2673
|
const persistWorktree = async (store: RunStore, metadata: WorkflowMetadata, owner: string): Promise<WorktreeReference> => {
|
|
2294
2674
|
const existing = (await store.worktrees()).some((worktree) => worktree.owner === owner);
|
|
2295
2675
|
const worktree = await store.worktree(owner);
|
|
2296
|
-
if (!existing) await eventPublisher.worktree(store, metadata, worktree);
|
|
2676
|
+
if (!existing && await store.ownsWorktree(owner)) await eventPublisher.worktree(store, metadata, worktree);
|
|
2297
2677
|
return worktree;
|
|
2298
2678
|
};
|
|
2679
|
+
const resolveWorktree = async (store: RunStore, metadata: WorkflowMetadata, owner: string): Promise<Readonly<WorkflowWorktreeReference>> => {
|
|
2680
|
+
const run = runs.get(store.runId);
|
|
2681
|
+
if (!run) fail("INTERNAL_ERROR", `Unknown production run: ${store.runId}`);
|
|
2682
|
+
await run.lifecycle.enter();
|
|
2683
|
+
try {
|
|
2684
|
+
const worktree = await persistWorktree(store, metadata, owner);
|
|
2685
|
+
return { path: worktree.path, branch: worktree.branch };
|
|
2686
|
+
} finally { await run.lifecycle.leave(); }
|
|
2687
|
+
};
|
|
2688
|
+
const shellForRun = async (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, command: string, options: ShellOptions, signal: AbortSignal, identity: ShellIdentity): Promise<ShellResult> => {
|
|
2689
|
+
await lifecycle.enter();
|
|
2690
|
+
try {
|
|
2691
|
+
const path = shellIdentityPath(identity);
|
|
2692
|
+
const replayed = await store.replay(path);
|
|
2693
|
+
if (replayed) return readShellResult(replayed.value);
|
|
2694
|
+
const cwd = identity.worktreeOwner ? (await persistWorktree(store, metadata, identity.worktreeOwner)).cwd : store.cwd;
|
|
2695
|
+
const result = await executeShellCommand(command, options, signal, cwd);
|
|
2696
|
+
await store.complete(path, result as unknown as JsonValue);
|
|
2697
|
+
return result;
|
|
2698
|
+
} finally { await lifecycle.leave(); }
|
|
2699
|
+
};
|
|
2299
2700
|
const lifecycleFor = (store: RunStore, state: RunState, budget: WorkflowBudgetRuntime, metadata: WorkflowMetadata) => new RunLifecycle(state, async (next, previous, reason) => {
|
|
2300
2701
|
if (next !== "pausing") budget.transition(next);
|
|
2301
2702
|
const persisted = await persistRunState(store, metadata, (current) => {
|
|
@@ -2335,7 +2736,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2335
2736
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
2336
2737
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2337
2738
|
};
|
|
2338
|
-
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); });
|
|
2739
|
+
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); });
|
|
2339
2740
|
const before = (await run.store.load()).run;
|
|
2340
2741
|
await persistAgentAttempts(run.store, id, result.attempts);
|
|
2341
2742
|
const completed = (await run.store.load()).run;
|
|
@@ -2378,6 +2779,19 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2378
2779
|
await eventPublisher.agentStates(run.store, run.metadata, previousAgents, runState.agents);
|
|
2379
2780
|
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
2380
2781
|
});
|
|
2782
|
+
const cleanupTerminalRun = async (runId: string): Promise<void> => {
|
|
2783
|
+
const run = runs.get(runId);
|
|
2784
|
+
if (!run || !["completed", "failed", "stopped"].includes(run.lifecycle.state)) return;
|
|
2785
|
+
await scheduler.cancelRun(runId);
|
|
2786
|
+
await scheduler.flush();
|
|
2787
|
+
if (runs.get(runId) !== run) return;
|
|
2788
|
+
scheduler.removeRun(runId);
|
|
2789
|
+
terminalRunStates.set(runId, run.lifecycle.state as "completed" | "failed" | "stopped");
|
|
2790
|
+
run.checkpointResolvers.clear();
|
|
2791
|
+
liveActivities.delete(runId);
|
|
2792
|
+
eventPublisher.removeRun(runId);
|
|
2793
|
+
runs.delete(runId);
|
|
2794
|
+
};
|
|
2381
2795
|
type WorkflowStopResult = { runId: string; state: RunState | "unknown"; stopped: boolean; reason?: "unknown_run" | "already_terminal" };
|
|
2382
2796
|
const stopWorkflowRun = async (runId: string): Promise<WorkflowStopResult> => {
|
|
2383
2797
|
const run = runs.get(runId);
|
|
@@ -2390,6 +2804,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2390
2804
|
run.execution?.cancel();
|
|
2391
2805
|
await scheduler.cancelRun(run.store.runId);
|
|
2392
2806
|
await scheduler.flush();
|
|
2807
|
+
await cleanupTerminalRun(runId);
|
|
2393
2808
|
return { runId, state: "stopped", stopped: true };
|
|
2394
2809
|
};
|
|
2395
2810
|
const answerCheckpoint = async (runId: string, name: string, approved: boolean, silent = false) => {
|
|
@@ -2416,17 +2831,16 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2416
2831
|
if (!request) return undefined;
|
|
2417
2832
|
await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
|
|
2418
2833
|
const result = await applyBudgetDecision(request, approved);
|
|
2419
|
-
run.budgetResolvers.get(proposalId)?.(result);
|
|
2420
|
-
run.budgetResolvers.delete(proposalId);
|
|
2421
2834
|
if (!silent) deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
|
|
2422
2835
|
return result;
|
|
2423
2836
|
};
|
|
2424
|
-
const checkpointBridge = (runId: string, store: RunStore, metadata: WorkflowMetadata, foreground: boolean, ui?: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }) => {
|
|
2837
|
+
const checkpointBridge = (runId: string, store: RunStore, metadata: WorkflowMetadata, foreground: boolean, ui?: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, headless = false) => {
|
|
2425
2838
|
const checkpointCounters = new Map<string, number>();
|
|
2426
2839
|
return async (raw: Readonly<Record<string, JsonValue>>, signal: AbortSignal): Promise<boolean> => {
|
|
2427
2840
|
const input = validateCheckpoint(raw);
|
|
2428
2841
|
const label = nextNamedOccurrence(checkpointCounters, input.name);
|
|
2429
2842
|
const path = operationPath("checkpoint", label);
|
|
2843
|
+
if (headless) fail("RESUME_INCOMPATIBLE", "Headless CLI checkpoints are unsupported");
|
|
2430
2844
|
if (foreground && !ui?.select) fail("RESUME_INCOMPATIBLE", "Foreground checkpoints require UI");
|
|
2431
2845
|
const alreadyAwaiting = (await store.awaitingCheckpoints()).some((checkpoint) => checkpoint.path === path);
|
|
2432
2846
|
const replayed = await store.awaitCheckpoint({ ...input, name: label, path });
|
|
@@ -2501,13 +2915,16 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2501
2915
|
if (catalogRegistered || !pi.getActiveTools().includes("workflow")) return;
|
|
2502
2916
|
const catalog = registry.catalog();
|
|
2503
2917
|
const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0;
|
|
2504
|
-
if (!catalog.functions.length && !catalog.variables.length && !
|
|
2918
|
+
if (!catalog.functions.length && !catalog.variables.length && !hasAliases) return;
|
|
2505
2919
|
pi.registerTool({
|
|
2506
2920
|
name: "workflow_catalog",
|
|
2507
2921
|
label: "Workflow Catalog",
|
|
2508
|
-
description: "List
|
|
2509
|
-
parameters: Type.Object({}, { additionalProperties: false }),
|
|
2510
|
-
async execute(
|
|
2922
|
+
description: "List reusable workflow functions, variables, and model aliases, or load one entry in full",
|
|
2923
|
+
parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function or variable name for full detail" })) }, { additionalProperties: false }),
|
|
2924
|
+
async execute(_id, params = {}) {
|
|
2925
|
+
const result = params.name === undefined ? registry.catalogIndex() : registry.catalogDetail(params.name);
|
|
2926
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
|
|
2927
|
+
}
|
|
2511
2928
|
});
|
|
2512
2929
|
catalogRegistered = true;
|
|
2513
2930
|
};
|
|
@@ -2516,7 +2933,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2516
2933
|
const active = new Set(pi.getActiveTools().filter((tool) => tool !== "workflow" && tool !== "workflow_respond" && tool !== "workflow_stop" && tool !== "workflow_catalog"));
|
|
2517
2934
|
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
2518
2935
|
if (missing) throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
2519
|
-
const settingsPath = workflowSettingsPath();
|
|
2936
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2520
2937
|
const currentSettings = loadSettings(settingsPath);
|
|
2521
2938
|
resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath);
|
|
2522
2939
|
const currentAliases = currentSettings.modelAliases ?? {};
|
|
@@ -2529,13 +2946,14 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2529
2946
|
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2530
2947
|
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
|
|
2531
2948
|
await run.store.saveSnapshot(snapshot);
|
|
2532
|
-
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);
|
|
2949
|
+
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);
|
|
2533
2950
|
run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
|
|
2534
2951
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
2535
2952
|
if (drift.length) await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
2536
2953
|
};
|
|
2537
2954
|
const coldResumeRun = async (run: NonNullable<ReturnType<typeof runs.get>>, hasUI: boolean, ui: { select?: (prompt: string, options: string[]) => Promise<string | undefined> }, trustedProject: boolean, context?: { model: { provider: string; id: string } | undefined; modelRegistry: { getAll?: () => Array<{ provider: string; id: string }>; getAvailable?: () => Array<{ provider: string; id: string }> } | undefined }) => {
|
|
2538
2955
|
const loaded = await run.store.load();
|
|
2956
|
+
await run.store.validateBorrowedWorktrees();
|
|
2539
2957
|
if (loaded.snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
|
|
2540
2958
|
if (loaded.snapshot.roles === undefined) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow role definitions are missing from the launch snapshot");
|
|
2541
2959
|
if ((loaded.snapshot.projectRoles?.length ?? 0) > 0 && !trustedProject) throw new WorkflowError("RESUME_INCOMPATIBLE", "Cannot restore project roles in an untrusted project");
|
|
@@ -2544,7 +2962,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2544
2962
|
const active = new Set(pi.getActiveTools().filter((tool) => tool !== "workflow" && tool !== "workflow_respond" && tool !== "workflow_stop" && tool !== "workflow_catalog"));
|
|
2545
2963
|
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
2546
2964
|
if (missing) throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
2547
|
-
const settingsPath = workflowSettingsPath();
|
|
2965
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2548
2966
|
const currentSettings = loadSettings(settingsPath);
|
|
2549
2967
|
resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
|
|
2550
2968
|
const currentAliases = currentSettings.modelAliases ?? {};
|
|
@@ -2556,10 +2974,11 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2556
2974
|
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
2557
2975
|
const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2558
2976
|
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2559
|
-
|
|
2977
|
+
const script = launchScriptForSnapshot(loaded.snapshot, registry);
|
|
2978
|
+
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);
|
|
2560
2979
|
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
|
|
2561
2980
|
await run.store.saveSnapshot(snapshot);
|
|
2562
|
-
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);
|
|
2981
|
+
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);
|
|
2563
2982
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
2564
2983
|
if (drift.length) await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
2565
2984
|
const controller = new AbortController();
|
|
@@ -2571,11 +2990,12 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2571
2990
|
catch (error) {
|
|
2572
2991
|
const typed = asWorkflowError(error);
|
|
2573
2992
|
if (!["completed", "failed", "stopped"].includes(run.lifecycle.state)) { await run.lifecycle.terminal("failed", typed.code).catch(() => undefined); const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, error: { code: typed.code, message: typed.message } })); await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed"); run.update?.(workflowToolUpdate(persisted)); }
|
|
2993
|
+
await cleanupTerminalRun(run.store.runId);
|
|
2574
2994
|
throw typed;
|
|
2575
2995
|
}
|
|
2576
2996
|
await scheduler.cancelRun(run.store.runId);
|
|
2577
2997
|
await run.lifecycle.resume();
|
|
2578
|
-
const execution = runWorkflow(
|
|
2998
|
+
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) => {
|
|
2579
2999
|
await run.lifecycle.enter();
|
|
2580
3000
|
const conversationId = identity.conversation ? conversationIdentityPath(identity) : undefined;
|
|
2581
3001
|
const conversationLock = conversationId ? `${run.store.runId}:${conversationId}` : "";
|
|
@@ -2613,7 +3033,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2613
3033
|
await run.store.complete(path, outcome.value);
|
|
2614
3034
|
return outcome.value;
|
|
2615
3035
|
} finally { if (conversationLock) conversationLocks.delete(conversationLock); await run.lifecycle.leave(); }
|
|
2616
|
-
|
|
3036
|
+
}, 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 { let previousPhase: string | undefined; const persisted = await persistRunState(run.store, run.metadata, (current) => { previousPhase = current.phase; return { ...current, phase }; }); await eventPublisher.phase(run.store, run.metadata, previousPhase, phase); runs.get(run.store.runId)?.update?.(workflowToolUpdate(persisted)); } finally { await run.lifecycle.leave(); } }, log: logBridge(run.lifecycle, run.metadata.name) }, run.store, runContext, variables, registry), controller.signal);
|
|
2617
3037
|
run.execution = execution;
|
|
2618
3038
|
const completion = execution.result.then(async (value) => {
|
|
2619
3039
|
await scheduler.flush();
|
|
@@ -2630,8 +3050,9 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2630
3050
|
const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
|
|
2631
3051
|
await eventPublisher.runFailed(run.store, run.metadata, typed, state);
|
|
2632
3052
|
run.update?.(workflowToolUpdate(persisted));
|
|
2633
|
-
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) deliverFailure(pi, run.metadata
|
|
2634
|
-
});
|
|
3053
|
+
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) deliverFailure(pi, createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted));
|
|
3054
|
+
}).finally(() => cleanupTerminalRun(run.store.runId));
|
|
3055
|
+
run.completion = completion;
|
|
2635
3056
|
void completion;
|
|
2636
3057
|
};
|
|
2637
3058
|
const applyBudgetDecision = async (request: BudgetApprovalRequest, approved: boolean): Promise<BudgetDecisionResult> => {
|
|
@@ -2659,11 +3080,10 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2659
3080
|
if (budgetRelaxed(currentBudget, nextBudget)) {
|
|
2660
3081
|
const proposalId = randomUUID();
|
|
2661
3082
|
const request: BudgetApprovalRequest = { kind: "budget", proposalId, runId, consumed: usage, previous: currentBudget ?? {}, proposed: nextBudget ?? {}, budgetVersion: loaded.run.budgetVersion ?? 1 };
|
|
2662
|
-
|
|
2663
|
-
|
|
3083
|
+
await run.store.requestWorkflowDecision(request);
|
|
3084
|
+
await appendBudgetDecisionEvent(run, request, "adjustment_requested");
|
|
2664
3085
|
deliver(pi, budgetDecisionDelivery(run.metadata, request));
|
|
2665
|
-
|
|
2666
|
-
return { state: decisionResult.state };
|
|
3086
|
+
return { state: "awaiting_approval", proposalId };
|
|
2667
3087
|
}
|
|
2668
3088
|
const changed = JSON.stringify(currentBudget ?? {}) !== JSON.stringify(nextBudget ?? {});
|
|
2669
3089
|
if (changed) {
|
|
@@ -2712,7 +3132,9 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2712
3132
|
const lifecycle = lifecycleFor(store, loaded.run.state, budgetRuntime, loaded.snapshot.metadata);
|
|
2713
3133
|
const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
2714
3134
|
const roleDefinitions = loaded.snapshot.roles ?? {};
|
|
2715
|
-
|
|
3135
|
+
const abortController = new AbortController();
|
|
3136
|
+
const providerErrorRecovery = createProviderErrorRecovery(ctx, new Set(loaded.snapshot.models), () => { abortController.abort(); });
|
|
3137
|
+
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 } : {}) });
|
|
2716
3138
|
for (const checkpoint of await store.awaitingCheckpoints()) deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
|
|
2717
3139
|
for (const decision of await store.pendingWorkflowDecisions()) deliver(pi, budgetDecisionDelivery(loaded.snapshot.metadata, decision));
|
|
2718
3140
|
scheduler.restoreRun(runId, loaded.snapshot.settings.concurrency, loaded.snapshot.identityVersion === LAUNCH_SNAPSHOT_IDENTITY_VERSION ? await store.loadOwnership() : [], () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
@@ -2737,7 +3159,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2737
3159
|
});
|
|
2738
3160
|
pi.on("before_agent_start", (event, ctx) => {
|
|
2739
3161
|
if (!pi.getActiveTools().includes("workflow")) return;
|
|
2740
|
-
const roles = Object.entries(loadAgentDefinitions(ctx.cwd,
|
|
3162
|
+
const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx))).filter(([, definition]) => definition.description);
|
|
2741
3163
|
if (!roles.length) return;
|
|
2742
3164
|
const content = `Workflow role descriptions:\n${roles.map(([name, definition]) => `- \`${name}\`: ${String(definition.description)}`).join("\n")}`;
|
|
2743
3165
|
return { systemPrompt: `${event.systemPrompt}\n\n${content}` };
|
|
@@ -2748,9 +3170,10 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2748
3170
|
description: WORKFLOW_TOOL_DESCRIPTION,
|
|
2749
3171
|
promptSnippet: WORKFLOW_TOOL_PROMPT_SNIPPET,
|
|
2750
3172
|
parameters: WORKFLOW_TOOL_PARAMETERS,
|
|
2751
|
-
async execute(
|
|
3173
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
2752
3174
|
try {
|
|
2753
|
-
const
|
|
3175
|
+
const headless = object(ctx) && ctx.headless === true;
|
|
3176
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2754
3177
|
const defaults = loadSettings(settingsPath);
|
|
2755
3178
|
if (!ctx.model) throw new WorkflowError("UNKNOWN_MODEL", "A launching model is required");
|
|
2756
3179
|
const settings = Object.freeze({ concurrency: params.concurrency ?? defaults.concurrency, ...(defaults.modelAliases ? { modelAliases: defaults.modelAliases } : {}) });
|
|
@@ -2764,8 +3187,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2764
3187
|
const rootTools = pi.getActiveTools().filter((name) => name !== "workflow" && name !== "workflow_respond" && name !== "workflow_stop" && name !== "workflow_catalog");
|
|
2765
3188
|
const trustedProject = projectTrusted(ctx);
|
|
2766
3189
|
if (typeof ctx.cwd === "string") resolveAgentResourcePolicy(ctx.cwd, trustedProject, settingsPath);
|
|
2767
|
-
const validated = validateWorkflowLaunchWithRegistry(params, { cwd: ctx.cwd, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases: defaults.modelAliases ?? {}, knownModels, settingsPath }, registry);
|
|
2768
|
-
const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames } = validated;
|
|
3190
|
+
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);
|
|
3191
|
+
const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, functionName } = validated;
|
|
2769
3192
|
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
2770
3193
|
const runId = randomUUID();
|
|
2771
3194
|
const args = (params.args ?? null) as JsonValue;
|
|
@@ -2775,22 +3198,25 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2775
3198
|
const runContext = workflowRunContext(ctx.cwd, ctx.sessionManager.getSessionId(), runId, checked.metadata, args, runController.signal);
|
|
2776
3199
|
const variables = await resolveWorkflowVariables(runContext, runController, registry);
|
|
2777
3200
|
const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
|
|
3201
|
+
const parentRunId = params.parentRunId;
|
|
3202
|
+
if (parentRunId !== undefined) await store.validateParentRun(parentRunId);
|
|
2778
3203
|
const roles = Object.fromEntries(roleNames.map((role) => [role, agentDefinitions[role]])) as Record<string, AgentDefinition>;
|
|
2779
3204
|
const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
|
|
2780
3205
|
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, defaults.modelAliases, knownModels, settingsPath)] : []; });
|
|
2781
3206
|
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
|
|
2782
|
-
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 });
|
|
3207
|
+
const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, ...(functionName ? { launchKind: "function" as const, functionName } : {}), ...(defaults.modelAliases ? { modelAliases: defaults.modelAliases } : {}), ...(budget ? { budget } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
|
|
2783
3208
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
2784
3209
|
const initialBudget = budgetRuntime.snapshot();
|
|
2785
|
-
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);
|
|
3210
|
+
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);
|
|
2786
3211
|
const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
|
|
2787
3212
|
const background = !params.foreground;
|
|
2788
3213
|
const providerPause = async () => { if (background) deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
2789
|
-
const
|
|
2790
|
-
|
|
3214
|
+
const providerErrorRecovery = createProviderErrorRecovery(ctx, availableModels, () => { runController.abort(); });
|
|
3215
|
+
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);
|
|
3216
|
+
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 } : {}) });
|
|
2791
3217
|
if (params.foreground && onUpdate) onUpdate(workflowToolUpdate((await store.load()).run));
|
|
2792
3218
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
2793
|
-
const execution = runWorkflow(script, args, withWorkflowFunctions({ agent: async (prompt, options, agentSignal, identity) => {
|
|
3219
|
+
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) => {
|
|
2794
3220
|
await lifecycle.enter();
|
|
2795
3221
|
const conversationId = identity.conversation ? conversationIdentityPath(identity) : undefined;
|
|
2796
3222
|
const conversationLock = conversationId ? `${runId}:${conversationId}` : "";
|
|
@@ -2828,7 +3254,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2828
3254
|
await store.complete(path, outcome.value);
|
|
2829
3255
|
return outcome.value;
|
|
2830
3256
|
} finally { if (conversationLock) conversationLocks.delete(conversationLock); await lifecycle.leave(); }
|
|
2831
|
-
}, checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined), phase: async (phase) => {
|
|
3257
|
+
}, 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) => {
|
|
2832
3258
|
await lifecycle.enter();
|
|
2833
3259
|
try {
|
|
2834
3260
|
let previousPhase: string | undefined;
|
|
@@ -2850,21 +3276,29 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2850
3276
|
await scheduler.flush();
|
|
2851
3277
|
const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
|
|
2852
3278
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(lifecycle.state)) await lifecycle.terminal(typed.code === "CANCELLED" ? "stopped" : typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
2853
|
-
await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot(), error: { code: typed.code, message: typed.message } }));
|
|
3279
|
+
const persisted = await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot(), error: { code: typed.code, message: typed.message } }));
|
|
2854
3280
|
const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
|
|
2855
3281
|
await eventPublisher.runFailed(store, checked.metadata, typed, state);
|
|
3282
|
+
const diagnostic = createWorkflowFailureDiagnostics(store, checked.metadata, typed, persisted);
|
|
3283
|
+
Object.defineProperty(typed, WORKFLOW_FAILURE_DIAGNOSTICS, { value: diagnostic });
|
|
3284
|
+
if (params.foreground) pendingFailureDiagnostics.set(toolCallId, diagnostic);
|
|
2856
3285
|
throw typed;
|
|
2857
3286
|
});
|
|
2858
|
-
|
|
3287
|
+
const completion = finish.finally(() => cleanupTerminalRun(runId));
|
|
3288
|
+
(runs.get(runId) as NonNullable<ReturnType<typeof runs.get>>).completion = completion;
|
|
2859
3289
|
if (background) {
|
|
2860
|
-
void
|
|
3290
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
2861
3291
|
deliver(pi, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
2862
|
-
}, (error: unknown) => {
|
|
3292
|
+
}, (error: unknown) => {
|
|
3293
|
+
const diagnostic = failureDiagnosticsFrom(error);
|
|
3294
|
+
if (diagnostic) deliverFailure(pi, diagnostic);
|
|
3295
|
+
else deliver(pi, `Workflow ${checked.metadata.name} failed: ${formatWorkflowFailure(error)}`);
|
|
3296
|
+
});
|
|
2863
3297
|
return { content: [{ type: "text" as const, text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
2864
3298
|
}
|
|
2865
|
-
const { value } = await
|
|
3299
|
+
const { value } = await completion;
|
|
2866
3300
|
const run = (await store.load()).run;
|
|
2867
|
-
return { content: [{ type: "text" as const, text: JSON.stringify(value) }], details: { runId, value, run } };
|
|
3301
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(value) }, { type: "text" as const, text: `Workflow run ID: ${runId}` }], details: { runId, value, run } };
|
|
2868
3302
|
} catch (error) {
|
|
2869
3303
|
throw mainAgentError(error);
|
|
2870
3304
|
}
|
|
@@ -2873,18 +3307,20 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2873
3307
|
return textBlock(formatWorkflowPreview(args));
|
|
2874
3308
|
},
|
|
2875
3309
|
renderResult(result, { isPartial }, _theme, context) {
|
|
2876
|
-
const details = result.details
|
|
3310
|
+
const details = result.details;
|
|
3311
|
+
if (isWorkflowFailureDiagnostics(details)) return textBlock(formatWorkflowFailureDiagnostics(details));
|
|
3312
|
+
const runDetails = details as { run?: PersistedRun; value?: JsonValue; preview?: string } | undefined;
|
|
2877
3313
|
const state = context.state as { workflowSpinner?: ReturnType<typeof setInterval> };
|
|
2878
|
-
if (
|
|
3314
|
+
if (runDetails?.run && isPartial && runDetails.run.state === "running" && !state.workflowSpinner) {
|
|
2879
3315
|
state.workflowSpinner = setInterval(context.invalidate, 80);
|
|
2880
3316
|
state.workflowSpinner.unref();
|
|
2881
|
-
} else if ((!isPartial ||
|
|
3317
|
+
} else if ((!isPartial || runDetails?.run?.state !== "running") && state.workflowSpinner) {
|
|
2882
3318
|
clearInterval(state.workflowSpinner);
|
|
2883
3319
|
delete state.workflowSpinner;
|
|
2884
3320
|
}
|
|
2885
|
-
if (
|
|
3321
|
+
if (runDetails?.run) return workflowProgressBlock(runDetails.run);
|
|
2886
3322
|
const content = result.content[0];
|
|
2887
|
-
return textBlock(isPartial ? "Workflow starting..." :
|
|
3323
|
+
return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
|
|
2888
3324
|
},
|
|
2889
3325
|
});
|
|
2890
3326
|
pi.registerCommand("workflow", {
|
|
@@ -2976,7 +3412,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2976
3412
|
}
|
|
2977
3413
|
};
|
|
2978
3414
|
const manageAliases = async (): Promise<void> => {
|
|
2979
|
-
const settingsPath = workflowSettingsPath();
|
|
3415
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2980
3416
|
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
2981
3417
|
const available = () => [...new Set((modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`))].sort();
|
|
2982
3418
|
const selectTarget = async (): Promise<string | undefined> => {
|
|
@@ -3052,9 +3488,18 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3052
3488
|
const labels = navigatorRunLabels(sorted);
|
|
3053
3489
|
const terminalStates = new Set(["completed", "failed", "stopped"]);
|
|
3054
3490
|
const hasCompleted = sorted.some(({ loaded: { run } }) => run.state === "completed");
|
|
3055
|
-
const pickerOptions = [...labels, "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : [])];
|
|
3491
|
+
const pickerOptions = [...labels, ...(herdrPaneId() ? ["Inspect session in pane"] : []), "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : [])];
|
|
3056
3492
|
const runChoice = await ctx.ui.select("Workflows\n", pickerOptions);
|
|
3057
3493
|
if (!runChoice || runChoice === "Close") return;
|
|
3494
|
+
if (runChoice === "Inspect session in pane") {
|
|
3495
|
+
try {
|
|
3496
|
+
await openHerdrPane({ action: "inspect", cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId() });
|
|
3497
|
+
ctx.ui.notify("Opened session inspector in pane.", "info");
|
|
3498
|
+
} catch (error) {
|
|
3499
|
+
ctx.ui.notify(`Cannot open session inspector in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
3500
|
+
}
|
|
3501
|
+
continue;
|
|
3502
|
+
}
|
|
3058
3503
|
if (runChoice === "Model aliases") { await manageAliases(); stores = await loadStores(); continue; }
|
|
3059
3504
|
if (runChoice === "Delete all completed") {
|
|
3060
3505
|
if (!await ctx.ui.confirm("Delete completed runs?", "Delete all completed workflow runs and their artifacts? This cannot be undone.")) continue;
|
|
@@ -3076,38 +3521,6 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3076
3521
|
ctx.ui.notify(`Failed to copy ${artifact}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3077
3522
|
}
|
|
3078
3523
|
};
|
|
3079
|
-
const openTranscript = async (transcript: string): Promise<void> => {
|
|
3080
|
-
try {
|
|
3081
|
-
const entries = SessionManager.open(transcript).buildContextEntries();
|
|
3082
|
-
if (ctx.mode !== "tui") { ctx.ui.notify(`Transcript: ${transcript}`, "info"); return; }
|
|
3083
|
-
await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {
|
|
3084
|
-
let offset = 0;
|
|
3085
|
-
let renderedLines: string[] = [];
|
|
3086
|
-
const viewport = () => Math.max(1, tuiRows(tui) - 3);
|
|
3087
|
-
const move = (delta: number) => { offset = Math.max(0, Math.min(Math.max(0, renderedLines.length - viewport()), offset + delta)); };
|
|
3088
|
-
return {
|
|
3089
|
-
render(width: number) {
|
|
3090
|
-
renderedLines = transcriptLines(entries).flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
|
|
3091
|
-
offset = Math.min(offset, Math.max(0, renderedLines.length - viewport()));
|
|
3092
|
-
return [theme.fg("accent", "Native Pi transcript"), ...renderedLines.slice(offset, offset + viewport()), "", theme.fg("dim", "↑↓/pgup/pgdn scroll · esc close")];
|
|
3093
|
-
},
|
|
3094
|
-
invalidate() {},
|
|
3095
|
-
handleInput(data: string) {
|
|
3096
|
-
if (keybindings.matches(data, "tui.select.up")) move(-1);
|
|
3097
|
-
else if (keybindings.matches(data, "tui.select.down")) move(1);
|
|
3098
|
-
else if (keybindings.matches(data, "tui.select.pageUp")) move(-viewport());
|
|
3099
|
-
else if (keybindings.matches(data, "tui.select.pageDown")) move(viewport());
|
|
3100
|
-
else if (keybindings.matches(data, "tui.editor.cursorLineStart")) offset = 0;
|
|
3101
|
-
else if (keybindings.matches(data, "tui.editor.cursorLineEnd")) offset = Math.max(0, renderedLines.length - viewport());
|
|
3102
|
-
else if (keybindings.matches(data, "tui.select.cancel")) done(undefined);
|
|
3103
|
-
tui.requestRender();
|
|
3104
|
-
},
|
|
3105
|
-
};
|
|
3106
|
-
}, { overlay: true });
|
|
3107
|
-
} catch (error) {
|
|
3108
|
-
ctx.ui.notify(`Cannot open transcript: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
3109
|
-
}
|
|
3110
|
-
};
|
|
3111
3524
|
const loadDashboard = async () => {
|
|
3112
3525
|
const loaded = await store.load();
|
|
3113
3526
|
const checkpoints = await store.awaitingCheckpoints();
|
|
@@ -3138,16 +3551,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3138
3551
|
}
|
|
3139
3552
|
if (ctx.mode !== "tui") actions.set("Refresh", "refresh");
|
|
3140
3553
|
else actions.set("View script", "view-script");
|
|
3141
|
-
const transcripts = [...new Set([...loaded.run.agents.flatMap((agent) => (agent.attemptDetails ?? []).map((attempt) => attempt.sessionFile)), ...loaded.run.nativeSessions.map(({ sessionFile }) => sessionFile)])];
|
|
3142
3554
|
if (loaded.run.agents.length) actions.set("Agents...", "agents");
|
|
3143
|
-
if (!loaded.run.agents.length && ctx.mode === "tui" && transcripts.length) actions.set("View transcript", "view-transcript");
|
|
3144
|
-
if (!loaded.run.agents.length && transcripts.length) actions.set("Transcript paths", "transcripts");
|
|
3145
3555
|
if (terminalStates.has(loaded.run.state)) add("Delete", "delete");
|
|
3146
3556
|
if (ctx.mode === "tui") {
|
|
3147
3557
|
addCopy("Copy run path", store.directory, "run path");
|
|
3148
3558
|
addCopy("Copy run ID", store.runId, "run ID");
|
|
3149
3559
|
}
|
|
3150
|
-
return { dashboard: formatNavigatorDashboard(loaded.run, checkpoints, worktrees), actions, copies, reviews,
|
|
3560
|
+
return { dashboard: formatNavigatorDashboard(loaded.run, checkpoints, worktrees), actions, copies, reviews, script: loaded.snapshot.script, agents: loaded.run.agents, worktrees, cwd: loaded.run.cwd };
|
|
3151
3561
|
};
|
|
3152
3562
|
const selectAgent = async (dashboard: Awaited<ReturnType<typeof loadDashboard>>): Promise<void> => {
|
|
3153
3563
|
const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
|
|
@@ -3168,14 +3578,14 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3168
3578
|
const attempts = [...(selected.attemptDetails ?? [])].sort((left, right) => left.attempt - right.attempt);
|
|
3169
3579
|
const worktree = selected.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === selected.worktreeOwner) : undefined;
|
|
3170
3580
|
const actions = [
|
|
3171
|
-
...(attempts.length ? ["
|
|
3581
|
+
...(attempts.length && herdrPaneId() ? ["Fork as Pi session in pane"] : []),
|
|
3172
3582
|
...(worktree ? ["Copy branch", "Copy worktree path"] : []),
|
|
3173
3583
|
"Copy agent ID",
|
|
3174
3584
|
"Back",
|
|
3175
3585
|
];
|
|
3176
3586
|
const chooseAttempt = async (): Promise<AgentAttemptSummary | undefined> => {
|
|
3177
3587
|
const choices = attempts.map((attempt) => `Attempt ${String(attempt.attempt)}`);
|
|
3178
|
-
const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("
|
|
3588
|
+
const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("Fork attempts", [...choices, "Back"]);
|
|
3179
3589
|
const index = choice ? choices.indexOf(choice) : -1;
|
|
3180
3590
|
return index >= 0 ? attempts[index] : undefined;
|
|
3181
3591
|
};
|
|
@@ -3185,11 +3595,17 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3185
3595
|
if (action === "Copy agent ID") { await copyArtifact(selected.id, "agent ID"); continue; }
|
|
3186
3596
|
if (action === "Copy branch" && worktree) { await copyArtifact(worktree.branch, "branch"); continue; }
|
|
3187
3597
|
if (action === "Copy worktree path" && worktree) { await copyArtifact(worktree.path, "worktree path"); continue; }
|
|
3188
|
-
if (action === "
|
|
3598
|
+
if (action === "Fork as Pi session in pane") {
|
|
3189
3599
|
const attempt = await chooseAttempt();
|
|
3190
3600
|
if (!attempt) continue;
|
|
3191
|
-
|
|
3192
|
-
|
|
3601
|
+
const running = !SETTLED_AGENT_STATES.has(selected.state) && attempt.attempt === attempts.at(-1)?.attempt && !attempt.error;
|
|
3602
|
+
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?")) continue;
|
|
3603
|
+
try {
|
|
3604
|
+
await openHerdrPane({ action: "fork", cwd: worktree?.cwd ?? attempt.setup?.cwd ?? dashboard.cwd, original: attempt.sessionFile, ...(running ? { readOnly: true } : {}) });
|
|
3605
|
+
ctx.ui.notify("Forked Pi session in pane.", "info");
|
|
3606
|
+
} catch (error) {
|
|
3607
|
+
ctx.ui.notify(`Cannot fork Pi session in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
3608
|
+
}
|
|
3193
3609
|
}
|
|
3194
3610
|
}
|
|
3195
3611
|
};
|
|
@@ -3204,7 +3620,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3204
3620
|
let disposed = false;
|
|
3205
3621
|
let stopRequested = false;
|
|
3206
3622
|
let stopStatus: string | undefined;
|
|
3207
|
-
const terminalRows = () => Math.max(1, tuiRows(tui));
|
|
3623
|
+
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
3208
3624
|
const keyLabels: Record<string, string> = { up: "↑", down: "↓", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
|
|
3209
3625
|
const keyLabel = (binding: string, fallback: string) => {
|
|
3210
3626
|
const keys = keybindingKeys(keybindings, binding);
|
|
@@ -3213,9 +3629,9 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3213
3629
|
const dashboardLayout = () => {
|
|
3214
3630
|
const rows = terminalRows();
|
|
3215
3631
|
const hintRows = rows >= 4 ? 1 : 0;
|
|
3216
|
-
const separatorRows = rows >=
|
|
3632
|
+
const separatorRows = rows >= 8 ? 1 : 0;
|
|
3217
3633
|
const available = Math.max(1, rows - hintRows - separatorRows);
|
|
3218
|
-
const actionViewport = Math.min(options.length, Math.max(1, Math.
|
|
3634
|
+
const actionViewport = Math.min(options.length, Math.max(1, Math.ceil(available / 2)));
|
|
3219
3635
|
return { rows, hintRows, separatorRows, actionViewport, dashboardViewport: available - actionViewport };
|
|
3220
3636
|
};
|
|
3221
3637
|
const updateDashboard = async (selectedOption: string | undefined) => {
|
|
@@ -3252,7 +3668,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3252
3668
|
void updateDashboard(selectedOption).catch(() => undefined).finally(() => { refreshing = false; });
|
|
3253
3669
|
}, 1000);
|
|
3254
3670
|
timer.unref();
|
|
3255
|
-
return {
|
|
3671
|
+
return borderWorkflowOverlay({
|
|
3256
3672
|
render(width: number) {
|
|
3257
3673
|
const dashboard = stopStatus ? `${view.dashboard}\n\n${stopStatus}` : view.dashboard;
|
|
3258
3674
|
const dashboardLines = truncateToVisualLines(theme.fg("accent", dashboard), Number.MAX_SAFE_INTEGER, width, 1).visualLines;
|
|
@@ -3290,7 +3706,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3290
3706
|
tui.requestRender();
|
|
3291
3707
|
},
|
|
3292
3708
|
dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
|
|
3293
|
-
};
|
|
3709
|
+
}, theme);
|
|
3294
3710
|
}, { overlay: true })
|
|
3295
3711
|
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Close"]);
|
|
3296
3712
|
if (!actionChoice || actionChoice === "Close") return;
|
|
@@ -3301,12 +3717,12 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3301
3717
|
const highlighted = highlightCode(view.script, "javascript");
|
|
3302
3718
|
let offset = 0;
|
|
3303
3719
|
let renderedLines: string[] = [];
|
|
3304
|
-
const viewport = () => Math.max(1, tuiRows(tui) - 3);
|
|
3720
|
+
const viewport = () => Math.max(1, tuiRows(tui) - 3 - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
3305
3721
|
const move = (delta: number) => {
|
|
3306
3722
|
const maxOffset = Math.max(0, renderedLines.length - viewport());
|
|
3307
3723
|
offset = Math.max(0, Math.min(maxOffset, offset + delta));
|
|
3308
3724
|
};
|
|
3309
|
-
return {
|
|
3725
|
+
return borderWorkflowOverlay({
|
|
3310
3726
|
render(width: number) {
|
|
3311
3727
|
renderedLines = highlighted.flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
|
|
3312
3728
|
const maxOffset = Math.max(0, renderedLines.length - viewport());
|
|
@@ -3327,21 +3743,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3327
3743
|
else if (keybindings.matches(data, "tui.select.cancel")) done(undefined);
|
|
3328
3744
|
tui.requestRender();
|
|
3329
3745
|
},
|
|
3330
|
-
};
|
|
3331
|
-
});
|
|
3332
|
-
continue;
|
|
3333
|
-
}
|
|
3334
|
-
if (actionChoice === "View transcript") {
|
|
3335
|
-
const transcript = await ctx.ui.select("Native Pi transcripts", [...view.transcripts, "Back"]);
|
|
3336
|
-
if (transcript && transcript !== "Back") await openTranscript(transcript);
|
|
3337
|
-
continue;
|
|
3338
|
-
}
|
|
3339
|
-
if (actionChoice === "Transcript paths") {
|
|
3340
|
-
const transcript = await ctx.ui.select("Native Pi transcript paths", [...view.transcripts, "Back"]);
|
|
3341
|
-
if (transcript && transcript !== "Back") {
|
|
3342
|
-
if (ctx.mode === "tui") await copyArtifact(transcript, "transcript path");
|
|
3343
|
-
else ctx.ui.notify(transcript, "info");
|
|
3344
|
-
}
|
|
3746
|
+
}, theme);
|
|
3747
|
+
}, { overlay: true, overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" } });
|
|
3345
3748
|
continue;
|
|
3346
3749
|
}
|
|
3347
3750
|
const copy = view.copies.get(actionChoice);
|
|
@@ -3355,11 +3758,11 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3355
3758
|
let offset = 0;
|
|
3356
3759
|
let renderedLines: string[] = [];
|
|
3357
3760
|
const layout = () => {
|
|
3358
|
-
const rows = Math.max(1, tuiRows(tui));
|
|
3761
|
+
const rows = Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
3359
3762
|
const compactControls = rows < 4;
|
|
3360
3763
|
const titleRows = rows >= 5 ? 1 : 0;
|
|
3361
3764
|
const hintRows = rows >= 8 ? 1 : 0;
|
|
3362
|
-
const separatorRows = rows >= 8 ?
|
|
3765
|
+
const separatorRows = rows >= 8 ? 1 : 0;
|
|
3363
3766
|
const controlRows = compactControls ? 1 : options.length;
|
|
3364
3767
|
const contentViewport = Math.max(0, rows - titleRows - hintRows - separatorRows - controlRows);
|
|
3365
3768
|
return { rows, compactControls, titleRows, hintRows, separatorRows, contentViewport };
|
|
@@ -3368,7 +3771,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3368
3771
|
const maxOffset = Math.max(0, renderedLines.length - layout().contentViewport);
|
|
3369
3772
|
offset = Math.max(0, Math.min(maxOffset, offset + delta));
|
|
3370
3773
|
};
|
|
3371
|
-
return {
|
|
3774
|
+
return borderWorkflowOverlay({
|
|
3372
3775
|
render(width: number) {
|
|
3373
3776
|
renderedLines = truncateToVisualLines(formatCheckpointReview(checkpoint), Number.MAX_SAFE_INTEGER, width, 0).visualLines;
|
|
3374
3777
|
const currentLayout = layout();
|
|
@@ -3383,7 +3786,6 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3383
3786
|
...renderedLines.slice(offset, offset + currentLayout.contentViewport),
|
|
3384
3787
|
...(currentLayout.separatorRows ? [""] : []),
|
|
3385
3788
|
...controls,
|
|
3386
|
-
...(currentLayout.separatorRows ? [""] : []),
|
|
3387
3789
|
...(currentLayout.hintRows ? [hint] : []),
|
|
3388
3790
|
];
|
|
3389
3791
|
},
|
|
@@ -3397,8 +3799,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3397
3799
|
else if (keybindings.matches(data, "tui.select.cancel")) done(undefined);
|
|
3398
3800
|
tui.requestRender();
|
|
3399
3801
|
},
|
|
3400
|
-
};
|
|
3401
|
-
});
|
|
3802
|
+
}, theme);
|
|
3803
|
+
}, { overlay: true, overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" } });
|
|
3402
3804
|
if (decision) {
|
|
3403
3805
|
const accepted = await answerCheckpoint(store.runId, checkpoint.name, decision === "Approve", true);
|
|
3404
3806
|
if (!accepted) ctx.ui.notify("Checkpoint is not awaiting a response.", "warning");
|
|
@@ -3451,6 +3853,6 @@ function modelSpec(value: string, fallback: ModelSpec): ModelSpec {
|
|
|
3451
3853
|
export { acquireSessionLease, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
|
|
3452
3854
|
export type { AwaitingCheckpoint, CompletedOperation, ConversationHead, NativeSessionReference, PendingWorkflowDecision, PersistedConversation, PersistedOwnershipNode, PersistedRun, WorktreeReference } from "./persistence.js";
|
|
3453
3855
|
export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
|
|
3454
|
-
export type { AgentAccounting, AgentAttempt, AgentBudgetHooks, AgentDefinition, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentSetup, AgentSetupContext, AgentSetupHook, AgentToolCallProgress, RegisteredAgentSetupHook, SessionInput } from "./agent-execution.js";
|
|
3856
|
+
export type { AgentAccounting, AgentAttempt, AgentBudgetHooks, AgentDefinition, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentSetup, AgentSetupContext, AgentSetupHook, AgentToolCallProgress, RegisteredAgentSetupHook, SessionInput } from "./agent-execution.js";
|
|
3455
3857
|
export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
|
|
3456
|
-
export type { DoctorDiagnostic, DoctorOptions, DoctorPiState, DoctorReport, DoctorRole, DoctorSeverity, DoctorTrust
|
|
3858
|
+
export type { DoctorDiagnostic, DoctorFunction, DoctorOptions, DoctorPiState, DoctorReport, DoctorRole, DoctorSeverity, DoctorTrust } from "./doctor.js";
|