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.
Files changed (47) hide show
  1. package/README.md +2 -31
  2. package/dist/src/agent-execution.d.ts +67 -9
  3. package/dist/src/agent-execution.js +385 -141
  4. package/dist/src/bundles.d.ts +53 -0
  5. package/dist/src/bundles.js +457 -0
  6. package/dist/src/cli.d.ts +3 -1
  7. package/dist/src/cli.js +156 -22
  8. package/dist/src/doctor-cleanup.js +68 -31
  9. package/dist/src/eval-capture-extension.js +16 -1
  10. package/dist/src/execution.d.ts +1 -1
  11. package/dist/src/execution.js +29 -13
  12. package/dist/src/host.d.ts +12 -9
  13. package/dist/src/host.js +525 -195
  14. package/dist/src/index.d.ts +5 -4
  15. package/dist/src/index.js +3 -2
  16. package/dist/src/persistence.d.ts +43 -8
  17. package/dist/src/persistence.js +54 -0
  18. package/dist/src/registry.d.ts +3 -2
  19. package/dist/src/registry.js +16 -4
  20. package/dist/src/session-inspector.d.ts +12 -2
  21. package/dist/src/session-inspector.js +50 -24
  22. package/dist/src/types.d.ts +141 -60
  23. package/dist/src/types.js +1 -0
  24. package/dist/src/validation.js +28 -7
  25. package/dist/src/workflow-artifacts.d.ts +1 -0
  26. package/dist/src/workflow-artifacts.js +1 -0
  27. package/dist/src/workflow-evals.d.ts +7 -1
  28. package/dist/src/workflow-evals.js +23 -3
  29. package/evals/cases/recovery-completed-worktree.yaml +17 -0
  30. package/evals/cases/recovery-failed-run.yaml +14 -0
  31. package/package.json +1 -1
  32. package/skills/pi-extensible-workflows/SKILL.md +13 -5
  33. package/src/agent-execution.ts +302 -107
  34. package/src/bundles.ts +471 -0
  35. package/src/cli.ts +130 -22
  36. package/src/doctor-cleanup.ts +38 -4
  37. package/src/eval-capture-extension.ts +15 -1
  38. package/src/execution.ts +27 -15
  39. package/src/host.ts +454 -175
  40. package/src/index.ts +5 -4
  41. package/src/persistence.ts +58 -4
  42. package/src/registry.ts +14 -5
  43. package/src/session-inspector.ts +49 -26
  44. package/src/types.ts +55 -30
  45. package/src/validation.ts +19 -6
  46. package/src/workflow-artifacts.ts +1 -0
  47. package/src/workflow-evals.ts +24 -3
@@ -1,9 +1,10 @@
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 } from "@earendil-works/pi-coding-agent";
6
- import { jsonObject, disabledResources, mergeAgentResourceExclusions, modelAliasName, modelCapability, resolveModelReference, unmatchedResourcePatterns } from "./utils.js";
7
+ import { deepFreeze, jsonObject, disabledResources, mergeAgentResourceExclusions, modelAliasName, modelCapability, resolveModelReference, unmatchedResourcePatterns } from "./utils.js";
7
8
  import { WorkflowError } from "./types.js";
