@marcoscale98/pi-extensible-workflows 5.14.1-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 (94) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/dist/src/agent-execution.js +114 -87
  3. package/dist/src/background-widget.d.ts +61 -0
  4. package/dist/src/background-widget.js +23 -16
  5. package/dist/src/decoders.js +28 -1
  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-navigator.d.ts +18 -0
  10. package/dist/src/host-navigator.js +71 -13
  11. package/dist/src/host-recovery.d.ts +5 -2
  12. package/dist/src/host-recovery.js +3 -3
  13. package/dist/src/host-view.d.ts +21 -1
  14. package/dist/src/host-view.js +138 -47
  15. package/dist/src/host.d.ts +2 -2
  16. package/dist/src/host.js +246 -60
  17. package/dist/src/index.d.ts +3 -1
  18. package/dist/src/index.js +1346 -622
  19. package/dist/src/index.js.map +3 -3
  20. package/dist/src/paths.d.ts +2 -0
  21. package/dist/src/paths.js +2 -0
  22. package/dist/src/registry.js +3 -2
  23. package/dist/src/roles.d.ts +70 -0
  24. package/dist/src/roles.js +326 -0
  25. package/dist/src/script-tools.d.ts +27 -0
  26. package/dist/src/script-tools.js +87 -0
  27. package/dist/src/settings.d.ts +26 -0
  28. package/dist/src/settings.js +277 -0
  29. package/dist/src/trajectory.d.ts +39 -4
  30. package/dist/src/trajectory.js +82 -5
  31. package/dist/src/types.d.ts +30 -3
  32. package/dist/src/types.js +1 -1
  33. package/dist/src/utils.d.ts +21 -1
  34. package/dist/src/utils.js +40 -0
  35. package/dist/src/validation.d.ts +10 -17
  36. package/dist/src/validation.js +29 -458
  37. package/dist/starter/index.js +0 -2
  38. package/dist/starter/index.js.map +1 -1
  39. package/dist/starter/review-loop.d.ts +0 -13
  40. package/dist/starter/review-loop.js +0 -2
  41. package/dist/subagents/index.js +1344 -798
  42. package/dist/subagents/index.js.map +3 -3
  43. package/dist/subagents/src/contracts.d.ts +49 -29
  44. package/dist/subagents/src/contracts.js +18 -7
  45. package/dist/subagents/src/index.d.ts +1 -1
  46. package/dist/subagents/src/index.js +15 -7
  47. package/dist/subagents/src/manager.js +32 -13
  48. package/dist/subagents/src/navigator.js +396 -247
  49. package/dist/subagents/src/view.d.ts +24 -2
  50. package/dist/subagents/src/view.js +107 -80
  51. package/dist/trajectory/assets/index.html +370 -232
  52. package/dist/trajectory/assets/prism.min.js +13 -0
  53. package/dist/trajectory/index.js +445 -277
  54. package/dist/trajectory/index.js.map +3 -3
  55. package/dist/trajectory/src/assets/index.html +370 -232
  56. package/dist/trajectory/src/assets/prism.min.js +13 -0
  57. package/dist/trajectory/src/export.js +4 -2
  58. package/dist/trajectory/src/index.d.ts +2 -0
  59. package/dist/trajectory/src/index.js +151 -60
  60. package/dist/trajectory/src/server.js +138 -11
  61. package/dist/trajectory/src/server.js.map +2 -2
  62. package/package.json +7 -6
  63. package/skills/pi-extensible-workflows/SKILL.md +2 -0
  64. package/src/agent-execution.ts +110 -80
  65. package/src/background-widget.ts +26 -19
  66. package/src/decoders.ts +22 -1
  67. package/src/execution.ts +41 -4
  68. package/src/host-delivery.ts +1 -0
  69. package/src/host-navigator.ts +58 -14
  70. package/src/host-recovery.ts +8 -7
  71. package/src/host-view.ts +116 -42
  72. package/src/host.ts +223 -55
  73. package/src/index.ts +3 -1
  74. package/src/paths.ts +3 -0
  75. package/src/registry.ts +3 -2
  76. package/src/roles.ts +327 -0
  77. package/src/script-tools.ts +76 -0
  78. package/src/settings.ts +224 -0
  79. package/src/trajectory.ts +97 -9
  80. package/src/types.ts +12 -5
  81. package/src/utils.ts +36 -1
  82. package/src/validation.ts +30 -356
  83. package/starter/review-loop.ts +0 -2
  84. package/subagents/README.md +3 -3
  85. package/subagents/src/contracts.ts +22 -7
  86. package/subagents/src/index.ts +18 -7
  87. package/subagents/src/manager.ts +31 -13
  88. package/subagents/src/navigator.ts +336 -180
  89. package/subagents/src/view.ts +108 -76
  90. package/trajectory/src/assets/index.html +370 -232
  91. package/trajectory/src/assets/prism.min.js +13 -0
  92. package/trajectory/src/export.ts +4 -2
  93. package/trajectory/src/index.ts +124 -47
  94. package/trajectory/src/server.ts +114 -14
