pi-extensible-workflows 1.0.1 → 2.0.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 +7 -2
- package/dist/src/agent-execution.d.ts +13 -0
- package/dist/src/agent-execution.js +130 -33
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +530 -6
- package/dist/src/doctor.d.ts +4 -4
- package/dist/src/doctor.js +9 -29
- package/dist/src/herdr.d.ts +12 -0
- package/dist/src/herdr.js +74 -0
- package/dist/src/index.d.ts +101 -24
- package/dist/src/index.js +669 -212
- package/dist/src/persistence.d.ts +26 -0
- package/dist/src/persistence.js +186 -12
- package/dist/src/session-inspector.d.ts +1 -0
- package/dist/src/session-inspector.js +3 -0
- package/dist/src/workflow-evals.js +2 -1
- package/package.json +4 -3
- package/skills/pi-extensible-workflows/SKILL.md +48 -64
- package/src/agent-execution.ts +95 -20
- package/src/cli.ts +418 -6
- package/src/doctor.ts +13 -32
- package/src/herdr.ts +73 -0
- package/src/index.ts +595 -193
- package/src/persistence.ts +169 -12
- package/src/session-inspector.ts +4 -0
- package/src/workflow-evals.ts +2 -1
package/src/agent-execution.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface AgentBudgetHooks {
|
|
|
17
17
|
instruction(): string | undefined;
|
|
18
18
|
}
|
|
19
19
|
export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: ThinkingLevel; tools?: readonly string[]; disabledAgentResources?: AgentResourceExclusions }
|
|
20
|
+
export interface AgentProviderFailure { label: string; provider: string; model: string; error: string }
|
|
21
|
+
export type AgentProviderRecovery = "retry" | "abort" | { model: string };
|
|
20
22
|
export interface AgentExecutionOptions {
|
|
21
23
|
label: string;
|
|
22
24
|
workflowName: string;
|
|
@@ -26,6 +28,8 @@ export interface AgentExecutionOptions {
|
|
|
26
28
|
thinking?: ThinkingLevel;
|
|
27
29
|
onProgress?: (progress: AgentProgress) => void | Promise<void>;
|
|
28
30
|
onAttempt?: (attempt: Pick<AgentAttempt, "attempt" | "sessionId" | "sessionFile" | "setup">) => void | Promise<void>;
|
|
31
|
+
providerErrorRecovery?: (failure: AgentProviderFailure) => Promise<AgentProviderRecovery>;
|
|
32
|
+
modelOverride?: ModelSpec;
|
|
29
33
|
tools?: readonly string[];
|
|
30
34
|
effectiveTools?: readonly string[];
|
|
31
35
|
role?: string;
|
|
@@ -85,7 +89,7 @@ export interface NativeSession {
|
|
|
85
89
|
abort?(): Promise<void>;
|
|
86
90
|
dispose(): void;
|
|
87
91
|
}
|
|
88
|
-
export interface SessionInput { cwd: string; model: ModelSpec; tools: string[]; sessionLabel: string; agentDir?: string; customTools?: ToolDefinition[]; resultTool?: ToolDefinition; systemPromptAppend?: string; extensionFactories?: InlineExtension[]; resourcePolicy?: AgentResourcePolicy; options?: Record<string, JsonValue>; continuation?: { sessionId: string; sessionFile: string; leafId: string } }
|
|
92
|
+
export interface SessionInput { cwd: string; model: ModelSpec; tools: string[]; sessionLabel: string; agentDir?: string; customTools?: ToolDefinition[]; resultTool?: ToolDefinition; systemPromptAppend?: string; extensionFactories?: InlineExtension[]; resourcePolicy?: AgentResourcePolicy; options?: Record<string, JsonValue>; continuation?: { sessionId: string; sessionFile: string; leafId: string }; allowModelChange?: boolean }
|
|
89
93
|
export type SessionFactory = (input: SessionInput) => Promise<NativeSession>;
|
|
90
94
|
|
|
91
95
|
function parseModel(value: string | undefined, fallback: ModelSpec, thinking?: ThinkingLevel, aliases: Readonly<Record<string, string>> = {}, knownModels?: ReadonlySet<string>, settingsPath?: string): ModelSpec {
|
|
@@ -110,9 +114,22 @@ function latestAssistantHasToolCall(messages: readonly AgentMessage[]): boolean
|
|
|
110
114
|
return hasToolCall(message);
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
|
|
117
|
+
type TerminalProviderError = { provider: string; model: string; error: string };
|
|
118
|
+
function throwIfTerminalAssistantError(session: NativeSession, fallbackModel: ModelSpec): void {
|
|
114
119
|
const message = [...session.messages].reverse().find((item) => item.role === "assistant");
|
|
115
|
-
if (message?.stopReason
|
|
120
|
+
if (message?.stopReason !== "error") return;
|
|
121
|
+
const provider = session.model?.provider ?? fallbackModel.provider;
|
|
122
|
+
const model = session.model?.model ?? session.model?.id ?? fallbackModel.model;
|
|
123
|
+
const error = message.errorMessage ?? "Native Pi assistant ended with a terminal provider error";
|
|
124
|
+
const failure = new WorkflowError("AGENT_FAILED", error);
|
|
125
|
+
Object.defineProperty(failure, "terminalProviderError", { value: { provider, model, error }, configurable: true });
|
|
126
|
+
throw failure;
|
|
127
|
+
}
|
|
128
|
+
function terminalProviderError(error: WorkflowError): TerminalProviderError | undefined {
|
|
129
|
+
const value = (error as WorkflowError & { terminalProviderError?: unknown }).terminalProviderError;
|
|
130
|
+
if (!value || typeof value !== "object") return undefined;
|
|
131
|
+
const candidate = value as Partial<TerminalProviderError>;
|
|
132
|
+
return typeof candidate.provider === "string" && typeof candidate.model === "string" && typeof candidate.error === "string" ? { provider: candidate.provider, model: candidate.model, error: candidate.error } : undefined;
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
function accounting(stats: NativeSessionStats): AgentAccounting {
|
|
@@ -130,7 +147,10 @@ export async function createNativeAgentSession(input: SessionInput): Promise<Nat
|
|
|
130
147
|
if (!header || canonicalSourcePath(header.cwd) !== canonicalSourcePath(input.cwd) || manager.getSessionId() !== input.continuation.sessionId || !manager.getEntry(input.continuation.leafId)) throw new Error("Persisted transcript identity does not match the conversation head");
|
|
131
148
|
manager.branch(input.continuation.leafId);
|
|
132
149
|
const context = manager.buildSessionContext();
|
|
133
|
-
if (context.model && (context.model.provider !== input.model.provider || context.model.modelId !== input.model.model))
|
|
150
|
+
if (context.model && (context.model.provider !== input.model.provider || context.model.modelId !== input.model.model)) {
|
|
151
|
+
if (!input.allowModelChange) throw new Error("Persisted transcript model does not match the conversation execution policy");
|
|
152
|
+
manager.appendModelChange(input.model.provider, input.model.model);
|
|
153
|
+
}
|
|
134
154
|
if (input.model.thinking && context.thinkingLevel !== input.model.thinking) throw new Error("Persisted transcript thinking level does not match the conversation execution policy");
|
|
135
155
|
} catch (error) {
|
|
136
156
|
if (error instanceof WorkflowError && error.code === "RESUME_INCOMPATIBLE") throw error;
|
|
@@ -262,6 +282,21 @@ function conversationExecutionPolicy(options: AgentExecutionOptions, setup: Agen
|
|
|
262
282
|
resourcePolicy: setup.sessionInput.resourcePolicy ? resourcePolicySummary(setup.sessionInput.resourcePolicy) : null,
|
|
263
283
|
}) as unknown as JsonValue;
|
|
264
284
|
}
|
|
285
|
+
function conversationPolicyMatches(expected: JsonValue, current: JsonValue, allowModelChange: boolean): boolean {
|
|
286
|
+
if (fingerprint(expected) === fingerprint(current)) return true;
|
|
287
|
+
if (!allowModelChange || !expected || typeof expected !== "object" || Array.isArray(expected) || !current || typeof current !== "object" || Array.isArray(current)) return false;
|
|
288
|
+
const expectedModel = conversationPolicyModel(expected);
|
|
289
|
+
const currentModel = conversationPolicyModel(current);
|
|
290
|
+
if (!expectedModel || !currentModel) return false;
|
|
291
|
+
return fingerprint(expected) === fingerprint({ ...current, model: { ...currentModel, provider: expectedModel.provider, model: expectedModel.model } });
|
|
292
|
+
}
|
|
293
|
+
function conversationPolicyModel(policy: JsonValue): ModelSpec | undefined {
|
|
294
|
+
if (!policy || typeof policy !== "object" || Array.isArray(policy)) return undefined;
|
|
295
|
+
const model = policy.model;
|
|
296
|
+
if (!model || typeof model !== "object" || Array.isArray(model) || typeof model.provider !== "string" || typeof model.model !== "string") return undefined;
|
|
297
|
+
const thinking = typeof model.thinking === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(model.thinking) ? model.thinking as ModelSpec["thinking"] : undefined;
|
|
298
|
+
return thinking === undefined ? { provider: model.provider, model: model.model } : { provider: model.provider, model: model.model, thinking };
|
|
299
|
+
}
|
|
265
300
|
function conversationFailure(message: string): WorkflowError { return new WorkflowError("RESUME_INCOMPATIBLE", message); }
|
|
266
301
|
async function prepareAgentSetup(root: AgentExecutionRoot, createSession: SessionFactory, task: string, options: AgentExecutionOptions, resolved: { model: ModelSpec; tools: readonly string[]; systemPromptAppend: string }, cwd: string, attempt: number, signal: AbortSignal | undefined, customTools: readonly ToolDefinition[], resultTool: ToolDefinition | undefined, continuation?: ConversationHead): Promise<{ setup: AgentSetup; summary: AgentSetupSummary }> {
|
|
267
302
|
const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
|
|
@@ -307,7 +342,7 @@ export class WorkflowAgentExecutor {
|
|
|
307
342
|
const hasAlias = requestedModel !== undefined && Object.prototype.hasOwnProperty.call(this.root.modelAliases ?? {}, requestedModel);
|
|
308
343
|
if (requestedModel !== undefined && this.root.blockedAliases?.has(requestedModel) && !hasAlias) { const target = this.root.blockedAliasTargets?.[requestedModel]; throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${requestedModel}${target ? ` resolved to ${target}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`); }
|
|
309
344
|
const aliasThinking = requestedModel !== undefined && hasAlias ? resolveModelReference(requestedModel, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath).thinking : undefined;
|
|
310
|
-
const model = parseModel(requestedModel, this.root.model, options.thinking ?? (aliasThinking === undefined ? definition?.thinking : undefined), this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
|
|
345
|
+
const model = options.modelOverride ?? parseModel(requestedModel, this.root.model, options.thinking ?? (aliasThinking === undefined ? definition?.thinking : undefined), this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
|
|
311
346
|
const availableModels = this.root.knownModels ?? this.root.availableModels ?? new Set([modelCapability(this.root.model)]);
|
|
312
347
|
if (!availableModels.has(modelCapability(model))) throw new WorkflowError("UNKNOWN_MODEL", `Unknown model${requestedModel ? ` ${requestedModel} resolved to ${modelCapability(model)}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
|
|
313
348
|
return { model, ...(hasAlias ? { requestedModel } : {}), tools: [...requested], systemPromptAppend: definition?.prompt ?? "" };
|
|
@@ -317,7 +352,8 @@ export class WorkflowAgentExecutor {
|
|
|
317
352
|
const executionSignal = signal ?? this.root.runContext?.signal;
|
|
318
353
|
if (!Number.isInteger(options.retries ?? 0) || (options.retries ?? 0) < 0) throw new WorkflowError("INVALID_METADATA", "retries must be a non-negative integer");
|
|
319
354
|
if (options.timeoutMs !== undefined && options.timeoutMs !== null && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0)) throw new WorkflowError("INVALID_METADATA", "timeoutMs must be null or a positive integer");
|
|
320
|
-
|
|
355
|
+
let resolved = this.resolve(options);
|
|
356
|
+
let recoveryModel: ModelSpec | undefined;
|
|
321
357
|
let cwd: string;
|
|
322
358
|
if (options.parent) {
|
|
323
359
|
if (!options.cwd) throw new WorkflowError("INVALID_METADATA", "Child agents require their parent cwd");
|
|
@@ -342,15 +378,19 @@ export class WorkflowAgentExecutor {
|
|
|
342
378
|
const store = this.root.runStore;
|
|
343
379
|
if (!store) throw conversationFailure("Conversation persistence is unavailable");
|
|
344
380
|
try { conversationRecord = await store.conversation(options.conversation.id); } catch (error) { throw conversationFailure(`Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`); }
|
|
381
|
+
if (conversationRecord) {
|
|
382
|
+
const model = conversationPolicyModel(conversationRecord.policy);
|
|
383
|
+
if (model) resolved = this.resolve({ ...options, modelOverride: model });
|
|
384
|
+
}
|
|
345
385
|
if (!Number.isInteger(options.conversation.turn) || options.conversation.turn < 1) throw conversationFailure("Conversation turn must be a positive integer");
|
|
346
386
|
if (conversationRecord ? conversationRecord.head.turn + 1 !== options.conversation.turn : options.conversation.turn !== 1) throw conversationFailure(`Conversation turn ${String(options.conversation.turn)} does not continue its persisted head`);
|
|
347
387
|
}
|
|
348
388
|
const attempts: AgentAttempt[] = [];
|
|
349
389
|
let conversationBaseline: { executionPolicy: JsonValue; toolDefinitionsSha256: string; systemPrompt?: string; systemPromptSha256?: string } | undefined;
|
|
350
|
-
|
|
390
|
+
let maxAttempts = (options.retries ?? 0) + 1;
|
|
351
391
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
392
|
+
if (recoveryModel) resolved = this.resolve({ ...options, modelOverride: recoveryModel });
|
|
352
393
|
options.budget?.beforeAttempt();
|
|
353
|
-
let accepted = false;
|
|
354
394
|
let schemaResult: JsonValue | undefined;
|
|
355
395
|
let session: NativeSession | undefined;
|
|
356
396
|
let setup: AgentSetup | undefined;
|
|
@@ -366,8 +406,8 @@ export class WorkflowAgentExecutor {
|
|
|
366
406
|
const resultTool = options.schema ? {
|
|
367
407
|
name: "workflow_result", label: "Workflow Result", description: "Submit the terminal structured workflow result", parameters: Type.Unsafe(options.schema),
|
|
368
408
|
async execute(_id: string, value: unknown) {
|
|
369
|
-
if (!accepted) return { content: [{ type: "text" as const, text: "Result acceptance is not enabled yet." }], details: {}, isError: true };
|
|
370
409
|
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 };
|
|
410
|
+
if (schemaResult !== undefined) return { content: [{ type: "text" as const, text: "Result has already been accepted." }], details: {}, isError: true };
|
|
371
411
|
schemaResult = structuredClone(value) as JsonValue;
|
|
372
412
|
void session?.abort?.();
|
|
373
413
|
return { content: [{ type: "text" as const, text: "Result accepted." }], details: {} };
|
|
@@ -396,6 +436,7 @@ export class WorkflowAgentExecutor {
|
|
|
396
436
|
setupSummary = prepared.summary;
|
|
397
437
|
setupFailed = false;
|
|
398
438
|
if (executionSignal?.aborted) throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
439
|
+
if (recoveryModel && conversationRecord) setup.sessionInput.allowModelChange = true;
|
|
399
440
|
const started = Date.now();
|
|
400
441
|
session = await setup.createSession(setup.sessionInput);
|
|
401
442
|
if (setup.sessionInput.resourcePolicy) setupSummary = { ...setupSummary, disabledAgentResources: resourcePolicySummary(setup.sessionInput.resourcePolicy) };
|
|
@@ -405,12 +446,12 @@ export class WorkflowAgentExecutor {
|
|
|
405
446
|
const currentExecutionPolicy = conversationExecutionPolicy(options, setup);
|
|
406
447
|
if (conversationRecord) {
|
|
407
448
|
if (session.sessionId !== conversationRecord.head.sessionId || requiredFile(session.sessionFile) !== conversationRecord.head.sessionFile) throw conversationFailure("Conversation transcript identity changed");
|
|
408
|
-
if (!session.getLeafId || session.getLeafId() !== conversationRecord.head.leafId) throw conversationFailure("Conversation transcript leaf identity changed");
|
|
409
|
-
if (
|
|
449
|
+
if (!session.getLeafId || (!recoveryModel && session.getLeafId() !== conversationRecord.head.leafId)) throw conversationFailure("Conversation transcript leaf identity changed");
|
|
450
|
+
if (!conversationPolicyMatches(conversationRecord.policy, currentExecutionPolicy, Boolean(recoveryModel))) throw conversationFailure("Conversation execution policy changed");
|
|
410
451
|
if (!session.subscribe && (promptFingerprint(conversationSystemPrompt) !== conversationRecord.head.systemPromptSha256 || conversationSystemPrompt !== conversationRecord.head.systemPrompt)) throw conversationFailure("Conversation system prompt changed");
|
|
411
452
|
if (conversationToolDefinitionsSha256 !== conversationRecord.head.toolDefinitionsSha256) throw conversationFailure("Conversation tool definitions changed");
|
|
412
453
|
} else if (conversationBaseline) {
|
|
413
|
-
if (
|
|
454
|
+
if (!conversationPolicyMatches(conversationBaseline.executionPolicy, currentExecutionPolicy, Boolean(recoveryModel))) throw conversationFailure("Conversation execution policy changed");
|
|
414
455
|
if (conversationToolDefinitionsSha256 !== conversationBaseline.toolDefinitionsSha256) throw conversationFailure("Conversation tool definitions changed");
|
|
415
456
|
} else {
|
|
416
457
|
conversationBaseline = { executionPolicy: currentExecutionPolicy, toolDefinitionsSha256: conversationToolDefinitionsSha256 };
|
|
@@ -449,7 +490,7 @@ export class WorkflowAgentExecutor {
|
|
|
449
490
|
activity = undefined;
|
|
450
491
|
if (event.message.role === "assistant") {
|
|
451
492
|
const needsMoreWork = hasToolCall(event.message);
|
|
452
|
-
const final = !needsMoreWork || (options.schema !== undefined &&
|
|
493
|
+
const final = !needsMoreWork || (options.schema !== undefined && hasSchemaResult());
|
|
453
494
|
if (!budgetError) { try { options.budget?.afterTurn(accounting(activeSession.getSessionStats()), final); if (!final) { const instruction = options.budget?.instruction(); if (instruction) void session?.steer?.(instruction); } } catch (error) { budgetError ??= error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", error instanceof Error ? error.message : String(error)); void session?.abort?.(); } }
|
|
454
495
|
turnStarted = false;
|
|
455
496
|
report(true);
|
|
@@ -468,18 +509,19 @@ export class WorkflowAgentExecutor {
|
|
|
468
509
|
const promptText = `${context}\n\nTask:\n${setup.prompt}${instruction ? `\n\n${instruction}` : ""}`;
|
|
469
510
|
options.budget?.beforeTurn();
|
|
470
511
|
turnStarted = true;
|
|
471
|
-
await promptWithProviderPause(session, promptText, remaining(options.timeoutMs, started), executionSignal, this.root.providerPause);
|
|
512
|
+
try { await promptWithProviderPause(session, promptText, remaining(options.timeoutMs, started), executionSignal, this.root.providerPause); } catch (error) { if (!hasSchemaResult()) throw error; }
|
|
472
513
|
if (conversationMismatch) throw conversationMismatch;
|
|
473
|
-
throwIfTerminalAssistantError(session);
|
|
474
|
-
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, options.schema !== undefined ?
|
|
514
|
+
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
515
|
+
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(session.messages)); turnStarted = false; }
|
|
475
516
|
if (budgetError) throw budgetError;
|
|
476
517
|
if (options.schema) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
518
|
+
if (!hasSchemaResult()) {
|
|
519
|
+
try { options.budget?.beforeTurn(); turnStarted = true; await promptWithProviderPause(session, "Submit the final result now by calling workflow_result exactly once. Do not return prose.", remaining(options.timeoutMs, started), executionSignal, this.root.providerPause); { const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, true); turnStarted = false; } } catch (error) { if (!hasSchemaResult()) throw error; }
|
|
520
|
+
}
|
|
521
|
+
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
480
522
|
if (!hasSchemaResult()) {
|
|
481
523
|
try { options.budget?.beforeTurn(); turnStarted = true; await promptWithProviderPause(session, "Your result was missing or invalid. Repair it by calling workflow_result exactly once with a schema-valid value.", remaining(options.timeoutMs, started), executionSignal, this.root.providerPause); { const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, true); turnStarted = false; } } catch (error) { if (!hasSchemaResult()) throw error; }
|
|
482
|
-
throwIfTerminalAssistantError(session);
|
|
524
|
+
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
483
525
|
}
|
|
484
526
|
if (schemaResult === undefined) throw new WorkflowError("RESULT_INVALID", "Agent did not submit a valid workflow_result after one repair");
|
|
485
527
|
}
|
|
@@ -517,6 +559,22 @@ export class WorkflowAgentExecutor {
|
|
|
517
559
|
session.dispose();
|
|
518
560
|
}
|
|
519
561
|
if (options.worktreeOwner && typed.code !== "WORKTREE_FAILED") await this.root.runStore?.snapshotWorktree(options.worktreeOwner).catch(() => undefined);
|
|
562
|
+
const terminal = terminalProviderError(typed);
|
|
563
|
+
if (terminal && options.providerErrorRecovery) {
|
|
564
|
+
let recovery: AgentProviderRecovery;
|
|
565
|
+
try { recovery = await options.providerErrorRecovery({ label: options.label, ...terminal }); } catch { throw Object.assign(typed, { attempts }); }
|
|
566
|
+
if (recovery === "retry" || typeof recovery === "object" && typeof recovery.model === "string") {
|
|
567
|
+
if (typeof recovery === "object") {
|
|
568
|
+
try {
|
|
569
|
+
const selected = resolveModelReference(recovery.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
|
|
570
|
+
recoveryModel = selected.thinking === undefined && resolved.model.thinking ? { ...selected, thinking: resolved.model.thinking } : selected;
|
|
571
|
+
} catch { throw Object.assign(typed, { attempts }); }
|
|
572
|
+
}
|
|
573
|
+
maxAttempts += 1;
|
|
574
|
+
beforeRetry?.();
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
520
578
|
if (attempt === maxAttempts || setupFailed || typed.code === "CANCELLED" || typed.code === "WORKTREE_FAILED" || typed.code === "RESUME_INCOMPATIBLE") throw Object.assign(typed, { attempts });
|
|
521
579
|
beforeRetry?.();
|
|
522
580
|
}
|
|
@@ -676,6 +734,22 @@ export class FairAgentScheduler {
|
|
|
676
734
|
if (nodes.every(({ restored }) => restored)) run.logical = 0;
|
|
677
735
|
}
|
|
678
736
|
|
|
737
|
+
removeRun(runId: string): void {
|
|
738
|
+
const run = this.#runs.get(runId);
|
|
739
|
+
if (!run) return;
|
|
740
|
+
const nodes = [...this.#nodes.values()].filter((node) => node.runId === runId);
|
|
741
|
+
if (run.active > 0 || nodes.some(({ state }) => !["completed", "failed", "cancelled"].includes(state))) throw new WorkflowError("INTERNAL_ERROR", `Cannot remove active scheduler run: ${runId}`);
|
|
742
|
+
for (const { id } of nodes) this.#nodes.delete(id);
|
|
743
|
+
this.#runs.delete(runId);
|
|
744
|
+
const index = this.#runOrder.indexOf(runId);
|
|
745
|
+
if (index >= 0) {
|
|
746
|
+
this.#runOrder.splice(index, 1);
|
|
747
|
+
if (index < this.#cursor) this.#cursor -= 1;
|
|
748
|
+
if (this.#cursor >= this.#runOrder.length) this.#cursor = 0;
|
|
749
|
+
}
|
|
750
|
+
this.#dispatch();
|
|
751
|
+
}
|
|
752
|
+
|
|
679
753
|
toolsFor(parentId: string, resolveTools?: (role: string | undefined, tools: readonly string[] | undefined, model: string | undefined, inheritedTools: readonly string[], thinking: ThinkingLevel | undefined) => readonly string[]): ToolDefinition[] {
|
|
680
754
|
const parent = this.#node(parentId);
|
|
681
755
|
if (!parent.options.tools.includes("agent")) return [];
|
|
@@ -772,6 +846,7 @@ export class FairAgentScheduler {
|
|
|
772
846
|
if (["completed", "failed", "cancelled"].includes(node.state)) return;
|
|
773
847
|
const heldPermit = node.state === "running" || node.state === "retrying";
|
|
774
848
|
node.state = result.ok ? "completed" : result.error.code === "CANCELLED" ? "cancelled" : "failed";
|
|
849
|
+
Reflect.deleteProperty(node, "steer");
|
|
775
850
|
this.#persist(node.runId);
|
|
776
851
|
if (heldPermit) this.#release(node.runId);
|
|
777
852
|
for (const childId of node.children) { const child = this.#nodes.get(childId); if (child && !child.collected) this.#cancelTree(child); }
|