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
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Turn multi-agent tasks into deterministic jobs that fan out in parallel, pause for approval, and resume without rerunning completed work.
|
|
8
8
|
|
|
9
|
-
[Documentation](https://vekexasia.github.io/pi-extensible-workflows/) | [Developer guide](https://vekexasia.github.io/pi-extensible-workflows/developers.html) | [Agent guide](https://vekexasia.github.io/pi-extensible-workflows/agents.html) | [Extension authoring](https://vekexasia.github.io/pi-extensible-workflows/extensions.html)
|
|
9
|
+
[Documentation](https://vekexasia.github.io/pi-extensible-workflows/) | [Developer guide](https://vekexasia.github.io/pi-extensible-workflows/developers.html) | [Agent guide](https://vekexasia.github.io/pi-extensible-workflows/agents.html) | [Extension authoring](https://vekexasia.github.io/pi-extensible-workflows/extensions.html) | [Video overview](https://youtu.be/qAiivspEHmU)
|
|
10
10
|
|
|
11
11
|
Requires Node.js 22.19 or newer. This is a trusted Pi extension with the same filesystem and process access as Pi.
|
|
12
12
|
|
|
@@ -20,32 +20,24 @@ For source installs and local development, see the [installation guide](https://
|
|
|
20
20
|
|
|
21
21
|
## Capabilities
|
|
22
22
|
|
|
23
|
-
The
|
|
24
|
-
|
|
25
|
-
Inline workflow launches require a non-empty `name`; registered function launches reject `name` and use their registered function name as the run name. Workflow worktree scopes always use the explicit `withWorktree(name, callback)` form.
|
|
26
|
-
|
|
27
|
-
A workflow can fan out across specialized agents, combine their results, and resume without rerunning completed work.
|
|
23
|
+
The default path is a named inline workflow: write a `script` that fans out independent work with `parallel(...)`, awaits the keyed results, passes them into one summarizing `agent(...)`, and returns. Inline launches require a non-empty `name`; registered function launches reject `name` and use their registered function name as the run name. Runs are backgrounded by default; set `foreground: true` when the final value must be returned in the same tool call. If a foreground tool call detaches before its result is accepted by the next event-loop turn, the terminal success or failure is promoted to exactly one follow-up message.
|
|
28
24
|
|
|
29
25
|
```js
|
|
30
26
|
const reviews = await parallel("review", {
|
|
31
|
-
correctness: () =>
|
|
32
|
-
|
|
33
|
-
security: () =>
|
|
34
|
-
agent("Review the current changes for security risks.", {
|
|
35
|
-
role: "security-specialist",
|
|
36
|
-
}),
|
|
27
|
+
correctness: () => agent("Review the current changes for correctness issues."),
|
|
28
|
+
security: () => agent("Review the current changes for security risks."),
|
|
37
29
|
tests: () => agent("Review the current changes for missing test coverage."),
|
|
38
30
|
});
|
|
39
31
|
|
|
40
|
-
|
|
41
|
-
prompt("
|
|
42
|
-
reviews,
|
|
43
|
-
}),
|
|
32
|
+
return await agent(
|
|
33
|
+
prompt("Summarize and prioritize these findings:\n\n{reviews}", { reviews }),
|
|
44
34
|
);
|
|
45
|
-
|
|
46
|
-
return summary;
|
|
47
35
|
```
|
|
48
36
|
|
|
37
|
+
**Advanced capabilities:** Use registered functions, `outputSchema`, budgets, checkpoints, worktrees, retry/resume, CLI export, and `pipeline(...)` when the task requires them. They remain available without complicating the basic inline path. Workflow worktree scopes always use the explicit `withWorktree(name, callback)` form.
|
|
38
|
+
|
|
39
|
+
The main Pi agent writes these scripts on the fly for each task; extensions can add reusable functions and variables, and completed workflows can resume without rerunning completed work.
|
|
40
|
+
|
|
49
41
|
Learn more about roles, workflow contracts, and extension APIs in the documentation:
|
|
50
42
|
|
|
51
43
|
- [Workflow tool and invocation API](https://vekexasia.github.io/pi-extensible-workflows/developers.html#tool-api)
|
|
@@ -53,40 +45,12 @@ Learn more about roles, workflow contracts, and extension APIs in the documentat
|
|
|
53
45
|
- [Aggregate run budgets](https://vekexasia.github.io/pi-extensible-workflows/developers.html#budgets)
|
|
54
46
|
- [Workflow DSL and worktrees](https://vekexasia.github.io/pi-extensible-workflows/developers.html#dsl)
|
|
55
47
|
- [Extension authoring guide](https://vekexasia.github.io/pi-extensible-workflows/extensions.html)
|
|
48
|
+
- [Copy-paste extension template](https://github.com/vekexasia/pi-extensible-workflows/tree/main/examples/workflow-extension-template)
|
|
56
49
|
- [Run artifacts and lifecycle events](https://vekexasia.github.io/pi-extensible-workflows/developers.html#lifecycle)
|
|
57
50
|
- [Run inspection and recovery](https://vekexasia.github.io/pi-extensible-workflows/developers.html#operations)
|
|
58
51
|
- [Agent patterns and model selection](https://vekexasia.github.io/pi-extensible-workflows/agents.html#patterns)
|
|
59
52
|
- [Checkpoints](https://vekexasia.github.io/pi-extensible-workflows/agents.html#checkpoints)
|
|
60
53
|
|
|
61
|
-
## Configuration
|
|
62
|
-
|
|
63
|
-
Global workflow settings live at `~/.pi/agent/pi-extensible-workflows/settings.json` by default. Trusted projects can add partial overrides at `<project>/.pi/pi-extensible-workflows/settings.json`; precedence is defaults < global < trusted project < per-run options. Project collections replace global collections, so `{}` and empty arrays clear inherited aliases or resource exclusions. Untrusted project settings are ignored. See [global and project settings](https://vekexasia.github.io/pi-extensible-workflows/developers.html#settings) for the schema, trust gate, source reporting, and merge rules.
|
|
64
|
-
|
|
65
|
-
## CLI
|
|
66
|
-
|
|
67
|
-
```sh
|
|
68
|
-
npx pi-extensible-workflows doctor
|
|
69
|
-
npx pi-extensible-workflows doctor cleanup [--older-than-days <days>] [--yes]
|
|
70
|
-
npx pi-extensible-workflows inspect [session-id]
|
|
71
|
-
npx pi-extensible-workflows transcript <session-file>
|
|
72
|
-
npx pi-extensible-workflows run <workflow-name> [workflow arguments]
|
|
73
|
-
npx pi-extensible-workflows export <workflow-name> [--name <command>] [--output <path>] [--force]
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
`doctor` validates the installation and active Pi resources and remains read-only. `doctor cleanup` is an explicit, dry-run-first maintenance command: it previews terminal workflow runs older than 90 days across the current project's stored sessions and deletes them only with `--yes`; use `--older-than-days` to change the positive age threshold. It protects active, corrupt, leased, and dependency-linked runs. `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.
|
|
77
|
-
`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. Headless `run` and generated `export` launchers cannot execute workflows containing checkpoints; use the Pi workflow tool/UI path for checkpointed workflows.
|
|
78
|
-
Launch snapshots use identity version 5. Cold resume rejects older snapshots, including v4 snapshots created with the previous worktree or registered-function naming contracts, with `RESUME_INCOMPATIBLE`; relaunch the workflow instead.
|
|
79
|
-
|
|
80
|
-
## Development
|
|
81
|
-
|
|
82
|
-
```sh
|
|
83
|
-
npm ci
|
|
84
|
-
npm run check
|
|
85
|
-
npm run acceptance
|
|
86
|
-
npm pack --dry-run --json
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Model-backed evaluations are optional. See the [evaluation guide](https://vekexasia.github.io/pi-extensible-workflows/developers.html#evaluation).
|
|
90
54
|
|
|
91
55
|
## License
|
|
92
56
|
|
|
@@ -15,6 +15,7 @@ export interface AgentDefinition {
|
|
|
15
15
|
model?: string;
|
|
16
16
|
thinking?: ThinkingLevel;
|
|
17
17
|
tools?: readonly string[];
|
|
18
|
+
overrideSystemPrompt?: boolean;
|
|
18
19
|
disabledAgentResources?: AgentResourceExclusions;
|
|
19
20
|
}
|
|
20
21
|
export interface AgentProviderFailure {
|
|
@@ -56,6 +57,7 @@ export interface AgentExecutionRoot {
|
|
|
56
57
|
tools: ReadonlySet<string>;
|
|
57
58
|
agentDefinitions?: Readonly<Record<string, AgentDefinition>>;
|
|
58
59
|
agentDir?: string;
|
|
60
|
+
additionalSkillPaths?: readonly string[];
|
|
59
61
|
availableModels?: ReadonlySet<string>;
|
|
60
62
|
knownModels?: ReadonlySet<string>;
|
|
61
63
|
modelAliases?: Readonly<Record<string, string>>;
|
|
@@ -88,6 +90,7 @@ export interface AgentProgress {
|
|
|
88
90
|
accounting: AgentAccounting;
|
|
89
91
|
toolCalls: readonly AgentToolCallProgress[];
|
|
90
92
|
activity?: AgentActivity;
|
|
93
|
+
lastEventAt?: number;
|
|
91
94
|
persist: boolean;
|
|
92
95
|
}
|
|
93
96
|
export interface AgentAttempt {
|
|
@@ -117,6 +120,7 @@ export declare class WorkflowAgentExecutor {
|
|
|
117
120
|
model: ModelSpec;
|
|
118
121
|
requestedModel?: string;
|
|
119
122
|
tools: readonly string[];
|
|
123
|
+
systemPrompt?: string;
|
|
120
124
|
systemPromptAppend: string;
|
|
121
125
|
};
|
|
122
126
|
execute(task: string, options: AgentExecutionOptions, signal?: AbortSignal, customTools?: readonly ToolDefinition[], setSteer?: (handler: (message: string) => void | Promise<void>) => void, beforeRetry?: () => void): Promise<AgentExecutionResult>;
|
|
@@ -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";
|
|
@@ -42,6 +43,37 @@ function terminalProviderError(error) {
|
|
|
42
43
|
const candidate = value;
|
|
43
44
|
return typeof candidate.provider === "string" && typeof candidate.model === "string" && typeof candidate.error === "string" ? { provider: candidate.provider, model: candidate.model, error: candidate.error } : undefined;
|
|
44
45
|
}
|
|
46
|
+
const providerContinuationPrompt = "The provider error was transient. Continue the task from your current state.";
|
|
47
|
+
async function recoverTerminalProviderError(session, fallbackModel, label, recovery, continuePrompt) {
|
|
48
|
+
let continued = false;
|
|
49
|
+
for (;;) {
|
|
50
|
+
try {
|
|
51
|
+
throwIfTerminalAssistantError(session, fallbackModel);
|
|
52
|
+
return continued;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const typed = error instanceof WorkflowError ? error : new WorkflowError("AGENT_FAILED", error instanceof Error ? error.message : String(error));
|
|
56
|
+
const terminal = terminalProviderError(typed);
|
|
57
|
+
if (!terminal || !recovery)
|
|
58
|
+
throw error;
|
|
59
|
+
let action;
|
|
60
|
+
try {
|
|
61
|
+
action = await recovery({ label, ...terminal });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
Object.assign(typed, { providerRecoveryHandled: true, providerRecoveryFailed: true });
|
|
65
|
+
throw typed;
|
|
66
|
+
}
|
|
67
|
+
if (action === "retry") {
|
|
68
|
+
continued = true;
|
|
69
|
+
await continuePrompt();
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
Object.assign(typed, { providerRecoveryHandled: true, providerRecovery: action });
|
|
73
|
+
throw typed;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
45
77
|
function accounting(stats) {
|
|
46
78
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
47
79
|
}
|
|
@@ -51,8 +83,18 @@ function canonicalSourcePath(path) { try {
|
|
|
51
83
|
catch {
|
|
52
84
|
return resolve(path);
|
|
53
85
|
} }
|
|
86
|
+
const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
|
|
87
|
+
function workflowSystemPromptPath(cwd, agentDir, projectTrusted) {
|
|
88
|
+
const projectPath = join(cwd, ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md");
|
|
89
|
+
if (projectTrusted && existsSync(projectPath))
|
|
90
|
+
return projectPath;
|
|
91
|
+
const globalPaths = [join(homedir(), ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md"), join(agentDir, WORKFLOW_DIRECTORY, "SYSTEM.md")];
|
|
92
|
+
return globalPaths.find((path) => existsSync(path));
|
|
93
|
+
}
|
|
54
94
|
export async function createNativeAgentSession(input) {
|
|
55
95
|
const agentDir = input.agentDir ?? getAgentDir();
|
|
96
|
+
const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
|
|
97
|
+
const systemPromptOptions = input.systemPrompt !== undefined ? { systemPromptOverride: () => input.systemPrompt } : systemPromptSource !== undefined ? { systemPrompt: systemPromptSource } : {};
|
|
56
98
|
const manager = input.agentDir ? SessionManager.create(input.cwd, join(agentDir, "sessions")) : SessionManager.create(input.cwd);
|
|
57
99
|
manager.appendSessionInfo(input.sessionLabel);
|
|
58
100
|
const modelRuntime = await ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json") });
|
|
@@ -87,18 +129,19 @@ export async function createNativeAgentSession(input) {
|
|
|
87
129
|
noExtensions: true,
|
|
88
130
|
additionalExtensionPaths: extensionPaths,
|
|
89
131
|
noSkills: true,
|
|
90
|
-
additionalSkillPaths: skillPaths,
|
|
132
|
+
additionalSkillPaths: [...new Set([...skillPaths, ...(input.additionalSkillPaths ?? [])])],
|
|
91
133
|
...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
|
|
92
134
|
skillsOverride: (base) => {
|
|
93
135
|
const disabledSkills = updateSkillMatches(base.skills);
|
|
94
136
|
return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
|
|
95
137
|
},
|
|
96
138
|
...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
|
|
139
|
+
...systemPromptOptions,
|
|
97
140
|
});
|
|
98
141
|
await resourceLoader.reload();
|
|
99
142
|
}
|
|
100
|
-
else if (input.systemPromptAppend || input.extensionFactories?.length) {
|
|
101
|
-
resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
|
|
143
|
+
else if (input.systemPrompt !== undefined || systemPromptSource !== undefined || input.systemPromptAppend || input.extensionFactories?.length || input.additionalSkillPaths?.length) {
|
|
144
|
+
resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: [...input.additionalSkillPaths] } : {}), ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...systemPromptOptions, ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
|
|
102
145
|
await resourceLoader.reload();
|
|
103
146
|
}
|
|
104
147
|
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 });
|
|
@@ -142,7 +185,7 @@ async function prepareAgentSetup(root, createSession, task, options, resolved, c
|
|
|
142
185
|
const baseResourcePolicy = await root.agentResourcePolicy?.();
|
|
143
186
|
const roleExclusions = options.role ? root.agentDefinitions?.[options.role]?.disabledAgentResources : undefined;
|
|
144
187
|
const resourcePolicy = baseResourcePolicy && roleExclusions ? { ...baseResourcePolicy, effective: mergeAgentResourceExclusions(baseResourcePolicy.effective, roleExclusions) } : baseResourcePolicy;
|
|
145
|
-
const 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) };
|
|
188
|
+
const sessionInput = { cwd, model: { ...resolved.model }, tools: [...resolved.tools], sessionLabel: `${options.workflowName}:${options.label}:attempt-${String(attempt)}`, ...(root.agentDir ? { agentDir: root.agentDir } : {}), ...(root.additionalSkillPaths?.length ? { additionalSkillPaths: [...root.additionalSkillPaths] } : {}), ...(customTools.length ? { customTools: [...customTools] } : {}), ...(resultTool ? { resultTool } : {}), ...(resolved.systemPrompt !== undefined ? { systemPrompt: resolved.systemPrompt } : {}), systemPromptAppend: resolved.systemPromptAppend, ...(resourcePolicy ? { resourcePolicy } : {}), options: structuredClone(baselineOptions) };
|
|
146
189
|
const setup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, createSession };
|
|
147
190
|
const base = fallbackSetupContext(root, options, setupSignal);
|
|
148
191
|
const context = Object.freeze({ run: base.run, identity: base.identity, attempt, signal: setupSignal });
|
|
@@ -206,7 +249,8 @@ export class WorkflowAgentExecutor {
|
|
|
206
249
|
const availableModels = this.root.knownModels ?? this.root.availableModels ?? new Set([modelCapability(this.root.model)]);
|
|
207
250
|
if (!availableModels.has(modelCapability(model)))
|
|
208
251
|
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model${requestedModel ? ` ${requestedModel} resolved to ${modelCapability(model)}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
|
|
209
|
-
|
|
252
|
+
const overrideSystemPrompt = definition?.overrideSystemPrompt === true;
|
|
253
|
+
return { model, ...(alias && requestedModel ? { requestedModel } : {}), tools: [...requested], ...(overrideSystemPrompt ? { systemPrompt: definition.prompt ?? "" } : {}), systemPromptAppend: overrideSystemPrompt ? "" : definition?.prompt ?? "" };
|
|
210
254
|
}
|
|
211
255
|
async execute(task, options, signal, customTools = [], setSteer, beforeRetry) {
|
|
212
256
|
const executionSignal = signal ?? this.root.runContext?.signal;
|
|
@@ -272,6 +316,8 @@ export class WorkflowAgentExecutor {
|
|
|
272
316
|
} : undefined;
|
|
273
317
|
const toolCalls = new Map();
|
|
274
318
|
let activity;
|
|
319
|
+
let lastEventAt;
|
|
320
|
+
let lastReportedEventAt;
|
|
275
321
|
let progress = Promise.resolve();
|
|
276
322
|
let unsubscribe;
|
|
277
323
|
let systemPromptTurn = 0;
|
|
@@ -285,9 +331,17 @@ export class WorkflowAgentExecutor {
|
|
|
285
331
|
const report = (persist) => {
|
|
286
332
|
if (!session || !options.onProgress)
|
|
287
333
|
return;
|
|
288
|
-
const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], ...(activity ? { activity } : {}), persist };
|
|
334
|
+
const update = { accounting: accounting(session.getSessionStats()), toolCalls: [...toolCalls.values()], ...(activity ? { activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }), persist };
|
|
335
|
+
if (lastEventAt !== undefined)
|
|
336
|
+
lastReportedEventAt = lastEventAt;
|
|
289
337
|
progress = progress.then(() => options.onProgress?.(update)).then(() => undefined);
|
|
290
338
|
};
|
|
339
|
+
const reportTimestamp = () => {
|
|
340
|
+
if (lastEventAt !== undefined && lastReportedEventAt !== undefined && lastEventAt - lastReportedEventAt < 1000)
|
|
341
|
+
return;
|
|
342
|
+
report(false);
|
|
343
|
+
};
|
|
344
|
+
const activityChanged = (previous) => previous?.kind !== activity?.kind || previous?.text !== activity?.text;
|
|
291
345
|
try {
|
|
292
346
|
setupFailed = true;
|
|
293
347
|
const prepared = await prepareAgentSetup(this.root, this.createSession, task, options, resolved, cwd, attempt, executionSignal, customTools, resultTool);
|
|
@@ -303,7 +357,33 @@ export class WorkflowAgentExecutor {
|
|
|
303
357
|
const includeAttemptSetup = Boolean(this.root.agentSetupHooks?.length || setup.sessionInput.resourcePolicy);
|
|
304
358
|
await options.onAttempt?.({ attempt, sessionId: session.sessionId, sessionFile: requiredFile(session.sessionFile), ...(includeAttemptSetup ? { setup: setupSummary } : {}) });
|
|
305
359
|
const activeSession = session;
|
|
360
|
+
const sessionModel = setup.sessionInput.model;
|
|
361
|
+
const recoverTerminal = () => recoverTerminalProviderError(activeSession, sessionModel, options.label, options.providerErrorRecovery, async () => { try {
|
|
362
|
+
await promptWithProviderPause(activeSession, providerContinuationPrompt, remaining(options.timeoutMs, started), executionSignal, this.root.providerPause);
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
if (!hasSchemaResult())
|
|
366
|
+
throw error;
|
|
367
|
+
} });
|
|
368
|
+
const promptAndRecover = async (prompt) => {
|
|
369
|
+
let promptFailed = false;
|
|
370
|
+
let promptError;
|
|
371
|
+
try {
|
|
372
|
+
await promptWithProviderPause(activeSession, prompt, remaining(options.timeoutMs, started), executionSignal, this.root.providerPause);
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
promptFailed = true;
|
|
376
|
+
promptError = error;
|
|
377
|
+
}
|
|
378
|
+
const recovered = await recoverTerminal();
|
|
379
|
+
if (promptFailed && !hasSchemaResult() && !recovered)
|
|
380
|
+
throw promptError;
|
|
381
|
+
};
|
|
306
382
|
unsubscribe = activeSession.subscribe?.((event) => {
|
|
383
|
+
lastEventAt = Date.now();
|
|
384
|
+
let persist = false;
|
|
385
|
+
let shouldReport = false;
|
|
386
|
+
let removeToolCallId;
|
|
307
387
|
if (event.type === "agent_start" && session?.systemPrompt !== undefined) {
|
|
308
388
|
if (this.root.runStore) {
|
|
309
389
|
systemPromptTurn += 1;
|
|
@@ -323,10 +403,20 @@ export class WorkflowAgentExecutor {
|
|
|
323
403
|
}
|
|
324
404
|
}
|
|
325
405
|
activity = { kind: "text", text: "responding" };
|
|
326
|
-
|
|
406
|
+
shouldReport = true;
|
|
407
|
+
}
|
|
408
|
+
if (event.type === "message_update") {
|
|
409
|
+
const previousActivity = activity;
|
|
410
|
+
if (["thinking_start", "thinking_delta", "thinking_end"].includes(event.assistantMessageEvent.type))
|
|
411
|
+
activity = { kind: "reasoning", text: "reasoning" };
|
|
412
|
+
else if (["text_start", "text_delta", "text_end", "toolcall_start", "toolcall_delta", "toolcall_end"].includes(event.assistantMessageEvent.type))
|
|
413
|
+
activity = { kind: "text", text: "responding" };
|
|
414
|
+
shouldReport = activityChanged(previousActivity);
|
|
327
415
|
}
|
|
328
416
|
if (event.type === "message_end") {
|
|
417
|
+
const previousActivity = activity;
|
|
329
418
|
activity = undefined;
|
|
419
|
+
shouldReport = activityChanged(previousActivity);
|
|
330
420
|
if (event.message.role === "assistant") {
|
|
331
421
|
const needsMoreWork = hasToolCall(event.message);
|
|
332
422
|
const final = !needsMoreWork || (options.schema !== undefined && hasSchemaResult());
|
|
@@ -345,21 +435,32 @@ export class WorkflowAgentExecutor {
|
|
|
345
435
|
}
|
|
346
436
|
}
|
|
347
437
|
turnStarted = false;
|
|
348
|
-
|
|
438
|
+
persist = true;
|
|
349
439
|
}
|
|
350
440
|
}
|
|
351
441
|
if (event.type === "tool_execution_start") {
|
|
352
442
|
toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: "running" });
|
|
353
443
|
activity = { kind: "tool", text: event.toolName };
|
|
354
|
-
|
|
444
|
+
shouldReport = true;
|
|
445
|
+
}
|
|
446
|
+
if (event.type === "tool_execution_update") {
|
|
447
|
+
const previousActivity = activity;
|
|
448
|
+
activity = { kind: "tool", text: event.toolName };
|
|
449
|
+
shouldReport = activityChanged(previousActivity);
|
|
355
450
|
}
|
|
356
451
|
if (event.type === "tool_execution_end") {
|
|
357
452
|
toolCalls.set(event.toolCallId, { id: event.toolCallId, name: event.toolName, state: event.isError ? "failed" : "completed" });
|
|
358
453
|
if (activity?.kind === "tool" && activity.text === event.toolName)
|
|
359
454
|
activity = undefined;
|
|
360
|
-
|
|
361
|
-
|
|
455
|
+
shouldReport = true;
|
|
456
|
+
removeToolCallId = event.toolCallId;
|
|
362
457
|
}
|
|
458
|
+
if (shouldReport || persist)
|
|
459
|
+
report(persist);
|
|
460
|
+
else
|
|
461
|
+
reportTimestamp();
|
|
462
|
+
if (removeToolCallId)
|
|
463
|
+
toolCalls.delete(removeToolCallId);
|
|
363
464
|
});
|
|
364
465
|
report(false);
|
|
365
466
|
if (setSteer) {
|
|
@@ -372,14 +473,7 @@ export class WorkflowAgentExecutor {
|
|
|
372
473
|
const promptText = `${context}\n\nTask:\n${setup.prompt}${instruction ? `\n\n${instruction}` : ""}`;
|
|
373
474
|
options.budget?.beforeTurn();
|
|
374
475
|
turnStarted = true;
|
|
375
|
-
|
|
376
|
-
await promptWithProviderPause(session, promptText, remaining(options.timeoutMs, started), executionSignal, this.root.providerPause);
|
|
377
|
-
}
|
|
378
|
-
catch (error) {
|
|
379
|
-
if (!hasSchemaResult())
|
|
380
|
-
throw error;
|
|
381
|
-
}
|
|
382
|
-
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
476
|
+
await promptAndRecover(promptText);
|
|
383
477
|
{
|
|
384
478
|
const completedAccounting = accounting(session.getSessionStats());
|
|
385
479
|
options.budget?.afterTurn(completedAccounting, options.schema !== undefined ? hasSchemaResult() : !latestAssistantHasToolCall(session.messages));
|
|
@@ -389,38 +483,24 @@ export class WorkflowAgentExecutor {
|
|
|
389
483
|
throw budgetError;
|
|
390
484
|
if (options.schema) {
|
|
391
485
|
if (!hasSchemaResult()) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
turnStarted = false;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
catch (error) {
|
|
403
|
-
if (!hasSchemaResult())
|
|
404
|
-
throw error;
|
|
486
|
+
options.budget?.beforeTurn();
|
|
487
|
+
turnStarted = true;
|
|
488
|
+
await promptAndRecover("Submit the final result now by calling workflow_result exactly once. Do not return prose.");
|
|
489
|
+
{
|
|
490
|
+
const completedAccounting = accounting(session.getSessionStats());
|
|
491
|
+
options.budget?.afterTurn(completedAccounting, true);
|
|
492
|
+
turnStarted = false;
|
|
405
493
|
}
|
|
406
494
|
}
|
|
407
|
-
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
408
495
|
if (!hasSchemaResult()) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
turnStarted = false;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
catch (error) {
|
|
420
|
-
if (!hasSchemaResult())
|
|
421
|
-
throw error;
|
|
496
|
+
options.budget?.beforeTurn();
|
|
497
|
+
turnStarted = true;
|
|
498
|
+
await promptAndRecover("Your result was missing or invalid. Repair it by calling workflow_result exactly once with a schema-valid value.");
|
|
499
|
+
{
|
|
500
|
+
const completedAccounting = accounting(session.getSessionStats());
|
|
501
|
+
options.budget?.afterTurn(completedAccounting, true);
|
|
502
|
+
turnStarted = false;
|
|
422
503
|
}
|
|
423
|
-
throwIfTerminalAssistantError(session, setup.sessionInput.model);
|
|
424
504
|
}
|
|
425
505
|
if (schemaResult === undefined)
|
|
426
506
|
throw new WorkflowError("RESULT_INVALID", "Agent did not submit a valid workflow_result after one repair");
|
|
@@ -464,29 +544,35 @@ export class WorkflowAgentExecutor {
|
|
|
464
544
|
if (options.worktreeOwner && typed.code !== "WORKTREE_FAILED")
|
|
465
545
|
await this.root.runStore?.snapshotWorktree(options.worktreeOwner).catch(() => undefined);
|
|
466
546
|
const terminal = terminalProviderError(typed);
|
|
467
|
-
|
|
468
|
-
|
|
547
|
+
const recoveryState = typed;
|
|
548
|
+
let recovery = recoveryState.providerRecovery;
|
|
549
|
+
if (terminal && options.providerErrorRecovery && !recoveryState.providerRecoveryHandled) {
|
|
469
550
|
try {
|
|
470
551
|
recovery = await options.providerErrorRecovery({ label: options.label, ...terminal });
|
|
471
552
|
}
|
|
472
553
|
catch {
|
|
473
554
|
throw Object.assign(typed, { attempts });
|
|
474
555
|
}
|
|
475
|
-
if (recovery === "retry"
|
|
476
|
-
if (typeof recovery === "object") {
|
|
477
|
-
try {
|
|
478
|
-
const selected = resolveModelReference(recovery.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
|
|
479
|
-
recoveryModel = selected.thinking === undefined && resolved.model.thinking ? { ...selected, thinking: resolved.model.thinking } : selected;
|
|
480
|
-
}
|
|
481
|
-
catch {
|
|
482
|
-
throw Object.assign(typed, { attempts });
|
|
483
|
-
}
|
|
484
|
-
}
|
|
556
|
+
if (recovery === "retry") {
|
|
485
557
|
maxAttempts += 1;
|
|
486
558
|
beforeRetry?.();
|
|
487
559
|
continue;
|
|
488
560
|
}
|
|
489
561
|
}
|
|
562
|
+
if (recoveryState.providerRecoveryFailed || recovery === "abort")
|
|
563
|
+
throw Object.assign(typed, { attempts });
|
|
564
|
+
if (typeof recovery === "object" && typeof recovery.model === "string") {
|
|
565
|
+
try {
|
|
566
|
+
const selected = resolveModelReference(recovery.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
|
|
567
|
+
recoveryModel = selected.thinking === undefined && resolved.model.thinking ? { ...selected, thinking: resolved.model.thinking } : selected;
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
throw Object.assign(typed, { attempts });
|
|
571
|
+
}
|
|
572
|
+
maxAttempts += 1;
|
|
573
|
+
beforeRetry?.();
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
490
576
|
if (attempt === maxAttempts || setupFailed || typed.code === "CANCELLED" || typed.code === "WORKTREE_FAILED" || typed.code === "RESUME_INCOMPATIBLE")
|
|
491
577
|
throw Object.assign(typed, { attempts });
|
|
492
578
|
beforeRetry?.();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AgentDefinition, WorkflowCatalogFunction } from "./types.js";
|
|
2
|
+
export interface PortableWorkflowManifest {
|
|
3
|
+
format: "pi-extensible-workflows-bundle";
|
|
4
|
+
version: 1;
|
|
5
|
+
command: string;
|
|
6
|
+
workflow: {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
input: Record<string, unknown>;
|
|
10
|
+
output: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
runtime: {
|
|
13
|
+
pi: string;
|
|
14
|
+
"pi-extensible-workflows": string;
|
|
15
|
+
};
|
|
16
|
+
requirements: {
|
|
17
|
+
roles: readonly string[];
|
|
18
|
+
aliases: readonly string[];
|
|
19
|
+
tools: readonly string[];
|
|
20
|
+
commands: readonly string[];
|
|
21
|
+
environment: readonly string[];
|
|
22
|
+
};
|
|
23
|
+
aliasTargets?: Readonly<Record<string, string>>;
|
|
24
|
+
payload?: {
|
|
25
|
+
extensions?: readonly string[];
|
|
26
|
+
skills?: readonly string[];
|
|
27
|
+
static?: readonly string[];
|
|
28
|
+
dependencies?: readonly string[];
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface PortableWorkflowBundleResources {
|
|
32
|
+
extensions?: readonly string[];
|
|
33
|
+
skills?: readonly string[];
|
|
34
|
+
static?: readonly string[];
|
|
35
|
+
dependencies?: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
export interface PortableWorkflowBundleInput {
|
|
38
|
+
destination: string;
|
|
39
|
+
command: string;
|
|
40
|
+
workflow: WorkflowCatalogFunction;
|
|
41
|
+
functionSource: string;
|
|
42
|
+
piVersion?: string;
|
|
43
|
+
engineVersion?: string;
|
|
44
|
+
force?: boolean;
|
|
45
|
+
requirements?: Partial<PortableWorkflowManifest["requirements"]>;
|
|
46
|
+
aliasTargets?: Readonly<Record<string, string>>;
|
|
47
|
+
roles?: Readonly<Record<string, AgentDefinition>>;
|
|
48
|
+
resources?: PortableWorkflowBundleResources;
|
|
49
|
+
}
|
|
50
|
+
export declare function portableEngineVersion(): string;
|
|
51
|
+
export declare function portablePiVersion(): string;
|
|
52
|
+
export declare function normalizePortableFunctionSource(functionSource: string): string;
|
|
53
|
+
export declare function writePortableWorkflowBundle(input: PortableWorkflowBundleInput): PortableWorkflowManifest;
|