package/CHANGELOG.md CHANGED
@@ -1,6 +1,141 @@
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
+
72
+ ## [5.17.0] - 2026-09-23
73
+
74
+ ### New capabilities
75
+
76
+ - `/subagents` opens the `/workflow` dashboard layout instead of a picker followed by a `key=value` detail dump. The run list sits beside the selected run's details at 80 columns or wider and drills down from list to details to actions below that, with the same configured and vim key hints and page scrolling. While a run is active, the active runs and the selection refresh every second. Details show the run ID and the shared agent fields in the `/workflow` agent order; the prompt, live system prompt, and result open in the editor, as in `/workflow`, and editor and copy actions leave the action menu open. A running run shows its live activity through the new opt-in `includeActivity` inspection context; `subagents_inspect` still leaves activity out. A retry selects the new run, and a repeated name carries its short ID in the list, as in the `/workflow` picker. Outside the TUI, the picker rows follow the `/workflow` picker: state glyph, name, state, model, cost, and runtime.
77
+ - `/subagents` gains the `/workflow` run actions that apply to standalone runs: `Delete`, `Delete all completed`, and `Delete all failed` (also in the picker outside the TUI), and `Copy run path`. Stop and deletions ask for confirmation. `SubagentManager` gains an optional `delete(request, context)` that removes a settled record and refuses running runs and runs whose worktree cleanup is pending; it is not a model tool.
78
+ - Background subagents appear below the editor in the workflow widget's frame, one row per run with model, tokens, cost, elapsed time, and quiet or stalled warnings, and each leaves a receipt in the transcript when it settles, like a background workflow run.
79
+ - The `subagents_run` result header shows tokens and cost, like the workflow header.
80
+
81
+ ### Fixes
82
+
83
+ - Declining the Stop confirmation in the `/workflow` dashboard no longer leaves Pi stuck. Pi's confirm dialog replaced the dashboard and then restored the editor, so the `/workflow` command never finished: later messages and extension commands such as `/subagents` queued without running, and the workflow widget stayed hidden. The question now appears inside the dashboard, as in `/subagents`; declining keeps the dashboard open on the action menu.
84
+ - A subagent that settled just before its manager was disposed, for example at session shutdown, no longer stays `running` on disk. Disposal skipped the terminal status write, and a new manager in the same Pi process treated the record as owned by a live process, so it kept showing as running and could not be retried.
85
+
86
+ ## [5.16.3] - 2026-09-23
87
+
88
+ ### Fixes
89
+
90
+ - A live foreground workflow block taller than the terminal no longer snaps pi's scrollback to the bottom on every update. Past `rows - 10` lines the block turns compact: finished phases collapse to one summary line (done, failed, wall time, tokens, cost) plus their failures, and the current phase lists failed, active, and the three latest settled agents with a count of the rest. `ctrl+o` still shows everything, and a finished block keeps the full list.
91
+
92
+ ## [5.16.2] - 2026-09-23
93
+
94
+ ### New capabilities
95
+
96
+ - Trajectory highlights workflow scripts, JSON values (tool and workflow arguments, environment, and output), and fenced code in messages with a vendored Prism 1.30.0 build (JavaScript, TypeScript, JSON, bash, diff, Python).
97
+ - Trajectory lists runs by newest completion with active runs on top. Each session shows every active run or subagent and at least three in total; the rest sit behind a "+ N more" toggle that is remembered per browser, and the selected entry always stays visible.
98
+
99
+ ### Fixes
100
+
101
+ - Pi no longer adopts a Trajectory server that answers on the port without owning the lock, such as an older server whose lock was lost. `/health` now reports the server pid and fingerprint; a mismatching server that reports its pid is replaced, and one without an identity fails the attach with a clear error instead of serving outdated code indefinitely.
102
+
103
+ ## [5.16.1] - 2026-09-23
104
+
105
+ ### Fixes
106
+
107
+ - Trajectory no longer blanks a live subagent's or agent's events for about a second when its transcript grows between a state poll and the transcript read; the stale reply keeps the cached entries until the newer revision arrives.
108
+
109
+ ## [5.16.0] - 2026-09-23
110
+
111
+ ### Breaking changes
112
+
113
+ - `extensionSettings.trajectory.themes` is removed; settings files that still set it fail validation. Persisted launch snapshots that carry it still load.
114
+
115
+ ### New capabilities
116
+
117
+ - Trajectory has a paper-style look in light and dark versions. It follows the system colour scheme, and a sidebar toggle overrides it per browser. On phones it shows one panel at a time, with Runs, Trace, and Details tabs.
118
+ - The Trajectory inspector shows workflow arguments (bounded for large values) (#300) and an Agent details Output tab (#301).
119
+
120
+ ### Performance
121
+
122
+ - Foreground workflow progress bursts collapse to one update per 100ms, and progress reads `state.json` alone instead of state plus snapshot, so Pi no longer re-renders the whole tool block for every agent event.
123
+ - Trajectory ships tool timing only for the run a browser shows and omits timing it already delivered at the same revision. The gantt patches only changed lanes and merges tool calls that overlap on screen.
124
+ - Trajectory publishers poll every 10s while no browser is attached, and hidden tabs skip rendering.
125
+
126
+ ### Fixes
127
+
128
+ - A Trajectory browser tab that drops without a WebSocket close frame is released, so viewer counts and pending requests no longer leak.
129
+ - Live Trajectory timing keeps the newest entries within its 64 KB per-agent budget, so a long agent's gantt keeps growing instead of freezing on its first calls.
130
+ - `piewf` `npx` examples use the scoped `@piewf/cli` package.
131
+
132
+ ## [5.15.0] - 2026-09-18
133
+
134
+ ### New capabilities
135
+
136
+ - Roles are exposed as a standalone `pi-extensible-workflows/roles` subpath (`discoverRoles`, `loadRole`, `resolveRole`) that resolves a role into plain data (model, tools, skills, extensions, prompt, context file scopes, unmatched selectors) without a workflow run; the workflow executor, validation, CLI, and subagents consume the same implementation (#294).
137
+ - `@piewf/cli` adds a `pi-role <role> [pi arguments...]` launcher that resolves a role and starts the regular Pi CLI with the matching model, tools, skills, extensions, and system prompt arguments; remaining arguments are forwarded to Pi (#295).
138
+
4
139
  ## [5.14.1] - 2026-09-17
5
140
 
6
141
  ### Fixes
@@ -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, modelAliasName, modelCapability, resolveModelReference, resourcePatternHasMagic, selectResourcesByLayers, 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,9 @@ 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";
20
+ import { canonicalExtensionSelector, resolveRole } from "./roles.js";
19
21
  const localToolTimingExtension = createToolTimingExtension();
20
22
  function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
21
23
  if (!value)
@@ -26,25 +28,26 @@ function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
26
28
  function accounting(stats) {
27
29
  return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
28
30
  }
29
- const extensionDirectory = dirname(fileURLToPath(import.meta.url));
30
- const workflowPackageRoot = basename(dirname(extensionDirectory)) === "dist" ? resolve(extensionDirectory, "../..") : resolve(extensionDirectory, "..");
31
- const WORKFLOW_HOST_ENTRIES = new Set([
32
- canonicalPath(resolve(workflowPackageRoot, "src/index.ts")),
33
- canonicalPath(resolve(workflowPackageRoot, "dist/src/index.js")),
34
- canonicalPath(resolve(workflowPackageRoot, "starter/index.ts")),
35
- canonicalPath(resolve(workflowPackageRoot, "dist/starter/index.js")),
36
- canonicalPath(resolve(workflowPackageRoot, "subagents/index.ts")),
37
- canonicalPath(resolve(workflowPackageRoot, "dist/subagents/index.js")),
38
- ]);
39
- function canonicalExtensionSelector(selector, base = process.cwd()) {
40
- const negated = selector.startsWith("!");
41
- const body = negated ? selector.slice(1) : selector;
42
- if (body === "*" || body === "**" || body.startsWith("**/"))
43
- return selector;
44
- const resolved = resolve(base, body);
45
- if (resourcePatternHasMagic(body))
46
- return `${negated ? "!" : ""}${resolved}`;
47
- return `${negated ? "!" : ""}${canonicalPath(resolved)}`;
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;
48
51
  }
49
52
  const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
50
53
  function workflowSystemPromptPath(cwd, agentDir, projectTrusted) {
@@ -80,34 +83,41 @@ function loadPiPackageDirectory() {
80
83
  }
81
84
  }).catch((error) => ({ error: `could not load @earendil-works/pi-coding-agent package metadata: ${errorText(error)}` }));
82
85
  }
83
- let nativePromptTemplateModule;
84
- 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) {
85
89
  const packageDirectory = await loadPiPackageDirectory();
86
- let promptTemplatePath;
90
+ let modulePath;
87
91
  if (packageDirectory.directory) {
88
- const candidate = resolve(packageDirectory.directory, "dist/core/prompt-templates.js");
92
+ const candidate = resolve(packageDirectory.directory, "dist/core", file);
89
93
  if (existsSync(candidate))
90
- promptTemplatePath = candidate;
94
+ modulePath = candidate;
91
95
  }
92
- 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);
93
97
  const packageDirectoryError = packageDirectory.error ? ` Package directory lookup failed: ${packageDirectory.error}` : "";
