@marcoscale98/pi-extensible-workflows 5.17.0-fork.1 → 6.2.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 (128) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +2 -2
  3. package/dist/src/agent-execution.d.ts +43 -10
  4. package/dist/src/agent-execution.js +442 -162
  5. package/dist/src/background-widget.d.ts +5 -1
  6. package/dist/src/background-widget.js +5 -10
  7. package/dist/src/decoders.d.ts +2 -3
  8. package/dist/src/decoders.js +59 -37
  9. package/dist/src/execution.d.ts +6 -2
  10. package/dist/src/execution.js +48 -7
  11. package/dist/src/host-delivery.js +5 -8
  12. package/dist/src/host-navigator.js +5 -2
  13. package/dist/src/host-phases.d.ts +1 -0
  14. package/dist/src/host-recovery.d.ts +11 -8
  15. package/dist/src/host-recovery.js +14 -22
  16. package/dist/src/host-runtime.js +56 -5
  17. package/dist/src/host-view.d.ts +6 -1
  18. package/dist/src/host-view.js +46 -20
  19. package/dist/src/host.d.ts +2 -2
  20. package/dist/src/host.js +246 -118
  21. package/dist/src/index.d.ts +1 -2
  22. package/dist/src/index.js +1985 -1690
  23. package/dist/src/index.js.map +3 -3
  24. package/dist/src/paths.d.ts +8 -5
  25. package/dist/src/paths.js +24 -13
  26. package/dist/src/persistence.d.ts +1 -0
  27. package/dist/src/persistence.js +1 -0
  28. package/dist/src/pi-runtime-runner.js +4 -1
  29. package/dist/src/registry.d.ts +9 -7
  30. package/dist/src/registry.js +29 -71
  31. package/dist/src/runtime/agent-runner.d.ts +2 -0
  32. package/dist/src/script-tools.d.ts +27 -0
  33. package/dist/src/script-tools.js +87 -0
  34. package/dist/src/session-lease.d.ts +1 -1
  35. package/dist/src/session-lease.js +45 -11
  36. package/dist/src/settings.d.ts +19 -3
  37. package/dist/src/settings.js +63 -50
  38. package/dist/src/store.d.ts +3 -1
  39. package/dist/src/store.js +17 -1
  40. package/dist/src/trajectory.d.ts +12 -3
  41. package/dist/src/trajectory.js +32 -11
  42. package/dist/src/types.d.ts +129 -45
  43. package/dist/src/types.js +5 -8
  44. package/dist/src/utils.d.ts +21 -1
  45. package/dist/src/utils.js +46 -6
  46. package/dist/src/validation.d.ts +14 -3
  47. package/dist/src/validation.js +143 -87
  48. package/dist/starter/index.js +8 -11
  49. package/dist/starter/index.js.map +1 -1
  50. package/dist/starter/review-loop.d.ts +0 -13
  51. package/dist/starter/review-loop.js +8 -9
  52. package/dist/subagents/index.js +904 -956
  53. package/dist/subagents/index.js.map +3 -3
  54. package/dist/subagents/src/contracts.d.ts +48 -31
  55. package/dist/subagents/src/contracts.js +20 -9
  56. package/dist/subagents/src/index.d.ts +1 -1
  57. package/dist/subagents/src/index.js +14 -8
  58. package/dist/subagents/src/manager.js +52 -73
  59. package/dist/subagents/src/navigator.js +3 -14
  60. package/dist/subagents/src/view.d.ts +1 -1
  61. package/dist/subagents/src/view.js +15 -68
  62. package/dist/subagents/src/worktree.js +1 -1
  63. package/dist/trajectory/assets/index.html +35 -13
  64. package/dist/trajectory/index.js +576 -932
  65. package/dist/trajectory/index.js.map +3 -3
  66. package/dist/trajectory/src/assets/index.html +35 -13
  67. package/dist/trajectory/src/index.js +134 -104
  68. package/dist/trajectory/src/server.js +268 -27
  69. package/dist/trajectory/src/server.js.map +3 -3
  70. package/dist/trajectory/src/startup-mutex.d.ts +17 -0
  71. package/dist/trajectory/src/startup-mutex.js +172 -0
  72. package/examples/workflow-extension-template/README.md +4 -7
  73. package/examples/workflow-extension-template/extension.test.mjs +4 -8
  74. package/examples/workflow-extension-template/index.js +0 -3
  75. package/package.json +10 -10
  76. package/skills/pi-extensible-workflows/SKILL.md +12 -8
  77. package/src/agent-execution.ts +412 -164
  78. package/src/background-widget.ts +5 -11
  79. package/src/decoders.ts +51 -37
  80. package/src/execution.ts +46 -9
  81. package/src/host-delivery.ts +5 -8
  82. package/src/host-navigator.ts +4 -2
  83. package/src/host-phases.ts +1 -1
  84. package/src/host-recovery.ts +23 -28
  85. package/src/host-runtime.ts +44 -6
  86. package/src/host-view.ts +41 -16
  87. package/src/host.ts +229 -142
  88. package/src/index.ts +1 -2
  89. package/src/paths.ts +18 -9
  90. package/src/persistence.ts +1 -0
  91. package/src/pi-runtime-runner.ts +3 -1
  92. package/src/registry.ts +28 -64
  93. package/src/runtime/agent-runner.ts +2 -0
  94. package/src/script-tools.ts +76 -0
  95. package/src/session-lease.ts +34 -6
  96. package/src/settings.ts +56 -50
  97. package/src/store.ts +18 -2
  98. package/src/trajectory.ts +32 -15
  99. package/src/types.ts +82 -34
  100. package/src/utils.ts +42 -5
  101. package/src/validation.ts +121 -80
  102. package/starter/index.ts +0 -1
  103. package/starter/review-loop.ts +8 -9
  104. package/starter/roles/developer.md +0 -1
  105. package/starter/roles/oracle.md +0 -1
  106. package/starter/roles/researcher.md +0 -1
  107. package/starter/roles/reviewer.md +0 -1
  108. package/starter/roles/scout.md +0 -1
  109. package/subagents/README.md +10 -16
  110. package/subagents/src/contracts.ts +21 -11
  111. package/subagents/src/index.ts +17 -8
  112. package/subagents/src/manager.ts +48 -128
  113. package/subagents/src/navigator.ts +4 -13
  114. package/subagents/src/view.ts +16 -63
  115. package/subagents/src/worktree.ts +1 -1
  116. package/trajectory/src/assets/index.html +35 -13
  117. package/trajectory/src/index.ts +112 -91
  118. package/trajectory/src/server.ts +46 -10
  119. package/trajectory/src/startup-mutex.ts +127 -0
  120. package/dist/src/roles.d.ts +0 -70
  121. package/dist/src/roles.js +0 -325
  122. package/dist/starter/roles/developer.md +0 -6
  123. package/dist/starter/roles/oracle.md +0 -18
  124. package/dist/starter/roles/researcher.md +0 -20
  125. package/dist/starter/roles/reviewer.md +0 -19
  126. package/dist/starter/roles/scout.md +0 -18
  127. package/examples/workflow-extension-template/roles/reviewer.md +0 -4
  128. package/src/roles.ts +0 -326
