@marcoscale98/pi-extensible-workflows 5.19.1-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 (123) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +2 -2
  3. package/dist/src/agent-execution.d.ts +43 -10
  4. package/dist/src/agent-execution.js +365 -127
  5. package/dist/src/background-widget.d.ts +0 -1
  6. package/dist/src/background-widget.js +4 -9
  7. package/dist/src/decoders.d.ts +2 -3
  8. package/dist/src/decoders.js +33 -37
  9. package/dist/src/execution.d.ts +2 -0
  10. package/dist/src/execution.js +5 -5
  11. package/dist/src/host-delivery.js +4 -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 +6 -6
  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 +1 -1
  18. package/dist/src/host-view.js +17 -15
  19. package/dist/src/host.js +62 -87
  20. package/dist/src/index.d.ts +1 -2
  21. package/dist/src/index.js +1523 -1630
  22. package/dist/src/index.js.map +3 -3
  23. package/dist/src/paths.d.ts +7 -6
  24. package/dist/src/paths.js +24 -15
  25. package/dist/src/persistence.d.ts +1 -0
  26. package/dist/src/persistence.js +1 -0
  27. package/dist/src/pi-runtime-runner.js +4 -1
  28. package/dist/src/registry.d.ts +9 -7
  29. package/dist/src/registry.js +28 -70
  30. package/dist/src/runtime/agent-runner.d.ts +2 -0
  31. package/dist/src/session-lease.d.ts +1 -1
  32. package/dist/src/session-lease.js +45 -11
  33. package/dist/src/settings.d.ts +6 -3
  34. package/dist/src/settings.js +36 -47
  35. package/dist/src/store.d.ts +3 -1
  36. package/dist/src/store.js +17 -1
  37. package/dist/src/trajectory.d.ts +0 -1
  38. package/dist/src/trajectory.js +2 -3
  39. package/dist/src/types.d.ts +100 -44
  40. package/dist/src/types.js +4 -7
  41. package/dist/src/utils.d.ts +3 -2
  42. package/dist/src/utils.js +20 -10
  43. package/dist/src/validation.d.ts +8 -2
  44. package/dist/src/validation.js +122 -86
  45. package/dist/starter/index.js +8 -9
  46. package/dist/starter/index.js.map +1 -1
  47. package/dist/starter/review-loop.js +8 -7
  48. package/dist/subagents/index.js +654 -814
  49. package/dist/subagents/index.js.map +3 -3
  50. package/dist/subagents/src/contracts.d.ts +4 -3
  51. package/dist/subagents/src/contracts.js +2 -2
  52. package/dist/subagents/src/index.d.ts +1 -1
  53. package/dist/subagents/src/index.js +1 -2
  54. package/dist/subagents/src/manager.js +49 -70
  55. package/dist/subagents/src/navigator.js +3 -14
  56. package/dist/subagents/src/view.d.ts +0 -1
  57. package/dist/subagents/src/view.js +2 -16
  58. package/dist/subagents/src/worktree.js +1 -1
  59. package/dist/trajectory/assets/index.html +8 -8
  60. package/dist/trajectory/index.js +487 -891
  61. package/dist/trajectory/index.js.map +3 -3
  62. package/dist/trajectory/src/assets/index.html +8 -8
  63. package/dist/trajectory/src/index.js +126 -86
  64. package/dist/trajectory/src/server.js +264 -25
  65. package/dist/trajectory/src/server.js.map +3 -3
  66. package/dist/trajectory/src/startup-mutex.d.ts +17 -0
  67. package/dist/trajectory/src/startup-mutex.js +172 -0
  68. package/examples/workflow-extension-template/README.md +4 -7
  69. package/examples/workflow-extension-template/extension.test.mjs +4 -8
  70. package/examples/workflow-extension-template/index.js +0 -3
  71. package/package.json +5 -5
  72. package/skills/pi-extensible-workflows/SKILL.md +10 -8
  73. package/src/agent-execution.ts +339 -127
  74. package/src/background-widget.ts +4 -10
  75. package/src/decoders.ts +31 -37
  76. package/src/execution.ts +5 -5
  77. package/src/host-delivery.ts +4 -8
  78. package/src/host-navigator.ts +4 -2
  79. package/src/host-phases.ts +1 -1
  80. package/src/host-recovery.ts +20 -26
  81. package/src/host-runtime.ts +44 -6
  82. package/src/host-view.ts +18 -11
  83. package/src/host.ts +62 -113
  84. package/src/index.ts +1 -2
  85. package/src/paths.ts +18 -12
  86. package/src/persistence.ts +1 -0
  87. package/src/pi-runtime-runner.ts +3 -1
  88. package/src/registry.ts +27 -63
  89. package/src/runtime/agent-runner.ts +2 -0
  90. package/src/session-lease.ts +34 -6
  91. package/src/settings.ts +35 -47
  92. package/src/store.ts +18 -2
  93. package/src/trajectory.ts +2 -4
  94. package/src/types.ts +71 -30
  95. package/src/utils.ts +18 -9
  96. package/src/validation.ts +102 -79
  97. package/starter/index.ts +0 -1
  98. package/starter/review-loop.ts +8 -7
  99. package/starter/roles/developer.md +0 -1
  100. package/starter/roles/oracle.md +0 -1
  101. package/starter/roles/researcher.md +0 -1
  102. package/starter/roles/reviewer.md +0 -1
  103. package/starter/roles/scout.md +0 -1
  104. package/subagents/README.md +8 -14
  105. package/subagents/src/contracts.ts +3 -4
  106. package/subagents/src/index.ts +2 -3
  107. package/subagents/src/manager.ts +45 -125
  108. package/subagents/src/navigator.ts +4 -13
  109. package/subagents/src/view.ts +4 -14
  110. package/subagents/src/worktree.ts +1 -1
  111. package/trajectory/src/assets/index.html +8 -8
  112. package/trajectory/src/index.ts +106 -82
  113. package/trajectory/src/server.ts +44 -8
  114. package/trajectory/src/startup-mutex.ts +127 -0
  115. package/dist/src/roles.d.ts +0 -70
  116. package/dist/src/roles.js +0 -326
  117. package/dist/starter/roles/developer.md +0 -6
  118. package/dist/starter/roles/oracle.md +0 -18
  119. package/dist/starter/roles/researcher.md +0 -20
  120. package/dist/starter/roles/reviewer.md +0 -19
  121. package/dist/starter/roles/scout.md +0 -18
  122. package/examples/workflow-extension-template/roles/reviewer.md +0 -4
  123. package/src/roles.ts +0 -327