94
- 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;
95
104
  }
96
105
  async function expandPromptTemplateForInspection(text, templates) {
97
- 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);
98
110
  }
99
111
  async function preparePiPrompt(native, text) {
100
112
  const session = native;
101
113
  const diagnostics = [];
102
114
  const runner = session._extensionRunner;
103
- const baseSystemPrompt = typeof session._baseSystemPrompt === "string" ? session._baseSystemPrompt : session.systemPrompt ?? "";
115
+ const baseSystemPrompt = session.systemPrompt ?? "";
104
116
  const baseOptions = session._baseSystemPromptOptions;
105
117
  const expandSkillCommand = session._expandSkillCommand;
106
118
  const templates = session.promptTemplates;
107
119
  if (!runner)
108
120
  diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: extension runner is missing", source: "Pi session" });
109
- if (typeof session._baseSystemPrompt !== "string")
110
- diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt is missing", source: "Pi session" });
111
121
  if (baseOptions === undefined)
112
122
  diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt options are missing", source: "Pi session" });
113
123
  if (typeof expandSkillCommand !== "function")
@@ -138,9 +148,11 @@ async function preparePiPrompt(native, text) {
138
148
  diagnostics.push({ type: "error", message: `Pi prompt template expansion is unavailable: ${errorText(error)}`, source: "Pi session" });
139
149
  }
140
150
  }