@@ -1,22 +1,25 @@
1
- import { existsSync, realpathSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { copyFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  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
+ import { Value } from "typebox/value";
7
8
  import { Compile } from "typebox/compile";
8
- import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
9
+ import { createAgentSession, createCodemodeExtension, createMcpExtension, createToolSearchExtension, DefaultPackageManager, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager, SettingsManager, loadSkills } from "@earendil-works/pi-coding-agent";
9
10
  ;
10
- import { SerialLane, assertModelThinking, byPriorityThenName, deepFreeze, errorText, jsonObject, jsonValue, mergeWorkflowExtensionSettings, object, resolveModelReference, resourcePatternHasMagic, unmatchedResourcePatterns } from "./utils.js";
11
+ import { SerialLane, assertModelThinking, byPriorityThenName, deepFreeze, errorCode, errorText, jsonObject, jsonValue, object, physicalModel, resolveModelReference, resourcePatternHasMagic, selectResourcesByLayers, modelCapability, unmatchedResourcePatterns } from "./utils.js";
11
12
  import { SETTLED_AGENT_STATES, WorkflowError, isContextFileScope, zeroAccounting } from "./types.js";
12
13
  import { createLiveSessionHandoff } from "./session-handoff.js";
13
14
  import { createToolTimingExtension } from "./tool-timing.js";
14
15
  import { isEmptyAbortedAssistant, normalizePiMessage, normalizePiSessionEvent, runtimeProgressToAgentProgress } from "./pi-runtime-adapter.js";
15
16
  import { createPiRuntimeAgentRunner, isRuntimeAgentProviderError, normalizePiRuntimeError } from "./pi-runtime-runner.js";
16
17
  import { defaultWorkflowResultSchema } from "./runtime/workflow-result.js";
17
- import { validateAgentOptions, validateSchema } from "./validation.js";
18
- import { canonicalPath } from "./paths.js";
19
- import { canonicalExtensionSelector, resolveRole } from "./roles.js";
18
+ import { isCoreAgentOption, validateAgentOptions, validateSchema } from "./validation.js";
19
+ import { canonicalPath, extensionIdentity, piResourceContains, piResourcePath, sameFilesystemPath } from "./paths.js";
20
+ import { isCorePackageName } from "./package-identity.js";
21
+ import { canonicalExtensionSelector, validateContextFileScopes, validateSelectorList, validateWorkflowExtensionSettings } from "./settings.js";
22
+ import { agentIdentityPath } from "./execution.js";
20
23
  const localToolTimingExtension = createToolTimingExtension();
21
24
  function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
22
25
  if (!value)
@@ -24,19 +27,83 @@ function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
24
27
  assertModelThinking(value);
25
28
  return resolveModelReference(value, aliases, knownModels, settingsPath);
26
29
  }
30
+ async function authorizedResources(root, cwd, options) {
31
+ const agentDir = root.agentDir ?? getAgentDir();
32
+ const trusted = root.projectTrusted ?? false;
33
+ const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
34
+ settingsManager.setProjectTrusted(trusted);
35
+ const resolved = await new DefaultPackageManager({ cwd, agentDir, settingsManager, builtinExtensions: ["codemode", "tool-search", "mcp"] }).resolve();
36
+ const extensions = resolved.extensions.filter(({ enabled, metadata }) => enabled && (trusted || metadata.scope !== "project")).map(({ path }) => extensionIdentity(path));
37
+ const skillPaths = resolved.skills.filter(({ enabled, metadata }) => enabled && (trusted || metadata.scope !== "project")).map(({ path }) => path);
38
+ const skills = loadSkills({ cwd, agentDir, skillPaths: [...skillPaths, ...(root.additionalSkillPaths ?? [])], includeDefaults: false }).skills.map(({ name }) => name);
39
+ const parent = options.capabilities;
40
+ return { tools: [...root.tools].filter((tool) => options.effectiveTools === undefined || options.effectiveTools.includes(tool)).filter((tool) => parent === undefined || parent.tools.includes(tool)), skills: [...new Set(skills)].filter((name) => parent === undefined || parent.skills.includes(name)), extensions: [...new Set(extensions)].filter((path) => parent === undefined || parent.extensions.includes(path)) };
41
+ }
42
+ function validatePreparedConfiguration(root, configuration, capabilities, cwd, resumed) {
43
+ const code = resumed ? "RESUME_INCOMPATIBLE" : "INVALID_METADATA";
44
+ if (!jsonObject(configuration) || typeof configuration.projectTrusted !== "boolean" || typeof configuration.systemPromptAppend !== "string" || configuration.systemPrompt !== undefined && typeof configuration.systemPrompt !== "string")
45
+ throw new WorkflowError(code, "Invalid prepared agent configuration");
46
+ if (configuration.projectTrusted && !root.projectTrusted)
47
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Prepared agent configuration lost project trust");
48
+ if (!object(configuration.model) || typeof configuration.model.provider !== "string" || typeof configuration.model.model !== "string")
49
+ throw new WorkflowError(code, "Invalid prepared model");
50
+ const model = resolveModelReference(`${configuration.model.provider}/${configuration.model.model}${configuration.model.thinking === undefined ? "" : `:${configuration.model.thinking}`}`);
51
+ if (root.availableModels !== undefined && !root.availableModels.has(modelCapability(model)))
52
+ throw new WorkflowError(resumed ? "RESUME_INCOMPATIBLE" : "UNKNOWN_MODEL", `Required model is unavailable: ${modelCapability(model)}`);
53
+ const tools = validateSelectorList(configuration.tools, "configuration", "tools", "INVALID_METADATA", false);
54
+ if (tools === undefined)
55
+ throw new WorkflowError(code, "Prepared tools are missing");
56
+ const missing = tools.find((tool) => !capabilities.tools.includes(tool));
57
+ if (missing)
58
+ throw new WorkflowError(resumed ? "RESUME_INCOMPATIBLE" : "UNKNOWN_TOOL", `Required tool is unavailable: ${missing}`);
59
+ const excludeTools = configuration.excludeTools;
60
+ try {
61
+ validateAgentOptions({ excludeTools });
62
+ }
63
+ catch {
64
+ throw new WorkflowError(code, "Invalid prepared excludeTools");
65
+ }
66
+ if (!Array.isArray(excludeTools) || tools.some((tool) => excludeTools.includes(tool)))
67
+ throw new WorkflowError(code, "Prepared excludeTools contradict the prepared tools");
68
+ validateSelectorList(configuration.skills, "configuration", "skills", "INVALID_METADATA", false);
69
+ validateSelectorList(configuration.extensions, "configuration", "extensions", "INVALID_METADATA", false);
70
+ if (resumed)
71
+ for (const key of ["skills", "extensions"]) {
72
+ const selectors = configuration[key];
73
+ const selected = selectors.slice(selectors.lastIndexOf("!*") + 1);
74
+ const missing = selected.find((name) => !capabilities[key].includes(name));
75
+ if (missing)
76
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Required ${key} capability is unavailable: ${missing}`);
77
+ }
78
+ validateContextFileScopes(configuration.contextFiles, "configuration");
79
+ validateWorkflowExtensionSettings(configuration.settings, "configuration", "INVALID_METADATA");
80
+ root.validateExtensionSettings?.(configuration.settings, { source: "effective", cwd, projectTrusted: root.projectTrusted ?? false });
81
+ return deepFreeze(structuredClone(configuration));
82
+ }
27
83
  function accounting(stats) {
28
84
  return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
29
85
  }
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
- ]);
86
+ 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"];
87
+ function isWorkflowHostEntry(path) {
88
+ const entryPath = canonicalPath(path);
89
+ let packageRoot = dirname(entryPath);
90
+ while (packageRoot !== dirname(packageRoot)) {
91
+ const packageMetadataPath = join(packageRoot, "package.json");
92
+ if (existsSync(packageMetadataPath)) {
93
+ try {
94
+ const metadata = JSON.parse(readFileSync(packageMetadataPath, "utf8"));
95
+ if (typeof metadata !== "object" || metadata === null || !("name" in metadata) || !isCorePackageName(metadata.name))
96
+ return false;
97
+ return WORKFLOW_HOST_ENTRY_PATHS.some((entry) => canonicalPath(join(packageRoot, entry)) === entryPath);
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ packageRoot = dirname(packageRoot);
104
+ }
105
+ return false;
106
+ }
40
107
  const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
41
108
  function workflowSystemPromptPath(cwd, agentDir, projectTrusted) {
42
109
  const projectPath = join(cwd, ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md");
@@ -71,34 +138,41 @@ function loadPiPackageDirectory() {
71
138
  }
72
139
  }).catch((error) => ({ error: `could not load @earendil-works/pi-coding-agent package metadata: ${errorText(error)}` }));
73
140
  }
74
- let nativePromptTemplateModule;
75
- async function loadNativePromptTemplateModule() {
141
+ const nativeCoreModules = new Map();
142
+ /** Pi does not export these core modules, so inspection loads them from the installed package. */
143
+ async function loadNativeCoreModule(file, label) {
76
144
  const packageDirectory = await loadPiPackageDirectory();
77
- let promptTemplatePath;
145
+ let modulePath;
78
146
  if (packageDirectory.directory) {
79
- const candidate = resolve(packageDirectory.directory, "dist/core/prompt-templates.js");
147
+ const candidate = resolve(packageDirectory.directory, "dist/core", file);
80
148
  if (existsSync(candidate))
81
- promptTemplatePath = candidate;
149
+ modulePath = candidate;
82
150
  }
83
- promptTemplatePath ??= resolve(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "core/prompt-templates.js");
151
+ modulePath ??= resolve(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "core", file);
84
152
  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 }); });
153
+ let loaded = nativeCoreModules.get(file);
154
+ if (!loaded) {
155
+ loaded = import(modulePath).catch((error) => { throw new Error(`Could not load Pi ${label}.${packageDirectoryError}`, { cause: error }); });
156
+ nativeCoreModules.set(file, loaded);
157
+ }
158
+ return loaded;
86
159
  }
87
160
  async function expandPromptTemplateForInspection(text, templates) {
88
- return (await loadNativePromptTemplateModule()).expandPromptTemplate(text, templates);
161
+ return (await loadNativeCoreModule("prompt-templates.js", "prompt templates")).expandPromptTemplate(text, templates);
162
+ }
163
+ async function buildSystemPromptForInspection(options) {
164
+ return (await loadNativeCoreModule("system-prompt.js", "system prompt builder")).buildSystemPrompt(options);
89
165
  }
90
166
  async function preparePiPrompt(native, text) {
91
167
  const session = native;
92
168
  const diagnostics = [];
93
169
  const runner = session._extensionRunner;
94
- const baseSystemPrompt = typeof session._baseSystemPrompt === "string" ? session._baseSystemPrompt : session.systemPrompt ?? "";
170
+ const baseSystemPrompt = session.systemPrompt ?? "";
95
171
  const baseOptions = session._baseSystemPromptOptions;
96
172
  const expandSkillCommand = session._expandSkillCommand;
97
173
  const templates = session.promptTemplates;
98
174
  if (!runner)
99
175
  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
176
  if (baseOptions === undefined)
103
177
  diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt options are missing", source: "Pi session" });
104
178
  if (typeof expandSkillCommand !== "function")
@@ -129,9 +203,11 @@ async function preparePiPrompt(native, text) {
129
203
  diagnostics.push({ type: "error", message: `Pi prompt template expansion is unavailable: ${errorText(error)}`, source: "Pi session" });
130
204
  }
131
205
  }
132
- const result = !inputHandled && runner && baseOptions !== undefined ? await runner.emitBeforeAgentStart(expandedPrompt, undefined, baseSystemPrompt, baseOptions) : undefined;
206
+ const result = !inputHandled && runner && baseOptions !== undefined ? await runner.emitBeforeAgentStart(expandedPrompt, undefined, baseOptions) : undefined;
133
207
  const prepared = result;
134
- return { prompt: text, expandedPrompt, systemPrompt: typeof prepared?.systemPrompt === "string" ? prepared.systemPrompt : baseSystemPrompt, systemPromptOptions: baseOptions, messages: prepared?.messages ?? [], inputHandled, diagnostics };
208
+ const systemPromptOptions = prepared?.systemPromptOptions ?? baseOptions;
209
+ const systemPrompt = prepared?.systemPromptOptions === undefined ? baseSystemPrompt : await buildSystemPromptForInspection(prepared.systemPromptOptions);
210
+ return { prompt: text, expandedPrompt, systemPrompt, systemPromptOptions, messages: prepared?.messages ?? [], inputHandled, diagnostics };
135
211
  }
136
212
  finally {
137
213
  unsubscribe?.();
@@ -175,6 +251,64 @@ export function flushExtensionProviders(resourceLoader, modelRuntime) {
175
251
  runtime.pendingNativeProviderRegistrations = [];
176
252
  return failures;
177
253
  }
254
+ /**
255
+ * The CLI adds Pi's built-in extensions itself; SDK sessions only get them from these public
256
+ * factories (docs/sdk.md). As `builtin` entries they obey `-builtin:<name>` settings, and like the
257
+ * CLI's they are replaceable by an extension that registers the same tools or commands.
258
+ * NOTE: Pi does not export the built-in llama.cpp extension, so workflow agents cannot use models it
259
+ * provides. Declare the server as a provider in models.json instead.
260
+ */
261
+ function builtinExtensions() {
262
+ return [
263
+ { name: "codemode", factory: createCodemodeExtension(), replaceable: true, builtin: true },
264
+ { name: "tool-search", factory: createToolSearchExtension(), replaceable: true, builtin: true },
265
+ { name: "mcp", factory: createMcpExtension(), replaceable: true, builtin: true },
266
+ ];
267
+ }
268
+ const BUILTIN_EXTENSION_PREFIX = "builtin:";
269
+ function isLlamaModel(model) { return model.provider === "llama.cpp"; }
270
+ /**
271
+ * Pi only extends resources with paths that loaded extensions return from resources_discover. Recording them, normalized as
272
+ * Pi does, lets selection tell those skills from the static inventory. Like Pi's own source attribution, the record belongs
273
+ * to one resource generation: reload() starts a new one, so withdrawn contributions stop counting as discovered.
274
+ */
275
+ class WorkflowResourceLoader extends DefaultResourceLoader {
276
+ loaderCwd;
277
+ extensionSkillPaths = [];
278
+ constructor(loaderCwd, options) {
279
+ super(options);
280
+ this.loaderCwd = loaderCwd;
281
+ }
282
+ async reload(...args) {
283
+ this.extensionSkillPaths.length = 0;
284
+ await super.reload(...args);
285
+ }
286
+ extendResources(paths) {
287
+ this.extensionSkillPaths.push(...(paths.skillPaths ?? []).map(({ path }) => piResourcePath(path, this.loaderCwd)));
288
+ super.extendResources(paths);
289
+ }
290
+ }
291
+ /**
292
+ * Prepared selectors end with a frozen segment: `!*` followed by the static inventory. Extension-discovered skills keep every
293
+ * other selector, including negations setup hooks inserted anywhere (a terminal `!*` too), but not that frozen restriction.
294
+ * Setup can only insert negations, so the inventory names are matched from the right and the boundary is the nearest `!*`
295
+ * before them; any `!*` with only negations up to the inventory selects the same skills.
296
+ */
297
+ function discoveredSkillSelectors(selectors, inventory) {
298
+ if (selectors === undefined || inventory === undefined)
299
+ return selectors;
300
+ const frozen = new Set();
301
+ let index = selectors.length;
302
+ for (const name of [...inventory].reverse().concat("!*")) {
303
+ do
304
+ index -= 1;
305
+ while (index >= 0 && selectors[index] !== name);
306
+ if (index < 0)
307
+ return selectors;
308
+ frozen.add(index);
309
+ }
310
+ return selectors.filter((_selector, position) => !frozen.has(position));
311
+ }
178
312
  async function createLocalPiSessionHandle(input, sessionStartEvent) {
179
313
  const agentDir = input.agentDir ?? getAgentDir();
180
314
  const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
@@ -188,33 +322,34 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
188
322
  const tools = [...new Set([...input.tools, ...customTools.map(({ name }) => name)])];
189
323
  let settingsManager;
190
324
  let resourceLoader;
325
+ const builtins = builtinExtensions();
326
+ const builtinNames = builtins.flatMap((extension) => typeof extension === "function" ? [] : [extension.name]);
327
+ const extensionFactories = [...builtins, ...(input.extensionFactories ?? [])];
191
328
  const policy = input.resourcePolicy;
192
329
  if (policy) {
193
330
  settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: false });
194
331
  settingsManager.setProjectTrusted(policy.projectTrusted);
195
- const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
332
+ const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
196
333
  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)))];
334
+ const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => extensionIdentity(path)))];
198
335
  const selectorSources = policy.selectorSources;
199
- const selection = resolveRole(undefined, {
200
- cwd: input.cwd,
201
- selectorSources,
202
- resources: { extensions: discoveredExtensions },
203
- });
204
- const selectedExtensions = selection.selectedExtensions ?? [];
205
- const extensionPaths = selectedExtensions.filter((path) => !WORKFLOW_HOST_ENTRIES.has(path));
336
+ const selectedExtensions = selectResourcesByLayers(resourceSelectorLayers(selectorSources, "extensions"), discoveredExtensions).filter((path) => policy.capabilities === undefined || policy.capabilities.extensions.includes(path));
337
+ const extensionPaths = selectedExtensions.filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path));
206
338
  policy.selectedExtensions = selectedExtensions;
207
- policy.unmatchedExtensions = selection.unmatchedExtensions ?? [];
339
+ policy.unmatchedExtensions = unmatchedResourcePatterns(policy.effective.extensions, discoveredExtensions);
208
340
  const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
209
- const updateSkillMatches = (skills) => {
341
+ const updateSkillMatches = (skills, extensionSkillPaths) => {
210
342
  const names = [...new Set(skills.map(({ name }) => name))];
211
- const selected = resolveRole(undefined, { cwd: input.cwd, selectorSources, resources: { skills: names } });
212
- const selectedSkills = selected.selectedSkills ?? [];
343
+ const discovered = new Set(skills.filter(({ filePath }) => extensionSkillPaths.some((path) => piResourceContains(path, filePath))).map(({ name }) => name));
344
+ const [global, project, call] = resourceSelectorLayers(selectorSources, "skills");
345
+ const selectedStatic = selectResourcesByLayers([global, project, call], names.filter((name) => !discovered.has(name))).filter((name) => policy.capabilities === undefined || policy.capabilities.skills.includes(name));
346
+ const selectedDiscovered = selectResourcesByLayers([discoveredSkillSelectors(global, policy.capabilities?.skills), project, call], [...discovered]).filter((name) => policy.parentSkills === undefined || policy.parentSkills.includes(name));
347
+ const selectedSkills = [...selectedStatic, ...selectedDiscovered];
213
348
  policy.selectedSkills = selectedSkills;
214
- policy.unmatchedSkills = selected.unmatchedSkills ?? [];
349
+ policy.unmatchedSkills = unmatchedResourcePatterns(policy.effective.skills, names);
215
350
  return new Set(names.filter((name) => !selectedSkills.includes(name)));
216
351
  };
217
- resourceLoader = new DefaultResourceLoader({
352
+ const workflowLoader = new WorkflowResourceLoader(input.cwd, {
218
353
  cwd: input.cwd,
219
354
  agentDir,
220
355
  settingsManager,
@@ -222,23 +357,24 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
222
357
  additionalExtensionPaths: extensionPaths,
223
358
  noSkills: true,
224
359
  additionalSkillPaths: [...new Set([...skillPaths, ...(input.additionalSkillPaths ?? [])])],
225
- ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
360
+ extensionFactories,
226
361
  ...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}),
227
362
  skillsOverride: (base) => {
228
- const disabledSkills = updateSkillMatches(base.skills);
363
+ const disabledSkills = updateSkillMatches(base.skills, workflowLoader.extensionSkillPaths);
229
364
  return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
230
365
  },
231
366
  ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
232
367
  ...systemPromptOptions,
233
368
  });
369
+ resourceLoader = workflowLoader;
234
370
  await resourceLoader.reload();
235
371
  }
236
372
  else {
237
373
  settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: true });
238
- const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
374
+ const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager, builtinExtensions: builtinNames });
239
375
  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 ?? ""] } : {}) });
376
+ const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled }) => enabled).map(({ path }) => extensionIdentity(path)).filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path)))];
377
+ 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
378
  await resourceLoader.reload();
243
379
  }
244
380
  const providerFailures = flushExtensionProviders(resourceLoader, modelRuntime);
@@ -249,11 +385,11 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
249
385
  // missing, so the failure travels with the complaint rather than being
250
386
  // swallowed — otherwise a misconfigured gateway reads as a typo in a model
251
387
  // name.
252
- const because = providerFailures.length > 0 ? ` (provider registration failed — ${providerFailures.join("; ")})` : "";
388
+ 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
389
  throw new WorkflowError("UNKNOWN_MODEL", `Unknown model: ${input.model.provider}/${input.model.model}${because}`);
254
390
  }
255
391
  const effectiveSessionStartEvent = input.settings === undefined && sessionStartEvent === undefined ? undefined : { ...(sessionStartEvent ?? { type: "session_start", reason: "startup" }), settings: input.settings ?? Object.freeze({}) };
256
- const { session } = await createAgentSession({ ...(input.options ?? {}), cwd: input.cwd, agentDir, modelRuntime, model, settingsManager, ...(input.model.thinking ? { thinkingLevel: input.model.thinking } : {}), tools, ...(customTools.length ? { customTools } : {}), ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), resourceLoader, ...(effectiveSessionStartEvent ? { sessionStartEvent: effectiveSessionStartEvent } : {}), sessionManager: manager });
392
+ const { session } = await createAgentSession({ cwd: input.cwd, agentDir, modelRuntime, model, settingsManager, ...(input.model.thinking ? { thinkingLevel: input.model.thinking } : {}), tools, ...(customTools.length ? { customTools } : {}), ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), resourceLoader, ...(effectiveSessionStartEvent ? { sessionStartEvent: effectiveSessionStartEvent } : {}), sessionManager: manager });
257
393
  const nativeDispose = session.dispose.bind(session);
258
394
  let disposal;
259
395
  const shutdown = (reason, targetSessionFile) => {
@@ -278,13 +414,13 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
278
414
  await shutdown("quit").catch(() => undefined);
279
415
  throw error;
280
416
  }
281
- const resourcePaths = { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) => canonicalPath(resolvedPath)), skills: resourceLoader.getSkills().skills.map(({ filePath }) => canonicalPath(filePath)) };
417
+ const resourcePaths = { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) => extensionIdentity(resolvedPath)), skills: resourceLoader.getSkills().skills.map(({ filePath }) => canonicalPath(filePath)) };
282
418
  const resourceInspection = () => {
283
419
  const extensions = resourceLoader.getExtensions();
284
420
  const skills = resourceLoader.getSkills();
285
421
  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
422
  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" } : {}) };
423
+ 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
424
  };
289
425
  const managedSession = Object.assign(session, {
290
426
  getLeafId: () => manager.getLeafId(),
@@ -623,6 +759,11 @@ export async function createLocalWorkflowAgentSession(prepared, context) {
623
759
  return session;
624
760
  }
625
761
  export const localAgentTransport = Object.freeze({ id: "local", createSession: createLocalWorkflowAgentSession });
762
+ /** Like Pi's excludeTools, exclusions name tools exactly and apply to every tool the session would get, custom tools included. */
763
+ function excludedToolNames(options) {
764
+ const own = options.agentOptions?.excludeTools;
765
+ return new Set([...(options.inheritedExcludeTools ?? []), ...(Array.isArray(own) ? own.filter((tool) => typeof tool === "string") : [])]);
766
+ }
626
767
  function changedOption(options, baseline, key) { return JSON.stringify(options[key]) !== JSON.stringify(baseline[key]); }
627
768
  function isChildAgentToolParams(value) {
628
769
  if (!jsonObject(value) || typeof value.prompt !== "string" || typeof value.label !== "string")
@@ -635,8 +776,6 @@ function isChildAgentToolParams(value) {
635
776
  return false;
636
777
  if (value.model !== undefined && typeof value.model !== "string")
637
778
  return false;
638
- if (value.role !== undefined && (typeof value.role !== "string" || !value.role.trim()))
639
- return false;
640
779
  if (value.contextFiles !== undefined && (!Array.isArray(value.contextFiles) || !value.contextFiles.every(isContextFileScope)))
641
780
  return false;
642
781
  if (value.outputSchema !== undefined && !jsonObject(value.outputSchema))
@@ -662,7 +801,7 @@ function resourcePolicySummary(policy, tools = policy.selectedTools ?? []) {
662
801
  }
663
802
  const RESOURCE_SELECTOR_KEYS = ["skills", "extensions", "tools"];
664
803
  function resourceSelectorLayers(sources, key) {
665
- return [sources.global[key], sources.project[key], sources.role?.[key], sources.call?.[key]];
804
+ return [sources.global[key], sources.project[key], sources.call?.[key]];
666
805
  }
667
806
  function selectorListWidened(ceiling, candidate) {
668
807
  let candidateIndex = 0;
@@ -735,7 +874,7 @@ function appendResourcePolicyNarrowing(ceiling, candidate) {
735
874
  };
736
875
  }
737
876
  function packageRoot(start) {
738
- let current = dirname(realpathSync(start));
877
+ let current = dirname(canonicalPath(start));
739
878
  for (;;) {
740
879
  const candidates = [current, join(current, "..", "@earendil-works", "pi-coding-agent"), join(current, "..", "pi-coding-agent")];
741
880
  for (const candidate of candidates) {
@@ -795,10 +934,9 @@ async function resolvePiRuntime() {
795
934
  }
796
935
  }
797
936
  const piRuntimeResolution = resolvePiRuntime();
798
- function workflowAgentPrompt(task, options, attempt, previousError) {
799
- const basePrompt = [`Workflow: ${options.workflowName}`, `Agent: ${options.label}`, options.phase ? `Phase: ${options.phase}` : "", options.parent ? `Parent: ${options.parent}` : "", "You own this task and any direct child agents you create. Return child results to your parent; do not leave descendants running.", options.schema ? "Call workflow_result exactly once with the final result matching its schema. Do not return prose as the final result." : "Call workflow_result exactly once with { result: \"final response\" } to submit the final response. Do not return prose as the final result.", attempt > 1 ? `Retry attempt ${String(attempt)}. Previous state: ${options.retryState ?? previousError ?? "failed attempt"}` : ""].filter(Boolean).join("\n");
800
- const instruction = options.budget?.instruction();
801
- return `${basePrompt}\n\nTask:\n${task}${instruction ? `\n\n${instruction}` : ""}`;
937
+ /** Transient per-attempt control text. It is queued as a separate steered message after the task, so the system prompt stays stable and the task message stays exact. */
938
+ function attemptControlText(options, attempt, previousError) {
939
+ return [attempt > 1 ? `Retry attempt ${String(attempt)}. Previous state: ${options.retryState ?? previousError ?? "failed attempt"}` : "", options.budget?.instruction() ?? ""].filter(Boolean).join("\n");
802
940
  }
803
941
  async function preparedAgentSession(input, initialPrompt) {
804
942
  const { runtime: piRuntime, error: piRuntimeError } = await piRuntimeResolution;
@@ -821,47 +959,27 @@ async function attemptSessionInput(source, attempt) {
821
959
  await copyFile(source, target);
822
960
  return target;
823
961
  }
824
- async function prepareAgentSetup(root, transport, task, options, resolved, cwd, attempt, signal, customTools, resultTool, inspection = false, previousError) {
962
+ async function prepareAgentSetup(root, transport, task, options, resolved, cwd, attempt, signal, customTools, resultTool, inspection = false) {
825
963
  const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
826
964
  const baselineOptions = structuredClone(options.agentOptions ?? {});
827
965
  const baseResourcePolicy = await root.agentResourcePolicy?.();
828
- const roleName = typeof options.role === "string" ? options.role : undefined;
829
- const roleDefinition = roleName ? root.agentDefinitions?.[roleName] : undefined;
830
- const roleSelectors = roleDefinition ? {
831
- ...(roleDefinition.skills === undefined ? {} : { skills: roleDefinition.skills }),
832
- ...(roleDefinition.extensions === undefined ? {} : { extensions: roleDefinition.extensions.map((selector) => canonicalExtensionSelector(selector, cwd)) }),
833
- ...(roleDefinition.tools === undefined ? {} : { tools: roleDefinition.tools }),
834
- } : undefined;
835
- const selectorValue = (key, fallback) => {
836
- const value = baselineOptions[key];
837
- if (Array.isArray(value) && value.every((entry) => typeof entry === "string"))
838
- return value;
839
- return options.agentOptions === undefined ? fallback : undefined;
966
+ const configuration = options.configuration;
967
+ const resourcePolicy = {
968
+ ...(baseResourcePolicy ?? { globalSettingsPath: "", projectSettingsPath: "", projectTrusted: root.projectTrusted ?? false, global: { skills: [], extensions: [] }, project: { skills: [], extensions: [] }, unmatchedSkills: [], unmatchedExtensions: [] }),
969
+ projectTrusted: configuration?.projectTrusted ?? root.projectTrusted ?? baseResourcePolicy?.projectTrusted ?? false,
970
+ effective: { skills: configuration?.skills ?? [], extensions: configuration?.extensions ?? [], tools: resolved.tools },
971
+ selectorSources: { global: { skills: configuration?.skills ?? [], extensions: configuration?.extensions ?? [], tools: resolved.tools }, project: {} },
972
+ ...(options.capabilities ? { capabilities: options.capabilities } : {}),
973
+ ...(options.parentSkills ? { parentSkills: [...options.parentSkills] } : {}),
974
+ selectedTools: resolved.tools,
840
975
  };
841
- const rawCallSkills = selectorValue("skills", options.skills);
842
- const rawCallExtensions = selectorValue("extensions", options.extensions)?.map((selector) => canonicalExtensionSelector(selector, cwd));
843
- const rawCallTools = selectorValue("tools", options.tools);
844
- const callSelectors = { ...(rawCallSkills === undefined ? {} : { skills: rawCallSkills }), ...(rawCallExtensions === undefined ? {} : { extensions: rawCallExtensions }), ...(rawCallTools === undefined ? {} : { tools: rawCallTools }) };
845
- const resourcePolicy = baseResourcePolicy ? {
846
- ...baseResourcePolicy,
847
- effective: {
848
- skills: [...baseResourcePolicy.effective.skills, ...(roleSelectors?.skills ?? []), ...(rawCallSkills ?? [])],
849
- extensions: [...baseResourcePolicy.effective.extensions, ...(roleSelectors?.extensions ?? []), ...(rawCallExtensions ?? [])],
850
- ...(baseResourcePolicy.effective.tools === undefined && roleSelectors?.tools === undefined && rawCallTools === undefined ? {} : { tools: [...(baseResourcePolicy.effective.tools ?? []), ...(roleSelectors?.tools ?? []), ...(rawCallTools ?? [])] }),
851
- },
852
- selectorSources: { global: baseResourcePolicy.selectorSources.global, project: baseResourcePolicy.selectorSources.project, ...(roleSelectors ? { role: roleSelectors } : {}), ...(Object.keys(callSelectors).length ? { call: callSelectors } : {}) },
853
- } : undefined;
854
- if (resourcePolicy) {
855
- resourcePolicy.selectedTools = resolved.tools;
856
- resourcePolicy.unmatchedTools = unmatchedResourcePatterns(resourceSelectorLayers(resourcePolicy.selectorSources, "tools").flatMap((layer) => layer ?? []), [...root.tools]);
857
- }
858
- const resourcePolicyCeiling = resourcePolicy ? structuredClone(resourcePolicy) : undefined;
976
+ const resourcePolicyCeiling = structuredClone(resourcePolicy);
859
977
  const sessionPath = options.sessionPath === undefined || inspection ? options.sessionPath : await attemptSessionInput(options.sessionPath, attempt);
860
- const extensionSettings = mergeWorkflowExtensionSettings(options.inheritedExtensionSettings ?? root.extensionSettings, roleDefinition?.extensionSettings) ?? Object.freeze({});
861
- root.validateExtensionSettings?.(extensionSettings, { source: roleName === undefined ? "effective" : "role", cwd, projectTrusted: resourcePolicy?.projectTrusted ?? true, ...((root.extensionSettingsPath ?? root.settingsPath) ? { settingsPath: root.extensionSettingsPath ?? root.settingsPath } : {}), ...(roleName === undefined ? {} : { role: roleName }) });
978
+ const extensionSettings = configuration?.settings ?? options.inheritedExtensionSettings ?? root.extensionSettings ?? Object.freeze({});
979
+ root.validateExtensionSettings?.(extensionSettings, { source: "effective", cwd, projectTrusted: resourcePolicy.projectTrusted, ...((root.extensionSettingsPath ?? root.settingsPath) ? { settingsPath: root.extensionSettingsPath ?? root.settingsPath } : {}) });
862
980
  if (options.agentNodeId !== undefined)
863
981
  root.onAgentSettings?.(options.agentNodeId, extensionSettings);
864
- const sessionInput = { cwd, model: { ...resolved.model }, tools: [...resolved.tools], sessionLabel: `${options.workflowName}:${options.label}:attempt-${String(attempt)}`, ...(sessionPath ? { sessionPath } : {}), ...(root.agentDir ? { agentDir: root.agentDir } : {}), ...(root.additionalSkillPaths?.length ? { additionalSkillPaths: [...root.additionalSkillPaths] } : {}), ...(resolved.contextFiles === undefined ? {} : { contextFiles: [...resolved.contextFiles] }), ...(customTools.length ? { customTools: [...customTools] } : {}), ...(resultTool ? { resultTool } : {}), ...(resolved.systemPrompt !== undefined ? { systemPrompt: resolved.systemPrompt } : {}), systemPromptAppend: resolved.systemPromptAppend, ...(resourcePolicy ? { resourcePolicy } : {}), settings: extensionSettings, options: structuredClone(baselineOptions) };
982
+ const sessionInput = { cwd, model: { ...resolved.model }, tools: [...resolved.tools], sessionLabel: `${options.workflowName}:${options.label}:attempt-${String(attempt)}`, ...(sessionPath ? { sessionPath } : {}), ...(root.agentDir ? { agentDir: root.agentDir } : {}), ...(root.additionalSkillPaths?.length ? { additionalSkillPaths: [...root.additionalSkillPaths] } : {}), ...(resolved.contextFiles === undefined ? {} : { contextFiles: [...resolved.contextFiles] }), ...(customTools.length ? { customTools: [...customTools] } : {}), ...(resultTool ? { resultTool } : {}), ...(resolved.systemPrompt !== undefined ? { systemPrompt: resolved.systemPrompt } : {}), systemPromptAppend: resolved.systemPromptAppend, resourcePolicy, settings: extensionSettings, options: structuredClone(baselineOptions) };
865
983
  const setup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, prepared: await preparedAgentSession(sessionInput, task), transport };
866
984
  const base = fallbackSetupContext(root, options, setupSignal);
867
985
  const context = Object.freeze({ run: base.run, identity: base.identity, attempt, signal: setupSignal, settings: extensionSettings, ...(base.tuiIndex === undefined ? {} : { tuiIndex: base.tuiIndex }), ...(base.tuiLabel === undefined ? {} : { tuiLabel: base.tuiLabel }), ...(inspection ? { mode: "inspection" } : {}) });
@@ -885,30 +1003,69 @@ async function prepareAgentSetup(root, transport, task, options, resolved, cwd,
885
1003
  if (resourcePolicyWidened(resourcePolicyCeiling, setup.sessionInput.resourcePolicy))
886
1004
  throw new WorkflowError("INVALID_METADATA", "Agent setup widened the prepared resource policy");
887
1005
  appendResourcePolicyNarrowing(resourcePolicyCeiling, setup.sessionInput.resourcePolicy);
1006
+ if (setup.sessionInput.resourcePolicy) {
1007
+ const policy = setup.sessionInput.resourcePolicy;
1008
+ policy.effective.extensions = policy.effective.extensions.map((selector) => canonicalExtensionSelector(selector, cwd));
1009
+ for (const layer of [policy.selectorSources.global, policy.selectorSources.project, policy.selectorSources.call])
1010
+ if (layer?.extensions !== undefined)
1011
+ layer.extensions = layer.extensions.map((selector) => canonicalExtensionSelector(selector, cwd));
1012
+ if (resourcePolicyCeiling.parentSkills === undefined)
1013
+ Reflect.deleteProperty(policy, "parentSkills");
1014
+ else
1015
+ policy.parentSkills = [...resourcePolicyCeiling.parentSkills];
1016
+ }
888
1017
  setup.sessionInput.options = setup.options;
889
1018
  if (changedOption(setup.options, baselineOptions, "model") && typeof setup.options.model === "string")
890
1019
  setup.sessionInput.model = parseModel(setup.options.model, setup.sessionInput.model, root.modelAliases, root.knownModels ?? root.availableModels, root.settingsPath);
891
1020
  if (changedOption(setup.options, baselineOptions, "tools") && Array.isArray(setup.options.tools) && setup.options.tools.every((tool) => typeof tool === "string"))
892
1021
  setup.sessionInput.tools = [...setup.options.tools];
893
- if (changedOption(setup.options, baselineOptions, "cwd") && typeof setup.options.cwd === "string")
894
- setup.sessionInput.cwd = setup.options.cwd;
1022
+ // Pi derives trust, settings and .pi resources from cwd, so neither the session input nor a cwd option may move it.
1023
+ if (changedOption(setup.options, baselineOptions, "cwd") || !sameFilesystemPath(cwd, setup.sessionInput.cwd))
1024
+ throw new WorkflowError("INVALID_METADATA", "Agent setup cannot change cwd");
1025
+ if (!root.availableModels?.has(modelCapability(setup.sessionInput.model)) && root.availableModels !== undefined)
1026
+ throw new WorkflowError("UNKNOWN_MODEL", "Agent setup selected an unavailable model");
1027
+ // A custom tool must not reintroduce, by name, a root tool the prepared policy left out (excludeTools, parent ceilings, selectors).
1028
+ const shadowed = (setup.sessionInput.customTools ?? []).map(({ name }) => name).find((name) => root.tools.has(name) && !resolved.tools.includes(name));
1029
+ if (shadowed)
1030
+ throw new WorkflowError("UNKNOWN_TOOL", `Custom tool reintroduces a tool outside the prepared agent policy: ${shadowed}`);
1031
+ // Like Pi's excludeTools, own and inherited exclusions also remove custom tools of those names, including ones setup hooks
1032
+ // add, before materialization. A name that matches no root or custom tool excludes nothing and only warns. The names are
1033
+ // the frozen configuration's, so re-evaluated options of a later turn, retry or resume cannot change them.
1034
+ const excludedTools = new Set(configuration?.excludeTools ?? excludedToolNames(options));
1035
+ const excludedCustomTools = (setup.sessionInput.customTools ?? []).filter(({ name }) => excludedTools.has(name)).map(({ name }) => name);
1036
+ if (excludedCustomTools.length) {
1037
+ setup.sessionInput.customTools = setup.sessionInput.customTools?.filter(({ name }) => !excludedTools.has(name)) ?? [];
1038
+ // Hooks may also list their custom tool by name; root names stay subject to the policy checks below.
1039
+ setup.sessionInput.tools = setup.sessionInput.tools.filter((tool) => root.tools.has(tool) || !excludedCustomTools.includes(tool));
1040
+ }
1041
+ for (const tool of excludedTools)
1042
+ if (!options.inheritedExcludeTools?.includes(tool) && !root.tools.has(tool) && !excludedCustomTools.includes(tool))
1043
+ root.onResourceWarning?.(`excludeTools names no root or custom tool of this agent: ${tool}`);
895
1044
  const customToolNames = new Set([...(setup.sessionInput.customTools ?? []).map(({ name }) => name), ...(setup.sessionInput.resultTool ? [setup.sessionInput.resultTool.name] : [])]);
896
1045
  const widened = setup.sessionInput.tools.find((tool) => !resolved.tools.includes(tool) && !customToolNames.has(tool));
897
1046
  const outsideTool = widened ?? setup.sessionInput.tools.find((tool) => !root.tools.has(tool) && !customToolNames.has(tool));
898
1047
  if (outsideTool)
899
1048
  throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${outsideTool}`);
1049
+ if (setup.sessionInput.resourcePolicy) {
1050
+ const policy = setup.sessionInput.resourcePolicy;
1051
+ setup.sessionInput.tools = selectResourcesByLayers(resourceSelectorLayers(policy.selectorSources, "tools"), setup.sessionInput.tools);
1052
+ if (policy.projectTrusted !== resourcePolicyCeiling.projectTrusted)
1053
+ policy.capabilities = await authorizedResources({ ...root, projectTrusted: policy.projectTrusted }, cwd, options);
1054
+ }
1055
+ if (!setup.sessionInput.tools.includes("agent"))
1056
+ setup.sessionInput.customTools = setup.sessionInput.customTools?.filter(({ name }) => name !== "agent" && name !== "get_subagent_result" && name !== "steer_subagent") ?? [];
900
1057
  }
901
1058
  catch (error) {
902
1059
  setup.prepared = await preparedAgentSession(setup.sessionInput, task);
903
1060
  return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error } };
