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/README.md
CHANGED
|
@@ -20,7 +20,7 @@ For source installs and local development, see the [installation guide](https://
|
|
|
20
20
|
|
|
21
21
|
## Capabilities
|
|
22
22
|
|
|
23
|
-
The main Pi agent acts as the orchestrator: it writes workflow scripts on the fly for each task. Pi extensions can add reusable functions and variables to those scripts
|
|
23
|
+
The main Pi agent acts as the orchestrator: it writes workflow scripts on the fly for each task. Pi extensions can add reusable functions and variables to those scripts; every registered function is also directly runnable as a top-level workflow.
|
|
24
24
|
|
|
25
25
|
A workflow can fan out across specialized agents, combine their results, and resume without rerunning completed work.
|
|
26
26
|
|
|
@@ -61,9 +61,14 @@ Global workflow settings live at `~/.pi/agent/pi-extensible-workflows/settings.j
|
|
|
61
61
|
```sh
|
|
62
62
|
npx pi-extensible-workflows doctor
|
|
63
63
|
npx pi-extensible-workflows inspect [session-id]
|
|
64
|
+
npx pi-extensible-workflows transcript <session-file>
|
|
65
|
+
npx pi-extensible-workflows run <workflow-name> [workflow arguments]
|
|
66
|
+
npx pi-extensible-workflows export <workflow-name> [--name <command>] [--output <path>] [--force]
|
|
64
67
|
```
|
|
65
68
|
|
|
66
|
-
`doctor` validates the installation and active Pi resources. `inspect` opens a read-only terminal view of persisted workflow runs.
|
|
69
|
+
`doctor` validates the installation and active Pi resources. `inspect` opens a read-only terminal view of persisted workflow runs. `transcript` renders a session transcript to stdout. `run` derives flat CLI arguments and help from a registered function's input schema. Use `--input '<json>'` for nested or otherwise complex inputs. It executes in the current working directory, writes the final JSON result to stdout, and writes progress and errors to stderr. `export` creates an executable POSIX launcher in `~/.local/bin` by default.
|
|
70
|
+
`run` and `export` accept the trust overrides `--approve` and `--no-approve`; the generated launcher forwards its arguments to `run`. `--` ends launcher option parsing, and later tokens are passed to workflow input instead of being interpreted as launcher options.
|
|
71
|
+
Launch snapshots use identity version 4. A cold resume intentionally rejects persisted v3 runs with `RESUME_INCOMPATIBLE`; relaunch the workflow instead.
|
|
67
72
|
|
|
68
73
|
## Development
|
|
69
74
|
|
|
@@ -31,6 +31,15 @@ export interface AgentDefinition {
|
|
|
31
31
|
tools?: readonly string[];
|
|
32
32
|
disabledAgentResources?: AgentResourceExclusions;
|
|
33
33
|
}
|
|
34
|
+
export interface AgentProviderFailure {
|
|
35
|
+
label: string;
|
|
36
|
+
provider: string;
|
|
37
|
+
model: string;
|
|
38
|
+
error: string;
|
|
39
|
+
}
|
|
40
|
+
export type AgentProviderRecovery = "retry" | "abort" | {
|
|
41
|
+
model: string;
|
|
42
|
+
};
|
|
34
43
|
export interface AgentExecutionOptions {
|
|
35
44
|
label: string;
|
|
36
45
|
workflowName: string;
|
|
@@ -40,6 +49,8 @@ export interface AgentExecutionOptions {
|
|
|
40
49
|
thinking?: ThinkingLevel;
|
|
41
50
|
onProgress?: (progress: AgentProgress) => void | Promise<void>;
|
|
42
51
|
onAttempt?: (attempt: Pick<AgentAttempt, "attempt" | "sessionId" | "sessionFile" | "setup">) => void | Promise<void>;
|
|
52
|
+
providerErrorRecovery?: (failure: AgentProviderFailure) => Promise<AgentProviderRecovery>;
|
|
53
|
+
modelOverride?: ModelSpec;
|
|
43
54
|
tools?: readonly string[];
|
|
44
55
|
effectiveTools?: readonly string[];
|
|
45
56
|
role?: string;
|
|
@@ -179,6 +190,7 @@ export interface SessionInput {
|
|
|
179
190
|
sessionFile: string;
|
|
180
191
|
leafId: string;
|
|
181
192
|
};
|
|
193
|
+
allowModelChange?: boolean;
|
|
182
194
|
}
|
|
183
195
|
export type SessionFactory = (input: SessionInput) => Promise<NativeSession>;
|
|
184
196
|
export declare function createNativeAgentSession(input: SessionInput): Promise<NativeSession>;
|
|
@@ -278,6 +290,7 @@ export declare class FairAgentScheduler {
|
|
|
278
290
|
retry(id: string): void;
|
|
279
291
|
attemptStarted(id: string): void;
|
|
280
292
|
cancelRun(runId: string): Promise<void>;
|
|
293
|
+
removeRun(runId: string): void;
|
|
281
294
|
toolsFor(parentId: string, resolveTools?: (role: string | undefined, tools: readonly string[] | undefined, model: string | undefined, inheritedTools: readonly string[], thinking: ThinkingLevel | undefined) => readonly string[]): ToolDefinition[];
|
|
282
295
|
snapshot(): readonly OwnershipRecord[];
|
|
283
296
|
restoreRun(runId: string, limit: number, ownership: readonly OwnershipRecord[], beforeLaunch?: () => void): void;
|
|
@@ -25,10 +25,23 @@ function latestAssistantHasToolCall(messages) {
|
|
|
25
25
|
const message = [...messages].reverse().find((item) => item.role === "assistant");
|
|
26
26
|
return hasToolCall(message);
|
|
27
27
|
}
|
|
28
|
-
function throwIfTerminalAssistantError(session) {
|
|
28
|
+
function throwIfTerminalAssistantError(session, fallbackModel) {
|
|
29
29
|
const message = [...session.messages].reverse().find((item) => item.role === "assistant");
|
|
30
|
-
if (message?.stopReason
|
|
31
|
-
|
|
30
|
+
if (message?.stopReason !== "error")
|
|
31
|
+
return;
|
|
32
|
+
const provider = session.model?.provider ?? fallbackModel.provider;
|
|
33
|
+
const model = session.model?.model ?? session.model?.id ?? fallbackModel.model;
|
|
34
|
+
const error = message.errorMessage ?? "Native Pi assistant ended with a terminal provider error";
|
|
35
|
+
const failure = new WorkflowError("AGENT_FAILED", error);
|
|
36
|
+
Object.defineProperty(failure, "terminalProviderError", { value: { provider, model, error }, configurable: true });
|
|
37
|
+
throw failure;
|
|
38
|
+
}
|
|
39
|
+
function terminalProviderError(error) {
|
|
40
|
+
const value = error.terminalProviderError;
|
|
41
|
+
if (!value || typeof value !== "object")
|
|
42
|
+
return undefined;
|
|
43
|
+
const candidate = value;
|
|
44
|
+
return typeof candidate.provider === "string" && typeof candidate.model === "string" && typeof candidate.error === "string" ? { provider: candidate.provider, model: candidate.model, error: candidate.error } : undefined;
|
|
32
45
|
}
|
|
33
46
|
function accounting(stats) {
|
|
34
47
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
@@ -50,8 +63,11 @@ export async function createNativeAgentSession(input) {
|
|
|
50
63
|
throw new Error("Persisted transcript identity does not match the conversation head");
|
|
51
64
|
manager.branch(input.continuation.leafId);
|
|
52
65
|
const context = manager.buildSessionContext();
|
|
53
|
-
if (context.model && (context.model.provider !== input.model.provider || context.model.modelId !== input.model.model))
|
|
54
|
-
|
|
66
|
+
if (context.model && (context.model.provider !== input.model.provider || context.model.modelId !== input.model.model)) {
|
|
67
|
+
if (!input.allowModelChange)
|
|
68
|
+
throw new Error("Persisted transcript model does not match the conversation execution policy");
|
|
69
|
+
manager.appendModelChange(input.model.provider, input.model.model);
|
|
70
|
+
}
|
|
55
71
|
if (input.model.thinking && context.thinkingLevel !== input.model.thinking)
|
|
56
72
|
throw new Error("Persisted transcript thinking level does not match the conversation execution policy");
|
|
57
73
|
}
|
|
@@ -193,6 +209,26 @@ function conversationExecutionPolicy(options, setup) {
|
|
|
193
209
|
resourcePolicy: setup.sessionInput.resourcePolicy ? resourcePolicySummary(setup.sessionInput.resourcePolicy) : null,
|
|
194
210
|
});
|
|
195
211
|
}
|
|
212
|
+
function conversationPolicyMatches(expected, current, allowModelChange) {
|
|
213
|
+
if (fingerprint(expected) === fingerprint(current))
|
|
214
|
+
return true;
|
|
215
|
+
if (!allowModelChange || !expected || typeof expected !== "object" || Array.isArray(expected) || !current || typeof current !== "object" || Array.isArray(current))
|
|
216
|
+
return false;
|
|
217
|
+
const expectedModel = conversationPolicyModel(expected);
|
|
218
|
+
const currentModel = conversationPolicyModel(current);
|
|
219
|
+
if (!expectedModel || !currentModel)
|
|
220
|
+
return false;
|
|
221
|
+
return fingerprint(expected) === fingerprint({ ...current, model: { ...currentModel, provider: expectedModel.provider, model: expectedModel.model } });
|
|
222
|
+
}
|
|
223
|
+
function conversationPolicyModel(policy) {
|
|
224
|
+
if (!policy || typeof policy !== "object" || Array.isArray(policy))
|
|
225
|
+
return undefined;
|
|
226
|
+
const model = policy.model;
|
|
227
|
+
if (!model || typeof model !== "object" || Array.isArray(model) || typeof model.provider !== "string" || typeof model.model !== "string")
|
|
228
|
+
return undefined;
|
|
229
|
+
const thinking = typeof model.thinking === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(model.thinking) ? model.thinking : undefined;
|
|
230
|
+
return thinking === undefined ? { provider: model.provider, model: model.model } : { provider: model.provider, model: model.model, thinking };
|
|
231
|
+
}
|
|
196
232
|
function conversationFailure(message) { return new WorkflowError("RESUME_INCOMPATIBLE", message); }
|
|
197
233
|
async function prepareAgentSetup(root, createSession, task, options, resolved, cwd, attempt, signal, customTools, resultTool, continuation) {
|
|
198
234
|
const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
|
|
@@ -261,7 +297,7 @@ export class WorkflowAgentExecutor {
|
|
|
261
297
|
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${requestedModel}${target ? ` resolved to ${target}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
|
|
262
298
|
}
|
|
263
299
|
const aliasThinking = requestedModel !== undefined && hasAlias ? resolveModelReference(requestedModel, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath).thinking : undefined;
|
|
264
|
-
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);
|
|
300
|
+
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);
|
|
265
301
|
const availableModels = this.root.knownModels ?? this.root.availableModels ?? new Set([modelCapability(this.root.model)]);
|
|
266
302
|
if (!availableModels.has(modelCapability(model)))
|
|
267
303
|
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model${requestedModel ? ` ${requestedModel} resolved to ${modelCapability(model)}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
|
|
@@ -273,7 +309,8 @@ export class WorkflowAgentExecutor {
|
|
|
273
309
|
throw new WorkflowError("INVALID_METADATA", "retries must be a non-negative integer");
|
|
274
310
|
if (options.timeoutMs !== undefined && options.timeoutMs !== null && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0))
|
|
275
311
|
throw new WorkflowError("INVALID_METADATA", "timeoutMs must be null or a positive integer");
|
|
276
|
-
|
|
312
|
+
let resolved = this.resolve(options);
|
|
313
|
+
let recoveryModel;
|
|
277
314
|
let cwd;
|
|
278
315
|
if (options.parent) {
|
|
279
316
|
if (!options.cwd)
|
|
@@ -313,6 +350,11 @@ export class WorkflowAgentExecutor {
|
|
|
313
350
|
catch (error) {
|
|
314
351
|
throw conversationFailure(`Cannot load conversation state: ${error instanceof Error ? error.message : String(error)}`);
|
|
315
352
|
}
|
|
353
|
+
if (conversationRecord) {
|
|
354
|
+
const model = conversationPolicyModel(conversationRecord.policy);
|
|
355
|
+
if (model)
|
|
356
|
+
resolved = this.resolve({ ...options, modelOverride: model });
|
|
357
|
+
}
|
|
316
358
|
if (!Number.isInteger(options.conversation.turn) || options.conversation.turn < 1)
|
|
317
359
|
throw conversationFailure("Conversation turn must be a positive integer");
|
|
318
360
|
if (conversationRecord ? conversationRecord.head.turn + 1 !== options.conversation.turn : options.conversation.turn !== 1)
|
|
@@ -320,10 +362,11 @@ export class WorkflowAgentExecutor {
|
|
|
320
362
|
}
|
|
321
363
|
const attempts = [];
|
|
322
364
|
let conversationBaseline;
|
|
323
|
-
|
|
365
|
+
let maxAttempts = (options.retries ?? 0) + 1;
|
|
324
366
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
367
|
+
if (recoveryModel)
|
|
368
|
+
resolved = this.resolve({ ...options, modelOverride: recoveryModel });
|
|
325
369
|
options.budget?.beforeAttempt();
|
|
326
|
-
let accepted = false;
|
|
327
370
|
let schemaResult;
|
|
328
371
|
let session;
|
|
329
372
|
let setup;
|
|
@@ -339,10 +382,10 @@ export class WorkflowAgentExecutor {
|
|
|
339
382
|
const resultTool = options.schema ? {
|
|
340
383
|
name: "workflow_result", label: "Workflow Result", description: "Submit the terminal structured workflow result", parameters: Type.Unsafe(options.schema),
|
|
341
384
|
async execute(_id, value) {
|
|
342
|
-
if (!accepted)
|
|
343
|
-
return { content: [{ type: "text", text: "Result acceptance is not enabled yet." }], details: {}, isError: true };
|
|
344
385
|
if (!Value.Check(options.schema, value))
|
|
345
386
|
return { content: [{ type: "text", text: "Result does not match the required schema." }], details: {}, isError: true };
|
|
387
|
+
if (schemaResult !== undefined)
|
|
388
|
+
return { content: [{ type: "text", text: "Result has already been accepted." }], details: {}, isError: true };
|
|
346
389
|
schemaResult = structuredClone(value);
|
|
347
390
|
void session?.abort?.();
|
|
348
391
|
return { content: [{ type: "text", text: "Result accepted." }], details: {} };
|
|
@@ -374,6 +417,8 @@ export class WorkflowAgentExecutor {
|
|
|
374
417
|
setupFailed = false;
|
|
375
418
|
if (executionSignal?.aborted)
|
|
376
419
|
throw new WorkflowError("CANCELLED", "Agent cancelled");
|
|
420
|
+
if (recoveryModel && conversationRecord)
|
|
421
|
+
setup.sessionInput.allowModelChange = true;
|
|
377
422
|
const started = Date.now();
|
|
378
423
|
session = await setup.createSession(setup.sessionInput);
|
|
379
424
|
if (setup.sessionInput.resourcePolicy)
|
|
@@ -385,9 +430,9 @@ export class WorkflowAgentExecutor {
|
|
|
385
430
|
if (conversationRecord) {
|
|
386
431
|
if (session.sessionId !== conversationRecord.head.sessionId || requiredFile(session.sessionFile) !== conversationRecord.head.sessionFile)
|
|
387
432
|
throw conversationFailure("Conversation transcript identity changed");
|
|
388
|
-
if (!session.getLeafId || session.getLeafId() !== conversationRecord.head.leafId)
|
|
433
|
+
if (!session.getLeafId || (!recoveryModel && session.getLeafId() !== conversationRecord.head.leafId))
|
|
389
434
|
throw conversationFailure("Conversation transcript leaf identity changed");
|
|
390
|
-
if (
|
|
435
|
+
if (!conversationPolicyMatches(conversationRecord.policy, currentExecutionPolicy, Boolean(recoveryModel)))
|
|
391
436
|
throw conversationFailure("Conversation execution policy changed");
|
|
392
437
|
if (!session.subscribe && (promptFingerprint(conversationSystemPrompt) !== conversationRecord.head.systemPromptSha256 || conversationSystemPrompt !== conversationRecord.head.systemPrompt))
|
|
393
438
|
throw conversationFailure("Conversation system prompt changed");
|
|
@@ -395,7 +440,7 @@ export class WorkflowAgentExecutor {
|
|
|
395
440
|
throw conversationFailure("Conversation tool definitions changed");
|
|
396
441
|
}
|
|
397
442
|
else if (conversationBaseline) {
|
|
398
|
-
if (
|
|
443
|
+
if (!conversationPolicyMatches(conversationBaseline.executionPolicy, currentExecutionPolicy, Boolean(recoveryModel)))
|
|
399
444
|
throw conversationFailure("Conversation execution policy changed");
|
|
400
445
|
if (conversationToolDefinitionsSha256 !== conversationBaseline.toolDefinitionsSha256)
|
|
401
446
|
throw conversationFailure("Conversation tool definitions changed");
|
|
@@ -454,7 +499,7 @@ export class WorkflowAgentExecutor {
|
|
|
454
499
|
activity = undefined;
|
|
455
500
|
if (event.message.role === "assistant") {
|
|
456
501
|
const needsMoreWork = hasToolCall(event.message);
|
|
457
|
-
const final = !needsMoreWork || (options.schema !== undefined &&
|
|
502
|
+
const final = !needsMoreWork || (options.schema !== undefined && hasSchemaResult());
|
|
458
503
|
if (!budgetError) {
|
|
459
504
|
try {
|
|
460
505
|
options.budget?.afterTurn(accounting(activeSession.getSessionStats()), final);
|
|
@@ -496,34 +541,41 @@ export class WorkflowAgentExecutor {
|
|
|
496
541
|
const promptText = `${context}\n\nTask:\n${setup.prompt}${instruction ? `\n\n${instruction}` : ""}`;
|
|
497
542
|
options.budget?.beforeTurn();
|
|
498
543
|
turnStarted = true;
|
|
499
|
-
|
|
544
|
+
try {
|
|
545
|
+
await promptWithProviderPause(session, promptText, remaining(options.timeoutMs, started), executionSignal, this.root.providerPause);
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
if (!hasSchemaResult())
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
500
551
|
if (conversationMismatch)
|
|
501
552
|
throw conversationMismatch;
|
|
502
|
-
throwIfTerminalAssistantError(session);
|
|
553
|
+
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
503
554
|
{
|
|
504
555
|
const completedAccounting = accounting(session.getSessionStats());
|
|
505
|
-
options.budget?.afterTurn(completedAccounting, options.schema !== undefined ?
|
|
556
|
+
options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(session.messages));
|
|
506
557
|
turnStarted = false;
|
|
507
558
|
}
|
|
508
559
|
if (budgetError)
|
|
509
560
|
throw budgetError;
|
|
510
561
|
if (options.schema) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
562
|
+
if (!hasSchemaResult()) {
|
|
563
|
+
try {
|
|
564
|
+
options.budget?.beforeTurn();
|
|
565
|
+
turnStarted = true;
|
|
566
|
+
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);
|
|
567
|
+
{
|
|
568
|
+
const completedAccounting = accounting(session.getSessionStats());
|
|
569
|
+
options.budget?.afterTurn(completedAccounting, true);
|
|
570
|
+
turnStarted = false;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
if (!hasSchemaResult())
|
|
575
|
+
throw error;
|
|
520
576
|
}
|
|
521
577
|
}
|
|
522
|
-
|
|
523
|
-
if (!hasSchemaResult())
|
|
524
|
-
throw error;
|
|
525
|
-
}
|
|
526
|
-
throwIfTerminalAssistantError(session);
|
|
578
|
+
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
527
579
|
if (!hasSchemaResult()) {
|
|
528
580
|
try {
|
|
529
581
|
options.budget?.beforeTurn();
|
|
@@ -539,7 +591,7 @@ export class WorkflowAgentExecutor {
|
|
|
539
591
|
if (!hasSchemaResult())
|
|
540
592
|
throw error;
|
|
541
593
|
}
|
|
542
|
-
throwIfTerminalAssistantError(session);
|
|
594
|
+
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
543
595
|
}
|
|
544
596
|
if (schemaResult === undefined)
|
|
545
597
|
throw new WorkflowError("RESULT_INVALID", "Agent did not submit a valid workflow_result after one repair");
|
|
@@ -594,6 +646,30 @@ export class WorkflowAgentExecutor {
|
|
|
594
646
|
}
|
|
595
647
|
if (options.worktreeOwner && typed.code !== "WORKTREE_FAILED")
|
|
596
648
|
await this.root.runStore?.snapshotWorktree(options.worktreeOwner).catch(() => undefined);
|
|
649
|
+
const terminal = terminalProviderError(typed);
|
|
650
|
+
if (terminal && options.providerErrorRecovery) {
|
|
651
|
+
let recovery;
|
|
652
|
+
try {
|
|
653
|
+
recovery = await options.providerErrorRecovery({ label: options.label, ...terminal });
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
throw Object.assign(typed, { attempts });
|
|
657
|
+
}
|
|
658
|
+
if (recovery === "retry" || typeof recovery === "object" && typeof recovery.model === "string") {
|
|
659
|
+
if (typeof recovery === "object") {
|
|
660
|
+
try {
|
|
661
|
+
const selected = resolveModelReference(recovery.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
|
|
662
|
+
recoveryModel = selected.thinking === undefined && resolved.model.thinking ? { ...selected, thinking: resolved.model.thinking } : selected;
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
throw Object.assign(typed, { attempts });
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
maxAttempts += 1;
|
|
669
|
+
beforeRetry?.();
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
597
673
|
if (attempt === maxAttempts || setupFailed || typed.code === "CANCELLED" || typed.code === "WORKTREE_FAILED" || typed.code === "RESUME_INCOMPATIBLE")
|
|
598
674
|
throw Object.assign(typed, { attempts });
|
|
599
675
|
beforeRetry?.();
|
|
@@ -723,6 +799,26 @@ export class FairAgentScheduler {
|
|
|
723
799
|
if (nodes.every(({ restored }) => restored))
|
|
724
800
|
run.logical = 0;
|
|
725
801
|
}
|
|
802
|
+
removeRun(runId) {
|
|
803
|
+
const run = this.#runs.get(runId);
|
|
804
|
+
if (!run)
|
|
805
|
+
return;
|
|
806
|
+
const nodes = [...this.#nodes.values()].filter((node) => node.runId === runId);
|
|
807
|
+
if (run.active > 0 || nodes.some(({ state }) => !["completed", "failed", "cancelled"].includes(state)))
|
|
808
|
+
throw new WorkflowError("INTERNAL_ERROR", `Cannot remove active scheduler run: ${runId}`);
|
|
809
|
+
for (const { id } of nodes)
|
|
810
|
+
this.#nodes.delete(id);
|
|
811
|
+
this.#runs.delete(runId);
|
|
812
|
+
const index = this.#runOrder.indexOf(runId);
|
|
813
|
+
if (index >= 0) {
|
|
814
|
+
this.#runOrder.splice(index, 1);
|
|
815
|
+
if (index < this.#cursor)
|
|
816
|
+
this.#cursor -= 1;
|
|
817
|
+
if (this.#cursor >= this.#runOrder.length)
|
|
818
|
+
this.#cursor = 0;
|
|
819
|
+
}
|
|
820
|
+
this.#dispatch();
|
|
821
|
+
}
|
|
726
822
|
toolsFor(parentId, resolveTools) {
|
|
727
823
|
const parent = this.#node(parentId);
|
|
728
824
|
if (!parent.options.tools.includes("agent"))
|
|
@@ -842,6 +938,7 @@ export class FairAgentScheduler {
|
|
|
842
938
|
return;
|
|
843
939
|
const heldPermit = node.state === "running" || node.state === "retrying";
|
|
844
940
|
node.state = result.ok ? "completed" : result.error.code === "CANCELLED" ? "cancelled" : "failed";
|
|
941
|
+
Reflect.deleteProperty(node, "steer");
|
|
845
942
|
this.#persist(node.runId);
|
|
846
943
|
if (heldPermit)
|
|
847
944
|
this.#release(node.runId);
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type DoctorOptions } from "./doctor.js";
|
|
3
|
+
import { type JsonSchema, type JsonValue } from "./index.js";
|
|
4
|
+
import type { WorkflowCatalogFunction } from "./index.js";
|
|
3
5
|
export interface CliOptions extends DoctorOptions {
|
|
4
6
|
inspect?: (sessionId?: string) => Promise<void>;
|
|
7
|
+
transcript?: (sessionFile: string) => Promise<void>;
|
|
8
|
+
stderr?: (text: string) => void;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
trustOverride?: boolean;
|
|
11
|
+
isTTY?: boolean;
|
|
5
12
|
}
|
|
13
|
+
export declare function formatWorkflowCliHelp(fn: WorkflowCatalogFunction, command?: string): string;
|
|
14
|
+
export declare function parseWorkflowCliArgs(schema: JsonSchema, rawArgs: readonly string[]): Record<string, JsonValue>;
|
|
6
15
|
export declare function runCli(args: readonly string[], options?: CliOptions, write?: (text: string) => void): Promise<number>;
|