141
- 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;
142
152
  const prepared = result;
143
- 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 };
144
156
  }
145
157
  finally {
146
158
  unsubscribe?.();
@@ -184,6 +196,22 @@ export function flushExtensionProviders(resourceLoader, modelRuntime) {
184
196
  runtime.pendingNativeProviderRegistrations = [];
185
197
  return failures;
186
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"; }
187
215
  async function createLocalPiSessionHandle(input, sessionStartEvent) {
188
216
  const agentDir = input.agentDir ?? getAgentDir();
189
217
  const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
@@ -197,28 +225,33 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
197
225
  const tools = [...new Set([...input.tools, ...customTools.map(({ name }) => name)])];
198
226
  let settingsManager;
199
227
  let resourceLoader;
228
+ const builtins = builtinExtensions();
229
+ const builtinNames = builtins.flatMap((extension) => typeof extension === "function" ? [] : [extension.name]);
230
+ const extensionFactories = [...builtins, ...(input.extensionFactories ?? [])];
200
231
  const policy = input.resourcePolicy;
201
232
  if (policy) {
202
233
  settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: false });
203
234
  settingsManager.setProjectTrusted(policy.projectTrusted);
204
- const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
235
+ const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
205
236
  const resolved = await packageManager.resolve();
206
- 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)))];
207
238
  const selectorSources = policy.selectorSources;