904
1061
  }
905
- setup.prepared = await preparedAgentSession(setup.sessionInput, inspection ? task : workflowAgentPrompt(setup.prompt, options, attempt, previousError));
1062
+ setup.prepared = await preparedAgentSession(setup.sessionInput, inspection ? task : setup.prompt);
906
1063
  return { setup, summary: agentSetupSummary(setup, hookNames) };
907
1064
  }
908
1065
  export async function prepareAgentSetupForInspection(root, task, options, transport) {
909
1066
  const executor = new WorkflowAgentExecutor(root);
910
- const resolved = executor.resolve(options);
911
- return prepareAgentSetup(root, transport, task, options, resolved, root.cwd, 1, root.runContext?.signal, [], undefined, true);
1067
+ const configuration = await executor.prepare(options, root.cwd, root.runContext?.signal, "inspection");
1068
+ return prepareAgentSetup(root, transport, task, { ...options, label: configuration.label ?? options.label, configuration, capabilities: { tools: configuration.tools, skills: configuration.skills.slice(configuration.skills.lastIndexOf("!*") + 1), extensions: configuration.extensions.slice(configuration.extensions.lastIndexOf("!*") + 1) } }, configuration, root.cwd, 1, root.runContext?.signal, [], undefined, true);
912
1069
  }