8
9
  function parseModel(value, fallback, thinking, aliases = {}, knownModels, settingsPath) {
9
10
  if (!value)
@@ -11,8 +12,7 @@ function parseModel(value, fallback, thinking, aliases = {}, knownModels, settin
11
12
  const parsed = resolveModelReference(value, aliases, knownModels, settingsPath);
12
13
  return { ...parsed, ...(thinking ? { thinking } : !parsed.thinking && fallback.thinking ? { thinking: fallback.thinking } : {}) };
13
14
  }
14
- function text(messages) {
15
- const message = [...messages].reverse().find((item) => item.role === "assistant");
15
+ function text(message) {
16
16
  if (!message || !Array.isArray(message.content))
17
17
  return "";
18
18
  return message.content.filter((part) => typeof part === "object" && part !== null && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string").map((part) => part.text).join("");
@@ -20,19 +20,14 @@ function text(messages) {
20
20
  function hasToolCall(message) {
21
21
  return typeof message === "object" && message !== null && Array.isArray(message.content) && message.content.some((part) => typeof part === "object" && part !== null && part.type === "toolCall");
22
22
  }
23
- function latestAssistantHasToolCall(messages) {
24
- const message = [...messages].reverse().find((item) => item.role === "assistant");
25
- return hasToolCall(message);
26
- }
27
- function throwIfTerminalAssistantError(session, fallbackModel) {
28
- const message = [...session.messages].reverse().find((item) => item.role === "assistant");
23
+ function latestAssistantHasToolCall(message) { return hasToolCall(message); }
24
+ function throwIfTerminalAssistantError(session, message) {
29
25
  if (message?.stopReason !== "error")
30
26
  return;
31
- const provider = session.model?.provider ?? fallbackModel.provider;
32
- const model = session.model?.model ?? session.model?.id ?? fallbackModel.model;
33
- const error = message.errorMessage ?? "Native Pi assistant ended with a terminal provider error";
27
+ const state = session.getState();
28
+ const error = message.errorMessage ?? "Workflow agent session ended with a terminal provider error";
34
29
  const failure = new WorkflowError("AGENT_FAILED", error);
35
- Object.defineProperty(failure, "terminalProviderError", { value: { provider, model, error }, configurable: true });
30
+ Object.defineProperty(failure, "terminalProviderError", { value: { provider: state.model.provider, model: state.model.model, error }, configurable: true });
36
31
  throw failure;
37
32
  }
38
33
  function terminalProviderError(error) {
@@ -42,6 +37,37 @@ function terminalProviderError(error) {
42
37
  const candidate = value;
43
38
  return typeof candidate.provider === "string" && typeof candidate.model === "string" && typeof candidate.error === "string" ? { provider: candidate.provider, model: candidate.model, error: candidate.error } : undefined;
44
39
  }
40
+ const providerContinuationPrompt = "The provider error was transient. Continue the task from your current state.";
41
+ async function recoverTerminalProviderError(session, label, recovery, continuePrompt, getAssistant) {
42
+ let continued = false;
43
+ for (;;) {
44
+ try {
45
+ throwIfTerminalAssistantError(session, getAssistant());
46
+ return continued;
47
+ }
48
+ catch (error) {
49
+ const typed = error instanceof WorkflowError ? error : new WorkflowError("AGENT_FAILED", error instanceof Error ? error.message : String(error));
50
+ const terminal = terminalProviderError(typed);
51
+ if (!terminal || !recovery)
52
+ throw error;
53
+ let action;
54
+ try {
55
+ action = await recovery({ label, ...terminal });
56
+ }
57
+ catch {
58
+ Object.assign(typed, { providerRecoveryHandled: true, providerRecoveryFailed: true });
59
+ throw typed;
60
+ }
61
+ if (action === "retry") {
62
+ continued = true;
63
+ await continuePrompt();
64
+ continue;
65
+ }
66
+ Object.assign(typed, { providerRecoveryHandled: true, providerRecovery: action });
67
+ throw typed;
68
+ }
69
+ }
70
+ }
45
71
  function accounting(stats) {
46
72
  return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
47
73
  }
@@ -51,8 +77,18 @@ function canonicalSourcePath(path) { try {
51
77
  catch {
52
78
  return resolve(path);
53
79
  } }
54
- export async function createNativeAgentSession(input) {
80
+ const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
81
+ function workflowSystemPromptPath(cwd, agentDir, projectTrusted) {
82
+ const projectPath = join(cwd, ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md");
83
+ if (projectTrusted && existsSync(projectPath))
84
+ return projectPath;
85
+ const globalPaths = [join(homedir(), ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md"), join(agentDir, WORKFLOW_DIRECTORY, "SYSTEM.md")];
86
+ return globalPaths.find((path) => existsSync(path));
87
+ }
88
+ export async function createLocalPiSession(input) {
55
89
  const agentDir = input.agentDir ?? getAgentDir();
90
+ const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
91
+ const systemPromptOptions = input.systemPrompt !== undefined ? { systemPromptOverride: () => input.systemPrompt } : systemPromptSource !== undefined ? { systemPrompt: systemPromptSource } : {};
56
92
  const manager = input.agentDir ? SessionManager.create(input.cwd, join(agentDir, "sessions")) : SessionManager.create(input.cwd);
57
93
  manager.appendSessionInfo(input.sessionLabel);
58
94
  const modelRuntime = await ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json") });
@@ -87,18 +123,19 @@ export async function createNativeAgentSession(input) {
87
123
  noExtensions: true,
88
124
  additionalExtensionPaths: extensionPaths,
89
125
  noSkills: true,
90
- additionalSkillPaths: skillPaths,
126
+ additionalSkillPaths: [...new Set([...skillPaths, ...(input.additionalSkillPaths ?? [])])],
91
127
  ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
92
128
  skillsOverride: (base) => {
93
129
  const disabledSkills = updateSkillMatches(base.skills);
94
130
  return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
95
131
  },
96
132
  ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
133
+ ...systemPromptOptions,
97
134
  });
98
135
  await resourceLoader.reload();
99
136
  }
100
- else if (input.systemPromptAppend || input.extensionFactories?.length) {
101
- resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
137
+ else if (input.systemPrompt !== undefined || systemPromptSource !== undefined || input.systemPromptAppend || input.extensionFactories?.length || input.additionalSkillPaths?.length) {
138
+ 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 ?? ""] } : {}) });
102
139
  await resourceLoader.reload();
103
140
  }
104
141
  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 });
@@ -107,6 +144,72 @@ export async function createNativeAgentSession(input) {
107
144
  getToolDefinitions: () => session.getAllTools().map(({ name, description, parameters, promptGuidelines }) => ({ name, description, parameters, ...(promptGuidelines ? { promptGuidelines } : {}) })),
108
145
  });
109
146
  }
147
+ function workflowAgentMessage(message) { 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; }
148
+ function workflowAgentStats(stats) { 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 }; }
149
+ function workflowAgentState(native, prepared) {
150
+ const tools = native.agent?.state.tools.map(({ name }) => name) ?? prepared.tools;
151
+ 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 };
152
+ return { model, ...(model.thinking ? { thinking: model.thinking } : {}), tools: [...tools], ...(native.systemPrompt === undefined ? {} : { systemPrompt: native.systemPrompt }) };
153
+ }
154
+ function localSessionEvent(event) { return event; }
155
+ export async function createLocalWorkflowAgentSession(prepared, context) {
156
+ void context;
157
+ const input = {
158
+ cwd: prepared.cwd, model: { ...prepared.model }, tools: [...prepared.tools], sessionLabel: prepared.sessionLabel,
159
+ ...(prepared.agentDir ? { agentDir: prepared.agentDir } : {}), ...(prepared.customTools?.length ? { customTools: [...prepared.customTools] } : {}),
160
+ ...(prepared.resultTool ? { resultTool: prepared.resultTool } : {}), ...(prepared.systemPrompt === undefined ? {} : { systemPrompt: prepared.systemPrompt }),
161
+ ...(prepared.systemPromptAppend ? { systemPromptAppend: prepared.systemPromptAppend } : {}), ...(prepared.extensionFactories?.length ? { extensionFactories: [...prepared.extensionFactories] } : {}),
162
+ ...(prepared.additionalSkillPaths?.length ? { additionalSkillPaths: [...prepared.additionalSkillPaths] } : {}), ...(prepared.resourcePolicy ? { resourcePolicy: structuredClone(prepared.resourcePolicy) } : {}), ...(prepared.options ? { options: { ...prepared.options } } : {}),
163
+ };
164
+ const native = await createLocalPiSession(input);
165
+ let disposal;
166
+ let aborting;
167
+ let prompting;
168
+ let disposed = false;
169
+ const startAbort = () => aborting ??= Promise.resolve().then(() => native.abort?.()).then(() => undefined).finally(() => { aborting = undefined; });
170
+ const reference = { transport: "local", sessionId: native.sessionId, ...(native.sessionFile ? { locator: { sessionFile: native.sessionFile } } : {}) };
171
+ const session = {
172
+ reference,
173
+ getState: () => Object.freeze(workflowAgentState(native, prepared)),
174
+ getSessionStats: () => workflowAgentStats(native.getSessionStats()),
175
+ subscribe(listener) { listener({ type: "state_changed", state: workflowAgentState(native, prepared) }); return native.subscribe?.((event) => { listener(localSessionEvent(event)); }) ?? (() => undefined); },
176
+ getLastAssistant: () => workflowAgentMessage([...native.messages].reverse().find((message) => message.role === "assistant")),
177
+ async prompt(text) {
178
+ if (disposed)
179
+ throw new WorkflowError("INTERNAL_ERROR", "Local workflow session is disposed");
180
+ const prompt = Promise.resolve().then(async () => { await native.prompt(text); const assistant = workflowAgentMessage([...native.messages].reverse().find((message) => message.role === "assistant")); return assistant ? { assistant } : {}; });
181
+ prompting = prompt;
182
+ try {
183
+ return await prompt;
184
+ }
185
+ finally {
186
+ if (prompting === prompt)
187
+ prompting = undefined;
188
+ }
189
+ },
190
+ async steer(text) { if (!native.steer)
191
+ throw new WorkflowError("INTERNAL_ERROR", "Local workflow session does not support steering"); await native.steer(text); },
192
+ async abort() { if (disposed)
193
+ return; await startAbort(); },
194
+ async dispose() {
195
+ disposal ??= (async () => {
196
+ disposed = true;
197
+ try {
198
+ await startAbort();
199
+ }
200
+ catch { /* Abort failure must not prevent the prompt from settling. */ }
201
+ try {
202
+ await prompting;
203
+ }
204
+ catch { /* Prompt rejection is expected after abort during teardown. */ }
205
+ native.dispose();
206
+ })();
207
+ await disposal;
208
+ },
209
+ };
210
+ return session;
211
+ }
212
+ export const localAgentTransport = Object.freeze({ id: "local", createSession: createLocalWorkflowAgentSession });
110
213
  function changedOption(options, baseline, key) { return JSON.stringify(options[key]) !== JSON.stringify(baseline[key]); }
111
214
  function validThinking(value) { return typeof value === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value); }