208
- const extensionLayers = [selectorSources.global.extensions, selectorSources.project.extensions, selectorSources.role?.extensions, selectorSources.call?.extensions];
209
- const extensionSelectors = extensionLayers.flatMap((layer) => (layer ?? []).map((original) => ({ original, matching: canonicalExtensionSelector(original, input.cwd) })));
210
- const selectedExtensions = selectResourcesByLayers(extensionLayers.map((layer) => layer?.map((selector) => canonicalExtensionSelector(selector, input.cwd))), discoveredExtensions);
211
- const extensionPaths = selectedExtensions.filter((path) => !WORKFLOW_HOST_ENTRIES.has(path));
212
- const unmatchedExtensions = extensionSelectors.filter(({ matching }) => unmatchedResourcePatterns([matching], discoveredExtensions).length > 0).map(({ original }) => original);
239
+ const selection = resolveRole(undefined, {
240
+ cwd: input.cwd,
241
+ selectorSources,
242
+ resources: { extensions: discoveredExtensions },
243
+ });
244
+ const selectedExtensions = selection.selectedExtensions ?? [];
245
+ const extensionPaths = selectedExtensions.filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path));
213
246
  policy.selectedExtensions = selectedExtensions;
214
- policy.unmatchedExtensions = unmatchedExtensions;
247
+ policy.unmatchedExtensions = selection.unmatchedExtensions ?? [];
215
248
  const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
216
249
  const updateSkillMatches = (skills) => {
217
250
  const names = [...new Set(skills.map(({ name }) => name))];
218
- const skillLayers = [selectorSources.global.skills, selectorSources.project.skills, selectorSources.role?.skills, selectorSources.call?.skills];
219
- const selectedSkills = selectResourcesByLayers(skillLayers, names);
251
+ const selected = resolveRole(undefined, { cwd: input.cwd, selectorSources, resources: { skills: names } });
252
+ const selectedSkills = selected.selectedSkills ?? [];
220
253
  policy.selectedSkills = selectedSkills;
221
- policy.unmatchedSkills = unmatchedResourcePatterns(skillLayers.flatMap((layer) => layer ?? []), names);
254
+ policy.unmatchedSkills = selected.unmatchedSkills ?? [];
222
255
  return new Set(names.filter((name) => !selectedSkills.includes(name)));
223
256
  };
224
257
  resourceLoader = new DefaultResourceLoader({
@@ -229,7 +262,7 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
229
262
  additionalExtensionPaths: extensionPaths,
230
263
  noSkills: true,
231
264
  additionalSkillPaths: [...new Set([...skillPaths, ...(input.additionalSkillPaths ?? [])])],
232
- ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
265
+ extensionFactories,
233
266
  ...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}),
234
267
  skillsOverride: (base) => {
235
268
  const disabledSkills = updateSkillMatches(base.skills);
@@ -242,10 +275,10 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
242
275
  }
243
276
  else {
244
277
  settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: true });
245
- const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
278
+ const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
246
279
  const resolved = await packageManager.resolve();
