pi-extensible-workflows 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/dist/src/agent-execution.d.ts +13 -0
- package/dist/src/agent-execution.js +177 -40
- package/dist/src/ambient-workflow-evals.js +14 -33
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +530 -6
- package/dist/src/doctor.d.ts +4 -4
- package/dist/src/doctor.js +9 -31
- package/dist/src/herdr.d.ts +12 -0
- package/dist/src/herdr.js +74 -0
- package/dist/src/index.d.ts +107 -27
- package/dist/src/index.js +838 -322
- package/dist/src/persistence.d.ts +28 -0
- package/dist/src/persistence.js +213 -16
- package/dist/src/session-inspector.d.ts +1 -0
- package/dist/src/session-inspector.js +3 -0
- package/dist/src/workflow-evals.d.ts +1 -0
- package/dist/src/workflow-evals.js +25 -26
- package/package.json +8 -4
- package/skills/pi-extensible-workflows/SKILL.md +48 -60
- package/src/agent-execution.ts +140 -27
- package/src/ambient-workflow-evals.ts +18 -28
- package/src/cli.ts +418 -6
- package/src/doctor.ts +13 -35
- package/src/herdr.ts +73 -0
- package/src/index.ts +734 -266
- package/src/persistence.ts +192 -16
- package/src/session-inspector.ts +4 -0
- package/src/workflow-evals.ts +15 -17
package/src/index.ts
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
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
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync,
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
6
6
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { StringDecoder } from "node:string_decoder";
|
|
8
9
|
import * as acorn from "acorn";
|
|
9
10
|
import { Script } from "node:vm";
|
|
10
11
|
import { Type } from "@earendil-works/pi-ai";
|
|
11
12
|
import { Value } from "typebox/value";
|
|
12
|
-
import { copyToClipboard, getAgentDir, parseFrontmatter, highlightCode,
|
|
13
|
-
import { 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 {
|
|
15
|
-
import { acquireSessionLease, listRunIds, RunStore, SessionLease, structuralPath as operationPath } from "./persistence.js";
|
|
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";
|
|
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
|
|
21
|
-
export
|
|
21
|
+
export const WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree", "shell"] as const;
|
|
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";
|
|
24
25
|
export const WORKFLOW_RUN_STATE_CHANGED_EVENT = "workflow:run-state-changed";
|
|
@@ -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,35 +72,72 @@ 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
|
-
export interface WorkflowRunEvent { type:
|
|
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[] }
|
|
77
|
+
export interface WorkflowRunEvent { type: string; message: string }
|
|
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;
|
|
88
96
|
}
|
|
89
97
|
export interface WorkflowRunContext { cwd: string; sessionId: string; runId: string; workflow: Readonly<WorkflowMetadata>; args: JsonValue; signal: AbortSignal }
|
|
90
|
-
export interface WorkflowFunctionContext extends WorkflowOrchestrationContext {
|
|
98
|
+
export interface WorkflowFunctionContext extends WorkflowOrchestrationContext {
|
|
99
|
+
run: Readonly<WorkflowRunContext>;
|
|
100
|
+
invoke: (name: string, input: Readonly<Record<string, JsonValue>>) => Promise<JsonValue>;
|
|
101
|
+
}
|
|
102
|
+
export type WorkflowWorktreeCallback = (reference: Readonly<WorkflowWorktreeReference>) => JsonValue | Promise<JsonValue>;
|
|
91
103
|
export interface WorkflowFunction { description: string; input: JsonSchema; output: JsonSchema; run: (input: Readonly<Record<string, JsonValue>>, context: Readonly<WorkflowFunctionContext>) => Promise<JsonValue> | JsonValue }
|
|
92
104
|
export interface WorkflowVariable { description: string; schema: JsonSchema; resolve: (run: Readonly<WorkflowRunContext>) => Promise<JsonValue> | JsonValue }
|
|
93
|
-
export interface
|
|
94
|
-
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>> }
|
|
95
106
|
export interface WorkflowJournal { get(path: string): JsonValue | undefined; put(path: string, value: JsonValue): void }
|
|
96
107
|
|
|
97
108
|
export class WorkflowError extends Error {
|
|
98
109
|
constructor(public readonly code: WorkflowErrorCode, message: string) { super(message); this.name = "WorkflowError"; }
|
|
99
110
|
}
|
|
100
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
|
+
|
|
101
141
|
const WORKFLOW_AUTHORED_ERROR = Symbol("workflowAuthoredError");
|
|
102
142
|
type WorkerErrorShape = WorkflowErrorShape & { authored?: boolean; failedAt?: string };
|
|
103
143
|
|
|
@@ -127,12 +167,13 @@ const WORKFLOW_ERROR_PROSE: Record<WorkflowErrorCode, (detail: string) => string
|
|
|
127
167
|
INVALID_SCHEMA: (detail) => `The workflow schema is invalid: ${detail}.`,
|
|
128
168
|
REGISTRY_FROZEN: (detail) => `Workflow extension registration is closed: ${detail}.`,
|
|
129
169
|
GLOBAL_COLLISION: (detail) => `The workflow global name is already in use: ${detail}.`,
|
|
130
|
-
MISSING_WORKFLOW: (detail) => `The workflow
|
|
170
|
+
MISSING_WORKFLOW: (detail) => `The registered workflow function is unavailable: ${detail}.`,
|
|
131
171
|
UNKNOWN_MODEL: (detail) => `The workflow requested the unavailable model ${detail.replace(/^(?:Unknown model(?: for role [^:]+)?|Invalid model spec):\s*/, "")}.`,
|
|
132
172
|
UNKNOWN_TOOL: (detail) => `The workflow requested the unavailable tool ${detail.replace(/^Unknown tool:\s*/, "")}.`,
|
|
133
173
|
UNKNOWN_AGENT_TYPE: (detail) => `The workflow requested the unavailable agent role ${detail.replace(/^Unknown agent role:\s*/, "")}.`,
|
|
134
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}.`,
|
|
135
175
|
RPC_LIMIT_EXCEEDED: (detail) => `The workflow communication data exceeded its size limit: ${detail}.`,
|
|
176
|
+
SHELL_FAILED: (detail) => `The workflow shell command failed: ${detail}.`,
|
|
136
177
|
AGENT_TIMEOUT: (detail) => `The workflow agent timed out: ${detail}.`,
|
|
137
178
|
AGENT_FAILED: (detail) => `The workflow agent failed: ${detail}.`,
|
|
138
179
|
RESULT_INVALID: (detail) => `The workflow produced an invalid result: ${detail}.`,
|
|
@@ -234,6 +275,7 @@ export const DEFAULT_SETTINGS: Readonly<WorkflowSettings> = Object.freeze({ conc
|
|
|
234
275
|
|
|
235
276
|
function fail(code: WorkflowErrorCode, message: string): never { throw new WorkflowError(code, message); }
|
|
236
277
|
function object(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
278
|
+
export { object as isObject };
|
|
237
279
|
function nonNegativeInteger(value: unknown): value is number { return Number.isInteger(value) && (value as number) >= 0; }
|
|
238
280
|
function nonNegativeFinite(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
|
|
239
281
|
export function validateBudget(value: unknown): WorkflowBudget | undefined {
|
|
@@ -496,9 +538,7 @@ export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonl
|
|
|
496
538
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
497
539
|
}
|
|
498
540
|
mkdirSync(dirname(path), { recursive: true });
|
|
499
|
-
|
|
500
|
-
writeFileSync(temporary, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, { mode: 0o600 });
|
|
501
|
-
renameSync(temporary, path);
|
|
541
|
+
atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
|
|
502
542
|
}
|
|
503
543
|
|
|
504
544
|
export function parseRoleMarkdown(content: string, strict = false, rolePath?: string): AgentDefinition {
|
|
@@ -613,15 +653,25 @@ type WorkflowCall = acorn.CallExpression & { callee: acorn.Identifier };
|
|
|
613
653
|
function astNode(value: unknown): value is acorn.AnyNode {
|
|
614
654
|
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
615
655
|
}
|
|
656
|
+
function astChildren(node: acorn.AnyNode): acorn.AnyNode[] {
|
|
657
|
+
const children: acorn.AnyNode[] = [];
|
|
658
|
+
for (const value of Object.values(node) as unknown[]) {
|
|
659
|
+
if (Array.isArray(value)) {
|
|
660
|
+
for (const child of value) if (astNode(child)) children.push(child);
|
|
661
|
+
} else if (astNode(value)) children.push(value);
|
|
662
|
+
}
|
|
663
|
+
return children;
|
|
664
|
+
}
|
|
665
|
+
function workflowCallKind(node: acorn.AnyNode): WorkflowCallKind | undefined {
|
|
666
|
+
if (node.type !== "CallExpression" || node.callee.type !== "Identifier") return undefined;
|
|
667
|
+
const kind = node.callee.name as WorkflowCallKind;
|
|
668
|
+
return WORKFLOW_CALL_KINDS.includes(kind) ? kind : undefined;
|
|
669
|
+
}
|
|
616
670
|
function workflowCalls(program: acorn.Program): WorkflowCall[] {
|
|
617
671
|
const calls: WorkflowCall[] = [];
|
|
618
672
|
const visit = (node: acorn.AnyNode): void => {
|
|
619
|
-
if (
|
|
620
|
-
for (const
|
|
621
|
-
if (Array.isArray(value)) {
|
|
622
|
-
for (const child of value as unknown[]) if (astNode(child)) visit(child);
|
|
623
|
-
} else if (astNode(value)) visit(value);
|
|
624
|
-
}
|
|
673
|
+
if (workflowCallKind(node)) calls.push(node as WorkflowCall);
|
|
674
|
+
for (const child of astChildren(node)) visit(child);
|
|
625
675
|
};
|
|
626
676
|
visit(program);
|
|
627
677
|
return calls.sort((left, right) => left.start - right.start);
|
|
@@ -638,9 +688,9 @@ function workflowCallsWithStructure(program: acorn.Program): Array<{ call: Workf
|
|
|
638
688
|
const key = node.key.type === "Identifier" ? node.key.name : node.key.type === "Literal" ? String(node.key.value) : undefined;
|
|
639
689
|
if (scope?.key === null && key) current = { ...current, structure: [...current.structure.slice(0, -1), { ...scope, key }] };
|
|
640
690
|
}
|
|
641
|
-
|
|
691
|
+
const operation = workflowCallKind(node);
|
|
692
|
+
if (operation) {
|
|
642
693
|
const call = node as WorkflowCall;
|
|
643
|
-
const operation = call.callee.name as StaticWorkflowCall["kind"];
|
|
644
694
|
const execution = operation === "parallel" ? "parallel" : operation === "pipeline" ? "sequential" : current.execution;
|
|
645
695
|
calls.push({ call, execution, structure: current.structure });
|
|
646
696
|
for (const [index, argument] of call.arguments.entries()) {
|
|
@@ -650,16 +700,11 @@ function workflowCallsWithStructure(program: acorn.Program): Array<{ call: Workf
|
|
|
650
700
|
}
|
|
651
701
|
return;
|
|
652
702
|
}
|
|
653
|
-
for (const
|
|
654
|
-
if (Array.isArray(value)) {
|
|
655
|
-
for (const child of value as unknown[]) if (astNode(child)) visit(child, current);
|
|
656
|
-
} else if (astNode(value)) visit(value, current);
|
|
657
|
-
}
|
|
703
|
+
for (const child of astChildren(node)) visit(child, current);
|
|
658
704
|
};
|
|
659
705
|
visit(program, { execution: "sequential", structure: [] });
|
|
660
706
|
return calls.sort((left, right) => left.call.start - right.call.start);
|
|
661
707
|
}
|
|
662
|
-
|
|
663
708
|
function validateDirectPrimitiveReferences(program: acorn.AnyNode, name: string): void {
|
|
664
709
|
const visit = (node: acorn.AnyNode, parent?: acorn.AnyNode): void => {
|
|
665
710
|
if (node.type === "Identifier" && node.name === name) {
|
|
@@ -667,23 +712,19 @@ function validateDirectPrimitiveReferences(program: acorn.AnyNode, name: string)
|
|
|
667
712
|
const propertyKey = parent?.type === "Property" && parent.key === node && !parent.computed && !parent.shorthand;
|
|
668
713
|
if (!directCall && !propertyKey) fail("INVALID_METADATA", `${name} calls must use a direct ${name}(...) call; aliases and indirect calls are unsupported`);
|
|
669
714
|
}
|
|
670
|
-
for (const
|
|
671
|
-
if (Array.isArray(value)) {
|
|
672
|
-
for (const child of value as unknown[]) if (astNode(child)) visit(child, node);
|
|
673
|
-
} else if (astNode(value)) visit(value, node);
|
|
674
|
-
}
|
|
715
|
+
for (const child of astChildren(node)) visit(child, node);
|
|
675
716
|
};
|
|
676
717
|
visit(program);
|
|
677
718
|
}
|
|
678
|
-
|
|
679
719
|
function hasIdentifier(node: acorn.AnyNode, name: string): boolean {
|
|
680
720
|
if (node.type === "Identifier" && node.name === name) return true;
|
|
681
|
-
return
|
|
721
|
+
return astChildren(node).some((child) => hasIdentifier(child, name));
|
|
682
722
|
}
|
|
683
723
|
|
|
684
724
|
const INTERNAL_AGENT_NAME = "__pi_extensible_workflows_agent";
|
|
685
725
|
const INTERNAL_CONVERSATION_NAME = "__pi_extensible_workflows_conversation";
|
|
686
726
|
const INTERNAL_WORKTREE_NAME = "__pi_extensible_workflows_withWorktree";
|
|
727
|
+
const INTERNAL_SHELL_NAME = "__pi_extensible_workflows_shell";
|
|
687
728
|
|
|
688
729
|
function callHasTrailingComma(source: string, call: WorkflowCall): boolean {
|
|
689
730
|
let previous: acorn.Token | undefined;
|
|
@@ -702,12 +743,13 @@ function instrumentWorkflow(script: string): string {
|
|
|
702
743
|
if (hasIdentifier(program, INTERNAL_AGENT_NAME)) fail("INVALID_METADATA", `${INTERNAL_AGENT_NAME} is reserved for workflow agent instrumentation`);
|
|
703
744
|
if (hasIdentifier(program, INTERNAL_CONVERSATION_NAME)) fail("INVALID_METADATA", `${INTERNAL_CONVERSATION_NAME} is reserved for workflow conversation instrumentation`);
|
|
704
745
|
if (hasIdentifier(program, INTERNAL_WORKTREE_NAME)) fail("INVALID_METADATA", `${INTERNAL_WORKTREE_NAME} is reserved for workflow withWorktree instrumentation`);
|
|
705
|
-
|
|
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));
|
|
706
748
|
const edits = calls.flatMap((call) => {
|
|
707
749
|
const callSite = `${String(call.start)}:${String(call.end)}`;
|
|
708
750
|
const hiddenArgument = call.arguments.length === 0 || callHasTrailingComma(body, call) ? "" : ", ";
|
|
709
751
|
return [
|
|
710
|
-
{ 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 },
|
|
711
753
|
{ start: call.end - 1, end: call.end - 1, text: `${hiddenArgument}${JSON.stringify(callSite)}` },
|
|
712
754
|
];
|
|
713
755
|
}).sort((left, right) => right.start - left.start);
|
|
@@ -819,6 +861,18 @@ function validateAgentOptions(value: unknown): Readonly<Record<string, JsonValue
|
|
|
819
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");
|
|
820
862
|
return value;
|
|
821
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
|
+
}
|
|
822
876
|
|
|
823
877
|
type StaticValue = { known: true; value: unknown } | { known: false };
|
|
824
878
|
|
|
@@ -854,7 +908,7 @@ function staticValue(node: acorn.AnyNode | undefined): StaticValue {
|
|
|
854
908
|
}
|
|
855
909
|
|
|
856
910
|
export interface StaticWorkflowCall {
|
|
857
|
-
kind:
|
|
911
|
+
kind: WorkflowCallKind;
|
|
858
912
|
start: number;
|
|
859
913
|
end: number;
|
|
860
914
|
name: string | null;
|
|
@@ -899,6 +953,7 @@ export function inspectWorkflowScript(script: string): StaticWorkflowCall[] {
|
|
|
899
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 } : {}) };
|
|
900
954
|
}
|
|
901
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 };
|
|
902
957
|
return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
|
|
903
958
|
});
|
|
904
959
|
}
|
|
@@ -912,6 +967,14 @@ function validateStaticAgentOptions(node: acorn.AnyNode | undefined, aliases: Re
|
|
|
912
967
|
if (value.known) validateAgentOption(key, value.value, aliases, knownModels, settingsPath);
|
|
913
968
|
}
|
|
914
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
|
+
}
|
|
915
978
|
|
|
916
979
|
function validateStaticWithWorktree(call: WorkflowCall): void {
|
|
917
980
|
if (call.arguments.some((argument) => argument.type === "SpreadElement")) return;
|
|
@@ -926,16 +989,18 @@ function validateStaticWithWorktree(call: WorkflowCall): void {
|
|
|
926
989
|
|
|
927
990
|
export interface WorkflowCatalogFunction { name: string; version: string; headline: string; extensionDescription: string; description: string; input: JsonSchema; output: JsonSchema }
|
|
928
991
|
export interface WorkflowCatalogVariable { name: string; version: string; headline: string; extensionDescription: string; description: string; schema: JsonSchema }
|
|
929
|
-
export interface
|
|
930
|
-
export interface
|
|
931
|
-
|
|
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"]);
|
|
932
998
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
933
999
|
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
934
1000
|
|
|
935
1001
|
export class WorkflowRegistry {
|
|
936
1002
|
readonly #extensions = new Set<Readonly<WorkflowExtension>>();
|
|
937
1003
|
readonly #globals = new Map<string, string>();
|
|
938
|
-
readonly #workflows = new Map<string, WorkflowScriptDefinition>();
|
|
939
1004
|
readonly #hooks = new Map<string, RegisteredAgentSetupHook>();
|
|
940
1005
|
#frozen = false;
|
|
941
1006
|
|
|
@@ -944,12 +1009,12 @@ export class WorkflowRegistry {
|
|
|
944
1009
|
|
|
945
1010
|
register(extension: WorkflowExtension): void {
|
|
946
1011
|
if (this.#frozen) fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
|
|
947
|
-
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");
|
|
948
1014
|
const functions = extension.functions ?? {};
|
|
949
1015
|
const variables = extension.variables ?? {};
|
|
950
|
-
const workflows = extension.workflows ?? {};
|
|
951
1016
|
const agentSetupHooks = extension.agentSetupHooks ?? {};
|
|
952
|
-
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");
|
|
953
1018
|
const names = [...Object.keys(functions), ...Object.keys(variables)];
|
|
954
1019
|
if (new Set(names).size !== names.length) fail("GLOBAL_COLLISION", "Global name collision inside extension");
|
|
955
1020
|
for (const name of names) {
|
|
@@ -967,46 +1032,54 @@ export class WorkflowRegistry {
|
|
|
967
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}`);
|
|
968
1033
|
validateSchema(variable.schema, `${name} schema`);
|
|
969
1034
|
}
|
|
970
|
-
for (const [name, workflow] of Object.entries(workflows)) {
|
|
971
|
-
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}`);
|
|
972
|
-
if (this.#workflows.has(name)) fail("DUPLICATE_NAME", `Reusable workflow already registered: ${name}`);
|
|
973
|
-
parseWorkflow(workflow.script);
|
|
974
|
-
}
|
|
975
1035
|
for (const [name, hook] of Object.entries(agentSetupHooks)) {
|
|
976
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}`);
|
|
977
1037
|
if (this.#hooks.has(name)) fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
|
|
978
1038
|
}
|
|
979
|
-
const stored = deepFreeze({ ...extension, functions, variables,
|
|
1039
|
+
const stored = deepFreeze({ ...extension, functions, variables, agentSetupHooks });
|
|
980
1040
|
this.#extensions.add(stored);
|
|
981
1041
|
for (const name of names) this.#globals.set(name, name);
|
|
982
|
-
for (const [name, workflow] of Object.entries(workflows)) this.#workflows.set(name, workflow);
|
|
983
1042
|
for (const [name, hook] of Object.entries(agentSetupHooks)) this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
|
|
984
1043
|
}
|
|
985
1044
|
|
|
986
|
-
|
|
987
|
-
if (!IDENTIFIER.test(name)) fail("MISSING_WORKFLOW", `Registered
|
|
988
|
-
const
|
|
989
|
-
if (!
|
|
990
|
-
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;
|
|
991
1050
|
}
|
|
992
1051
|
|
|
993
|
-
|
|
994
|
-
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 ?? {}))));
|
|
995
1054
|
}
|
|
996
1055
|
|
|
997
1056
|
catalog(): WorkflowCatalog {
|
|
998
1057
|
const functions: WorkflowCatalogFunction[] = [];
|
|
999
1058
|
const variables: WorkflowCatalogVariable[] = [];
|
|
1000
|
-
const workflows: WorkflowCatalogWorkflow[] = [];
|
|
1001
1059
|
for (const extension of this.#extensions) {
|
|
1002
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) });
|
|
1003
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) });
|
|
1004
|
-
for (const [name, workflow] of Object.entries(extension.workflows ?? {})) workflows.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: workflow.description });
|
|
1005
1062
|
}
|
|
1006
1063
|
let aliases: Readonly<Record<string, string>> | undefined;
|
|
1007
1064
|
try { aliases = loadSettings().modelAliases; } catch { aliases = undefined; }
|
|
1008
1065
|
const sort = (left: { name: string }, right: { name: string }) => left.name.localeCompare(right.name);
|
|
1009
|
-
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}` } });
|
|
1010
1083
|
}
|
|
1011
1084
|
|
|
1012
1085
|
globals(): Readonly<Record<string, { name: string }>> {
|
|
@@ -1014,15 +1087,14 @@ export class WorkflowRegistry {
|
|
|
1014
1087
|
}
|
|
1015
1088
|
|
|
1016
1089
|
async invokeFunction(name: string, input: unknown, context: Readonly<WorkflowFunctionContext>, path: string, journal: WorkflowJournal): Promise<JsonValue> {
|
|
1017
|
-
const fn =
|
|
1018
|
-
if (!fn) fail("MISSING_WORKFLOW", `Workflow function is unavailable: ${name}`);
|
|
1090
|
+
const fn = this.function(name);
|
|
1019
1091
|
if (!object(input) || !jsonValue(input) || !Value.Check(fn.input, input)) fail("RESULT_INVALID", `Invalid input for ${name}`);
|
|
1020
1092
|
const replayed = journal.get(path);
|
|
1021
1093
|
if (replayed !== undefined) {
|
|
1022
1094
|
if (!jsonValue(replayed) || !Value.Check(fn.output, replayed)) fail("RESULT_INVALID", `Invalid replay for ${name}`);
|
|
1023
1095
|
return structuredClone(replayed);
|
|
1024
1096
|
}
|
|
1025
|
-
const result: unknown = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, agent: context.agent, prompt: context.prompt, parallel: context.parallel, pipeline: context.pipeline, withWorktree: context.withWorktree, checkpoint: context.checkpoint, phase: context.phase, log: context.log }));
|
|
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 }));
|
|
1026
1098
|
if (!jsonValue(result) || !Value.Check(fn.output, result)) fail("RESULT_INVALID", `Invalid output from ${name}`);
|
|
1027
1099
|
const stored = structuredClone(result);
|
|
1028
1100
|
journal.put(path, stored);
|
|
@@ -1036,7 +1108,7 @@ export class WorkflowRegistry {
|
|
|
1036
1108
|
return [...this.#hooks.values()].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
1037
1109
|
}
|
|
1038
1110
|
}
|
|
1039
|
-
type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "
|
|
1111
|
+
type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "agentSetupHooks">;
|
|
1040
1112
|
interface WorkflowRegistryHost { api: WorkflowRegistryApi }
|
|
1041
1113
|
const WORKFLOW_REGISTRY_KEY = Symbol.for("pi-extensible-workflows.workflow-registry");
|
|
1042
1114
|
const globalRegistry = globalThis as typeof globalThis & Record<symbol, WorkflowRegistryHost | undefined>;
|
|
@@ -1045,9 +1117,11 @@ function createWorkflowRegistryApi(registry: WorkflowRegistry): WorkflowRegistry
|
|
|
1045
1117
|
get frozen() { return registry.frozen; },
|
|
1046
1118
|
freeze: () => { registry.freeze(); },
|
|
1047
1119
|
register: (extension) => { registry.register(extension); },
|
|
1048
|
-
|
|
1049
|
-
|
|
1120
|
+
function: (name) => registry.function(name),
|
|
1121
|
+
functions: () => registry.functions(),
|
|
1050
1122
|
catalog: () => registry.catalog(),
|
|
1123
|
+
catalogIndex: () => registry.catalogIndex(),
|
|
1124
|
+
catalogDetail: (name) => registry.catalogDetail(name),
|
|
1051
1125
|
globals: () => registry.globals(),
|
|
1052
1126
|
invokeFunction: (...args) => registry.invokeFunction(...args),
|
|
1053
1127
|
variables: () => registry.variables(),
|
|
@@ -1067,26 +1141,29 @@ function loadingRegistry(): WorkflowRegistryApi { return workflowRegistryHost().
|
|
|
1067
1141
|
beginWorkflowExtensionLoading();
|
|
1068
1142
|
export function registerWorkflowExtension(extension: WorkflowExtension): void { loadingRegistry().register(extension); }
|
|
1069
1143
|
export function workflowCatalog(): WorkflowCatalog { return loadingRegistry().catalog(); }
|
|
1070
|
-
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(); }
|
|
1071
1147
|
|
|
1072
1148
|
|
|
1073
1149
|
export function formatWorkflowPreview(args: { script?: unknown; workflow?: unknown; name?: unknown; description?: unknown }): string {
|
|
1074
1150
|
const name = typeof args.name === "string" && args.name.trim() ? args.name.trim() : typeof args.workflow === "string" && args.workflow.trim() ? args.workflow : "workflow";
|
|
1075
|
-
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" : ""}`;
|
|
1076
1152
|
return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
|
|
1077
1153
|
}
|
|
1078
1154
|
export const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
1079
1155
|
export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow";
|
|
1080
|
-
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.";
|
|
1081
1157
|
export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
|
|
1082
1158
|
name: Type.Optional(Type.String({ description: "Workflow name for inline scripts" })),
|
|
1083
1159
|
description: Type.Optional(Type.String({ description: "Optional human-readable workflow description" })),
|
|
1084
1160
|
script: Type.Optional(Type.String({ description: "Immutable workflow source without metadata" })),
|
|
1085
|
-
workflow: Type.Optional(Type.String({ description: "Registered reusable
|
|
1161
|
+
workflow: Type.Optional(Type.String({ description: "Registered reusable function as an unqualified name" })),
|
|
1086
1162
|
args: Type.Optional(Type.Unknown({ description: "JSON-compatible workflow arguments" })),
|
|
1087
1163
|
foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of running in the background" })),
|
|
1088
1164
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16 })),
|
|
1089
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" })),
|
|
1090
1167
|
});
|
|
1091
1168
|
|
|
1092
1169
|
function hasDynamicAgentRole(node: acorn.AnyNode | undefined): boolean {
|
|
@@ -1107,8 +1184,10 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
|
|
|
1107
1184
|
if (hasIdentifier(program, INTERNAL_AGENT_NAME)) fail("INVALID_METADATA", `${INTERNAL_AGENT_NAME} is reserved for workflow agent instrumentation`);
|
|
1108
1185
|
if (hasIdentifier(program, INTERNAL_CONVERSATION_NAME)) fail("INVALID_METADATA", `${INTERNAL_CONVERSATION_NAME} is reserved for workflow conversation instrumentation`);
|
|
1109
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`);
|
|
1110
1188
|
validateDirectPrimitiveReferences(program, "withWorktree");
|
|
1111
1189
|
validateDirectPrimitiveReferences(program, "conversation");
|
|
1190
|
+
validateDirectPrimitiveReferences(program, "shell");
|
|
1112
1191
|
for (const [index, schema] of schemas.entries()) validateSchema(schema, `schema[${String(index)}]`);
|
|
1113
1192
|
const calls = workflowCalls(program);
|
|
1114
1193
|
const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase): phase is string => phase !== undefined);
|
|
@@ -1119,6 +1198,7 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
|
|
|
1119
1198
|
validateStaticAgentOptions(call.arguments[1], capabilities.modelAliases ?? {}, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath);
|
|
1120
1199
|
}
|
|
1121
1200
|
if (operation === "withWorktree") validateStaticWithWorktree(call);
|
|
1201
|
+
if (operation === "shell") validateStaticShellOptions(call);
|
|
1122
1202
|
if ((operation === "parallel" || operation === "pipeline") && call.arguments.some((argument) => argument.type === "SpreadElement")) continue;
|
|
1123
1203
|
if (operation === "checkpoint" && stableName(call.arguments[0]) === false) fail("INVALID_METADATA", `${operation} requires a stable explicit name`);
|
|
1124
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");
|
|
@@ -1153,6 +1233,7 @@ export interface WorkflowValidationParameters {
|
|
|
1153
1233
|
description?: string;
|
|
1154
1234
|
script?: string;
|
|
1155
1235
|
workflow?: string;
|
|
1236
|
+
args?: unknown;
|
|
1156
1237
|
}
|
|
1157
1238
|
|
|
1158
1239
|
export interface WorkflowValidationContext {
|
|
@@ -1163,6 +1244,7 @@ export interface WorkflowValidationContext {
|
|
|
1163
1244
|
modelAliases?: Readonly<Record<string, string>>;
|
|
1164
1245
|
knownModels?: ReadonlySet<string>;
|
|
1165
1246
|
settingsPath?: string;
|
|
1247
|
+
agentDir?: string;
|
|
1166
1248
|
}
|
|
1167
1249
|
|
|
1168
1250
|
|
|
@@ -1172,21 +1254,27 @@ export interface ValidatedWorkflowLaunch {
|
|
|
1172
1254
|
agentDefinitions: Readonly<Record<string, AgentDefinition>>;
|
|
1173
1255
|
projectAgentDefinitions: Readonly<Record<string, AgentDefinition>>;
|
|
1174
1256
|
roleNames: readonly string[];
|
|
1257
|
+
functionName?: string;
|
|
1175
1258
|
}
|
|
1176
1259
|
|
|
1260
|
+
function functionLaunchScript(name: string): string { return `return await ${name}(args);`; }
|
|
1261
|
+
|
|
1177
1262
|
export function validateWorkflowLaunch(params: WorkflowValidationParameters, context: WorkflowValidationContext): ValidatedWorkflowLaunch {
|
|
1178
1263
|
return validateWorkflowLaunchWithRegistry(params, context, loadingRegistry());
|
|
1179
1264
|
}
|
|
1180
1265
|
function validateWorkflowLaunchWithRegistry(params: WorkflowValidationParameters, context: WorkflowValidationContext, registry: WorkflowRegistryApi): ValidatedWorkflowLaunch {
|
|
1181
1266
|
if (Object.prototype.hasOwnProperty.call(params, "maxAgentLaunches")) fail("INVALID_METADATA", "maxAgentLaunches has been removed; use budget.agentLaunches");
|
|
1182
1267
|
if (params.script !== undefined && params.workflow !== undefined) fail("INVALID_METADATA", "Provide either script or workflow, not both");
|
|
1183
|
-
const
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
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() : "");
|
|
1187
1275
|
if (!workflowName) fail("INVALID_METADATA", "Inline workflows require name");
|
|
1188
|
-
const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } :
|
|
1189
|
-
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);
|
|
1190
1278
|
const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};
|
|
1191
1279
|
const agentDefinitions = deepFreeze({ ...globalAgentDefinitions, ...projectAgentDefinitions });
|
|
1192
1280
|
const aliases = context.modelAliases ?? {};
|
|
@@ -1194,7 +1282,7 @@ function validateWorkflowLaunchWithRegistry(params: WorkflowValidationParameters
|
|
|
1194
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);
|
|
1195
1283
|
const roleNames = checked.dynamicAgentRoles ? Object.keys(agentDefinitions) : checked.referenced.agentTypes;
|
|
1196
1284
|
validateRolePolicies(agentDefinitions, roleNames, context.availableModels, context.rootTools, aliases, knownModels, context.settingsPath);
|
|
1197
|
-
return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames };
|
|
1285
|
+
return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, ...(functionName ? { functionName } : {}) };
|
|
1198
1286
|
}
|
|
1199
1287
|
|
|
1200
1288
|
function deepFreeze<T>(value: T): T {
|
|
@@ -1208,22 +1296,34 @@ function deepFreeze<T>(value: T): T {
|
|
|
1208
1296
|
type LaunchSnapshotInput = Omit<LaunchSnapshot, "identityVersion"> & { identityVersion?: number };
|
|
1209
1297
|
|
|
1210
1298
|
export function createLaunchSnapshot(input: LaunchSnapshotInput): Readonly<LaunchSnapshot> {
|
|
1211
|
-
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 }));
|
|
1212
1300
|
}
|
|
1213
1301
|
|
|
1214
1302
|
export function loadLaunchSnapshot(input: LaunchSnapshot): Readonly<LaunchSnapshot> {
|
|
1215
|
-
if (object(input.settings) && Object.prototype.hasOwnProperty.call(input.settings, "maxAgentLaunches")) fail("RESUME_INCOMPATIBLE", "Persisted workflow settings contain removed maxAgentLaunches");
|
|
1216
1303
|
return deepFreeze(structuredClone(input));
|
|
1217
1304
|
}
|
|
1218
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
|
+
|
|
1219
1316
|
export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
1220
1317
|
export const HEARTBEAT_TIMEOUT_MS = 5000;
|
|
1221
1318
|
|
|
1222
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 }
|
|
1223
1321
|
export interface WorkflowBridge {
|
|
1224
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>;
|
|
1225
1324
|
checkpoint?: (input: Readonly<Record<string, JsonValue>>, signal: AbortSignal) => boolean | Promise<boolean>;
|
|
1226
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>>;
|
|
1227
1327
|
functions?: Readonly<Record<string, { name: string }>>;
|
|
1228
1328
|
variables?: Readonly<Record<string, JsonValue>>;
|
|
1229
1329
|
phase?: (name: string) => void | Promise<void>;
|
|
@@ -1296,10 +1396,12 @@ const named = (value, kind) => { if (typeof value !== "string" || !value.trim())
|
|
|
1296
1396
|
const path = (...names) => names.map(encodeURIComponent).join("/");
|
|
1297
1397
|
const inheritedAgentPath = new AsyncLocalStorage();
|
|
1298
1398
|
const agentOccurrences = new Map();
|
|
1399
|
+
const shellOccurrences = new Map();
|
|
1299
1400
|
const conversationOccurrences = new Map();
|
|
1300
1401
|
const worktreeOwners = new AsyncLocalStorage();
|
|
1301
1402
|
const worktreeOccurrences = new Map();
|
|
1302
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"); };
|
|
1303
1405
|
const rejectWorktree = () => { throw workError("INVALID_METADATA", "withWorktree calls must use a direct withWorktree(...) call; aliases and indirect calls are unsupported"); };
|
|
1304
1406
|
const internalWithWorktree = async (...values) => {
|
|
1305
1407
|
const callSite = values.pop();
|
|
@@ -1318,7 +1420,9 @@ const internalWithWorktree = async (...values) => {
|
|
|
1318
1420
|
worktreeOccurrences.set(occurrenceKey, occurrence);
|
|
1319
1421
|
owner = path("worktree", "unnamed", ...inherited, "callsite:" + callSite, "occurrence:" + String(occurrence));
|
|
1320
1422
|
}
|
|
1321
|
-
|
|
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 })));
|
|
1322
1426
|
};
|
|
1323
1427
|
const internalConversation = (...values) => {
|
|
1324
1428
|
const callSite = values.pop();
|
|
@@ -1377,6 +1481,31 @@ const internalAgent = (...values) => {
|
|
|
1377
1481
|
});
|
|
1378
1482
|
return result;
|
|
1379
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;
|
|
1380
1509
|
const agent = rejectAgent;
|
|
1381
1510
|
const promptPath = (at, key) => /^[A-Za-z_$][\w$]*$/.test(key) ? at + "." + key : at + "[" + JSON.stringify(key) + "]";
|
|
1382
1511
|
const plainPromptObject = value => {
|
|
@@ -1498,13 +1627,13 @@ const pipeline = async (operationName, items, stages) => {
|
|
|
1498
1627
|
return Object.fromEntries(results.map(result => [result.name, result.value]));
|
|
1499
1628
|
};
|
|
1500
1629
|
const safeMath = Object.fromEntries(Object.getOwnPropertyNames(Math).filter(name => name !== "random").map(name => [name, Math[name]]));
|
|
1501
|
-
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) };
|
|
1502
1631
|
for (const [name, fn] of Object.entries(functions)) Object.defineProperty(sandbox, name, { value: fn, writable: false, configurable: false });
|
|
1503
1632
|
for (const [name, value] of Object.entries(config.variables || {})) Object.defineProperty(sandbox, name, { value: freeze(value), writable: false, configurable: false });
|
|
1504
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;
|
|
1505
1634
|
const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
|
|
1506
1635
|
const body = config.script;
|
|
1507
|
-
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))
|
|
1508
1637
|
.then(async value => { await Promise.all(inflight); send({ type: "result", value: value === undefined ? null : value }); })
|
|
1509
1638
|
.catch(error => send({ type: "error", error: workerError(error) }))
|
|
1510
1639
|
.finally(() => clearInterval(heartbeat));
|
|
@@ -1517,6 +1646,10 @@ function encoded(value: unknown): string {
|
|
|
1517
1646
|
return json;
|
|
1518
1647
|
}
|
|
1519
1648
|
|
|
1649
|
+
function encodedRpcResult(id: number, value: JsonValue): string {
|
|
1650
|
+
return encoded({ type: "rpcResult", id, ok: true, value });
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1520
1653
|
function readAgentIdentity(value: unknown): AgentIdentity {
|
|
1521
1654
|
if (!object(value)) fail("INTERNAL_ERROR", "Invalid workflow agent identity");
|
|
1522
1655
|
const structuralPath = value.structuralPath;
|
|
@@ -1529,10 +1662,17 @@ function readAgentIdentity(value: unknown): AgentIdentity {
|
|
|
1529
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");
|
|
1530
1663
|
return { structuralPath: [...structuralPath], callSite, occurrence, ...(typeof parentBreadcrumb === "string" ? { parentBreadcrumb } : {}), ...(typeof worktreeOwner === "string" ? { worktreeOwner } : {}), ...(parsedConversation ? { conversation: parsedConversation } : {}) };
|
|
1531
1664
|
}
|
|
1532
|
-
|
|
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
|
+
}
|
|
1533
1670
|
function agentIdentityPath(identity: AgentIdentity): string {
|
|
1534
1671
|
return operationPath("agent", ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
|
|
1535
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
|
+
}
|
|
1536
1676
|
function conversationIdentityPath(identity: AgentIdentity): string {
|
|
1537
1677
|
if (!identity.conversation) throw new WorkflowError("INTERNAL_ERROR", "Missing conversation identity");
|
|
1538
1678
|
return operationPath("conversation", identity.conversation.name, ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
|
|
@@ -1541,9 +1681,82 @@ function conversationTurnPath(identity: AgentIdentity): string {
|
|
|
1541
1681
|
if (!identity.conversation) throw new WorkflowError("INTERNAL_ERROR", "Missing conversation identity");
|
|
1542
1682
|
return operationPath(conversationIdentityPath(identity), `turn:${String(identity.conversation.turn)}`);
|
|
1543
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
|
+
}
|
|
1544
1688
|
function agentWorktree(identity: AgentIdentity): { worktreeOwner?: string } {
|
|
1545
1689
|
return identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {};
|
|
1546
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
|
+
}
|
|
1547
1760
|
export function runWorkflow(script: string, args: JsonValue = null, bridge: WorkflowBridge = {}, signal?: AbortSignal): WorkflowExecution {
|
|
1548
1761
|
encoded(args);
|
|
1549
1762
|
const config = JSON.stringify({ script: instrumentWorkflow(script), args: structuredClone(args), functions: bridge.functions ?? {}, variables: bridge.variables ?? {} });
|
|
@@ -1613,6 +1826,12 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
1613
1826
|
if (!OUTCOME_ERRORS.has(typed.code)) throw typed;
|
|
1614
1827
|
value = branded({ name: label, ok: false, failedAt: path, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } });
|
|
1615
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;
|
|
1616
1835
|
} else if (method === "checkpoint") {
|
|
1617
1836
|
if (!bridge.checkpoint || !object(values[0])) fail("INTERNAL_ERROR", "checkpoint requires an available bridge and object input");
|
|
1618
1837
|
const name = typeof values[0].name === "string" ? values[0].name : "checkpoint";
|
|
@@ -1639,6 +1858,9 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
1639
1858
|
if (!OUTCOME_ERRORS.has(typed.code)) throw typed;
|
|
1640
1859
|
value = branded({ name, ok: false, failedAt: name, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } });
|
|
1641
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);
|
|
1642
1864
|
} else if (method === "phase") {
|
|
1643
1865
|
if (typeof values[0] !== "string") fail("INTERNAL_ERROR", "phase name must be a string");
|
|
1644
1866
|
await bridge.phase?.(values[0]);
|
|
@@ -1648,7 +1870,7 @@ export function runWorkflow(script: string, args: JsonValue = null, bridge: Work
|
|
|
1648
1870
|
}
|
|
1649
1871
|
else fail("INTERNAL_ERROR", `Unknown worker RPC method: ${method}`);
|
|
1650
1872
|
encoded(value);
|
|
1651
|
-
child.send(
|
|
1873
|
+
child.send(encodedRpcResult(id, value));
|
|
1652
1874
|
} catch (error) {
|
|
1653
1875
|
const typed = asWorkflowError(error);
|
|
1654
1876
|
child.send(encoded({ type: "rpcResult", id, ok: false, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } }));
|
|
@@ -1845,13 +2067,9 @@ export function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonl
|
|
|
1845
2067
|
const render = ({ agent, depth }: { agent: AgentRecord; index: number; depth: number }, grouped: boolean) => {
|
|
1846
2068
|
const icon = agent.state === "completed" ? "✓" : agent.state === "failed" || agent.state === "cancelled" ? "✗" : agent.state === "running" ? "⠦" : "○";
|
|
1847
2069
|
const breadcrumb = grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId);
|
|
1848
|
-
const setup = agent.attemptDetails?.at(-1)?.setup;
|
|
1849
|
-
const thinking = setup?.model.thinking ?? agent.model.thinking;
|
|
1850
|
-
const model = `${setup?.model.provider ?? agent.model.provider}/${setup?.model.model ?? agent.model.model}${thinking ? `:${thinking}` : ""}`;
|
|
1851
|
-
const policy = [`model=${model}`, agent.requestedModel ? `requested=${agent.requestedModel}` : "", `tools=${(setup?.tools ?? agent.tools).join(",") || "(none)"}`, agent.role ? `role=${agent.role}` : ""];
|
|
1852
2070
|
const tokens = agent.accounting ? formatAccounting(agent.accounting) : "";
|
|
1853
2071
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
1854
|
-
const result = [`${indent}${icon} ${breadcrumb} · ${agent.state}
|
|
2072
|
+
const result = [`${indent}${icon} ${breadcrumb} · ${agent.state}${tokens ? ` · ${tokens}` : ""}`];
|
|
1855
2073
|
if (agent.state === "failed" && agent.attemptDetails?.length) {
|
|
1856
2074
|
const last = agent.attemptDetails[agent.attemptDetails.length - 1];
|
|
1857
2075
|
if (last?.error) result.push(`${indent} error: ${last.error.code}: ${last.error.message}`);
|
|
@@ -1902,7 +2120,8 @@ export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readon
|
|
|
1902
2120
|
return lines.join("\n");
|
|
1903
2121
|
}
|
|
1904
2122
|
function formatCheckpointReview(checkpoint: AwaitingCheckpoint): string {
|
|
1905
|
-
|
|
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");
|
|
1906
2125
|
}
|
|
1907
2126
|
|
|
1908
2127
|
const DELIVERY_LIMIT_BYTES = 4 * 1024;
|
|
@@ -1925,12 +2144,109 @@ function utf8Prefix(value: string, maxBytes: number): string {
|
|
|
1925
2144
|
while (end < bytes.length && end > 0 && ((bytes[end] ?? 0) & 0xc0) === 0x80) end -= 1;
|
|
1926
2145
|
return bytes.subarray(0, end).toString("utf8");
|
|
1927
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
|
+
}
|
|
1928
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
|
+
}
|
|
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
|
+
}
|
|
1929
2245
|
function deliver(pi: ExtensionAPI, content: string): void {
|
|
1930
2246
|
pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
1931
2247
|
}
|
|
1932
|
-
function deliverFailure(pi: ExtensionAPI,
|
|
1933
|
-
deliver(pi, `Workflow ${
|
|
2248
|
+
function deliverFailure(pi: ExtensionAPI, diagnostic: WorkflowFailureDiagnostics): void {
|
|
2249
|
+
deliver(pi, `Workflow ${utf8Prefix(diagnostic.workflowName, 128)} failure diagnostics: ${serializeWorkflowFailureDiagnostics(diagnostic)}`);
|
|
1934
2250
|
}
|
|
1935
2251
|
|
|
1936
2252
|
type WorkflowEventSink = { emit: (name: string, payload: unknown) => unknown };
|
|
@@ -1947,19 +2263,19 @@ class WorkflowEventPublisher {
|
|
|
1947
2263
|
|
|
1948
2264
|
constructor(private readonly sink: WorkflowEventSink | undefined) {}
|
|
1949
2265
|
|
|
2266
|
+
removeRun(runId: string): void {
|
|
2267
|
+
this.#queues.delete(runId);
|
|
2268
|
+
this.#budgetEvents.delete(runId);
|
|
2269
|
+
this.#worktrees.delete(runId);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
1950
2272
|
seedBudget(runId: string, events: readonly BudgetEvent[] | undefined): void {
|
|
1951
2273
|
const seen = this.#budgetEvents.get(runId) ?? new Set<string>();
|
|
1952
2274
|
for (const event of events ?? []) seen.add(this.budgetKey(event));
|
|
1953
2275
|
this.#budgetEvents.set(runId, seen);
|
|
1954
2276
|
}
|
|
1955
2277
|
|
|
1956
|
-
async runStarted(store: RunStore, metadata: WorkflowMetadata
|
|
1957
|
-
await this.#publish(store, metadata, WORKFLOW_RUN_STARTED_EVENT, {});
|
|
1958
|
-
if (background) await this.legacyStarted(store, metadata);
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
async legacyStarted(store: RunStore, metadata: WorkflowMetadata): Promise<void> { await this.#publish(store, metadata, WORKFLOW_ASYNC_STARTED_EVENT, { id: store.runId, runId: store.runId, pid: process.pid, sessionId: store.sessionId, asyncDir: store.directory, agent: metadata.name }); }
|
|
1962
|
-
|
|
2278
|
+
async runStarted(store: RunStore, metadata: WorkflowMetadata): Promise<void> { await this.#publish(store, metadata, WORKFLOW_RUN_STARTED_EVENT, {}); }
|
|
1963
2279
|
async runResumed(store: RunStore, metadata: WorkflowMetadata): Promise<void> { await this.#publish(store, metadata, WORKFLOW_RUN_RESUMED_EVENT, {}); }
|
|
1964
2280
|
|
|
1965
2281
|
async runState(store: RunStore, metadata: WorkflowMetadata, previousState: RunState, state: RunState, reason?: string): Promise<void> {
|
|
@@ -1967,16 +2283,9 @@ class WorkflowEventPublisher {
|
|
|
1967
2283
|
if ((previousState === "paused" || previousState === "interrupted" || previousState === "budget_exhausted") && state === "running") await this.runResumed(store, metadata);
|
|
1968
2284
|
}
|
|
1969
2285
|
|
|
1970
|
-
async runCompleted(store: RunStore, metadata: WorkflowMetadata, resultPath: string
|
|
1971
|
-
|
|
1972
|
-
if (background) await this.#publish(store, metadata, WORKFLOW_ASYNC_COMPLETE_EVENT, { id: store.runId, runId: store.runId, sessionId: store.sessionId, asyncDir: store.directory, success: true, state: "complete" });
|
|
1973
|
-
}
|
|
1974
|
-
async runFailed(store: RunStore, metadata: WorkflowMetadata, error: unknown, background: boolean, state: "failed" | "stopped" | "interrupted" | "budget_exhausted"): Promise<void> {
|
|
2286
|
+
async runCompleted(store: RunStore, metadata: WorkflowMetadata, resultPath: string): Promise<void> { await this.#publish(store, metadata, WORKFLOW_RUN_COMPLETED_EVENT, { resultPath }); }
|
|
2287
|
+
async runFailed(store: RunStore, metadata: WorkflowMetadata, error: unknown, state: "failed" | "stopped" | "interrupted" | "budget_exhausted"): Promise<void> {
|
|
1975
2288
|
if (state === "failed") await this.#publish(store, metadata, WORKFLOW_RUN_FAILED_EVENT, { error: safeEventError(error) });
|
|
1976
|
-
if (background) {
|
|
1977
|
-
const legacyState = state === "interrupted" ? "failed" : state;
|
|
1978
|
-
await this.#publish(store, metadata, WORKFLOW_ASYNC_COMPLETE_EVENT, { id: store.runId, runId: store.runId, sessionId: store.sessionId, asyncDir: store.directory, success: false, state: legacyState, ...(state === "stopped" ? { stopped: true } : {}), ...(legacyState === "budget_exhausted" || legacyState === "failed" ? { error: safeEventError(error).message } : {}) });
|
|
1979
|
-
}
|
|
1980
2289
|
}
|
|
1981
2290
|
|
|
1982
2291
|
async agentState(store: RunStore, metadata: WorkflowMetadata, previous: AgentRecord | undefined, agent: AgentRecord): Promise<void> {
|
|
@@ -2037,7 +2346,11 @@ function namedRecord(value: unknown, kind: string): Array<[string, unknown]> {
|
|
|
2037
2346
|
if (!object(value)) fail("INVALID_METADATA", `${kind} must be a record`);
|
|
2038
2347
|
return Object.entries(value);
|
|
2039
2348
|
}
|
|
2040
|
-
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> {
|
|
2041
2354
|
if (args.length !== 1 && args.length !== 2) fail("INVALID_METADATA", "withWorktree requires a callback or a name and callback");
|
|
2042
2355
|
const callback = args[args.length - 1];
|
|
2043
2356
|
if (typeof callback !== "function") fail("INVALID_METADATA", "withWorktree callback must be a function");
|
|
@@ -2052,7 +2365,9 @@ function hostWithWorktree(args: readonly unknown[], identity: string, occurrence
|
|
|
2052
2365
|
occurrences.set(key, occurrence);
|
|
2053
2366
|
owner = operationPath("worktree", "unnamed", "function", identity, ...structuralPath, `occurrence:${String(occurrence)}`);
|
|
2054
2367
|
}
|
|
2055
|
-
|
|
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>;
|
|
2056
2371
|
}
|
|
2057
2372
|
function workflowRunContext(cwd: string, sessionId: string, runId: string, workflow: WorkflowMetadata, args: JsonValue, signal: AbortSignal): Readonly<WorkflowRunContext> {
|
|
2058
2373
|
return Object.freeze({ cwd, sessionId, runId, workflow: deepFreeze(structuredClone(workflow)), args: deepFreeze(structuredClone(args)), signal });
|
|
@@ -2133,16 +2448,27 @@ function nextNamedOccurrence(counters: Map<string, number>, label: string): stri
|
|
|
2133
2448
|
return count === 1 ? label : `${label}#${String(count)}`;
|
|
2134
2449
|
}
|
|
2135
2450
|
|
|
2136
|
-
|
|
2137
2451
|
function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, runContext: Readonly<WorkflowRunContext>, variables: Readonly<Record<string, JsonValue>>, registry: WorkflowRegistryApi): WorkflowBridge {
|
|
2138
2452
|
const functionAgentOccurrences = new Map<string, number>();
|
|
2453
|
+
const functionShellOccurrences = new Map<string, number>();
|
|
2139
2454
|
const functionWorktreeOccurrences = new Map<string, number>();
|
|
2140
|
-
|
|
2455
|
+
const functionInvokeOccurrences = new Map<string, number>();
|
|
2456
|
+
const invokeFunction = async (name: string, input: Readonly<Record<string, JsonValue>>, path: string, signal: AbortSignal, worktreeOwner?: string, structuralPath: readonly string[] = [], breadcrumb?: string): Promise<JsonValue> => {
|
|
2141
2457
|
const replayed = await store.replay(path);
|
|
2142
2458
|
let stored: JsonValue | undefined;
|
|
2143
2459
|
const sideEffects: Promise<void>[] = [];
|
|
2460
|
+
const functionBreadcrumb = breadcrumb ?? name;
|
|
2144
2461
|
const context: WorkflowFunctionContext = {
|
|
2145
2462
|
run: runContext,
|
|
2463
|
+
invoke: async (targetName, targetInput) => {
|
|
2464
|
+
const inherited = inheritedHostAgentPath.getStore() ?? structuralPath;
|
|
2465
|
+
const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
|
|
2466
|
+
const key = JSON.stringify([path, inherited, targetName]);
|
|
2467
|
+
const occurrence = (functionInvokeOccurrences.get(key) ?? 0) + 1;
|
|
2468
|
+
functionInvokeOccurrences.set(key, occurrence);
|
|
2469
|
+
const nestedPath = operationPath("function", "nested", path, ...inherited, targetName, `occurrence:${String(occurrence)}`);
|
|
2470
|
+
return invokeFunction(targetName, targetInput, nestedPath, signal, scopedWorktreeOwner, inherited, `${functionBreadcrumb} > ${targetName}`);
|
|
2471
|
+
},
|
|
2146
2472
|
agent: async (...args: readonly unknown[]) => {
|
|
2147
2473
|
if (!bridge.agent || typeof args[0] !== "string") fail("AGENT_FAILED", "No agent bridge is available");
|
|
2148
2474
|
const options = validateAgentOptions(args[1] === undefined ? {} : args[1]);
|
|
@@ -2151,12 +2477,23 @@ function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, runConte
|
|
|
2151
2477
|
const key = `${path}\0${JSON.stringify(inherited)}`;
|
|
2152
2478
|
const occurrence = (functionAgentOccurrences.get(key) ?? 0) + 1;
|
|
2153
2479
|
functionAgentOccurrences.set(key, occurrence);
|
|
2154
|
-
return bridge.agent(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, parentBreadcrumb:
|
|
2480
|
+
return bridge.agent(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, parentBreadcrumb: functionBreadcrumb, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
|
|
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 } : {}) });
|
|
2155
2492
|
},
|
|
2156
2493
|
prompt: workflowPrompt,
|
|
2157
2494
|
parallel: (...args: readonly unknown[]) => hostParallel(args[0], args[1]),
|
|
2158
2495
|
pipeline: (...args: readonly unknown[]) => hostPipeline(args[0], args[1], args[2]),
|
|
2159
|
-
withWorktree: (...args: readonly unknown[]) => hostWithWorktree(args, path, functionWorktreeOccurrences),
|
|
2496
|
+
withWorktree: (...args: readonly unknown[]) => hostWithWorktree(args, path, functionWorktreeOccurrences, bridge.worktree, signal),
|
|
2160
2497
|
checkpoint: async (...args: readonly unknown[]) => {
|
|
2161
2498
|
if (!bridge.checkpoint || !object(args[0]) || !jsonValue(args[0])) fail("INTERNAL_ERROR", "No checkpoint bridge is available");
|
|
2162
2499
|
return bridge.checkpoint(args[0], signal);
|
|
@@ -2168,14 +2505,73 @@ function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, runConte
|
|
|
2168
2505
|
await Promise.all(sideEffects);
|
|
2169
2506
|
if (!replayed) await store.complete(path, stored ?? result);
|
|
2170
2507
|
return result;
|
|
2171
|
-
}
|
|
2508
|
+
};
|
|
2509
|
+
return { ...bridge, functions: registry.globals(), variables, function: invokeFunction };
|
|
2172
2510
|
}
|
|
2173
2511
|
|
|
2174
2512
|
function projectTrusted(ctx: unknown): boolean {
|
|
2175
2513
|
const check = object(ctx) ? ctx.isProjectTrusted : undefined;
|
|
2176
2514
|
return typeof check === "function" ? Boolean(Reflect.apply(check, ctx, [])) : true;
|
|
2177
2515
|
}
|
|
2178
|
-
|
|
2516
|
+
type PiHostCapabilities = { registerEntryRenderer?: ExtensionAPI["registerEntryRenderer"]; events?: WorkflowEventSink };
|
|
2517
|
+
function isEntryRenderer(value: unknown): value is NonNullable<PiHostCapabilities["registerEntryRenderer"]> { return typeof value === "function"; }
|
|
2518
|
+
function isWorkflowEventSink(value: unknown): value is WorkflowEventSink { return object(value) && typeof value.emit === "function"; }
|
|
2519
|
+
function piHostCapabilities(pi: unknown): PiHostCapabilities {
|
|
2520
|
+
if (!object(pi)) return {};
|
|
2521
|
+
const registerEntryRenderer = pi.registerEntryRenderer;
|
|
2522
|
+
const events = pi.events;
|
|
2523
|
+
return { ...(isEntryRenderer(registerEntryRenderer) ? { registerEntryRenderer } : {}), ...(isWorkflowEventSink(events) ? { events } : {}) };
|
|
2524
|
+
}
|
|
2525
|
+
type ContextHostCapabilities = { modelRegistry?: ModelRegistryCapability };
|
|
2526
|
+
type ModelSummary = { provider: string; id: string };
|
|
2527
|
+
type ModelRegistryGetter = () => readonly ModelSummary[];
|
|
2528
|
+
type ModelRegistryCapability = { getAll?: ModelRegistryGetter; getAvailable?: ModelRegistryGetter };
|
|
2529
|
+
function isModelRegistryGetter(value: unknown): value is ModelRegistryGetter { return typeof value === "function"; }
|
|
2530
|
+
function contextHostCapabilities(ctx: unknown): ContextHostCapabilities {
|
|
2531
|
+
if (!object(ctx) || !object(ctx.modelRegistry)) return {};
|
|
2532
|
+
const registry = ctx.modelRegistry;
|
|
2533
|
+
const getAll = registry.getAll;
|
|
2534
|
+
const getAvailable = registry.getAvailable;
|
|
2535
|
+
return { modelRegistry: { ...(isModelRegistryGetter(getAll) ? { getAll: () => getAll.call(registry) } : {}), ...(isModelRegistryGetter(getAvailable) ? { getAvailable: () => getAvailable.call(registry) } : {}) } };
|
|
2536
|
+
}
|
|
2537
|
+
type UiSelect = (title: string, options: string[]) => Promise<string | undefined>;
|
|
2538
|
+
type UiInput = (title: string, placeholder?: string) => Promise<string | undefined>;
|
|
2539
|
+
type UiSetStatus = (key: string, text?: string) => void;
|
|
2540
|
+
type UiHostCapabilities = { select?: UiSelect; input?: UiInput; setStatus?: UiSetStatus };
|
|
2541
|
+
function isUiSelect(value: unknown): value is UiSelect { return typeof value === "function"; }
|
|
2542
|
+
function isUiInput(value: unknown): value is UiInput { return typeof value === "function"; }
|
|
2543
|
+
function isUiSetStatus(value: unknown): value is UiSetStatus { return typeof value === "function"; }
|
|
2544
|
+
function uiHostCapabilities(ui: unknown): UiHostCapabilities | undefined {
|
|
2545
|
+
if (!object(ui)) return undefined;
|
|
2546
|
+
const select = ui.select;
|
|
2547
|
+
const input = ui.input;
|
|
2548
|
+
const setStatus = ui.setStatus;
|
|
2549
|
+
return { ...(isUiSelect(select) ? { select } : {}), ...(isUiInput(input) ? { input } : {}), ...(isUiSetStatus(setStatus) ? { setStatus } : {}) };
|
|
2550
|
+
}
|
|
2551
|
+
type TuiHostCapabilities = { terminal?: { rows?: unknown } };
|
|
2552
|
+
function tuiHostCapabilities(tui: unknown): TuiHostCapabilities {
|
|
2553
|
+
if (!object(tui) || !object(tui.terminal)) return {};
|
|
2554
|
+
return { terminal: { ...(tui.terminal.rows === undefined ? {} : { rows: tui.terminal.rows }) } };
|
|
2555
|
+
}
|
|
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
|
+
}
|
|
2568
|
+
type KeybindingsHostCapabilities = { getKeys?: (name: string) => readonly string[] };
|
|
2569
|
+
function isKeybindingGetter(value: unknown): value is NonNullable<KeybindingsHostCapabilities["getKeys"]> { return typeof value === "function"; }
|
|
2570
|
+
function keybindingsHostCapabilities(keybindings: unknown): KeybindingsHostCapabilities {
|
|
2571
|
+
if (!object(keybindings) || !isKeybindingGetter(keybindings.getKeys)) return {};
|
|
2572
|
+
return { getKeys: keybindings.getKeys };
|
|
2573
|
+
}
|
|
2574
|
+
function keybindingKeys(keybindings: unknown, name: string): readonly string[] | undefined { const getKeys = keybindingsHostCapabilities(keybindings).getKeys; return typeof getKeys === "function" ? getKeys.call(keybindings, name) : undefined; }
|
|
2179
2575
|
function parseThinking(value: unknown): ModelSpec["thinking"] | undefined {
|
|
2180
2576
|
switch (value) {
|
|
2181
2577
|
case "off": case "minimal": case "low": case "medium": case "high": case "xhigh": case "max": return value;
|
|
@@ -2183,10 +2579,11 @@ function parseThinking(value: unknown): ModelSpec["thinking"] | undefined {
|
|
|
2183
2579
|
}
|
|
2184
2580
|
}
|
|
2185
2581
|
|
|
2186
|
-
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) {
|
|
2187
2583
|
beginWorkflowExtensionLoading();
|
|
2188
2584
|
const registry = loadingRegistry();
|
|
2189
|
-
const
|
|
2585
|
+
const extensionAgentDir = agentDir ?? getAgentDir();
|
|
2586
|
+
const registerEntryRenderer = piHostCapabilities(pi).registerEntryRenderer;
|
|
2190
2587
|
registerEntryRenderer?.<WorkflowLogEntry>(WORKFLOW_LOG_ENTRY, (entry) => {
|
|
2191
2588
|
const data = entry.data;
|
|
2192
2589
|
return textBlock(data ? `Workflow ${data.workflowName}: ${data.message}` : "");
|
|
@@ -2196,7 +2593,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2196
2593
|
try { pi.appendEntry<WorkflowLogEntry>(WORKFLOW_LOG_ENTRY, { workflowName, message: utf8Prefix(message, DELIVERY_LIMIT_BYTES) }); }
|
|
2197
2594
|
finally { await lifecycle.leave(); }
|
|
2198
2595
|
};
|
|
2199
|
-
const eventPublisher = new WorkflowEventPublisher((pi
|
|
2596
|
+
const eventPublisher = new WorkflowEventPublisher(piHostCapabilities(pi).events);
|
|
2200
2597
|
pi.on("resources_discover", () => {
|
|
2201
2598
|
if (!pi.getActiveTools().includes("workflow")) return;
|
|
2202
2599
|
const extensionDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -2204,7 +2601,54 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2204
2601
|
return skillPath ? { skillPaths: [skillPath] } : undefined;
|
|
2205
2602
|
});
|
|
2206
2603
|
type BudgetDecisionResult = { state: "running" | "budget_exhausted"; approved: boolean };
|
|
2207
|
-
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
|
+
});
|
|
2634
|
+
const liveActivities = new Map<string, Map<string, AgentActivity>>();
|
|
2635
|
+
const setLiveActivity = (runId: string, agentId: string, activity?: AgentActivity) => {
|
|
2636
|
+
const activities = liveActivities.get(runId);
|
|
2637
|
+
if (activity) {
|
|
2638
|
+
if (activities) activities.set(agentId, activity);
|
|
2639
|
+
else liveActivities.set(runId, new Map([[agentId, activity]]));
|
|
2640
|
+
} else {
|
|
2641
|
+
activities?.delete(agentId);
|
|
2642
|
+
if (activities?.size === 0) liveActivities.delete(runId);
|
|
2643
|
+
}
|
|
2644
|
+
};
|
|
2645
|
+
const withLiveActivities = (run: PersistedRun): PersistedRun => {
|
|
2646
|
+
const activities = liveActivities.get(run.id);
|
|
2647
|
+
return activities?.size ? { ...run, agents: run.agents.map((agent) => {
|
|
2648
|
+
const activity = activities.get(agent.id);
|
|
2649
|
+
return activity ? { ...agent, activity } : agent;
|
|
2650
|
+
}) } : run;
|
|
2651
|
+
};
|
|
2208
2652
|
const conversationLocks = new Set<string>();
|
|
2209
2653
|
const terminalRunStates = new Map<string, "completed" | "failed" | "stopped">();
|
|
2210
2654
|
let sessionLease: SessionLease | undefined;
|
|
@@ -2229,9 +2673,30 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2229
2673
|
const persistWorktree = async (store: RunStore, metadata: WorkflowMetadata, owner: string): Promise<WorktreeReference> => {
|
|
2230
2674
|
const existing = (await store.worktrees()).some((worktree) => worktree.owner === owner);
|
|
2231
2675
|
const worktree = await store.worktree(owner);
|
|
2232
|
-
if (!existing) await eventPublisher.worktree(store, metadata, worktree);
|
|
2676
|
+
if (!existing && await store.ownsWorktree(owner)) await eventPublisher.worktree(store, metadata, worktree);
|
|
2233
2677
|
return worktree;
|
|
2234
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
|
+
};
|
|
2235
2700
|
const lifecycleFor = (store: RunStore, state: RunState, budget: WorkflowBudgetRuntime, metadata: WorkflowMetadata) => new RunLifecycle(state, async (next, previous, reason) => {
|
|
2236
2701
|
if (next !== "pausing") budget.transition(next);
|
|
2237
2702
|
const persisted = await persistRunState(store, metadata, (current) => {
|
|
@@ -2240,7 +2705,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2240
2705
|
return nextRun;
|
|
2241
2706
|
});
|
|
2242
2707
|
await eventPublisher.runState(store, metadata, previous, next, reason);
|
|
2243
|
-
runs.get(store.runId)?.update?.(workflowToolUpdate(persisted));
|
|
2708
|
+
runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2244
2709
|
});
|
|
2245
2710
|
const scheduler = new FairAgentScheduler(async ({ id, runId, parentId, prompt, options, signal, setSteer }) => {
|
|
2246
2711
|
const run = runs.get(runId);
|
|
@@ -2257,7 +2722,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2257
2722
|
runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity } : agent) };
|
|
2258
2723
|
}
|
|
2259
2724
|
if (!runState.agents.some((agent) => agent.id === id)) return;
|
|
2260
|
-
|
|
2725
|
+
setLiveActivity(runId, id, progress.activity);
|
|
2726
|
+
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
2261
2727
|
};
|
|
2262
2728
|
const onAttempt = async (attempt: Pick<AgentAttempt, "attempt" | "sessionId" | "sessionFile" | "setup">) => {
|
|
2263
2729
|
await scheduler.flush();
|
|
@@ -2268,15 +2734,16 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2268
2734
|
const active = (await run.store.load()).run;
|
|
2269
2735
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, active.agents);
|
|
2270
2736
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
2271
|
-
run.update?.(workflowToolUpdate(persisted));
|
|
2737
|
+
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2272
2738
|
};
|
|
2273
|
-
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); });
|
|
2274
2740
|
const before = (await run.store.load()).run;
|
|
2275
2741
|
await persistAgentAttempts(run.store, id, result.attempts);
|
|
2276
2742
|
const completed = (await run.store.load()).run;
|
|
2277
2743
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, completed.agents);
|
|
2278
2744
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
2279
|
-
|
|
2745
|
+
setLiveActivity(runId, id);
|
|
2746
|
+
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2280
2747
|
return result.value;
|
|
2281
2748
|
} catch (error) {
|
|
2282
2749
|
const attempts = (error as WorkflowError & { attempts?: readonly AgentAttempt[] }).attempts;
|
|
@@ -2287,7 +2754,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2287
2754
|
await eventPublisher.agentStates(run.store, run.metadata, before.agents, failed.agents);
|
|
2288
2755
|
}
|
|
2289
2756
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
|
|
2290
|
-
|
|
2757
|
+
setLiveActivity(runId, id);
|
|
2758
|
+
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
2291
2759
|
throw error;
|
|
2292
2760
|
}
|
|
2293
2761
|
}, 16, async (runId, ownership) => {
|
|
@@ -2309,8 +2777,21 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2309
2777
|
return { ...current, agents };
|
|
2310
2778
|
});
|
|
2311
2779
|
await eventPublisher.agentStates(run.store, run.metadata, previousAgents, runState.agents);
|
|
2312
|
-
run.update?.(workflowToolUpdate(runState));
|
|
2780
|
+
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
2313
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
|
+
};
|
|
2314
2795
|
type WorkflowStopResult = { runId: string; state: RunState | "unknown"; stopped: boolean; reason?: "unknown_run" | "already_terminal" };
|
|
2315
2796
|
const stopWorkflowRun = async (runId: string): Promise<WorkflowStopResult> => {
|
|
2316
2797
|
const run = runs.get(runId);
|
|
@@ -2323,6 +2804,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2323
2804
|
run.execution?.cancel();
|
|
2324
2805
|
await scheduler.cancelRun(run.store.runId);
|
|
2325
2806
|
await scheduler.flush();
|
|
2807
|
+
await cleanupTerminalRun(runId);
|
|
2326
2808
|
return { runId, state: "stopped", stopped: true };
|
|
2327
2809
|
};
|
|
2328
2810
|
const answerCheckpoint = async (runId: string, name: string, approved: boolean, silent = false) => {
|
|
@@ -2349,17 +2831,16 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2349
2831
|
if (!request) return undefined;
|
|
2350
2832
|
await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
|
|
2351
2833
|
const result = await applyBudgetDecision(request, approved);
|
|
2352
|
-
run.budgetResolvers.get(proposalId)?.(result);
|
|
2353
|
-
run.budgetResolvers.delete(proposalId);
|
|
2354
2834
|
if (!silent) deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
|
|
2355
2835
|
return result;
|
|
2356
2836
|
};
|
|
2357
|
-
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) => {
|
|
2358
2838
|
const checkpointCounters = new Map<string, number>();
|
|
2359
2839
|
return async (raw: Readonly<Record<string, JsonValue>>, signal: AbortSignal): Promise<boolean> => {
|
|
2360
2840
|
const input = validateCheckpoint(raw);
|
|
2361
2841
|
const label = nextNamedOccurrence(checkpointCounters, input.name);
|
|
2362
2842
|
const path = operationPath("checkpoint", label);
|
|
2843
|
+
if (headless) fail("RESUME_INCOMPATIBLE", "Headless CLI checkpoints are unsupported");
|
|
2363
2844
|
if (foreground && !ui?.select) fail("RESUME_INCOMPATIBLE", "Foreground checkpoints require UI");
|
|
2364
2845
|
const alreadyAwaiting = (await store.awaitingCheckpoints()).some((checkpoint) => checkpoint.path === path);
|
|
2365
2846
|
const replayed = await store.awaitCheckpoint({ ...input, name: label, path });
|
|
@@ -2434,13 +2915,16 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2434
2915
|
if (catalogRegistered || !pi.getActiveTools().includes("workflow")) return;
|
|
2435
2916
|
const catalog = registry.catalog();
|
|
2436
2917
|
const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0;
|
|
2437
|
-
if (!catalog.functions.length && !catalog.variables.length && !
|
|
2918
|
+
if (!catalog.functions.length && !catalog.variables.length && !hasAliases) return;
|
|
2438
2919
|
pi.registerTool({
|
|
2439
2920
|
name: "workflow_catalog",
|
|
2440
2921
|
label: "Workflow Catalog",
|
|
2441
|
-
description: "List
|
|
2442
|
-
parameters: Type.Object({}, { additionalProperties: false }),
|
|
2443
|
-
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
|
+
}
|
|
2444
2928
|
});
|
|
2445
2929
|
catalogRegistered = true;
|
|
2446
2930
|
};
|
|
@@ -2449,7 +2933,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2449
2933
|
const active = new Set(pi.getActiveTools().filter((tool) => tool !== "workflow" && tool !== "workflow_respond" && tool !== "workflow_stop" && tool !== "workflow_catalog"));
|
|
2450
2934
|
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
2451
2935
|
if (missing) throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
2452
|
-
const settingsPath = workflowSettingsPath();
|
|
2936
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2453
2937
|
const currentSettings = loadSettings(settingsPath);
|
|
2454
2938
|
resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath);
|
|
2455
2939
|
const currentAliases = currentSettings.modelAliases ?? {};
|
|
@@ -2462,13 +2946,14 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2462
2946
|
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2463
2947
|
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
|
|
2464
2948
|
await run.store.saveSnapshot(snapshot);
|
|
2465
|
-
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);
|
|
2466
2950
|
run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
|
|
2467
2951
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
2468
2952
|
if (drift.length) await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
2469
2953
|
};
|
|
2470
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 }) => {
|
|
2471
2955
|
const loaded = await run.store.load();
|
|
2956
|
+
await run.store.validateBorrowedWorktrees();
|
|
2472
2957
|
if (loaded.snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
|
|
2473
2958
|
if (loaded.snapshot.roles === undefined) throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow role definitions are missing from the launch snapshot");
|
|
2474
2959
|
if ((loaded.snapshot.projectRoles?.length ?? 0) > 0 && !trustedProject) throw new WorkflowError("RESUME_INCOMPATIBLE", "Cannot restore project roles in an untrusted project");
|
|
@@ -2477,7 +2962,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2477
2962
|
const active = new Set(pi.getActiveTools().filter((tool) => tool !== "workflow" && tool !== "workflow_respond" && tool !== "workflow_stop" && tool !== "workflow_catalog"));
|
|
2478
2963
|
const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
|
|
2479
2964
|
if (missing) throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
|
|
2480
|
-
const settingsPath = workflowSettingsPath();
|
|
2965
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2481
2966
|
const currentSettings = loadSettings(settingsPath);
|
|
2482
2967
|
resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
|
|
2483
2968
|
const currentAliases = currentSettings.modelAliases ?? {};
|
|
@@ -2489,10 +2974,11 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2489
2974
|
const resumeAliases = { ...previousAliases, ...currentAliases };
|
|
2490
2975
|
const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2491
2976
|
const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
|
|
2492
|
-
|
|
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);
|
|
2493
2979
|
const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
|
|
2494
2980
|
await run.store.saveSnapshot(snapshot);
|
|
2495
|
-
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);
|
|
2496
2982
|
const drift = aliasDrift(previousAliases, currentAliases);
|
|
2497
2983
|
if (drift.length) await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
|
|
2498
2984
|
const controller = new AbortController();
|
|
@@ -2503,12 +2989,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2503
2989
|
try { variables = await resolveWorkflowVariables(runContext, controller, registry); }
|
|
2504
2990
|
catch (error) {
|
|
2505
2991
|
const typed = asWorkflowError(error);
|
|
2506
|
-
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,
|
|
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);
|
|
2507
2994
|
throw typed;
|
|
2508
2995
|
}
|
|
2509
2996
|
await scheduler.cancelRun(run.store.runId);
|
|
2510
2997
|
await run.lifecycle.resume();
|
|
2511
|
-
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) => {
|
|
2512
2999
|
await run.lifecycle.enter();
|
|
2513
3000
|
const conversationId = identity.conversation ? conversationIdentityPath(identity) : undefined;
|
|
2514
3001
|
const conversationLock = conversationId ? `${run.store.runId}:${conversationId}` : "";
|
|
@@ -2546,14 +3033,14 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2546
3033
|
await run.store.complete(path, outcome.value);
|
|
2547
3034
|
return outcome.value;
|
|
2548
3035
|
} finally { if (conversationLock) conversationLocks.delete(conversationLock); await run.lifecycle.leave(); }
|
|
2549
|
-
|
|
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);
|
|
2550
3037
|
run.execution = execution;
|
|
2551
3038
|
const completion = execution.result.then(async (value) => {
|
|
2552
3039
|
await scheduler.flush();
|
|
2553
3040
|
if (run.budget.hardExhausted) throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
|
|
2554
3041
|
const resultPath = await run.store.saveResult(value);
|
|
2555
3042
|
await run.lifecycle.terminal("completed", "completed");
|
|
2556
|
-
await eventPublisher.runCompleted(run.store, run.metadata, resultPath
|
|
3043
|
+
await eventPublisher.runCompleted(run.store, run.metadata, resultPath);
|
|
2557
3044
|
return { value, resultPath };
|
|
2558
3045
|
}).catch(async (error: unknown) => {
|
|
2559
3046
|
await scheduler.flush();
|
|
@@ -2561,10 +3048,11 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2561
3048
|
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) await run.lifecycle.terminal(typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
|
|
2562
3049
|
const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), error: { code: typed.code, message: typed.message } }));
|
|
2563
3050
|
const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
|
|
2564
|
-
await eventPublisher.runFailed(run.store, run.metadata, typed,
|
|
3051
|
+
await eventPublisher.runFailed(run.store, run.metadata, typed, state);
|
|
2565
3052
|
run.update?.(workflowToolUpdate(persisted));
|
|
2566
|
-
if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) deliverFailure(pi, run.metadata
|
|
2567
|
-
});
|
|
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;
|
|
2568
3056
|
void completion;
|
|
2569
3057
|
};
|
|
2570
3058
|
const applyBudgetDecision = async (request: BudgetApprovalRequest, approved: boolean): Promise<BudgetDecisionResult> => {
|
|
@@ -2592,11 +3080,10 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2592
3080
|
if (budgetRelaxed(currentBudget, nextBudget)) {
|
|
2593
3081
|
const proposalId = randomUUID();
|
|
2594
3082
|
const request: BudgetApprovalRequest = { kind: "budget", proposalId, runId, consumed: usage, previous: currentBudget ?? {}, proposed: nextBudget ?? {}, budgetVersion: loaded.run.budgetVersion ?? 1 };
|
|
2595
|
-
|
|
2596
|
-
|
|
3083
|
+
await run.store.requestWorkflowDecision(request);
|
|
3084
|
+
await appendBudgetDecisionEvent(run, request, "adjustment_requested");
|
|
2597
3085
|
deliver(pi, budgetDecisionDelivery(run.metadata, request));
|
|
2598
|
-
|
|
2599
|
-
return { state: decisionResult.state };
|
|
3086
|
+
return { state: "awaiting_approval", proposalId };
|
|
2600
3087
|
}
|
|
2601
3088
|
const changed = JSON.stringify(currentBudget ?? {}) !== JSON.stringify(nextBudget ?? {});
|
|
2602
3089
|
if (changed) {
|
|
@@ -2645,12 +3132,14 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2645
3132
|
const lifecycle = lifecycleFor(store, loaded.run.state, budgetRuntime, loaded.snapshot.metadata);
|
|
2646
3133
|
const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
2647
3134
|
const roleDefinitions = loaded.snapshot.roles ?? {};
|
|
2648
|
-
|
|
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 } : {}) });
|
|
2649
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.`);
|
|
2650
3139
|
for (const decision of await store.pendingWorkflowDecisions()) deliver(pi, budgetDecisionDelivery(loaded.snapshot.metadata, decision));
|
|
2651
3140
|
scheduler.restoreRun(runId, loaded.snapshot.settings.concurrency, loaded.snapshot.identityVersion === LAUNCH_SNAPSHOT_IDENTITY_VERSION ? await store.loadOwnership() : [], () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
2652
3141
|
}
|
|
2653
|
-
const resumeSelect = (ctx.ui
|
|
3142
|
+
const resumeSelect = uiHostCapabilities(ctx.ui)?.select;
|
|
2654
3143
|
if (ctx.hasUI && resumeSelect) {
|
|
2655
3144
|
const interrupted = [...runs.values()].filter((r) => r.lifecycle.state === "interrupted");
|
|
2656
3145
|
if (interrupted.length > 0) {
|
|
@@ -2670,7 +3159,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2670
3159
|
});
|
|
2671
3160
|
pi.on("before_agent_start", (event, ctx) => {
|
|
2672
3161
|
if (!pi.getActiveTools().includes("workflow")) return;
|
|
2673
|
-
const roles = Object.entries(loadAgentDefinitions(ctx.cwd,
|
|
3162
|
+
const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx))).filter(([, definition]) => definition.description);
|
|
2674
3163
|
if (!roles.length) return;
|
|
2675
3164
|
const content = `Workflow role descriptions:\n${roles.map(([name, definition]) => `- \`${name}\`: ${String(definition.description)}`).join("\n")}`;
|
|
2676
3165
|
return { systemPrompt: `${event.systemPrompt}\n\n${content}` };
|
|
@@ -2681,24 +3170,25 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2681
3170
|
description: WORKFLOW_TOOL_DESCRIPTION,
|
|
2682
3171
|
promptSnippet: WORKFLOW_TOOL_PROMPT_SNIPPET,
|
|
2683
3172
|
parameters: WORKFLOW_TOOL_PARAMETERS,
|
|
2684
|
-
async execute(
|
|
3173
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
2685
3174
|
try {
|
|
2686
|
-
const
|
|
3175
|
+
const headless = object(ctx) && ctx.headless === true;
|
|
3176
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
2687
3177
|
const defaults = loadSettings(settingsPath);
|
|
2688
3178
|
if (!ctx.model) throw new WorkflowError("UNKNOWN_MODEL", "A launching model is required");
|
|
2689
3179
|
const settings = Object.freeze({ concurrency: params.concurrency ?? defaults.concurrency, ...(defaults.modelAliases ? { modelAliases: defaults.modelAliases } : {}) });
|
|
2690
3180
|
const budget = validateBudget(params.budget);
|
|
2691
3181
|
const rootModel: ModelSpec = { provider: ctx.model.provider, model: ctx.model.id, thinking: pi.getThinkingLevel() };
|
|
2692
3182
|
const rootModelName = `${rootModel.provider}/${rootModel.model}`;
|
|
2693
|
-
const modelRegistry = (ctx
|
|
3183
|
+
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
2694
3184
|
const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? [ctx.model]).map((model) => `${model.provider}/${model.id}`));
|
|
2695
3185
|
knownModels.add(rootModelName);
|
|
2696
3186
|
const availableModels = knownModels;
|
|
2697
3187
|
const rootTools = pi.getActiveTools().filter((name) => name !== "workflow" && name !== "workflow_respond" && name !== "workflow_stop" && name !== "workflow_catalog");
|
|
2698
3188
|
const trustedProject = projectTrusted(ctx);
|
|
2699
3189
|
if (typeof ctx.cwd === "string") resolveAgentResourcePolicy(ctx.cwd, trustedProject, settingsPath);
|
|
2700
|
-
const validated = validateWorkflowLaunchWithRegistry(params, { cwd: ctx.cwd, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases: defaults.modelAliases ?? {}, knownModels, settingsPath }, registry);
|
|
2701
|
-
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;
|
|
2702
3192
|
await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
|
|
2703
3193
|
const runId = randomUUID();
|
|
2704
3194
|
const args = (params.args ?? null) as JsonValue;
|
|
@@ -2708,22 +3198,25 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2708
3198
|
const runContext = workflowRunContext(ctx.cwd, ctx.sessionManager.getSessionId(), runId, checked.metadata, args, runController.signal);
|
|
2709
3199
|
const variables = await resolveWorkflowVariables(runContext, runController, registry);
|
|
2710
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);
|
|
2711
3203
|
const roles = Object.fromEntries(roleNames.map((role) => [role, agentDefinitions[role]])) as Record<string, AgentDefinition>;
|
|
2712
3204
|
const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
|
|
2713
3205
|
const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, defaults.modelAliases, knownModels, settingsPath)] : []; });
|
|
2714
3206
|
const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
|
|
2715
|
-
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 });
|
|
2716
3208
|
const budgetRuntime = new WorkflowBudgetRuntime(budget);
|
|
2717
3209
|
const initialBudget = budgetRuntime.snapshot();
|
|
2718
|
-
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);
|
|
2719
3211
|
const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
|
|
2720
3212
|
const background = !params.foreground;
|
|
2721
3213
|
const providerPause = async () => { if (background) deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
|
|
2722
|
-
const
|
|
2723
|
-
|
|
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 } : {}) });
|
|
2724
3217
|
if (params.foreground && onUpdate) onUpdate(workflowToolUpdate((await store.load()).run));
|
|
2725
3218
|
scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch());
|
|
2726
|
-
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) => {
|
|
2727
3220
|
await lifecycle.enter();
|
|
2728
3221
|
const conversationId = identity.conversation ? conversationIdentityPath(identity) : undefined;
|
|
2729
3222
|
const conversationLock = conversationId ? `${runId}:${conversationId}` : "";
|
|
@@ -2761,7 +3254,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2761
3254
|
await store.complete(path, outcome.value);
|
|
2762
3255
|
return outcome.value;
|
|
2763
3256
|
} finally { if (conversationLock) conversationLocks.delete(conversationLock); await lifecycle.leave(); }
|
|
2764
|
-
}, 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) => {
|
|
2765
3258
|
await lifecycle.enter();
|
|
2766
3259
|
try {
|
|
2767
3260
|
let previousPhase: string | undefined;
|
|
@@ -2771,33 +3264,41 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2771
3264
|
} finally { await lifecycle.leave(); }
|
|
2772
3265
|
}, log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
|
|
2773
3266
|
(runs.get(runId) as NonNullable<ReturnType<typeof runs.get>>).execution = execution;
|
|
2774
|
-
await eventPublisher.runStarted(store, checked.metadata
|
|
3267
|
+
await eventPublisher.runStarted(store, checked.metadata);
|
|
2775
3268
|
const finish = execution.result.then(async (value) => {
|
|
2776
3269
|
await scheduler.flush();
|
|
2777
3270
|
if (budgetRuntime.hardExhausted) throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
|
|
2778
3271
|
const resultPath = await store.saveResult(value);
|
|
2779
3272
|
await lifecycle.terminal("completed", "completed");
|
|
2780
|
-
await eventPublisher.runCompleted(store, checked.metadata, resultPath
|
|
3273
|
+
await eventPublisher.runCompleted(store, checked.metadata, resultPath);
|
|
2781
3274
|
return { value, resultPath };
|
|
2782
3275
|
}).catch(async (error: unknown) => {
|
|
2783
3276
|
await scheduler.flush();
|
|
2784
3277
|
const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
|
|
2785
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);
|
|
2786
|
-
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 } }));
|
|
2787
3280
|
const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
|
|
2788
|
-
await eventPublisher.runFailed(store, checked.metadata, typed,
|
|
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);
|
|
2789
3285
|
throw typed;
|
|
2790
3286
|
});
|
|
2791
|
-
|
|
3287
|
+
const completion = finish.finally(() => cleanupTerminalRun(runId));
|
|
3288
|
+
(runs.get(runId) as NonNullable<ReturnType<typeof runs.get>>).completion = completion;
|
|
2792
3289
|
if (background) {
|
|
2793
|
-
void
|
|
3290
|
+
void completion.then(async ({ value, resultPath }) => {
|
|
2794
3291
|
deliver(pi, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
|
|
2795
|
-
}, (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
|
+
});
|
|
2796
3297
|
return { content: [{ type: "text" as const, text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
|
|
2797
3298
|
}
|
|
2798
|
-
const { value } = await
|
|
3299
|
+
const { value } = await completion;
|
|
2799
3300
|
const run = (await store.load()).run;
|
|
2800
|
-
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 } };
|
|
2801
3302
|
} catch (error) {
|
|
2802
3303
|
throw mainAgentError(error);
|
|
2803
3304
|
}
|
|
@@ -2806,18 +3307,20 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2806
3307
|
return textBlock(formatWorkflowPreview(args));
|
|
2807
3308
|
},
|
|
2808
3309
|
renderResult(result, { isPartial }, _theme, context) {
|
|
2809
|
-
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;
|
|
2810
3313
|
const state = context.state as { workflowSpinner?: ReturnType<typeof setInterval> };
|
|
2811
|
-
if (
|
|
3314
|
+
if (runDetails?.run && isPartial && runDetails.run.state === "running" && !state.workflowSpinner) {
|
|
2812
3315
|
state.workflowSpinner = setInterval(context.invalidate, 80);
|
|
2813
3316
|
state.workflowSpinner.unref();
|
|
2814
|
-
} else if ((!isPartial ||
|
|
3317
|
+
} else if ((!isPartial || runDetails?.run?.state !== "running") && state.workflowSpinner) {
|
|
2815
3318
|
clearInterval(state.workflowSpinner);
|
|
2816
3319
|
delete state.workflowSpinner;
|
|
2817
3320
|
}
|
|
2818
|
-
if (
|
|
3321
|
+
if (runDetails?.run) return workflowProgressBlock(runDetails.run);
|
|
2819
3322
|
const content = result.content[0];
|
|
2820
|
-
return textBlock(isPartial ? "Workflow starting..." :
|
|
3323
|
+
return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
|
|
2821
3324
|
},
|
|
2822
3325
|
});
|
|
2823
3326
|
pi.registerCommand("workflow", {
|
|
@@ -2842,7 +3345,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2842
3345
|
let stores = await loadStores();
|
|
2843
3346
|
const usage = "Usage: /workflow [doctor|model-aliases], or /workflow pause|resume|stop|approve|reject|delete <run-id> [checkpoint or proposal-id]. Use workflow_resume for budget patches."
|
|
2844
3347
|
const setWorkflowStatus = (text: string | undefined) => {
|
|
2845
|
-
const setStatus = (ctx.ui
|
|
3348
|
+
const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
|
|
2846
3349
|
setStatus?.call(ctx.ui, "workflow-stop", text);
|
|
2847
3350
|
};
|
|
2848
3351
|
const runAction = async (actionCommand: string, keepContext: boolean, status: (text: string | undefined) => void = setWorkflowStatus): Promise<"dashboard" | "picker" | "done"> => {
|
|
@@ -2883,7 +3386,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2883
3386
|
return keepContext ? "dashboard" : "done";
|
|
2884
3387
|
}
|
|
2885
3388
|
if (action === "adjust" && run?.lifecycle.state === "budget_exhausted") {
|
|
2886
|
-
const input = await (ctx.ui
|
|
3389
|
+
const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}" );
|
|
2887
3390
|
if (input === undefined) return keepContext ? "dashboard" : "done";
|
|
2888
3391
|
const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input));
|
|
2889
3392
|
ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "running" ? "info" : "warning");
|
|
@@ -2909,8 +3412,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2909
3412
|
}
|
|
2910
3413
|
};
|
|
2911
3414
|
const manageAliases = async (): Promise<void> => {
|
|
2912
|
-
const settingsPath = workflowSettingsPath();
|
|
2913
|
-
const modelRegistry = (ctx
|
|
3415
|
+
const settingsPath = workflowSettingsPath(extensionAgentDir);
|
|
3416
|
+
const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
|
|
2914
3417
|
const available = () => [...new Set((modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`))].sort();
|
|
2915
3418
|
const selectTarget = async (): Promise<string | undefined> => {
|
|
2916
3419
|
const models = available();
|
|
@@ -2985,9 +3488,18 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
2985
3488
|
const labels = navigatorRunLabels(sorted);
|
|
2986
3489
|
const terminalStates = new Set(["completed", "failed", "stopped"]);
|
|
2987
3490
|
const hasCompleted = sorted.some(({ loaded: { run } }) => run.state === "completed");
|
|
2988
|
-
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"] : [])];
|
|
2989
3492
|
const runChoice = await ctx.ui.select("Workflows\n", pickerOptions);
|
|
2990
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
|
+
}
|
|
2991
3503
|
if (runChoice === "Model aliases") { await manageAliases(); stores = await loadStores(); continue; }
|
|
2992
3504
|
if (runChoice === "Delete all completed") {
|
|
2993
3505
|
if (!await ctx.ui.confirm("Delete completed runs?", "Delete all completed workflow runs and their artifacts? This cannot be undone.")) continue;
|
|
@@ -3009,38 +3521,6 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3009
3521
|
ctx.ui.notify(`Failed to copy ${artifact}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
3010
3522
|
}
|
|
3011
3523
|
};
|
|
3012
|
-
const openTranscript = async (transcript: string): Promise<void> => {
|
|
3013
|
-
try {
|
|
3014
|
-
const entries = SessionManager.open(transcript).buildContextEntries();
|
|
3015
|
-
if (ctx.mode !== "tui") { ctx.ui.notify(`Transcript: ${transcript}`, "info"); return; }
|
|
3016
|
-
await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {
|
|
3017
|
-
let offset = 0;
|
|
3018
|
-
let renderedLines: string[] = [];
|
|
3019
|
-
const viewport = () => Math.max(1, (((tui as unknown as { terminal?: { rows?: number } }).terminal?.rows) ?? 24) - 3);
|
|
3020
|
-
const move = (delta: number) => { offset = Math.max(0, Math.min(Math.max(0, renderedLines.length - viewport()), offset + delta)); };
|
|
3021
|
-
return {
|
|
3022
|
-
render(width: number) {
|
|
3023
|
-
renderedLines = transcriptLines(entries).flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
|
|
3024
|
-
offset = Math.min(offset, Math.max(0, renderedLines.length - viewport()));
|
|
3025
|
-
return [theme.fg("accent", "Native Pi transcript"), ...renderedLines.slice(offset, offset + viewport()), "", theme.fg("dim", "↑↓/pgup/pgdn scroll · esc close")];
|
|
3026
|
-
},
|
|
3027
|
-
invalidate() {},
|
|
3028
|
-
handleInput(data: string) {
|
|
3029
|
-
if (keybindings.matches(data, "tui.select.up")) move(-1);
|
|
3030
|
-
else if (keybindings.matches(data, "tui.select.down")) move(1);
|
|
3031
|
-
else if (keybindings.matches(data, "tui.select.pageUp")) move(-viewport());
|
|
3032
|
-
else if (keybindings.matches(data, "tui.select.pageDown")) move(viewport());
|
|
3033
|
-
else if (keybindings.matches(data, "tui.editor.cursorLineStart")) offset = 0;
|
|
3034
|
-
else if (keybindings.matches(data, "tui.editor.cursorLineEnd")) offset = Math.max(0, renderedLines.length - viewport());
|
|
3035
|
-
else if (keybindings.matches(data, "tui.select.cancel")) done(undefined);
|
|
3036
|
-
tui.requestRender();
|
|
3037
|
-
},
|
|
3038
|
-
};
|
|
3039
|
-
}, { overlay: true });
|
|
3040
|
-
} catch (error) {
|
|
3041
|
-
ctx.ui.notify(`Cannot open transcript: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
3042
|
-
}
|
|
3043
|
-
};
|
|
3044
3524
|
const loadDashboard = async () => {
|
|
3045
3525
|
const loaded = await store.load();
|
|
3046
3526
|
const checkpoints = await store.awaitingCheckpoints();
|
|
@@ -3071,16 +3551,13 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3071
3551
|
}
|
|
3072
3552
|
if (ctx.mode !== "tui") actions.set("Refresh", "refresh");
|
|
3073
3553
|
else actions.set("View script", "view-script");
|
|
3074
|
-
const transcripts = [...new Set([...loaded.run.agents.flatMap((agent) => (agent.attemptDetails ?? []).map((attempt) => attempt.sessionFile)), ...loaded.run.nativeSessions.map(({ sessionFile }) => sessionFile)])];
|
|
3075
3554
|
if (loaded.run.agents.length) actions.set("Agents...", "agents");
|
|
3076
|
-
if (!loaded.run.agents.length && ctx.mode === "tui" && transcripts.length) actions.set("View transcript", "view-transcript");
|
|
3077
|
-
if (!loaded.run.agents.length && transcripts.length) actions.set("Transcript paths", "transcripts");
|
|
3078
3555
|
if (terminalStates.has(loaded.run.state)) add("Delete", "delete");
|
|
3079
3556
|
if (ctx.mode === "tui") {
|
|
3080
3557
|
addCopy("Copy run path", store.directory, "run path");
|
|
3081
3558
|
addCopy("Copy run ID", store.runId, "run ID");
|
|
3082
3559
|
}
|
|
3083
|
-
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 };
|
|
3084
3561
|
};
|
|
3085
3562
|
const selectAgent = async (dashboard: Awaited<ReturnType<typeof loadDashboard>>): Promise<void> => {
|
|
3086
3563
|
const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
|
|
@@ -3101,14 +3578,14 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3101
3578
|
const attempts = [...(selected.attemptDetails ?? [])].sort((left, right) => left.attempt - right.attempt);
|
|
3102
3579
|
const worktree = selected.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === selected.worktreeOwner) : undefined;
|
|
3103
3580
|
const actions = [
|
|
3104
|
-
...(attempts.length ? ["
|
|
3581
|
+
...(attempts.length && herdrPaneId() ? ["Fork as Pi session in pane"] : []),
|
|
3105
3582
|
...(worktree ? ["Copy branch", "Copy worktree path"] : []),
|
|
3106
3583
|
"Copy agent ID",
|
|
3107
3584
|
"Back",
|
|
3108
3585
|
];
|
|
3109
3586
|
const chooseAttempt = async (): Promise<AgentAttemptSummary | undefined> => {
|
|
3110
3587
|
const choices = attempts.map((attempt) => `Attempt ${String(attempt.attempt)}`);
|
|
3111
|
-
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"]);
|
|
3112
3589
|
const index = choice ? choices.indexOf(choice) : -1;
|
|
3113
3590
|
return index >= 0 ? attempts[index] : undefined;
|
|
3114
3591
|
};
|
|
@@ -3118,11 +3595,17 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3118
3595
|
if (action === "Copy agent ID") { await copyArtifact(selected.id, "agent ID"); continue; }
|
|
3119
3596
|
if (action === "Copy branch" && worktree) { await copyArtifact(worktree.branch, "branch"); continue; }
|
|
3120
3597
|
if (action === "Copy worktree path" && worktree) { await copyArtifact(worktree.path, "worktree path"); continue; }
|
|
3121
|
-
if (action === "
|
|
3598
|
+
if (action === "Fork as Pi session in pane") {
|
|
3122
3599
|
const attempt = await chooseAttempt();
|
|
3123
3600
|
if (!attempt) continue;
|
|
3124
|
-
|
|
3125
|
-
|
|
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
|
+
}
|
|
3126
3609
|
}
|
|
3127
3610
|
}
|
|
3128
3611
|
};
|
|
@@ -3137,19 +3620,18 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3137
3620
|
let disposed = false;
|
|
3138
3621
|
let stopRequested = false;
|
|
3139
3622
|
let stopStatus: string | undefined;
|
|
3140
|
-
const terminalRows = () => Math.max(1, (
|
|
3623
|
+
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
3141
3624
|
const keyLabels: Record<string, string> = { up: "↑", down: "↓", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
|
|
3142
3625
|
const keyLabel = (binding: string, fallback: string) => {
|
|
3143
|
-
const
|
|
3144
|
-
const keys = getKeys?.call(keybindings, binding);
|
|
3626
|
+
const keys = keybindingKeys(keybindings, binding);
|
|
3145
3627
|
return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
|
|
3146
3628
|
};
|
|
3147
3629
|
const dashboardLayout = () => {
|
|
3148
3630
|
const rows = terminalRows();
|
|
3149
3631
|
const hintRows = rows >= 4 ? 1 : 0;
|
|
3150
|
-
const separatorRows = rows >=
|
|
3632
|
+
const separatorRows = rows >= 8 ? 1 : 0;
|
|
3151
3633
|
const available = Math.max(1, rows - hintRows - separatorRows);
|
|
3152
|
-
const actionViewport = Math.min(options.length, Math.max(1, Math.
|
|
3634
|
+
const actionViewport = Math.min(options.length, Math.max(1, Math.ceil(available / 2)));
|
|
3153
3635
|
return { rows, hintRows, separatorRows, actionViewport, dashboardViewport: available - actionViewport };
|
|
3154
3636
|
};
|
|
3155
3637
|
const updateDashboard = async (selectedOption: string | undefined) => {
|
|
@@ -3186,7 +3668,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3186
3668
|
void updateDashboard(selectedOption).catch(() => undefined).finally(() => { refreshing = false; });
|
|
3187
3669
|
}, 1000);
|
|
3188
3670
|
timer.unref();
|
|
3189
|
-
return {
|
|
3671
|
+
return borderWorkflowOverlay({
|
|
3190
3672
|
render(width: number) {
|
|
3191
3673
|
const dashboard = stopStatus ? `${view.dashboard}\n\n${stopStatus}` : view.dashboard;
|
|
3192
3674
|
const dashboardLines = truncateToVisualLines(theme.fg("accent", dashboard), Number.MAX_SAFE_INTEGER, width, 1).visualLines;
|
|
@@ -3224,7 +3706,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3224
3706
|
tui.requestRender();
|
|
3225
3707
|
},
|
|
3226
3708
|
dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
|
|
3227
|
-
};
|
|
3709
|
+
}, theme);
|
|
3228
3710
|
}, { overlay: true })
|
|
3229
3711
|
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Close"]);
|
|
3230
3712
|
if (!actionChoice || actionChoice === "Close") return;
|
|
@@ -3235,12 +3717,12 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3235
3717
|
const highlighted = highlightCode(view.script, "javascript");
|
|
3236
3718
|
let offset = 0;
|
|
3237
3719
|
let renderedLines: string[] = [];
|
|
3238
|
-
const viewport = () => Math.max(1, (
|
|
3720
|
+
const viewport = () => Math.max(1, tuiRows(tui) - 3 - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
3239
3721
|
const move = (delta: number) => {
|
|
3240
3722
|
const maxOffset = Math.max(0, renderedLines.length - viewport());
|
|
3241
3723
|
offset = Math.max(0, Math.min(maxOffset, offset + delta));
|
|
3242
3724
|
};
|
|
3243
|
-
return {
|
|
3725
|
+
return borderWorkflowOverlay({
|
|
3244
3726
|
render(width: number) {
|
|
3245
3727
|
renderedLines = highlighted.flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
|
|
3246
3728
|
const maxOffset = Math.max(0, renderedLines.length - viewport());
|
|
@@ -3261,21 +3743,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3261
3743
|
else if (keybindings.matches(data, "tui.select.cancel")) done(undefined);
|
|
3262
3744
|
tui.requestRender();
|
|
3263
3745
|
},
|
|
3264
|
-
};
|
|
3265
|
-
});
|
|
3266
|
-
continue;
|
|
3267
|
-
}
|
|
3268
|
-
if (actionChoice === "View transcript") {
|
|
3269
|
-
const transcript = await ctx.ui.select("Native Pi transcripts", [...view.transcripts, "Back"]);
|
|
3270
|
-
if (transcript && transcript !== "Back") await openTranscript(transcript);
|
|
3271
|
-
continue;
|
|
3272
|
-
}
|
|
3273
|
-
if (actionChoice === "Transcript paths") {
|
|
3274
|
-
const transcript = await ctx.ui.select("Native Pi transcript paths", [...view.transcripts, "Back"]);
|
|
3275
|
-
if (transcript && transcript !== "Back") {
|
|
3276
|
-
if (ctx.mode === "tui") await copyArtifact(transcript, "transcript path");
|
|
3277
|
-
else ctx.ui.notify(transcript, "info");
|
|
3278
|
-
}
|
|
3746
|
+
}, theme);
|
|
3747
|
+
}, { overlay: true, overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" } });
|
|
3279
3748
|
continue;
|
|
3280
3749
|
}
|
|
3281
3750
|
const copy = view.copies.get(actionChoice);
|
|
@@ -3289,11 +3758,11 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3289
3758
|
let offset = 0;
|
|
3290
3759
|
let renderedLines: string[] = [];
|
|
3291
3760
|
const layout = () => {
|
|
3292
|
-
const rows = Math.max(1, (
|
|
3761
|
+
const rows = Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
|
|
3293
3762
|
const compactControls = rows < 4;
|
|
3294
3763
|
const titleRows = rows >= 5 ? 1 : 0;
|
|
3295
3764
|
const hintRows = rows >= 8 ? 1 : 0;
|
|
3296
|
-
const separatorRows = rows >= 8 ?
|
|
3765
|
+
const separatorRows = rows >= 8 ? 1 : 0;
|
|
3297
3766
|
const controlRows = compactControls ? 1 : options.length;
|
|
3298
3767
|
const contentViewport = Math.max(0, rows - titleRows - hintRows - separatorRows - controlRows);
|
|
3299
3768
|
return { rows, compactControls, titleRows, hintRows, separatorRows, contentViewport };
|
|
@@ -3302,7 +3771,7 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3302
3771
|
const maxOffset = Math.max(0, renderedLines.length - layout().contentViewport);
|
|
3303
3772
|
offset = Math.max(0, Math.min(maxOffset, offset + delta));
|
|
3304
3773
|
};
|
|
3305
|
-
return {
|
|
3774
|
+
return borderWorkflowOverlay({
|
|
3306
3775
|
render(width: number) {
|
|
3307
3776
|
renderedLines = truncateToVisualLines(formatCheckpointReview(checkpoint), Number.MAX_SAFE_INTEGER, width, 0).visualLines;
|
|
3308
3777
|
const currentLayout = layout();
|
|
@@ -3317,7 +3786,6 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3317
3786
|
...renderedLines.slice(offset, offset + currentLayout.contentViewport),
|
|
3318
3787
|
...(currentLayout.separatorRows ? [""] : []),
|
|
3319
3788
|
...controls,
|
|
3320
|
-
...(currentLayout.separatorRows ? [""] : []),
|
|
3321
3789
|
...(currentLayout.hintRows ? [hint] : []),
|
|
3322
3790
|
];
|
|
3323
3791
|
},
|
|
@@ -3331,8 +3799,8 @@ export default function workflowExtension(pi: ExtensionAPI, home?: string, clipb
|
|
|
3331
3799
|
else if (keybindings.matches(data, "tui.select.cancel")) done(undefined);
|
|
3332
3800
|
tui.requestRender();
|
|
3333
3801
|
},
|
|
3334
|
-
};
|
|
3335
|
-
});
|
|
3802
|
+
}, theme);
|
|
3803
|
+
}, { overlay: true, overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" } });
|
|
3336
3804
|
if (decision) {
|
|
3337
3805
|
const accepted = await answerCheckpoint(store.runId, checkpoint.name, decision === "Approve", true);
|
|
3338
3806
|
if (!accepted) ctx.ui.notify("Checkpoint is not awaiting a response.", "warning");
|
|
@@ -3385,6 +3853,6 @@ function modelSpec(value: string, fallback: ModelSpec): ModelSpec {
|
|
|
3385
3853
|
export { acquireSessionLease, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
|
|
3386
3854
|
export type { AwaitingCheckpoint, CompletedOperation, ConversationHead, NativeSessionReference, PendingWorkflowDecision, PersistedConversation, PersistedOwnershipNode, PersistedRun, WorktreeReference } from "./persistence.js";
|
|
3387
3855
|
export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
|
|
3388
|
-
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";
|
|
3389
3857
|
export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
|
|
3390
|
-
export type { DoctorDiagnostic, DoctorOptions, DoctorPiState, DoctorReport, DoctorRole, DoctorSeverity, DoctorTrust
|
|
3858
|
+
export type { DoctorDiagnostic, DoctorFunction, DoctorOptions, DoctorPiState, DoctorReport, DoctorRole, DoctorSeverity, DoctorTrust } from "./doctor.js";
|