@marcoscale98/pi-extensible-workflows 5.17.0-fork.1 → 5.19.1-fork.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/dist/src/agent-execution.js +81 -39
- package/dist/src/background-widget.d.ts +5 -0
- package/dist/src/background-widget.js +1 -1
- package/dist/src/decoders.js +26 -0
- package/dist/src/execution.d.ts +4 -2
- package/dist/src/execution.js +43 -2
- package/dist/src/host-delivery.js +1 -0
- package/dist/src/host-recovery.d.ts +5 -2
- package/dist/src/host-recovery.js +3 -3
- package/dist/src/host-view.d.ts +5 -0
- package/dist/src/host-view.js +29 -5
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +190 -37
- package/dist/src/index.js +495 -93
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/registry.js +1 -1
- package/dist/src/roles.js +4 -3
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/settings.d.ts +13 -0
- package/dist/src/settings.js +27 -3
- package/dist/src/trajectory.d.ts +12 -2
- package/dist/src/trajectory.js +30 -8
- package/dist/src/types.d.ts +30 -2
- package/dist/src/types.js +1 -1
- package/dist/src/utils.d.ts +19 -0
- package/dist/src/utils.js +30 -0
- package/dist/src/validation.d.ts +6 -1
- package/dist/src/validation.js +21 -1
- package/dist/starter/index.js +0 -2
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +0 -2
- package/dist/subagents/index.js +267 -159
- package/dist/subagents/index.js.map +2 -2
- package/dist/subagents/src/contracts.d.ts +45 -29
- package/dist/subagents/src/contracts.js +18 -7
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +13 -6
- package/dist/subagents/src/manager.js +3 -3
- package/dist/subagents/src/view.d.ts +1 -0
- package/dist/subagents/src/view.js +13 -52
- package/dist/trajectory/assets/index.html +27 -5
- package/dist/trajectory/index.js +76 -28
- package/dist/trajectory/index.js.map +2 -2
- package/dist/trajectory/src/assets/index.html +27 -5
- package/dist/trajectory/src/index.js +8 -18
- package/dist/trajectory/src/server.js +4 -2
- package/dist/trajectory/src/server.js.map +2 -2
- package/package.json +6 -6
- package/skills/pi-extensible-workflows/SKILL.md +2 -0
- package/src/agent-execution.ts +77 -41
- package/src/background-widget.ts +1 -1
- package/src/decoders.ts +20 -0
- package/src/execution.ts +41 -4
- package/src/host-delivery.ts +1 -0
- package/src/host-recovery.ts +8 -7
- package/src/host-view.ts +23 -5
- package/src/host.ts +173 -35
- package/src/paths.ts +3 -0
- package/src/registry.ts +1 -1
- package/src/roles.ts +4 -3
- package/src/script-tools.ts +76 -0
- package/src/settings.ts +22 -4
- package/src/trajectory.ts +30 -11
- package/src/types.ts +11 -4
- package/src/utils.ts +28 -0
- package/src/validation.ts +19 -1
- package/starter/review-loop.ts +0 -2
- package/subagents/README.md +2 -2
- package/subagents/src/contracts.ts +18 -7
- package/subagents/src/index.ts +16 -6
- package/subagents/src/manager.ts +3 -3
- package/subagents/src/view.ts +13 -50
- package/trajectory/src/assets/index.html +27 -5
- package/trajectory/src/index.ts +6 -9
- package/trajectory/src/server.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
## Unreleased
|
|
3
3
|
|
|
4
|
+
## [5.19.1] - 2026-10-05
|
|
5
|
+
|
|
6
|
+
### New capabilities
|
|
7
|
+
|
|
8
|
+
- Trajectory shows workflow script `shell()` and `tools.<name>()` calls: CALLS and the `script` Gantt lane share status and colours, with phase and timing. Tool arguments and results are not recorded in call state or shown in Trajectory (including live state and HTML exports/shares); tool errors keep only the code. The replay journal retains tool results unchanged. Shell calls keep inputs and output, with environment override names, not values. Calls are recorded without a count cap; tool metadata writes are best-effort, and journaled calls without finish timing show completed with unknown duration.
|
|
9
|
+
|
|
10
|
+
## [5.19.0] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Breaking changes
|
|
13
|
+
|
|
14
|
+
- `tools` is a reserved workflow global: registering a workflow function named `tools` fails with `GLOBAL_COLLISION`. Rename the function.
|
|
15
|
+
|
|
16
|
+
### New capabilities
|
|
17
|
+
|
|
18
|
+
- Workflow and subagent tools declare an `outputSchema` and return `structuredContent`, so Pi `codemode` scripts receive objects instead of JSON text. For example, `await tools.workflow_status({ runId })` resolves to `{ runId, workflowName, state, agents, ... }`. A completed foreground `workflow`, `workflow_retry`, or `workflow_resume` call resolves with the full `value`, even when the model sees only a result descriptor. Inside workflow agents, the child agent tools `agent` and `get_subagent_result` do the same, and Herdr-bridged tools keep their `outputSchema`.
|
|
19
|
+
- Workflow scripts call the launching session's tools directly as `await tools.<name>(args)`, through Pi's `ctx.executeTool()`, with the names and results of Pi `codemode`. Permission handlers apply, results are journaled for resume and retry, the live workflow item shows calls in flight, and an unknown tool fails the launch with `UNKNOWN_TOOL`. Failed calls reject with the new `TOOL_FAILED` code.
|
|
20
|
+
- `workflow_status`, `workflow_catalog`, and `subagents_inspect` carry the `readOnlyHint` tool annotation, so permission extensions that read annotations can skip confirming them.
|
|
21
|
+
|
|
22
|
+
### Internal
|
|
23
|
+
|
|
24
|
+
- Build and test against Pi 1.0.0. `@piewf/cli` bundles Pi 1.0.0.
|
|
25
|
+
|
|
26
|
+
## [5.18.0] - 2026-09-30
|
|
27
|
+
|
|
28
|
+
### Breaking changes
|
|
29
|
+
|
|
30
|
+
- `pi-extensible-workflows` and `@piewf/herdr` require Pi 0.99.0 or later; their `@earendil-works/pi-coding-agent` peer dependency is now `>=0.99.0`. `@piewf/cli` bundles Pi 0.99.1.
|
|
31
|
+
|
|
32
|
+
### New capabilities
|
|
33
|
+
|
|
34
|
+
- Global model aliases appear in `/model` and `--model` as virtual models named `workflow/<alias>`, for example `workflow/cheap-model`. The alias target is read from settings on every request, and workflow agents and subagents that inherit the session model run on that target. The thinking level selected in `/model` replaces the one in the alias. Project and extension-provided aliases are not listed, and new aliases need `/reload`.
|
|
35
|
+
- Workflow agents and standalone subagents load Pi's built-in `codemode`, `tool-search`, and `mcp` extensions, so they can reach the MCP servers from `mcp.json` through `codemode` or `tool_search`. `-builtin:<name>` in Pi settings still disables them. Workflow extension selectors name them `builtin:codemode`, `builtin:tool-search`, and `builtin:mcp`; a selector list that starts with `!*` needs `builtin:*` or those names to keep them.
|
|
36
|
+
- The global `codemodeTools` workflow setting limits which workflow and subagent tools Pi `codemode` scripts may call: `"all"` (default), `"read-only"` (`workflow_status`, `workflow_catalog`, and `subagents_inspect`), or `"none"`. The model still calls every tool directly. Changes apply after `/reload`.
|
|
37
|
+
- The agent tool ceiling includes the parent session's tools that only codemode scripts call or `tool_search` loads, such as MCP tools with the default exposure. Role and call tool selectors still restrict them.
|
|
38
|
+
|
|
39
|
+
### Fixes
|
|
40
|
+
|
|
41
|
+
- Prompt inspection in `piewf doctor` and role inspection renders the system prompt again on Pi 0.99, including changes made by `before_agent_start` handlers.
|
|
42
|
+
- An agent that asks for a model of Pi's built-in `llama.cpp` provider fails with an error that explains why: Pi does not expose that extension to extension sessions. Declare the server as a provider in `models.json` instead.
|
|
43
|
+
|
|
44
|
+
### Internal
|
|
45
|
+
|
|
46
|
+
- Build and test against Pi 0.99.1. Package verification ignores three `brace-expansion` advisories that come from the `npm-shrinkwrap.json` of `@earendil-works/pi-coding-agent@0.99.1`.
|
|
47
|
+
|
|
48
|
+
## [5.17.2] - 2026-09-28
|
|
49
|
+
|
|
50
|
+
### Breaking changes
|
|
51
|
+
|
|
52
|
+
- `subagents_inspect({})` lists only the runs of the current Pi session, matching `/subagents` and Trajectory. Pass `scope: "all"` to list every stored run as before. Lookup by `id` still resolves runs from any session ([#305](https://github.com/vekexasia/pi-extensible-workflows/pull/305)).
|
|
53
|
+
|
|
54
|
+
### New capabilities
|
|
55
|
+
|
|
56
|
+
- `piewf doctor` inspects a role file before it is installed: a role ending in `.md`, such as `piewf doctor ./roles/reviewer.md`, is read as a file relative to the current working directory, while other values still name an installed role. A missing file reports `ROLE_FILE_NOT_FOUND` ([#303](https://github.com/vekexasia/pi-extensible-workflows/issues/303)).
|
|
57
|
+
|
|
58
|
+
### Fixes
|
|
59
|
+
|
|
60
|
+
- `piewf doctor` no longer fails because of roles that another role overrides: it still reports their invalid frontmatter, but checks tools, models, and settings only for the roles that apply. A role tool missing from doctor's headless tool list is now a `ROLE_TOOL_INACTIVE` warning, because doctor cannot see tools that extensions add when a session starts and the runtime skips unavailable role tools. Role inspection and workflow agent sessions now skip the workflow host extensions of every installed copy of the package, so a Pi package installed next to `@piewf/cli` no longer fails with `Global name is already registered: reviewLoop` ([#302](https://github.com/vekexasia/pi-extensible-workflows/issues/302)).
|
|
61
|
+
|
|
62
|
+
## [5.17.1] - 2026-09-26
|
|
63
|
+
|
|
64
|
+
### Fixes
|
|
65
|
+
|
|
66
|
+
- Running workflow agents and standalone subagents show a fixed status glyph before their names and keep the activity spinner visible after `[running]`, even without a current activity label.
|
|
67
|
+
|
|
68
|
+
### Internal
|
|
69
|
+
|
|
70
|
+
- Remove duplicated view and trajectory helpers, unused internal code, and tracked generated images. Preserve the public `pi-extensible-workflows/runtime` type exports.
|
|
71
|
+
|
|
4
72
|
## [5.17.0] - 2026-09-23
|
|
5
73
|
|
|
6
74
|
### New capabilities
|
|
@@ -5,9 +5,9 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { Type } from "@earendil-works/pi-ai";
|
|
7
7
|
import { Compile } from "typebox/compile";
|
|
8
|
-
import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { createAgentSession, createCodemodeExtension, createMcpExtension, createToolSearchExtension, DefaultPackageManager, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
;
|
|
10
|
-
import { SerialLane, assertModelThinking, byPriorityThenName, deepFreeze, errorText, jsonObject, jsonValue, mergeWorkflowExtensionSettings, object, resolveModelReference, resourcePatternHasMagic, unmatchedResourcePatterns } from "./utils.js";
|
|
10
|
+
import { SerialLane, assertModelThinking, byPriorityThenName, deepFreeze, errorText, jsonObject, jsonValue, mergeWorkflowExtensionSettings, object, physicalModel, resolveModelReference, resourcePatternHasMagic, unmatchedResourcePatterns } from "./utils.js";
|
|
11
11
|
import { SETTLED_AGENT_STATES, WorkflowError, isContextFileScope, zeroAccounting } from "./types.js";
|
|
12
12
|
import { createLiveSessionHandoff } from "./session-handoff.js";
|
|
13
13
|
import { createToolTimingExtension } from "./tool-timing.js";
|
|
@@ -15,7 +15,8 @@ import { isEmptyAbortedAssistant, normalizePiMessage, normalizePiSessionEvent, r
|
|
|
15
15
|
import { createPiRuntimeAgentRunner, isRuntimeAgentProviderError, normalizePiRuntimeError } from "./pi-runtime-runner.js";
|
|
16
16
|
import { defaultWorkflowResultSchema } from "./runtime/workflow-result.js";
|
|
17
17
|
import { validateAgentOptions, validateSchema } from "./validation.js";
|
|
18
|
-
import { canonicalPath } from "./paths.js";
|
|
18
|
+
import { canonicalPath, extensionIdentity } from "./paths.js";
|
|
19
|
+
import { isCorePackageName } from "./package-identity.js";
|
|
19
20
|
import { canonicalExtensionSelector, resolveRole } from "./roles.js";
|
|
20
21
|
const localToolTimingExtension = createToolTimingExtension();
|
|
21
22
|
function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
|
|
@@ -27,16 +28,27 @@ function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
|
|
|
27
28
|
function accounting(stats) {
|
|
28
29
|
return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
|
|
29
30
|
}
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
const WORKFLOW_HOST_ENTRY_PATHS = ["src/index.ts", "dist/src/index.js", "starter/index.ts", "dist/starter/index.js", "subagents/index.ts", "dist/subagents/index.js"];
|
|
32
|
+
function isWorkflowHostEntry(path) {
|
|
33
|
+
const entryPath = canonicalPath(path);
|
|
34
|
+
let packageRoot = dirname(entryPath);
|
|
35
|
+
while (packageRoot !== dirname(packageRoot)) {
|
|
36
|
+
const packageMetadataPath = join(packageRoot, "package.json");
|
|
37
|
+
if (existsSync(packageMetadataPath)) {
|
|
38
|
+
try {
|
|
39
|
+
const metadata = JSON.parse(readFileSync(packageMetadataPath, "utf8"));
|
|
40
|
+
if (typeof metadata !== "object" || metadata === null || !("name" in metadata) || !isCorePackageName(metadata.name))
|
|
41
|
+
return false;
|
|
42
|
+
return WORKFLOW_HOST_ENTRY_PATHS.some((entry) => canonicalPath(join(packageRoot, entry)) === entryPath);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
packageRoot = dirname(packageRoot);
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
40
52
|
const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
|
|
41
53
|
function workflowSystemPromptPath(cwd, agentDir, projectTrusted) {
|
|
42
54
|
const projectPath = join(cwd, ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md");
|
|
@@ -71,34 +83,41 @@ function loadPiPackageDirectory() {
|
|
|
71
83
|
}
|
|
72
84
|
}).catch((error) => ({ error: `could not load @earendil-works/pi-coding-agent package metadata: ${errorText(error)}` }));
|
|
73
85
|
}
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
const nativeCoreModules = new Map();
|
|
87
|
+
/** Pi does not export these core modules, so inspection loads them from the installed package. */
|
|
88
|
+
async function loadNativeCoreModule(file, label) {
|
|
76
89
|
const packageDirectory = await loadPiPackageDirectory();
|
|
77
|
-
let
|
|
90
|
+
let modulePath;
|
|
78
91
|
if (packageDirectory.directory) {
|
|
79
|
-
const candidate = resolve(packageDirectory.directory, "dist/core
|
|
92
|
+
const candidate = resolve(packageDirectory.directory, "dist/core", file);
|
|
80
93
|
if (existsSync(candidate))
|
|
81
|
-
|
|
94
|
+
modulePath = candidate;
|
|
82
95
|
}
|
|
83
|
-
|
|
96
|
+
modulePath ??= resolve(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "core", file);
|
|
84
97
|
const packageDirectoryError = packageDirectory.error ? ` Package directory lookup failed: ${packageDirectory.error}` : "";
|
|
85
|
-
|
|
98
|
+
let loaded = nativeCoreModules.get(file);
|
|
99
|
+
if (!loaded) {
|
|
100
|
+
loaded = import(modulePath).catch((error) => { throw new Error(`Could not load Pi ${label}.${packageDirectoryError}`, { cause: error }); });
|
|
101
|
+
nativeCoreModules.set(file, loaded);
|
|
102
|
+
}
|
|
103
|
+
return loaded;
|
|
86
104
|
}
|
|
87
105
|
async function expandPromptTemplateForInspection(text, templates) {
|
|
88
|
-
return (await
|
|
106
|
+
return (await loadNativeCoreModule("prompt-templates.js", "prompt templates")).expandPromptTemplate(text, templates);
|
|
107
|
+
}
|
|
108
|
+
async function buildSystemPromptForInspection(options) {
|
|
109
|
+
return (await loadNativeCoreModule("system-prompt.js", "system prompt builder")).buildSystemPrompt(options);
|
|
89
110
|
}
|
|
90
111
|
async function preparePiPrompt(native, text) {
|
|
91
112
|
const session = native;
|
|
92
113
|
const diagnostics = [];
|
|
93
114
|
const runner = session._extensionRunner;
|
|
94
|
-
const baseSystemPrompt =
|
|
115
|
+
const baseSystemPrompt = session.systemPrompt ?? "";
|
|
95
116
|
const baseOptions = session._baseSystemPromptOptions;
|
|
96
117
|
const expandSkillCommand = session._expandSkillCommand;
|
|
97
118
|
const templates = session.promptTemplates;
|
|
98
119
|
if (!runner)
|
|
99
120
|
diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: extension runner is missing", source: "Pi session" });
|
|
100
|
-
if (typeof session._baseSystemPrompt !== "string")
|
|
101
|
-
diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt is missing", source: "Pi session" });
|
|
102
121
|
if (baseOptions === undefined)
|
|
103
122
|
diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt options are missing", source: "Pi session" });
|
|
104
123
|
if (typeof expandSkillCommand !== "function")
|
|
@@ -129,9 +148,11 @@ async function preparePiPrompt(native, text) {
|
|
|
129
148
|
diagnostics.push({ type: "error", message: `Pi prompt template expansion is unavailable: ${errorText(error)}`, source: "Pi session" });
|
|
130
149
|
}
|
|
131
150
|
}
|
|
132
|
-
const result = !inputHandled && runner && baseOptions !== undefined ? await runner.emitBeforeAgentStart(expandedPrompt, undefined,
|
|
151
|
+
const result = !inputHandled && runner && baseOptions !== undefined ? await runner.emitBeforeAgentStart(expandedPrompt, undefined, baseOptions) : undefined;
|
|
133
152
|
const prepared = result;
|
|
134
|
-
|
|
153
|
+
const systemPromptOptions = prepared?.systemPromptOptions ?? baseOptions;
|
|
154
|
+
const systemPrompt = prepared?.systemPromptOptions === undefined ? baseSystemPrompt : await buildSystemPromptForInspection(prepared.systemPromptOptions);
|
|
155
|
+
return { prompt: text, expandedPrompt, systemPrompt, systemPromptOptions, messages: prepared?.messages ?? [], inputHandled, diagnostics };
|
|
135
156
|
}
|
|
136
157
|
finally {
|
|
137
158
|
unsubscribe?.();
|
|
@@ -175,6 +196,22 @@ export function flushExtensionProviders(resourceLoader, modelRuntime) {
|
|
|
175
196
|
runtime.pendingNativeProviderRegistrations = [];
|
|
176
197
|
return failures;
|
|
177
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* The CLI adds Pi's built-in extensions itself; SDK sessions only get them from these public
|
|
201
|
+
* factories (docs/sdk.md). As `builtin` entries they obey `-builtin:<name>` settings, and like the
|
|
202
|
+
* CLI's they are replaceable by an extension that registers the same tools or commands.
|
|
203
|
+
* NOTE: Pi does not export the built-in llama.cpp extension, so workflow agents cannot use models it
|
|
204
|
+
* provides. Declare the server as a provider in models.json instead.
|
|
205
|
+
*/
|
|
206
|
+
function builtinExtensions() {
|
|
207
|
+
return [
|
|
208
|
+
{ name: "codemode", factory: createCodemodeExtension(), replaceable: true, builtin: true },
|
|
209
|
+
{ name: "tool-search", factory: createToolSearchExtension(), replaceable: true, builtin: true },
|
|
210
|
+
{ name: "mcp", factory: createMcpExtension(), replaceable: true, builtin: true },
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
const BUILTIN_EXTENSION_PREFIX = "builtin:";
|
|
214
|
+
function isLlamaModel(model) { return model.provider === "llama.cpp"; }
|
|
178
215
|
async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
179
216
|
const agentDir = input.agentDir ?? getAgentDir();
|
|
180
217
|
const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
|
|
@@ -188,13 +225,16 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
|
188
225
|
const tools = [...new Set([...input.tools, ...customTools.map(({ name }) => name)])];
|
|
189
226
|
let settingsManager;
|
|
190
227
|
let resourceLoader;
|
|
228
|
+
const builtins = builtinExtensions();
|
|
229
|
+
const builtinNames = builtins.flatMap((extension) => typeof extension === "function" ? [] : [extension.name]);
|
|
230
|
+
const extensionFactories = [...builtins, ...(input.extensionFactories ?? [])];
|
|
191
231
|
const policy = input.resourcePolicy;
|
|
192
232
|
if (policy) {
|
|
193
233
|
settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: false });
|
|
194
234
|
settingsManager.setProjectTrusted(policy.projectTrusted);
|
|
195
|
-
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
|
|
235
|
+
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
|
|
196
236
|
const resolved = await packageManager.resolve();
|
|
197
|
-
const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) =>
|
|
237
|
+
const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => extensionIdentity(path)))];
|
|
198
238
|
const selectorSources = policy.selectorSources;
|
|
199
239
|
const selection = resolveRole(undefined, {
|
|
200
240
|
cwd: input.cwd,
|
|
@@ -202,7 +242,7 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
|
202
242
|
resources: { extensions: discoveredExtensions },
|
|
203
243
|
});
|
|
204
244
|
const selectedExtensions = selection.selectedExtensions ?? [];
|
|
205
|
-
const extensionPaths = selectedExtensions.filter((path) =>
|
|
245
|
+
const extensionPaths = selectedExtensions.filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path));
|
|
206
246
|
policy.selectedExtensions = selectedExtensions;
|
|
207
247
|
policy.unmatchedExtensions = selection.unmatchedExtensions ?? [];
|
|
208
248
|
const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
|
|
@@ -222,7 +262,7 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
|
222
262
|
additionalExtensionPaths: extensionPaths,
|
|
223
263
|
noSkills: true,
|
|
224
264
|
additionalSkillPaths: [...new Set([...skillPaths, ...(input.additionalSkillPaths ?? [])])],
|
|
225
|
-
|
|
265
|
+
extensionFactories,
|
|
226
266
|
...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}),
|
|
227
267
|
skillsOverride: (base) => {
|
|
228
268
|
const disabledSkills = updateSkillMatches(base.skills);
|
|
@@ -235,10 +275,10 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
|
235
275
|
}
|
|
236
276
|
else {
|
|
237
277
|
settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: true });
|
|
238
|
-
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
|
|
278
|
+
const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
|
|
239
279
|
const resolved = await packageManager.resolve();
|
|
240
|
-
const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled }) => enabled).map(({ path }) =>
|
|
241
|
-
resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, settingsManager, noExtensions: true, additionalExtensionPaths: extensionPaths, ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: [...input.additionalSkillPaths] } : {}),
|
|
280
|
+
const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled }) => enabled).map(({ path }) => extensionIdentity(path)).filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path)))];
|
|
281
|
+
resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, settingsManager, noExtensions: true, additionalExtensionPaths: extensionPaths, ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: [...input.additionalSkillPaths] } : {}), extensionFactories, ...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}), ...systemPromptOptions, ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
|
|
242
282
|
await resourceLoader.reload();
|
|
243
283
|
}
|
|
244
284
|
const providerFailures = flushExtensionProviders(resourceLoader, modelRuntime);
|
|
@@ -249,7 +289,7 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
|
249
289
|
// missing, so the failure travels with the complaint rather than being
|
|
250
290
|
// swallowed — otherwise a misconfigured gateway reads as a typo in a model
|
|
251
291
|
// name.
|
|
252
|
-
const because = providerFailures.length > 0 ? ` (provider registration failed — ${providerFailures.join("; ")})` : "";
|
|
292
|
+
const because = providerFailures.length > 0 ? ` (provider registration failed — ${providerFailures.join("; ")})` : isLlamaModel(input.model) ? " (Pi does not expose its built-in llama.cpp provider to extension sessions; declare the llama.cpp server as a provider in models.json and use that model instead)" : "";
|
|
253
293
|
throw new WorkflowError("UNKNOWN_MODEL", `Unknown model: ${input.model.provider}/${input.model.model}${because}`);
|
|
254
294
|
}
|
|
255
295
|
const effectiveSessionStartEvent = input.settings === undefined && sessionStartEvent === undefined ? undefined : { ...(sessionStartEvent ?? { type: "session_start", reason: "startup" }), settings: input.settings ?? Object.freeze({}) };
|
|
@@ -278,13 +318,13 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
|
|
|
278
318
|
await shutdown("quit").catch(() => undefined);
|
|
279
319
|
throw error;
|
|
280
320
|
}
|
|
281
|
-
const resourcePaths = { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) =>
|
|
321
|
+
const resourcePaths = { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) => extensionIdentity(resolvedPath)), skills: resourceLoader.getSkills().skills.map(({ filePath }) => canonicalPath(filePath)) };
|
|
282
322
|
const resourceInspection = () => {
|
|
283
323
|
const extensions = resourceLoader.getExtensions();
|
|
284
324
|
const skills = resourceLoader.getSkills();
|
|
285
325
|
const diagnostics = [...extensions.errors.map(({ path, error }) => ({ type: "error", message: error, source: path })), ...skills.diagnostics.map(({ type, path, message }) => ({ type, message, ...(path ? { source: path } : {}) }))];
|
|
286
326
|
const systemSource = systemPromptSource;
|
|
287
|
-
return { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) =>
|
|
327
|
+
return { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) => extensionIdentity(resolvedPath)), skills: skills.skills.map(({ name }) => name), diagnostics, ...(systemSource ? { systemPromptSource: systemSource } : resourceLoader.getSystemPrompt() !== undefined ? { systemPromptSource: "Pi resource loader" } : {}) };
|
|
288
328
|
};
|
|
289
329
|
const managedSession = Object.assign(session, {
|
|
290
330
|
getLeafId: () => manager.getLeafId(),
|
|
@@ -971,7 +1011,7 @@ export class WorkflowAgentExecutor {
|
|
|
971
1011
|
...(options.contextFiles === undefined ? {} : { contextFiles: options.contextFiles }),
|
|
972
1012
|
...(options.effectiveTools === undefined ? {} : { effectiveTools: options.effectiveTools }),
|
|
973
1013
|
});
|
|
974
|
-
return { model: resolved.model ?? this.root.model, ...(resolved.requestedModel === undefined ? {} : { requestedModel: resolved.requestedModel }), tools: resolved.tools ?? [], ...(resolved.overrideSystemPrompt ? { systemPrompt: resolved.prompt } : {}), systemPromptAppend: resolved.overrideSystemPrompt ? "" : resolved.prompt, ...(resolved.contextFiles === undefined ? {} : { contextFiles: resolved.contextFiles }) };
|
|
1014
|
+
return { model: physicalModel(resolved.model ?? this.root.model, this.root.modelAliases, this.root.knownModels, this.root.settingsPath), ...(resolved.requestedModel === undefined ? {} : { requestedModel: resolved.requestedModel }), tools: resolved.tools ?? [], ...(resolved.overrideSystemPrompt ? { systemPrompt: resolved.prompt } : {}), systemPromptAppend: resolved.overrideSystemPrompt ? "" : resolved.prompt, ...(resolved.contextFiles === undefined ? {} : { contextFiles: resolved.contextFiles }) };
|
|
975
1015
|
}
|
|
976
1016
|
async execute(task, options, signal, customTools = [], setSteer, beforeRetry) {
|
|
977
1017
|
const executionSignal = signal ?? this.root.runContext?.signal;
|
|
@@ -1439,6 +1479,7 @@ export class FairAgentScheduler {
|
|
|
1439
1479
|
const agentTool = defineTool({
|
|
1440
1480
|
name: "agent", label: "Child Agent", description: "Start a direct child agent",
|
|
1441
1481
|
parameters: Type.Object({ prompt: Type.String(), label: Type.String(), tools: Type.Optional(Type.Array(Type.String())), skills: Type.Optional(Type.Array(Type.String())), extensions: Type.Optional(Type.Array(Type.String())), model: Type.Optional(Type.String()), role: Type.Optional(Type.String()), contextFiles: Type.Optional(Type.Array(Type.String())), outputSchema: Type.Optional(Type.Record(Type.String(), Type.Unknown())), retries: Type.Optional(Type.Integer({ minimum: 0 })), timeoutMs: Type.Optional(Type.Union([Type.Integer({ minimum: 1 }), Type.Null()])) }, { additionalProperties: true }),
|
|
1482
|
+
outputSchema: Type.Object({ id: Type.String() }),
|
|
1442
1483
|
execute: async (_id, params) => {
|
|
1443
1484
|
if (!isChildAgentToolParams(params))
|
|
1444
1485
|
throw new WorkflowError("INVALID_METADATA", "Invalid child agent parameters");
|
|
@@ -1451,14 +1492,15 @@ export class FairAgentScheduler {
|
|
|
1451
1492
|
Reflect.deleteProperty(agentOptions, "prompt");
|
|
1452
1493
|
const options = { label: params.label, requestedLabel: params.label, cwd: parent.options.cwd, tools, agentOptions, ...(params.skills ? { skills: params.skills } : {}), ...(params.extensions ? { extensions: params.extensions } : {}), ...(params.model ? { model: params.model } : {}), ...(params.role ? { role: params.role } : {}), ...(params.contextFiles ? { contextFiles: params.contextFiles } : {}), ...(outputSchema === undefined ? {} : { schema: outputSchema }), ...(params.retries === undefined ? {} : { retries: params.retries }), ...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }) };
|
|
1453
1494
|
const child = this.spawn(parent.runId, params.prompt, options, parentId);
|
|
1454
|
-
return { content: [{ type: "text", text: JSON.stringify({ id: child.id }) }], details: { id: child.id } };
|
|
1495
|
+
return { content: [{ type: "text", text: JSON.stringify({ id: child.id }) }], details: { id: child.id }, structuredContent: { id: child.id } };
|
|
1455
1496
|
},
|
|
1456
1497
|
});
|
|
1457
1498
|
const resultTool = defineTool({
|
|
1458
1499
|
name: "get_subagent_result", label: "Child Result", description: "Wait for a direct child and return its result once; repeated retrieval fails with AGENT_RESULT_COLLECTED",
|
|
1459
1500
|
parameters: Type.Object({ id: Type.String() }),
|
|
1501
|
+
outputSchema: Type.Object({ id: Type.String(), ok: Type.Boolean(), value: Type.Optional(Type.Unknown()), error: Type.Optional(Type.Object({ code: Type.String(), message: Type.String() })) }),
|
|
1460
1502
|
execute: async (_id, params) => { const value = await this.result(parentId, params.id); if (!value.ok && value.error.code === "BUDGET_EXHAUSTED")
|
|
1461
|
-
throw new WorkflowError("BUDGET_EXHAUSTED", value.error.message); return { content: [{ type: "text", text: JSON.stringify(value) }], details: value }; }
|
|
1503
|
+
throw new WorkflowError("BUDGET_EXHAUSTED", value.error.message); return { content: [{ type: "text", text: JSON.stringify(value) }], details: value, structuredContent: value }; }
|
|
1462
1504
|
});
|
|
1463
1505
|
const steerTool = defineTool({
|
|
1464
1506
|
name: "steer_subagent", label: "Steer Child", description: "Steer a running direct child",
|
|
@@ -22,6 +22,11 @@ export type BackgroundWidgetAPI = Pick<ExtensionAPI, "appendEntry" | "on"> & {
|
|
|
22
22
|
registerEntryRenderer?: ExtensionAPI["registerEntryRenderer"];
|
|
23
23
|
registerShortcut?: ExtensionAPI["registerShortcut"];
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Repaint interval. Fast enough that the spinner reads as motion rather than a
|
|
27
|
+
* stutter; the underlying data is only re-read when an event says it changed.
|
|
28
|
+
*/
|
|
29
|
+
export declare const REPAINT_MS = 125;
|
|
25
30
|
/**
|
|
26
31
|
* How long an agent may go without an event before the widget says so.
|
|
27
32
|
*
|
|
@@ -25,7 +25,7 @@ const ENTRY_TYPE = "piewf-run-receipt";
|
|
|
25
25
|
* Repaint interval. Fast enough that the spinner reads as motion rather than a
|
|
26
26
|
* stutter; the underlying data is only re-read when an event says it changed.
|
|
27
27
|
*/
|
|
28
|
-
const REPAINT_MS = 125;
|
|
28
|
+
export const REPAINT_MS = 125;
|
|
29
29
|
/**
|
|
30
30
|
* Filesystem rescan interval. Shell-only activity has no event, but it does not
|
|
31
31
|
* need to be polled at the animation cadence.
|
package/dist/src/decoders.js
CHANGED
|
@@ -434,6 +434,28 @@ function decodeRunEvent(value) {
|
|
|
434
434
|
return undefined;
|
|
435
435
|
return { type: value.type, message: value.message, ...(timestamp === undefined ? {} : { timestamp }) };
|
|
436
436
|
}
|
|
437
|
+
function decodeScriptCall(value) {
|
|
438
|
+
if (!object(value) || value.kind !== "shell" && value.kind !== "tool" || typeof value.name !== "string" || typeof value.path !== "string" || !finiteNumber(value.startedAt))
|
|
439
|
+
return undefined;
|
|
440
|
+
const shell = value.kind === "shell";
|
|
441
|
+
if (shell && !jsonValue(value.input))
|
|
442
|
+
return undefined;
|
|
443
|
+
const inputBytes = shell ? optionalNumber(value.inputBytes) : undefined;
|
|
444
|
+
const phase = optionalString(value.phase);
|
|
445
|
+
const finishedAt = optionalNumber(value.finishedAt);
|
|
446
|
+
let error;
|
|
447
|
+
if (value.error !== undefined) {
|
|
448
|
+
if (shell)
|
|
449
|
+
error = decodeWorkflowError(value.error);
|
|
450
|
+
else if (object(value.error) && typeof value.error.code === "string")
|
|
451
|
+
error = { code: isWorkflowErrorCode(value.error.code) ? value.error.code : "INTERNAL_ERROR" };
|
|
452
|
+
if (!error)
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
if (inputBytes === INVALID_PERSISTED_VALUE || phase === INVALID_PERSISTED_VALUE || finishedAt === INVALID_PERSISTED_VALUE)
|
|
456
|
+
return undefined;
|
|
457
|
+
return { kind: value.kind, name: value.name, path: value.path, ...(shell && jsonValue(value.input) ? { input: value.input } : {}), ...(inputBytes === undefined ? {} : { inputBytes }), ...(phase === undefined ? {} : { phase }), startedAt: value.startedAt, ...(finishedAt === undefined ? {} : { finishedAt }), ...(error === undefined ? {} : { error }) };
|
|
458
|
+
}
|
|
437
459
|
function decodeDelivery(value) {
|
|
438
460
|
if (!object(value))
|
|
439
461
|
return undefined;
|
|
@@ -467,6 +489,9 @@ export function decodePersistedRun(value, allowLegacyAgentSessions = false) {
|
|
|
467
489
|
const budgetEvents = value.budgetEvents === undefined ? undefined : decodeArray(value.budgetEvents, decodeBudgetEvent);
|
|
468
490
|
const events = value.events === undefined ? undefined : decodeArray(value.events, decodeRunEvent);
|
|
469
491
|
const delivery = value.delivery === undefined ? undefined : decodeDelivery(value.delivery);
|
|
492
|
+
const scriptCalls = value.scriptCalls === undefined ? undefined : decodeArray(value.scriptCalls, decodeScriptCall);
|
|
493
|
+
if (value.scriptCalls !== undefined && !scriptCalls)
|
|
494
|
+
return undefined;
|
|
470
495
|
if (!agentSessions || !agents || parentRunId === INVALID_PERSISTED_VALUE || value.retry !== undefined && !retry || phase === INVALID_PERSISTED_VALUE || value.phaseHistory !== undefined && !phaseHistory || phaseHistoryIndex === INVALID_PERSISTED_VALUE || activeShells === INVALID_PERSISTED_VALUE || activeShellStartedAt === INVALID_PERSISTED_VALUE || value.activeShellsByPhase !== undefined && !activeShellsByPhase || value.error !== undefined && !error || failedAt === INVALID_PERSISTED_VALUE || value.budget !== undefined && !budget || budgetVersion === INVALID_PERSISTED_VALUE || value.budgetVersion !== undefined && !integer(budgetVersion) || value.usage !== undefined && !usage || value.budgetEvents !== undefined && !budgetEvents || value.events !== undefined && !events || value.delivery !== undefined && !delivery)
|
|
471
496
|
return undefined;
|
|
472
497
|
if (value.agentSessions === undefined && !allowLegacyAgentSessions)
|
|
@@ -487,6 +512,7 @@ export function decodePersistedRun(value, allowLegacyAgentSessions = false) {
|
|
|
487
512
|
...(activeShells === undefined ? {} : { activeShells }),
|
|
488
513
|
...(activeShellStartedAt === undefined ? {} : { activeShellStartedAt }),
|
|
489
514
|
...(activeShellsByPhase === undefined ? {} : { activeShellsByPhase }),
|
|
515
|
+
...(scriptCalls === undefined ? {} : { scriptCalls }),
|
|
490
516
|
...(error === undefined ? {} : { error }),
|
|
491
517
|
...(failedAt === undefined ? {} : { failedAt }),
|
|
492
518
|
...(budget === undefined ? {} : { budget }),
|
package/dist/src/execution.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { RunStore } from "./persistence.js";
|
|
2
2
|
import type { AgentAttempt } from "./agent-execution.js";
|
|
3
|
-
import type { AgentIdentity, JsonValue, ShellIdentity, ShellOptions, ShellResult, WorkflowBridge, WorkflowExecution } from "./types.js";
|
|
3
|
+
import type { AgentIdentity, JsonValue, ShellIdentity, ShellOptions, ShellResult, ToolIdentity, WorkflowBridge, WorkflowExecution } from "./types.js";
|
|
4
4
|
export declare const RPC_LIMIT_BYTES: number;
|
|
5
5
|
export declare const HEARTBEAT_TIMEOUT_MS = 5000;
|
|
6
6
|
export declare function encoded(value: JsonValue): string;
|
|
7
7
|
export declare function encoded(value: unknown): asserts value is JsonValue;
|
|
8
|
+
/** The journal path of a script tool call; the arguments' digest keeps calls with different arguments apart whatever order they start in. */
|
|
9
|
+
export declare function toolIdentityPath(identifier: string, args: JsonValue, identity: ToolIdentity): string;
|
|
8
10
|
export declare function agentHandleTurnPath(handle: string, turn: number): string;
|
|
9
11
|
export declare function agentIdentityPath(identity: AgentIdentity): string;
|
|
10
12
|
export declare function shellIdentityPath(identity: ShellIdentity): string;
|
|
@@ -16,4 +18,4 @@ export declare function executeShellCommand(command: string, options: ShellOptio
|
|
|
16
18
|
export declare function runWorkflow(script: string, args?: JsonValue, bridge?: WorkflowBridge, signal?: AbortSignal): WorkflowExecution;
|
|
17
19
|
export declare function persistActiveAgentAttempt(store: RunStore, id: string, active: AgentAttempt): Promise<void>;
|
|
18
20
|
export declare function persistAgentAttempts(store: RunStore, id: string, attempts: readonly AgentAttempt[]): Promise<void>;
|
|
19
|
-
export type { AgentIdentity, FunctionIdentity, ShellIdentity, WorkflowBridge, WorkflowExecution } from "./types.js";
|
|
21
|
+
export type { AgentIdentity, FunctionIdentity, ShellIdentity, ToolIdentity, WorkflowBridge, WorkflowExecution } from "./types.js";
|
package/dist/src/execution.js
CHANGED
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { StringDecoder } from "node:string_decoder";
|
|
6
6
|
import { structuralPath as operationPath } from "./persistence.js";
|
|
7
7
|
import { WorkflowError, roleNameOf, sumAccounting, zeroAccounting } from "./types.js";
|
|
8
|
-
import { asWorkflowError, errorText, fail, isWorkflowAuthored, isWorkflowErrorCode, jsonValue, markWorkflowAuthored, object, positiveInteger } from "./utils.js";
|
|
8
|
+
import { asWorkflowError, errorText, fail, isWorkflowAuthored, isWorkflowErrorCode, jsonDigest, jsonValue, markWorkflowAuthored, object, positiveInteger } from "./utils.js";
|
|
9
9
|
import { instrumentWorkflow, validateAgentOptions, validateShellCommand, validateShellOptions } from "./validation.js";
|
|
10
10
|
export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
11
11
|
export const HEARTBEAT_TIMEOUT_MS = 5000;
|
|
@@ -268,6 +268,30 @@ const functions = Object.freeze(Object.fromEntries(Object.entries(config.functio
|
|
|
268
268
|
Object.defineProperty(result, "toJSON", { value() { throw workError("INVALID_METADATA", "Workflow function result is a Promise; await it before serialization"); } });
|
|
269
269
|
return result;
|
|
270
270
|
}])));
|
|
271
|
+
// tools.<id>(args) runs a Pi session tool through the host. Any identifier resolves, so a resumed run
|
|
272
|
+
// replays journaled calls even when the tool is gone; the host rejects identifiers it cannot call.
|
|
273
|
+
// A call is the n-th call with the same arguments in its scope, so replay does not depend on the
|
|
274
|
+
// order in which concurrent calls start.
|
|
275
|
+
const NOT_TOOLS = new Set(["then", "toJSON", "toString", "valueOf", "constructor"]);
|
|
276
|
+
const sortedJson = value => Array.isArray(value) ? value.map(sortedJson) : value && typeof value === "object" ? Object.fromEntries(Object.keys(value).sort().map(key => [key, sortedJson(value[key])])) : value;
|
|
277
|
+
const toolOccurrences = new Map();
|
|
278
|
+
const toolCall = id => (...values) => {
|
|
279
|
+
if (worktreeOwners.getStore()) throw workError("INVALID_METADATA", "tools." + id + " is unavailable inside withWorktree: session tools resolve paths against the session working directory");
|
|
280
|
+
if (values.length > 1 || values[0] !== undefined && (!values[0] || typeof values[0] !== "object" || Array.isArray(values[0]))) throw workError("INVALID_METADATA", "tools." + id + " takes one arguments object");
|
|
281
|
+
const args = values[0] === undefined ? {} : values[0];
|
|
282
|
+
const structuralPath = inheritedAgentPath.getStore() || [];
|
|
283
|
+
const key = JSON.stringify([structuralPath, id, sortedJson(args)]);
|
|
284
|
+
const occurrence = (toolOccurrences.get(key) || 0) + 1;
|
|
285
|
+
toolOccurrences.set(key, occurrence);
|
|
286
|
+
return guardedResult(rpc("tool", [id, args, { structuralPath: [...structuralPath], occurrence }]), "tool");
|
|
287
|
+
};
|
|
288
|
+
const tools = new Proxy(Object.create(null), {
|
|
289
|
+
get: (_target, key) => typeof key === "string" && !NOT_TOOLS.has(key) ? toolCall(key) : undefined,
|
|
290
|
+
// Probing would branch on the session that resumed the run; the launch already checks named tools.
|
|
291
|
+
has: () => { throw workError("INVALID_METADATA", "Workflow scripts cannot probe tools with 'in'; call tools.<name>(...) directly, and the launch checks that the tool exists"); },
|
|
292
|
+
set: () => false,
|
|
293
|
+
defineProperty: () => false,
|
|
294
|
+
});
|
|
271
295
|
const recordEntries = (value, kind) => {
|
|
272
296
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw workError("INVALID_METADATA", kind + " must be a record");
|
|
273
297
|
return Object.entries(value);
|
|
@@ -328,7 +352,7 @@ const pipeline = async (operationName, items, stages) => {
|
|
|
328
352
|
return Object.fromEntries(results.map(result => [result.name, result.value]));
|
|
329
353
|
};
|
|
330
354
|
const safeMath = Object.fromEntries(Object.getOwnPropertyNames(Math).filter(name => name !== "random").map(name => [name, Math[name]]));
|
|
331
|
-
const sandbox = { agent, shell, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
|
|
355
|
+
const sandbox = { agent, shell, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, tools, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
|
|
332
356
|
for (const [name, fn] of Object.entries(functions)) Object.defineProperty(sandbox, name, { value: fn, writable: false, configurable: false });
|
|
333
357
|
for (const name of ["Date","eval","Function","WebAssembly","process","require","module","exports","console","fetch","XMLHttpRequest","WebSocket","performance","crypto","setTimeout","setInterval","setImmediate","queueMicrotask","Intl","SharedArrayBuffer","Atomics"]) sandbox[name] = undefined;
|
|
334
358
|
const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
|
|
@@ -378,6 +402,15 @@ function readShellIdentity(value) {
|
|
|
378
402
|
const identity = readAgentIdentity(value);
|
|
379
403
|
return { structuralPath: identity.structuralPath, callSite: identity.callSite, occurrence: identity.occurrence, ...(identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {}) };
|
|
380
404
|
}
|
|
405
|
+
function readToolIdentity(value) {
|
|
406
|
+
if (!object(value) || !Array.isArray(value.structuralPath) || !value.structuralPath.every((part) => typeof part === "string" && Boolean(part.trim())) || !positiveInteger(value.occurrence))
|
|
407
|
+
fail("INTERNAL_ERROR", "Invalid workflow tool identity");
|
|
408
|
+
return { structuralPath: [...value.structuralPath], occurrence: value.occurrence };
|
|
409
|
+
}
|
|
410
|
+
/** The journal path of a script tool call; the arguments' digest keeps calls with different arguments apart whatever order they start in. */
|
|
411
|
+
export function toolIdentityPath(identifier, args, identity) {
|
|
412
|
+
return operationPath("tool", ...identity.structuralPath, identifier, `args:${jsonDigest(args)}`, String(identity.occurrence));
|
|
413
|
+
}
|
|
381
414
|
export function agentHandleTurnPath(handle, turn) {
|
|
382
415
|
return operationPath("agent", "handle", handle, `turn:${String(turn)}`);
|
|
383
416
|
}
|
|
@@ -681,6 +714,14 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
|
681
714
|
value = branded({ name, ok: false, failedAt: name, error: workerErrorShape(typed) });
|
|
682
715
|
}
|
|
683
716
|
}
|
|
717
|
+
else if (method === "tool") {
|
|
718
|
+
if (typeof values[0] !== "string" || !object(values[1]))
|
|
719
|
+
fail("INTERNAL_ERROR", "tool requires an identifier and object arguments");
|
|
720
|
+
const identity = readToolIdentity(values[2]);
|
|
721
|
+
if (!bridge.tool)
|
|
722
|
+
fail("UNKNOWN_TOOL", `tools.${values[0]} is unavailable: this run has no session tools`);
|
|
723
|
+
value = await bridge.tool(values[0], values[1], controller.signal, identity);
|
|
724
|
+
}
|
|
684
725
|
else if (method === "worktree") {
|
|
685
726
|
if (!bridge.worktree || typeof values[0] !== "string" || !values[0])
|
|
686
727
|
fail("INTERNAL_ERROR", "worktree requires an active host bridge and scope");
|
|
@@ -108,6 +108,7 @@ const WORKFLOW_ERROR_PROSE = {
|
|
|
108
108
|
RUN_NOT_FOUND: (detail) => /^Unknown workflow run\b/.test(detail) ? "The workflow run was not found." : `The workflow run was not found: ${detail}.`,
|
|
109
109
|
RPC_LIMIT_EXCEEDED: (detail) => `The workflow communication data exceeded its size limit: ${detail}.`,
|
|
110
110
|
SHELL_FAILED: (detail) => `The workflow shell command failed: ${detail}.`,
|
|
111
|
+
TOOL_FAILED: (detail) => `The workflow tool call failed: ${detail}.`,
|
|
111
112
|
AGENT_TIMEOUT: (detail) => `The workflow agent timed out: ${detail}.`,
|
|
112
113
|
AGENT_FAILED: (detail) => `The workflow agent failed: ${detail}.`,
|
|
113
114
|
AGENT_RESULT_COLLECTED: (detail) => `The nested agent result was already collected: ${detail}.`,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
-
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type ExtensionAPI, type ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { FairAgentScheduler, WorkflowAgentExecutor, type AgentDefinition, type AgentExecutionRoot, type AgentProviderFailure, type AgentProviderRecovery } from "./agent-execution.js";
|
|
4
4
|
import { RunStore, type PersistedRun } from "./persistence.js";
|
|
5
|
-
import { WorkflowError, type AgentIdentity, type AgentResourcePolicy, type BudgetApprovalRequest, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowMetadata, type WorkflowWorktreeReference } from "./types.js";
|
|
5
|
+
import { WorkflowError, type AgentIdentity, type AgentResourcePolicy, type BudgetApprovalRequest, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type ToolIdentity, type WorkflowMetadata, type WorkflowWorktreeReference } from "./types.js";
|
|
6
6
|
import { type WorkflowRegistryApi } from "./registry.js";
|
|
7
7
|
import { RunLifecycle, WorkflowEventPublisher, type WorkflowRunRecord, type WorkflowToolUpdate } from "./host-runtime.js";
|
|
8
8
|
import { type CompletionDeliveryContext, type CompletionDeliveryResult } from "./host-delivery.js";
|
|
@@ -21,6 +21,7 @@ export type WorkflowRecoveryContext = {
|
|
|
21
21
|
} | undefined;
|
|
22
22
|
modelRegistry: ModelRegistryCapability | undefined;
|
|
23
23
|
deliveryContext: CompletionDeliveryContext;
|
|
24
|
+
toolContext?: ExtensionToolContext | undefined;
|
|
24
25
|
signal?: AbortSignal;
|
|
25
26
|
resolvedAliases?: Readonly<Record<string, string>>;
|
|
26
27
|
blockedAliases?: ReadonlySet<string>;
|
|
@@ -70,6 +71,7 @@ export type WorkflowRecoveryDependencies = {
|
|
|
70
71
|
};
|
|
71
72
|
workflowAgentHandler: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string, captureRole?: (role: string, model: ModelSpec) => Promise<void>) => (prompt: string, options: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: AgentIdentity) => Promise<JsonValue>;
|
|
72
73
|
shellForRun: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, command: string, options: ShellOptions, signal: AbortSignal, identity: ShellIdentity) => Promise<ShellResult>;
|
|
74
|
+
toolForRun: (store: RunStore, lifecycle: RunLifecycle, context: ExtensionToolContext | undefined, identifier: string, args: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: ToolIdentity) => Promise<JsonValue>;
|
|
73
75
|
resolveWorktree: (store: RunStore, metadata: WorkflowMetadata, owner: string) => Promise<Readonly<WorkflowWorktreeReference>>;
|
|
74
76
|
checkpointBridge: (runId: string, store: RunStore, metadata: WorkflowMetadata, foreground: boolean, ui?: {
|
|
75
77
|
select?: (prompt: string, options: string[]) => Promise<string | undefined>;
|
|
@@ -104,6 +106,7 @@ export declare function createWorkflowRecovery(deps: WorkflowRecoveryDependencie
|
|
|
104
106
|
} | undefined;
|
|
105
107
|
modelRegistry: WorkflowRecoveryContext["modelRegistry"];
|
|
106
108
|
deliveryContext: CompletionDeliveryContext;
|
|
109
|
+
toolContext?: ExtensionToolContext | undefined;
|
|
107
110
|
signal?: AbortSignal | undefined;
|
|
108
111
|
resolvedAliases?: Readonly<Record<string, string>>;
|
|
109
112
|
blockedAliases?: ReadonlySet<string>;
|
|
@@ -46,7 +46,7 @@ function assertExpectedWorkflowState(expectedState, actualState) {
|
|
|
46
46
|
}
|
|
47
47
|
export function persistedFailure(run, error) { const failedAt = workflowFailedAt(error); return { ...run, error: { code: error.code, message: error.message, ...(failedAt ? { failedAt } : {}) }, ...(failedAt ? { failedAt } : {}) }; }
|
|
48
48
|
export function createWorkflowRecovery(deps) {
|
|
49
|
-
const { pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver, deliverTerminal, workflowToolUpdate, registry, modelSpec } = deps;
|
|
49
|
+
const { pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, toolForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver, deliverTerminal, workflowToolUpdate, registry, modelSpec } = deps;
|
|
50
50
|
const coordinateRunMutation = deps.coordinateRunMutation ?? ((task) => task());
|
|
51
51
|
const budgetDecisionDelivery = (metadata, request) => `Workflow ${metadata.name} budget adjustment ${request.proposalId} for run ${request.runId} requires approval. Consumed usage: ${JSON.stringify(request.consumed)}. Previous limits: ${JSON.stringify(request.previous)}. Proposed limits: ${JSON.stringify(request.proposed)}. Respond with workflow_respond using proposalId ${request.proposalId}.`;
|
|
52
52
|
const appendBudgetDecisionEvent = async (run, request, type) => {
|
|
@@ -119,7 +119,7 @@ export function createWorkflowRecovery(deps) {
|
|
|
119
119
|
run.executor.setRunContext(runContext);
|
|
120
120
|
await scheduler.cancelRun(run.store.runId);
|
|
121
121
|
await run.lifecycle.resume();
|
|
122
|
-
const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId, roles.capture), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.store, run.lifecycle, run.metadata.name) }, run.store, runContext, registry, snapshot.settings.extensionSettings), controller.signal);
|
|
122
|
+
const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ tool: (identifier, args, signal, identity) => toolForRun(run.store, run.lifecycle, context?.toolContext, identifier, args, signal, identity), shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId, roles.capture), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.store, run.lifecycle, run.metadata.name) }, run.store, runContext, registry, snapshot.settings.extensionSettings), controller.signal);
|
|
123
123
|
run.execution = execution;
|
|
124
124
|
const completion = execution.result.then(async (value) => {
|
|
125
125
|
await scheduler.flush(run.store.runId);
|
|
@@ -333,7 +333,7 @@ export function createWorkflowRecovery(deps) {
|
|
|
333
333
|
await eventPublisher.runStarted(prepared.childRun.store, prepared.childRun.metadata);
|
|
334
334
|
const { hasUI, ui } = recoveryUi(context);
|
|
335
335
|
const recoveryContext = resumeHostContext(context);
|
|
336
|
-
const completed = await coldResumeRun(prepared.childRun, hasUI, ui, projectTrusted(context), { model: prepared.hostModel, modelRegistry: prepared.modelRegistry, deliveryContext: recoveryContext.deliveryContext, resolvedAliases: prepared.currentAliases, blockedAliases: prepared.blockedAliases, blockedAliasTargets: prepared.blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
|
|
336
|
+
const completed = await coldResumeRun(prepared.childRun, hasUI, ui, projectTrusted(context), { model: prepared.hostModel, modelRegistry: prepared.modelRegistry, deliveryContext: recoveryContext.deliveryContext, toolContext: recoveryContext.toolContext, resolvedAliases: prepared.currentAliases, blockedAliases: prepared.blockedAliases, blockedAliasTargets: prepared.blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
|
|
337
337
|
const completion = runs.get(prepared.childRun.store.runId)?.completion;
|
|
338
338
|
if (completion) {
|
|
339
339
|
childStarted = true;
|
package/dist/src/host-view.d.ts
CHANGED
|
@@ -84,6 +84,11 @@ export declare function navigatorRunLabels(entries: readonly {
|
|
|
84
84
|
export declare function agentBreadcrumbParts(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath?: boolean): string[];
|
|
85
85
|
export declare function agentBreadcrumb(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath?: boolean): string;
|
|
86
86
|
export declare function formatStalledDuration(durationMs: number): string;
|
|
87
|
+
export declare function stalledDuration(agent: Pick<AgentDetailPresentation, "state" | "lastEventAt">, now: number): number | undefined;
|
|
88
|
+
export declare function agentActivityLabel(agent: {
|
|
89
|
+
readonly activity?: AgentRecord["activity"];
|
|
90
|
+
readonly toolCalls?: AgentRecord["toolCalls"];
|
|
91
|
+
}): string;
|
|
87
92
|
export interface AgentDetailPresentation {
|
|
88
93
|
readonly state: string;
|
|
89
94
|
readonly activity?: AgentRecord["activity"];
|