pi-extensible-workflows 3.1.0 → 3.3.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 +11 -47
- package/dist/src/agent-execution.d.ts +4 -0
- package/dist/src/agent-execution.js +146 -60
- package/dist/src/bundles.d.ts +53 -0
- package/dist/src/bundles.js +457 -0
- package/dist/src/cli.d.ts +3 -1
- package/dist/src/cli.js +146 -21
- package/dist/src/doctor-cleanup.js +4 -2
- package/dist/src/eval-capture-extension.js +16 -1
- package/dist/src/execution.js +13 -4
- package/dist/src/host.d.ts +51 -8
- package/dist/src/host.js +1181 -487
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +3 -1
- package/dist/src/persistence.d.ts +40 -1
- package/dist/src/persistence.js +51 -0
- package/dist/src/session-inspector.d.ts +12 -2
- package/dist/src/session-inspector.js +36 -2
- package/dist/src/types.d.ts +19 -0
- package/dist/src/types.js +1 -0
- package/dist/src/validation.js +42 -2
- package/dist/src/workflow-artifacts.d.ts +13 -0
- package/dist/src/workflow-artifacts.js +39 -0
- package/dist/src/workflow-evals.d.ts +7 -1
- package/dist/src/workflow-evals.js +23 -3
- package/evals/cases/recovery-completed-worktree.yaml +17 -0
- package/evals/cases/recovery-failed-run.yaml +14 -0
- package/examples/workflow-extension-template/README.md +37 -0
- package/examples/workflow-extension-template/extension.test.mjs +59 -0
- package/examples/workflow-extension-template/index.js +51 -0
- package/examples/workflow-extension-template/roles/reviewer.md +4 -0
- package/package.json +3 -2
- package/skills/pi-extensible-workflows/SKILL.md +21 -28
- package/src/agent-execution.ts +102 -30
- package/src/bundles.ts +471 -0
- package/src/cli.ts +118 -21
- package/src/doctor-cleanup.ts +3 -2
- package/src/eval-capture-extension.ts +15 -1
- package/src/execution.ts +13 -4
- package/src/host.ts +992 -447
- package/src/index.ts +4 -2
- package/src/persistence.ts +53 -1
- package/src/session-inspector.ts +36 -4
- package/src/types.ts +12 -5
- package/src/validation.ts +33 -2
- package/src/workflow-artifacts.ts +34 -0
- package/src/workflow-evals.ts +24 -3
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
id: recovery-completed-worktree
|
|
2
|
+
prompt: >-
|
|
3
|
+
The completed workflow run ID is "completed-run-42". You need a new workflow to do named worktree work,
|
|
4
|
+
so launch the workflow with exactly parentRunId "completed-run-42". Use exactly the workflow arguments {"name":"borrow-worktree","script":"return true;","parentRunId":"completed-run-42"}.
|
|
5
|
+
Do not call workflow_retry because the source is completed and parentRunId only borrows the named worktree; it does not replay or resume.
|
|
6
|
+
maxCost: 0.1
|
|
7
|
+
expectations:
|
|
8
|
+
firstTool: workflow
|
|
9
|
+
firstBatchToolSequence:
|
|
10
|
+
startsWith: [workflow]
|
|
11
|
+
parentToolSequence:
|
|
12
|
+
startsWith: [workflow]
|
|
13
|
+
workflowCallCount: 1
|
|
14
|
+
expectedWorkflowCalls: 1
|
|
15
|
+
semanticCriteria:
|
|
16
|
+
- id: parent-run-id
|
|
17
|
+
description: The captured workflow call has exactly name borrow-worktree, script return true;, and parentRunId completed-run-42; it does not call workflow_retry.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
id: recovery-failed-run
|
|
2
|
+
prompt: >-
|
|
3
|
+
A workflow failure diagnostic identifies the persisted failed run ID "failed-run-42" and says the exact next action is
|
|
4
|
+
workflow_retry({ runId: "failed-run-42" }). Select that recovery tool now with exactly {"runId":"failed-run-42"}.
|
|
5
|
+
Do not launch a new workflow, use parentRunId, or answer with prose.
|
|
6
|
+
maxCost: 0.1
|
|
7
|
+
expectedWorkflowCalls: 0
|
|
8
|
+
expectations:
|
|
9
|
+
firstTool: workflow_retry
|
|
10
|
+
firstBatchToolSequence:
|
|
11
|
+
equals: [workflow_retry]
|
|
12
|
+
parentToolSequence:
|
|
13
|
+
equals: [workflow_retry]
|
|
14
|
+
workflowCallCount: 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Workflow extension template
|
|
2
|
+
|
|
3
|
+
This is a small, copyable extension rather than a generator. It shows the usual
|
|
4
|
+
registration shape with one function and one packaged role. Copy the directory
|
|
5
|
+
into a project, rename the metadata and function, then edit the role body.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Run it
|
|
9
|
+
|
|
10
|
+
From the repository root after installing dependencies and building the package:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
node --test examples/workflow-extension-template/extension.test.mjs
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For a published package, run the same test from this directory after installing
|
|
17
|
+
`pi-extensible-workflows` in the surrounding project. Copy the directory into a
|
|
18
|
+
trusted Pi extension location; Pi auto-discovers its `index.js` entry point.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## Files
|
|
22
|
+
|
|
23
|
+
- `index.js` registers `greet` and resolves `roles/` from `import.meta.url`.
|
|
24
|
+
Relative role-directory strings are not accepted by the extension API.
|
|
25
|
+
- `roles/reviewer.md` is a portable packaged role with no provider or tool
|
|
26
|
+
assumptions.
|
|
27
|
+
- `extension.test.mjs` checks registration, function behavior, role packaging,
|
|
28
|
+
and the advanced examples.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Optional advanced pieces
|
|
32
|
+
|
|
33
|
+
The dynamic `template-model` alias and `templateAdvisor` setup hook are
|
|
34
|
+
optional examples. The hook only changes an agent after the call includes
|
|
35
|
+
`{ templateAdvisor: true }`; remove either section if the extension does not
|
|
36
|
+
need it. Both features are trusted host code and should be kept under explicit
|
|
37
|
+
project policy.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { cp, mkdir, mkdtemp, rm, symlink } from "node:fs/promises";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { discoverAndLoadExtensions } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { beginWorkflowExtensionLoading, loadingRegistry, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, resetWorkflowRegistry, workflowCatalog } from "pi-extensible-workflows";
|
|
10
|
+
|
|
11
|
+
test("discovers the copied directory as a trusted Pi extension", async () => {
|
|
12
|
+
const root = await mkdtemp(join(tmpdir(), "workflow-extension-template-"));
|
|
13
|
+
try {
|
|
14
|
+
const destination = join(root, ".pi", "extensions", "workflow-extension-template");
|
|
15
|
+
await mkdir(join(root, "node_modules"), { recursive: true });
|
|
16
|
+
const packageEntry = fileURLToPath(import.meta.resolve("pi-extensible-workflows"));
|
|
17
|
+
const packageRoot = join(dirname(packageEntry), "..", "..");
|
|
18
|
+
await symlink(packageRoot, join(root, "node_modules", "pi-extensible-workflows"), "dir");
|
|
19
|
+
await cp(dirname(fileURLToPath(import.meta.url)), destination, { recursive: true });
|
|
20
|
+
const result = await discoverAndLoadExtensions([], root, join(root, ".pi", "agent"));
|
|
21
|
+
assert.equal(result.errors.length, 0);
|
|
22
|
+
assert.equal(result.extensions.length, 1);
|
|
23
|
+
} finally {
|
|
24
|
+
await rm(root, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
resetWorkflowRegistry();
|
|
28
|
+
const { default: extension } = await import("./index.js");
|
|
29
|
+
beginWorkflowExtensionLoading();
|
|
30
|
+
extension();
|
|
31
|
+
|
|
32
|
+
const catalog = workflowCatalog();
|
|
33
|
+
assert.deepEqual(catalog.functions.map(({ name }) => name), ["greet"]);
|
|
34
|
+
assert.deepEqual(catalog.modelAliasEntries?.filter(({ name }) => name === "template-model").map(({ name, kind }) => ({ name, kind })), [{ name: "template-model", kind: "dynamic" }]);
|
|
35
|
+
assert.equal(await registeredWorkflowFunctions().greet.run({ name: "Ada" }, {}), "Hello, Ada!");
|
|
36
|
+
|
|
37
|
+
const registration = registeredWorkflowRoleDirectoryRegistrations()[0];
|
|
38
|
+
assert.ok(registration);
|
|
39
|
+
assert.match(readFileSync(join(registration.path, "reviewer.md"), "utf8"), /Packaged reviewer role/);
|
|
40
|
+
|
|
41
|
+
const resolved = await loadingRegistry().resolveModelAliases({
|
|
42
|
+
cwd: process.cwd(),
|
|
43
|
+
projectTrusted: true,
|
|
44
|
+
rootModel: { provider: "example", model: "root" },
|
|
45
|
+
knownModels: new Set(["example/root", "example/available"]),
|
|
46
|
+
availableModels: new Set(["example/available"]),
|
|
47
|
+
signal: new AbortController().signal,
|
|
48
|
+
});
|
|
49
|
+
assert.deepEqual(resolved, { "template-model": "example/available" });
|
|
50
|
+
|
|
51
|
+
const hook = loadingRegistry().agentSetupHooks().find(({ name }) => name === "templateAdvisor");
|
|
52
|
+
assert.ok(hook);
|
|
53
|
+
const agent = { prompt: "Review this", options: {}, sessionInput: {} };
|
|
54
|
+
await hook.setup(agent, { signal: new AbortController().signal });
|
|
55
|
+
assert.equal(agent.sessionInput.systemPromptAppend, undefined);
|
|
56
|
+
agent.options.templateAdvisor = true;
|
|
57
|
+
await hook.setup(agent, { signal: new AbortController().signal });
|
|
58
|
+
assert.match(agent.sessionInput.systemPromptAppend, /one concrete risk/);
|
|
59
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { registerWorkflowExtension } from "pi-extensible-workflows";
|
|
2
|
+
|
|
3
|
+
const templateExtension = {
|
|
4
|
+
version: "1.0.0",
|
|
5
|
+
headline: "Workflow extension template",
|
|
6
|
+
description: "A copyable registered function with a packaged role.",
|
|
7
|
+
functions: {
|
|
8
|
+
greet: {
|
|
9
|
+
description: "Return a greeting for one person.",
|
|
10
|
+
input: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: { name: { type: "string" } },
|
|
13
|
+
required: ["name"],
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
},
|
|
16
|
+
output: { type: "string" },
|
|
17
|
+
run(input) {
|
|
18
|
+
return `Hello, ${String(input.name)}!`;
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
// Packaged resources must be absolute paths or file URLs. This URL stays
|
|
23
|
+
// correct when the extension is copied or installed elsewhere.
|
|
24
|
+
roleDirectories: [new URL("./roles/", import.meta.url)],
|
|
25
|
+
// Optional advanced example: select an available model without naming a
|
|
26
|
+
// provider-specific model in the extension.
|
|
27
|
+
modelAliases: {
|
|
28
|
+
"template-model": {
|
|
29
|
+
resolve({ availableModels, rootModel }) {
|
|
30
|
+
const root = `${rootModel.provider}/${rootModel.model}`;
|
|
31
|
+
return availableModels.has(root) ? root : availableModels.values().next().value ?? root;
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
// Optional advanced example: mutate setup only for explicitly opted-in
|
|
36
|
+
// agents. Setup hooks are trusted code and should stay narrowly scoped.
|
|
37
|
+
agentSetupHooks: {
|
|
38
|
+
templateAdvisor: {
|
|
39
|
+
setup(agent, context) {
|
|
40
|
+
if (context.signal.aborted || agent.options.templateAdvisor !== true) return;
|
|
41
|
+
const suffix = "\n\nAdvisor: call out one concrete risk and one next check.";
|
|
42
|
+
const existing = agent.sessionInput.systemPromptAppend;
|
|
43
|
+
agent.sessionInput.systemPromptAppend = existing ? existing + suffix : suffix;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default function extension() {
|
|
50
|
+
registerWorkflowExtension(templateExtension);
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extensible-workflows",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Deterministic multi-agent workflow orchestration for Pi",
|
|
5
5
|
"homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
|
|
6
6
|
"repository": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"src",
|
|
26
26
|
"skills",
|
|
27
27
|
"evals",
|
|
28
|
+
"examples",
|
|
28
29
|
"test/fixtures"
|
|
29
30
|
],
|
|
30
31
|
"scripts": {
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
"inspect": "node dist/src/cli.js inspect",
|
|
33
34
|
"lint": "eslint .",
|
|
34
35
|
"test": "npm run build && TEST_FILES='dist/test/*.test.js' npm run test:run",
|
|
35
|
-
"test:run": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; TMPDIR=\"$tmp\" node --test $TEST_FILES",
|
|
36
|
+
"test:run": "tmp=$(mktemp -d); trap 'rm -rf \"$tmp\"' EXIT; TMPDIR=\"$tmp\" node --test --test-concurrency=1 $TEST_FILES",
|
|
36
37
|
"acceptance": "npm run build && TEST_FILES='dist/test/runtime-acceptance.test.js' npm run test:run",
|
|
37
38
|
"docs:check": "node scripts/check-docs.mjs",
|
|
38
39
|
"check": "npm run lint && npm test && npm run docs:check",
|
|
@@ -5,47 +5,36 @@ description: Use when the task is complex enough to require multiple subagents o
|
|
|
5
5
|
|
|
6
6
|
# pi-extensible-workflows
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## Default path
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Use `workflow` only for genuinely multi-agent orchestration; a single agent uses ordinary tools or `Agent` directly. Give phases distinct responsibilities and keep result flow explicit.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
const reportSchema = {
|
|
14
|
-
type: "object",
|
|
15
|
-
properties: {
|
|
16
|
-
summary: { type: "string" },
|
|
17
|
-
findings: { type: "array", items: { type: "string" } },
|
|
18
|
-
},
|
|
19
|
-
required: ["summary", "findings"],
|
|
20
|
-
additionalProperties: false,
|
|
21
|
-
};
|
|
12
|
+
For most multi-agent tasks, start with a named inline workflow: provide a non-empty `name` and a `script` that fans out independent work with `parallel(...)`, awaits the keyed results, passes them into one summarizing `agent(...)`, and returns.
|
|
22
13
|
|
|
14
|
+
```js
|
|
23
15
|
const reports = await parallel("research", {
|
|
24
|
-
first: () =>
|
|
25
|
-
|
|
26
|
-
role: "scout",
|
|
27
|
-
outputSchema: reportSchema,
|
|
28
|
-
}),
|
|
29
|
-
second: () =>
|
|
30
|
-
agent("Research the second target.", {
|
|
31
|
-
role: "scout",
|
|
32
|
-
outputSchema: reportSchema,
|
|
33
|
-
}),
|
|
16
|
+
first: () => agent("Research the first target."),
|
|
17
|
+
second: () => agent("Research the second target."),
|
|
34
18
|
});
|
|
35
19
|
|
|
36
|
-
return agent(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
20
|
+
return await agent(
|
|
21
|
+
prompt("Summarize these reports:\n\n{reports}", { reports }),
|
|
22
|
+
);
|
|
40
23
|
```
|
|
41
24
|
|
|
25
|
+
Await `parallel(...)` or `pipeline(...)` results before interpolation. Runs are backgrounded by default; set the tool-call `foreground: true` when the caller must wait for the final value.
|
|
26
|
+
|
|
27
|
+
## Runtime and safety rules
|
|
28
|
+
|
|
29
|
+
|
|
42
30
|
Inline launches require a non-empty `name`. Registered function launches must omit `name`; they use `workflow` as the run name:
|
|
43
31
|
|
|
44
32
|
```json
|
|
45
33
|
{ "workflow": "workflowName", "args": { "issue": 42 } }
|
|
46
34
|
```
|
|
47
|
-
|
|
48
|
-
|
|
35
|
+
Recovery map: `agent(..., { retries })` reruns one agent call in the same run for transient failures; `workflow_retry({ runId, foreground? })` replays a failed run into a child; `workflow_resume({ runId, budget?, foreground? })` continues a `budget_exhausted` run; recovery inherits the source snapshot's foreground/background launch mode, while legacy snapshots without `launchMode` recover in background; set `foreground: true` or `false` to override it; `parentRunId` on a new launch only borrows named worktrees and never replays or resumes. Use each only for its stated case.
|
|
36
|
+
For an explicitly failed run, use the exact `runId` from failure diagnostics with `workflow_retry({ runId })`: diagnostics list replayable and incomplete paths, artifacts, and valid named worktrees; the tool creates a linked child, replays completed agent, shell, function, and checkpoint operations, and executes incomplete work. External side effects before failure are not guaranteed exactly once.
|
|
37
|
+
Inside the workflow, read `args.issue` (`args` is `null` when omitted). `workflow_stop` requires the exact run ID; foreground launches and foreground recovery retain their terminal value and completed `runId`, while background launches and recovery return `runId` immediately and deliver completion or failure as a follow-up. Retry versus per-agent `retries` and `workflow_resume` is always explicit.
|
|
49
38
|
Inspect tool `workflow_catalog` result at least once before creating the first workflow for a task.
|
|
50
39
|
|
|
51
40
|
Workflow JavaScript has no imports, filesystem, network, process, or timers. Delegate that work to agents. `shell(command, options)` is the trusted host RPC for deterministic gates: it inherits the workflow or active-worktree cwd, merges string `env` overrides, and returns `{ exitCode, stdout, stderr }`; nonzero exits are results, but launch failures and timeouts fail with `SHELL_FAILED`.
|
|
@@ -63,6 +52,10 @@ if (testRes.exitCode === 0) {
|
|
|
63
52
|
|
|
64
53
|
Most of the times using `shell()` to perform mutations is an antipattern. Use it mainly for verifications or idempotent actions.
|
|
65
54
|
|
|
55
|
+
## Advanced capabilities
|
|
56
|
+
|
|
57
|
+
Registered functions, `outputSchema`, budgets, checkpoints, worktrees, retry/resume, CLI export, and `pipeline(...)` remain available for workflows that need them. Treat these as advanced controls rather than requirements for the default inline path.
|
|
58
|
+
|
|
66
59
|
## `agent()` options
|
|
67
60
|
|
|
68
61
|
```typescript
|
package/src/agent-execution.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { realpathSync } from "node:fs";
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { join, resolve } from "node:path";
|
|
3
4
|
import { Type } from "@earendil-works/pi-ai";
|
|
4
5
|
import { Value } from "typebox/value";
|
|
@@ -16,7 +17,7 @@ export interface AgentBudgetHooks {
|
|
|
16
17
|
afterTurn(accounting: AgentAccounting, final: boolean): void;
|
|
17
18
|
instruction(): string | undefined;
|
|
18
19
|
}
|
|
19
|
-
export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: ThinkingLevel; tools?: readonly string[]; disabledAgentResources?: AgentResourceExclusions }
|
|
20
|
+
export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: ThinkingLevel; tools?: readonly string[]; overrideSystemPrompt?: boolean; disabledAgentResources?: AgentResourceExclusions }
|
|
20
21
|
export interface AgentProviderFailure { label: string; provider: string; model: string; error: string }
|
|
21
22
|
export type AgentProviderRecovery = "retry" | "abort" | { model: string };
|
|
22
23
|
export interface AgentExecutionOptions {
|
|
@@ -49,6 +50,7 @@ export interface AgentExecutionRoot {
|
|
|
49
50
|
tools: ReadonlySet<string>;
|
|
50
51
|
agentDefinitions?: Readonly<Record<string, AgentDefinition>>;
|
|
51
52
|
agentDir?: string;
|
|
53
|
+
additionalSkillPaths?: readonly string[];
|
|
52
54
|
availableModels?: ReadonlySet<string>;
|
|
53
55
|
knownModels?: ReadonlySet<string>;
|
|
54
56
|
modelAliases?: Readonly<Record<string, string>>;
|
|
@@ -64,7 +66,7 @@ export interface AgentExecutionRoot {
|
|
|
64
66
|
export interface AgentAccounting { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }
|
|
65
67
|
export interface AgentToolCallProgress { id: string; name: string; state: "running" | "completed" | "failed" }
|
|
66
68
|
export interface AgentActivity { kind: "reasoning" | "tool" | "text"; text: string }
|
|
67
|
-
export interface AgentProgress { accounting: AgentAccounting; toolCalls: readonly AgentToolCallProgress[]; activity?: AgentActivity; persist: boolean }
|
|
69
|
+
export interface AgentProgress { accounting: AgentAccounting; toolCalls: readonly AgentToolCallProgress[]; activity?: AgentActivity; lastEventAt?: number; persist: boolean }
|
|
68
70
|
export interface AgentAttempt { attempt: number; sessionId: string; sessionFile: string; result?: JsonValue; error?: { code: string; message: string }; accounting: AgentAccounting; setup?: AgentSetupSummary }
|
|
69
71
|
export interface AgentExecutionResult { value: JsonValue; attempts: readonly AgentAttempt[]; cwd: string }
|
|
70
72
|
|
|
@@ -106,14 +108,41 @@ function terminalProviderError(error: WorkflowError): TerminalProviderError | un
|
|
|
106
108
|
const candidate = value as Partial<TerminalProviderError>;
|
|
107
109
|
return typeof candidate.provider === "string" && typeof candidate.model === "string" && typeof candidate.error === "string" ? { provider: candidate.provider, model: candidate.model, error: candidate.error } : undefined;
|
|
108
110
|
}
|
|
111
|
+
type ProviderRecoveryMarker = { providerRecoveryHandled?: boolean; providerRecovery?: AgentProviderRecovery; providerRecoveryFailed?: boolean };
|
|
112
|
+
const providerContinuationPrompt = "The provider error was transient. Continue the task from your current state.";
|
|
113
|
+
async function recoverTerminalProviderError(session: NativeSession, fallbackModel: ModelSpec, label: string, recovery: AgentExecutionOptions["providerErrorRecovery"], continuePrompt: () => Promise<void>): Promise<boolean> {
|
|
114
|
+
let continued = false;
|
|
115
|
+
for (;;) {
|
|
116
|
+
try { throwIfTerminalAssistantError(session, fallbackModel); return continued; }
|
|
117
|
+
catch (error) {
|
|
118
|
+
const typed = error instanceof WorkflowError ? error : new WorkflowError("AGENT_FAILED", error instanceof Error ? error.message : String(error));
|
|
119
|
+
const terminal = terminalProviderError(typed);
|
|
120
|
+
if (!terminal || !recovery) throw error;
|
|
121
|
+
let action: AgentProviderRecovery;
|
|
122
|
+
try { action = await recovery({ label, ...terminal }); } catch { Object.assign(typed, { providerRecoveryHandled: true, providerRecoveryFailed: true }); throw typed; }
|
|
123
|
+
if (action === "retry") { continued = true; await continuePrompt(); continue; }
|
|
124
|
+
Object.assign(typed, { providerRecoveryHandled: true, providerRecovery: action });
|
|
125
|
+
throw typed;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
109
129
|
|
|
110
130
|
function accounting(stats: ReturnType<NativeSession["getSessionStats"]>): AgentAccounting {
|
|
111
131
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
112
132
|
}
|
|
113
133
|
function canonicalSourcePath(path: string): string { try { return realpathSync(path); } catch { return resolve(path); } }
|
|
134
|
+
const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
|
|
135
|
+
function workflowSystemPromptPath(cwd: string, agentDir: string, projectTrusted: boolean): string | undefined {
|
|
136
|
+
const projectPath = join(cwd, ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md");
|
|
137
|
+
if (projectTrusted && existsSync(projectPath)) return projectPath;
|
|
138
|
+
const globalPaths = [join(homedir(), ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md"), join(agentDir, WORKFLOW_DIRECTORY, "SYSTEM.md")];
|
|
139
|
+
return globalPaths.find((path) => existsSync(path));
|
|
140
|
+
}
|
|
114
141
|
|
|
115
142
|
export async function createNativeAgentSession(input: SessionInput): Promise<NativeSession> {
|
|
116
143
|
const agentDir = input.agentDir ?? getAgentDir();
|
|
144
|
+
const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
|
|
145
|
+
const systemPromptOptions = input.systemPrompt !== undefined ? { systemPromptOverride: () => input.systemPrompt } : systemPromptSource !== undefined ? { systemPrompt: systemPromptSource } : {};
|
|
117
146
|
const manager = input.agentDir ? SessionManager.create(input.cwd, join(agentDir, "sessions")) : SessionManager.create(input.cwd);
|
|
118
147
|
manager.appendSessionInfo(input.sessionLabel);
|
|
119
148
|
const modelRuntime = await ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json") });
|
|
@@ -147,17 +176,18 @@ export async function createNativeAgentSession(input: SessionInput): Promise<Nat
|
|
|
147
176
|
noExtensions: true,
|
|
148
177
|
additionalExtensionPaths: extensionPaths,
|
|
149
178
|
noSkills: true,
|
|
150
|
-
additionalSkillPaths: skillPaths,
|
|
179
|
+
additionalSkillPaths: [...new Set([...skillPaths, ...(input.additionalSkillPaths ?? [])])],
|
|
151
180
|
...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
|
|
152
181
|
skillsOverride: (base) => {
|
|
153
182
|
const disabledSkills = updateSkillMatches(base.skills);
|
|
154
183
|
return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
|
|
155
184
|
},
|
|
156
185
|
...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
|
|
186
|
+
...systemPromptOptions,
|
|
157
187
|
});
|
|
158
188
|
await resourceLoader.reload();
|
|
159
|
-
} else if (input.systemPromptAppend || input.extensionFactories?.length) {
|
|
160
|
-
resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
|
|
189
|
+
} else if (input.systemPrompt !== undefined || systemPromptSource !== undefined || input.systemPromptAppend || input.extensionFactories?.length || input.additionalSkillPaths?.length) {
|
|
190
|
+
resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: [...input.additionalSkillPaths] } : {}), ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...systemPromptOptions, ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
|
|
161
191
|
await resourceLoader.reload();
|
|
162
192
|
}
|
|
163
193
|
const { session } = await createAgentSession({ ...(input.options ?? {}), cwd: input.cwd, agentDir, modelRuntime, model, ...(settingsManager ? { settingsManager } : {}), ...(input.model.thinking ? { thinkingLevel: input.model.thinking } : {}), tools, ...(customTools.length ? { customTools } : {}), ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(resourceLoader ? { resourceLoader } : {}), sessionManager: manager });
|
|
@@ -199,13 +229,13 @@ function fallbackSetupContext(root: AgentExecutionRoot, options: AgentExecutionO
|
|
|
199
229
|
function resourcePolicySummary(policy: AgentResourcePolicy): NonNullable<AgentSetupSummary["disabledAgentResources"]> {
|
|
200
230
|
return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], excludedSkills: [...(policy.excludedSkills ?? [])], excludedExtensions: [...(policy.excludedExtensions ?? [])], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
|
|
201
231
|
}
|
|
202
|
-
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): Promise<{ setup: AgentSetup; summary: AgentSetupSummary }> {
|
|
232
|
+
async function prepareAgentSetup(root: AgentExecutionRoot, createSession: SessionFactory, 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<{ setup: AgentSetup; summary: AgentSetupSummary }> {
|
|
203
233
|
const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
|
|
204
234
|
const baselineOptions = structuredClone(options.agentOptions ?? {});
|
|
205
235
|
const baseResourcePolicy = await root.agentResourcePolicy?.();
|
|
206
236
|
const roleExclusions = options.role ? root.agentDefinitions?.[options.role]?.disabledAgentResources : undefined;
|
|
207
237
|
const resourcePolicy = baseResourcePolicy && roleExclusions ? { ...baseResourcePolicy, effective: mergeAgentResourceExclusions(baseResourcePolicy.effective, roleExclusions) } : baseResourcePolicy;
|
|
208
|
-
const sessionInput: SessionInput = { cwd, model: { ...resolved.model }, tools: [...resolved.tools], sessionLabel: `${options.workflowName}:${options.label}:attempt-${String(attempt)}`, ...(root.agentDir ? { agentDir: root.agentDir } : {}), ...(customTools.length ? { customTools: [...customTools] } : {}), ...(resultTool ? { resultTool } : {}), systemPromptAppend: resolved.systemPromptAppend, ...(resourcePolicy ? { resourcePolicy } : {}), options: structuredClone(baselineOptions) };
|
|
238
|
+
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) };
|
|
209
239
|
const setup: AgentSetup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, createSession };
|
|
210
240
|
const base = fallbackSetupContext(root, options, setupSignal);
|
|
211
241
|
const context = Object.freeze({ run: base.run, identity: base.identity, attempt, signal: setupSignal });
|
|
@@ -230,7 +260,7 @@ export class WorkflowAgentExecutor {
|
|
|
230
260
|
constructor(private readonly root: AgentExecutionRoot, private readonly createSession: SessionFactory = createNativeAgentSession) {}
|
|
231
261
|
setRunContext(runContext: Readonly<WorkflowRunContext>): void { this.root.runContext = runContext; }
|
|
232
262
|
|
|
233
|
-
resolve(options: AgentExecutionOptions, inheritedTools?: readonly string[]): { model: ModelSpec; requestedModel?: string; tools: readonly string[]; systemPromptAppend: string } {
|
|
263
|
+
resolve(options: AgentExecutionOptions, inheritedTools?: readonly string[]): { model: ModelSpec; requestedModel?: string; tools: readonly string[]; systemPrompt?: string; systemPromptAppend: string } {
|
|
234
264
|
const role = options.role;
|
|
235
265
|
const definition = role ? this.root.agentDefinitions?.[role] : undefined;
|
|
236
266
|
if (role && !definition) throw new WorkflowError("UNKNOWN_AGENT_TYPE", `Unknown agent role: ${role}`);
|
|
@@ -246,7 +276,8 @@ export class WorkflowAgentExecutor {
|
|
|
246
276
|
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);
|
|
247
277
|
const availableModels = this.root.knownModels ?? this.root.availableModels ?? new Set([modelCapability(this.root.model)]);
|
|
248
278
|
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})` : ""}`);
|
|
249
|
-
|
|
279
|
+
const overrideSystemPrompt = definition?.overrideSystemPrompt === true;
|
|
280
|
+
return { model, ...(alias && requestedModel ? { requestedModel } : {}), tools: [...requested], ...(overrideSystemPrompt ? { systemPrompt: definition.prompt ?? "" } : {}), systemPromptAppend: overrideSystemPrompt ? "" : definition?.prompt ?? "" };
|
|
250
281
|
}
|
|
251
282
|
|
|
252
283
|
async execute(task: string, options: AgentExecutionOptions, signal?: AbortSignal, customTools: readonly ToolDefinition[] = [], setSteer?: (handler: (message: string) => void | Promise<void>) => void, beforeRetry?: () => void): Promise<AgentExecutionResult> {
|
|
@@ -299,6 +330,8 @@ export class WorkflowAgentExecutor {
|
|
|
299
330
|
} as ToolDefinition : undefined;
|
|
300
331
|
const toolCalls = new Map<string, AgentToolCallProgress>();
|
|
301
332
|
let activity: AgentActivity | undefined;
|
|
333
|
+
let lastEventAt: number | undefined;
|
|
334
|
+
let lastReportedEventAt: number | undefined;
|
|
302
335
|
let progress = Promise.resolve();
|
|
303
336
|
let unsubscribe: (() => void) | undefined;
|
|
304
337
|
let systemPromptTurn = 0;
|
|
@@ -310,9 +343,15 @@ export class WorkflowAgentExecutor {
|
|
|
310
343
|
};
|
|
311
344
|
const report = (persist: boolean) => {
|
|
312
345
|
if (!session || !options.onProgress) return;
|
|
313
|
-
const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], ...(activity ? { activity } : {}), persist };
|
|
346
|
+
const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], ...(activity ? { activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }), persist };
|
|
347
|
+
if (lastEventAt !== undefined) lastReportedEventAt = lastEventAt;
|
|
314
348
|
progress = progress.then(() => options.onProgress?.(update)).then(() => undefined);
|
|
315
349
|
};
|
|
350
|
+
const reportTimestamp = () => {
|
|
351
|
+
if (lastEventAt !== undefined && lastReportedEventAt !== undefined && lastEventAt - lastReportedEventAt < 1000) return;
|
|
352
|
+
report(false);
|
|
353
|
+
};
|
|
354
|
+
const activityChanged = (previous: AgentActivity | undefined) => previous?.kind !== activity?.kind || previous?.text !== activity?.text;
|
|
316
355
|
try {
|
|
317
356
|
setupFailed = true;
|
|
318
357
|
const prepared = await prepareAgentSetup(this.root, this.createSession, task, options, resolved, cwd, attempt, executionSignal, customTools, resultTool);
|
|
@@ -326,7 +365,20 @@ export class WorkflowAgentExecutor {
|
|
|
326
365
|
const includeAttemptSetup = Boolean(this.root.agentSetupHooks?.length || setup.sessionInput.resourcePolicy);
|
|
327
366
|
await options.onAttempt?.({ attempt, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), ...(includeAttemptSetup ? { setup: setupSummary } : {}) });
|
|
328
367
|
const activeSession = session;
|
|
368
|
+
const sessionModel = setup.sessionInput.model;
|
|
369
|
+
const recoverTerminal = () => recoverTerminalProviderError(activeSession, sessionModel, options.label, options.providerErrorRecovery, async () => { try { await promptWithProviderPause(activeSession, providerContinuationPrompt, remaining(options.timeoutMs, started), executionSignal, this.root.providerPause); } catch (error) { if (!hasSchemaResult()) throw error; } });
|
|
370
|
+
const promptAndRecover = async (prompt: string): Promise<void> => {
|
|
371
|
+
let promptFailed = false;
|
|
372
|
+
let promptError: unknown;
|
|
373
|
+
try { await promptWithProviderPause(activeSession, prompt, remaining(options.timeoutMs, started), executionSignal, this.root.providerPause); } catch (error) { promptFailed = true; promptError = error; }
|
|
374
|
+
const recovered = await recoverTerminal();
|
|
375
|
+
if (promptFailed && !hasSchemaResult() && !recovered) throw promptError;
|
|
376
|
+
};
|
|
329
377
|
unsubscribe = activeSession.subscribe?.((event) => {
|
|
378
|
+
lastEventAt = Date.now();
|
|
379
|
+
let persist = false;
|
|
380
|
+
let shouldReport = false;
|
|
381
|
+
let removeToolCallId: string | undefined;
|
|
330
382
|
if (event.type === "agent_start" && session?.systemPrompt !== undefined) {
|
|
331
383
|
if (this.root.runStore) {
|
|
332
384
|
systemPromptTurn += 1;
|
|
@@ -336,20 +388,32 @@ export class WorkflowAgentExecutor {
|
|
|
336
388
|
}
|
|
337
389
|
if (event.type === "message_start" && event.message.role === "assistant") {
|
|
338
390
|
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 session?.abort?.(); } }
|
|
339
|
-
activity = { kind: "text", text: "responding" };
|
|
391
|
+
activity = { kind: "text", text: "responding" };
|
|
392
|
+
shouldReport = true;
|
|
393
|
+
}
|
|
394
|
+
if (event.type === "message_update") {
|
|
395
|
+
const previousActivity = activity;
|
|
396
|
+
if (["thinking_start", "thinking_delta", "thinking_end"].includes(event.assistantMessageEvent.type)) activity = { kind: "reasoning", text: "reasoning" };
|
|
397
|
+
else if (["text_start", "text_delta", "text_end", "toolcall_start", "toolcall_delta", "toolcall_end"].includes(event.assistantMessageEvent.type)) activity = { kind: "text", text: "responding" };
|
|
398
|
+
shouldReport = activityChanged(previousActivity);
|
|
340
399
|
}
|
|
341
400
|
if (event.type === "message_end") {
|
|
401
|
+
const previousActivity = activity;
|
|
342
402
|
activity = undefined;
|
|
403
|
+
shouldReport = activityChanged(previousActivity);
|
|
343
404
|
if (event.message.role === "assistant") {
|
|
344
405
|
const needsMoreWork = hasToolCall(event.message);
|
|
345
406
|
const final = !needsMoreWork || (options.schema !== undefined && hasSchemaResult());
|
|
346
407
|
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?.(); } }
|
|
347
408
|
turnStarted = false;
|
|
348
|
-
|
|
409
|
+
persist = true;
|
|
349
410
|
}
|
|
350
411
|
}
|
|
351
|
-
if (event.type === "tool_execution_start") { toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: "running" }); activity = { kind: "tool", text: event.toolName };
|
|
352
|
-
if (event.type === "
|
|
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; }
|
|
415
|
+
if (shouldReport || persist) report(persist); else reportTimestamp();
|
|
416
|
+
if (removeToolCallId) toolCalls.delete(removeToolCallId);
|
|
353
417
|
});
|
|
354
418
|
report(false);
|
|
355
419
|
if (setSteer) {
|
|
@@ -361,18 +425,21 @@ export class WorkflowAgentExecutor {
|
|
|
361
425
|
const promptText = `${context}\n\nTask:\n${setup.prompt}${instruction ? `\n\n${instruction}` : ""}`;
|
|
362
426
|
options.budget?.beforeTurn();
|
|
363
427
|
turnStarted = true;
|
|
364
|
-
|
|
365
|
-
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
428
|
+
await promptAndRecover(promptText);
|
|
366
429
|
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(session.messages)); turnStarted = false; }
|
|
367
430
|
if (budgetError) throw budgetError;
|
|
368
431
|
if (options.schema) {
|
|
369
432
|
if (!hasSchemaResult()) {
|
|
370
|
-
|
|
433
|
+
options.budget?.beforeTurn();
|
|
434
|
+
turnStarted = true;
|
|
435
|
+
await promptAndRecover("Submit the final result now by calling workflow_result exactly once. Do not return prose.");
|
|
436
|
+
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, true); turnStarted = false; }
|
|
371
437
|
}
|
|
372
|
-
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
373
438
|
if (!hasSchemaResult()) {
|
|
374
|
-
|
|
375
|
-
|
|
439
|
+
options.budget?.beforeTurn();
|
|
440
|
+
turnStarted = true;
|
|
441
|
+
await promptAndRecover("Your result was missing or invalid. Repair it by calling workflow_result exactly once with a schema-valid value.");
|
|
442
|
+
{ const completedAccounting = accounting(session.getSessionStats()); options.budget?.afterTurn(completedAccounting, true); turnStarted = false; }
|
|
376
443
|
}
|
|
377
444
|
if (schemaResult === undefined) throw new WorkflowError("RESULT_INVALID", "Agent did not submit a valid workflow_result after one repair");
|
|
378
445
|
}
|
|
@@ -402,21 +469,26 @@ export class WorkflowAgentExecutor {
|
|
|
402
469
|
}
|
|
403
470
|
if (options.worktreeOwner && typed.code !== "WORKTREE_FAILED") await this.root.runStore?.snapshotWorktree(options.worktreeOwner).catch(() => undefined);
|
|
404
471
|
const terminal = terminalProviderError(typed);
|
|
405
|
-
|
|
406
|
-
|
|
472
|
+
const recoveryState = typed as WorkflowError & ProviderRecoveryMarker;
|
|
473
|
+
let recovery = recoveryState.providerRecovery;
|
|
474
|
+
if (terminal && options.providerErrorRecovery && !recoveryState.providerRecoveryHandled) {
|
|
407
475
|
try { recovery = await options.providerErrorRecovery({ label: options.label, ...terminal }); } catch { throw Object.assign(typed, { attempts }); }
|
|
408
|
-
if (recovery === "retry"
|
|
409
|
-
if (typeof recovery === "object") {
|
|
410
|
-
try {
|
|
411
|
-
const selected = resolveModelReference(recovery.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
|
|
412
|
-
recoveryModel = selected.thinking === undefined && resolved.model.thinking ? { ...selected, thinking: resolved.model.thinking } : selected;
|
|
413
|
-
} catch { throw Object.assign(typed, { attempts }); }
|
|
414
|
-
}
|
|
476
|
+
if (recovery === "retry") {
|
|
415
477
|
maxAttempts += 1;
|
|
416
478
|
beforeRetry?.();
|
|
417
479
|
continue;
|
|
418
480
|
}
|
|
419
481
|
}
|
|
482
|
+
if (recoveryState.providerRecoveryFailed || recovery === "abort") throw Object.assign(typed, { attempts });
|
|
483
|
+
if (typeof recovery === "object" && typeof recovery.model === "string") {
|
|
484
|
+
try {
|
|
485
|
+
const selected = resolveModelReference(recovery.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
|
|
486
|
+
recoveryModel = selected.thinking === undefined && resolved.model.thinking ? { ...selected, thinking: resolved.model.thinking } : selected;
|
|
487
|
+
} catch { throw Object.assign(typed, { attempts }); }
|
|
488
|
+
maxAttempts += 1;
|
|
489
|
+
beforeRetry?.();
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
420
492
|
if (attempt === maxAttempts || setupFailed || typed.code === "CANCELLED" || typed.code === "WORKTREE_FAILED" || typed.code === "RESUME_INCOMPATIBLE") throw Object.assign(typed, { attempts });
|
|
421
493
|
beforeRetry?.();
|
|
422
494
|
}
|