@@ -1,23 +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, createCodemodeExtension, createMcpExtension, createToolSearchExtension, 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, physicalModel, 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, extensionIdentity } from "./paths.js";
18
+ import { isCoreAgentOption, validateAgentOptions, validateSchema } from "./validation.js";
19
+ import { canonicalPath, extensionIdentity, piResourceContains, piResourcePath, sameFilesystemPath } from "./paths.js";
19
20
  import { isCorePackageName } from "./package-identity.js";
20
- import { canonicalExtensionSelector, resolveRole } from "./roles.js";
21
+ import { canonicalExtensionSelector, validateContextFileScopes, validateSelectorList, validateWorkflowExtensionSettings } from "./settings.js";
22
+ import { agentIdentityPath } from "./execution.js";
21
23
  const localToolTimingExtension = createToolTimingExtension();
22
24
  function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
23
25
  if (!value)
@@ -25,6 +27,59 @@ function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
25
27
  assertModelThinking(value);
26
28
  return resolveModelReference(value, aliases, knownModels, settingsPath);
27
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
+ }
28
83
  function accounting(stats) {
29
84
  return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
30
85
  }
@@ -212,6 +267,48 @@ function builtinExtensions() {
212
267
  }
213
268
  const BUILTIN_EXTENSION_PREFIX = "builtin:";
214
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
+ }
215
312
  async function createLocalPiSessionHandle(input, sessionStartEvent) {
216
313
  const agentDir = input.agentDir ?? getAgentDir();
217
314
  const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
@@ -236,25 +333,23 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
236
333
  const resolved = await packageManager.resolve();
237
334
  const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => extensionIdentity(path)))];
238
335
  const selectorSources = policy.selectorSources;
239
- const selection = resolveRole(undefined, {
240
- cwd: input.cwd,
241
- selectorSources,
242
- resources: { extensions: discoveredExtensions },
243
- });
244
- const selectedExtensions = selection.selectedExtensions ?? [];
336
+ const selectedExtensions = selectResourcesByLayers(resourceSelectorLayers(selectorSources, "extensions"), discoveredExtensions).filter((path) => policy.capabilities === undefined || policy.capabilities.extensions.includes(path));
245
337
  const extensionPaths = selectedExtensions.filter((path) => path.startsWith(BUILTIN_EXTENSION_PREFIX) || !isWorkflowHostEntry(path));
