pi-extensible-workflows 3.3.0 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/src/agent-execution.d.ts +63 -9
- package/dist/src/agent-execution.js +265 -92
- package/dist/src/cli.js +11 -2
- package/dist/src/doctor-cleanup.js +66 -31
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +29 -13
- package/dist/src/host.d.ts +5 -5
- package/dist/src/host.js +160 -76
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +1 -1
- package/dist/src/persistence.d.ts +4 -8
- package/dist/src/persistence.js +3 -0
- package/dist/src/registry.d.ts +3 -2
- package/dist/src/registry.js +16 -4
- package/dist/src/session-inspector.js +14 -22
- package/dist/src/types.d.ts +123 -60
- package/dist/src/validation.js +20 -5
- package/dist/src/workflow-artifacts.d.ts +1 -0
- package/dist/src/workflow-artifacts.js +1 -0
- package/package.json +2 -2
- package/skills/pi-extensible-workflows/SKILL.md +13 -5
- package/src/agent-execution.ts +221 -90
- package/src/cli.ts +14 -3
- package/src/doctor-cleanup.ts +36 -3
- package/src/execution.ts +27 -15
- package/src/host.ts +136 -62
- package/src/index.ts +3 -3
- package/src/persistence.ts +5 -3
- package/src/registry.ts +14 -5
- package/src/session-inspector.ts +13 -22
- package/src/types.ts +45 -27
- package/src/validation.ts +13 -4
- package/src/workflow-artifacts.ts +1 -0
package/src/agent-execution.ts
CHANGED
|
@@ -6,11 +6,25 @@ import { Value } from "typebox/value";
|
|
|
6
6
|
import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, SettingsManager, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
8
8
|
type AgentMessage = { role: string; content?: unknown; stopReason?: string; errorMessage?: string; usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: { total: number } } };
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
type PiSession = {
|
|
10
|
+
readonly sessionId: string;
|
|
11
|
+
readonly sessionFile: string | undefined;
|
|
12
|
+
readonly messages: readonly AgentMessage[];
|
|
13
|
+
getSessionStats(): { tokens: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number }; cost: number };
|
|
14
|
+
readonly systemPrompt?: string;
|
|
15
|
+
readonly model?: { provider: string; model?: string; id?: string };
|
|
16
|
+
readonly agent?: { state: { tools: readonly { name: string }[] } };
|
|
17
|
+
subscribe?(listener: (event: unknown) => void): () => void;
|
|
18
|
+
prompt(text: string): Promise<void>;
|
|
19
|
+
steer?(text: string): Promise<void>;
|
|
20
|
+
abort?(): Promise<void>;
|
|
21
|
+
dispose(): void;
|
|
22
|
+
};
|
|
23
|
+
import type { AgentIdentity, AgentResourceExclusions, AgentResourcePolicy, AgentSetup, AgentSetupSummary, AgentTransport, AgentTransportContext, JsonSchema, JsonValue, ModelSpec, PreparedAgentSession, RegisteredAgentSetupHook, SessionInput, WorkflowAgentMessage, WorkflowAgentSession, WorkflowAgentSessionEvent, WorkflowAgentSessionReference, WorkflowAgentSessionState, WorkflowAgentSessionStats, WorkflowAgentTurnResult, WorkflowRunContext } from "./types.js";
|
|
24
|
+
import { deepFreeze, jsonObject, disabledResources, mergeAgentResourceExclusions, modelAliasName, modelCapability, resolveModelReference, unmatchedResourcePatterns } from "./utils.js";
|
|
11
25
|
import { WorkflowError } from "./types.js";
|
|
12
26
|
import type { RunStore } from "./persistence.js";
|
|
13
|
-
export type { AgentSetup, AgentSetupContext, AgentSetupHook,
|
|
27
|
+
export type { AgentSetup, AgentSetupContext, AgentSetupHook, AgentTransport, AgentTransportContext, PreparedAgentSession, RegisteredAgentSetupHook, SessionInput, WorkflowAgentMessage, WorkflowAgentSession, WorkflowAgentSessionEvent, WorkflowAgentSessionReference, WorkflowAgentSessionState, WorkflowAgentSessionStats, WorkflowAgentTurnResult } from "./types.js";
|
|
14
28
|
export interface AgentBudgetHooks {
|
|
15
29
|
beforeAttempt(): void;
|
|
16
30
|
beforeTurn(): void;
|
|
@@ -28,7 +42,7 @@ export interface AgentExecutionOptions {
|
|
|
28
42
|
model?: string;
|
|
29
43
|
thinking?: ThinkingLevel;
|
|
30
44
|
onProgress?: (progress: AgentProgress) => void | Promise<void>;
|
|
31
|
-
onAttempt?: (attempt:
|
|
45
|
+
onAttempt?: (attempt: AgentAttempt) => void | Promise<void>;
|
|
32
46
|
providerErrorRecovery?: (failure: AgentProviderFailure) => Promise<AgentProviderRecovery>;
|
|
33
47
|
modelOverride?: ModelSpec;
|
|
34
48
|
tools?: readonly string[];
|
|
@@ -66,8 +80,8 @@ export interface AgentExecutionRoot {
|
|
|
66
80
|
export interface AgentAccounting { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }
|
|
67
81
|
export interface AgentToolCallProgress { id: string; name: string; state: "running" | "completed" | "failed" }
|
|
68
82
|
export interface AgentActivity { kind: "reasoning" | "tool" | "text"; text: string }
|
|
69
|
-
export interface AgentProgress { accounting: AgentAccounting; toolCalls: readonly AgentToolCallProgress[]; activity?: AgentActivity; lastEventAt?: number; persist: boolean }
|
|
70
|
-
export interface AgentAttempt { attempt: number;
|
|
83
|
+
export interface AgentProgress { accounting: AgentAccounting; toolCalls: readonly AgentToolCallProgress[]; state?: WorkflowAgentSessionState; activity?: AgentActivity; lastEventAt?: number; persist: boolean }
|
|
84
|
+
export interface AgentAttempt { attempt: number; transport: string; session?: WorkflowAgentSessionReference; liveSession?: WorkflowAgentSession; result?: JsonValue; error?: { code: string; message: string }; accounting: AgentAccounting; setup: AgentSetupSummary }
|
|
71
85
|
export interface AgentExecutionResult { value: JsonValue; attempts: readonly AgentAttempt[]; cwd: string }
|
|
72
86
|
|
|
73
87
|
function parseModel(value: string | undefined, fallback: ModelSpec, thinking?: ThinkingLevel, aliases: Readonly<Record<string, string>> = {}, knownModels?: ReadonlySet<string>, settingsPath?: string): ModelSpec {
|
|
@@ -76,30 +90,25 @@ function parseModel(value: string | undefined, fallback: ModelSpec, thinking?: T
|
|
|
76
90
|
return { ...parsed, ...(thinking ? { thinking } : !parsed.thinking && fallback.thinking ? { thinking: fallback.thinking } : {}) };
|
|
77
91
|
}
|
|
78
92
|
|
|
79
|
-
function text(
|
|
80
|
-
const message = [...messages].reverse().find((item) => item.role === "assistant");
|
|
93
|
+
function text(message: WorkflowAgentMessage | undefined): string {
|
|
81
94
|
if (!message || !Array.isArray(message.content)) return "";
|
|
82
95
|
return message.content.filter((part: unknown): part is { type: "text"; text: string } => typeof part === "object" && part !== null && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string").map((part) => part.text).join("");
|
|
83
96
|
}
|
|
97
|
+
function isEmptyAbortedAssistant(message: WorkflowAgentMessage | undefined): boolean { return message?.stopReason === "aborted" && Array.isArray(message.content) && message.content.length === 0; }
|
|
84
98
|
|
|
85
99
|
function hasToolCall(message: unknown): boolean {
|
|
86
100
|
return typeof message === "object" && message !== null && Array.isArray((message as { content?: unknown }).content) && (message as { content: unknown[] }).content.some((part) => typeof part === "object" && part !== null && (part as { type?: unknown }).type === "toolCall");
|
|
87
101
|
}
|
|
88
102
|
|
|
89
|
-
function latestAssistantHasToolCall(
|
|
90
|
-
const message = [...messages].reverse().find((item) => item.role === "assistant");
|
|
91
|
-
return hasToolCall(message);
|
|
92
|
-
}
|
|
103
|
+
function latestAssistantHasToolCall(message: WorkflowAgentMessage | undefined): boolean { return hasToolCall(message); }
|
|
93
104
|
|
|
94
105
|
type TerminalProviderError = { provider: string; model: string; error: string };
|
|
95
|
-
function throwIfTerminalAssistantError(session:
|
|
96
|
-
const message = [...session.messages].reverse().find((item) => item.role === "assistant");
|
|
106
|
+
function throwIfTerminalAssistantError(session: WorkflowAgentSession, message: WorkflowAgentMessage | undefined): void {
|
|
97
107
|
if (message?.stopReason !== "error") return;
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const error = message.errorMessage ?? "Native Pi assistant ended with a terminal provider error";
|
|
108
|
+
const state = session.getState();
|
|
109
|
+
const error = message.errorMessage ?? "Workflow agent session ended with a terminal provider error";
|
|
101
110
|
const failure = new WorkflowError("AGENT_FAILED", error);
|
|
102
|
-
Object.defineProperty(failure, "terminalProviderError", { value: { provider, model, error }, configurable: true });
|
|
111
|
+
Object.defineProperty(failure, "terminalProviderError", { value: { provider: state.model.provider, model: state.model.model, error }, configurable: true });
|
|
103
112
|
throw failure;
|
|
104
113
|
}
|
|
105
114
|
function terminalProviderError(error: WorkflowError): TerminalProviderError | undefined {
|
|
@@ -110,10 +119,10 @@ function terminalProviderError(error: WorkflowError): TerminalProviderError | un
|
|
|
110
119
|
}
|
|
111
120
|
type ProviderRecoveryMarker = { providerRecoveryHandled?: boolean; providerRecovery?: AgentProviderRecovery; providerRecoveryFailed?: boolean };
|
|
112
121
|
const providerContinuationPrompt = "The provider error was transient. Continue the task from your current state.";
|
|
113
|
-
async function recoverTerminalProviderError(session:
|
|
122
|
+
async function recoverTerminalProviderError(session: WorkflowAgentSession, label: string, recovery: AgentExecutionOptions["providerErrorRecovery"], continuePrompt: () => Promise<void>, getAssistant: () => WorkflowAgentMessage | undefined): Promise<boolean> {
|
|
114
123
|
let continued = false;
|
|
115
124
|
for (;;) {
|
|
116
|
-
try { throwIfTerminalAssistantError(session,
|
|
125
|
+
try { throwIfTerminalAssistantError(session, getAssistant()); return continued; }
|
|
117
126
|
catch (error) {
|
|
118
127
|
const typed = error instanceof WorkflowError ? error : new WorkflowError("AGENT_FAILED", error instanceof Error ? error.message : String(error));
|
|
119
128
|
const terminal = terminalProviderError(typed);
|
|
@@ -127,7 +136,7 @@ async function recoverTerminalProviderError(session: NativeSession, fallbackMode
|
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
function accounting(stats:
|
|
139
|
+
function accounting(stats: WorkflowAgentSessionStats): AgentAccounting {
|
|
131
140
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
132
141
|
}
|
|
133
142
|
function canonicalSourcePath(path: string): string { try { return realpathSync(path); } catch { return resolve(path); } }
|
|
@@ -139,7 +148,7 @@ function workflowSystemPromptPath(cwd: string, agentDir: string, projectTrusted:
|
|
|
139
148
|
return globalPaths.find((path) => existsSync(path));
|
|
140
149
|
}
|
|
141
150
|
|
|
142
|
-
export async function
|
|
151
|
+
export async function createLocalPiSession(input: SessionInput): Promise<PiSession> {
|
|
143
152
|
const agentDir = input.agentDir ?? getAgentDir();
|
|
144
153
|
const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
|
|
145
154
|
const systemPromptOptions = input.systemPrompt !== undefined ? { systemPromptOverride: () => input.systemPrompt } : systemPromptSource !== undefined ? { systemPrompt: systemPromptSource } : {};
|
|
@@ -194,8 +203,60 @@ export async function createNativeAgentSession(input: SessionInput): Promise<Nat
|
|
|
194
203
|
return Object.assign(session, {
|
|
195
204
|
getLeafId: () => manager.getLeafId(),
|
|
196
205
|
getToolDefinitions: () => session.getAllTools().map(({ name, description, parameters, promptGuidelines }) => ({ name, description, parameters, ...(promptGuidelines ? { promptGuidelines } : {}) })),
|
|
197
|
-
}) as unknown as
|
|
206
|
+
}) as unknown as PiSession;
|
|
207
|
+
}
|
|
208
|
+
function workflowAgentMessage(message: AgentMessage | undefined): WorkflowAgentMessage | undefined { return message ? { role: message.role, ...(message.content === undefined ? {} : { content: message.content }), ...(message.stopReason === undefined ? {} : { stopReason: message.stopReason }), ...(message.errorMessage === undefined ? {} : { errorMessage: message.errorMessage }), ...(message.usage === undefined ? {} : { usage: message.usage }) } : undefined; }
|
|
209
|
+
function latestUsableAssistant(messages: readonly AgentMessage[]): WorkflowAgentMessage | undefined { for (let index = messages.length - 1; index >= 0; index -= 1) { const candidate = workflowAgentMessage(messages[index]); if (candidate?.role === "assistant" && !isEmptyAbortedAssistant(candidate)) return candidate; } return undefined; }
|
|
210
|
+
function workflowAgentStats(stats: ReturnType<PiSession["getSessionStats"]>): WorkflowAgentSessionStats { return { tokens: { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, total: stats.tokens.total }, cost: stats.cost }; }
|
|
211
|
+
function workflowAgentState(native: PiSession, prepared: Readonly<PreparedAgentSession>): WorkflowAgentSessionState {
|
|
212
|
+
const tools = native.agent?.state.tools.map(({ name }) => name) ?? prepared.tools;
|
|
213
|
+
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 };
|
|
214
|
+
return { model, ...(model.thinking ? { thinking: model.thinking } : {}), tools: [...tools], ...(native.systemPrompt === undefined ? {} : { systemPrompt: native.systemPrompt }) };
|
|
198
215
|
}
|
|
216
|
+
function localSessionEvent(event: unknown): WorkflowAgentSessionEvent { return event as WorkflowAgentSessionEvent; }
|
|
217
|
+
export async function createLocalWorkflowAgentSession(prepared: Readonly<PreparedAgentSession>, context: Readonly<AgentTransportContext>): Promise<WorkflowAgentSession> {
|
|
218
|
+
void context;
|
|
219
|
+
const input: SessionInput = {
|
|
220
|
+
cwd: prepared.cwd, model: { ...prepared.model }, tools: [...prepared.tools] as SessionInput["tools"], sessionLabel: prepared.sessionLabel,
|
|
221
|
+
...(prepared.agentDir ? { agentDir: prepared.agentDir } : {}), ...(prepared.customTools?.length ? { customTools: [...prepared.customTools] as NonNullable<SessionInput["customTools"]> } : {}),
|
|
222
|
+
...(prepared.resultTool ? { resultTool: prepared.resultTool } : {}), ...(prepared.systemPrompt === undefined ? {} : { systemPrompt: prepared.systemPrompt }),
|
|
223
|
+
...(prepared.systemPromptAppend ? { systemPromptAppend: prepared.systemPromptAppend } : {}), ...(prepared.extensionFactories?.length ? { extensionFactories: [...prepared.extensionFactories] } : {}),
|
|
224
|
+
...(prepared.additionalSkillPaths?.length ? { additionalSkillPaths: [...prepared.additionalSkillPaths] } : {}), ...(prepared.resourcePolicy ? { resourcePolicy: structuredClone(prepared.resourcePolicy) } : {}), ...(prepared.options ? { options: { ...prepared.options } } : {}),
|
|
225
|
+
};
|
|
226
|
+
const native = await createLocalPiSession(input);
|
|
227
|
+
let disposal: Promise<void> | undefined;
|
|
228
|
+
let aborting: Promise<void> | undefined;
|
|
229
|
+
let prompting: Promise<WorkflowAgentTurnResult> | undefined;
|
|
230
|
+
let disposed = false;
|
|
231
|
+
const startAbort = () => aborting ??= Promise.resolve().then(() => native.abort?.()).then(() => undefined).finally(() => { aborting = undefined; });
|
|
232
|
+
const reference: WorkflowAgentSessionReference = { transport: "local", sessionId: native.sessionId, ...(native.sessionFile ? { locator: { sessionFile: native.sessionFile } } : {}) };
|
|
233
|
+
const session = {
|
|
234
|
+
reference,
|
|
235
|
+
getState: () => Object.freeze(workflowAgentState(native, prepared)),
|
|
236
|
+
getSessionStats: () => workflowAgentStats(native.getSessionStats()),
|
|
237
|
+
subscribe(listener: (event: WorkflowAgentSessionEvent) => void) { listener({ type: "state_changed", state: workflowAgentState(native, prepared) }); return native.subscribe?.((event) => { listener(localSessionEvent(event)); }) ?? (() => undefined); },
|
|
238
|
+
getLastAssistant: () => latestUsableAssistant(native.messages),
|
|
239
|
+
async prompt(text: string) {
|
|
240
|
+
if (disposed) throw new WorkflowError("INTERNAL_ERROR", "Local workflow session is disposed");
|
|
241
|
+
const prompt = Promise.resolve().then(async () => { await native.prompt(text); const assistant = latestUsableAssistant(native.messages); return assistant ? { assistant } : {}; });
|
|
242
|
+
prompting = prompt;
|
|
243
|
+
try { return await prompt; } finally { if (prompting === prompt) prompting = undefined; }
|
|
244
|
+
},
|
|
245
|
+
async steer(text: string) { if (!native.steer) throw new WorkflowError("INTERNAL_ERROR", "Local workflow session does not support steering"); await native.steer(text); },
|
|
246
|
+
async abort() { if (disposed) return; await startAbort(); },
|
|
247
|
+
async dispose() {
|
|
248
|
+
disposal ??= (async () => {
|
|
249
|
+
disposed = true;
|
|
250
|
+
try { await startAbort(); } catch { /* Abort failure must not prevent the prompt from settling. */ }
|
|
251
|
+
try { await prompting; } catch { /* Prompt rejection is expected after abort during teardown. */ }
|
|
252
|
+
native.dispose();
|
|
253
|
+
})();
|
|
254
|
+
await disposal;
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
return session;
|
|
258
|
+
}
|
|
259
|
+
export const localAgentTransport: AgentTransport = Object.freeze({ id: "local", createSession: createLocalWorkflowAgentSession });
|
|
199
260
|
function changedOption(options: Readonly<Record<string, JsonValue>>, baseline: Readonly<Record<string, JsonValue>>, key: string): boolean { return JSON.stringify(options[key]) !== JSON.stringify(baseline[key]); }
|
|
200
261
|
function validThinking(value: unknown): value is ThinkingLevel { return typeof value === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(value); }
|
|
201
262
|
interface ChildAgentToolParams {
|
|
@@ -229,35 +290,77 @@ function fallbackSetupContext(root: AgentExecutionRoot, options: AgentExecutionO
|
|
|
229
290
|
function resourcePolicySummary(policy: AgentResourcePolicy): NonNullable<AgentSetupSummary["disabledAgentResources"]> {
|
|
230
291
|
return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], excludedSkills: [...(policy.excludedSkills ?? [])], excludedExtensions: [...(policy.excludedExtensions ?? [])], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
|
|
231
292
|
}
|
|
232
|
-
|
|
293
|
+
function resourcePolicyWidened(ceiling: AgentResourcePolicy | undefined, candidate: AgentResourcePolicy | undefined): boolean {
|
|
294
|
+
if (!ceiling) return false;
|
|
295
|
+
if (!candidate) return true;
|
|
296
|
+
if (!ceiling.projectTrusted && candidate.projectTrusted) return true;
|
|
297
|
+
return ceiling.effective.skills.some((pattern) => !candidate.effective.skills.includes(pattern)) || ceiling.effective.extensions.some((pattern) => !candidate.effective.extensions.includes(pattern));
|
|
298
|
+
}
|
|
299
|
+
function preparedAgentSession(input: SessionInput): Readonly<PreparedAgentSession> {
|
|
300
|
+
const prepared = {
|
|
301
|
+
cwd: input.cwd, model: Object.freeze({ ...input.model }), tools: Object.freeze([...input.tools]), sessionLabel: input.sessionLabel,
|
|
302
|
+
...(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)) } : {}),
|
|
303
|
+
...(input.systemPrompt === undefined ? {} : { systemPrompt: input.systemPrompt }), ...(input.systemPromptAppend ? { systemPromptAppend: input.systemPromptAppend } : {}),
|
|
304
|
+
...(input.extensionFactories?.length ? { extensionFactories: Object.freeze([...input.extensionFactories]) } : {}), ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: Object.freeze([...input.additionalSkillPaths]) } : {}),
|
|
305
|
+
...(input.resourcePolicy ? { resourcePolicy: Object.freeze(structuredClone(input.resourcePolicy)) } : {}),
|
|
306
|
+
};
|
|
307
|
+
return deepFreeze(prepared);
|
|
308
|
+
}
|
|
309
|
+
function agentSetupSummary(setup: AgentSetup, hookNames: readonly string[]): AgentSetupSummary {
|
|
310
|
+
const model = setup.sessionInput.model;
|
|
311
|
+
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) } : {}) };
|
|
312
|
+
}
|
|
313
|
+
type PreparedAgentSetup = { setup: AgentSetup; summary: AgentSetupSummary; failure?: { error: unknown } };
|
|
314
|
+
async function prepareAgentSetup(root: AgentExecutionRoot, transport: AgentTransport, task: string, options: AgentExecutionOptions, resolved: { model: ModelSpec; tools: readonly string[]; systemPrompt?: string; systemPromptAppend: string }, cwd: string, attempt: number, signal: AbortSignal | undefined, customTools: readonly ToolDefinition[], resultTool: ToolDefinition | undefined): Promise<PreparedAgentSetup> {
|
|
233
315
|
const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
|
|
234
316
|
const baselineOptions = structuredClone(options.agentOptions ?? {});
|
|
235
317
|
const baseResourcePolicy = await root.agentResourcePolicy?.();
|
|
236
318
|
const roleExclusions = options.role ? root.agentDefinitions?.[options.role]?.disabledAgentResources : undefined;
|
|
237
319
|
const resourcePolicy = baseResourcePolicy && roleExclusions ? { ...baseResourcePolicy, effective: mergeAgentResourceExclusions(baseResourcePolicy.effective, roleExclusions) } : baseResourcePolicy;
|
|
320
|
+
const resourcePolicyCeiling = resourcePolicy ? structuredClone(resourcePolicy) : undefined;
|
|
238
321
|
const sessionInput: SessionInput = { cwd, model: { ...resolved.model }, tools: [...resolved.tools], sessionLabel: `${options.workflowName}:${options.label}:attempt-${String(attempt)}`, ...(root.agentDir ? { agentDir: root.agentDir } : {}), ...(root.additionalSkillPaths?.length ? { additionalSkillPaths: [...root.additionalSkillPaths] } : {}), ...(customTools.length ? { customTools: [...customTools] } : {}), ...(resultTool ? { resultTool } : {}), ...(resolved.systemPrompt !== undefined ? { systemPrompt: resolved.systemPrompt } : {}), systemPromptAppend: resolved.systemPromptAppend, ...(resourcePolicy ? { resourcePolicy } : {}), options: structuredClone(baselineOptions) };
|
|
239
|
-
const setup
|
|
322
|
+
const setup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, prepared: preparedAgentSession(sessionInput), transport };
|
|
240
323
|
const base = fallbackSetupContext(root, options, setupSignal);
|
|
241
324
|
const context = Object.freeze({ run: base.run, identity: base.identity, attempt, signal: setupSignal });
|
|
242
325
|
const hookNames: string[] = [];
|
|
243
326
|
for (const hook of [...(root.agentSetupHooks ?? [])].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))) {
|
|
244
|
-
if (setupSignal.aborted)
|
|
245
|
-
try { await hook.setup(setup, context); } catch (error) {
|
|
327
|
+
if (setupSignal.aborted) return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error: new WorkflowError("CANCELLED", "Agent cancelled") } };
|
|
328
|
+
try { await hook.setup(setup, context); } catch (error) {
|
|
329
|
+
setup.prepared = preparedAgentSession(setup.sessionInput);
|
|
330
|
+
return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error: setupSignal.reason !== undefined ? new WorkflowError("CANCELLED", "Agent cancelled") : error } };
|
|
331
|
+
}
|
|
332
|
+
setup.prepared = preparedAgentSession(setup.sessionInput);
|
|
246
333
|
hookNames.push(hook.name);
|
|
247
|
-
if (setupSignal.reason !== undefined)
|
|
334
|
+
if (setupSignal.reason !== undefined) return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error: new WorkflowError("CANCELLED", "Agent cancelled") } };
|
|
248
335
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
336
|
+
try {
|
|
337
|
+
if (resourcePolicyWidened(resourcePolicyCeiling, setup.sessionInput.resourcePolicy)) throw new WorkflowError("INVALID_METADATA", "Agent setup widened the prepared resource policy");
|
|
338
|
+
setup.sessionInput.options = setup.options;
|
|
339
|
+
if (changedOption(setup.options, baselineOptions, "model") && typeof setup.options.model === "string") setup.sessionInput.model = parseModel(setup.options.model, setup.sessionInput.model, changedOption(setup.options, baselineOptions, "thinking") && validThinking(setup.options.thinking) ? setup.options.thinking : undefined, root.modelAliases, root.knownModels ?? root.availableModels, root.settingsPath);
|
|
340
|
+
if (changedOption(setup.options, baselineOptions, "thinking") && validThinking(setup.options.thinking)) setup.sessionInput.model = { ...setup.sessionInput.model, thinking: setup.options.thinking };
|
|
341
|
+
if (changedOption(setup.options, baselineOptions, "tools") && Array.isArray(setup.options.tools) && setup.options.tools.every((tool) => typeof tool === "string")) setup.sessionInput.tools = [...setup.options.tools];
|
|
342
|
+
if (changedOption(setup.options, baselineOptions, "cwd") && typeof setup.options.cwd === "string") setup.sessionInput.cwd = setup.options.cwd;
|
|
343
|
+
const customToolNames = new Set([...(setup.sessionInput.customTools ?? []).map(({ name }) => name), ...(setup.sessionInput.resultTool ? [setup.sessionInput.resultTool.name] : [])]);
|
|
344
|
+
const widened = setup.sessionInput.tools.find((tool) => !resolved.tools.includes(tool) && !customToolNames.has(tool));
|
|
345
|
+
const outsideTool = widened ?? setup.sessionInput.tools.find((tool) => !root.tools.has(tool) && !customToolNames.has(tool));
|
|
346
|
+
if (outsideTool) throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${outsideTool}`);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
setup.prepared = preparedAgentSession(setup.sessionInput);
|
|
349
|
+
return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error } };
|
|
350
|
+
}
|
|
351
|
+
setup.prepared = preparedAgentSession(setup.sessionInput);
|
|
352
|
+
return { setup, summary: agentSetupSummary(setup, hookNames) };
|
|
353
|
+
}
|
|
354
|
+
function attemptRecord(transport: string, attempt: number, session: WorkflowAgentSession, setup: AgentSetupSummary, stats: AgentAccounting, result?: JsonValue, error?: { code: string; message: string }): AgentAttempt {
|
|
355
|
+
return { attempt, transport, session: session.reference, ...(result === undefined ? {} : { result }), ...(error ? { error } : {}), accounting: stats, setup };
|
|
356
|
+
}
|
|
357
|
+
function errorWithAttempts(error: unknown, attempts: readonly AgentAttempt[]): Error {
|
|
358
|
+
const typed = error instanceof Error ? error : new Error(typeof error === "string" ? error : String(error));
|
|
359
|
+
return Object.assign(typed, { attempts });
|
|
257
360
|
}
|
|
258
|
-
|
|
259
361
|
export class WorkflowAgentExecutor {
|
|
260
|
-
|
|
362
|
+
private readonly transport: AgentTransport;
|
|
363
|
+
constructor(private readonly root: AgentExecutionRoot, transport: AgentTransport = localAgentTransport) { this.transport = transport; }
|
|
261
364
|
setRunContext(runContext: Readonly<WorkflowRunContext>): void { this.root.runContext = runContext; }
|
|
262
365
|
|
|
263
366
|
resolve(options: AgentExecutionOptions, inheritedTools?: readonly string[]): { model: ModelSpec; requestedModel?: string; tools: readonly string[]; systemPrompt?: string; systemPromptAppend: string } {
|
|
@@ -308,15 +411,15 @@ export class WorkflowAgentExecutor {
|
|
|
308
411
|
const attempts: AgentAttempt[] = [];
|
|
309
412
|
let maxAttempts = (options.retries ?? 0) + 1;
|
|
310
413
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
414
|
+
const attemptSignal = executionSignal ?? new AbortController().signal;
|
|
311
415
|
if (recoveryModel) resolved = this.resolve({ ...options, modelOverride: recoveryModel });
|
|
312
416
|
options.budget?.beforeAttempt();
|
|
313
417
|
let schemaResult: JsonValue | undefined;
|
|
314
|
-
let session:
|
|
418
|
+
let session: WorkflowAgentSession | undefined;
|
|
315
419
|
let setup: AgentSetup | undefined;
|
|
316
|
-
let setupSummary: AgentSetupSummary
|
|
420
|
+
let setupSummary: AgentSetupSummary = { hookNames: [], model: { ...resolved.model }, tools: [...resolved.tools], cwd };
|
|
317
421
|
let setupFailed = false;
|
|
318
422
|
let budgetError: WorkflowError | undefined;
|
|
319
|
-
let turnStarted = false;
|
|
320
423
|
const hasSchemaResult = () => schemaResult !== undefined;
|
|
321
424
|
const resultTool = options.schema ? {
|
|
322
425
|
name: "workflow_result", label: "Workflow Result", description: "Submit the terminal structured workflow result", parameters: Type.Unsafe(options.schema),
|
|
@@ -324,7 +427,8 @@ export class WorkflowAgentExecutor {
|
|
|
324
427
|
if (!Value.Check(options.schema as object, value)) return { content: [{ type: "text" as const, text: "Result does not match the required schema." }], details: {}, isError: true };
|
|
325
428
|
if (schemaResult !== undefined) return { content: [{ type: "text" as const, text: "Result has already been accepted." }], details: {}, isError: true };
|
|
326
429
|
schemaResult = structuredClone(value) as JsonValue;
|
|
327
|
-
|
|
430
|
+
const currentSession = session;
|
|
431
|
+
if (currentSession) void currentSession.abort();
|
|
328
432
|
return { content: [{ type: "text" as const, text: "Result accepted." }], details: {} };
|
|
329
433
|
},
|
|
330
434
|
} as ToolDefinition : undefined;
|
|
@@ -337,13 +441,15 @@ export class WorkflowAgentExecutor {
|
|
|
337
441
|
let systemPromptTurn = 0;
|
|
338
442
|
let systemPromptWrite = Promise.resolve();
|
|
339
443
|
let systemPromptWriteError: unknown;
|
|
444
|
+
let turnStarted = false;
|
|
445
|
+
let completedAttempt: AgentAttempt | undefined;
|
|
340
446
|
const flushSystemPrompts = async () => {
|
|
341
447
|
await systemPromptWrite;
|
|
342
448
|
if (systemPromptWriteError) throw new WorkflowError("INTERNAL_ERROR", `Failed to persist effective system prompt: ${systemPromptWriteError instanceof Error ? systemPromptWriteError.message : typeof systemPromptWriteError === "string" ? systemPromptWriteError : "unknown error"}`);
|
|
343
449
|
};
|
|
344
450
|
const report = (persist: boolean) => {
|
|
345
451
|
if (!session || !options.onProgress) return;
|
|
346
|
-
const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], ...(activity ? { activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }), persist };
|
|
452
|
+
const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], state: session.getState(), ...(activity ? { activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }), persist };
|
|
347
453
|
if (lastEventAt !== undefined) lastReportedEventAt = lastEventAt;
|
|
348
454
|
progress = progress.then(() => options.onProgress?.(update)).then(() => undefined);
|
|
349
455
|
};
|
|
@@ -354,71 +460,90 @@ export class WorkflowAgentExecutor {
|
|
|
354
460
|
const activityChanged = (previous: AgentActivity | undefined) => previous?.kind !== activity?.kind || previous?.text !== activity?.text;
|
|
355
461
|
try {
|
|
356
462
|
setupFailed = true;
|
|
357
|
-
const prepared = await prepareAgentSetup(this.root, this.
|
|
463
|
+
const prepared = await prepareAgentSetup(this.root, this.transport, task, options, resolved, cwd, attempt, attemptSignal, customTools, resultTool);
|
|
358
464
|
setup = prepared.setup;
|
|
359
465
|
setupSummary = prepared.summary;
|
|
466
|
+
if (prepared.failure) throw prepared.failure.error;
|
|
360
467
|
setupFailed = false;
|
|
361
|
-
if (
|
|
468
|
+
if (attemptSignal.aborted) throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
362
469
|
const started = Date.now();
|
|
363
|
-
|
|
470
|
+
const transportSignal = attemptSignal;
|
|
471
|
+
await options.onAttempt?.({ attempt, transport: setup.transport.id, accounting: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 }, setup: setupSummary });
|
|
472
|
+
const transportBase = fallbackSetupContext(this.root, options, transportSignal);
|
|
473
|
+
const createdSession = await setup.transport.createSession(setup.prepared, Object.freeze({ run: transportBase.run, identity: transportBase.identity, attempt, signal: transportSignal }));
|
|
474
|
+
if (createdSession.reference.transport !== setup.transport.id) {
|
|
475
|
+
await createdSession.dispose();
|
|
476
|
+
throw new WorkflowError("INTERNAL_ERROR", `Agent transport ${setup.transport.id} created a session for ${createdSession.reference.transport}`);
|
|
477
|
+
}
|
|
478
|
+
session = createdSession;
|
|
479
|
+
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 });
|
|
480
|
+
const preparedTools = new Set([...setup.prepared.tools, ...(setup.prepared.customTools ?? []).map(({ name }) => name), ...(setup.prepared.resultTool ? [setup.prepared.resultTool.name] : [])]);
|
|
481
|
+
if (session.getState().tools.some((tool) => !preparedTools.has(tool))) throw new WorkflowError("INTERNAL_ERROR", `Agent transport ${setup.transport.id} widened the prepared tool policy`);
|
|
364
482
|
if (setup.sessionInput.resourcePolicy) setupSummary = { ...setupSummary, disabledAgentResources: resourcePolicySummary(setup.sessionInput.resourcePolicy) };
|
|
365
|
-
const includeAttemptSetup = Boolean(this.root.agentSetupHooks?.length || setup.sessionInput.resourcePolicy);
|
|
366
|
-
await options.onAttempt?.({ attempt, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), ...(includeAttemptSetup ? { setup: setupSummary } : {}) });
|
|
367
483
|
const activeSession = session;
|
|
368
|
-
|
|
369
|
-
const
|
|
484
|
+
let lastAssistant: WorkflowAgentMessage | undefined;
|
|
485
|
+
const acceptAssistant = (candidate: WorkflowAgentMessage | undefined) => {
|
|
486
|
+
if (isEmptyAbortedAssistant(candidate)) {
|
|
487
|
+
const previous = (activeSession as WorkflowAgentSession & { getLastAssistant?: () => WorkflowAgentMessage | undefined }).getLastAssistant?.();
|
|
488
|
+
if (previous && !isEmptyAbortedAssistant(previous)) lastAssistant = previous;
|
|
489
|
+
} else if (candidate) lastAssistant = candidate;
|
|
490
|
+
};
|
|
491
|
+
const recoverTerminal = () => recoverTerminalProviderError(activeSession, options.label, options.providerErrorRecovery, async () => { try { acceptAssistant((await promptWithProviderPause(activeSession, providerContinuationPrompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant); } catch (error) { acceptAssistant((activeSession as WorkflowAgentSession & { getLastAssistant?: () => WorkflowAgentMessage | undefined }).getLastAssistant?.() ?? lastAssistant); if (!hasSchemaResult()) throw error; } }, () => lastAssistant);
|
|
370
492
|
const promptAndRecover = async (prompt: string): Promise<void> => {
|
|
371
493
|
let promptFailed = false;
|
|
372
494
|
let promptError: unknown;
|
|
373
|
-
try { await promptWithProviderPause(activeSession, prompt, remaining(options.timeoutMs, started),
|
|
495
|
+
try { acceptAssistant((await promptWithProviderPause(activeSession, prompt, remaining(options.timeoutMs, started), attemptSignal, this.root.providerPause)).assistant); } catch (error) { acceptAssistant((activeSession as WorkflowAgentSession & { getLastAssistant?: () => WorkflowAgentMessage | undefined }).getLastAssistant?.() ?? lastAssistant); promptFailed = true; promptError = error; }
|
|
374
496
|
const recovered = await recoverTerminal();
|
|
375
497
|
if (promptFailed && !hasSchemaResult() && !recovered) throw promptError;
|
|
376
498
|
};
|
|
377
|
-
unsubscribe = activeSession.subscribe
|
|
499
|
+
unsubscribe = activeSession.subscribe((event) => {
|
|
378
500
|
lastEventAt = Date.now();
|
|
379
501
|
let persist = false;
|
|
380
502
|
let shouldReport = false;
|
|
381
503
|
let removeToolCallId: string | undefined;
|
|
382
|
-
if (event.type === "agent_start" && session?.systemPrompt !== undefined) {
|
|
504
|
+
if (event.type === "agent_start" && session?.getState().systemPrompt !== undefined) {
|
|
383
505
|
if (this.root.runStore) {
|
|
384
506
|
systemPromptTurn += 1;
|
|
385
|
-
const entry = { sessionId: session.sessionId, attempt, turn: systemPromptTurn, prompt: session.systemPrompt };
|
|
507
|
+
const entry = { sessionId: session.reference.sessionId, attempt, turn: systemPromptTurn, prompt: session.getState().systemPrompt ?? "" };
|
|
386
508
|
systemPromptWrite = systemPromptWrite.then(() => this.root.runStore?.recordSystemPrompt(entry)).then(() => undefined).catch((error: unknown) => { systemPromptWriteError ??= error; });
|
|
387
509
|
}
|
|
388
510
|
}
|
|
389
|
-
if (event.type === "
|
|
390
|
-
|
|
511
|
+
if (event.type === "state_changed") { shouldReport = true; persist = true; }
|
|
512
|
+
if (event.type === "turnStarted" || event.type === "turn_started") { if (!turnStarted) { try { options.budget?.beforeTurn(); turnStarted = true; } catch (error) { budgetError ??= error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", error instanceof Error ? error.message : String(error)); void activeSession.abort(); } } }
|
|
513
|
+
if (event.type === "message_start" && event.message?.role === "assistant") {
|
|
514
|
+
if (!turnStarted) { try { options.budget?.beforeTurn(); turnStarted = true; } catch (error) { budgetError ??= error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", error instanceof Error ? error.message : String(error)); void activeSession.abort(); } }
|
|
391
515
|
activity = { kind: "text", text: "responding" };
|
|
392
516
|
shouldReport = true;
|
|
393
517
|
}
|
|
394
518
|
if (event.type === "message_update") {
|
|
395
519
|
const previousActivity = activity;
|
|
396
|
-
|
|
397
|
-
|
|
520
|
+
const updateType = event.assistantMessageEvent?.type;
|
|
521
|
+
if (updateType && ["thinking_start", "thinking_delta", "thinking_end"].includes(updateType)) activity = { kind: "reasoning", text: "reasoning" };
|
|
522
|
+
else if (updateType && ["text_start", "text_delta", "text_end", "toolcall_start", "toolcall_delta", "toolcall_end"].includes(updateType)) activity = { kind: "text", text: "responding" };
|
|
398
523
|
shouldReport = activityChanged(previousActivity);
|
|
399
524
|
}
|
|
400
525
|
if (event.type === "message_end") {
|
|
401
526
|
const previousActivity = activity;
|
|
402
527
|
activity = undefined;
|
|
403
528
|
shouldReport = activityChanged(previousActivity);
|
|
404
|
-
if (event.message
|
|
529
|
+
if (event.message?.role === "assistant") {
|
|
530
|
+
acceptAssistant(event.message);
|
|
405
531
|
const needsMoreWork = hasToolCall(event.message);
|
|
406
532
|
const final = !needsMoreWork || (options.schema !== undefined && hasSchemaResult());
|
|
407
|
-
if (!budgetError) { try { options.budget?.afterTurn(accounting(activeSession.getSessionStats()), final); if (!final) { const instruction = options.budget?.instruction(); if (instruction) void
|
|
533
|
+
if (!budgetError) { try { options.budget?.afterTurn(accounting(activeSession.getSessionStats()), final); if (!final) { const instruction = options.budget?.instruction(); if (instruction) void activeSession.steer(instruction); } } catch (error) { budgetError ??= error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", error instanceof Error ? error.message : String(error)); void activeSession.abort(); } }
|
|
408
534
|
turnStarted = false;
|
|
409
535
|
persist = true;
|
|
410
536
|
}
|
|
411
537
|
}
|
|
412
|
-
if (event.type === "tool_execution_start") { toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: "running" }); activity = { kind: "tool", text: event.toolName }; shouldReport = true; }
|
|
413
|
-
if (event.type === "tool_execution_update") { const previousActivity = activity; activity = { kind: "tool", text: event.toolName }; shouldReport = activityChanged(previousActivity); }
|
|
414
|
-
if (event.type === "tool_execution_end") { toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: event.isError ? "failed" : "completed" }); if (activity?.kind === "tool" && activity.text === event.toolName) activity = undefined; shouldReport = true; removeToolCallId = event.toolCallId; }
|
|
538
|
+
if (event.type === "tool_execution_start" && event.toolCallId && event.toolName) { toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: "running" }); activity = { kind: "tool", text: event.toolName }; shouldReport = true; }
|
|
539
|
+
if (event.type === "tool_execution_update" && event.toolName) { const previousActivity = activity; activity = { kind: "tool", text: event.toolName }; shouldReport = activityChanged(previousActivity); }
|
|
540
|
+
if (event.type === "tool_execution_end" && event.toolCallId && event.toolName) { toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: event.isError ? "failed" : "completed" }); if (activity?.kind === "tool" && activity.text === event.toolName) activity = undefined; shouldReport = true; removeToolCallId = event.toolCallId; }
|
|
415
541
|
if (shouldReport || persist) report(persist); else reportTimestamp();
|
|
416
542
|
if (removeToolCallId) toolCalls.delete(removeToolCallId);
|
|
417
543
|
});
|
|
418
544
|
report(false);
|
|
419
545
|
if (setSteer) {
|
|
420
|
-
|
|
421
|
-
setSteer((message) => session?.steer?.(message));
|
|
546
|
+
setSteer((message) => activeSession.steer(message));
|
|
422
547
|
}
|
|
423
548
|
const context = [`Workflow: ${options.workflowName}`, `Agent: ${options.label}`, options.phase ? `Phase: ${options.phase}` : "", options.parent ? `Parent: ${options.parent}` : "", "You own this task and any direct child agents you create. Return child results to your parent; do not leave descendants running.", attempt > 1 ? `Retry attempt ${String(attempt)}. Previous state: ${options.retryState ?? attempts.at(-1)?.error?.message ?? "failed attempt"}` : ""].filter(Boolean).join("\n");
|
|
424
549
|
const instruction = options.budget?.instruction();
|
|
@@ -426,7 +551,7 @@ export class WorkflowAgentExecutor {
|
|
|
426
551
|
options.budget?.beforeTurn();
|
|
427
552
|
turnStarted = true;
|
|
428
553
|
await promptAndRecover(promptText);
|
|
429
|
-
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(
|
|
554
|
+
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(lastAssistant)); turnStarted = false; }
|
|
430
555
|
if (budgetError) throw budgetError;
|
|
431
556
|
if (options.schema) {
|
|
432
557
|
if (!hasSchemaResult()) {
|
|
@@ -443,29 +568,39 @@ export class WorkflowAgentExecutor {
|
|
|
443
568
|
}
|
|
444
569
|
if (schemaResult === undefined) throw new WorkflowError("RESULT_INVALID", "Agent did not submit a valid workflow_result after one repair");
|
|
445
570
|
}
|
|
446
|
-
const value = options.schema ? schemaResult as JsonValue : text(
|
|
571
|
+
const value = options.schema ? schemaResult as JsonValue : text(lastAssistant);
|
|
447
572
|
if (options.worktreeOwner) await this.root.runStore?.snapshotWorktree(options.worktreeOwner);
|
|
448
573
|
report(true);
|
|
449
574
|
await progress;
|
|
450
575
|
await flushSystemPrompts();
|
|
451
|
-
unsubscribe
|
|
576
|
+
unsubscribe();
|
|
452
577
|
const attemptAccounting = accounting(session.getSessionStats());
|
|
453
|
-
|
|
454
|
-
attempts.push(
|
|
455
|
-
session.dispose();
|
|
578
|
+
completedAttempt = attemptRecord(setup.transport.id, attempt, session, setupSummary, attemptAccounting, value);
|
|
579
|
+
attempts.push(completedAttempt);
|
|
580
|
+
try { await options.onAttempt?.(completedAttempt); } finally { await session.dispose(); }
|
|
456
581
|
return { value, attempts, cwd: setupSummary.cwd };
|
|
457
582
|
} catch (error) {
|
|
458
|
-
|
|
583
|
+
if (completedAttempt) throw errorWithAttempts(error, attempts);
|
|
584
|
+
const typed = budgetError ?? (error instanceof WorkflowError ? error : new WorkflowError(attemptSignal.aborted && setupFailed ? "CANCELLED" : "AGENT_FAILED", error instanceof Error ? error.message : String(error)));
|
|
585
|
+
if (!session) {
|
|
586
|
+
const failedAttempt: AgentAttempt = { attempt, transport: setup?.transport.id ?? this.transport.id, accounting: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 }, error: { code: typed.code, message: typed.message }, setup: setupSummary };
|
|
587
|
+
attempts.push(failedAttempt);
|
|
588
|
+
try { await options.onAttempt?.(failedAttempt); } catch (persistenceError) { throw errorWithAttempts(persistenceError, attempts); }
|
|
589
|
+
}
|
|
459
590
|
if (session) {
|
|
460
591
|
report(true);
|
|
461
|
-
await progress;
|
|
592
|
+
await progress.catch(() => undefined);
|
|
462
593
|
try { await flushSystemPrompts(); } catch { /* Preserve the agent failure that prompted this cleanup. */ }
|
|
463
594
|
unsubscribe?.();
|
|
464
595
|
const attemptAccounting = accounting(session.getSessionStats());
|
|
465
596
|
if (!budgetError && typed.code !== "BUDGET_EXHAUSTED") { try { options.budget?.afterTurn(attemptAccounting, true); } catch (budgetFailure) { budgetError ??= budgetFailure instanceof WorkflowError ? budgetFailure : new WorkflowError("BUDGET_EXHAUSTED", budgetFailure instanceof Error ? budgetFailure.message : String(budgetFailure)); } }
|
|
466
|
-
const
|
|
467
|
-
attempts.push(
|
|
468
|
-
|
|
597
|
+
const failedAttempt = attemptRecord(setup?.transport.id ?? this.transport.id, attempt, session, setupSummary, attemptAccounting, undefined, { code: typed.code, message: typed.message });
|
|
598
|
+
attempts.push(failedAttempt);
|
|
599
|
+
try {
|
|
600
|
+
try { await options.onAttempt?.(failedAttempt); } finally { await session.dispose(); }
|
|
601
|
+
} catch (persistenceError) {
|
|
602
|
+
throw errorWithAttempts(persistenceError, attempts);
|
|
603
|
+
}
|
|
469
604
|
}
|
|
470
605
|
if (options.worktreeOwner && typed.code !== "WORKTREE_FAILED") await this.root.runStore?.snapshotWorktree(options.worktreeOwner).catch(() => undefined);
|
|
471
606
|
const terminal = terminalProviderError(typed);
|
|
@@ -534,6 +669,7 @@ type ScheduledNode = {
|
|
|
534
669
|
id: string;
|
|
535
670
|
runId: string;
|
|
536
671
|
parentId?: string;
|
|
672
|
+
prompt?: string;
|
|
537
673
|
options: Readonly<ScheduledAgentOptions>;
|
|
538
674
|
children: Set<string>;
|
|
539
675
|
collected: boolean;
|
|
@@ -547,7 +683,7 @@ type ScheduledNode = {
|
|
|
547
683
|
};
|
|
548
684
|
|
|
549
685
|
type ScheduledRun = { limit: number; beforeLaunch?: () => void; logical: number; active: number; queue: Array<{ node?: ScheduledNode; start: () => void }> };
|
|
550
|
-
export type OwnershipRecord = { id: string; parentId?: string; label: string; state: ScheduledNode["state"]; options: Readonly<ScheduledAgentOptions> };
|
|
686
|
+
export type OwnershipRecord = { id: string; parentId?: string; prompt?: string; label: string; state: ScheduledNode["state"]; options: Readonly<ScheduledAgentOptions> };
|
|
551
687
|
type OwnershipWriter = (runId: string, ownership: readonly OwnershipRecord[]) => void | Promise<void>;
|
|
552
688
|
|
|
553
689
|
export class FairAgentScheduler {
|
|
@@ -586,7 +722,7 @@ export class FairAgentScheduler {
|
|
|
586
722
|
const id = `${runId}:${String(++this.#nextId)}`;
|
|
587
723
|
let resolveResult: (result: ScheduledAgentResult) => void = () => undefined;
|
|
588
724
|
const promise = new Promise<ScheduledAgentResult>((resolve) => { resolveResult = resolve; });
|
|
589
|
-
const node: ScheduledNode = { id, runId, ...(parentId ? { parentId } : {}), options: effective, children: new Set<string>(), collected: false, state: "queued", controller: new AbortController(), promise, resolve: resolveResult, task: async () => undefined, restored: false };
|
|
725
|
+
const node: ScheduledNode = { id, runId, ...(parentId ? { parentId } : {}), prompt, options: effective, children: new Set<string>(), collected: false, state: "queued", controller: new AbortController(), promise, resolve: resolveResult, task: async () => undefined, restored: false };
|
|
590
726
|
node.task = async () => {
|
|
591
727
|
if (node.controller.signal.aborted) { this.#release(node.runId); return; }
|
|
592
728
|
node.state = "running";
|
|
@@ -702,7 +838,7 @@ export class FairAgentScheduler {
|
|
|
702
838
|
}
|
|
703
839
|
|
|
704
840
|
snapshot(): readonly OwnershipRecord[] {
|
|
705
|
-
return [...this.#nodes.values()].map(({ id, parentId, options, state }) => ({ id, ...(parentId ? { parentId } : {}), label: options.label, state, options }));
|
|
841
|
+
return [...this.#nodes.values()].map(({ id, parentId, prompt, options, state }) => ({ id, ...(parentId ? { parentId } : {}), ...(prompt === undefined ? {} : { prompt }), label: options.label, state, options }));
|
|
706
842
|
}
|
|
707
843
|
|
|
708
844
|
restoreRun(runId: string, limit: number, ownership: readonly OwnershipRecord[], beforeLaunch?: () => void): void {
|
|
@@ -712,7 +848,7 @@ export class FairAgentScheduler {
|
|
|
712
848
|
if (record.id.split(":").slice(0, -1).join(":") !== runId) throw new WorkflowError("RESUME_INCOMPATIBLE", `Persisted agent belongs to another run: ${record.id}`);
|
|
713
849
|
let resolveResult: (result: ScheduledAgentResult) => void = () => undefined;
|
|
714
850
|
const promise = new Promise<ScheduledAgentResult>((resolve) => { resolveResult = resolve; });
|
|
715
|
-
const node: ScheduledNode = { id: record.id, runId, ...(record.parentId ? { parentId: record.parentId } : {}), options: this.#inherit(undefined, record.options), children: new Set(), collected: false, state: record.state, controller: new AbortController(), promise, resolve: resolveResult, task: async () => undefined, restored: true };
|
|
851
|
+
const node: ScheduledNode = { id: record.id, runId, ...(record.parentId ? { parentId: record.parentId } : {}), ...(record.prompt === undefined ? {} : { prompt: record.prompt }), options: this.#inherit(undefined, record.options), children: new Set(), collected: false, state: record.state, controller: new AbortController(), promise, resolve: resolveResult, task: async () => undefined, restored: true };
|
|
716
852
|
this.#nodes.set(node.id, node);
|
|
717
853
|
run.logical += 1;
|
|
718
854
|
this.#nextId = Math.max(this.#nextId, Number(node.id.slice(node.id.lastIndexOf(":") + 1)) || 0);
|
|
@@ -795,11 +931,6 @@ export class FairAgentScheduler {
|
|
|
795
931
|
|
|
796
932
|
function resolvePath(path: string): string { return path.replace(/[\\/]+$/, ""); }
|
|
797
933
|
|
|
798
|
-
function requiredFile(file: string | undefined): string {
|
|
799
|
-
if (!file) throw new WorkflowError("INTERNAL_ERROR", "Workflow agents require persisted native Pi sessions");
|
|
800
|
-
return file;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
934
|
function remaining(timeoutMs: number | null | undefined, started: number): number | null | undefined {
|
|
804
935
|
return timeoutMs === null || timeoutMs === undefined ? timeoutMs : Math.max(1, timeoutMs - (Date.now() - started));
|
|
805
936
|
}
|
|
@@ -810,20 +941,20 @@ function providerLimited(error: unknown): boolean {
|
|
|
810
941
|
return candidate.status === 429 || candidate.code === 429 || candidate.code === "rate_limit_exceeded" || candidate.code === "RATE_LIMITED";
|
|
811
942
|
}
|
|
812
943
|
|
|
813
|
-
async function promptWithProviderPause(session:
|
|
944
|
+
async function promptWithProviderPause(session: WorkflowAgentSession, text: string, timeoutMs: number | null | undefined, signal: AbortSignal | undefined, pause?: () => Promise<void>): Promise<WorkflowAgentTurnResult> {
|
|
814
945
|
for (;;) {
|
|
815
|
-
try { await withTimeout(session.prompt(text), timeoutMs, signal, session);
|
|
946
|
+
try { return await withTimeout(session.prompt(text), timeoutMs, signal, session); }
|
|
816
947
|
catch (error) { if (!pause || !providerLimited(error)) throw error; await pause(); }
|
|
817
948
|
}
|
|
818
949
|
}
|
|
819
950
|
|
|
820
|
-
async function withTimeout(work: Promise<
|
|
951
|
+
async function withTimeout(work: Promise<WorkflowAgentTurnResult>, timeoutMs: number | null | undefined, signal: AbortSignal | undefined, session: WorkflowAgentSession): Promise<WorkflowAgentTurnResult> {
|
|
821
952
|
if (signal?.aborted) throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
822
953
|
let timer: NodeJS.Timeout | undefined;
|
|
823
954
|
let abort: (() => void) | undefined;
|
|
824
955
|
const state = { interrupted: false };
|
|
825
956
|
const timeout = timeoutMs ? new Promise<never>((_, reject) => { timer = setTimeout(() => { state.interrupted = true; reject(new WorkflowError("AGENT_TIMEOUT", "Agent attempt timed out")); }, timeoutMs); }) : new Promise<never>(() => {});
|
|
826
957
|
const cancelled = signal ? new Promise<never>((_, reject) => { abort = () => { state.interrupted = true; reject(new WorkflowError("CANCELLED", "Agent cancelled")); }; signal.addEventListener("abort", abort, { once: true }); }) : new Promise<never>(() => {});
|
|
827
|
-
try { await Promise.race([work, timeout, cancelled]); }
|
|
828
|
-
finally { if (timer) clearTimeout(timer); if (abort) signal?.removeEventListener("abort", abort); if (state.interrupted) await session.abort
|
|
958
|
+
try { return await Promise.race([work, timeout, cancelled]); }
|
|
959
|
+
finally { if (timer) clearTimeout(timer); if (abort) signal?.removeEventListener("abort", abort); if (state.interrupted) await session.abort(); }
|
|
829
960
|
}
|