pi-extensible-workflows 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -31
- package/dist/src/agent-execution.d.ts +67 -9
- package/dist/src/agent-execution.js +385 -141
- package/dist/src/bundles.d.ts +53 -0
- package/dist/src/bundles.js +457 -0
- package/dist/src/cli.d.ts +3 -1
- package/dist/src/cli.js +156 -22
- package/dist/src/doctor-cleanup.js +68 -31
- package/dist/src/eval-capture-extension.js +16 -1
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +29 -13
- package/dist/src/host.d.ts +12 -9
- package/dist/src/host.js +525 -195
- package/dist/src/index.d.ts +5 -4
- package/dist/src/index.js +3 -2
- package/dist/src/persistence.d.ts +43 -8
- package/dist/src/persistence.js +54 -0
- package/dist/src/registry.d.ts +3 -2
- package/dist/src/registry.js +16 -4
- package/dist/src/session-inspector.d.ts +12 -2
- package/dist/src/session-inspector.js +50 -24
- package/dist/src/types.d.ts +141 -60
- package/dist/src/types.js +1 -0
- package/dist/src/validation.js +28 -7
- package/dist/src/workflow-artifacts.d.ts +1 -0
- package/dist/src/workflow-artifacts.js +1 -0
- package/dist/src/workflow-evals.d.ts +7 -1
- package/dist/src/workflow-evals.js +23 -3
- package/evals/cases/recovery-completed-worktree.yaml +17 -0
- package/evals/cases/recovery-failed-run.yaml +14 -0
- package/package.json +1 -1
- package/skills/pi-extensible-workflows/SKILL.md +13 -5
- package/src/agent-execution.ts +302 -107
- package/src/bundles.ts +471 -0
- package/src/cli.ts +130 -22
- package/src/doctor-cleanup.ts +38 -4
- package/src/eval-capture-extension.ts +15 -1
- package/src/execution.ts +27 -15
- package/src/host.ts +454 -175
- package/src/index.ts +5 -4
- package/src/persistence.ts +58 -4
- package/src/registry.ts +14 -5
- package/src/session-inspector.ts +49 -26
- package/src/types.ts +55 -30
- package/src/validation.ts +19 -6
- package/src/workflow-artifacts.ts +1 -0
- package/src/workflow-evals.ts +24 -3
|
@@ -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(
|
|
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(
|
|
24
|
-
|
|
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
|
|
32
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
146
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
setup.
|
|
170
|
-
|
|
171
|
-
setup.
|
|
172
|
-
|
|
173
|
-
setup.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
181
|
-
constructor(root,
|
|
326
|
+
transport;
|
|
327
|
+
constructor(root, transport = localAgentTransport) {
|
|
182
328
|
this.root = root;
|
|
183
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
460
|
+
if (attemptSignal.aborted)
|
|
298
461
|
throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
299
462
|
const started = Date.now();
|
|
300
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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 === "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
573
|
+
void activeSession.abort();
|
|
345
574
|
}
|
|
346
575
|
}
|
|
347
576
|
turnStarted = false;
|
|
348
|
-
|
|
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
|
-
|
|
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
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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(
|
|
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
|
-
|
|
437
|
-
attempts.push(
|
|
438
|
-
|
|
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
|
-
|
|
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
|
|
461
|
-
attempts.push(
|
|
462
|
-
|
|
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
|
-
|
|
468
|
-
|
|
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"
|
|
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
|
}
|