pi-extensible-workflows 3.3.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 -2
- package/dist/src/agent-execution.d.ts +63 -9
- package/dist/src/agent-execution.js +250 -92
- package/dist/src/cli.js +11 -2
- package/dist/src/doctor-cleanup.js +66 -31
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +29 -13
- package/dist/src/host.d.ts +5 -5
- package/dist/src/host.js +160 -76
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +1 -1
- package/dist/src/persistence.d.ts +4 -8
- package/dist/src/persistence.js +3 -0
- package/dist/src/registry.d.ts +3 -2
- package/dist/src/registry.js +16 -4
- package/dist/src/session-inspector.js +14 -22
- package/dist/src/types.d.ts +123 -60
- package/dist/src/validation.js +20 -5
- package/dist/src/workflow-artifacts.d.ts +1 -0
- package/dist/src/workflow-artifacts.js +1 -0
- package/package.json +1 -1
- package/skills/pi-extensible-workflows/SKILL.md +13 -5
- package/src/agent-execution.ts +213 -90
- package/src/cli.ts +14 -3
- package/src/doctor-cleanup.ts +36 -3
- package/src/execution.ts +27 -15
- package/src/host.ts +136 -62
- package/src/index.ts +3 -3
- package/src/persistence.ts +5 -3
- package/src/registry.ts +14 -5
- package/src/session-inspector.ts +13 -22
- package/src/types.ts +45 -27
- package/src/validation.ts +13 -4
- package/src/workflow-artifacts.ts +1 -0
package/src/agent-execution.ts
CHANGED
|
@@ -6,11 +6,25 @@ import { Value } from "typebox/value";
|
|
|
6
6
|
import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, SettingsManager, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
8
8
|
type AgentMessage = { role: string; content?: unknown; stopReason?: string; errorMessage?: string; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: { total: number } } };
|
|
9
|
-
|
|
10
|
-
|
|
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";
|
|
11
25
|
import { WorkflowError } from "./types.js";
|
|
12
26
|
import type { RunStore } from "./persistence.js";
|
|
13
|
-
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";
|
|
14
28
|
export interface AgentBudgetHooks {
|
|
15
29
|
beforeAttempt(): void;
|
|
16
30
|
beforeTurn(): void;
|
|
@@ -28,7 +42,7 @@ export interface AgentExecutionOptions {
|
|
|
28
42
|
model?: string;
|
|
29
43
|
thinking?: ThinkingLevel;
|
|
30
44
|
onProgress?: (progress: AgentProgress) => void | Promise<void>;
|
|
31
|
-
onAttempt?: (attempt:
|
|
45
|
+
onAttempt?: (attempt: AgentAttempt) => void | Promise<void>;
|
|
32
46
|
providerErrorRecovery?: (failure: AgentProviderFailure) => Promise<AgentProviderRecovery>;
|
|
33
47
|
modelOverride?: ModelSpec;
|
|
34
48
|
tools?: readonly string[];
|
|
@@ -66,8 +80,8 @@ export interface AgentExecutionRoot {
|
|
|
66
80
|
export interface AgentAccounting { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }
|
|
67
81
|
export interface AgentToolCallProgress { id: string; name: string; state: "running" | "completed" | "failed" }
|
|
68
82
|
export interface AgentActivity { kind: "reasoning" | "tool" | "text"; text: string }
|
|
69
|
-
export interface AgentProgress { accounting: AgentAccounting; toolCalls: readonly AgentToolCallProgress[]; activity?: AgentActivity; lastEventAt?: number; persist: boolean }
|
|
70
|
-
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 }
|
|
71
85
|
export interface AgentExecutionResult { value: JsonValue; attempts: readonly AgentAttempt[]; cwd: string }
|
|
72
86
|
|
|
73
87
|
function parseModel(value: string | undefined, fallback: ModelSpec, thinking?: ThinkingLevel, aliases: Readonly<Record<string, string>> = {}, knownModels?: ReadonlySet<string>, settingsPath?: string): ModelSpec {
|
|
@@ -76,8 +90,7 @@ function parseModel(value: string | undefined, fallback: ModelSpec, thinking?: T
|
|
|
76
90
|
return { ...parsed, ...(thinking ? { thinking } : !parsed.thinking && fallback.thinking ? { thinking: fallback.thinking } : {}) };
|
|
77
91
|
}
|
|
78
92
|
|
|
79
|
-
function text(
|
|
80
|
-
const message = [...messages].reverse().find((item) => item.role === "assistant");
|
|
93
|
+
function text(message: WorkflowAgentMessage | undefined): string {
|
|
81
94
|
if (!message || !Array.isArray(message.content)) return "";
|
|
82
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("");
|
|
83
96
|
}
|
|
@@ -86,20 +99,15 @@ function hasToolCall(message: unknown): boolean {
|
|
|
86
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");
|
|
87
100
|
}
|
|
88
101
|
|
|
89
|
-
function latestAssistantHasToolCall(
|
|
90
|
-
const message = [...messages].reverse().find((item) => item.role === "assistant");
|
|
91
|
-
return hasToolCall(message);
|
|
92
|
-
}
|
|
102
|
+
function latestAssistantHasToolCall(message: WorkflowAgentMessage | undefined): boolean { return hasToolCall(message); }
|
|
93
103
|
|
|
94
104
|
type TerminalProviderError = { provider: string; model: string; error: string };
|
|
95
|
-
function throwIfTerminalAssistantError(session:
|
|
96
|
-
const message = [...session.messages].reverse().find((item) => item.role === "assistant");
|
|
105
|
+
function throwIfTerminalAssistantError(session: WorkflowAgentSession, message: WorkflowAgentMessage | undefined): void {
|
|
97
106
|
if (message?.stopReason !== "error") return;
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
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";
|
|
101
109
|
const failure = new WorkflowError("AGENT_FAILED", error);
|
|
102
|
-
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 });
|
|
103
111
|
throw failure;
|
|
104
112
|
}
|
|
105
113
|
function terminalProviderError(error: WorkflowError): TerminalProviderError | undefined {
|
|
@@ -110,10 +118,10 @@ function terminalProviderError(error: WorkflowError): TerminalProviderError | un
|
|
|
110
118
|
}
|
|
111
119
|
type ProviderRecoveryMarker = { providerRecoveryHandled?: boolean; providerRecovery?: AgentProviderRecovery; providerRecoveryFailed?: boolean };
|
|
112
120
|
const providerContinuationPrompt = "The provider error was transient. Continue the task from your current state.";
|
|
113
|
-
async function recoverTerminalProviderError(session:
|
|
121
|
+
async function recoverTerminalProviderError(session: WorkflowAgentSession, label: string, recovery: AgentExecutionOptions["providerErrorRecovery"], continuePrompt: () => Promise<void>, getAssistant: () => WorkflowAgentMessage | undefined): Promise<boolean> {
|
|
114
122
|
let continued = false;
|
|
115
123
|
for (;;) {
|
|
116
|
-
try { throwIfTerminalAssistantError(session,
|
|
124
|
+
try { throwIfTerminalAssistantError(session, getAssistant()); return continued; }
|
|
117
125
|
catch (error) {
|
|
118
126
|
const typed = error instanceof WorkflowError ? error : new WorkflowError("AGENT_FAILED", error instanceof Error ? error.message : String(error));
|
|
119
127
|
const terminal = terminalProviderError(typed);
|
|
@@ -127,7 +135,7 @@ async function recoverTerminalProviderError(session: NativeSession, fallbackMode
|
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
function accounting(stats:
|
|
138
|
+
function accounting(stats: WorkflowAgentSessionStats): AgentAccounting {
|
|
131
139
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
132
140
|
}
|
|
133
141
|
function canonicalSourcePath(path: string): string { try { return realpathSync(path); } catch { return resolve(path); } }
|
|
@@ -139,7 +147,7 @@ function workflowSystemPromptPath(cwd: string, agentDir: string, projectTrusted:
|
|
|
139
147
|
return globalPaths.find((path) => existsSync(path));
|
|
140
148
|
}
|
|
141
149
|
|
|
142
|
-
export async function
|
|
150
|
+
export async function createLocalPiSession(input: SessionInput): Promise<PiSession> {
|
|
143
151
|
const agentDir = input.agentDir ?? getAgentDir();
|
|
144
152
|
const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
|
|
145
153
|
const systemPromptOptions = input.systemPrompt !== undefined ? { systemPromptOverride: () => input.systemPrompt } : systemPromptSource !== undefined ? { systemPrompt: systemPromptSource } : {};
|
|
@@ -194,8 +202,59 @@ export async function createNativeAgentSession(input: SessionInput): Promise<Nat
|
|
|
194
202
|
return Object.assign(session, {
|
|
195
203
|
getLeafId: () => manager.getLeafId(),
|
|
196
204
|
getToolDefinitions: () => session.getAllTools().map(({ name, description, parameters, promptGuidelines }) => ({ name, description, parameters, ...(promptGuidelines ? { promptGuidelines } : {}) })),
|
|
197
|
-
}) 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 }) };
|
|
198
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 });
|
|
199
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]); }
|
|
200
259
|
function validThinking(value: unknown): value is ThinkingLevel { return typeof value === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value); }
|
|
201
260
|
interface ChildAgentToolParams {
|
|
@@ -229,35 +288,77 @@ function fallbackSetupContext(root: AgentExecutionRoot, options: AgentExecutionO
|
|
|
229
288
|
function resourcePolicySummary(policy: AgentResourcePolicy): NonNullable<AgentSetupSummary["disabledAgentResources"]> {
|
|
230
289
|
return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], excludedSkills: [...(policy.excludedSkills ?? [])], excludedExtensions: [...(policy.excludedExtensions ?? [])], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
|
|
231
290
|
}
|
|
232
|
-
|
|
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> {
|
|
233
313
|
const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
|
|
234
314
|
const baselineOptions = structuredClone(options.agentOptions ?? {});
|
|
235
315
|
const baseResourcePolicy = await root.agentResourcePolicy?.();
|
|
236
316
|
const roleExclusions = options.role ? root.agentDefinitions?.[options.role]?.disabledAgentResources : undefined;
|
|
237
317
|
const resourcePolicy = baseResourcePolicy && roleExclusions ? { ...baseResourcePolicy, effective: mergeAgentResourceExclusions(baseResourcePolicy.effective, roleExclusions) } : baseResourcePolicy;
|
|
318
|
+
const resourcePolicyCeiling = resourcePolicy ? structuredClone(resourcePolicy) : undefined;
|
|
238
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) };
|
|
239
|
-
const setup
|
|
320
|
+
const setup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, prepared: preparedAgentSession(sessionInput), transport };
|
|
240
321
|
const base = fallbackSetupContext(root, options, setupSignal);
|
|
241
322
|
const context = Object.freeze({ run: base.run, identity: base.identity, attempt, signal: setupSignal });
|
|
242
323
|
const hookNames: string[] = [];
|
|
243
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))) {
|
|
244
|
-
if (setupSignal.aborted)
|
|
245
|
-
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);
|
|
246
331
|
hookNames.push(hook.name);
|
|
247
|
-
if (setupSignal.reason !== undefined)
|
|
332
|
+
if (setupSignal.reason !== undefined) return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error: new WorkflowError("CANCELLED", "Agent cancelled") } };
|
|
248
333
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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 });
|
|
257
358
|
}
|
|
258
|
-
|
|
259
359
|
export class WorkflowAgentExecutor {
|
|
260
|
-
|
|
360
|
+
private readonly transport: AgentTransport;
|
|
361
|
+
constructor(private readonly root: AgentExecutionRoot, transport: AgentTransport = localAgentTransport) { this.transport = transport; }
|
|
261
362
|
setRunContext(runContext: Readonly<WorkflowRunContext>): void { this.root.runContext = runContext; }
|
|
262
363
|
|
|
263
364
|
resolve(options: AgentExecutionOptions, inheritedTools?: readonly string[]): { model: ModelSpec; requestedModel?: string; tools: readonly string[]; systemPrompt?: string; systemPromptAppend: string } {
|
|
@@ -308,15 +409,15 @@ export class WorkflowAgentExecutor {
|
|
|
308
409
|
const attempts: AgentAttempt[] = [];
|
|
309
410
|
let maxAttempts = (options.retries ?? 0) + 1;
|
|
310
411
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
412
|
+
const attemptSignal = executionSignal ?? new AbortController().signal;
|
|
311
413
|
if (recoveryModel) resolved = this.resolve({ ...options, modelOverride: recoveryModel });
|
|
312
414
|
options.budget?.beforeAttempt();
|
|
313
415
|
let schemaResult: JsonValue | undefined;
|
|
314
|
-
let session:
|
|
416
|
+
let session: WorkflowAgentSession | undefined;
|
|
315
417
|
let setup: AgentSetup | undefined;
|
|
316
|
-
let setupSummary: AgentSetupSummary
|
|
418
|
+
let setupSummary: AgentSetupSummary = { hookNames: [], model: { ...resolved.model }, tools: [...resolved.tools], cwd };
|
|
317
419
|
let setupFailed = false;
|
|
318
420
|
let budgetError: WorkflowError | undefined;
|
|
319
|
-
let turnStarted = false;
|
|
320
421
|
const hasSchemaResult = () => schemaResult !== undefined;
|
|
321
422
|
const resultTool = options.schema ? {
|
|
322
423
|
name: "workflow_result", label: "Workflow Result", description: "Submit the terminal structured workflow result", parameters: Type.Unsafe(options.schema),
|
|
@@ -324,7 +425,8 @@ export class WorkflowAgentExecutor {
|
|
|
324
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 };
|
|
325
426
|
if (schemaResult !== undefined) return { content: [{ type: "text" as const, text: "Result has already been accepted." }], details: {}, isError: true };
|
|
326
427
|
schemaResult = structuredClone(value) as JsonValue;
|
|
327
|
-
|
|
428
|
+
const currentSession = session;
|
|
429
|
+
if (currentSession) void currentSession.abort();
|
|
328
430
|
return { content: [{ type: "text" as const, text: "Result accepted." }], details: {} };
|
|
329
431
|
},
|
|
330
432
|
} as ToolDefinition : undefined;
|
|
@@ -337,13 +439,15 @@ export class WorkflowAgentExecutor {
|
|
|
337
439
|
let systemPromptTurn = 0;
|
|
338
440
|
let systemPromptWrite = Promise.resolve();
|
|
339
441
|
let systemPromptWriteError: unknown;
|
|
442
|
+
let turnStarted = false;
|
|
443
|
+
let completedAttempt: AgentAttempt | undefined;
|
|
340
444
|
const flushSystemPrompts = async () => {
|
|
341
445
|
await systemPromptWrite;
|
|
342
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"}`);
|
|
343
447
|
};
|
|
344
448
|
const report = (persist: boolean) => {
|
|
345
449
|
if (!session || !options.onProgress) return;
|
|
346
|
-
const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], ...(activity ? { activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }), persist };
|
|
450
|
+
const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], state: session.getState(), ...(activity ? { activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }), persist };
|
|
347
451
|
if (lastEventAt !== undefined) lastReportedEventAt = lastEventAt;
|
|
348
452
|
progress = progress.then(() => options.onProgress?.(update)).then(() => undefined);
|
|
349
453
|
};
|
|
@@ -354,71 +458,84 @@ export class WorkflowAgentExecutor {
|
|
|
354
458
|
const activityChanged = (previous: AgentActivity | undefined) => previous?.kind !== activity?.kind || previous?.text !== activity?.text;
|
|
355
459
|
try {
|
|
356
460
|
setupFailed = true;
|
|
357
|
-
const prepared = await prepareAgentSetup(this.root, this.
|
|
461
|
+
const prepared = await prepareAgentSetup(this.root, this.transport, task, options, resolved, cwd, attempt, attemptSignal, customTools, resultTool);
|
|
358
462
|
setup = prepared.setup;
|
|
359
463
|
setupSummary = prepared.summary;
|
|
464
|
+
if (prepared.failure) throw prepared.failure.error;
|
|
360
465
|
setupFailed = false;
|
|
361
|
-
if (
|
|
466
|
+
if (attemptSignal.aborted) throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
362
467
|
const started = Date.now();
|
|
363
|
-
|
|
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`);
|
|
364
480
|
if (setup.sessionInput.resourcePolicy) setupSummary = { ...setupSummary, disabledAgentResources: resourcePolicySummary(setup.sessionInput.resourcePolicy) };
|
|
365
|
-
const includeAttemptSetup = Boolean(this.root.agentSetupHooks?.length || setup.sessionInput.resourcePolicy);
|
|
366
|
-
await options.onAttempt?.({ attempt, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), ...(includeAttemptSetup ? { setup: setupSummary } : {}) });
|
|
367
481
|
const activeSession = session;
|
|
368
|
-
|
|
369
|
-
const recoverTerminal = () => recoverTerminalProviderError(activeSession,
|
|
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);
|
|
370
484
|
const promptAndRecover = async (prompt: string): Promise<void> => {
|
|
371
485
|
let promptFailed = false;
|
|
372
486
|
let promptError: unknown;
|
|
373
|
-
try { await promptWithProviderPause(activeSession, prompt, remaining(options.timeoutMs, started),
|
|
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; }
|
|
374
488
|
const recovered = await recoverTerminal();
|
|
375
489
|
if (promptFailed && !hasSchemaResult() && !recovered) throw promptError;
|
|
376
490
|
};
|
|
377
|
-
unsubscribe = activeSession.subscribe
|
|
491
|
+
unsubscribe = activeSession.subscribe((event) => {
|
|
378
492
|
lastEventAt = Date.now();
|
|
379
493
|
let persist = false;
|
|
380
494
|
let shouldReport = false;
|
|
381
495
|
let removeToolCallId: string | undefined;
|
|
382
|
-
if (event.type === "agent_start" && session?.systemPrompt !== undefined) {
|
|
496
|
+
if (event.type === "agent_start" && session?.getState().systemPrompt !== undefined) {
|
|
383
497
|
if (this.root.runStore) {
|
|
384
498
|
systemPromptTurn += 1;
|
|
385
|
-
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 ?? "" };
|
|
386
500
|
systemPromptWrite = systemPromptWrite.then(() => this.root.runStore?.recordSystemPrompt(entry)).then(() => undefined).catch((error: unknown) => { systemPromptWriteError ??= error; });
|
|
387
501
|
}
|
|
388
502
|
}
|
|
389
|
-
if (event.type === "
|
|
390
|
-
|
|
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(); } }
|
|
391
507
|
activity = { kind: "text", text: "responding" };
|
|
392
508
|
shouldReport = true;
|
|
393
509
|
}
|
|
394
510
|
if (event.type === "message_update") {
|
|
395
511
|
const previousActivity = activity;
|
|
396
|
-
|
|
397
|
-
|
|
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" };
|
|
398
515
|
shouldReport = activityChanged(previousActivity);
|
|
399
516
|
}
|
|
400
517
|
if (event.type === "message_end") {
|
|
401
518
|
const previousActivity = activity;
|
|
402
519
|
activity = undefined;
|
|
403
520
|
shouldReport = activityChanged(previousActivity);
|
|
404
|
-
if (event.message
|
|
521
|
+
if (event.message?.role === "assistant") {
|
|
522
|
+
lastAssistant = event.message;
|
|
405
523
|
const needsMoreWork = hasToolCall(event.message);
|
|
406
524
|
const final = !needsMoreWork || (options.schema !== undefined && hasSchemaResult());
|
|
407
|
-
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(); } }
|
|
408
526
|
turnStarted = false;
|
|
409
527
|
persist = true;
|
|
410
528
|
}
|
|
411
529
|
}
|
|
412
|
-
if (event.type === "tool_execution_start") { toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: "running" }); activity = { kind: "tool", text: event.toolName }; shouldReport = true; }
|
|
413
|
-
if (event.type === "tool_execution_update") { const previousActivity = activity; activity = { kind: "tool", text: event.toolName }; shouldReport = activityChanged(previousActivity); }
|
|
414
|
-
if (event.type === "tool_execution_end") { 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; }
|
|
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; }
|
|
415
533
|
if (shouldReport || persist) report(persist); else reportTimestamp();
|
|
416
534
|
if (removeToolCallId) toolCalls.delete(removeToolCallId);
|
|
417
535
|
});
|
|
418
536
|
report(false);
|
|
419
537
|
if (setSteer) {
|
|
420
|
-
|
|
421
|
-
setSteer((message) => session?.steer?.(message));
|
|
538
|
+
setSteer((message) => activeSession.steer(message));
|
|
422
539
|
}
|
|
423
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");
|
|
424
541
|
const instruction = options.budget?.instruction();
|
|
@@ -426,7 +543,7 @@ export class WorkflowAgentExecutor {
|
|
|
426
543
|
options.budget?.beforeTurn();
|
|
427
544
|
turnStarted = true;
|
|
428
545
|
await promptAndRecover(promptText);
|
|
429
|
-
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(
|
|
546
|
+
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(lastAssistant)); turnStarted = false; }
|
|
430
547
|
if (budgetError) throw budgetError;
|
|
431
548
|
if (options.schema) {
|
|
432
549
|
if (!hasSchemaResult()) {
|
|
@@ -443,29 +560,39 @@ export class WorkflowAgentExecutor {
|
|
|
443
560
|
}
|
|
444
561
|
if (schemaResult === undefined) throw new WorkflowError("RESULT_INVALID", "Agent did not submit a valid workflow_result after one repair");
|
|
445
562
|
}
|
|
446
|
-
const value = options.schema ? schemaResult as JsonValue : text(
|
|
563
|
+
const value = options.schema ? schemaResult as JsonValue : text(lastAssistant);
|
|
447
564
|
if (options.worktreeOwner) await this.root.runStore?.snapshotWorktree(options.worktreeOwner);
|
|
448
565
|
report(true);
|
|
449
566
|
await progress;
|
|
450
567
|
await flushSystemPrompts();
|
|
451
|
-
unsubscribe
|
|
568
|
+
unsubscribe();
|
|
452
569
|
const attemptAccounting = accounting(session.getSessionStats());
|
|
453
|
-
|
|
454
|
-
attempts.push(
|
|
455
|
-
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(); }
|
|
456
573
|
return { value, attempts, cwd: setupSummary.cwd };
|
|
457
574
|
} catch (error) {
|
|
458
|
-
|
|
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
|
+
}
|
|
459
582
|
if (session) {
|
|
460
583
|
report(true);
|
|
461
|
-
await progress;
|
|
584
|
+
await progress.catch(() => undefined);
|
|
462
585
|
try { await flushSystemPrompts(); } catch { /* Preserve the agent failure that prompted this cleanup. */ }
|
|
463
586
|
unsubscribe?.();
|
|
464
587
|
const attemptAccounting = accounting(session.getSessionStats());
|
|
465
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)); } }
|
|
466
|
-
const
|
|
467
|
-
attempts.push(
|
|
468
|
-
|
|
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
|
+
}
|
|
469
596
|
}
|
|
470
597
|
if (options.worktreeOwner && typed.code !== "WORKTREE_FAILED") await this.root.runStore?.snapshotWorktree(options.worktreeOwner).catch(() => undefined);
|
|
471
598
|
const terminal = terminalProviderError(typed);
|
|
@@ -534,6 +661,7 @@ type ScheduledNode = {
|
|
|
534
661
|
id: string;
|
|
535
662
|
runId: string;
|
|
536
663
|
parentId?: string;
|
|
664
|
+
prompt?: string;
|
|
537
665
|
options: Readonly<ScheduledAgentOptions>;
|
|
538
666
|
children: Set<string>;
|
|
539
667
|
collected: boolean;
|
|
@@ -547,7 +675,7 @@ type ScheduledNode = {
|
|
|
547
675
|
};
|
|
548
676
|
|
|
549
677
|
type ScheduledRun = { limit: number; beforeLaunch?: () => void; logical: number; active: number; queue: Array<{ node?: ScheduledNode; start: () => void }> };
|
|
550
|
-
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> };
|
|
551
679
|
type OwnershipWriter = (runId: string, ownership: readonly OwnershipRecord[]) => void | Promise<void>;
|
|
552
680
|
|
|
553
681
|
export class FairAgentScheduler {
|
|
@@ -586,7 +714,7 @@ export class FairAgentScheduler {
|
|
|
586
714
|
const id = `${runId}:${String(++this.#nextId)}`;
|
|
587
715
|
let resolveResult: (result: ScheduledAgentResult) => void = () => undefined;
|
|
588
716
|
const promise = new Promise<ScheduledAgentResult>((resolve) => { resolveResult = resolve; });
|
|
589
|
-
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 };
|
|
590
718
|
node.task = async () => {
|
|
591
719
|
if (node.controller.signal.aborted) { this.#release(node.runId); return; }
|
|
592
720
|
node.state = "running";
|
|
@@ -702,7 +830,7 @@ export class FairAgentScheduler {
|
|
|
702
830
|
}
|
|
703
831
|
|
|
704
832
|
snapshot(): readonly OwnershipRecord[] {
|
|
705
|
-
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 }));
|
|
706
834
|
}
|
|
707
835
|
|
|
708
836
|
restoreRun(runId: string, limit: number, ownership: readonly OwnershipRecord[], beforeLaunch?: () => void): void {
|
|
@@ -712,7 +840,7 @@ export class FairAgentScheduler {
|
|
|
712
840
|
if (record.id.split(":").slice(0, -1).join(":") !== runId) throw new WorkflowError("RESUME_INCOMPATIBLE", `Persisted agent belongs to another run: ${record.id}`);
|
|
713
841
|
let resolveResult: (result: ScheduledAgentResult) => void = () => undefined;
|
|
714
842
|
const promise = new Promise<ScheduledAgentResult>((resolve) => { resolveResult = resolve; });
|
|
715
|
-
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 };
|
|
716
844
|
this.#nodes.set(node.id, node);
|
|
717
845
|
run.logical += 1;
|
|
718
846
|
this.#nextId = Math.max(this.#nextId, Number(node.id.slice(node.id.lastIndexOf(":") + 1)) || 0);
|
|
@@ -795,11 +923,6 @@ export class FairAgentScheduler {
|
|
|
795
923
|
|
|
796
924
|
function resolvePath(path: string): string { return path.replace(/[\\/]+$/, ""); }
|
|
797
925
|
|
|
798
|
-
function requiredFile(file: string | undefined): string {
|
|
799
|
-
if (!file) throw new WorkflowError("INTERNAL_ERROR", "Workflow agents require persisted native Pi sessions");
|
|
800
|
-
return file;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
926
|
function remaining(timeoutMs: number | null | undefined, started: number): number | null | undefined {
|
|
804
927
|
return timeoutMs === null || timeoutMs === undefined ? timeoutMs : Math.max(1, timeoutMs - (Date.now() - started));
|
|
805
928
|
}
|
|
@@ -810,20 +933,20 @@ function providerLimited(error: unknown): boolean {
|
|
|
810
933
|
return candidate.status === 429 || candidate.code === 429 || candidate.code === "rate_limit_exceeded" || candidate.code === "RATE_LIMITED";
|
|
811
934
|
}
|
|
812
935
|
|
|
813
|
-
async function promptWithProviderPause(session:
|
|
936
|
+
async function promptWithProviderPause(session: WorkflowAgentSession, text: string, timeoutMs: number | null | undefined, signal: AbortSignal | undefined, pause?: () => Promise<void>): Promise<WorkflowAgentTurnResult> {
|
|
814
937
|
for (;;) {
|
|
815
|
-
try { await withTimeout(session.prompt(text), timeoutMs, signal, session);
|
|
938
|
+
try { return await withTimeout(session.prompt(text), timeoutMs, signal, session); }
|
|
816
939
|
catch (error) { if (!pause || !providerLimited(error)) throw error; await pause(); }
|
|
817
940
|
}
|
|
818
941
|
}
|
|
819
942
|
|
|
820
|
-
async function withTimeout(work: Promise<
|
|
943
|
+
async function withTimeout(work: Promise<WorkflowAgentTurnResult>, timeoutMs: number | null | undefined, signal: AbortSignal | undefined, session: WorkflowAgentSession): Promise<WorkflowAgentTurnResult> {
|
|
821
944
|
if (signal?.aborted) throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
822
945
|
let timer: NodeJS.Timeout | undefined;
|
|
823
946
|
let abort: (() => void) | undefined;
|
|
824
947
|
const state = { interrupted: false };
|
|
825
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>(() => {});
|
|
826
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>(() => {});
|
|
827
|
-
try { await Promise.race([work, timeout, cancelled]); }
|
|
828
|
-
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(); }
|
|
829
952
|
}
|