246
338
  policy.selectedExtensions = selectedExtensions;
247
- policy.unmatchedExtensions = selection.unmatchedExtensions ?? [];
339
+ policy.unmatchedExtensions = unmatchedResourcePatterns(policy.effective.extensions, discoveredExtensions);
248
340
  const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
249
- const updateSkillMatches = (skills) => {
341
+ const updateSkillMatches = (skills, extensionSkillPaths) => {
250
342
  const names = [...new Set(skills.map(({ name }) => name))];
251
- const selected = resolveRole(undefined, { cwd: input.cwd, selectorSources, resources: { skills: names } });
252
- 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];
253
348
  policy.selectedSkills = selectedSkills;
254
- policy.unmatchedSkills = selected.unmatchedSkills ?? [];
349
+ policy.unmatchedSkills = unmatchedResourcePatterns(policy.effective.skills, names);
255
350
  return new Set(names.filter((name) => !selectedSkills.includes(name)));
256
351
  };
257
- resourceLoader = new DefaultResourceLoader({
352
+ const workflowLoader = new WorkflowResourceLoader(input.cwd, {
258
353
  cwd: input.cwd,
259
354
  agentDir,
260
355
  settingsManager,
@@ -265,12 +360,13 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
265
360
  extensionFactories,
266
361
  ...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}),
267
362
  skillsOverride: (base) => {
268
- const disabledSkills = updateSkillMatches(base.skills);
363
+ const disabledSkills = updateSkillMatches(base.skills, workflowLoader.extensionSkillPaths);
269
364
  return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
270
365
  },
271
366
  ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
272
367
  ...systemPromptOptions,
273
368
  });
369
+ resourceLoader = workflowLoader;
274
370
  await resourceLoader.reload();
275
371
  }
276
372
  else {
@@ -293,7 +389,7 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
293
389
  throw new WorkflowError("UNKNOWN_MODEL", `Unknown model: ${input.model.provider}/${input.model.model}${because}`);
294
390
  }
295
391
  const effectiveSessionStartEvent = input.settings === undefined && sessionStartEvent === undefined ? undefined : { ...(sessionStartEvent ?? { type: "session_start", reason: "startup" }), settings: input.settings ?? Object.freeze({}) };
296
- 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 });
297
393
  const nativeDispose = session.dispose.bind(session);
298
394
  let disposal;
299
395
  const shutdown = (reason, targetSessionFile) => {
@@ -663,6 +759,11 @@ export async function createLocalWorkflowAgentSession(prepared, context) {
663
759
  return session;
664
760
  }
665
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
+ }
666
767
  function changedOption(options, baseline, key) { return JSON.stringify(options[key]) !== JSON.stringify(baseline[key]); }