112
215
  function isChildAgentToolParams(value) {
@@ -136,51 +239,94 @@ function fallbackSetupContext(root, options, signal) {
136
239
  function resourcePolicySummary(policy) {
137
240
  return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], excludedSkills: [...(policy.excludedSkills ?? [])], excludedExtensions: [...(policy.excludedExtensions ?? [])], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
138
241
  }
139
- async function prepareAgentSetup(root, createSession, task, options, resolved, cwd, attempt, signal, customTools, resultTool) {
242
+ function resourcePolicyWidened(ceiling, candidate) {
243
+ if (!ceiling)
244
+ return false;
245
+ if (!candidate)
246
+ return true;
247
+ if (!ceiling.projectTrusted && candidate.projectTrusted)
248
+ return true;
249
+ return ceiling.effective.skills.some((pattern) => !candidate.effective.skills.includes(pattern)) || ceiling.effective.extensions.some((pattern) => !candidate.effective.extensions.includes(pattern));
250
+ }
251
+ function preparedAgentSession(input) {
252
+ const prepared = {
253
+ cwd: input.cwd, model: Object.freeze({ ...input.model }), tools: Object.freeze([...input.tools]), sessionLabel: input.sessionLabel,
254
+ ...(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)) } : {}),
255
+ ...(input.systemPrompt === undefined ? {} : { systemPrompt: input.systemPrompt }), ...(input.systemPromptAppend ? { systemPromptAppend: input.systemPromptAppend } : {}),
256
+ ...(input.extensionFactories?.length ? { extensionFactories: Object.freeze([...input.extensionFactories]) } : {}), ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: Object.freeze([...input.additionalSkillPaths]) } : {}),
257
+ ...(input.resourcePolicy ? { resourcePolicy: Object.freeze(structuredClone(input.resourcePolicy)) } : {}),
258
+ };
259
+ return deepFreeze(prepared);
260
+ }
261
+ function agentSetupSummary(setup, hookNames) {
262
+ const model = setup.sessionInput.model;
263
+ 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) } : {}) };
264
+ }
265
+ async function prepareAgentSetup(root, transport, task, options, resolved, cwd, attempt, signal, customTools, resultTool) {
140
266
  const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
141
267
  const baselineOptions = structuredClone(options.agentOptions ?? {});
142
268
  const baseResourcePolicy = await root.agentResourcePolicy?.();
143
269
  const roleExclusions = options.role ? root.agentDefinitions?.[options.role]?.disabledAgentResources : undefined;
144
270
  const resourcePolicy = baseResourcePolicy && roleExclusions ? { ...baseResourcePolicy, effective: mergeAgentResourceExclusions(baseResourcePolicy.effective, roleExclusions) } : baseResourcePolicy;
145
- const 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) };
146
- const setup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, createSession };
271
+ const resourcePolicyCeiling = resourcePolicy ? structuredClone(resourcePolicy) : undefined;
272
+ const 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) };
273
+ const setup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, prepared: preparedAgentSession(sessionInput), transport };
147
274
  const base = fallbackSetupContext(root, options, setupSignal);
148
275
  const context = Object.freeze({ run: base.run, identity: base.identity, attempt, signal: setupSignal });
149
276
  const hookNames = [];
150
277
  for (const hook of [...(root.agentSetupHooks ?? [])].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))) {
151
278
  if (setupSignal.aborted)
152
- throw new WorkflowError("CANCELLED", "Agent cancelled");
279
+ return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error: new WorkflowError("CANCELLED", "Agent cancelled") } };
153
280
  try {
154
281
  await hook.setup(setup, context);
155
282
  }
156
283
  catch (error) {
157
- if (setupSignal.reason !== undefined)
158
- throw new WorkflowError("CANCELLED", "Agent cancelled");
159
- throw error;
284
+ setup.prepared = preparedAgentSession(setup.sessionInput);
285
+ return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error: setupSignal.reason !== undefined ? new WorkflowError("CANCELLED", "Agent cancelled") : error } };
160
286
  }
287
+ setup.prepared = preparedAgentSession(setup.sessionInput);
161
288
  hookNames.push(hook.name);
162
289
  if (setupSignal.reason !== undefined)