247
- const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled }) => enabled).map(({ path }) => canonicalPath(path)).filter((path) => !WORKFLOW_HOST_ENTRIES.has(path)))];
248
- 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 ?? ""] } : {}) });
249
282
  await resourceLoader.reload();
250
283
  }
251
284
  const providerFailures = flushExtensionProviders(resourceLoader, modelRuntime);
@@ -256,7 +289,7 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
256
289
  // missing, so the failure travels with the complaint rather than being
257
290
  // swallowed — otherwise a misconfigured gateway reads as a typo in a model
258
291
  // name.
259
- 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)" : "";
260
293
  throw new WorkflowError("UNKNOWN_MODEL", `Unknown model: ${input.model.provider}/${input.model.model}${because}`);
261
294
  }
262
295
  const effectiveSessionStartEvent = input.settings === undefined && sessionStartEvent === undefined ? undefined : { ...(sessionStartEvent ?? { type: "session_start", reason: "startup" }), settings: input.settings ?? Object.freeze({}) };
@@ -285,13 +318,13 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
285
318
  await shutdown("quit").catch(() => undefined);
286
319
  throw error;
287
320
  }
288
- 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)) };
289
322
  const resourceInspection = () => {
290
323
  const extensions = resourceLoader.getExtensions();
291
324
  const skills = resourceLoader.getSkills();
292
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 } : {}) }))];
293
326
  const systemSource = systemPromptSource;
294
- 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" } : {}) };
295
328
  };