667
768
  function isChildAgentToolParams(value) {
668
769
  if (!jsonObject(value) || typeof value.prompt !== "string" || typeof value.label !== "string")
@@ -675,8 +776,6 @@ function isChildAgentToolParams(value) {
675
776
  return false;
676
777
  if (value.model !== undefined && typeof value.model !== "string")
677
778
  return false;
678
- if (value.role !== undefined && (typeof value.role !== "string" || !value.role.trim()))
679
- return false;
680
779
  if (value.contextFiles !== undefined && (!Array.isArray(value.contextFiles) || !value.contextFiles.every(isContextFileScope)))
681
780
  return false;
682
781
  if (value.outputSchema !== undefined && !jsonObject(value.outputSchema))
@@ -702,7 +801,7 @@ function resourcePolicySummary(policy, tools = policy.selectedTools ?? []) {
702
801
  }
703
802
  const RESOURCE_SELECTOR_KEYS = ["skills", "extensions", "tools"];
704
803
  function resourceSelectorLayers(sources, key) {
705
- return [sources.global[key], sources.project[key], sources.role?.[key], sources.call?.[key]];
804
+ return [sources.global[key], sources.project[key], sources.call?.[key]];
706
805
  }
707
806
  function selectorListWidened(ceiling, candidate) {
708
807
  let candidateIndex = 0;
@@ -775,7 +874,7 @@ function appendResourcePolicyNarrowing(ceiling, candidate) {
775
874
  };
776
875
  }
777
876
  function packageRoot(start) {
778
- let current = dirname(realpathSync(start));
877
+ let current = dirname(canonicalPath(start));
779
878
  for (;;) {
780
879
  const candidates = [current, join(current, "..", "@earendil-works", "pi-coding-agent"), join(current, "..", "pi-coding-agent")];
781
880
  for (const candidate of candidates) {
@@ -835,10 +934,9 @@ async function resolvePiRuntime() {
835
934
  }
836
935
  }
837
936
  const piRuntimeResolution = resolvePiRuntime();
838
- function workflowAgentPrompt(task, options, attempt, previousError) {
839
- 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");
840
- const instruction = options.budget?.instruction();
841
- 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");
842
940
  }
843
941
  async function preparedAgentSession(input, initialPrompt) {
844
942
  const { runtime: piRuntime, error: piRuntimeError } = await piRuntimeResolution;
@@ -861,47 +959,27 @@ async function attemptSessionInput(source, attempt) {
861
959
  await copyFile(source, target);
862
960
  return target;
863
961
  }
864
- 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) {
865
963
  const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
866
964
  const baselineOptions = structuredClone(options.agentOptions ?? {});
867
965
  const baseResourcePolicy = await root.agentResourcePolicy?.();
868
- const roleName = typeof options.role === "string" ? options.role : undefined;
869
- const roleDefinition = roleName ? root.agentDefinitions?.[roleName] : undefined;
870
- const roleSelectors = roleDefinition ? {
871
- ...(roleDefinition.skills === undefined ? {} : { skills: roleDefinition.skills }),
872
- ...(roleDefinition.extensions === undefined ? {} : { extensions: roleDefinition.extensions.map((selector) => canonicalExtensionSelector(selector, cwd)) }),
873
- ...(roleDefinition.tools === undefined ? {} : { tools: roleDefinition.tools }),
874
- } : undefined;
875
- const selectorValue = (key, fallback) => {
876
- const value = baselineOptions[key];
877
- if (Array.isArray(value) && value.every((entry) => typeof entry === "string"))
878
- return value;
879
- 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,
880
975
  };
881
- const rawCallSkills = selectorValue("skills", options.skills);
882
- const rawCallExtensions = selectorValue("extensions", options.extensions)?.map((selector) => canonicalExtensionSelector(selector, cwd));
883
- const rawCallTools = selectorValue("tools", options.tools);
884
- const callSelectors = { ...(rawCallSkills === undefined ? {} : { skills: rawCallSkills }), ...(rawCallExtensions === undefined ? {} : { extensions: rawCallExtensions }), ...(rawCallTools === undefined ? {} : { tools: rawCallTools }) };
885
- const resourcePolicy = baseResourcePolicy ? {
886
- ...baseResourcePolicy,
887
- effective: {
888
- skills: [...baseResourcePolicy.effective.skills, ...(roleSelectors?.skills ?? []), ...(rawCallSkills ?? [])],
889
- extensions: [...baseResourcePolicy.effective.extensions, ...(roleSelectors?.extensions ?? []), ...(rawCallExtensions ?? [])],
890
- ...(baseResourcePolicy.effective.tools === undefined && roleSelectors?.tools === undefined && rawCallTools === undefined ? {} : { tools: [...(baseResourcePolicy.effective.tools ?? []), ...(roleSelectors?.tools ?? []), ...(rawCallTools ?? [])] }),
891
- },
892
- selectorSources: { global: baseResourcePolicy.selectorSources.global, project: baseResourcePolicy.selectorSources.project, ...(roleSelectors ? { role: roleSelectors } : {}), ...(Object.keys(callSelectors).length ? { call: callSelectors } : {}) },
893
- } : undefined;
894
- if (resourcePolicy) {
895
- resourcePolicy.selectedTools = resolved.tools;
896
- resourcePolicy.unmatchedTools = unmatchedResourcePatterns(resourceSelectorLayers(resourcePolicy.selectorSources, "tools").flatMap((layer) => layer ?? []), [...root.tools]);
897
- }
898
- const resourcePolicyCeiling = resourcePolicy ? structuredClone(resourcePolicy) : undefined;
976
+ const resourcePolicyCeiling = structuredClone(resourcePolicy);
899
977
  const sessionPath = options.sessionPath === undefined || inspection ? options.sessionPath : await attemptSessionInput(options.sessionPath, attempt);
900
- const extensionSettings = mergeWorkflowExtensionSettings(options.inheritedExtensionSettings ?? root.extensionSettings, roleDefinition?.extensionSettings) ?? Object.freeze({});
901
- 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 } : {}) });
902
980
  if (options.agentNodeId !== undefined)
903
981
  root.onAgentSettings?.(options.agentNodeId, extensionSettings);
904
- 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) };
905
983
  const setup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, prepared: await preparedAgentSession(sessionInput, task), transport };