913
1070
  function attemptRecord(transport, attempt, session, setup, stats, result, error) {
914
1071
  return { attempt, transport, session: session.reference, ...(result === undefined ? {} : { result }), ...(error ? { error } : {}), accounting: stats, setup };
@@ -926,52 +1083,105 @@ export function getAgentAttempts(error) {
926
1083
  export class WorkflowAgentExecutor {
927
1084
  root;
928
1085
  transport;
929
- warnedRoleTools = new Set();
930
1086
  constructor(root, transport = localAgentTransport) {
931
1087
  this.root = root;
932
1088
  this.transport = transport;
933
1089
  }
934
1090
  setRunContext(runContext) { this.root.runContext = runContext; }
935
1091
  resolve(options, inheritedTools) {
936
- const roleName = typeof options.role === "string" ? options.role : undefined;
937
- const roleDefinition = roleName ? this.root.agentDefinitions?.[roleName] : undefined;
938
- if (roleName && !roleDefinition)
939
- throw new WorkflowError("UNKNOWN_AGENT_TYPE", `Unknown agent role: ${roleName}`);
940
- if (inheritedTools === undefined)
941
- for (const pattern of roleDefinition?.tools ?? []) {
942
- const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
943
- if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !this.root.tools.has(body)) {
944
- const key = `${roleName ?? "agent"}:${body}`;
945
- if (!this.warnedRoleTools.has(key)) {
946
- this.warnedRoleTools.add(key);
947
- this.root.onResourceWarning?.(`Tool not available in this session for role ${roleName ?? "agent"}: ${body}. The agent runs without it.`);
948
- }
949
- }
1092
+ if (options.configuration)
1093
+ return { ...options.configuration, ...(options.modelOverride ? { model: options.modelOverride } : {}) };
1094
+ const ceiling = inheritedTools ?? [...this.root.tools];
1095
+ for (const pattern of options.tools ?? [])
1096
+ if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !ceiling.includes(pattern))
1097
+ throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${pattern}`);
1098
+ const tools = options.effectiveTools ?? selectResourcesByLayers([this.root.resourceSelectors?.tools, options.tools], [...(inheritedTools ?? this.root.tools)]);
1099
+ const missing = tools.find((tool) => !ceiling.includes(tool));
1100
+ if (missing)
1101
+ throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${missing}`);
1102
+ const model = options.modelOverride ?? parseModel(options.model, this.root.model, this.root.modelAliases, this.root.knownModels, this.root.settingsPath);
1103
+ return { model: physicalModel(model, this.root.modelAliases, this.root.knownModels, this.root.settingsPath), tools, systemPromptAppend: "", ...(options.contextFiles === undefined ? {} : { contextFiles: options.contextFiles }) };
1104
+ }
1105
+ async prepare(options, cwd = this.root.cwd, signal = this.root.runContext?.signal ?? new AbortController().signal, mode = "execution") {
1106
+ const root = { ...this.root, projectTrusted: Boolean(this.root.projectTrusted) && options.projectTrusted !== false };
1107
+ const policy = await this.root.agentResourcePolicy?.();
1108
+ const capabilities = await authorizedResources(root, cwd, options);
1109
+ // Children are new spawns of their parent's attempt; their scheduler node keys the configuration their own retries reuse.
1110
+ const persistedKey = options.parent !== undefined ? options.agentNodeId : options.agentIdentity?.handle !== undefined ? `handle:${options.agentIdentity.handle}` : options.agentIdentity ? agentIdentityPath(options.agentIdentity) : options.agentNodeId;
1111
+ const stored = options.configuration ?? (mode === "execution" && persistedKey && this.root.runStore?.load ? (await this.root.runStore.load()).snapshot.agentConfigurations[persistedKey] : undefined);
1112
+ if (stored) {
1113
+ // Without the field, the configuration was prepared before exclusions were frozen. Re-evaluated options cannot prove
1114
+ // which exclusions it had, so it never resumes.
1115
+ if (stored.excludeTools === undefined)
1116
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Prepared agent configuration predates frozen excludeTools; relaunch the workflow");
1117
+ return validatePreparedConfiguration(root, stored, capabilities, cwd, true);
1118
+ }
1119
+ const original = deepFreeze(structuredClone(validateAgentOptions(options.agentOptions ?? {
1120
+ ...(options.model === undefined ? {} : { model: options.model }), ...(options.tools === undefined ? {} : { tools: [...options.tools] }), ...(options.skills === undefined ? {} : { skills: [...options.skills] }), ...(options.extensions === undefined ? {} : { extensions: [...options.extensions] }), ...(options.contextFiles === undefined ? {} : { contextFiles: [...options.contextFiles] }),
1121
+ })));
1122
+ const dynamicModelAliasNames = this.root.dynamicModelAliasNames;
1123
+ const defaults = deepFreeze({ model: { ...this.root.model }, modelAliases: { ...this.root.modelAliases }, ...(dynamicModelAliasNames === undefined ? {} : { dynamicModelAliasNames: [...dynamicModelAliasNames] }), selectorSources: structuredClone(policy?.selectorSources ?? { global: this.root.resourceSelectors ?? {}, project: {} }), settings: structuredClone(options.inheritedExtensionSettings ?? this.root.extensionSettings ?? {}) });
1124
+ const context = Object.freeze({ options: original, cwd, projectCwd: this.root.cwd, agentDir: this.root.agentDir ?? getAgentDir(), projectTrusted: root.projectTrusted, defaults, capabilities: deepFreeze(structuredClone(capabilities)), knownModels: new Set(this.root.knownModels), availableModels: new Set(this.root.availableModels), signal, mode });
1125
+ const configuration = {
1126
+ ...(typeof original.label === "string" ? { label: original.label } : {}), ...(typeof original.model === "string" ? { model: original.model } : {}),
1127
+ tools: [...(policy?.effective.tools ?? this.root.resourceSelectors?.tools ?? []), ...(options.tools ?? [])], skills: [...(policy?.effective.skills ?? this.root.resourceSelectors?.skills ?? []), ...(options.skills ?? [])], extensions: [...(policy?.effective.extensions ?? this.root.resourceSelectors?.extensions ?? []), ...(options.extensions ?? []).map((selector) => canonicalExtensionSelector(selector, cwd))],
1128
+ ...(options.contextFiles === undefined ? {} : { contextFiles: [...options.contextFiles] }), systemPromptAppend: "", settings: structuredClone(defaults.settings),
1129
+ };
1130
+ for (const key of ["tools", "skills", "extensions"])
1131
+ if (Array.isArray(original[key])) {
1132
+ const selectors = validateSelectorList(original[key], "agent options", key, "INVALID_METADATA", false) ?? [];
1133
+ configuration[key] = [...(policy?.effective[key] ?? this.root.resourceSelectors?.[key] ?? []), ...selectors.map((selector) => key === "extensions" ? canonicalExtensionSelector(selector, cwd) : selector)];
950
1134
  }
951
- const resolved = resolveRole(roleName, {
952
- cwd: this.root.cwd,
953
- ...(roleDefinition === undefined ? {} : { definition: roleDefinition }),
954
- ...(this.root.agentDefinitions === undefined ? {} : { definitions: this.root.agentDefinitions }),
955
- ...(this.root.agentDir === undefined ? {} : { agentDir: this.root.agentDir }),
956
- selectorSources: { global: this.root.resourceSelectors ?? {}, project: {} },
957
- ...(this.root.modelAliases === undefined ? {} : { modelAliases: this.root.modelAliases }),
958
- ...(this.root.knownModels === undefined ? {} : { knownModels: this.root.knownModels }),
959
- ...(this.root.availableModels === undefined ? {} : { availableModels: this.root.availableModels }),
960
- ...(this.root.blockedAliases === undefined ? {} : { blockedAliases: this.root.blockedAliases }),
961
- ...(this.root.blockedAliasTargets === undefined ? {} : { blockedAliasTargets: this.root.blockedAliasTargets }),
962
- ...(this.root.settingsPath === undefined ? {} : { settingsPath: this.root.settingsPath }),
963
- rootModel: this.root.model,
964
- rootTools: this.root.tools,
965
- ...(inheritedTools === undefined ? {} : { inheritedTools }),
966
- ...(options.model === undefined ? {} : { model: options.model }),
967
- ...(options.modelOverride === undefined ? {} : { modelOverride: options.modelOverride }),
968
- ...(options.tools === undefined ? {} : { tools: options.tools }),
969
- ...(options.skills === undefined ? {} : { skills: options.skills }),
970
- ...(options.extensions === undefined ? {} : { extensions: options.extensions }),
971
- ...(options.contextFiles === undefined ? {} : { contextFiles: options.contextFiles }),
972
- ...(options.effectiveTools === undefined ? {} : { effectiveTools: options.effectiveTools }),
973
- });
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 }) };
1135
+ if (original.contextFiles !== undefined)
1136
+ configuration.contextFiles = [...(validateContextFileScopes(original.contextFiles, "agent options") ?? [])];
1137
+ const hooks = [...(this.root.agentPreparationHooks ?? [])].sort(byPriorityThenName);
1138
+ const load = this.root.extensionLoad;
1139
+ if (load?.errors.length) {
1140
+ // A failed extension may have owned an option, and its factory may have registered hooks before it failed. Only a
1141
+ // schema whose registration names a loaded extension as `source` proves ownership.
1142
+ const loaded = new Set(load.loaded.map(extensionIdentity));
1143
+ const declared = new Set(hooks.flatMap(({ optionsSchema, source }) => source !== undefined && loaded.has(extensionIdentity(source)) && object(optionsSchema?.properties) ? Object.keys(optionsSchema.properties) : []));
1144
+ const unowned = Object.keys(original).find((key) => !isCoreAgentOption(key) && !declared.has(key));
1145
+ if (unowned !== undefined)
1146
+ throw new WorkflowError("INVALID_METADATA", `Agent option ${unowned} has no owner proven to have loaded while extensions failed to load: ${load.errors.join("; ")}`);
1147
+ }
1148
+ for (const hook of hooks)
1149
+ if (hook.optionsSchema !== undefined && !Value.Check(hook.optionsSchema, original))
1150
+ throw new WorkflowError("INVALID_METADATA", `Agent options do not match preparation schema: ${hook.name}`);
1151
+ if (typeof original.model === "string" && this.root.blockedAliases?.has(original.model.split(":")[0] ?? ""))
1152
+ throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${original.model} resolved to ${this.root.blockedAliasTargets?.[original.model.split(":")[0] ?? ""] ?? "an unavailable target"}`);
1153
+ for (const hook of hooks) {
1154
+ if (signal.aborted)
1155
+ throw new WorkflowError("CANCELLED", "Agent preparation cancelled");
1156
+ await hook.prepare(configuration, context);
1157
+ }
1158
+ if (signal.aborted)
1159
+ throw new WorkflowError("CANCELLED", "Agent preparation cancelled");
1160
+ const { model: preparedModel, ...preparedOptions } = configuration;
1161
+ validateAgentOptions(preparedOptions);
1162
+ if (preparedModel !== undefined && (typeof preparedModel !== "string" || !preparedModel.trim()))
1163
+ throw new WorkflowError("INVALID_METADATA", "Prepared agent model must be a non-empty string");
1164
+ if (preparedModel && this.root.blockedAliases?.has(preparedModel.split(":")[0] ?? ""))
1165
+ throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${preparedModel} resolved to ${this.root.blockedAliasTargets?.[preparedModel.split(":")[0] ?? ""] ?? "an unavailable target"}`);
1166
+ configuration.extensions = configuration.extensions.map((selector) => canonicalExtensionSelector(selector, cwd));
1167
+ // A hook that passes on an inherited virtual `workflow/<alias>` root model gets the same physical target as no hook.
1168
+ const model = physicalModel(preparedModel === undefined ? this.root.model : resolveModelReference(preparedModel, this.root.modelAliases, this.root.knownModels, this.root.settingsPath), this.root.modelAliases, this.root.knownModels, this.root.settingsPath);
1169
+ const excludedTools = excludedToolNames({ agentOptions: original, ...(options.inheritedExcludeTools ? { inheritedExcludeTools: options.inheritedExcludeTools } : {}) });
1170
+ const tools = selectResourcesByLayers([configuration.tools], capabilities.tools).filter((tool) => !excludedTools.has(tool));
1171
+ for (const pattern of validateSelectorList(original.tools, "agent options", "tools", "INVALID_METADATA", false) ?? [])
1172
+ if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !capabilities.tools.includes(pattern))
1173
+ throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${pattern}`);
1174
+ for (const pattern of unmatchedResourcePatterns(configuration.tools, capabilities.tools))
1175
+ this.root.onResourceWarning?.(`Tool selector currently matches no authorized tool: ${pattern}`);
1176
+ const skills = selectResourcesByLayers([configuration.skills], capabilities.skills);
1177
+ const extensions = selectResourcesByLayers([configuration.extensions], capabilities.extensions);
1178
+ const concrete = validatePreparedConfiguration(root, { ...configuration, model, tools, excludeTools: [...excludedTools], skills: [...configuration.skills, "!*", ...skills], extensions: [...configuration.extensions, "!*", ...extensions], projectTrusted: context.projectTrusted }, capabilities, cwd, false);
1179
+ if (mode === "execution") {
1180
+ if (persistedKey && this.root.runStore?.saveAgentConfiguration)
1181
+ await this.root.runStore.saveAgentConfiguration(persistedKey, concrete);
1182
+ await options.onConfiguration?.(concrete);
1183
+ }
1184
+ return concrete;
975
1185
  }
976
1186
  async execute(task, options, signal, customTools = [], setSteer, beforeRetry) {
977
1187
  const executionSignal = signal ?? this.root.runContext?.signal;
@@ -979,7 +1189,7 @@ export class WorkflowAgentExecutor {
979
1189
  throw new WorkflowError("INVALID_METADATA", "retries must be a non-negative integer");
980
1190
  if (options.timeoutMs !== undefined && options.timeoutMs !== null && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0))
981
1191
  throw new WorkflowError("INVALID_METADATA", "timeoutMs must be null or a positive integer");
982
- let resolved = this.resolve(options);
1192
+ let resolved;
983
1193
  let recoveryModel;
984
1194
  let cwd;
985
1195
  if (options.parent) {
@@ -1009,6 +1219,15 @@ export class WorkflowAgentExecutor {
1009
1219
  throw new WorkflowError("INVALID_METADATA", "Only child agents or worktree scopes may provide a cwd");
1010
1220
  cwd = this.root.cwd;
1011
1221
  }
1222
+ const configuration = await this.prepare(options, cwd, executionSignal);
1223
+ const parentSkills = options.parent === undefined ? undefined : options.capabilities?.skills;
1224
+ options = { ...options, label: configuration.label ?? options.label, configuration, ...(parentSkills ? { parentSkills } : {}), capabilities: { tools: configuration.tools, skills: configuration.skills.slice(configuration.skills.lastIndexOf("!*") + 1), extensions: configuration.extensions.slice(configuration.extensions.lastIndexOf("!*") + 1) } };
1225
+ resolved = this.resolve(options);
1226
+ if (!resolved.tools.includes("agent"))
1227
+ customTools = customTools.filter(({ name }) => name !== "agent" && name !== "get_subagent_result" && name !== "steer_subagent");
1228
+ // A child's skill ceiling is what its parent session has loaded when it spawns, so withdrawn skills never stay in it.
1229
+ let publishLoadedSkills;
1230
+ customTools = customTools.map((tool) => tool.name !== "agent" ? tool : { ...tool, execute: (...args) => { publishLoadedSkills?.(); return tool.execute(...args); } });
1012
1231
  const attempts = [];
1013
1232
  let maxAttempts = (options.retries ?? 0) + 1;
1014
1233
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
@@ -1036,7 +1255,7 @@ export class WorkflowAgentExecutor {
1036
1255
  const resultSchema = Compile(options.schema ?? defaultWorkflowResultSchema);
1037
1256
  let resultAccepted = false;
1038
1257
  const resultTool = defineTool({
1039
- name: "workflow_result", label: "Workflow Result", description: "Submit the terminal workflow result", parameters: resultSchema.Type(),
1258
+ name: "workflow_result", label: "Workflow Result", description: options.schema ? "Submit the terminal workflow result. Call exactly once to finish the current task or message, with a value matching the required schema. Do not return prose as the final result." : "Submit the terminal workflow result. Call exactly once to finish the current task or message, with { result: \"final response\" }. Do not return prose as the final result.", parameters: resultSchema.Type(),
1040
1259
  async execute(_id, value) {
1041
1260
  if (!resultSchema.Check(value) || !jsonValue(value))
1042
1261
  return { content: [{ type: "text", text: "Result does not match the required schema." }], details: {}, isError: true };
@@ -1072,12 +1291,24 @@ export class WorkflowAgentExecutor {
1072
1291
  } : undefined;
1073
1292
  try {
1074
1293
  setupFailed = true;
1075
- const prepared = await prepareAgentSetup(this.root, this.transport, task, options, resolved, cwd, attempt, attemptSignal, customTools, resultTool, false, attempts.at(-1)?.error?.message);
1294
+ const prepared = await prepareAgentSetup(this.root, this.transport, task, options, resolved, cwd, attempt, attemptSignal, customTools, resultTool, false);
1076
1295
  setup = prepared.setup;
1077
1296
  const attemptSetup = prepared.setup;
1078
1297
  setupSummary = prepared.summary;
1079
1298
  if (prepared.failure)
1080
1299
  throw prepared.failure.error;
1300
+ let attemptCapabilities;
1301
+ if (options.agentNodeId !== undefined) {
1302
+ const policy = setup.prepared.resourcePolicy;
1303
+ const sources = policy?.selectorSources;
1304
+ const capabilities = policy?.capabilities ?? options.capabilities;
1305
+ attemptCapabilities = {
1306
+ tools: setup.prepared.tools,
1307
+ skills: selectResourcesByLayers(sources ? resourceSelectorLayers(sources, "skills") : [], capabilities?.skills ?? []),
1308
+ extensions: selectResourcesByLayers(sources ? resourceSelectorLayers(sources, "extensions") : [], capabilities?.extensions ?? []),
1309
+ };
1310
+ this.root.onAgentCapabilities?.(options.agentNodeId, attemptCapabilities, policy?.projectTrusted ?? false);
1311
+ }
1081
1312
  setupFailed = false;
1082
1313
  if (attemptSignal.aborted)
1083
1314
  throw new WorkflowError("CANCELLED", "Agent cancelled");
@@ -1113,6 +1344,14 @@ export class WorkflowAgentExecutor {
1113
1344
  }
1114
1345
  if (policy)
1115
1346
  setupSummary = { ...setupSummary, resourceSelectors: { ...resourcePolicySummary(policy, attemptSetup.sessionInput.tools), ...(inspection ? { skills: [...inspection.skills], extensions: [...inspection.extensions] } : {}) } };
1347
+ // The session's currently loaded skills, within its authorization, are the skill ceiling its children inherit.
1348
+ const nodeId = options.agentNodeId, selected = attemptCapabilities, live = session;
1349
+ publishLoadedSkills = nodeId === undefined || selected === undefined || live?.getResourceInspection === undefined ? undefined : () => {
1350
+ const loaded = live.getResourceInspection?.();
1351
+ if (loaded)
1352
+ this.root.onAgentCapabilities?.(nodeId, { ...selected, skills: [...loaded.skills] }, attemptSetup.prepared.resourcePolicy?.projectTrusted ?? false);
1353
+ };
1354
+ publishLoadedSkills?.();
1116
1355
  },
1117
1356
  onBeforeComplete: async () => {
1118
1357
  if (options.worktreeOwner)
@@ -1152,6 +1391,7 @@ export class WorkflowAgentExecutor {
1152
1391
  const runtimeRun = { id: transportBase.run.runId, namespaceId: transportBase.run.sessionId, workflowName: transportBase.run.workflow.name };
1153
1392
  const providerRecovery = options.providerErrorRecovery;
1154
1393
  const runtimeAgent = { id: transportBase.identity.callSite, structuralPath: [...transportBase.identity.structuralPath], ...(options.parent ? { parentId: options.parent } : {}) };
1394
+ const attemptControl = attemptControlText(options, attempt, attempts.at(-1)?.error?.message);
1155
1395
  const runtimeRequest = {
1156
1396
  task: setup.prepared.initialPrompt ?? setup.prompt,
1157
1397
  cwd: setup.prepared.cwd,
@@ -1160,6 +1400,7 @@ export class WorkflowAgentExecutor {
1160
1400
  // Pi sessions execute the prepared native definitions; the executor does not duplicate them as neutral request tools.
1161
1401
  customTools: [],
1162
1402
  ...(options.schema ? { resultSchema: options.schema } : {}),
1403
+ ...(attemptControl ? { turnControl: attemptControl } : {}),
1163
1404
  run: runtimeRun,
1164
1405
  agent: runtimeAgent,
1165
1406
  signal: attemptSignal,
@@ -1232,6 +1473,30 @@ export class WorkflowAgentExecutor {
1232
1473
  throw new WorkflowError("AGENT_FAILED", "Agent execution failed");
1233
1474
  }
1234
1475
  }
1476
+ /**
1477
+ * A child spawned by its parent's `agent` tool shares the parent's structural scope, breadcrumb and worktree, but owns its
1478
+ * call site under the parent's identity and spawn generation, with a per-label occurrence. It never carries the parent's
1479
+ * handle or turn. Children are new spawns: a retried or resumed parent spawns them again under new identities.
1480
+ */
1481
+ function childIdentity(parent, scope, label) {
1482
+ const occurrences = parent.childOccurrences ?? new Map();
1483
+ parent.childOccurrences = occurrences;
1484
+ const occurrence = (occurrences.get(label) ?? 0) + 1;
1485
+ occurrences.set(label, occurrence);
1486
+ // The parent's scheduler node number marks the spawn generation: a cold-resumed or retried parent runs as a new node.
1487
+ const generation = parent.id.slice(parent.id.lastIndexOf(":") + 1);
1488
+ return { structuralPath: [...scope.structuralPath], callSite: `child:${agentIdentityPath(scope)}/spawn:${generation}/${label}`, occurrence, ...(scope.parentBreadcrumb ? { parentBreadcrumb: scope.parentBreadcrumb } : {}), ...(scope.worktreeOwner ? { worktreeOwner: scope.worktreeOwner } : {}) };
1489
+ }
1490
+ /**
1491
+ * Foreign errors keep descriptive core codes such as UNKNOWN_MODEL. Cancellation and budget exhaustion drive node and run
1492
+ * state, so a foreign error may claim CANCELLED only for an aborted node and never BUDGET_EXHAUSTED.
1493
+ */
1494
+ function scheduledErrorCode(error, aborted) {
1495
+ if (error instanceof WorkflowError)
1496
+ return error.code;
1497
+ const code = errorCode(error);
1498
+ return code === undefined || code === "BUDGET_EXHAUSTED" || (code === "CANCELLED" && !aborted) ? "AGENT_FAILED" : code;
1499
+ }
1235
1500
  export class FairAgentScheduler {
1236
1501
  runner;
1237
1502
  sessionLimit;
@@ -1274,6 +1539,17 @@ export class FairAgentScheduler {
1274
1539
  throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
1275
1540
  run.extensionSettings = settings === undefined ? undefined : deepFreeze(structuredClone(settings));
1276
1541
  }
1542
+ async setConfiguration(agentId, configuration) {
1543
+ const node = this.#node(agentId);
1544
+ node.options = Object.freeze({ ...node.options, configuration: deepFreeze(structuredClone(configuration)), tools: Object.freeze([...configuration.tools]) });
1545
+ this.#persist(node.runId);
1546
+ await this.flush(node.runId);
1547
+ }
1548
+ setAttemptCapabilities(agentId, capabilities, projectTrusted) {
1549
+ const node = this.#node(agentId);
1550
+ node.attemptCapabilities = deepFreeze(structuredClone(capabilities));
1551
+ node.attemptProjectTrusted = projectTrusted;
1552
+ }
1277
1553
  setExtensionSettings(agentId, settings) {
1278
1554
  const node = this.#node(agentId);
1279
1555
  node.options = Object.freeze({ ...node.options, extensionSettings: deepFreeze(structuredClone(settings)) });
@@ -1285,7 +1561,7 @@ export class FairAgentScheduler {
1285
1561
  throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
1286
1562
  const parent = parentId ? this.#nodes.get(parentId) : undefined;
1287
1563
  if (parentId && (!parent || parent.runId !== runId))
1288
- throw new WorkflowError("UNKNOWN_AGENT_TYPE", "Parent agent is not owned by this run");
1564
+ throw new WorkflowError("UNKNOWN_AGENT", "Parent agent is not owned by this run");
1289
1565
  const effective = this.#inherit(run, parent, options);
1290
1566
  const id = `${runId}:${String(++this.#nextId)}`;
1291
1567
  const tuiIndex = ++run.nextIndex;
@@ -1302,12 +1578,11 @@ export class FairAgentScheduler {
1302
1578
  node.state = "running";
1303
1579
  this.#persist(runId);
1304
1580
  try {
1305
- const value = await this.runner({ id, runId, tuiIndex, ...(parentId ? { parentId } : {}), prompt, options: effective, signal: node.controller.signal, setSteer: (handler) => { node.steer = handler; } });
1581
+ const value = await this.runner({ id, runId, tuiIndex, ...(parentId ? { parentId } : {}), prompt, options: node.options, signal: node.controller.signal, setSteer: (handler) => { node.steer = handler; } });
1306
1582
  this.#settle(node, { id, ok: true, value });
1307
1583
  }
1308
1584
  catch (error) {
1309
- const typed = error instanceof WorkflowError ? error : new WorkflowError("AGENT_FAILED", errorText(error));
1310
- this.#settle(node, { id, ok: false, error: { code: typed.code, message: typed.message } });
1585
+ this.#settle(node, { id, ok: false, error: { code: scheduledErrorCode(error, node.controller.signal.aborted), message: errorText(error) } });
1311
1586
  }
1312
1587
  };
1313
1588
  this.#nodes.set(id, node);
@@ -1320,7 +1595,7 @@ export class FairAgentScheduler {
1320
1595
  const parent = this.#node(parentId);
1321
1596
  const child = this.#node(childId);
1322
1597
  if (child.parentId !== parentId)
1323
- throw new WorkflowError("UNKNOWN_AGENT_TYPE", "Results are scoped to direct children");
1598
+ throw new WorkflowError("UNKNOWN_AGENT", "Results are scoped to direct children");
1324
1599
  if (child.collected || !child.promise)
1325
1600
  throw new WorkflowError("AGENT_RESULT_COLLECTED", "Child result has already been collected; nested results are one-shot");
1326
1601
  if (child.collecting)
@@ -1362,7 +1637,7 @@ export class FairAgentScheduler {
1362
1637
  async steer(parentId, childId, message) {
1363
1638
  const child = this.#node(childId);
1364
1639
  if (child.parentId !== parentId)
1365
- throw new WorkflowError("UNKNOWN_AGENT_TYPE", "Steering is scoped to direct children");
1640
+ throw new WorkflowError("UNKNOWN_AGENT", "Steering is scoped to direct children");
1366
1641
  if (child.state !== "running" && child.state !== "waiting_for_child")
1367
1642
  throw new WorkflowError("AGENT_FAILED", "Child is not running");
1368
1643
  if (!child.steer)
@@ -1432,13 +1707,14 @@ export class FairAgentScheduler {
1432
1707
  }
1433
1708
  this.#dispatch();
1434
1709
  }
1435
- toolsFor(parentId, resolveTools) {
1710
+ toolsFor(parentId) {
1436
1711
  const parent = this.#node(parentId);
1437
- if (!parent.options.tools.includes("agent"))
1712
+ if (!(parent.attemptCapabilities?.tools ?? parent.options.tools).includes("agent"))
1438
1713
  return [];
1439
1714
  const agentTool = defineTool({
1440
- name: "agent", label: "Child Agent", description: "Start a direct child agent",
1441
- 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 }),
1715
+ name: "agent", label: "Child Agent", description: "Start a direct child agent. You own the children you create: collect each result with get_subagent_result and do not leave descendants running.",
1716
+ parameters: Type.Object({ prompt: Type.String(), label: Type.String(), tools: Type.Optional(Type.Array(Type.String())), excludeTools: Type.Optional(Type.Array(Type.String(), { description: "Exact tool names removed after preparation; cannot remove workflow_result" })), skills: Type.Optional(Type.Array(Type.String())), extensions: Type.Optional(Type.Array(Type.String())), model: 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 }),
1717
+ outputSchema: Type.Object({ id: Type.String() }),
1442
1718
  execute: async (_id, params) => {
1443
1719
  if (!isChildAgentToolParams(params))
1444
1720
  throw new WorkflowError("INVALID_METADATA", "Invalid child agent parameters");
@@ -1446,19 +1722,20 @@ export class FairAgentScheduler {
1446
1722
  const outputSchema = params.outputSchema;
1447
1723
  if (outputSchema !== undefined)
1448
1724
  validateSchema(outputSchema, "agent outputSchema");
1449
- const tools = (params.tools !== undefined || params.role !== undefined || params.skills !== undefined || params.extensions !== undefined ? resolveTools?.(params.role, params.tools, params.model, parent.options.tools, params.skills, params.extensions) : undefined) ?? params.tools ?? parent.options.tools;
1725
+ const tools = parent.attemptCapabilities?.tools ?? parent.options.tools;
1450
1726
  const agentOptions = { ...params };
1451
1727
  Reflect.deleteProperty(agentOptions, "prompt");
1452
- 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 }) };
1728
+ 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.contextFiles ? { contextFiles: params.contextFiles } : {}), ...(outputSchema === undefined ? {} : { schema: outputSchema }), ...(params.retries === undefined ? {} : { retries: params.retries }), ...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }) };
1453
1729
  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 } };
