@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.
Files changed (80) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/src/agent-execution.js +81 -39
  3. package/dist/src/background-widget.d.ts +5 -0
  4. package/dist/src/background-widget.js +1 -1
  5. package/dist/src/decoders.js +26 -0
  6. package/dist/src/execution.d.ts +4 -2
  7. package/dist/src/execution.js +43 -2
  8. package/dist/src/host-delivery.js +1 -0
  9. package/dist/src/host-recovery.d.ts +5 -2
  10. package/dist/src/host-recovery.js +3 -3
  11. package/dist/src/host-view.d.ts +5 -0
  12. package/dist/src/host-view.js +29 -5
  13. package/dist/src/host.d.ts +2 -2
  14. package/dist/src/host.js +190 -37
  15. package/dist/src/index.js +495 -93
  16. package/dist/src/index.js.map +3 -3
  17. package/dist/src/paths.d.ts +2 -0
  18. package/dist/src/paths.js +2 -0
  19. package/dist/src/registry.js +1 -1
  20. package/dist/src/roles.js +4 -3
  21. package/dist/src/script-tools.d.ts +27 -0
  22. package/dist/src/script-tools.js +87 -0
  23. package/dist/src/settings.d.ts +13 -0
  24. package/dist/src/settings.js +27 -3
  25. package/dist/src/trajectory.d.ts +12 -2
  26. package/dist/src/trajectory.js +30 -8
  27. package/dist/src/types.d.ts +30 -2
  28. package/dist/src/types.js +1 -1
  29. package/dist/src/utils.d.ts +19 -0
  30. package/dist/src/utils.js +30 -0
  31. package/dist/src/validation.d.ts +6 -1
  32. package/dist/src/validation.js +21 -1
  33. package/dist/starter/index.js +0 -2
  34. package/dist/starter/index.js.map +1 -1
  35. package/dist/starter/review-loop.d.ts +0 -13
  36. package/dist/starter/review-loop.js +0 -2
  37. package/dist/subagents/index.js +267 -159
  38. package/dist/subagents/index.js.map +2 -2
  39. package/dist/subagents/src/contracts.d.ts +45 -29
  40. package/dist/subagents/src/contracts.js +18 -7
  41. package/dist/subagents/src/index.d.ts +1 -1
  42. package/dist/subagents/src/index.js +13 -6
  43. package/dist/subagents/src/manager.js +3 -3
  44. package/dist/subagents/src/view.d.ts +1 -0
  45. package/dist/subagents/src/view.js +13 -52
  46. package/dist/trajectory/assets/index.html +27 -5
  47. package/dist/trajectory/index.js +76 -28
  48. package/dist/trajectory/index.js.map +2 -2
  49. package/dist/trajectory/src/assets/index.html +27 -5
  50. package/dist/trajectory/src/index.js +8 -18
  51. package/dist/trajectory/src/server.js +4 -2
  52. package/dist/trajectory/src/server.js.map +2 -2
  53. package/package.json +6 -6
  54. package/skills/pi-extensible-workflows/SKILL.md +2 -0
  55. package/src/agent-execution.ts +77 -41
  56. package/src/background-widget.ts +1 -1
  57. package/src/decoders.ts +20 -0
  58. package/src/execution.ts +41 -4
  59. package/src/host-delivery.ts +1 -0
  60. package/src/host-recovery.ts +8 -7
  61. package/src/host-view.ts +23 -5
  62. package/src/host.ts +173 -35
  63. package/src/paths.ts +3 -0
  64. package/src/registry.ts +1 -1
  65. package/src/roles.ts +4 -3
  66. package/src/script-tools.ts +76 -0
  67. package/src/settings.ts +22 -4
  68. package/src/trajectory.ts +30 -11
  69. package/src/types.ts +11 -4
  70. package/src/utils.ts +28 -0
  71. package/src/validation.ts +19 -1
  72. package/starter/review-loop.ts +0 -2
  73. package/subagents/README.md +2 -2
  74. package/subagents/src/contracts.ts +18 -7
  75. package/subagents/src/index.ts +16 -6
  76. package/subagents/src/manager.ts +3 -3
  77. package/subagents/src/view.ts +13 -50
  78. package/trajectory/src/assets/index.html +27 -5
  79. package/trajectory/src/index.ts +6 -9
  80. 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 extensionDirectory = dirname(fileURLToPath(import.meta.url));
31
- const workflowPackageRoot = basename(dirname(extensionDirectory)) === "dist" ? resolve(extensionDirectory, "../..") : resolve(extensionDirectory, "..");
32
- const WORKFLOW_HOST_ENTRIES = new Set([
33
- canonicalPath(resolve(workflowPackageRoot, "src/index.ts")),
34
- canonicalPath(resolve(workflowPackageRoot, "dist/src/index.js")),
35
- canonicalPath(resolve(workflowPackageRoot, "starter/index.ts")),
36
- canonicalPath(resolve(workflowPackageRoot, "dist/starter/index.js")),
37
- canonicalPath(resolve(workflowPackageRoot, "subagents/index.ts")),
38
- canonicalPath(resolve(workflowPackageRoot, "dist/subagents/index.js")),
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
- let nativePromptTemplateModule;
75
- async function loadNativePromptTemplateModule() {
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 promptTemplatePath;
90
+ let modulePath;
78
91
  if (packageDirectory.directory) {
79
- const candidate = resolve(packageDirectory.directory, "dist/core/prompt-templates.js");
92
+ const candidate = resolve(packageDirectory.directory, "dist/core", file);
80
93
  if (existsSync(candidate))
81
- promptTemplatePath = candidate;
94
+ modulePath = candidate;
82
95
  }
83
- promptTemplatePath ??= resolve(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "core/prompt-templates.js");
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
- return nativePromptTemplateModule ??= import(promptTemplatePath).then((module) => module).catch((error) => { throw new Error(`Could not load Pi prompt templates.${packageDirectoryError}`, { cause: error }); });
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 loadNativePromptTemplateModule()).expandPromptTemplate(text, templates);
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 = typeof session._baseSystemPrompt === "string" ? session._baseSystemPrompt : session.systemPrompt ?? "";
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, baseSystemPrompt, baseOptions) : undefined;
151
+ const result = !inputHandled && runner && baseOptions !== undefined ? await runner.emitBeforeAgentStart(expandedPrompt, undefined, baseOptions) : undefined;
133
152
  const prepared = result;
134
- return { prompt: text, expandedPrompt, systemPrompt: typeof prepared?.systemPrompt === "string" ? prepared.systemPrompt : baseSystemPrompt, systemPromptOptions: baseOptions, messages: prepared?.messages ?? [], inputHandled, diagnostics };
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 }) => canonicalPath(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) => !WORKFLOW_HOST_ENTRIES.has(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
- ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
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 }) => canonicalPath(path)).filter((path) => !WORKFLOW_HOST_ENTRIES.has(path)))];
241
- resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, settingsManager, noExtensions: true, additionalExtensionPaths: extensionPaths, ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: [...input.additionalSkillPaths] } : {}), ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}), ...systemPromptOptions, ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
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 }) => canonicalPath(resolvedPath)), skills: resourceLoader.getSkills().skills.map(({ filePath }) => canonicalPath(filePath)) };
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 }) => canonicalPath(resolvedPath)), skills: skills.skills.map(({ name }) => name), diagnostics, ...(systemSource ? { systemPromptSource: systemSource } : resourceLoader.getSystemPrompt() !== undefined ? { systemPromptSource: "Pi resource loader" } : {}) };
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.
@@ -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 }),
@@ -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";
@@ -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;
@@ -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"];