906
984
  const base = fallbackSetupContext(root, options, setupSignal);
907
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" } : {}) });
@@ -925,30 +1003,69 @@ async function prepareAgentSetup(root, transport, task, options, resolved, cwd,
925
1003
  if (resourcePolicyWidened(resourcePolicyCeiling, setup.sessionInput.resourcePolicy))
926
1004
  throw new WorkflowError("INVALID_METADATA", "Agent setup widened the prepared resource policy");
927
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
+ }
928
1017
  setup.sessionInput.options = setup.options;
929
1018
  if (changedOption(setup.options, baselineOptions, "model") && typeof setup.options.model === "string")
930
1019
  setup.sessionInput.model = parseModel(setup.options.model, setup.sessionInput.model, root.modelAliases, root.knownModels ?? root.availableModels, root.settingsPath);
931
1020
  if (changedOption(setup.options, baselineOptions, "tools") && Array.isArray(setup.options.tools) && setup.options.tools.every((tool) => typeof tool === "string"))
932
1021
  setup.sessionInput.tools = [...setup.options.tools];
933
- if (changedOption(setup.options, baselineOptions, "cwd") && typeof setup.options.cwd === "string")
934
- 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}`);
935
1044
  const customToolNames = new Set([...(setup.sessionInput.customTools ?? []).map(({ name }) => name), ...(setup.sessionInput.resultTool ? [setup.sessionInput.resultTool.name] : [])]);
936
1045
  const widened = setup.sessionInput.tools.find((tool) => !resolved.tools.includes(tool) && !customToolNames.has(tool));
937
1046
  const outsideTool = widened ?? setup.sessionInput.tools.find((tool) => !root.tools.has(tool) && !customToolNames.has(tool));
938
1047
  if (outsideTool)
939
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") ?? [];
940
1057
  }
941
1058
  catch (error) {
942
1059
  setup.prepared = await preparedAgentSession(setup.sessionInput, task);
943
1060
  return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error } };
944
1061
  }
945
- 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);
946
1063
  return { setup, summary: agentSetupSummary(setup, hookNames) };
947
1064
  }
948
1065
  export async function prepareAgentSetupForInspection(root, task, options, transport) {
949
1066
  const executor = new WorkflowAgentExecutor(root);
950
- const resolved = executor.resolve(options);
951
- 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);
952
1069
  }
953
1070
  function attemptRecord(transport, attempt, session, setup, stats, result, error) {
954
1071
  return { attempt, transport, session: session.reference, ...(result === undefined ? {} : { result }), ...(error ? { error } : {}), accounting: stats, setup };
@@ -966,52 +1083,105 @@ export function getAgentAttempts(error) {
966
1083
  export class WorkflowAgentExecutor {
967
1084
  root;
968
1085
  transport;
969
- warnedRoleTools = new Set();
970
1086
  constructor(root, transport = localAgentTransport) {
971
1087
  this.root = root;
972
1088
  this.transport = transport;
973
1089
  }
974
1090
  setRunContext(runContext) { this.root.runContext = runContext; }
975
1091
  resolve(options, inheritedTools) {
976
- const roleName = typeof options.role === "string" ? options.role : undefined;
977
- const roleDefinition = roleName ? this.root.agentDefinitions?.[roleName] : undefined;
978
- if (roleName && !roleDefinition)
979
- throw new WorkflowError("UNKNOWN_AGENT_TYPE", `Unknown agent role: ${roleName}`);
980
- if (inheritedTools === undefined)
981
- for (const pattern of roleDefinition?.tools ?? []) {
982
- const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
983
- if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !this.root.tools.has(body)) {
984
- const key = `${roleName ?? "agent"}:${body}`;
985
- if (!this.warnedRoleTools.has(key)) {
986
- this.warnedRoleTools.add(key);
987
- this.root.onResourceWarning?.(`Tool not available in this session for role ${roleName ?? "agent"}: ${body}. The agent runs without it.`);
988
- }
989
- }
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)];
990
1134
  }
991
- const resolved = resolveRole(roleName, {
992
- cwd: this.root.cwd,
993
- ...(roleDefinition === undefined ? {} : { definition: roleDefinition }),
994
- ...(this.root.agentDefinitions === undefined ? {} : { definitions: this.root.agentDefinitions }),
995
- ...(this.root.agentDir === undefined ? {} : { agentDir: this.root.agentDir }),
996
- selectorSources: { global: this.root.resourceSelectors ?? {}, project: {} },
997
- ...(this.root.modelAliases === undefined ? {} : { modelAliases: this.root.modelAliases }),
998
- ...(this.root.knownModels === undefined ? {} : { knownModels: this.root.knownModels }),
999
- ...(this.root.availableModels === undefined ? {} : { availableModels: this.root.availableModels }),
1000
- ...(this.root.blockedAliases === undefined ? {} : { blockedAliases: this.root.blockedAliases }),
1001
- ...(this.root.blockedAliasTargets === undefined ? {} : { blockedAliasTargets: this.root.blockedAliasTargets }),
1002
- ...(this.root.settingsPath === undefined ? {} : { settingsPath: this.root.settingsPath }),
1003
- rootModel: this.root.model,
1004
- rootTools: this.root.tools,
1005
- ...(inheritedTools === undefined ? {} : { inheritedTools }),
1006
- ...(options.model === undefined ? {} : { model: options.model }),
1007
- ...(options.modelOverride === undefined ? {} : { modelOverride: options.modelOverride }),
1008
- ...(options.tools === undefined ? {} : { tools: options.tools }),
1009
- ...(options.skills === undefined ? {} : { skills: options.skills }),
1010
- ...(options.extensions === undefined ? {} : { extensions: options.extensions }),
1011
- ...(options.contextFiles === undefined ? {} : { contextFiles: options.contextFiles }),
1012
- ...(options.effectiveTools === undefined ? {} : { effectiveTools: options.effectiveTools }),
1013
- });
1014
- return { model: physicalModel(resolved.model ?? this.root.model, this.root.modelAliases, this.root.knownModels, this.root.settingsPath), ...(resolved.requestedModel === undefined ? {} : { requestedModel: resolved.requestedModel }), tools: resolved.tools ?? [], ...(resolved.overrideSystemPrompt ? { systemPrompt: resolved.prompt } : {}), systemPromptAppend: resolved.overrideSystemPrompt ? "" : resolved.prompt, ...(resolved.contextFiles === undefined ? {} : { contextFiles: resolved.contextFiles }) };
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;
1015
1185
  }
1016
1186
  async execute(task, options, signal, customTools = [], setSteer, beforeRetry) {
1017
1187
  const executionSignal = signal ?? this.root.runContext?.signal;
@@ -1019,7 +1189,7 @@ export class WorkflowAgentExecutor {
1019
1189
  throw new WorkflowError("INVALID_METADATA", "retries must be a non-negative integer");
1020
1190
  if (options.timeoutMs !== undefined && options.timeoutMs !== null && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0))
1021
1191
  throw new WorkflowError("INVALID_METADATA", "timeoutMs must be null or a positive integer");
1022
- let resolved = this.resolve(options);
1192
+ let resolved;
1023
1193
  let recoveryModel;
1024
1194
  let cwd;
1025
1195
  if (options.parent) {
@@ -1049,6 +1219,15 @@ export class WorkflowAgentExecutor {
1049
1219
  throw new WorkflowError("INVALID_METADATA", "Only child agents or worktree scopes may provide a cwd");
1050
1220
  cwd = this.root.cwd;
1051
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); } });
1052
1231
  const attempts = [];
1053
1232
  let maxAttempts = (options.retries ?? 0) + 1;
1054
1233
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
@@ -1076,7 +1255,7 @@ export class WorkflowAgentExecutor {
1076
1255
  const resultSchema = Compile(options.schema ?? defaultWorkflowResultSchema);
1077
1256
  let resultAccepted = false;
1078
1257
  const resultTool = defineTool({
1079
- 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(),
1080
1259
  async execute(_id, value) {
1081
1260
  if (!resultSchema.Check(value) || !jsonValue(value))
1082
1261
  return { content: [{ type: "text", text: "Result does not match the required schema." }], details: {}, isError: true };
@@ -1112,12 +1291,24 @@ export class WorkflowAgentExecutor {
1112
1291
  } : undefined;
1113
1292
  try {
1114
1293
  setupFailed = true;
1115
- 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);
1116
1295
  setup = prepared.setup;
1117
1296
  const attemptSetup = prepared.setup;
1118
1297
  setupSummary = prepared.summary;
1119
1298
  if (prepared.failure)
1120
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
+ }
1121
1312
  setupFailed = false;
1122
1313
  if (attemptSignal.aborted)
1123
1314
  throw new WorkflowError("CANCELLED", "Agent cancelled");
@@ -1153,6 +1344,14 @@ export class WorkflowAgentExecutor {
1153
1344
  }
1154
1345
  if (policy)
1155
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?.();
1156
1355
  },
1157
1356
  onBeforeComplete: async () => {
1158
1357
  if (options.worktreeOwner)
@@ -1192,6 +1391,7 @@ export class WorkflowAgentExecutor {
1192
1391
  const runtimeRun = { id: transportBase.run.runId, namespaceId: transportBase.run.sessionId, workflowName: transportBase.run.workflow.name };
1193
1392
  const providerRecovery = options.providerErrorRecovery;
1194
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);
1195
1395
  const runtimeRequest = {
1196
1396
  task: setup.prepared.initialPrompt ?? setup.prompt,
1197
1397
  cwd: setup.prepared.cwd,
@@ -1200,6 +1400,7 @@ export class WorkflowAgentExecutor {
1200
1400
  // Pi sessions execute the prepared native definitions; the executor does not duplicate them as neutral request tools.
1201
1401
  customTools: [],
1202
1402
  ...(options.schema ? { resultSchema: options.schema } : {}),
1403
+ ...(attemptControl ? { turnControl: attemptControl } : {}),
1203
1404
  run: runtimeRun,
1204
1405
  agent: runtimeAgent,
1205
1406
  signal: attemptSignal,
@@ -1272,6 +1473,30 @@ export class WorkflowAgentExecutor {
1272
1473
  throw new WorkflowError("AGENT_FAILED", "Agent execution failed");
1273
1474
  }
1274
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
+ }
1275
1500
  export class FairAgentScheduler {
1276
1501
  runner;
1277
1502
  sessionLimit;
@@ -1314,6 +1539,17 @@ export class FairAgentScheduler {
1314
1539
  throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
1315
1540
  run.extensionSettings = settings === undefined ? undefined : deepFreeze(structuredClone(settings));
1316
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
+ }
1317
1553
  setExtensionSettings(agentId, settings) {
1318
1554
  const node = this.#node(agentId);
1319
1555
  node.options = Object.freeze({ ...node.options, extensionSettings: deepFreeze(structuredClone(settings)) });
@@ -1325,7 +1561,7 @@ export class FairAgentScheduler {
1325
1561
  throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
1326
1562
  const parent = parentId ? this.#nodes.get(parentId) : undefined;
1327
1563
  if (parentId && (!parent || parent.runId !== runId))
1328
- 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");
1329
1565
  const effective = this.#inherit(run, parent, options);
1330
1566
  const id = `${runId}:${String(++this.#nextId)}`;
1331
1567
  const tuiIndex = ++run.nextIndex;
@@ -1342,12 +1578,11 @@ export class FairAgentScheduler {
1342
1578
  node.state = "running";
1343
1579
  this.#persist(runId);
1344
1580
  try {
1345
- 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; } });
1346
1582
  this.#settle(node, { id, ok: true, value });
1347
1583
  }