1730
+ return { content: [{ type: "text", text: JSON.stringify({ id: child.id }) }], details: { id: child.id }, structuredContent: { id: child.id } };
1455
1731
  },
1456
1732
  });
1457
1733
  const resultTool = defineTool({
1458
1734
  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
1735
  parameters: Type.Object({ id: Type.String() }),
1736
+ 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
1737
  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 }; }
1738
+ throw new WorkflowError("BUDGET_EXHAUSTED", value.error.message); return { content: [{ type: "text", text: JSON.stringify(value) }], details: value, structuredContent: value }; }
1462
1739
  });
1463
1740
  const steerTool = defineTool({
1464
1741
  name: "steer_subagent", label: "Steer Child", description: "Steer a running direct child",
@@ -1519,11 +1796,15 @@ export class FairAgentScheduler {
1519
1796
  return Object.freeze({ ...options, tools: Object.freeze([...inheritedTools]), ...(extensionSettings === undefined ? {} : { extensionSettings }), ...(options.agentOptions ? { agentOptions: structuredClone(options.agentOptions) } : {}), ...(options.agentIdentity ? { agentIdentity: Object.freeze({ ...options.agentIdentity, structuralPath: Object.freeze([...options.agentIdentity.structuralPath]) }) } : {}) });
1520
1797
  if (options.cwd !== parent.options.cwd)
1521
1798
  throw new WorkflowError("UNKNOWN_TOOL", "Child cwd cannot differ from its parent");
1522
- const forbidden = inheritedTools.find((tool) => !parent.options.tools.includes(tool));
1799
+ const parentCapabilities = parent.attemptCapabilities ?? (parent.options.configuration ? { tools: parent.options.configuration.tools, skills: parent.options.configuration.skills.slice(parent.options.configuration.skills.lastIndexOf("!*") + 1), extensions: parent.options.configuration.extensions.slice(parent.options.configuration.extensions.lastIndexOf("!*") + 1) } : parent.options.capabilities);
1800
+ const forbidden = inheritedTools.find((tool) => !(parentCapabilities?.tools ?? parent.options.tools).includes(tool));
1523
1801
  if (forbidden)
1524
1802
  throw new WorkflowError("UNKNOWN_TOOL", `Child tool escalates parent boundary: ${forbidden}`);
1525
- const identity = options.agentIdentity ?? parent.options.agentIdentity;
1526
- return Object.freeze({ ...options, cwd: parent.options.cwd, tools: Object.freeze([...inheritedTools]), ...(extensionSettings === undefined ? {} : { extensionSettings }), ...(options.agentOptions ? { agentOptions: structuredClone(options.agentOptions) } : {}), ...(parent.options.parentBreadcrumb && !options.parentBreadcrumb ? { parentBreadcrumb: parent.options.parentBreadcrumb } : {}), ...(identity ? { agentIdentity: Object.freeze({ ...identity, structuralPath: Object.freeze([...identity.structuralPath]) }) } : {}), ...(parent.options.worktreeOwner ? { worktreeOwner: parent.options.worktreeOwner } : {}) });
1803
+ const identity = options.agentIdentity ?? (parent.options.agentIdentity ? childIdentity(parent, parent.options.agentIdentity, options.label) : undefined);
1804
+ // Children inherit the exclusions frozen in their parent's configuration, not its re-evaluated options.
1805
+ const inheritedExcludeTools = [...(parent.options.configuration?.excludeTools ?? excludedToolNames(parent.options))];
1806
+ const projectTrusted = (parent.attemptProjectTrusted ?? parent.options.configuration?.projectTrusted ?? parent.options.projectTrusted) !== false && options.projectTrusted !== false;
1807
+ return Object.freeze({ ...options, projectTrusted, ...(inheritedExcludeTools.length ? { inheritedExcludeTools: Object.freeze(inheritedExcludeTools) } : {}), ...(parentCapabilities ? { capabilities: parentCapabilities } : {}), cwd: parent.options.cwd, tools: Object.freeze([...inheritedTools]), ...(extensionSettings === undefined ? {} : { extensionSettings }), ...(options.agentOptions ? { agentOptions: structuredClone(options.agentOptions) } : {}), ...(parent.options.parentBreadcrumb && !options.parentBreadcrumb ? { parentBreadcrumb: parent.options.parentBreadcrumb } : {}), ...(identity ? { agentIdentity: Object.freeze({ ...identity, structuralPath: Object.freeze([...identity.structuralPath]) }) } : {}), ...(parent.options.worktreeOwner ? { worktreeOwner: parent.options.worktreeOwner } : {}) });
1527
1808
  }
1528
1809
  #enqueue(runId, node, start) { this.#runs.get(runId)?.queue.push({ ...(node ? { node } : {}), start }); this.#dispatch(); }
1529
1810
  #dispatch() {
@@ -1548,8 +1829,7 @@ export class FairAgentScheduler {
1548
1829
  run.beforeLaunch?.();
1549
1830
  }
1550
1831
  catch (error) {
1551
- const typed = error instanceof WorkflowError ? error : new WorkflowError("AGENT_FAILED", errorText(error));
1552
- this.#settle(item.node, { id: item.node.id, ok: false, error: { code: typed.code, message: typed.message } });
1832
+ this.#settle(item.node, { id: item.node.id, ok: false, error: { code: scheduledErrorCode(error, item.node.controller.signal.aborted), message: errorText(error) } });
1553
1833
  continue;
1554
1834
  }
1555
1835
  }
@@ -1601,7 +1881,7 @@ export class FairAgentScheduler {
1601
1881
  #node(id) {
1602
1882
  const node = this.#nodes.get(id);
1603
1883
  if (!node)
1604
- throw new WorkflowError("UNKNOWN_AGENT_TYPE", `Unknown owned agent: ${id}`);
1884
+ throw new WorkflowError("UNKNOWN_AGENT", `Unknown owned agent: ${id}`);
1605
1885
  return node;
1606
1886
  }
1607
1887
  #persist(runId) {