296
329
  const managedSession = Object.assign(session, {
297
330
  getLeafId: () => manager.getLeafId(),
@@ -944,7 +977,6 @@ export class WorkflowAgentExecutor {
944
977
  const roleDefinition = roleName ? this.root.agentDefinitions?.[roleName] : undefined;
945
978
  if (roleName && !roleDefinition)
946
979
  throw new WorkflowError("UNKNOWN_AGENT_TYPE", `Unknown agent role: ${roleName}`);
947
- const candidateTools = inheritedTools !== undefined ? [...inheritedTools] : [...this.root.tools];
948
980
  if (inheritedTools === undefined)
949
981
  for (const pattern of roleDefinition?.tools ?? []) {
950
982
  const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
@@ -956,37 +988,30 @@ export class WorkflowAgentExecutor {
956
988
  }
957
989
  }
958
990
  }
959
- const selectorLayers = [
960
- this.root.resourceSelectors?.tools,
961
- roleDefinition?.tools,
962
- options.tools,
963
- ];
964
- for (const pattern of options.tools ?? []) {
965
- const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
966
- if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !candidateTools.includes(body))
967
- throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${body}`);
968
- }
969
- const outsideEffectiveTool = options.effectiveTools?.find((tool) => !candidateTools.includes(tool));
970
- if (outsideEffectiveTool)
971
- throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${outsideEffectiveTool}`);
972
- const requested = options.effectiveTools ?? selectResourcesByLayers(selectorLayers, candidateTools);
973
- const forbidden = requested.find((tool) => !this.root.tools.has(tool));
974
- if (forbidden)
975
- throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${forbidden}`);
976
- const requestedModel = options.model ?? (roleDefinition?.model && roleDefinition.thinking && !roleDefinition.model.includes(":") ? `${roleDefinition.model}:${roleDefinition.thinking}` : roleDefinition?.model);
977
- const alias = requestedModel === undefined ? undefined : modelAliasName(requestedModel, this.root.modelAliases ?? {});
978
- const blockedAlias = requestedModel?.split(":", 1)[0];
979
- if (requestedModel !== undefined && blockedAlias && this.root.blockedAliases?.has(blockedAlias) && !alias) {
980
- const target = this.root.blockedAliasTargets?.[blockedAlias];
981
- throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${requestedModel}${target ? ` resolved to ${target}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
982
- }
983
- const model = options.modelOverride ?? parseModel(requestedModel, this.root.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
984
- const availableModels = this.root.knownModels ?? this.root.availableModels ?? new Set([modelCapability(this.root.model)]);
985
- if (!availableModels.has(modelCapability(model)))
986
- throw new WorkflowError("UNKNOWN_MODEL", `Unknown model${requestedModel ? ` ${requestedModel} resolved to ${modelCapability(model)}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
987
- const overrideSystemPrompt = roleDefinition?.overrideSystemPrompt === true;
988
- const contextFiles = options.contextFiles ?? roleDefinition?.contextFiles;
989
- return { model, ...(alias && requestedModel ? { requestedModel } : {}), tools: requested, ...(overrideSystemPrompt ? { systemPrompt: roleDefinition.prompt ?? "" } : {}), systemPromptAppend: overrideSystemPrompt ? "" : roleDefinition?.prompt ?? "", ...(contextFiles === undefined ? {} : { contextFiles: [...contextFiles] }) };
991
+ const resolved = resolveRole(roleName, {
992
+ cwd: this.root.cwd,
993
+ ...(roleDefinition === undefined ? {} : { definition: roleDefinition }),
994
+ ...(this.root.agentDefinitions === undefined ? {} : { definitions: this.root.agentDefinitions }),
995
+ ...(this.root.agentDir === undefined ? {} : { agentDir: this.root.agentDir }),
996
+ selectorSources: { global: this.root.resourceSelectors ?? {}, project: {} },
997
+ ...(this.root.modelAliases === undefined ? {} : { modelAliases: this.root.modelAliases }),
998
+ ...(this.root.knownModels === undefined ? {} : { knownModels: this.root.knownModels }),
999
+ ...(this.root.availableModels === undefined ? {} : { availableModels: this.root.availableModels }),
1000
+ ...(this.root.blockedAliases === undefined ? {} : { blockedAliases: this.root.blockedAliases }),
1001
+ ...(this.root.blockedAliasTargets === undefined ? {} : { blockedAliasTargets: this.root.blockedAliasTargets }),
1002
+ ...(this.root.settingsPath === undefined ? {} : { settingsPath: this.root.settingsPath }),
1003
+ rootModel: this.root.model,
1004
+ rootTools: this.root.tools,
1005
+ ...(inheritedTools === undefined ? {} : { inheritedTools }),
1006
+ ...(options.model === undefined ? {} : { model: options.model }),
1007
+ ...(options.modelOverride === undefined ? {} : { modelOverride: options.modelOverride }),
1008
+ ...(options.tools === undefined ? {} : { tools: options.tools }),
1009
+ ...(options.skills === undefined ? {} : { skills: options.skills }),
1010
+ ...(options.extensions === undefined ? {} : { extensions: options.extensions }),
1011
+ ...(options.contextFiles === undefined ? {} : { contextFiles: options.contextFiles }),
1012
+ ...(options.effectiveTools === undefined ? {} : { effectiveTools: options.effectiveTools }),
1013
+ });
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 }) };
990
1015
  }
991
1016
  async execute(task, options, signal, customTools = [], setSteer, beforeRetry) {
992
1017
  const executionSignal = signal ?? this.root.runContext?.signal;
@@ -1454,6 +1479,7 @@ export class FairAgentScheduler {
1454
1479
  const agentTool = defineTool({
1455
1480
  name: "agent", label: "Child Agent", description: "Start a direct child agent",
1456
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() }),
1457
1483
  execute: async (_id, params) => {
1458
1484
  if (!isChildAgentToolParams(params))
1459
1485
  throw new WorkflowError("INVALID_METADATA", "Invalid child agent parameters");
@@ -1466,14 +1492,15 @@ export class FairAgentScheduler {
1466
1492
  Reflect.deleteProperty(agentOptions, "prompt");
1467
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 }) };
1468
1494
  const child = this.spawn(parent.runId, params.prompt, options, parentId);
1469
- 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 } };
1470
1496
  },
1471
1497
  });
1472
1498
  const resultTool = defineTool({
1473
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",
1474
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() })) }),
1475
1502
  execute: async (_id, params) => { const value = await this.result(parentId, params.id); if (!value.ok && value.error.code === "BUDGET_EXHAUSTED")
1476
- 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 }; }
1477
1504
  });