1348
1584
  catch (error) {
1349
- const typed = error instanceof WorkflowError ? error : new WorkflowError("AGENT_FAILED", errorText(error));
1350
- 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) } });
1351
1586
  }
1352
1587
  };
1353
1588
  this.#nodes.set(id, node);
@@ -1360,7 +1595,7 @@ export class FairAgentScheduler {
1360
1595
  const parent = this.#node(parentId);
1361
1596
  const child = this.#node(childId);
1362
1597
  if (child.parentId !== parentId)
1363
- throw new WorkflowError("UNKNOWN_AGENT_TYPE", "Results are scoped to direct children");
1598
+ throw new WorkflowError("UNKNOWN_AGENT", "Results are scoped to direct children");
1364
1599
  if (child.collected || !child.promise)
1365
1600
  throw new WorkflowError("AGENT_RESULT_COLLECTED", "Child result has already been collected; nested results are one-shot");
1366
1601
  if (child.collecting)
@@ -1402,7 +1637,7 @@ export class FairAgentScheduler {
1402
1637
  async steer(parentId, childId, message) {
1403
1638
  const child = this.#node(childId);
1404
1639
  if (child.parentId !== parentId)
1405
- throw new WorkflowError("UNKNOWN_AGENT_TYPE", "Steering is scoped to direct children");
1640
+ throw new WorkflowError("UNKNOWN_AGENT", "Steering is scoped to direct children");
1406
1641
  if (child.state !== "running" && child.state !== "waiting_for_child")
1407
1642
  throw new WorkflowError("AGENT_FAILED", "Child is not running");
1408
1643
  if (!child.steer)
@@ -1472,13 +1707,13 @@ export class FairAgentScheduler {
1472
1707
  }
1473
1708
  this.#dispatch();
1474
1709
  }
1475
- toolsFor(parentId, resolveTools) {
1710
+ toolsFor(parentId) {
1476
1711
  const parent = this.#node(parentId);
1477
- if (!parent.options.tools.includes("agent"))
1712
+ if (!(parent.attemptCapabilities?.tools ?? parent.options.tools).includes("agent"))
1478
1713
  return [];
1479
1714
  const agentTool = defineTool({
1480
- name: "agent", label: "Child Agent", description: "Start a direct child agent",
1481
- parameters: Type.Object({ prompt: Type.String(), label: Type.String(), tools: Type.Optional(Type.Array(Type.String())), skills: Type.Optional(Type.Array(Type.String())), extensions: Type.Optional(Type.Array(Type.String())), model: Type.Optional(Type.String()), role: Type.Optional(Type.String()), contextFiles: Type.Optional(Type.Array(Type.String())), outputSchema: Type.Optional(Type.Record(Type.String(), Type.Unknown())), retries: Type.Optional(Type.Integer({ minimum: 0 })), timeoutMs: Type.Optional(Type.Union([Type.Integer({ minimum: 1 }), Type.Null()])) }, { additionalProperties: true }),
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 }),
1482
1717
  outputSchema: Type.Object({ id: Type.String() }),
1483
1718
  execute: async (_id, params) => {
1484
1719
  if (!isChildAgentToolParams(params))
@@ -1487,10 +1722,10 @@ export class FairAgentScheduler {
1487
1722
  const outputSchema = params.outputSchema;
1488
1723
  if (outputSchema !== undefined)
1489
1724
  validateSchema(outputSchema, "agent outputSchema");
1490
- 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;
1491
1726
  const agentOptions = { ...params };
1492
1727
  Reflect.deleteProperty(agentOptions, "prompt");
1493
- const options = { label: params.label, requestedLabel: params.label, cwd: parent.options.cwd, tools, agentOptions, ...(params.skills ? { skills: params.skills } : {}), ...(params.extensions ? { extensions: params.extensions } : {}), ...(params.model ? { model: params.model } : {}), ...(params.role ? { role: params.role } : {}), ...(params.contextFiles ? { contextFiles: params.contextFiles } : {}), ...(outputSchema === undefined ? {} : { schema: outputSchema }), ...(params.retries === undefined ? {} : { retries: params.retries }), ...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }) };
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 }) };
1494
1729
  const child = this.spawn(parent.runId, params.prompt, options, parentId);
1495
1730
  return { content: [{ type: "text", text: JSON.stringify({ id: child.id }) }], details: { id: child.id }, structuredContent: { id: child.id } };
1496
1731
  },