163
- throw new WorkflowError("CANCELLED", "Agent cancelled");
164
- }
165
- setup.sessionInput.options = setup.options;
166
- if (changedOption(setup.options, baselineOptions, "model") && typeof setup.options.model === "string")
167
- 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);
168
- if (changedOption(setup.options, baselineOptions, "thinking") && validThinking(setup.options.thinking))
169
- setup.sessionInput.model = { ...setup.sessionInput.model, thinking: setup.options.thinking };
170
- if (changedOption(setup.options, baselineOptions, "tools") && Array.isArray(setup.options.tools) && setup.options.tools.every((tool) => typeof tool === "string"))
171
- setup.sessionInput.tools = [...setup.options.tools];
172
- if (changedOption(setup.options, baselineOptions, "cwd") && typeof setup.options.cwd === "string")
173
- setup.sessionInput.cwd = setup.options.cwd;
174
- const model = setup.sessionInput.model;
175
- const summary = { 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) } : {}) };
176
- return { setup, summary };
290
+ return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error: new WorkflowError("CANCELLED", "Agent cancelled") } };
291
+ }
292
+ try {
293
+ if (resourcePolicyWidened(resourcePolicyCeiling, setup.sessionInput.resourcePolicy))
294
+ throw new WorkflowError("INVALID_METADATA", "Agent setup widened the prepared resource policy");
295
+ setup.sessionInput.options = setup.options;
296
+ if (changedOption(setup.options, baselineOptions, "model") && typeof setup.options.model === "string")
297
+ 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);
298
+ if (changedOption(setup.options, baselineOptions, "thinking") && validThinking(setup.options.thinking))
299
+ setup.sessionInput.model = { ...setup.sessionInput.model, thinking: setup.options.thinking };
300
+ if (changedOption(setup.options, baselineOptions, "tools") && Array.isArray(setup.options.tools) && setup.options.tools.every((tool) => typeof tool === "string"))
301
+ setup.sessionInput.tools = [...setup.options.tools];
302
+ if (changedOption(setup.options, baselineOptions, "cwd") && typeof setup.options.cwd === "string")
303
+ setup.sessionInput.cwd = setup.options.cwd;
304
+ const customToolNames = new Set([...(setup.sessionInput.customTools ?? []).map(({ name }) => name), ...(setup.sessionInput.resultTool ? [setup.sessionInput.resultTool.name] : [])]);
305
+ const widened = setup.sessionInput.tools.find((tool) => !resolved.tools.includes(tool) && !customToolNames.has(tool));
306
+ const outsideTool = widened ?? setup.sessionInput.tools.find((tool) => !root.tools.has(tool) && !customToolNames.has(tool));
307
+ if (outsideTool)
308
+ throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${outsideTool}`);
309
+ }
310
+ catch (error) {
311
+ setup.prepared = preparedAgentSession(setup.sessionInput);
312
+ return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error } };
313
+ }
314
+ setup.prepared = preparedAgentSession(setup.sessionInput);
315
+ return { setup, summary: agentSetupSummary(setup, hookNames) };
316
+ }
317
+ function attemptRecord(transport, attempt, session, setup, stats, result, error) {
318
+ return { attempt, transport, session: session.reference, ...(result === undefined ? {} : { result }), ...(error ? { error } : {}), accounting: stats, setup };
319
+ }
320
+ function errorWithAttempts(error, attempts) {
321
+ const typed = error instanceof Error ? error : new Error(typeof error === "string" ? error : String(error));
322
+ return Object.assign(typed, { attempts });
177
323
  }
178
324
  export class WorkflowAgentExecutor {
179
325
  root;
180
- createSession;
181
- constructor(root, createSession = createNativeAgentSession) {
326
+ transport;
327
+ constructor(root, transport = localAgentTransport) {
182
328
  this.root = root;
183
- this.createSession = createSession;
329
+ this.transport = transport;
184
330
  }
185
331
  setRunContext(runContext) { this.root.runContext = runContext; }
186
332
  resolve(options, inheritedTools) {
@@ -206,7 +352,8 @@ export class WorkflowAgentExecutor {
206
352
  const availableModels = this.root.knownModels ?? this.root.availableModels ?? new Set([modelCapability(this.root.model)]);
207
353
  if (!availableModels.has(modelCapability(model)))
208
354
  throw new WorkflowError("UNKNOWN_MODEL", `Unknown model${requestedModel ? ` ${requestedModel} resolved to ${modelCapability(model)}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
209
- return { model, ...(alias && requestedModel ? { requestedModel } : {}), tools: [...requested], systemPromptAppend: definition?.prompt ?? "" };
355
+ const overrideSystemPrompt = definition?.overrideSystemPrompt === true;
356
+ return { model, ...(alias && requestedModel ? { requestedModel } : {}), tools: [...requested], ...(overrideSystemPrompt ? { systemPrompt: definition.prompt ?? "" } : {}), systemPromptAppend: overrideSystemPrompt ? "" : definition?.prompt ?? "" };
210
357
  }
211
358
  async execute(task, options, signal, customTools = [], setSteer, beforeRetry) {
212
359
  const executionSignal = signal ?? this.root.runContext?.signal;
@@ -247,16 +394,16 @@ export class WorkflowAgentExecutor {
247
394
  const attempts = [];
248
395
  let maxAttempts = (options.retries ?? 0) + 1;
249
396
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
397
+ const attemptSignal = executionSignal ?? new AbortController().signal;
250
398
  if (recoveryModel)
251
399
  resolved = this.resolve({ ...options, modelOverride: recoveryModel });
252
400
  options.budget?.beforeAttempt();
253
401
  let schemaResult;
254
402
  let session;
255
403
  let setup;
256
- let setupSummary;
404
+ let setupSummary = { hookNames: [], model: { ...resolved.model }, tools: [...resolved.tools], cwd };
257
405
  let setupFailed = false;
258
406
  let budgetError;
259
- let turnStarted = false;
260
407
  const hasSchemaResult = () => schemaResult !== undefined;
261
408
  const resultTool = options.schema ? {
262
409
  name: "workflow_result", label: "Workflow Result", description: "Submit the terminal structured workflow result", parameters: Type.Unsafe(options.schema),
@@ -266,17 +413,23 @@ export class WorkflowAgentExecutor {
266
413
  if (schemaResult !== undefined)
267
414
  return { content: [{ type: "text", text: "Result has already been accepted." }], details: {}, isError: true };
268
415
  schemaResult = structuredClone(value);
269
- void session?.abort?.();
416
+ const currentSession = session;
417
+ if (currentSession)
418
+ void currentSession.abort();
270
419
  return { content: [{ type: "text", text: "Result accepted." }], details: {} };
271
420
  },
272
421
  } : undefined;
273
422
  const toolCalls = new Map();
274
423
  let activity;
424
+ let lastEventAt;
425
+ let lastReportedEventAt;
275
426
  let progress = Promise.resolve();
276
427
  let unsubscribe;
277
428
  let systemPromptTurn = 0;
278
429
  let systemPromptWrite = Promise.resolve();
279
430
  let systemPromptWriteError;
431
+ let turnStarted = false;
432
+ let completedAttempt;
280
433
  const flushSystemPrompts = async () => {
281
434
  await systemPromptWrite;
282
435
  if (systemPromptWriteError)
@@ -285,33 +438,85 @@ export class WorkflowAgentExecutor {
285
438
  const report = (persist) => {
286
439
  if (!session || !options.onProgress)
287
440
  return;
288
- const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], ...(activity ? { activity } : {}), persist };
441
+ const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], state: session.getState(), ...(activity ? { activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }), persist };
442
+ if (lastEventAt !== undefined)
443
+ lastReportedEventAt = lastEventAt;
289
444
  progress = progress.then(() => options.onProgress?.(update)).then(() => undefined);
290
445
  };
446
+ const reportTimestamp = () => {
447
+ if (lastEventAt !== undefined && lastReportedEventAt !== undefined && lastEventAt - lastReportedEventAt < 1000)
448
+ return;
449
+ report(false);
450
+ };
451
+ const activityChanged = (previous) => previous?.kind !== activity?.kind || previous?.text !== activity?.text;
291
452
  try {
292
453
  setupFailed = true;
293
- const prepared = await prepareAgentSetup(this.root, this.createSession, task, options, resolved, cwd, attempt, executionSignal, customTools, resultTool);
454
+ const prepared = await prepareAgentSetup(this.root, this.transport, task, options, resolved, cwd, attempt, attemptSignal, customTools, resultTool);
294
455
  setup = prepared.setup;
295
456
  setupSummary = prepared.summary;
457
+ if (prepared.failure)
458
+ throw prepared.failure.error;
296
459
  setupFailed = false;
297
- if (executionSignal?.aborted)
460
+ if (attemptSignal.aborted)
298
461
  throw new WorkflowError("CANCELLED", "Agent cancelled");
299
462
  const started = Date.now();
300
- session = await setup.createSession(setup.sessionInput);
463
+ const transportSignal = attemptSignal;
464
+ await options.onAttempt?.({ attempt, transport: setup.transport.id, accounting: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 }, setup: setupSummary });
465
+ const transportBase = fallbackSetupContext(this.root, options, transportSignal);
466
+ const createdSession = await setup.transport.createSession(setup.prepared, Object.freeze({ run: transportBase.run, identity: transportBase.identity, attempt, signal: transportSignal }));
467
+ if (createdSession.reference.transport !== setup.transport.id) {
468
+ await createdSession.dispose();
469
+ throw new WorkflowError("INTERNAL_ERROR", `Agent transport ${setup.transport.id} created a session for ${createdSession.reference.transport}`);
470
+ }
471
+ session = createdSession;
472
+ 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 });
473
+ const preparedTools = new Set([...setup.prepared.tools, ...(setup.prepared.customTools ?? []).map(({ name }) => name), ...(setup.prepared.resultTool ? [setup.prepared.resultTool.name] : [])]);
474
+ if (session.getState().tools.some((tool) => !preparedTools.has(tool)))
475
+ throw new WorkflowError("INTERNAL_ERROR", `Agent transport ${setup.transport.id} widened the prepared tool policy`);
301
476
  if (setup.sessionInput.resourcePolicy)
302
477
  setupSummary = { ...setupSummary, disabledAgentResources: resourcePolicySummary(setup.sessionInput.resourcePolicy) };
303
- const includeAttemptSetup = Boolean(this.root.agentSetupHooks?.length || setup.sessionInput.resourcePolicy);
304
- await options.onAttempt?.({ attempt, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), ...(includeAttemptSetup ? { setup: setupSummary } : {}) });
305
478
  const activeSession = session;
306
- unsubscribe = activeSession.subscribe?.((event) => {
307
- if (event.type === "agent_start" && session?.systemPrompt !== undefined) {
479
+ let lastAssistant;
480
+ const recoverTerminal = () => recoverTerminalProviderError(activeSession, options.label, options.providerErrorRecovery, async () => { try {
481
+ lastAssistant = (await promptWithProviderPause(activeSession, providerContinuationPrompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant;
482
+ }
483
+ catch (error) {
484
+ lastAssistant = activeSession.getLastAssistant?.() ?? lastAssistant;
485
+ if (!hasSchemaResult())
486
+ throw error;
487
+ } }, () => lastAssistant);
488
+ const promptAndRecover = async (prompt) => {
489
+ let promptFailed = false;
490
+ let promptError;
491
+ try {
492
+ lastAssistant = (await promptWithProviderPause(activeSession, prompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant;
493
+ }
494
+ catch (error) {
495
+ lastAssistant = activeSession.getLastAssistant?.() ?? lastAssistant;
496
+ promptFailed = true;
497
+ promptError = error;
498
+ }
499
+ const recovered = await recoverTerminal();
500
+ if (promptFailed && !hasSchemaResult() && !recovered)
501
+ throw promptError;
502
+ };
503
+ unsubscribe = activeSession.subscribe((event) => {
504
+ lastEventAt = Date.now();
505
+ let persist = false;
506
+ let shouldReport = false;
507
+ let removeToolCallId;
508
+ if (event.type === "agent_start" && session?.getState().systemPrompt !== undefined) {
308
509
  if (this.root.runStore) {
309
510
  systemPromptTurn += 1;
310
- const entry = { sessionId: session.sessionId, attempt, turn: systemPromptTurn, prompt: session.systemPrompt };
511
+ const entry = { sessionId: session.reference.sessionId, attempt, turn: systemPromptTurn, prompt: session.getState().systemPrompt ?? "" };
311
512
  systemPromptWrite = systemPromptWrite.then(() => this.root.runStore?.recordSystemPrompt(entry)).then(() => undefined).catch((error) => { systemPromptWriteError ??= error; });
312
513
  }
313
514
  }
314
- if (event.type === "message_start" && event.message.role === "assistant") {
515
+ if (event.type === "state_changed") {
516
+ shouldReport = true;
517
+ persist = true;
518
+ }
519
+ if (event.type === "turnStarted" || event.type === "turn_started") {
315
520
  if (!turnStarted) {
316
521
  try {
317
522
  options.budget?.beforeTurn();
@@ -319,15 +524,39 @@ export class WorkflowAgentExecutor {
319
524
  }
320
525
  catch (error) {
321
526
  budgetError ??= error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", error instanceof Error ? error.message : String(error));
322
- void session?.abort?.();
527
+ void activeSession.abort();
528
+ }
529
+ }
530
+ }
531
+ if (event.type === "message_start" && event.message?.role === "assistant") {
532
+ if (!turnStarted) {
533
+ try {
534
+ options.budget?.beforeTurn();
535
+ turnStarted = true;
536
+ }
537
+ catch (error) {
538
+ budgetError ??= error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", error instanceof Error ? error.message : String(error));
539
+ void activeSession.abort();
323
540
  }
324
541
  }
325
542
  activity = { kind: "text", text: "responding" };
326
- report(false);
543
+ shouldReport = true;
544
+ }
545
+ if (event.type === "message_update") {
546
+ const previousActivity = activity;
547
+ const updateType = event.assistantMessageEvent?.type;
548
+ if (updateType && ["thinking_start", "thinking_delta", "thinking_end"].includes(updateType))
549
+ activity = { kind: "reasoning", text: "reasoning" };
550
+ else if (updateType && ["text_start", "text_delta", "text_end", "toolcall_start", "toolcall_delta", "toolcall_end"].includes(updateType))
551
+ activity = { kind: "text", text: "responding" };
552
+ shouldReport = activityChanged(previousActivity);
327
553
  }
328
554
  if (event.type === "message_end") {
555
+ const previousActivity = activity;
329
556
  activity = undefined;
330
- if (event.message.role === "assistant") {
557
+ shouldReport = activityChanged(previousActivity);
558
+ if (event.message?.role === "assistant") {
559
+ lastAssistant = event.message;
331
560
  const needsMoreWork = hasToolCall(event.message);
332
561
  const final = !needsMoreWork || (options.schema !== undefined && hasSchemaResult());
333
562
  if (!budgetError) {
@@ -336,113 +565,118 @@ export class WorkflowAgentExecutor {
336
565
  if (!final) {
337
566
  const instruction = options.budget?.instruction();
338
567
  if (instruction)
339
- void session?.steer?.(instruction);
568
+ void activeSession.steer(instruction);
340
569
  }
341
570
  }
342
571
  catch (error) {
343
572
  budgetError ??= error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", error instanceof Error ? error.message : String(error));
344
- void session?.abort?.();
573
+ void activeSession.abort();
345
574
  }
346
575
  }
347
576
  turnStarted = false;
348
- report(true);
577
+ persist = true;
349
578
  }
350
579
  }
351
- if (event.type === "tool_execution_start") {
580
+ if (event.type === "tool_execution_start" && event.toolCallId && event.toolName) {
352
581
  toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: "running" });
353
582
  activity = { kind: "tool", text: event.toolName };
354
- report(false);
583
+ shouldReport = true;
584
+ }
585
+ if (event.type === "tool_execution_update" && event.toolName) {
586
+ const previousActivity = activity;
587
+ activity = { kind: "tool", text: event.toolName };
588
+ shouldReport = activityChanged(previousActivity);
355
589
  }
356
- if (event.type === "tool_execution_end") {
590
+ if (event.type === "tool_execution_end" && event.toolCallId && event.toolName) {
357
591
  toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: event.isError ? "failed" : "completed" });
358
592
  if (activity?.kind === "tool" && activity.text === event.toolName)
359
593
  activity = undefined;
360
- report(false);
361
- toolCalls.delete(event.toolCallId);
594
+ shouldReport = true;
595
+ removeToolCallId = event.toolCallId;
362
596
  }
597
+ if (shouldReport || persist)
598
+ report(persist);
599
+ else
600
+ reportTimestamp();
601
+ if (removeToolCallId)
602
+ toolCalls.delete(removeToolCallId);
363
603
  });
364
604
  report(false);
365
605
  if (setSteer) {
366
- if (!session.steer)
367
- throw new WorkflowError("INTERNAL_ERROR", "Native Pi session does not support steering");
368
- setSteer((message) => session?.steer?.(message));
606
+ setSteer((message) => activeSession.steer(message));
369
607
  }
370
608
  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");
371
609
  const instruction = options.budget?.instruction();
372
610
  const promptText = `${context}\n\nTask:\n${setup.prompt}${instruction ? `\n\n${instruction}` : ""}`;
373
611
  options.budget?.beforeTurn();
374
612
  turnStarted = true;
375
- try {
376
- await promptWithProviderPause(session, promptText, remaining(options.timeoutMs, started), executionSignal, this.root.providerPause);
377
- }
378
- catch (error) {
379
- if (!hasSchemaResult())
380
- throw error;
381
- }
382
- throwIfTerminalAssistantError(session, setup.sessionInput.model);
613
+ await promptAndRecover(promptText);
383
614
  {
384
615
  const completedAccounting = accounting(session.getSessionStats());
385
- options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(session.messages));
616
+ options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(lastAssistant));
386
617
  turnStarted = false;
387
618
  }
388
619
  if (budgetError)
389
620
  throw budgetError;
390
621
  if (options.schema) {
391
622
  if (!hasSchemaResult()) {
392
- try {
393
- options.budget?.beforeTurn();
394
- turnStarted = true;
395
- await promptWithProviderPause(session, "Submit the final result now by calling workflow_result exactly once. Do not return prose.", remaining(options.timeoutMs, started), executionSignal, this.root.providerPause);
396
- {
397
- const completedAccounting = accounting(session.getSessionStats());
398
- options.budget?.afterTurn(completedAccounting, true);
399
- turnStarted = false;
400
- }
401
- }
402
- catch (error) {
403
- if (!hasSchemaResult())
404
- throw error;
623
+ options.budget?.beforeTurn();
624
+ turnStarted = true;
625
+ await promptAndRecover("Submit the final result now by calling workflow_result exactly once. Do not return prose.");
626
+ {
627
+ const completedAccounting = accounting(session.getSessionStats());
628
+ options.budget?.afterTurn(completedAccounting, true);
629
+ turnStarted = false;
405
630
  }
406
631
  }
407
- throwIfTerminalAssistantError(session, setup.sessionInput.model);
408
632
  if (!hasSchemaResult()) {
409
- try {
410
- options.budget?.beforeTurn();
411
- turnStarted = true;
412
- await promptWithProviderPause(session, "Your result was missing or invalid. Repair it by calling workflow_result exactly once with a schema-valid value.", remaining(options.timeoutMs, started), executionSignal, this.root.providerPause);
413
- {
414
- const completedAccounting = accounting(session.getSessionStats());
415
- options.budget?.afterTurn(completedAccounting, true);
416
- turnStarted = false;
417
- }
418
- }
419
- catch (error) {
420
- if (!hasSchemaResult())
421
- throw error;
633
+ options.budget?.beforeTurn();
634
+ turnStarted = true;
635
+ await promptAndRecover("Your result was missing or invalid. Repair it by calling workflow_result exactly once with a schema-valid value.");
636
+ {
637
+ const completedAccounting = accounting(session.getSessionStats());
638
+ options.budget?.afterTurn(completedAccounting, true);
639
+ turnStarted = false;
422
640
  }
423
- throwIfTerminalAssistantError(session, setup.sessionInput.model);
424
641
  }
425
642
  if (schemaResult === undefined)
426
643
  throw new WorkflowError("RESULT_INVALID", "Agent did not submit a valid workflow_result after one repair");
427
644
  }
428
- const value = options.schema ? schemaResult : text(session.messages);
645
+ const value = options.schema ? schemaResult : text(lastAssistant);
429
646
  if (options.worktreeOwner)
430
647
  await this.root.runStore?.snapshotWorktree(options.worktreeOwner);
431
648
  report(true);
432
649
  await progress;
433
650
  await flushSystemPrompts();
434
- unsubscribe?.();
651
+ unsubscribe();
435
652
  const attemptAccounting = accounting(session.getSessionStats());
436
- const includeCompletedSetup = Boolean(this.root.agentSetupHooks?.length || setup.sessionInput.resourcePolicy);
437
- attempts.push({ attempt, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), result: value, accounting: attemptAccounting, ...(includeCompletedSetup ? { setup: setupSummary } : {}) });
438
- session.dispose();
653
+ completedAttempt = attemptRecord(setup.transport.id, attempt, session, setupSummary, attemptAccounting, value);
654
+ attempts.push(completedAttempt);
655
+ try {
656
+ await options.onAttempt?.(completedAttempt);
657
+ }
658
+ finally {
659
+ await session.dispose();
660
+ }
439
661
  return { value, attempts, cwd: setupSummary.cwd };
440
662
  }
441
663
  catch (error) {
442
- const typed = budgetError ?? (error instanceof WorkflowError ? error : new WorkflowError(executionSignal?.aborted && setupFailed ? "CANCELLED" : "AGENT_FAILED", error instanceof Error ? error.message : String(error)));
664
+ if (completedAttempt)
665
+ throw errorWithAttempts(error, attempts);
666
+ const typed = budgetError ?? (error instanceof WorkflowError ? error : new WorkflowError(attemptSignal.aborted && setupFailed ? "CANCELLED" : "AGENT_FAILED", error instanceof Error ? error.message : String(error)));
667
+ if (!session) {
668
+ const failedAttempt = { 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 };
669
+ attempts.push(failedAttempt);
670
+ try {
671
+ await options.onAttempt?.(failedAttempt);
672
+ }
673
+ catch (persistenceError) {
674
+ throw errorWithAttempts(persistenceError, attempts);
675
+ }
676
+ }
443
677
  if (session) {
444
678
  report(true);
445
- await progress;
679
+ await progress.catch(() => undefined);
446
680
  try {
447
681
  await flushSystemPrompts();
448
682
  }
@@ -457,36 +691,52 @@ export class WorkflowAgentExecutor {
457
691
  budgetError ??= budgetFailure instanceof WorkflowError ? budgetFailure : new WorkflowError("BUDGET_EXHAUSTED", budgetFailure instanceof Error ? budgetFailure.message : String(budgetFailure));
458
692
  }
459
693
  }
460
- const includeFailedSetup = Boolean(this.root.agentSetupHooks?.length || setup?.sessionInput.resourcePolicy);
461
- attempts.push({ attempt, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), error: { code: typed.code, message: typed.message }, accounting: attemptAccounting, ...(includeFailedSetup && setupSummary ? { setup: setupSummary } : {}) });
462
- session.dispose();
694
+ const failedAttempt = attemptRecord(setup?.transport.id ?? this.transport.id, attempt, session, setupSummary, attemptAccounting, undefined, { code: typed.code, message: typed.message });
695
+ attempts.push(failedAttempt);
696
+ try {
697
+ try {
698
+ await options.onAttempt?.(failedAttempt);
699
+ }
700
+ finally {
701
+ await session.dispose();
702
+ }
703
+ }
704
+ catch (persistenceError) {
705
+ throw errorWithAttempts(persistenceError, attempts);
706
+ }
463
707
  }
464
708
  if (options.worktreeOwner && typed.code !== "WORKTREE_FAILED")
465
709
  await this.root.runStore?.snapshotWorktree(options.worktreeOwner).catch(() => undefined);
466
710
  const terminal = terminalProviderError(typed);
467
- if (terminal && options.providerErrorRecovery) {
468
- let recovery;
711
+ const recoveryState = typed;
712
+ let recovery = recoveryState.providerRecovery;
713
+ if (terminal && options.providerErrorRecovery && !recoveryState.providerRecoveryHandled) {
469
714
  try {
470
715
  recovery = await options.providerErrorRecovery({ label: options.label, ...terminal });
471
716
  }
472
717
  catch {
473
718
  throw Object.assign(typed, { attempts });
474
719
  }
475
- if (recovery === "retry" || typeof recovery === "object" && typeof recovery.model === "string") {
476
- if (typeof recovery === "object") {
477
- try {
478
- const selected = resolveModelReference(recovery.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
479
- recoveryModel = selected.thinking === undefined && resolved.model.thinking ? { ...selected, thinking: resolved.model.thinking } : selected;
480
- }
481
- catch {
482
- throw Object.assign(typed, { attempts });
483
- }
484
- }
720
+ if (recovery === "retry") {
485
721
  maxAttempts += 1;
486
722
  beforeRetry?.();
487
723
  continue;
488
724
  }
489
725
  }
726
+ if (recoveryState.providerRecoveryFailed || recovery === "abort")
727
+ throw Object.assign(typed, { attempts });
728
+ if (typeof recovery === "object" && typeof recovery.model === "string") {
729
+ try {
730
+ const selected = resolveModelReference(recovery.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
731
+ recoveryModel = selected.thinking === undefined && resolved.model.thinking ? { ...selected, thinking: resolved.model.thinking } : selected;
732
+ }
733
+ catch {
734
+ throw Object.assign(typed, { attempts });
735
+ }
736
+ maxAttempts += 1;
737
+ beforeRetry?.();
738
+ continue;
739
+ }
490
740
  if (attempt === maxAttempts || setupFailed || typed.code === "CANCELLED" || typed.code === "WORKTREE_FAILED" || typed.code === "RESUME_INCOMPATIBLE")
491
741
  throw Object.assign(typed, { attempts });
492
742
  beforeRetry?.();
@@ -541,7 +791,7 @@ export class FairAgentScheduler {
541
791
  const id = `${runId}:${String(++this.#nextId)}`;
542
792
  let resolveResult = () => undefined;
543
793
  const promise = new Promise((resolve) => { resolveResult = resolve; });
544
- const node = { id, runId, ...(parentId ? { parentId } : {}), options: effective, children: new Set(), collected: false, state: "queued", controller: new AbortController(), promise, resolve: resolveResult, task: async () => undefined, restored: false };
794
+ const node = { id, runId, ...(parentId ? { parentId } : {}), prompt, options: effective, children: new Set(), collected: false, state: "queued", controller: new AbortController(), promise, resolve: resolveResult, task: async () => undefined, restored: false };
545
795
  node.task = async () => {
546
796
  if (node.controller.signal.aborted) {
547
797
  this.#release(node.runId);
@@ -680,7 +930,7 @@ export class FairAgentScheduler {
680
930
  return [agentTool, resultTool, steerTool];
681
931
  }
682
932
  snapshot() {
683
- return [...this.#nodes.values()].map(({ id, parentId, options, state }) => ({ id, ...(parentId ? { parentId } : {}), label: options.label, state, options }));
933
+ return [...this.#nodes.values()].map(({ id, parentId, prompt, options, state }) => ({ id, ...(parentId ? { parentId } : {}), ...(prompt === undefined ? {} : { prompt }), label: options.label, state, options }));
684
934
  }
685
935
  restoreRun(runId, limit, ownership, beforeLaunch) {
686
936
  this.addRun(runId, limit, beforeLaunch);
@@ -690,7 +940,7 @@ export class FairAgentScheduler {
690
940
  throw new WorkflowError("RESUME_INCOMPATIBLE", `Persisted agent belongs to another run: ${record.id}`);
691
941
  let resolveResult = () => undefined;
692
942
  const promise = new Promise((resolve) => { resolveResult = resolve; });
693
- const node = { 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 };
943
+ const node = { 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 };
694
944
  this.#nodes.set(node.id, node);
695
945
  run.logical += 1;
696
946
  this.#nextId = Math.max(this.#nextId, Number(node.id.slice(node.id.lastIndexOf(":") + 1)) || 0);
@@ -801,11 +1051,6 @@ export class FairAgentScheduler {
801
1051
  }
802
1052
  }
803
1053
  function resolvePath(path) { return path.replace(/[\\/]+$/, ""); }
804
- function requiredFile(file) {
805
- if (!file)
806
- throw new WorkflowError("INTERNAL_ERROR", "Workflow agents require persisted native Pi sessions");
807
- return file;
808
- }
809
1054
  function remaining(timeoutMs, started) {
810
1055
  return timeoutMs === null || timeoutMs === undefined ? timeoutMs : Math.max(1, timeoutMs - (Date.now() - started));
811
1056
  }
@@ -818,8 +1063,7 @@ function providerLimited(error) {
818
1063
  async function promptWithProviderPause(session, text, timeoutMs, signal, pause) {
819
1064
  for (;;) {
820
1065
  try {
821
- await withTimeout(session.prompt(text), timeoutMs, signal, session);
822
- return;
1066
+ return await withTimeout(session.prompt(text), timeoutMs, signal, session);
823
1067
  }
824
1068
  catch (error) {
825
1069
  if (!pause || !providerLimited(error))
@@ -837,7 +1081,7 @@ async function withTimeout(work, timeoutMs, signal, session) {
837
1081
  const timeout = timeoutMs ? new Promise((_, reject) => { timer = setTimeout(() => { state.interrupted = true; reject(new WorkflowError("AGENT_TIMEOUT", "Agent attempt timed out")); }, timeoutMs); }) : new Promise(() => { });
838
1082
  const cancelled = signal ? new Promise((_, reject) => { abort = () => { state.interrupted = true; reject(new WorkflowError("CANCELLED", "Agent cancelled")); }; signal.addEventListener("abort", abort, { once: true }); }) : new Promise(() => { });
839
1083
  try {
840
- await Promise.race([work, timeout, cancelled]);
1084
+ return await Promise.race([work, timeout, cancelled]);
841
1085
  }
842
1086
  finally {
843
1087
  if (timer)
@@ -845,6 +1089,6 @@ async function withTimeout(work, timeoutMs, signal, session) {
845
1089
  if (abort)
846
1090
  signal?.removeEventListener("abort", abort);
847
1091
  if (state.interrupted)
848
- await session.abort?.();
1092
+ await session.abort();
849
1093
  }
850
1094
  }