pi-extensible-workflows 3.2.0 → 3.3.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 +1 -30
- package/dist/src/agent-execution.d.ts +4 -0
- package/dist/src/agent-execution.js +146 -60
- 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 +146 -21
- package/dist/src/doctor-cleanup.js +4 -2
- package/dist/src/eval-capture-extension.js +16 -1
- package/dist/src/host.d.ts +9 -6
- package/dist/src/host.js +375 -129
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +2 -1
- package/dist/src/persistence.d.ts +40 -1
- package/dist/src/persistence.js +51 -0
- package/dist/src/session-inspector.d.ts +12 -2
- package/dist/src/session-inspector.js +36 -2
- package/dist/src/types.d.ts +18 -0
- package/dist/src/types.js +1 -0
- package/dist/src/validation.js +8 -2
- 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 +2 -2
- package/src/agent-execution.ts +102 -30
- package/src/bundles.ts +471 -0
- package/src/cli.ts +118 -21
- package/src/doctor-cleanup.ts +3 -2
- package/src/eval-capture-extension.ts +15 -1
- package/src/host.ts +329 -124
- package/src/index.ts +3 -2
- package/src/persistence.ts +53 -1
- package/src/session-inspector.ts +36 -4
- package/src/types.ts +12 -5
- package/src/validation.ts +6 -2
- package/src/workflow-evals.ts +24 -3
package/src/agent-execution.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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";
|
|
@@ -16,7 +17,7 @@ export interface AgentBudgetHooks {
|
|
|
16
17
|
afterTurn(accounting: AgentAccounting, final: boolean): void;
|
|
17
18
|
instruction(): string | undefined;
|
|
18
19
|
}
|
|
19
|
-
export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: ThinkingLevel; tools?: readonly string[]; disabledAgentResources?: AgentResourceExclusions }
|
|
20
|
+
export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: ThinkingLevel; tools?: readonly string[]; overrideSystemPrompt?: boolean; disabledAgentResources?: AgentResourceExclusions }
|
|
20
21
|
export interface AgentProviderFailure { label: string; provider: string; model: string; error: string }
|
|
21
22
|
export type AgentProviderRecovery = "retry" | "abort" | { model: string };
|
|
22
23
|
export interface AgentExecutionOptions {
|
|
@@ -49,6 +50,7 @@ export interface AgentExecutionRoot {
|
|
|
49
50
|
tools: ReadonlySet<string>;
|
|
50
51
|
agentDefinitions?: Readonly<Record<string, AgentDefinition>>;
|
|
51
52
|
agentDir?: string;
|
|
53
|
+
additionalSkillPaths?: readonly string[];
|
|
52
54
|
availableModels?: ReadonlySet<string>;
|
|
53
55
|
knownModels?: ReadonlySet<string>;
|
|
54
56
|
modelAliases?: Readonly<Record<string, string>>;
|
|
@@ -64,7 +66,7 @@ export interface AgentExecutionRoot {
|
|
|
64
66
|
export interface AgentAccounting { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }
|
|
65
67
|
export interface AgentToolCallProgress { id: string; name: string; state: "running" | "completed" | "failed" }
|
|
66
68
|
export interface AgentActivity { kind: "reasoning" | "tool" | "text"; text: string }
|
|
67
|
-
export interface AgentProgress { accounting: AgentAccounting; toolCalls: readonly AgentToolCallProgress[]; activity?: AgentActivity; persist: boolean }
|
|
69
|
+
export interface AgentProgress { accounting: AgentAccounting; toolCalls: readonly AgentToolCallProgress[]; activity?: AgentActivity; lastEventAt?: number; persist: boolean }
|
|
68
70
|
export interface AgentAttempt { attempt: number; sessionId: string; sessionFile: string; result?: JsonValue; error?: { code: string; message: string }; accounting: AgentAccounting; setup?: AgentSetupSummary }
|
|
69
71
|
export interface AgentExecutionResult { value: JsonValue; attempts: readonly AgentAttempt[]; cwd: string }
|
|
70
72
|
|
|
@@ -106,14 +108,41 @@ function terminalProviderError(error: WorkflowError): TerminalProviderError | un
|
|
|
106
108
|
const candidate = value as Partial<TerminalProviderError>;
|
|
107
109
|
return typeof candidate.provider === "string" && typeof candidate.model === "string" && typeof candidate.error === "string" ? { provider: candidate.provider, model: candidate.model, error: candidate.error } : undefined;
|
|
108
110
|
}
|
|
111
|
+
type ProviderRecoveryMarker = { providerRecoveryHandled?: boolean; providerRecovery?: AgentProviderRecovery; providerRecoveryFailed?: boolean };
|
|
112
|
+
const providerContinuationPrompt = "The provider error was transient. Continue the task from your current state.";
|
|
113
|
+
async function recoverTerminalProviderError(session: NativeSession, fallbackModel: ModelSpec, label: string, recovery: AgentExecutionOptions["providerErrorRecovery"], continuePrompt: () => Promise<void>): Promise<boolean> {
|
|
114
|
+
let continued = false;
|
|
115
|
+
for (;;) {
|
|
116
|
+
try { throwIfTerminalAssistantError(session, fallbackModel); return continued; }
|
|
117
|
+
catch (error) {
|
|
118
|
+
const typed = error instanceof WorkflowError ? error : new WorkflowError("AGENT_FAILED", error instanceof Error ? error.message : String(error));
|
|
119
|
+
const terminal = terminalProviderError(typed);
|
|
120
|
+
if (!terminal || !recovery) throw error;
|
|
121
|
+
let action: AgentProviderRecovery;
|
|
122
|
+
try { action = await recovery({ label, ...terminal }); } catch { Object.assign(typed, { providerRecoveryHandled: true, providerRecoveryFailed: true }); throw typed; }
|
|
123
|
+
if (action === "retry") { continued = true; await continuePrompt(); continue; }
|
|
124
|
+
Object.assign(typed, { providerRecoveryHandled: true, providerRecovery: action });
|
|
125
|
+
throw typed;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
109
129
|
|
|
110
130
|
function accounting(stats: ReturnType<NativeSession["getSessionStats"]>): AgentAccounting {
|
|
111
131
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
112
132
|
}
|
|
113
133
|
function canonicalSourcePath(path: string): string { try { return realpathSync(path); } catch { return resolve(path); } }
|
|
134
|
+
const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
|
|
135
|
+
function workflowSystemPromptPath(cwd: string, agentDir: string, projectTrusted: boolean): string | undefined {
|
|
136
|
+
const projectPath = join(cwd, ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md");
|
|
137
|
+
if (projectTrusted && existsSync(projectPath)) return projectPath;
|
|
138
|
+
const globalPaths = [join(homedir(), ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md"), join(agentDir, WORKFLOW_DIRECTORY, "SYSTEM.md")];
|
|
139
|
+
return globalPaths.find((path) => existsSync(path));
|
|
140
|
+
}
|
|
114
141
|
|
|
115
142
|
export async function createNativeAgentSession(input: SessionInput): Promise<NativeSession> {
|
|
116
143
|
const agentDir = input.agentDir ?? getAgentDir();
|
|
144
|
+
const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
|
|
145
|
+
const systemPromptOptions = input.systemPrompt !== undefined ? { systemPromptOverride: () => input.systemPrompt } : systemPromptSource !== undefined ? { systemPrompt: systemPromptSource } : {};
|
|
117
146
|
const manager = input.agentDir ? SessionManager.create(input.cwd, join(agentDir, "sessions")) : SessionManager.create(input.cwd);
|
|
118
147
|
manager.appendSessionInfo(input.sessionLabel);
|
|
119
148
|
const modelRuntime = await ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json") });
|
|
@@ -147,17 +176,18 @@ export async function createNativeAgentSession(input: SessionInput): Promise<Nat
|
|
|
147
176
|
noExtensions: true,
|
|
148
177
|
additionalExtensionPaths: extensionPaths,
|
|
149
178
|
noSkills: true,
|
|
150
|
-
additionalSkillPaths: skillPaths,
|
|
179
|
+
additionalSkillPaths: [...new Set([...skillPaths, ...(input.additionalSkillPaths ?? [])])],
|
|
151
180
|
...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
|
|
152
181
|
skillsOverride: (base) => {
|
|
153
182
|
const disabledSkills = updateSkillMatches(base.skills);
|
|
154
183
|
return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
|
|
155
184
|
},
|
|
156
185
|
...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
|
|
186
|
+
...systemPromptOptions,
|
|
157
187
|
});
|
|
158
188
|
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 ?? ""] } : {}) });
|
|
189
|
+
} else if (input.systemPrompt !== undefined || systemPromptSource !== undefined || input.systemPromptAppend || input.extensionFactories?.length || input.additionalSkillPaths?.length) {
|
|
190
|
+
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
191
|
await resourceLoader.reload();
|
|
162
192
|
}
|
|
163
193
|
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 });
|
|
@@ -199,13 +229,13 @@ function fallbackSetupContext(root: AgentExecutionRoot, options: AgentExecutionO
|
|
|
199
229
|
function resourcePolicySummary(policy: AgentResourcePolicy): NonNullable<AgentSetupSummary["disabledAgentResources"]> {
|
|
200
230
|
return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], excludedSkills: [...(policy.excludedSkills ?? [])], excludedExtensions: [...(policy.excludedExtensions ?? [])], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
|
|
201
231
|
}
|
|
202
|
-
async function prepareAgentSetup(root: AgentExecutionRoot, createSession: SessionFactory, task: string, options: AgentExecutionOptions, resolved: { model: ModelSpec; tools: readonly string[]; systemPromptAppend: string }, cwd: string, attempt: number, signal: AbortSignal | undefined, customTools: readonly ToolDefinition[], resultTool: ToolDefinition | undefined): Promise<{ setup: AgentSetup; summary: AgentSetupSummary }> {
|
|
232
|
+
async function prepareAgentSetup(root: AgentExecutionRoot, createSession: SessionFactory, 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<{ setup: AgentSetup; summary: AgentSetupSummary }> {
|
|
203
233
|
const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
|
|
204
234
|
const baselineOptions = structuredClone(options.agentOptions ?? {});
|
|
205
235
|
const baseResourcePolicy = await root.agentResourcePolicy?.();
|
|
206
236
|
const roleExclusions = options.role ? root.agentDefinitions?.[options.role]?.disabledAgentResources : undefined;
|
|
207
237
|
const resourcePolicy = baseResourcePolicy && roleExclusions ? { ...baseResourcePolicy, effective: mergeAgentResourceExclusions(baseResourcePolicy.effective, roleExclusions) } : baseResourcePolicy;
|
|
208
|
-
const sessionInput: SessionInput = { cwd, model: { ...resolved.model }, tools: [...resolved.tools], sessionLabel: `${options.workflowName}:${options.label}:attempt-${String(attempt)}`, ...(root.agentDir ? { agentDir: root.agentDir } : {}), ...(customTools.length ? { customTools: [...customTools] } : {}), ...(resultTool ? { resultTool } : {}), systemPromptAppend: resolved.systemPromptAppend, ...(resourcePolicy ? { resourcePolicy } : {}), options: structuredClone(baselineOptions) };
|
|
238
|
+
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) };
|
|
209
239
|
const setup: AgentSetup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, createSession };
|
|
210
240
|
const base = fallbackSetupContext(root, options, setupSignal);
|
|
211
241
|
const context = Object.freeze({ run: base.run, identity: base.identity, attempt, signal: setupSignal });
|
|
@@ -230,7 +260,7 @@ export class WorkflowAgentExecutor {
|
|
|
230
260
|
constructor(private readonly root: AgentExecutionRoot, private readonly createSession: SessionFactory = createNativeAgentSession) {}
|
|
231
261
|
setRunContext(runContext: Readonly<WorkflowRunContext>): void { this.root.runContext = runContext; }
|
|
232
262
|
|
|
233
|
-
resolve(options: AgentExecutionOptions, inheritedTools?: readonly string[]): { model: ModelSpec; requestedModel?: string; tools: readonly string[]; systemPromptAppend: string } {
|
|
263
|
+
resolve(options: AgentExecutionOptions, inheritedTools?: readonly string[]): { model: ModelSpec; requestedModel?: string; tools: readonly string[]; systemPrompt?: string; systemPromptAppend: string } {
|
|
234
264
|
const role = options.role;
|
|
235
265
|
const definition = role ? this.root.agentDefinitions?.[role] : undefined;
|
|
236
266
|
if (role && !definition) throw new WorkflowError("UNKNOWN_AGENT_TYPE", `Unknown agent role: ${role}`);
|
|
@@ -246,7 +276,8 @@ export class WorkflowAgentExecutor {
|
|
|
246
276
|
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
277
|
const availableModels = this.root.knownModels ?? this.root.availableModels ?? new Set([modelCapability(this.root.model)]);
|
|
248
278
|
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
|
-
|
|
279
|
+
const overrideSystemPrompt = definition?.overrideSystemPrompt === true;
|
|
280
|
+
return { model, ...(alias && requestedModel ? { requestedModel } : {}), tools: [...requested], ...(overrideSystemPrompt ? { systemPrompt: definition.prompt ?? "" } : {}), systemPromptAppend: overrideSystemPrompt ? "" : definition?.prompt ?? "" };
|
|
250
281
|
}
|
|
251
282
|
|
|
252
283
|
async execute(task: string, options: AgentExecutionOptions, signal?: AbortSignal, customTools: readonly ToolDefinition[] = [], setSteer?: (handler: (message: string) => void | Promise<void>) => void, beforeRetry?: () => void): Promise<AgentExecutionResult> {
|
|
@@ -299,6 +330,8 @@ export class WorkflowAgentExecutor {
|
|
|
299
330
|
} as ToolDefinition : undefined;
|
|
300
331
|
const toolCalls = new Map<string, AgentToolCallProgress>();
|
|
301
332
|
let activity: AgentActivity | undefined;
|
|
333
|
+
let lastEventAt: number | undefined;
|
|
334
|
+
let lastReportedEventAt: number | undefined;
|
|
302
335
|
let progress = Promise.resolve();
|
|
303
336
|
let unsubscribe: (() => void) | undefined;
|
|
304
337
|
let systemPromptTurn = 0;
|
|
@@ -310,9 +343,15 @@ export class WorkflowAgentExecutor {
|
|
|
310
343
|
};
|
|
311
344
|
const report = (persist: boolean) => {
|
|
312
345
|
if (!session || !options.onProgress) return;
|
|
313
|
-
const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], ...(activity ? { activity } : {}), persist };
|
|
346
|
+
const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], ...(activity ? { activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }), persist };
|
|
347
|
+
if (lastEventAt !== undefined) lastReportedEventAt = lastEventAt;
|
|
314
348
|
progress = progress.then(() => options.onProgress?.(update)).then(() => undefined);
|
|
315
349
|
};
|
|
350
|
+
const reportTimestamp = () => {
|
|
351
|
+
if (lastEventAt !== undefined && lastReportedEventAt !== undefined && lastEventAt - lastReportedEventAt < 1000) return;
|
|
352
|
+
report(false);
|
|
353
|
+
};
|
|
354
|
+
const activityChanged = (previous: AgentActivity | undefined) => previous?.kind !== activity?.kind || previous?.text !== activity?.text;
|
|
316
355
|
try {
|
|
317
356
|
setupFailed = true;
|
|
318
357
|
const prepared = await prepareAgentSetup(this.root, this.createSession, task, options, resolved, cwd, attempt, executionSignal, customTools, resultTool);
|
|
@@ -326,7 +365,20 @@ export class WorkflowAgentExecutor {
|
|
|
326
365
|
const includeAttemptSetup = Boolean(this.root.agentSetupHooks?.length || setup.sessionInput.resourcePolicy);
|
|
327
366
|
await options.onAttempt?.({ attempt, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), ...(includeAttemptSetup ? { setup: setupSummary } : {}) });
|
|
328
367
|
const activeSession = session;
|
|
368
|
+
const sessionModel = setup.sessionInput.model;
|
|
369
|
+
const recoverTerminal = () => recoverTerminalProviderError(activeSession, sessionModel, options.label, options.providerErrorRecovery, async () => { try { await promptWithProviderPause(activeSession, providerContinuationPrompt, remaining(options.timeoutMs, started), executionSignal, this.root.providerPause); } catch (error) { if (!hasSchemaResult()) throw error; } });
|
|
370
|
+
const promptAndRecover = async (prompt: string): Promise<void> => {
|
|
371
|
+
let promptFailed = false;
|
|
372
|
+
let promptError: unknown;
|
|
373
|
+
try { await promptWithProviderPause(activeSession, prompt, remaining(options.timeoutMs, started), executionSignal, this.root.providerPause); } catch (error) { promptFailed = true; promptError = error; }
|
|
374
|
+
const recovered = await recoverTerminal();
|
|
375
|
+
if (promptFailed && !hasSchemaResult() && !recovered) throw promptError;
|
|
376
|
+
};
|
|
329
377
|
unsubscribe = activeSession.subscribe?.((event) => {
|
|
378
|
+
lastEventAt = Date.now();
|
|
379
|
+
let persist = false;
|
|
380
|
+
let shouldReport = false;
|
|
381
|
+
let removeToolCallId: string | undefined;
|
|
330
382
|
if (event.type === "agent_start" && session?.systemPrompt !== undefined) {
|
|
331
383
|
if (this.root.runStore) {
|
|
332
384
|
systemPromptTurn += 1;
|
|
@@ -336,20 +388,32 @@ export class WorkflowAgentExecutor {
|
|
|
336
388
|
}
|
|
337
389
|
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
338
390
|
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 session?.abort?.(); } }
|
|
339
|
-
activity = { kind: "text", text: "responding" };
|
|
391
|
+
activity = { kind: "text", text: "responding" };
|
|
392
|
+
shouldReport = true;
|
|
393
|
+
}
|
|
394
|
+
if (event.type === "message_update") {
|
|
395
|
+
const previousActivity = activity;
|
|
396
|
+
if (["thinking_start", "thinking_delta", "thinking_end"].includes(event.assistantMessageEvent.type)) activity = { kind: "reasoning", text: "reasoning" };
|
|
397
|
+
else if (["text_start", "text_delta", "text_end", "toolcall_start", "toolcall_delta", "toolcall_end"].includes(event.assistantMessageEvent.type)) activity = { kind: "text", text: "responding" };
|
|
398
|
+
shouldReport = activityChanged(previousActivity);
|
|
340
399
|
}
|
|
341
400
|
if (event.type === "message_end") {
|
|
401
|
+
const previousActivity = activity;
|
|
342
402
|
activity = undefined;
|
|
403
|
+
shouldReport = activityChanged(previousActivity);
|
|
343
404
|
if (event.message.role === "assistant") {
|
|
344
405
|
const needsMoreWork = hasToolCall(event.message);
|
|
345
406
|
const final = !needsMoreWork || (options.schema !== undefined && hasSchemaResult());
|
|
346
407
|
if (!budgetError) { try { options.budget?.afterTurn(accounting(activeSession.getSessionStats()), final); if (!final) { const instruction = options.budget?.instruction(); if (instruction) void session?.steer?.(instruction); } } catch (error) { budgetError ??= error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", error instanceof Error ? error.message : String(error)); void session?.abort?.(); } }
|
|
347
408
|
turnStarted = false;
|
|
348
|
-
|
|
409
|
+
persist = true;
|
|
349
410
|
}
|
|
350
411
|
}
|
|
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 === "
|
|
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; }
|
|
415
|
+
if (shouldReport || persist) report(persist); else reportTimestamp();
|
|
416
|
+
if (removeToolCallId) toolCalls.delete(removeToolCallId);
|
|
353
417
|
});
|
|
354
418
|
report(false);
|
|
355
419
|
if (setSteer) {
|
|
@@ -361,18 +425,21 @@ export class WorkflowAgentExecutor {
|
|
|
361
425
|
const promptText = `${context}\n\nTask:\n${setup.prompt}${instruction ? `\n\n${instruction}` : ""}`;
|
|
362
426
|
options.budget?.beforeTurn();
|
|
363
427
|
turnStarted = true;
|
|
364
|
-
|
|
365
|
-
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
428
|
+
await promptAndRecover(promptText);
|
|
366
429
|
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(session.messages)); turnStarted = false; }
|
|
367
430
|
if (budgetError) throw budgetError;
|
|
368
431
|
if (options.schema) {
|
|
369
432
|
if (!hasSchemaResult()) {
|
|
370
|
-
|
|
433
|
+
options.budget?.beforeTurn();
|
|
434
|
+
turnStarted = true;
|
|
435
|
+
await promptAndRecover("Submit the final result now by calling workflow_result exactly once. Do not return prose.");
|
|
436
|
+
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, true); turnStarted = false; }
|
|
371
437
|
}
|
|
372
|
-
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
373
438
|
if (!hasSchemaResult()) {
|
|
374
|
-
|
|
375
|
-
|
|
439
|
+
options.budget?.beforeTurn();
|
|
440
|
+
turnStarted = true;
|
|
441
|
+
await promptAndRecover("Your result was missing or invalid. Repair it by calling workflow_result exactly once with a schema-valid value.");
|
|
442
|
+
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, true); turnStarted = false; }
|
|
376
443
|
}
|
|
377
444
|
if (schemaResult === undefined) throw new WorkflowError("RESULT_INVALID", "Agent did not submit a valid workflow_result after one repair");
|
|
378
445
|
}
|
|
@@ -402,21 +469,26 @@ export class WorkflowAgentExecutor {
|
|
|
402
469
|
}
|
|
403
470
|
if (options.worktreeOwner && typed.code !== "WORKTREE_FAILED") await this.root.runStore?.snapshotWorktree(options.worktreeOwner).catch(() => undefined);
|
|
404
471
|
const terminal = terminalProviderError(typed);
|
|
405
|
-
|
|
406
|
-
|
|
472
|
+
const recoveryState = typed as WorkflowError & ProviderRecoveryMarker;
|
|
473
|
+
let recovery = recoveryState.providerRecovery;
|
|
474
|
+
if (terminal && options.providerErrorRecovery && !recoveryState.providerRecoveryHandled) {
|
|
407
475
|
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
|
-
}
|
|
476
|
+
if (recovery === "retry") {
|
|
415
477
|
maxAttempts += 1;
|
|
416
478
|
beforeRetry?.();
|
|
417
479
|
continue;
|
|
418
480
|
}
|
|
419
481
|
}
|
|
482
|
+
if (recoveryState.providerRecoveryFailed || recovery === "abort") throw Object.assign(typed, { attempts });
|
|
483
|
+
if (typeof recovery === "object" && typeof recovery.model === "string") {
|
|
484
|
+
try {
|
|
485
|
+
const selected = resolveModelReference(recovery.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
|
|
486
|
+
recoveryModel = selected.thinking === undefined && resolved.model.thinking ? { ...selected, thinking: resolved.model.thinking } : selected;
|
|
487
|
+
} catch { throw Object.assign(typed, { attempts }); }
|
|
488
|
+
maxAttempts += 1;
|
|
489
|
+
beforeRetry?.();
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
420
492
|
if (attempt === maxAttempts || setupFailed || typed.code === "CANCELLED" || typed.code === "WORKTREE_FAILED" || typed.code === "RESUME_INCOMPATIBLE") throw Object.assign(typed, { attempts });
|
|
421
493
|
beforeRetry?.();
|
|
422
494
|
}
|