@@ -1561,11 +1796,15 @@ export class FairAgentScheduler {
1561
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]) }) } : {}) });
1562
1797
  if (options.cwd !== parent.options.cwd)
1563
1798
  throw new WorkflowError("UNKNOWN_TOOL", "Child cwd cannot differ from its parent");
1564
- 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));
1565
1801
  if (forbidden)
1566
1802
  throw new WorkflowError("UNKNOWN_TOOL", `Child tool escalates parent boundary: ${forbidden}`);
1567
- const identity = options.agentIdentity ?? parent.options.agentIdentity;
1568
- 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 } : {}) });
1569
1808
  }
1570
1809
  #enqueue(runId, node, start) { this.#runs.get(runId)?.queue.push({ ...(node ? { node } : {}), start }); this.#dispatch(); }
1571
1810
  #dispatch() {
@@ -1590,8 +1829,7 @@ export class FairAgentScheduler {
1590
1829
  run.beforeLaunch?.();
1591
1830
  }
1592
1831
  catch (error) {
1593
- const typed = error instanceof WorkflowError ? error : new WorkflowError("AGENT_FAILED", errorText(error));
1594
- 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) } });
1595
1833
  continue;
1596
1834
  }
1597
1835
  }
@@ -1643,7 +1881,7 @@ export class FairAgentScheduler {
1643
1881
  #node(id) {
1644
1882
  const node = this.#nodes.get(id);
1645
1883
  if (!node)
1646
- throw new WorkflowError("UNKNOWN_AGENT_TYPE", `Unknown owned agent: ${id}`);
1884
+ throw new WorkflowError("UNKNOWN_AGENT", `Unknown owned agent: ${id}`);
1647
1885
  return node;
1648
1886
  }
1649
1887
  #persist(runId) {