1478
1505
  const steerTool = defineTool({
1479
1506
  name: "steer_subagent", label: "Steer Child", description: "Steer a running direct child",
@@ -22,6 +22,23 @@ 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;
30
+ /**
31
+ * How long an agent may go without an event before the widget says so.
32
+ *
33
+ * Deliberately shorter than the core's own stall threshold. That one answers
34
+ * "is this agent dead"; this one answers "should you go and look". An agent
35
+ * asking a question in its own pane produces exactly this signature — no tool
36
+ * calls, no events, waiting on a person — and the core cannot see it, since the
37
+ * pane is a separate process. Three minutes is longer than any single stretch
38
+ * of thinking and far shorter than the ten minutes it takes to be declared
39
+ * stalled.
40
+ */
41
+ export declare const QUIET_MS: number;
25
42
  /**
26
43
  * Overrides the switch above, for the tests that cover navigation.
27
44
  *
@@ -32,6 +49,11 @@ export type BackgroundWidgetAPI = Pick<ExtensionAPI, "appendEntry" | "on"> & {
32
49
  export declare const __navigationForTests: {
33
50
  enabled: boolean;
34
51
  };
52
+ /**
53
+ * Paints text in a themed colour role. Falls back to the bare text when no
54
+ * theme is available, so a frame rendered outside the TUI is still legible.
55
+ */
56
+ export type Paint = (role: Parameters<Theme["fg"]>[0], text: string) => string;
35
57
  interface ReceiptAgent {
36
58
  id?: string;
37
59
  parentId?: string;
@@ -62,8 +84,47 @@ interface Receipt {
62
84
  agents: readonly ReceiptAgent[];
63
85
  error?: string;
64
86
  }
87
+ /** Status mark for a finished-or-running unit of work. */
88
+ export declare function mark(state: string | undefined, now: number, paint: Paint, runState?: string): string;
89
+ export declare function formatElapsed(ms: number): string;
65
90
  export declare function formatTokens(tokens: number | undefined): string;
66
91
  export declare function formatCost(cost: number | undefined): string;
92
+ interface Row {
93
+ text: string;
94
+ /**
95
+ * Numbers to set against the right-hand rule rather than trailing the name.
96
+ *
97
+ * Names are ragged — a phase called `plan` beside one called `refactor-2` —
98
+ * so numbers that follow them are ragged too, and comparing the cost of one
99
+ * agent against another means reading across a gap that moves on every row.
100
+ * Held to the right edge, they form a column.
101
+ */
102
+ right?: string;
103
+ /** 2 = a run header or something needing an answer, 1 = live or failed work, 0 = finished detail. */
104
+ rank: number;
105
+ /**
106
+ * Set on the one row that stands for a whole run.
107
+ *
108
+ * Rank alone cannot protect it: a checkpoint needs an answer and so shares
109
+ * the top rank, and enough of them would spend the budget before a later
110
+ * run's header is reached. A run missing its header is not a terse run, it
111
+ * is an invisible one.
112
+ */
113
+ header?: boolean;
114
+ /**
115
+ * A stable name for a row that can be folded shut, and the key of the row it
116
+ * hangs from. Together they let the cursor walk a tree rather than a list:
117
+ * collapsing a phase hides its agents, collapsing a run hides everything
118
+ * under it, and the choice survives the repaint that follows.
119
+ */
120
+ key?: string;
121
+ parent?: string;
122
+ }
123
+ /**
124
+ * The box around the live rows, shared with the subagent widget so both sit
125
+ * under the editor in one style.
126
+ */
127
+ export declare function drawFrame(title: string, hint: string, body: readonly Pick<Row, "text" | "right">[], width: number, paint: Paint, accent: (text: string) => string, scrollbar?: (index: number) => string | undefined): string[];
67
128
  /** The receipt as transcript lines: the widget's tree, one level deeper. */
68
129
  export declare function renderReceipt(data: Receipt, expanded: boolean, theme: Theme): string[];
69
130
  export interface BackgroundWidgetController {