pi-extensible-workflows 3.2.0 → 3.4.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 +2 -31
- package/dist/src/agent-execution.d.ts +67 -9
- package/dist/src/agent-execution.js +385 -141
- package/dist/src/bundles.d.ts +53 -0
- package/dist/src/bundles.js +457 -0
- package/dist/src/cli.d.ts +3 -1
- package/dist/src/cli.js +156 -22
- package/dist/src/doctor-cleanup.js +68 -31
- package/dist/src/eval-capture-extension.js +16 -1
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +29 -13
- package/dist/src/host.d.ts +12 -9
- package/dist/src/host.js +525 -195
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.js +3 -2
- package/dist/src/persistence.d.ts +43 -8
- package/dist/src/persistence.js +54 -0
- package/dist/src/registry.d.ts +3 -2
- package/dist/src/registry.js +16 -4
- package/dist/src/session-inspector.d.ts +12 -2
- package/dist/src/session-inspector.js +50 -24
- package/dist/src/types.d.ts +141 -60
- package/dist/src/types.js +1 -0
- package/dist/src/validation.js +28 -7
- package/dist/src/workflow-artifacts.d.ts +1 -0
- package/dist/src/workflow-artifacts.js +1 -0
- package/dist/src/workflow-evals.d.ts +7 -1
- package/dist/src/workflow-evals.js +23 -3
- package/evals/cases/recovery-completed-worktree.yaml +17 -0
- package/evals/cases/recovery-failed-run.yaml +14 -0
- package/package.json +1 -1
- package/skills/pi-extensible-workflows/SKILL.md +13 -5
- package/src/agent-execution.ts +302 -107
- package/src/bundles.ts +471 -0
- package/src/cli.ts +130 -22
- package/src/doctor-cleanup.ts +38 -4
- package/src/eval-capture-extension.ts +15 -1
- package/src/execution.ts +27 -15
- package/src/host.ts +454 -175
- package/src/index.ts +5 -4
- package/src/persistence.ts +58 -4
- package/src/registry.ts +14 -5
- package/src/session-inspector.ts +49 -26
- package/src/types.ts +55 -30
- package/src/validation.ts +19 -6
- package/src/workflow-artifacts.ts +1 -0
- package/src/workflow-evals.ts +24 -3
package/src/agent-execution.ts
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
|
-
import { realpathSync } from "node:fs";
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { join, resolve } from "node:path";
|
|
3
4
|
import { Type } from "@earendil-works/pi-ai";
|
|
4
5
|
import { Value } from "typebox/value";
|
|
5
6
|
import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, SettingsManager, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
7
8
|
type AgentMessage = { role: string; content?: unknown; stopReason?: string; errorMessage?: string; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: { total: number } } };
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
type PiSession = {
|
|
10
|
+
readonly sessionId: string;
|
|
11
|
+
readonly sessionFile: string | undefined;
|
|
12
|
+
readonly messages: readonly AgentMessage[];
|
|
13
|
+
getSessionStats(): { tokens: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number }; cost: number };
|
|
14
|
+
readonly systemPrompt?: string;
|
|
15
|
+
readonly model?: { provider: string; model?: string; id?: string };
|
|
16
|
+
readonly agent?: { state: { tools: readonly { name: string }[] } };
|
|
17
|
+
subscribe?(listener: (event: unknown) => void): () => void;
|
|
18
|
+
prompt(text: string): Promise<void>;
|
|
19
|
+
steer?(text: string): Promise<void>;
|
|
20
|
+
abort?(): Promise<void>;
|
|
21
|
+
dispose(): void;
|
|
22
|
+
};
|
|
23
|
+
import type { AgentIdentity, AgentResourceExclusions, AgentResourcePolicy, AgentSetup, AgentSetupSummary, AgentTransport, AgentTransportContext, JsonSchema, JsonValue, ModelSpec, PreparedAgentSession, RegisteredAgentSetupHook, SessionInput, WorkflowAgentMessage, WorkflowAgentSession, WorkflowAgentSessionEvent, WorkflowAgentSessionReference, WorkflowAgentSessionState, WorkflowAgentSessionStats, WorkflowAgentTurnResult, WorkflowRunContext } from "./types.js";
|
|
24
|
+
import { deepFreeze, jsonObject, disabledResources, mergeAgentResourceExclusions, modelAliasName, modelCapability, resolveModelReference, unmatchedResourcePatterns } from "./utils.js";
|
|
10
25
|
import { WorkflowError } from "./types.js";
|
|
11
26
|
import type { RunStore } from "./persistence.js";
|
|
12
|
-
export type { AgentSetup, AgentSetupContext, AgentSetupHook,
|
|
27
|
+
export type { AgentSetup, AgentSetupContext, AgentSetupHook, AgentTransport, AgentTransportContext, PreparedAgentSession, RegisteredAgentSetupHook, SessionInput, WorkflowAgentMessage, WorkflowAgentSession, WorkflowAgentSessionEvent, WorkflowAgentSessionReference, WorkflowAgentSessionState, WorkflowAgentSessionStats, WorkflowAgentTurnResult } from "./types.js";
|
|
13
28
|
export interface AgentBudgetHooks {
|
|
14
29
|
beforeAttempt(): void;
|
|
15
30
|
beforeTurn(): void;
|
|
16
31
|
afterTurn(accounting: AgentAccounting, final: boolean): void;
|
|
17
32
|
instruction(): string | undefined;
|
|
18
33
|
}
|
|
19
|
-
export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: ThinkingLevel; tools?: readonly string[]; disabledAgentResources?: AgentResourceExclusions }
|
|
34
|
+
export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: ThinkingLevel; tools?: readonly string[]; overrideSystemPrompt?: boolean; disabledAgentResources?: AgentResourceExclusions }
|
|
20
35
|
export interface AgentProviderFailure { label: string; provider: string; model: string; error: string }
|
|
21
36
|
export type AgentProviderRecovery = "retry" | "abort" | { model: string };
|
|
22
37
|
export interface AgentExecutionOptions {
|
|
@@ -27,7 +42,7 @@ export interface AgentExecutionOptions {
|
|
|
27
42
|
model?: string;
|
|
28
43
|
thinking?: ThinkingLevel;
|
|
29
44
|
onProgress?: (progress: AgentProgress) => void | Promise<void>;
|
|
30
|
-
onAttempt?: (attempt:
|
|
45
|
+
onAttempt?: (attempt: AgentAttempt) => void | Promise<void>;
|
|
31
46
|
providerErrorRecovery?: (failure: AgentProviderFailure) => Promise<AgentProviderRecovery>;
|
|
32
47
|
modelOverride?: ModelSpec;
|
|
33
48
|
tools?: readonly string[];
|
|
@@ -49,6 +64,7 @@ export interface AgentExecutionRoot {
|
|
|
49
64
|
tools: ReadonlySet<string>;
|
|
50
65
|
agentDefinitions?: Readonly<Record<string, AgentDefinition>>;
|
|
51
66
|
agentDir?: string;
|
|
67
|
+
additionalSkillPaths?: readonly string[];
|
|
52
68
|
availableModels?: ReadonlySet<string>;
|
|
53
69
|
knownModels?: ReadonlySet<string>;
|
|
54
70
|
modelAliases?: Readonly<Record<string, string>>;
|
|
@@ -64,8 +80,8 @@ export interface AgentExecutionRoot {
|
|
|
64
80
|
export interface AgentAccounting { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }
|
|
65
81
|
export interface AgentToolCallProgress { id: string; name: string; state: "running" | "completed" | "failed" }
|
|
66
82
|
export interface AgentActivity { kind: "reasoning" | "tool" | "text"; text: string }
|
|
67
|
-
export interface AgentProgress { accounting: AgentAccounting; toolCalls: readonly AgentToolCallProgress[]; activity?: AgentActivity; persist: boolean }
|
|
68
|
-
export interface AgentAttempt { attempt: number;
|
|
83
|
+
export interface AgentProgress { accounting: AgentAccounting; toolCalls: readonly AgentToolCallProgress[]; state?: WorkflowAgentSessionState; activity?: AgentActivity; lastEventAt?: number; persist: boolean }
|
|
84
|
+
export interface AgentAttempt { attempt: number; transport: string; session?: WorkflowAgentSessionReference; liveSession?: WorkflowAgentSession; result?: JsonValue; error?: { code: string; message: string }; accounting: AgentAccounting; setup: AgentSetupSummary }
|
|
69
85
|
export interface AgentExecutionResult { value: JsonValue; attempts: readonly AgentAttempt[]; cwd: string }
|
|
70
86
|
|
|
71
87
|
function parseModel(value: string | undefined, fallback: ModelSpec, thinking?: ThinkingLevel, aliases: Readonly<Record<string, string>> = {}, knownModels?: ReadonlySet<string>, settingsPath?: string): ModelSpec {
|
|
@@ -74,8 +90,7 @@ function parseModel(value: string | undefined, fallback: ModelSpec, thinking?: T
|
|
|
74
90
|
return { ...parsed, ...(thinking ? { thinking } : !parsed.thinking && fallback.thinking ? { thinking: fallback.thinking } : {}) };
|
|
75
91
|
}
|
|
76
92
|
|
|
77
|
-
function text(
|
|
78
|
-
const message = [...messages].reverse().find((item) => item.role === "assistant");
|
|
93
|
+
function text(message: WorkflowAgentMessage | undefined): string {
|
|
79
94
|
if (!message || !Array.isArray(message.content)) return "";
|
|
80
95
|
return message.content.filter((part: unknown): part is { type: "text"; text: string } => typeof part === "object" && part !== null && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string").map((part) => part.text).join("");
|
|
81
96
|
}
|
|
@@ -84,20 +99,15 @@ function hasToolCall(message: unknown): boolean {
|
|
|
84
99
|
return typeof message === "object" && message !== null && Array.isArray((message as { content?: unknown }).content) && (message as { content: unknown[] }).content.some((part) => typeof part === "object" && part !== null && (part as { type?: unknown }).type === "toolCall");
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
function latestAssistantHasToolCall(
|
|
88
|
-
const message = [...messages].reverse().find((item) => item.role === "assistant");
|
|
89
|
-
return hasToolCall(message);
|
|
90
|
-
}
|
|
102
|
+
function latestAssistantHasToolCall(message: WorkflowAgentMessage | undefined): boolean { return hasToolCall(message); }
|
|
91
103
|
|
|
92
104
|
type TerminalProviderError = { provider: string; model: string; error: string };
|
|
93
|
-
function throwIfTerminalAssistantError(session:
|
|
94
|
-
const message = [...session.messages].reverse().find((item) => item.role === "assistant");
|
|
105
|
+
function throwIfTerminalAssistantError(session: WorkflowAgentSession, message: WorkflowAgentMessage | undefined): void {
|
|
95
106
|
if (message?.stopReason !== "error") return;
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const error = message.errorMessage ?? "Native Pi assistant ended with a terminal provider error";
|
|
107
|
+
const state = session.getState();
|
|
108
|
+
const error = message.errorMessage ?? "Workflow agent session ended with a terminal provider error";
|
|
99
109
|
const failure = new WorkflowError("AGENT_FAILED", error);
|
|
100
|
-
Object.defineProperty(failure, "terminalProviderError", { value: { provider, model, error }, configurable: true });
|
|
110
|
+
Object.defineProperty(failure, "terminalProviderError", { value: { provider: state.model.provider, model: state.model.model, error }, configurable: true });
|
|
101
111
|
throw failure;
|
|
102
112
|
}
|
|
103
113
|
function terminalProviderError(error: WorkflowError): TerminalProviderError | undefined {
|
|
@@ -106,14 +116,41 @@ function terminalProviderError(error: WorkflowError): TerminalProviderError | un
|
|
|
106
116
|
const candidate = value as Partial<TerminalProviderError>;
|
|
107
117
|
return typeof candidate.provider === "string" && typeof candidate.model === "string" && typeof candidate.error === "string" ? { provider: candidate.provider, model: candidate.model, error: candidate.error } : undefined;
|
|
108
118
|
}
|
|
119
|
+
type ProviderRecoveryMarker = { providerRecoveryHandled?: boolean; providerRecovery?: AgentProviderRecovery; providerRecoveryFailed?: boolean };
|
|
120
|
+
const providerContinuationPrompt = "The provider error was transient. Continue the task from your current state.";
|
|
121
|
+
async function recoverTerminalProviderError(session: WorkflowAgentSession, label: string, recovery: AgentExecutionOptions["providerErrorRecovery"], continuePrompt: () => Promise<void>, getAssistant: () => WorkflowAgentMessage | undefined): Promise<boolean> {
|
|
122
|
+
let continued = false;
|
|
123
|
+
for (;;) {
|
|
124
|
+
try { throwIfTerminalAssistantError(session, getAssistant()); return continued; }
|
|
125
|
+
catch (error) {
|
|
126
|
+
const typed = error instanceof WorkflowError ? error : new WorkflowError("AGENT_FAILED", error instanceof Error ? error.message : String(error));
|
|
127
|
+
const terminal = terminalProviderError(typed);
|
|
128
|
+
if (!terminal || !recovery) throw error;
|
|
129
|
+
let action: AgentProviderRecovery;
|
|
130
|
+
try { action = await recovery({ label, ...terminal }); } catch { Object.assign(typed, { providerRecoveryHandled: true, providerRecoveryFailed: true }); throw typed; }
|
|
131
|
+
if (action === "retry") { continued = true; await continuePrompt(); continue; }
|
|
132
|
+
Object.assign(typed, { providerRecoveryHandled: true, providerRecovery: action });
|
|
133
|
+
throw typed;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
109
137
|
|
|
110
|
-
function accounting(stats:
|
|
138
|
+
function accounting(stats: WorkflowAgentSessionStats): AgentAccounting {
|
|
111
139
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
112
140
|
}
|
|
113
141
|
function canonicalSourcePath(path: string): string { try { return realpathSync(path); } catch { return resolve(path); } }
|
|
142
|
+
const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
|
|
143
|
+
function workflowSystemPromptPath(cwd: string, agentDir: string, projectTrusted: boolean): string | undefined {
|
|
144
|
+
const projectPath = join(cwd, ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md");
|
|
145
|
+
if (projectTrusted && existsSync(projectPath)) return projectPath;
|
|
146
|
+
const globalPaths = [join(homedir(), ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md"), join(agentDir, WORKFLOW_DIRECTORY, "SYSTEM.md")];
|
|
147
|
+
return globalPaths.find((path) => existsSync(path));
|
|
148
|
+
}
|
|
114
149
|
|
|
115
|
-
export async function
|
|
150
|
+
export async function createLocalPiSession(input: SessionInput): Promise<PiSession> {
|
|
116
151
|
const agentDir = input.agentDir ?? getAgentDir();
|
|
152
|
+
const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
|
|
153
|
+
const systemPromptOptions = input.systemPrompt !== undefined ? { systemPromptOverride: () => input.systemPrompt } : systemPromptSource !== undefined ? { systemPrompt: systemPromptSource } : {};
|
|
117
154
|
const manager = input.agentDir ? SessionManager.create(input.cwd, join(agentDir, "sessions")) : SessionManager.create(input.cwd);
|
|
118
155
|
manager.appendSessionInfo(input.sessionLabel);
|
|
119
156
|
const modelRuntime = await ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json") });
|
|
@@ -147,25 +184,77 @@ export async function createNativeAgentSession(input: SessionInput): Promise<Nat
|
|
|
147
184
|
noExtensions: true,
|
|
148
185
|
additionalExtensionPaths: extensionPaths,
|
|
149
186
|
noSkills: true,
|
|
150
|
-
additionalSkillPaths: skillPaths,
|
|
187
|
+
additionalSkillPaths: [...new Set([...skillPaths, ...(input.additionalSkillPaths ?? [])])],
|
|
151
188
|
...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
|
|
152
189
|
skillsOverride: (base) => {
|
|
153
190
|
const disabledSkills = updateSkillMatches(base.skills);
|
|
154
191
|
return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
|
|
155
192
|
},
|
|
156
193
|
...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
|
|
194
|
+
...systemPromptOptions,
|
|
157
195
|
});
|
|
158
196
|
await resourceLoader.reload();
|
|
159
|
-
} else if (input.systemPromptAppend || input.extensionFactories?.length) {
|
|
160
|
-
resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
|
|
197
|
+
} else if (input.systemPrompt !== undefined || systemPromptSource !== undefined || input.systemPromptAppend || input.extensionFactories?.length || input.additionalSkillPaths?.length) {
|
|
198
|
+
resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: [...input.additionalSkillPaths] } : {}), ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...systemPromptOptions, ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
|
|
161
199
|
await resourceLoader.reload();
|
|
162
200
|
}
|
|
163
201
|
const { session } = await createAgentSession({ ...(input.options ?? {}), cwd: input.cwd, agentDir, modelRuntime, model, ...(settingsManager ? { settingsManager } : {}), ...(input.model.thinking ? { thinkingLevel: input.model.thinking } : {}), tools, ...(customTools.length ? { customTools } : {}), ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(resourceLoader ? { resourceLoader } : {}), sessionManager: manager });
|
|
164
202
|
return Object.assign(session, {
|
|
165
203
|
getLeafId: () => manager.getLeafId(),
|
|
166
204
|
getToolDefinitions: () => session.getAllTools().map(({ name, description, parameters, promptGuidelines }) => ({ name, description, parameters, ...(promptGuidelines ? { promptGuidelines } : {}) })),
|
|
167
|
-
}) as unknown as
|
|
205
|
+
}) as unknown as PiSession;
|
|
206
|
+
}
|
|
207
|
+
function workflowAgentMessage(message: AgentMessage | undefined): WorkflowAgentMessage | undefined { return message ? { role: message.role, ...(message.content === undefined ? {} : { content: message.content }), ...(message.stopReason === undefined ? {} : { stopReason: message.stopReason }), ...(message.errorMessage === undefined ? {} : { errorMessage: message.errorMessage }), ...(message.usage === undefined ? {} : { usage: message.usage }) } : undefined; }
|
|
208
|
+
function workflowAgentStats(stats: ReturnType<PiSession["getSessionStats"]>): WorkflowAgentSessionStats { return { tokens: { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, total: stats.tokens.total }, cost: stats.cost }; }
|
|
209
|
+
function workflowAgentState(native: PiSession, prepared: Readonly<PreparedAgentSession>): WorkflowAgentSessionState {
|
|
210
|
+
const tools = native.agent?.state.tools.map(({ name }) => name) ?? prepared.tools;
|
|
211
|
+
const model = native.model?.provider && (native.model.model ?? native.model.id) ? { provider: native.model.provider, model: native.model.model ?? native.model.id ?? prepared.model.model, ...(prepared.model.thinking ? { thinking: prepared.model.thinking } : {}) } : { ...prepared.model };
|
|
212
|
+
return { model, ...(model.thinking ? { thinking: model.thinking } : {}), tools: [...tools], ...(native.systemPrompt === undefined ? {} : { systemPrompt: native.systemPrompt }) };
|
|
168
213
|
}
|
|
214
|
+
function localSessionEvent(event: unknown): WorkflowAgentSessionEvent { return event as WorkflowAgentSessionEvent; }
|
|
215
|
+
export async function createLocalWorkflowAgentSession(prepared: Readonly<PreparedAgentSession>, context: Readonly<AgentTransportContext>): Promise<WorkflowAgentSession> {
|
|
216
|
+
void context;
|
|
217
|
+
const input: SessionInput = {
|
|
218
|
+
cwd: prepared.cwd, model: { ...prepared.model }, tools: [...prepared.tools] as SessionInput["tools"], sessionLabel: prepared.sessionLabel,
|
|
219
|
+
...(prepared.agentDir ? { agentDir: prepared.agentDir } : {}), ...(prepared.customTools?.length ? { customTools: [...prepared.customTools] as NonNullable<SessionInput["customTools"]> } : {}),
|
|
220
|
+
...(prepared.resultTool ? { resultTool: prepared.resultTool } : {}), ...(prepared.systemPrompt === undefined ? {} : { systemPrompt: prepared.systemPrompt }),
|
|
221
|
+
...(prepared.systemPromptAppend ? { systemPromptAppend: prepared.systemPromptAppend } : {}), ...(prepared.extensionFactories?.length ? { extensionFactories: [...prepared.extensionFactories] } : {}),
|
|
222
|
+
...(prepared.additionalSkillPaths?.length ? { additionalSkillPaths: [...prepared.additionalSkillPaths] } : {}), ...(prepared.resourcePolicy ? { resourcePolicy: structuredClone(prepared.resourcePolicy) } : {}), ...(prepared.options ? { options: { ...prepared.options } } : {}),
|
|
223
|
+
};
|
|
224
|
+
const native = await createLocalPiSession(input);
|
|
225
|
+
let disposal: Promise<void> | undefined;
|
|
226
|
+
let aborting: Promise<void> | undefined;
|
|
227
|
+
let prompting: Promise<WorkflowAgentTurnResult> | undefined;
|
|
228
|
+
let disposed = false;
|
|
229
|
+
const startAbort = () => aborting ??= Promise.resolve().then(() => native.abort?.()).then(() => undefined).finally(() => { aborting = undefined; });
|
|
230
|
+
const reference: WorkflowAgentSessionReference = { transport: "local", sessionId: native.sessionId, ...(native.sessionFile ? { locator: { sessionFile: native.sessionFile } } : {}) };
|
|
231
|
+
const session = {
|
|
232
|
+
reference,
|
|
233
|
+
getState: () => Object.freeze(workflowAgentState(native, prepared)),
|
|
234
|
+
getSessionStats: () => workflowAgentStats(native.getSessionStats()),
|
|
235
|
+
subscribe(listener: (event: WorkflowAgentSessionEvent) => void) { listener({ type: "state_changed", state: workflowAgentState(native, prepared) }); return native.subscribe?.((event) => { listener(localSessionEvent(event)); }) ?? (() => undefined); },
|
|
236
|
+
getLastAssistant: () => workflowAgentMessage([...native.messages].reverse().find((message) => message.role === "assistant")),
|
|
237
|
+
async prompt(text: string) {
|
|
238
|
+
if (disposed) throw new WorkflowError("INTERNAL_ERROR", "Local workflow session is disposed");
|
|
239
|
+
const prompt = Promise.resolve().then(async () => { await native.prompt(text); const assistant = workflowAgentMessage([...native.messages].reverse().find((message) => message.role === "assistant")); return assistant ? { assistant } : {}; });
|
|
240
|
+
prompting = prompt;
|
|
241
|
+
try { return await prompt; } finally { if (prompting === prompt) prompting = undefined; }
|
|
242
|
+
},
|
|
243
|
+
async steer(text: string) { if (!native.steer) throw new WorkflowError("INTERNAL_ERROR", "Local workflow session does not support steering"); await native.steer(text); },
|
|
244
|
+
async abort() { if (disposed) return; await startAbort(); },
|
|
245
|
+
async dispose() {
|
|
246
|
+
disposal ??= (async () => {
|
|
247
|
+
disposed = true;
|
|
248
|
+
try { await startAbort(); } catch { /* Abort failure must not prevent the prompt from settling. */ }
|
|
249
|
+
try { await prompting; } catch { /* Prompt rejection is expected after abort during teardown. */ }
|
|
250
|
+
native.dispose();
|
|
251
|
+
})();
|
|
252
|
+
await disposal;
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
return session;
|
|
256
|
+
}
|
|
257
|
+
export const localAgentTransport: AgentTransport = Object.freeze({ id: "local", createSession: createLocalWorkflowAgentSession });
|
|
169
258
|
function changedOption(options: Readonly<Record<string, JsonValue>>, baseline: Readonly<Record<string, JsonValue>>, key: string): boolean { return JSON.stringify(options[key]) !== JSON.stringify(baseline[key]); }
|
|
170
259
|
function validThinking(value: unknown): value is ThinkingLevel { return typeof value === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value); }
|
|
171
260
|
interface ChildAgentToolParams {
|
|
@@ -199,38 +288,80 @@ function fallbackSetupContext(root: AgentExecutionRoot, options: AgentExecutionO
|
|
|
199
288
|
function resourcePolicySummary(policy: AgentResourcePolicy): NonNullable<AgentSetupSummary["disabledAgentResources"]> {
|
|
200
289
|
return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], excludedSkills: [...(policy.excludedSkills ?? [])], excludedExtensions: [...(policy.excludedExtensions ?? [])], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
|
|
201
290
|
}
|
|
202
|
-
|
|
291
|
+
function resourcePolicyWidened(ceiling: AgentResourcePolicy | undefined, candidate: AgentResourcePolicy | undefined): boolean {
|
|
292
|
+
if (!ceiling) return false;
|
|
293
|
+
if (!candidate) return true;
|
|
294
|
+
if (!ceiling.projectTrusted && candidate.projectTrusted) return true;
|
|
295
|
+
return ceiling.effective.skills.some((pattern) => !candidate.effective.skills.includes(pattern)) || ceiling.effective.extensions.some((pattern) => !candidate.effective.extensions.includes(pattern));
|
|
296
|
+
}
|
|
297
|
+
function preparedAgentSession(input: SessionInput): Readonly<PreparedAgentSession> {
|
|
298
|
+
const prepared = {
|
|
299
|
+
cwd: input.cwd, model: Object.freeze({ ...input.model }), tools: Object.freeze([...input.tools]), sessionLabel: input.sessionLabel,
|
|
300
|
+
...(input.agentDir ? { agentDir: input.agentDir } : {}), ...(input.customTools?.length ? { customTools: Object.freeze([...input.customTools]) } : {}), ...(input.resultTool ? { resultTool: input.resultTool } : {}), ...(input.options ? { options: Object.freeze(structuredClone(input.options)) } : {}),
|
|
301
|
+
...(input.systemPrompt === undefined ? {} : { systemPrompt: input.systemPrompt }), ...(input.systemPromptAppend ? { systemPromptAppend: input.systemPromptAppend } : {}),
|
|
302
|
+
...(input.extensionFactories?.length ? { extensionFactories: Object.freeze([...input.extensionFactories]) } : {}), ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: Object.freeze([...input.additionalSkillPaths]) } : {}),
|
|
303
|
+
...(input.resourcePolicy ? { resourcePolicy: Object.freeze(structuredClone(input.resourcePolicy)) } : {}),
|
|
304
|
+
};
|
|
305
|
+
return deepFreeze(prepared);
|
|
306
|
+
}
|
|
307
|
+
function agentSetupSummary(setup: AgentSetup, hookNames: readonly string[]): AgentSetupSummary {
|
|
308
|
+
const model = setup.sessionInput.model;
|
|
309
|
+
return { hookNames: [...hookNames], model: { provider: model.provider, model: model.model, ...(model.thinking ? { thinking: model.thinking } : {}) }, tools: [...setup.sessionInput.tools], cwd: setup.sessionInput.cwd, ...(setup.sessionInput.resourcePolicy ? { disabledAgentResources: resourcePolicySummary(setup.sessionInput.resourcePolicy) } : {}) };
|
|
310
|
+
}
|
|
311
|
+
type PreparedAgentSetup = { setup: AgentSetup; summary: AgentSetupSummary; failure?: { error: unknown } };
|
|
312
|
+
async function prepareAgentSetup(root: AgentExecutionRoot, transport: AgentTransport, task: string, options: AgentExecutionOptions, resolved: { model: ModelSpec; tools: readonly string[]; systemPrompt?: string; systemPromptAppend: string }, cwd: string, attempt: number, signal: AbortSignal | undefined, customTools: readonly ToolDefinition[], resultTool: ToolDefinition | undefined): Promise<PreparedAgentSetup> {
|
|
203
313
|
const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
|
|
204
314
|
const baselineOptions = structuredClone(options.agentOptions ?? {});
|
|
205
315
|
const baseResourcePolicy = await root.agentResourcePolicy?.();
|
|
206
316
|
const roleExclusions = options.role ? root.agentDefinitions?.[options.role]?.disabledAgentResources : undefined;
|
|
207
317
|
const resourcePolicy = baseResourcePolicy && roleExclusions ? { ...baseResourcePolicy, effective: mergeAgentResourceExclusions(baseResourcePolicy.effective, roleExclusions) } : baseResourcePolicy;
|
|
208
|
-
const
|
|
209
|
-
const
|
|
318
|
+
const resourcePolicyCeiling = resourcePolicy ? structuredClone(resourcePolicy) : undefined;
|
|
319
|
+
const sessionInput: SessionInput = { cwd, model: { ...resolved.model }, tools: [...resolved.tools], sessionLabel: `${options.workflowName}:${options.label}:attempt-${String(attempt)}`, ...(root.agentDir ? { agentDir: root.agentDir } : {}), ...(root.additionalSkillPaths?.length ? { additionalSkillPaths: [...root.additionalSkillPaths] } : {}), ...(customTools.length ? { customTools: [...customTools] } : {}), ...(resultTool ? { resultTool } : {}), ...(resolved.systemPrompt !== undefined ? { systemPrompt: resolved.systemPrompt } : {}), systemPromptAppend: resolved.systemPromptAppend, ...(resourcePolicy ? { resourcePolicy } : {}), options: structuredClone(baselineOptions) };
|
|
320
|
+
const setup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, prepared: preparedAgentSession(sessionInput), transport };
|
|
210
321
|
const base = fallbackSetupContext(root, options, setupSignal);
|
|
211
322
|
const context = Object.freeze({ run: base.run, identity: base.identity, attempt, signal: setupSignal });
|
|
212
323
|
const hookNames: string[] = [];
|
|
213
324
|
for (const hook of [...(root.agentSetupHooks ?? [])].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))) {
|
|
214
|
-
if (setupSignal.aborted)
|
|
215
|
-
try { await hook.setup(setup, context); } catch (error) {
|
|
325
|
+
if (setupSignal.aborted) return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error: new WorkflowError("CANCELLED", "Agent cancelled") } };
|
|
326
|
+
try { await hook.setup(setup, context); } catch (error) {
|
|
327
|
+
setup.prepared = preparedAgentSession(setup.sessionInput);
|
|
328
|
+
return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error: setupSignal.reason !== undefined ? new WorkflowError("CANCELLED", "Agent cancelled") : error } };
|
|
329
|
+
}
|
|
330
|
+
setup.prepared = preparedAgentSession(setup.sessionInput);
|
|
216
331
|
hookNames.push(hook.name);
|
|
217
|
-
if (setupSignal.reason !== undefined)
|
|
332
|
+
if (setupSignal.reason !== undefined) return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error: new WorkflowError("CANCELLED", "Agent cancelled") } };
|
|
218
333
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
334
|
+
try {
|
|
335
|
+
if (resourcePolicyWidened(resourcePolicyCeiling, setup.sessionInput.resourcePolicy)) throw new WorkflowError("INVALID_METADATA", "Agent setup widened the prepared resource policy");
|
|
336
|
+
setup.sessionInput.options = setup.options;
|
|
337
|
+
if (changedOption(setup.options, baselineOptions, "model") && typeof setup.options.model === "string") setup.sessionInput.model = parseModel(setup.options.model, setup.sessionInput.model, changedOption(setup.options, baselineOptions, "thinking") && validThinking(setup.options.thinking) ? setup.options.thinking : undefined, root.modelAliases, root.knownModels ?? root.availableModels, root.settingsPath);
|
|
338
|
+
if (changedOption(setup.options, baselineOptions, "thinking") && validThinking(setup.options.thinking)) setup.sessionInput.model = { ...setup.sessionInput.model, thinking: setup.options.thinking };
|
|
339
|
+
if (changedOption(setup.options, baselineOptions, "tools") && Array.isArray(setup.options.tools) && setup.options.tools.every((tool) => typeof tool === "string")) setup.sessionInput.tools = [...setup.options.tools];
|
|
340
|
+
if (changedOption(setup.options, baselineOptions, "cwd") && typeof setup.options.cwd === "string") setup.sessionInput.cwd = setup.options.cwd;
|
|
341
|
+
const customToolNames = new Set([...(setup.sessionInput.customTools ?? []).map(({ name }) => name), ...(setup.sessionInput.resultTool ? [setup.sessionInput.resultTool.name] : [])]);
|
|
342
|
+
const widened = setup.sessionInput.tools.find((tool) => !resolved.tools.includes(tool) && !customToolNames.has(tool));
|
|
343
|
+
const outsideTool = widened ?? setup.sessionInput.tools.find((tool) => !root.tools.has(tool) && !customToolNames.has(tool));
|
|
344
|
+
if (outsideTool) throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${outsideTool}`);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
setup.prepared = preparedAgentSession(setup.sessionInput);
|
|
347
|
+
return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error } };
|
|
348
|
+
}
|
|
349
|
+
setup.prepared = preparedAgentSession(setup.sessionInput);
|
|
350
|
+
return { setup, summary: agentSetupSummary(setup, hookNames) };
|
|
351
|
+
}
|
|
352
|
+
function attemptRecord(transport: string, attempt: number, session: WorkflowAgentSession, setup: AgentSetupSummary, stats: AgentAccounting, result?: JsonValue, error?: { code: string; message: string }): AgentAttempt {
|
|
353
|
+
return { attempt, transport, session: session.reference, ...(result === undefined ? {} : { result }), ...(error ? { error } : {}), accounting: stats, setup };
|
|
354
|
+
}
|
|
355
|
+
function errorWithAttempts(error: unknown, attempts: readonly AgentAttempt[]): Error {
|
|
356
|
+
const typed = error instanceof Error ? error : new Error(typeof error === "string" ? error : String(error));
|
|
357
|
+
return Object.assign(typed, { attempts });
|
|
227
358
|
}
|
|
228
|
-
|
|
229
359
|
export class WorkflowAgentExecutor {
|
|
230
|
-
|
|
360
|
+
private readonly transport: AgentTransport;
|
|
361
|
+
constructor(private readonly root: AgentExecutionRoot, transport: AgentTransport = localAgentTransport) { this.transport = transport; }
|
|
231
362
|
setRunContext(runContext: Readonly<WorkflowRunContext>): void { this.root.runContext = runContext; }
|
|
232
363
|
|
|
233
|
-
resolve(options: AgentExecutionOptions, inheritedTools?: readonly string[]): { model: ModelSpec; requestedModel?: string; tools: readonly string[]; systemPromptAppend: string } {
|
|
364
|
+
resolve(options: AgentExecutionOptions, inheritedTools?: readonly string[]): { model: ModelSpec; requestedModel?: string; tools: readonly string[]; systemPrompt?: string; systemPromptAppend: string } {
|
|
234
365
|
const role = options.role;
|
|
235
366
|
const definition = role ? this.root.agentDefinitions?.[role] : undefined;
|
|
236
367
|
if (role && !definition) throw new WorkflowError("UNKNOWN_AGENT_TYPE", `Unknown agent role: ${role}`);
|
|
@@ -246,7 +377,8 @@ export class WorkflowAgentExecutor {
|
|
|
246
377
|
const model = options.modelOverride ?? parseModel(requestedModel, this.root.model, options.thinking ?? (aliasThinking === undefined ? definition?.thinking : undefined), this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
|
|
247
378
|
const availableModels = this.root.knownModels ?? this.root.availableModels ?? new Set([modelCapability(this.root.model)]);
|
|
248
379
|
if (!availableModels.has(modelCapability(model))) throw new WorkflowError("UNKNOWN_MODEL", `Unknown model${requestedModel ? ` ${requestedModel} resolved to ${modelCapability(model)}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
|
|
249
|
-
|
|
380
|
+
const overrideSystemPrompt = definition?.overrideSystemPrompt === true;
|
|
381
|
+
return { model, ...(alias && requestedModel ? { requestedModel } : {}), tools: [...requested], ...(overrideSystemPrompt ? { systemPrompt: definition.prompt ?? "" } : {}), systemPromptAppend: overrideSystemPrompt ? "" : definition?.prompt ?? "" };
|
|
250
382
|
}
|
|
251
383
|
|
|
252
384
|
async execute(task: string, options: AgentExecutionOptions, signal?: AbortSignal, customTools: readonly ToolDefinition[] = [], setSteer?: (handler: (message: string) => void | Promise<void>) => void, beforeRetry?: () => void): Promise<AgentExecutionResult> {
|
|
@@ -277,15 +409,15 @@ export class WorkflowAgentExecutor {
|
|
|
277
409
|
const attempts: AgentAttempt[] = [];
|
|
278
410
|
let maxAttempts = (options.retries ?? 0) + 1;
|
|
279
411
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
412
|
+
const attemptSignal = executionSignal ?? new AbortController().signal;
|
|
280
413
|
if (recoveryModel) resolved = this.resolve({ ...options, modelOverride: recoveryModel });
|
|
281
414
|
options.budget?.beforeAttempt();
|
|
282
415
|
let schemaResult: JsonValue | undefined;
|
|
283
|
-
let session:
|
|
416
|
+
let session: WorkflowAgentSession | undefined;
|
|
284
417
|
let setup: AgentSetup | undefined;
|
|
285
|
-
let setupSummary: AgentSetupSummary
|
|
418
|
+
let setupSummary: AgentSetupSummary = { hookNames: [], model: { ...resolved.model }, tools: [...resolved.tools], cwd };
|
|
286
419
|
let setupFailed = false;
|
|
287
420
|
let budgetError: WorkflowError | undefined;
|
|
288
|
-
let turnStarted = false;
|
|
289
421
|
const hasSchemaResult = () => schemaResult !== undefined;
|
|
290
422
|
const resultTool = options.schema ? {
|
|
291
423
|
name: "workflow_result", label: "Workflow Result", description: "Submit the terminal structured workflow result", parameters: Type.Unsafe(options.schema),
|
|
@@ -293,130 +425,197 @@ export class WorkflowAgentExecutor {
|
|
|
293
425
|
if (!Value.Check(options.schema as object, value)) return { content: [{ type: "text" as const, text: "Result does not match the required schema." }], details: {}, isError: true };
|
|
294
426
|
if (schemaResult !== undefined) return { content: [{ type: "text" as const, text: "Result has already been accepted." }], details: {}, isError: true };
|
|
295
427
|
schemaResult = structuredClone(value) as JsonValue;
|
|
296
|
-
|
|
428
|
+
const currentSession = session;
|
|
429
|
+
if (currentSession) void currentSession.abort();
|
|
297
430
|
return { content: [{ type: "text" as const, text: "Result accepted." }], details: {} };
|
|
298
431
|
},
|
|
299
432
|
} as ToolDefinition : undefined;
|
|
300
433
|
const toolCalls = new Map<string, AgentToolCallProgress>();
|
|
301
434
|
let activity: AgentActivity | undefined;
|
|
435
|
+
let lastEventAt: number | undefined;
|
|
436
|
+
let lastReportedEventAt: number | undefined;
|
|
302
437
|
let progress = Promise.resolve();
|
|
303
438
|
let unsubscribe: (() => void) | undefined;
|
|
304
439
|
let systemPromptTurn = 0;
|
|
305
440
|
let systemPromptWrite = Promise.resolve();
|
|
306
441
|
let systemPromptWriteError: unknown;
|
|
442
|
+
let turnStarted = false;
|
|
443
|
+
let completedAttempt: AgentAttempt | undefined;
|
|
307
444
|
const flushSystemPrompts = async () => {
|
|
308
445
|
await systemPromptWrite;
|
|
309
446
|
if (systemPromptWriteError) throw new WorkflowError("INTERNAL_ERROR", `Failed to persist effective system prompt: ${systemPromptWriteError instanceof Error ? systemPromptWriteError.message : typeof systemPromptWriteError === "string" ? systemPromptWriteError : "unknown error"}`);
|
|
310
447
|
};
|
|
311
448
|
const report = (persist: boolean) => {
|
|
312
449
|
if (!session || !options.onProgress) return;
|
|
313
|
-
const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], ...(activity ? { activity } : {}), persist };
|
|
450
|
+
const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], state: session.getState(), ...(activity ? { activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }), persist };
|
|
451
|
+
if (lastEventAt !== undefined) lastReportedEventAt = lastEventAt;
|
|
314
452
|
progress = progress.then(() => options.onProgress?.(update)).then(() => undefined);
|
|
315
453
|
};
|
|
454
|
+
const reportTimestamp = () => {
|
|
455
|
+
if (lastEventAt !== undefined && lastReportedEventAt !== undefined && lastEventAt - lastReportedEventAt < 1000) return;
|
|
456
|
+
report(false);
|
|
457
|
+
};
|
|
458
|
+
const activityChanged = (previous: AgentActivity | undefined) => previous?.kind !== activity?.kind || previous?.text !== activity?.text;
|
|
316
459
|
try {
|
|
317
460
|
setupFailed = true;
|
|
318
|
-
const prepared = await prepareAgentSetup(this.root, this.
|
|
461
|
+
const prepared = await prepareAgentSetup(this.root, this.transport, task, options, resolved, cwd, attempt, attemptSignal, customTools, resultTool);
|
|
319
462
|
setup = prepared.setup;
|
|
320
463
|
setupSummary = prepared.summary;
|
|
464
|
+
if (prepared.failure) throw prepared.failure.error;
|
|
321
465
|
setupFailed = false;
|
|
322
|
-
if (
|
|
466
|
+
if (attemptSignal.aborted) throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
323
467
|
const started = Date.now();
|
|
324
|
-
|
|
468
|
+
const transportSignal = attemptSignal;
|
|
469
|
+
await options.onAttempt?.({ attempt, transport: setup.transport.id, accounting: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 }, setup: setupSummary });
|
|
470
|
+
const transportBase = fallbackSetupContext(this.root, options, transportSignal);
|
|
471
|
+
const createdSession = await setup.transport.createSession(setup.prepared, Object.freeze({ run: transportBase.run, identity: transportBase.identity, attempt, signal: transportSignal }));
|
|
472
|
+
if (createdSession.reference.transport !== setup.transport.id) {
|
|
473
|
+
await createdSession.dispose();
|
|
474
|
+
throw new WorkflowError("INTERNAL_ERROR", `Agent transport ${setup.transport.id} created a session for ${createdSession.reference.transport}`);
|
|
475
|
+
}
|
|
476
|
+
session = createdSession;
|
|
477
|
+
await options.onAttempt?.({ attempt, transport: setup.transport.id, session: session.reference, liveSession: session, accounting: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 }, setup: setupSummary });
|
|
478
|
+
const preparedTools = new Set([...setup.prepared.tools, ...(setup.prepared.customTools ?? []).map(({ name }) => name), ...(setup.prepared.resultTool ? [setup.prepared.resultTool.name] : [])]);
|
|
479
|
+
if (session.getState().tools.some((tool) => !preparedTools.has(tool))) throw new WorkflowError("INTERNAL_ERROR", `Agent transport ${setup.transport.id} widened the prepared tool policy`);
|
|
325
480
|
if (setup.sessionInput.resourcePolicy) setupSummary = { ...setupSummary, disabledAgentResources: resourcePolicySummary(setup.sessionInput.resourcePolicy) };
|
|
326
|
-
const includeAttemptSetup = Boolean(this.root.agentSetupHooks?.length || setup.sessionInput.resourcePolicy);
|
|
327
|
-
await options.onAttempt?.({ attempt, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), ...(includeAttemptSetup ? { setup: setupSummary } : {}) });
|
|
328
481
|
const activeSession = session;
|
|
329
|
-
|
|
330
|
-
|
|
482
|
+
let lastAssistant: WorkflowAgentMessage | undefined;
|
|
483
|
+
const recoverTerminal = () => recoverTerminalProviderError(activeSession, options.label, options.providerErrorRecovery, async () => { try { lastAssistant = (await promptWithProviderPause(activeSession, providerContinuationPrompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant; } catch (error) { lastAssistant = (activeSession as WorkflowAgentSession & { getLastAssistant?: () => WorkflowAgentMessage | undefined }).getLastAssistant?.() ?? lastAssistant; if (!hasSchemaResult()) throw error; } }, () => lastAssistant);
|
|
484
|
+
const promptAndRecover = async (prompt: string): Promise<void> => {
|
|
485
|
+
let promptFailed = false;
|
|
486
|
+
let promptError: unknown;
|
|
487
|
+
try { lastAssistant = (await promptWithProviderPause(activeSession, prompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant; } catch (error) { lastAssistant = (activeSession as WorkflowAgentSession & { getLastAssistant?: () => WorkflowAgentMessage | undefined }).getLastAssistant?.() ?? lastAssistant; promptFailed = true; promptError = error; }
|
|
488
|
+
const recovered = await recoverTerminal();
|
|
489
|
+
if (promptFailed && !hasSchemaResult() && !recovered) throw promptError;
|
|
490
|
+
};
|
|
491
|
+
unsubscribe = activeSession.subscribe((event) => {
|
|
492
|
+
lastEventAt = Date.now();
|
|
493
|
+
let persist = false;
|
|
494
|
+
let shouldReport = false;
|
|
495
|
+
let removeToolCallId: string | undefined;
|
|
496
|
+
if (event.type === "agent_start" && session?.getState().systemPrompt !== undefined) {
|
|
331
497
|
if (this.root.runStore) {
|
|
332
498
|
systemPromptTurn += 1;
|
|
333
|
-
const entry = { sessionId: session.sessionId, attempt, turn: systemPromptTurn, prompt: session.systemPrompt };
|
|
499
|
+
const entry = { sessionId: session.reference.sessionId, attempt, turn: systemPromptTurn, prompt: session.getState().systemPrompt ?? "" };
|
|
334
500
|
systemPromptWrite = systemPromptWrite.then(() => this.root.runStore?.recordSystemPrompt(entry)).then(() => undefined).catch((error: unknown) => { systemPromptWriteError ??= error; });
|
|
335
501
|
}
|
|
336
502
|
}
|
|
337
|
-
if (event.type === "
|
|
338
|
-
|
|
339
|
-
|
|
503
|
+
if (event.type === "state_changed") { shouldReport = true; persist = true; }
|
|
504
|
+
if (event.type === "turnStarted" || event.type === "turn_started") { if (!turnStarted) { try { options.budget?.beforeTurn(); turnStarted = true; } catch (error) { budgetError ??= error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", error instanceof Error ? error.message : String(error)); void activeSession.abort(); } } }
|
|
505
|
+
if (event.type === "message_start" && event.message?.role === "assistant") {
|
|
506
|
+
if (!turnStarted) { try { options.budget?.beforeTurn(); turnStarted = true; } catch (error) { budgetError ??= error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", error instanceof Error ? error.message : String(error)); void activeSession.abort(); } }
|
|
507
|
+
activity = { kind: "text", text: "responding" };
|
|
508
|
+
shouldReport = true;
|
|
509
|
+
}
|
|
510
|
+
if (event.type === "message_update") {
|
|
511
|
+
const previousActivity = activity;
|
|
512
|
+
const updateType = event.assistantMessageEvent?.type;
|
|
513
|
+
if (updateType && ["thinking_start", "thinking_delta", "thinking_end"].includes(updateType)) activity = { kind: "reasoning", text: "reasoning" };
|
|
514
|
+
else if (updateType && ["text_start", "text_delta", "text_end", "toolcall_start", "toolcall_delta", "toolcall_end"].includes(updateType)) activity = { kind: "text", text: "responding" };
|
|
515
|
+
shouldReport = activityChanged(previousActivity);
|
|
340
516
|
}
|
|
341
517
|
if (event.type === "message_end") {
|
|
518
|
+
const previousActivity = activity;
|
|
342
519
|
activity = undefined;
|
|
343
|
-
|
|
520
|
+
shouldReport = activityChanged(previousActivity);
|
|
521
|
+
if (event.message?.role === "assistant") {
|
|
522
|
+
lastAssistant = event.message;
|
|
344
523
|
const needsMoreWork = hasToolCall(event.message);
|
|
345
524
|
const final = !needsMoreWork || (options.schema !== undefined && hasSchemaResult());
|
|
346
|
-
if (!budgetError) { try { options.budget?.afterTurn(accounting(activeSession.getSessionStats()), final); if (!final) { const instruction = options.budget?.instruction(); if (instruction) void
|
|
525
|
+
if (!budgetError) { try { options.budget?.afterTurn(accounting(activeSession.getSessionStats()), final); if (!final) { const instruction = options.budget?.instruction(); if (instruction) void activeSession.steer(instruction); } } catch (error) { budgetError ??= error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", error instanceof Error ? error.message : String(error)); void activeSession.abort(); } }
|
|
347
526
|
turnStarted = false;
|
|
348
|
-
|
|
527
|
+
persist = true;
|
|
349
528
|
}
|
|
350
529
|
}
|
|
351
|
-
if (event.type === "tool_execution_start") { toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: "running" }); activity = { kind: "tool", text: event.toolName };
|
|
352
|
-
if (event.type === "
|
|
530
|
+
if (event.type === "tool_execution_start" && event.toolCallId && event.toolName) { toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: "running" }); activity = { kind: "tool", text: event.toolName }; shouldReport = true; }
|
|
531
|
+
if (event.type === "tool_execution_update" && event.toolName) { const previousActivity = activity; activity = { kind: "tool", text: event.toolName }; shouldReport = activityChanged(previousActivity); }
|
|
532
|
+
if (event.type === "tool_execution_end" && event.toolCallId && event.toolName) { toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: event.isError ? "failed" : "completed" }); if (activity?.kind === "tool" && activity.text === event.toolName) activity = undefined; shouldReport = true; removeToolCallId = event.toolCallId; }
|
|
533
|
+
if (shouldReport || persist) report(persist); else reportTimestamp();
|
|
534
|
+
if (removeToolCallId) toolCalls.delete(removeToolCallId);
|
|
353
535
|
});
|
|
354
536
|
report(false);
|
|
355
537
|
if (setSteer) {
|
|
356
|
-
|
|
357
|
-
setSteer((message) => session?.steer?.(message));
|
|
538
|
+
setSteer((message) => activeSession.steer(message));
|
|
358
539
|
}
|
|
359
540
|
const context = [`Workflow: ${options.workflowName}`, `Agent: ${options.label}`, options.phase ? `Phase: ${options.phase}` : "", options.parent ? `Parent: ${options.parent}` : "", "You own this task and any direct child agents you create. Return child results to your parent; do not leave descendants running.", attempt > 1 ? `Retry attempt ${String(attempt)}. Previous state: ${options.retryState ?? attempts.at(-1)?.error?.message ?? "failed attempt"}` : ""].filter(Boolean).join("\n");
|
|
360
541
|
const instruction = options.budget?.instruction();
|
|
361
542
|
const promptText = `${context}\n\nTask:\n${setup.prompt}${instruction ? `\n\n${instruction}` : ""}`;
|
|
362
543
|
options.budget?.beforeTurn();
|
|
363
544
|
turnStarted = true;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(session.messages)); turnStarted = false; }
|
|
545
|
+
await promptAndRecover(promptText);
|
|
546
|
+
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(lastAssistant)); turnStarted = false; }
|
|
367
547
|
if (budgetError) throw budgetError;
|
|
368
548
|
if (options.schema) {
|
|
369
549
|
if (!hasSchemaResult()) {
|
|
370
|
-
|
|
550
|
+
options.budget?.beforeTurn();
|
|
551
|
+
turnStarted = true;
|
|
552
|
+
await promptAndRecover("Submit the final result now by calling workflow_result exactly once. Do not return prose.");
|
|
553
|
+
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, true); turnStarted = false; }
|
|
371
554
|
}
|
|
372
|
-
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
373
555
|
if (!hasSchemaResult()) {
|
|
374
|
-
|
|
375
|
-
|
|
556
|
+
options.budget?.beforeTurn();
|
|
557
|
+
turnStarted = true;
|
|
558
|
+
await promptAndRecover("Your result was missing or invalid. Repair it by calling workflow_result exactly once with a schema-valid value.");
|
|
559
|
+
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, true); turnStarted = false; }
|
|
376
560
|
}
|
|
377
561
|
if (schemaResult === undefined) throw new WorkflowError("RESULT_INVALID", "Agent did not submit a valid workflow_result after one repair");
|
|
378
562
|
}
|
|
379
|
-
const value = options.schema ? schemaResult as JsonValue : text(
|
|
563
|
+
const value = options.schema ? schemaResult as JsonValue : text(lastAssistant);
|
|
380
564
|
if (options.worktreeOwner) await this.root.runStore?.snapshotWorktree(options.worktreeOwner);
|
|
381
565
|
report(true);
|
|
382
566
|
await progress;
|
|
383
567
|
await flushSystemPrompts();
|
|
384
|
-
unsubscribe
|
|
568
|
+
unsubscribe();
|
|
385
569
|
const attemptAccounting = accounting(session.getSessionStats());
|
|
386
|
-
|
|
387
|
-
attempts.push(
|
|
388
|
-
session.dispose();
|
|
570
|
+
completedAttempt = attemptRecord(setup.transport.id, attempt, session, setupSummary, attemptAccounting, value);
|
|
571
|
+
attempts.push(completedAttempt);
|
|
572
|
+
try { await options.onAttempt?.(completedAttempt); } finally { await session.dispose(); }
|
|
389
573
|
return { value, attempts, cwd: setupSummary.cwd };
|
|
390
574
|
} catch (error) {
|
|
391
|
-
|
|
575
|
+
if (completedAttempt) throw errorWithAttempts(error, attempts);
|
|
576
|
+
const typed = budgetError ?? (error instanceof WorkflowError ? error : new WorkflowError(attemptSignal.aborted && setupFailed ? "CANCELLED" : "AGENT_FAILED", error instanceof Error ? error.message : String(error)));
|
|
577
|
+
if (!session) {
|
|
578
|
+
const failedAttempt: AgentAttempt = { attempt, transport: setup?.transport.id ?? this.transport.id, accounting: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 }, error: { code: typed.code, message: typed.message }, setup: setupSummary };
|
|
579
|
+
attempts.push(failedAttempt);
|
|
580
|
+
try { await options.onAttempt?.(failedAttempt); } catch (persistenceError) { throw errorWithAttempts(persistenceError, attempts); }
|
|
581
|
+
}
|
|
392
582
|
if (session) {
|
|
393
583
|
report(true);
|
|
394
|
-
await progress;
|
|
584
|
+
await progress.catch(() => undefined);
|
|
395
585
|
try { await flushSystemPrompts(); } catch { /* Preserve the agent failure that prompted this cleanup. */ }
|
|
396
586
|
unsubscribe?.();
|
|
397
587
|
const attemptAccounting = accounting(session.getSessionStats());
|
|
398
588
|
if (!budgetError && typed.code !== "BUDGET_EXHAUSTED") { try { options.budget?.afterTurn(attemptAccounting, true); } catch (budgetFailure) { budgetError ??= budgetFailure instanceof WorkflowError ? budgetFailure : new WorkflowError("BUDGET_EXHAUSTED", budgetFailure instanceof Error ? budgetFailure.message : String(budgetFailure)); } }
|
|
399
|
-
const
|
|
400
|
-
attempts.push(
|
|
401
|
-
|
|
589
|
+
const failedAttempt = attemptRecord(setup?.transport.id ?? this.transport.id, attempt, session, setupSummary, attemptAccounting, undefined, { code: typed.code, message: typed.message });
|
|
590
|
+
attempts.push(failedAttempt);
|
|
591
|
+
try {
|
|
592
|
+
try { await options.onAttempt?.(failedAttempt); } finally { await session.dispose(); }
|
|
593
|
+
} catch (persistenceError) {
|
|
594
|
+
throw errorWithAttempts(persistenceError, attempts);
|
|
595
|
+
}
|
|
402
596
|
}
|
|
403
597
|
if (options.worktreeOwner && typed.code !== "WORKTREE_FAILED") await this.root.runStore?.snapshotWorktree(options.worktreeOwner).catch(() => undefined);
|
|
404
598
|
const terminal = terminalProviderError(typed);
|
|
405
|
-
|
|
406
|
-
|
|
599
|
+
const recoveryState = typed as WorkflowError & ProviderRecoveryMarker;
|
|
600
|
+
let recovery = recoveryState.providerRecovery;
|
|
601
|
+
if (terminal && options.providerErrorRecovery && !recoveryState.providerRecoveryHandled) {
|
|
407
602
|
try { recovery = await options.providerErrorRecovery({ label: options.label, ...terminal }); } catch { throw Object.assign(typed, { attempts }); }
|
|
408
|
-
if (recovery === "retry"
|
|
409
|
-
if (typeof recovery === "object") {
|
|
410
|
-
try {
|
|
411
|
-
const selected = resolveModelReference(recovery.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
|
|
412
|
-
recoveryModel = selected.thinking === undefined && resolved.model.thinking ? { ...selected, thinking: resolved.model.thinking } : selected;
|
|
413
|
-
} catch { throw Object.assign(typed, { attempts }); }
|
|
414
|
-
}
|
|
603
|
+
if (recovery === "retry") {
|
|
415
604
|
maxAttempts += 1;
|
|
416
605
|
beforeRetry?.();
|
|
417
606
|
continue;
|
|
418
607
|
}
|
|
419
608
|
}
|
|
609
|
+
if (recoveryState.providerRecoveryFailed || recovery === "abort") throw Object.assign(typed, { attempts });
|
|
610
|
+
if (typeof recovery === "object" && typeof recovery.model === "string") {
|
|
611
|
+
try {
|
|
612
|
+
const selected = resolveModelReference(recovery.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
|
|
613
|
+
recoveryModel = selected.thinking === undefined && resolved.model.thinking ? { ...selected, thinking: resolved.model.thinking } : selected;
|
|
614
|
+
} catch { throw Object.assign(typed, { attempts }); }
|
|
615
|
+
maxAttempts += 1;
|
|
616
|
+
beforeRetry?.();
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
420
619
|
if (attempt === maxAttempts || setupFailed || typed.code === "CANCELLED" || typed.code === "WORKTREE_FAILED" || typed.code === "RESUME_INCOMPATIBLE") throw Object.assign(typed, { attempts });
|
|
421
620
|
beforeRetry?.();
|
|
422
621
|
}
|
|
@@ -462,6 +661,7 @@ type ScheduledNode = {
|
|
|
462
661
|
id: string;
|
|
463
662
|
runId: string;
|
|
464
663
|
parentId?: string;
|
|
664
|
+
prompt?: string;
|
|
465
665
|
options: Readonly<ScheduledAgentOptions>;
|
|
466
666
|
children: Set<string>;
|
|
467
667
|
collected: boolean;
|
|
@@ -475,7 +675,7 @@ type ScheduledNode = {
|
|
|
475
675
|
};
|
|
476
676
|
|
|
477
677
|
type ScheduledRun = { limit: number; beforeLaunch?: () => void; logical: number; active: number; queue: Array<{ node?: ScheduledNode; start: () => void }> };
|
|
478
|
-
export type OwnershipRecord = { id: string; parentId?: string; label: string; state: ScheduledNode["state"]; options: Readonly<ScheduledAgentOptions> };
|
|
678
|
+
export type OwnershipRecord = { id: string; parentId?: string; prompt?: string; label: string; state: ScheduledNode["state"]; options: Readonly<ScheduledAgentOptions> };
|
|
479
679
|
type OwnershipWriter = (runId: string, ownership: readonly OwnershipRecord[]) => void | Promise<void>;
|
|
480
680
|
|
|
481
681
|
export class FairAgentScheduler {
|
|
@@ -514,7 +714,7 @@ export class FairAgentScheduler {
|
|
|
514
714
|
const id = `${runId}:${String(++this.#nextId)}`;
|
|
515
715
|
let resolveResult: (result: ScheduledAgentResult) => void = () => undefined;
|
|
516
716
|
const promise = new Promise<ScheduledAgentResult>((resolve) => { resolveResult = resolve; });
|
|
517
|
-
const node: ScheduledNode = { id, runId, ...(parentId ? { parentId } : {}), options: effective, children: new Set<string>(), collected: false, state: "queued", controller: new AbortController(), promise, resolve: resolveResult, task: async () => undefined, restored: false };
|
|
717
|
+
const node: ScheduledNode = { id, runId, ...(parentId ? { parentId } : {}), prompt, options: effective, children: new Set<string>(), collected: false, state: "queued", controller: new AbortController(), promise, resolve: resolveResult, task: async () => undefined, restored: false };
|
|
518
718
|
node.task = async () => {
|
|
519
719
|
if (node.controller.signal.aborted) { this.#release(node.runId); return; }
|
|
520
720
|
node.state = "running";
|
|
@@ -630,7 +830,7 @@ export class FairAgentScheduler {
|
|
|
630
830
|
}
|
|
631
831
|
|
|
632
832
|
snapshot(): readonly OwnershipRecord[] {
|
|
633
|
-
return [...this.#nodes.values()].map(({ id, parentId, options, state }) => ({ id, ...(parentId ? { parentId } : {}), label: options.label, state, options }));
|
|
833
|
+
return [...this.#nodes.values()].map(({ id, parentId, prompt, options, state }) => ({ id, ...(parentId ? { parentId } : {}), ...(prompt === undefined ? {} : { prompt }), label: options.label, state, options }));
|
|
634
834
|
}
|
|
635
835
|
|
|
636
836
|
restoreRun(runId: string, limit: number, ownership: readonly OwnershipRecord[], beforeLaunch?: () => void): void {
|
|
@@ -640,7 +840,7 @@ export class FairAgentScheduler {
|
|
|
640
840
|
if (record.id.split(":").slice(0, -1).join(":") !== runId) throw new WorkflowError("RESUME_INCOMPATIBLE", `Persisted agent belongs to another run: ${record.id}`);
|
|
641
841
|
let resolveResult: (result: ScheduledAgentResult) => void = () => undefined;
|
|
642
842
|
const promise = new Promise<ScheduledAgentResult>((resolve) => { resolveResult = resolve; });
|
|
643
|
-
const node: ScheduledNode = { id: record.id, runId, ...(record.parentId ? { parentId: record.parentId } : {}), options: this.#inherit(undefined, record.options), children: new Set(), collected: false, state: record.state, controller: new AbortController(), promise, resolve: resolveResult, task: async () => undefined, restored: true };
|
|
843
|
+
const node: ScheduledNode = { id: record.id, runId, ...(record.parentId ? { parentId: record.parentId } : {}), ...(record.prompt === undefined ? {} : { prompt: record.prompt }), options: this.#inherit(undefined, record.options), children: new Set(), collected: false, state: record.state, controller: new AbortController(), promise, resolve: resolveResult, task: async () => undefined, restored: true };
|
|
644
844
|
this.#nodes.set(node.id, node);
|
|
645
845
|
run.logical += 1;
|
|
646
846
|
this.#nextId = Math.max(this.#nextId, Number(node.id.slice(node.id.lastIndexOf(":") + 1)) || 0);
|
|
@@ -723,11 +923,6 @@ export class FairAgentScheduler {
|
|
|
723
923
|
|
|
724
924
|
function resolvePath(path: string): string { return path.replace(/[\\/]+$/, ""); }
|
|
725
925
|
|
|
726
|
-
function requiredFile(file: string | undefined): string {
|
|
727
|
-
if (!file) throw new WorkflowError("INTERNAL_ERROR", "Workflow agents require persisted native Pi sessions");
|
|
728
|
-
return file;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
926
|
function remaining(timeoutMs: number | null | undefined, started: number): number | null | undefined {
|
|
732
927
|
return timeoutMs === null || timeoutMs === undefined ? timeoutMs : Math.max(1, timeoutMs - (Date.now() - started));
|
|
733
928
|
}
|
|
@@ -738,20 +933,20 @@ function providerLimited(error: unknown): boolean {
|
|
|
738
933
|
return candidate.status === 429 || candidate.code === 429 || candidate.code === "rate_limit_exceeded" || candidate.code === "RATE_LIMITED";
|
|
739
934
|
}
|
|
740
935
|
|
|
741
|
-
async function promptWithProviderPause(session:
|
|
936
|
+
async function promptWithProviderPause(session: WorkflowAgentSession, text: string, timeoutMs: number | null | undefined, signal: AbortSignal | undefined, pause?: () => Promise<void>): Promise<WorkflowAgentTurnResult> {
|
|
742
937
|
for (;;) {
|
|
743
|
-
try { await withTimeout(session.prompt(text), timeoutMs, signal, session);
|
|
938
|
+
try { return await withTimeout(session.prompt(text), timeoutMs, signal, session); }
|
|
744
939
|
catch (error) { if (!pause || !providerLimited(error)) throw error; await pause(); }
|
|
745
940
|
}
|
|
746
941
|
}
|
|
747
942
|
|
|
748
|
-
async function withTimeout(work: Promise<
|
|
943
|
+
async function withTimeout(work: Promise<WorkflowAgentTurnResult>, timeoutMs: number | null | undefined, signal: AbortSignal | undefined, session: WorkflowAgentSession): Promise<WorkflowAgentTurnResult> {
|
|
749
944
|
if (signal?.aborted) throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
750
945
|
let timer: NodeJS.Timeout | undefined;
|
|
751
946
|
let abort: (() => void) | undefined;
|
|
752
947
|
const state = { interrupted: false };
|
|
753
948
|
const timeout = timeoutMs ? new Promise<never>((_, reject) => { timer = setTimeout(() => { state.interrupted = true; reject(new WorkflowError("AGENT_TIMEOUT", "Agent attempt timed out")); }, timeoutMs); }) : new Promise<never>(() => {});
|
|
754
949
|
const cancelled = signal ? new Promise<never>((_, reject) => { abort = () => { state.interrupted = true; reject(new WorkflowError("CANCELLED", "Agent cancelled")); }; signal.addEventListener("abort", abort, { once: true }); }) : new Promise<never>(() => {});
|
|
755
|
-
try { await Promise.race([work, timeout, cancelled]); }
|
|
756
|
-
finally { if (timer) clearTimeout(timer); if (abort) signal?.removeEventListener("abort", abort); if (state.interrupted) await session.abort
|
|
950
|
+
try { return await Promise.race([work, timeout, cancelled]); }
|
|
951
|
+
finally { if (timer) clearTimeout(timer); if (abort) signal?.removeEventListener("abort", abort); if (state.interrupted) await session.abort(); }
|
|
757
952
|
}
|