@marcoscale98/pi-extensible-workflows 5.14.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 (204) hide show
  1. package/CHANGELOG.md +536 -0
  2. package/README.md +64 -0
  3. package/dist/src/agent-execution.d.ts +338 -0
  4. package/dist/src/agent-execution.js +1633 -0
  5. package/dist/src/background-widget.d.ts +74 -0
  6. package/dist/src/background-widget.js +1340 -0
  7. package/dist/src/budget.d.ts +38 -0
  8. package/dist/src/budget.js +188 -0
  9. package/dist/src/changelog.d.ts +6 -0
  10. package/dist/src/changelog.js +112 -0
  11. package/dist/src/decoders.d.ts +96 -0
  12. package/dist/src/decoders.js +629 -0
  13. package/dist/src/execution.d.ts +19 -0
  14. package/dist/src/execution.js +749 -0
  15. package/dist/src/herdr.d.ts +53 -0
  16. package/dist/src/herdr.js +270 -0
  17. package/dist/src/host-delivery.d.ts +95 -0
  18. package/dist/src/host-delivery.js +376 -0
  19. package/dist/src/host-navigator.d.ts +83 -0
  20. package/dist/src/host-navigator.js +1087 -0
  21. package/dist/src/host-phases.d.ts +89 -0
  22. package/dist/src/host-phases.js +320 -0
  23. package/dist/src/host-recovery.d.ts +141 -0
  24. package/dist/src/host-recovery.js +356 -0
  25. package/dist/src/host-runtime.d.ts +78 -0
  26. package/dist/src/host-runtime.js +378 -0
  27. package/dist/src/host-view.d.ts +116 -0
  28. package/dist/src/host-view.js +868 -0
  29. package/dist/src/host.d.ts +40 -0
  30. package/dist/src/host.js +1641 -0
  31. package/dist/src/index.d.ts +29 -0
  32. package/dist/src/index.js +13725 -0
  33. package/dist/src/index.js.map +6 -0
  34. package/dist/src/io.d.ts +14 -0
  35. package/dist/src/io.js +82 -0
  36. package/dist/src/package-identity.d.ts +4 -0
  37. package/dist/src/package-identity.js +6 -0
  38. package/dist/src/paths.d.ts +14 -0
  39. package/dist/src/paths.js +51 -0
  40. package/dist/src/persistence.d.ts +5 -0
  41. package/dist/src/persistence.js +5 -0
  42. package/dist/src/pi-runtime-adapter.d.ts +27 -0
  43. package/dist/src/pi-runtime-adapter.js +243 -0
  44. package/dist/src/pi-runtime-runner.d.ts +42 -0
  45. package/dist/src/pi-runtime-runner.js +505 -0
  46. package/dist/src/registry.d.ts +48 -0
  47. package/dist/src/registry.js +379 -0
  48. package/dist/src/retention.d.ts +17 -0
  49. package/dist/src/retention.js +195 -0
  50. package/dist/src/runtime/agent-runner.d.ts +143 -0
  51. package/dist/src/runtime/agent-runner.js +12 -0
  52. package/dist/src/runtime/index.d.ts +2 -0
  53. package/dist/src/runtime/index.js +2 -0
  54. package/dist/src/runtime/namespace.d.ts +14 -0
  55. package/dist/src/runtime/namespace.js +1 -0
  56. package/dist/src/runtime/workflow-result.d.ts +2 -0
  57. package/dist/src/runtime/workflow-result.js +6 -0
  58. package/dist/src/session-handoff.d.ts +2 -0
  59. package/dist/src/session-handoff.js +82 -0
  60. package/dist/src/session-lease.d.ts +12 -0
  61. package/dist/src/session-lease.js +180 -0
  62. package/dist/src/store.d.ts +95 -0
  63. package/dist/src/store.js +951 -0
  64. package/dist/src/subagent-manager-handle.d.ts +4 -0
  65. package/dist/src/subagent-manager-handle.js +6 -0
  66. package/dist/src/tool-timing.d.ts +12 -0
  67. package/dist/src/tool-timing.js +33 -0
  68. package/dist/src/trajectory-contracts.d.ts +8 -0
  69. package/dist/src/trajectory-contracts.js +9 -0
  70. package/dist/src/trajectory-host-handle.d.ts +12 -0
  71. package/dist/src/trajectory-host-handle.js +6 -0
  72. package/dist/src/trajectory.d.ts +128 -0
  73. package/dist/src/trajectory.js +594 -0
  74. package/dist/src/types.d.ts +960 -0
  75. package/dist/src/types.js +60 -0
  76. package/dist/src/utils.d.ts +57 -0
  77. package/dist/src/utils.js +225 -0
  78. package/dist/src/validation.d.ts +29 -0
  79. package/dist/src/validation.js +1100 -0
  80. package/dist/src/workflow-artifacts.d.ts +14 -0
  81. package/dist/src/workflow-artifacts.js +40 -0
  82. package/dist/starter/index.d.ts +1 -0
  83. package/dist/starter/index.js +103 -0
  84. package/dist/starter/index.js.map +6 -0
  85. package/dist/starter/review-loop.d.ts +72 -0
  86. package/dist/starter/review-loop.js +43 -0
  87. package/dist/starter/roles/developer.md +6 -0
  88. package/dist/starter/roles/oracle.md +18 -0
  89. package/dist/starter/roles/researcher.md +20 -0
  90. package/dist/starter/roles/reviewer.md +19 -0
  91. package/dist/starter/roles/scout.md +18 -0
  92. package/dist/subagents/index.d.ts +8 -0
  93. package/dist/subagents/index.js +8021 -0
  94. package/dist/subagents/index.js.map +6 -0
  95. package/dist/subagents/src/contracts.d.ts +173 -0
  96. package/dist/subagents/src/contracts.js +68 -0
  97. package/dist/subagents/src/decode.d.ts +15 -0
  98. package/dist/subagents/src/decode.js +182 -0
  99. package/dist/subagents/src/index.d.ts +12 -0
  100. package/dist/subagents/src/index.js +180 -0
  101. package/dist/subagents/src/manager.d.ts +3 -0
  102. package/dist/subagents/src/manager.js +1636 -0
  103. package/dist/subagents/src/navigator.d.ts +5 -0
  104. package/dist/subagents/src/navigator.js +627 -0
  105. package/dist/subagents/src/view.d.ts +70 -0
  106. package/dist/subagents/src/view.js +387 -0
  107. package/dist/subagents/src/worktree.d.ts +34 -0
  108. package/dist/subagents/src/worktree.js +68 -0
  109. package/dist/trajectory/assets/favicon.png +0 -0
  110. package/dist/trajectory/assets/index.html +772 -0
  111. package/dist/trajectory/assets/marked.min.js +6 -0
  112. package/dist/trajectory/assets/morphdom.min.js +1 -0
  113. package/dist/trajectory/index.d.ts +5 -0
  114. package/dist/trajectory/index.js +3459 -0
  115. package/dist/trajectory/index.js.map +6 -0
  116. package/dist/trajectory/src/assets/favicon.png +0 -0
  117. package/dist/trajectory/src/assets/index.html +772 -0
  118. package/dist/trajectory/src/assets/marked.min.js +6 -0
  119. package/dist/trajectory/src/assets/morphdom.min.js +1 -0
  120. package/dist/trajectory/src/export.d.ts +25 -0
  121. package/dist/trajectory/src/export.js +80 -0
  122. package/dist/trajectory/src/index.d.ts +26 -0
  123. package/dist/trajectory/src/index.js +732 -0
  124. package/dist/trajectory/src/server.d.ts +7 -0
  125. package/dist/trajectory/src/server.js +2534 -0
  126. package/dist/trajectory/src/server.js.map +6 -0
  127. package/examples/advanced-patterns/auto-model-router.mjs +78 -0
  128. package/examples/workflow-extension-template/README.md +37 -0
  129. package/examples/workflow-extension-template/extension.test.mjs +59 -0
  130. package/examples/workflow-extension-template/index.js +51 -0
  131. package/examples/workflow-extension-template/roles/reviewer.md +4 -0
  132. package/package.json +105 -0
  133. package/skills/pi-extensible-workflows/SKILL.md +160 -0
  134. package/src/agent-execution.ts +1533 -0
  135. package/src/background-widget.ts +1496 -0
  136. package/src/budget.ts +109 -0
  137. package/src/changelog.ts +114 -0
  138. package/src/decoders.ts +567 -0
  139. package/src/execution.ts +652 -0
  140. package/src/herdr.ts +240 -0
  141. package/src/host-delivery.ts +346 -0
  142. package/src/host-navigator.ts +871 -0
  143. package/src/host-phases.ts +264 -0
  144. package/src/host-recovery.ts +333 -0
  145. package/src/host-runtime.ts +373 -0
  146. package/src/host-view.ts +828 -0
  147. package/src/host.ts +1502 -0
  148. package/src/index.ts +29 -0
  149. package/src/io.ts +73 -0
  150. package/src/package-identity.ts +7 -0
  151. package/src/paths.ts +49 -0
  152. package/src/persistence.ts +11 -0
  153. package/src/pi-runtime-adapter.ts +247 -0
  154. package/src/pi-runtime-runner.ts +436 -0
  155. package/src/registry.ts +321 -0
  156. package/src/retention.ts +178 -0
  157. package/src/runtime/agent-runner.ts +158 -0
  158. package/src/runtime/index.ts +2 -0
  159. package/src/runtime/namespace.ts +16 -0
  160. package/src/runtime/workflow-result.ts +8 -0
  161. package/src/session-handoff.ts +78 -0
  162. package/src/session-lease.ts +125 -0
  163. package/src/store.ts +832 -0
  164. package/src/subagent-manager-handle.ts +8 -0
  165. package/src/tool-timing.ts +47 -0
  166. package/src/trajectory-contracts.ts +13 -0
  167. package/src/trajectory-host-handle.ts +16 -0
  168. package/src/trajectory.ts +543 -0
  169. package/src/types.ts +374 -0
  170. package/src/utils.ts +192 -0
  171. package/src/validation.ts +910 -0
  172. package/src/workflow-artifacts.ts +35 -0
  173. package/starter/index.ts +35 -0
  174. package/starter/prompts/council.md +16 -0
  175. package/starter/prompts/deep-research.md +16 -0
  176. package/starter/prompts/oracle.md +14 -0
  177. package/starter/prompts/parallel-review.md +22 -0
  178. package/starter/prompts/parallel-scout.md +16 -0
  179. package/starter/prompts/review-loop.md +14 -0
  180. package/starter/prompts/review.md +14 -0
  181. package/starter/prompts/scout.md +14 -0
  182. package/starter/review-loop.ts +73 -0
  183. package/starter/roles/developer.md +6 -0
  184. package/starter/roles/oracle.md +18 -0
  185. package/starter/roles/researcher.md +20 -0
  186. package/starter/roles/reviewer.md +19 -0
  187. package/starter/roles/scout.md +18 -0
  188. package/subagents/README.md +145 -0
  189. package/subagents/index.ts +8 -0
  190. package/subagents/src/contracts.ts +176 -0
  191. package/subagents/src/decode.ts +158 -0
  192. package/subagents/src/index.ts +194 -0
  193. package/subagents/src/manager.ts +1560 -0
  194. package/subagents/src/navigator.ts +488 -0
  195. package/subagents/src/view.ts +376 -0
  196. package/subagents/src/worktree.ts +99 -0
  197. package/trajectory/index.ts +5 -0
  198. package/trajectory/src/assets/favicon.png +0 -0
  199. package/trajectory/src/assets/index.html +772 -0
  200. package/trajectory/src/assets/marked.min.js +6 -0
  201. package/trajectory/src/assets/morphdom.min.js +1 -0
  202. package/trajectory/src/export.ts +85 -0
  203. package/trajectory/src/index.ts +604 -0
  204. package/trajectory/src/server.ts +447 -0
@@ -0,0 +1,1633 @@
1
+ import { existsSync, realpathSync, readFileSync } from "node:fs";
2
+ import { copyFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { Type } from "@earendil-works/pi-ai";
7
+ import { Compile } from "typebox/compile";
8
+ import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
9
+ ;
10
+ import { SerialLane, assertModelThinking, byPriorityThenName, deepFreeze, errorText, jsonObject, jsonValue, mergeWorkflowExtensionSettings, object, modelAliasName, modelCapability, resolveModelReference, resourcePatternHasMagic, selectResourcesByLayers, unmatchedResourcePatterns } from "./utils.js";
11
+ import { SETTLED_AGENT_STATES, WorkflowError, isContextFileScope, zeroAccounting } from "./types.js";
12
+ import { createLiveSessionHandoff } from "./session-handoff.js";
13
+ import { createToolTimingExtension } from "./tool-timing.js";
14
+ import { isEmptyAbortedAssistant, normalizePiMessage, normalizePiSessionEvent, runtimeProgressToAgentProgress } from "./pi-runtime-adapter.js";
15
+ import { createPiRuntimeAgentRunner, isRuntimeAgentProviderError, normalizePiRuntimeError } from "./pi-runtime-runner.js";
16
+ import { defaultWorkflowResultSchema } from "./runtime/workflow-result.js";
17
+ import { validateAgentOptions, validateSchema } from "./validation.js";
18
+ import { canonicalPath } from "./paths.js";
19
+ const localToolTimingExtension = createToolTimingExtension();
20
+ function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
21
+ if (!value)
22
+ return fallback;
23
+ assertModelThinking(value);
24
+ return resolveModelReference(value, aliases, knownModels, settingsPath);
25
+ }
26
+ function accounting(stats) {
27
+ return { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, cost: stats.cost };
28
+ }
29
+ const extensionDirectory = dirname(fileURLToPath(import.meta.url));
30
+ const workflowPackageRoot = basename(dirname(extensionDirectory)) === "dist" ? resolve(extensionDirectory, "../..") : resolve(extensionDirectory, "..");
31
+ const WORKFLOW_HOST_ENTRIES = new Set([
32
+ canonicalPath(resolve(workflowPackageRoot, "src/index.ts")),
33
+ canonicalPath(resolve(workflowPackageRoot, "dist/src/index.js")),
34
+ canonicalPath(resolve(workflowPackageRoot, "starter/index.ts")),
35
+ canonicalPath(resolve(workflowPackageRoot, "dist/starter/index.js")),
36
+ canonicalPath(resolve(workflowPackageRoot, "subagents/index.ts")),
37
+ canonicalPath(resolve(workflowPackageRoot, "dist/subagents/index.js")),
38
+ ]);
39
+ function canonicalExtensionSelector(selector, base = process.cwd()) {
40
+ const negated = selector.startsWith("!");
41
+ const body = negated ? selector.slice(1) : selector;
42
+ if (body === "*" || body === "**" || body.startsWith("**/"))
43
+ return selector;
44
+ const resolved = resolve(base, body);
45
+ if (resourcePatternHasMagic(body))
46
+ return `${negated ? "!" : ""}${resolved}`;
47
+ return `${negated ? "!" : ""}${canonicalPath(resolved)}`;
48
+ }
49
+ const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
50
+ function workflowSystemPromptPath(cwd, agentDir, projectTrusted) {
51
+ const projectPath = join(cwd, ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md");
52
+ if (projectTrusted && existsSync(projectPath))
53
+ return projectPath;
54
+ const globalPaths = [join(homedir(), ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md"), join(agentDir, WORKFLOW_DIRECTORY, "SYSTEM.md")];
55
+ return globalPaths.find((path) => existsSync(path));
56
+ }
57
+ function filterContextFiles(base, scopes, cwd, agentDir) {
58
+ const globalPath = canonicalPath(agentDir);
59
+ const cwdPath = canonicalPath(cwd);
60
+ return base.filter(({ path }) => {
61
+ const resolvedPath = canonicalPath(dirname(path));
62
+ return scopes.some((scope) => scope === "global" ? resolvedPath === globalPath : scope === "cwd" ? resolvedPath === cwdPath : resolvedPath !== globalPath && resolvedPath !== cwdPath);
63
+ });
64
+ }
65
+ function isPackageDirectoryProvider(value) { return typeof value === "function"; }
66
+ let piCodingAgentModule;
67
+ function loadPiPackageDirectory() {
68
+ return piCodingAgentModule ??= import("@earendil-works/pi-coding-agent").then((module) => {
69
+ const provider = module.getPackageDir;
70
+ if (provider === undefined)
71
+ return {};
72
+ if (!isPackageDirectoryProvider(provider))
73
+ return { error: "@earendil-works/pi-coding-agent exports a non-callable getPackageDir" };
74
+ try {
75
+ const directory = Reflect.apply(provider, module, []);
76
+ return typeof directory === "string" && directory.trim() ? { directory } : { error: "@earendil-works/pi-coding-agent returned an invalid package directory" };
77
+ }
78
+ catch (error) {
79
+ return { error: `getPackageDir failed: ${errorText(error)}` };
80
+ }
81
+ }).catch((error) => ({ error: `could not load @earendil-works/pi-coding-agent package metadata: ${errorText(error)}` }));
82
+ }
83
+ let nativePromptTemplateModule;
84
+ async function loadNativePromptTemplateModule() {
85
+ const packageDirectory = await loadPiPackageDirectory();
86
+ let promptTemplatePath;
87
+ if (packageDirectory.directory) {
88
+ const candidate = resolve(packageDirectory.directory, "dist/core/prompt-templates.js");
89
+ if (existsSync(candidate))
90
+ promptTemplatePath = candidate;
91
+ }
92
+ promptTemplatePath ??= resolve(dirname(fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"))), "core/prompt-templates.js");
93
+ const packageDirectoryError = packageDirectory.error ? ` Package directory lookup failed: ${packageDirectory.error}` : "";
94
+ return nativePromptTemplateModule ??= import(promptTemplatePath).then((module) => module).catch((error) => { throw new Error(`Could not load Pi prompt templates.${packageDirectoryError}`, { cause: error }); });
95
+ }
96
+ async function expandPromptTemplateForInspection(text, templates) {
97
+ return (await loadNativePromptTemplateModule()).expandPromptTemplate(text, templates);
98
+ }
99
+ async function preparePiPrompt(native, text) {
100
+ const session = native;
101
+ const diagnostics = [];
102
+ const runner = session._extensionRunner;
103
+ const baseSystemPrompt = typeof session._baseSystemPrompt === "string" ? session._baseSystemPrompt : session.systemPrompt ?? "";
104
+ const baseOptions = session._baseSystemPromptOptions;
105
+ const expandSkillCommand = session._expandSkillCommand;
106
+ const templates = session.promptTemplates;
107
+ if (!runner)
108
+ diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: extension runner is missing", source: "Pi session" });
109
+ if (typeof session._baseSystemPrompt !== "string")
110
+ diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt is missing", source: "Pi session" });
111
+ if (baseOptions === undefined)
112
+ diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: base system prompt options are missing", source: "Pi session" });
113
+ if (typeof expandSkillCommand !== "function")
114
+ diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: skill expansion is missing", source: "Pi session" });
115
+ if (!Array.isArray(templates))
116
+ diagnostics.push({ type: "error", message: "Pi prompt inspection seam is unavailable: prompt templates are missing", source: "Pi session" });
117
+ const unsubscribe = runner?.onError((error) => {
118
+ const value = error;
119
+ diagnostics.push({ type: "error", message: typeof value.error === "string" ? value.error : String(value.error ?? error), ...(typeof value.extensionPath === "string" ? { source: value.extensionPath } : {}) });
120
+ });
121
+ try {
122
+ let current = text;
123
+ let inputHandled = false;
124
+ if (runner?.hasHandlers("input")) {
125
+ const result = await runner.emitInput(current, undefined, "interactive");
126
+ if (result.action === "handled")
127
+ inputHandled = true;
128
+ else if (result.action === "transform" && typeof result.text === "string")
129
+ current = result.text;
130
+ }
131
+ let expandedPrompt = current;
132
+ if (!inputHandled) {
133
+ const skillExpanded = typeof expandSkillCommand === "function" ? expandSkillCommand.call(session, current) : current;
134
+ try {
135
+ expandedPrompt = await expandPromptTemplateForInspection(skillExpanded, Array.isArray(templates) ? templates : []);
136
+ }
137
+ catch (error) {
138
+ diagnostics.push({ type: "error", message: `Pi prompt template expansion is unavailable: ${errorText(error)}`, source: "Pi session" });
139
+ }
140
+ }
141
+ const result = !inputHandled && runner && baseOptions !== undefined ? await runner.emitBeforeAgentStart(expandedPrompt, undefined, baseSystemPrompt, baseOptions) : undefined;
142
+ const prepared = result;
143
+ return { prompt: text, expandedPrompt, systemPrompt: typeof prepared?.systemPrompt === "string" ? prepared.systemPrompt : baseSystemPrompt, systemPromptOptions: baseOptions, messages: prepared?.messages ?? [], inputHandled, diagnostics };
144
+ }
145
+ finally {
146
+ unsubscribe?.();
147
+ }
148
+ }
149
+ export async function createLocalPiSession(input) { return (await createLocalPiSessionHandle(input)).session; }
150
+ /**
151
+ * Hands the runtime the providers that extensions registered while loading.
152
+ *
153
+ * An extension that adds models (a proxy front-end, a gateway) registers them
154
+ * from its factory, and the loader parks those registrations on the extension
155
+ * runtime rather than applying them. Pi's own session assembly drains that
156
+ * queue; a workflow agent builds its runtime here by hand, so without this the
157
+ * models an extension provides stay unknown and every agent asking for one
158
+ * fails with UNKNOWN_MODEL.
159
+ */
160
+ export function flushExtensionProviders(resourceLoader, modelRuntime) {
161
+ const { runtime } = resourceLoader.getExtensions();
162
+ const failures = [];
163
+ for (const { name, config } of runtime.pendingProviderRegistrations) {
164
+ // A provider that cannot be registered must not stop the agent from
165
+ // starting: the model it offers may not be the one this agent asked for.
166
+ // The reason is kept, though — if the model does turn out to be missing,
167
+ // "unknown model" alone sends the reader looking in the wrong place.
168
+ try {
169
+ modelRuntime.registerProvider(name, config);
170
+ }
171
+ catch (error) {
172
+ failures.push(`${name}: ${errorText(error)}`);
173
+ }
174
+ }
175
+ runtime.pendingProviderRegistrations = [];
176
+ for (const { provider } of runtime.pendingNativeProviderRegistrations) {
177
+ try {
178
+ modelRuntime.registerNativeProvider(provider);
179
+ }
180
+ catch (error) {
181
+ failures.push(`${provider.id || "native provider"}: ${errorText(error)}`);
182
+ }
183
+ }
184
+ runtime.pendingNativeProviderRegistrations = [];
185
+ return failures;
186
+ }
187
+ async function createLocalPiSessionHandle(input, sessionStartEvent) {
188
+ const agentDir = input.agentDir ?? getAgentDir();
189
+ const systemPromptSource = workflowSystemPromptPath(input.cwd, agentDir, input.resourcePolicy?.projectTrusted ?? true);
190
+ const systemPromptOptions = input.systemPrompt !== undefined ? { systemPromptOverride: () => input.systemPrompt } : systemPromptSource !== undefined ? { systemPrompt: systemPromptSource } : {};
191
+ const contextFilesOverride = input.contextFiles === undefined ? undefined : (base) => ({ ...base, agentsFiles: filterContextFiles(base.agentsFiles, input.contextFiles ?? [], input.cwd, agentDir) });
192
+ const manager = input.sessionManager ?? (input.sessionPath ? SessionManager.open(input.sessionPath, join(agentDir, "sessions"), input.cwd) : input.agentDir ? SessionManager.create(input.cwd, join(agentDir, "sessions")) : SessionManager.create(input.cwd));
193
+ if (!input.sessionPath)
194
+ manager.appendSessionInfo(input.sessionLabel);
195
+ const modelRuntime = await ModelRuntime.create({ authPath: join(agentDir, "auth.json"), modelsPath: join(agentDir, "models.json") });
196
+ const customTools = [...(input.customTools ?? []), ...(input.resultTool ? [input.resultTool] : [])];
197
+ const tools = [...new Set([...input.tools, ...customTools.map(({ name }) => name)])];
198
+ let settingsManager;
199
+ let resourceLoader;
200
+ const policy = input.resourcePolicy;
201
+ if (policy) {
202
+ settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: false });
203
+ settingsManager.setProjectTrusted(policy.projectTrusted);
204
+ const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
205
+ const resolved = await packageManager.resolve();
206
+ const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => canonicalPath(path)))];
207
+ const selectorSources = policy.selectorSources;
208
+ const extensionLayers = [selectorSources.global.extensions, selectorSources.project.extensions, selectorSources.role?.extensions, selectorSources.call?.extensions];
209
+ const extensionSelectors = extensionLayers.flatMap((layer) => (layer ?? []).map((original) => ({ original, matching: canonicalExtensionSelector(original, input.cwd) })));
210
+ const selectedExtensions = selectResourcesByLayers(extensionLayers.map((layer) => layer?.map((selector) => canonicalExtensionSelector(selector, input.cwd))), discoveredExtensions);
211
+ const extensionPaths = selectedExtensions.filter((path) => !WORKFLOW_HOST_ENTRIES.has(path));
212
+ const unmatchedExtensions = extensionSelectors.filter(({ matching }) => unmatchedResourcePatterns([matching], discoveredExtensions).length > 0).map(({ original }) => original);
213
+ policy.selectedExtensions = selectedExtensions;
214
+ policy.unmatchedExtensions = unmatchedExtensions;
215
+ const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
216
+ const updateSkillMatches = (skills) => {
217
+ const names = [...new Set(skills.map(({ name }) => name))];
218
+ const skillLayers = [selectorSources.global.skills, selectorSources.project.skills, selectorSources.role?.skills, selectorSources.call?.skills];
219
+ const selectedSkills = selectResourcesByLayers(skillLayers, names);
220
+ policy.selectedSkills = selectedSkills;
221
+ policy.unmatchedSkills = unmatchedResourcePatterns(skillLayers.flatMap((layer) => layer ?? []), names);
222
+ return new Set(names.filter((name) => !selectedSkills.includes(name)));
223
+ };
224
+ resourceLoader = new DefaultResourceLoader({
225
+ cwd: input.cwd,
226
+ agentDir,
227
+ settingsManager,
228
+ noExtensions: true,
229
+ additionalExtensionPaths: extensionPaths,
230
+ noSkills: true,
231
+ additionalSkillPaths: [...new Set([...skillPaths, ...(input.additionalSkillPaths ?? [])])],
232
+ ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
233
+ ...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}),
234
+ skillsOverride: (base) => {
235
+ const disabledSkills = updateSkillMatches(base.skills);
236
+ return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
237
+ },
238
+ ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
239
+ ...systemPromptOptions,
240
+ });
241
+ await resourceLoader.reload();
242
+ }
243
+ else {
244
+ settingsManager = SettingsManager.create(input.cwd, agentDir, { projectTrusted: true });
245
+ const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
246
+ const resolved = await packageManager.resolve();
247
+ const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled }) => enabled).map(({ path }) => canonicalPath(path)).filter((path) => !WORKFLOW_HOST_ENTRIES.has(path)))];
248
+ resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, settingsManager, noExtensions: true, additionalExtensionPaths: extensionPaths, ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: [...input.additionalSkillPaths] } : {}), ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(contextFilesOverride ? { agentsFilesOverride: contextFilesOverride } : {}), ...systemPromptOptions, ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
249
+ await resourceLoader.reload();
250
+ }
251
+ const providerFailures = flushExtensionProviders(resourceLoader, modelRuntime);
252
+ await modelRuntime.refresh({ allowNetwork: false });
253
+ const model = modelRuntime.getModel(input.model.provider, input.model.model);
254
+ if (!model) {
255
+ // A provider that failed to register is the likeliest reason its models are
256
+ // missing, so the failure travels with the complaint rather than being
257
+ // swallowed — otherwise a misconfigured gateway reads as a typo in a model
258
+ // name.
259
+ const because = providerFailures.length > 0 ? ` (provider registration failed — ${providerFailures.join("; ")})` : "";
260
+ throw new WorkflowError("UNKNOWN_MODEL", `Unknown model: ${input.model.provider}/${input.model.model}${because}`);
261
+ }
262
+ const effectiveSessionStartEvent = input.settings === undefined && sessionStartEvent === undefined ? undefined : { ...(sessionStartEvent ?? { type: "session_start", reason: "startup" }), settings: input.settings ?? Object.freeze({}) };
263
+ 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 });
264
+ const nativeDispose = session.dispose.bind(session);
265
+ let disposal;
266
+ const shutdown = (reason, targetSessionFile) => {
267
+ if (disposal)
268
+ return disposal;
269
+ disposal = (async () => {
270
+ try {
271
+ if (session.extensionRunner.hasHandlers("session_shutdown"))
272
+ await session.extensionRunner.emit({ type: "session_shutdown", reason, ...(targetSessionFile === undefined ? {} : { targetSessionFile }) });
273
+ }
274
+ finally {
275
+ nativeDispose();
276
+ }
277
+ })();
278
+ return disposal;
279
+ };
280
+ Object.assign(session, { dispose: () => shutdown("quit") });
281
+ try {
282
+ await session.bindExtensions({ mode: "print" });
283
+ }
284
+ catch (error) {
285
+ await shutdown("quit").catch(() => undefined);
286
+ throw error;
287
+ }
288
+ const resourcePaths = { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) => canonicalPath(resolvedPath)), skills: resourceLoader.getSkills().skills.map(({ filePath }) => canonicalPath(filePath)) };
289
+ const resourceInspection = () => {
290
+ const extensions = resourceLoader.getExtensions();
291
+ const skills = resourceLoader.getSkills();
292
+ const diagnostics = [...extensions.errors.map(({ path, error }) => ({ type: "error", message: error, source: path })), ...skills.diagnostics.map(({ type, path, message }) => ({ type, message, ...(path ? { source: path } : {}) }))];
293
+ const systemSource = systemPromptSource;
294
+ return { extensions: resourceLoader.getExtensions().extensions.filter(({ path }) => !path.startsWith("<")).map(({ resolvedPath }) => canonicalPath(resolvedPath)), skills: skills.skills.map(({ name }) => name), diagnostics, ...(systemSource ? { systemPromptSource: systemSource } : resourceLoader.getSystemPrompt() !== undefined ? { systemPromptSource: "Pi resource loader" } : {}) };
295
+ };
296
+ const managedSession = Object.assign(session, {
297
+ getLeafId: () => manager.getLeafId(),
298
+ getToolDefinitions: () => session.getAllTools().map(({ name, description, parameters, promptGuidelines }) => ({ name, description, parameters, ...(promptGuidelines ? { promptGuidelines } : {}) })),
299
+ preparePrompt: (text) => preparePiPrompt(session, text),
300
+ getResourceInspection: resourceInspection,
301
+ herdrModelContext: { model: session.model, modelRegistry: session.extensionRunner.getModelRegistry() },
302
+ herdrResourcePaths: resourcePaths,
303
+ herdrContextFiles: resourceLoader.getAgentsFiles().agentsFiles,
304
+ });
305
+ return { session: managedSession, shutdown };
306
+ }
307
+ function workflowAgentMessage(message) { return normalizePiMessage(message); }
308
+ function latestUsableAssistant(messages) { for (let index = messages.length - 1; index >= 0; index -= 1) {
309
+ const candidate = workflowAgentMessage(messages[index]);
310
+ if (candidate?.role === "assistant" && !isEmptyAbortedAssistant(candidate))
311
+ return candidate;
312
+ } return undefined; }
313
+ function workflowAgentStats(stats) { return { tokens: { input: stats.tokens.input, output: stats.tokens.output, cacheRead: stats.tokens.cacheRead, cacheWrite: stats.tokens.cacheWrite, total: stats.tokens.total }, cost: stats.cost }; }
314
+ function workflowAgentState(native, prepared) {
315
+ const tools = native.agent?.state.tools.map(({ name }) => name) ?? prepared.tools;
316
+ const model = native.model?.provider && (native.model.model ?? native.model.id) ? { provider: native.model.provider, model: native.model.model ?? native.model.id ?? prepared.model.model, ...(prepared.model.thinking ? { thinking: prepared.model.thinking } : {}) } : { ...prepared.model };
317
+ return { model, ...(model.thinking ? { thinking: model.thinking } : {}), tools: [...tools], ...(native.systemPrompt === undefined ? {} : { systemPrompt: native.systemPrompt }) };
318
+ }
319
+ function notifyPiSessionEvent(notify, event, settledOnly = false) {
320
+ const normalized = normalizePiSessionEvent(event);
321
+ if (!normalized || (settledOnly && normalized.type !== "agent_settled"))
322
+ return undefined;
323
+ return notify(normalized);
324
+ }
325
+ export async function createLocalWorkflowAgentSession(prepared, context) {
326
+ const input = {
327
+ cwd: prepared.cwd, model: { ...prepared.model }, tools: [...prepared.tools], sessionLabel: prepared.sessionLabel, ...(prepared.sessionPath ? { sessionPath: prepared.sessionPath } : {}),
328
+ ...(prepared.agentDir ? { agentDir: prepared.agentDir } : {}), ...(prepared.customTools?.length ? { customTools: [...prepared.customTools] } : {}),
329
+ ...(prepared.resultTool ? { resultTool: prepared.resultTool } : {}), ...(prepared.systemPrompt === undefined ? {} : { systemPrompt: prepared.systemPrompt }),
330
+ ...(prepared.systemPromptAppend ? { systemPromptAppend: prepared.systemPromptAppend } : {}), extensionFactories: [...(prepared.extensionFactories ?? []), ...(prepared.extensionFactories?.includes(localToolTimingExtension) ? [] : [localToolTimingExtension])],
331
+ ...(prepared.additionalSkillPaths?.length ? { additionalSkillPaths: [...prepared.additionalSkillPaths] } : {}), ...(prepared.contextFiles === undefined ? {} : { contextFiles: [...prepared.contextFiles] }), ...(prepared.resourcePolicy ? { resourcePolicy: structuredClone(prepared.resourcePolicy) } : {}), settings: context.settings, ...(prepared.options ? { options: { ...prepared.options } } : {}),
332
+ };
333
+ let nativeHandle = await createLocalPiSessionHandle(input);
334
+ let native = nativeHandle.session;
335
+ let nativeShutdownReason;
336
+ let disposal;
337
+ let aborting;
338
+ const lifecycle = new SerialLane();
339
+ /*
340
+ * Lifecycle transitions: active -> suspending -> suspended; active|suspended -> resuming -> active;
341
+ * suspending|resuming -> suspended on failure; active|suspending|suspended|resuming -> disposing -> disposed.
342
+ * Only suspend, resume, and disposeSession, including their queued lifecycle operations, may write state.
343
+ */
344
+ let state = "active";
345
+ let suspendOperation;
346
+ let resumeOperation;
347
+ let resumingActive = false;
348
+ const prompts = new Set();
349
+ const listeners = new Set();
350
+ let coreUnsubscribe;
351
+ let sessionUnsubscribe;
352
+ const eventNotifications = new Set();
353
+ const noEventNotificationFailure = Symbol("no event notification failure");
354
+ let eventNotificationFailure = noEventNotificationFailure;
355
+ let observationGeneration = 0;
356
+ const notify = async (event) => { for (const listener of listeners)
357
+ await listener(event); };
358
+ const trackNotification = (notification, generation) => {
359
+ if (!notification)
360
+ return;
361
+ if (generation !== observationGeneration) {
362
+ void notification.then(() => undefined, () => undefined);
363
+ return;
364
+ }
365
+ eventNotifications.add(notification);
366
+ void notification.then(() => {
367
+ eventNotifications.delete(notification);
368
+ }, (error) => {
369
+ eventNotifications.delete(notification);
370
+ if (generation === observationGeneration && eventNotificationFailure === noEventNotificationFailure) {
371
+ eventNotificationFailure = error;
372
+ }
373
+ });
374
+ };
375
+ const flushNotifications = async () => {
376
+ let failure;
377
+ let failed = false;
378
+ const takeFailure = () => {
379
+ if (eventNotificationFailure === noEventNotificationFailure)
380
+ return;
381
+ if (!failed) {
382
+ failure = eventNotificationFailure;
383
+ failed = true;
384
+ }
385
+ eventNotificationFailure = noEventNotificationFailure;
386
+ };
387
+ takeFailure();
388
+ while (eventNotifications.size > 0) {
389
+ const pending = [...eventNotifications];
390
+ const outcomes = await Promise.allSettled(pending);
391
+ for (const outcome of outcomes) {
392
+ if (outcome.status === "rejected" && !failed) {
393
+ failure = outcome.reason;
394
+ failed = true;
395
+ }
396
+ }
397
+ for (const notification of pending)
398
+ eventNotifications.delete(notification);
399
+ takeFailure();
400
+ }
401
+ takeFailure();
402
+ if (failed)
403
+ throw failure;
404
+ };
405
+ const unbindNative = () => {
406
+ observationGeneration += 1;
407
+ coreUnsubscribe?.();
408
+ sessionUnsubscribe?.();
409
+ coreUnsubscribe = undefined;
410
+ sessionUnsubscribe = undefined;
411
+ eventNotifications.clear();
412
+ eventNotificationFailure = noEventNotificationFailure;
413
+ };
414
+ const bindNative = (next) => {
415
+ unbindNative();
416
+ const generation = observationGeneration;
417
+ coreUnsubscribe = next.agent?.subscribe?.((event) => { trackNotification(notifyPiSessionEvent(notify, event), generation); });
418
+ sessionUnsubscribe = next.subscribe?.((event) => { trackNotification(notifyPiSessionEvent(notify, event, true), generation); });
419
+ };
420
+ const enqueue = (operation) => lifecycle.run(operation);
421
+ const shutdownNative = (reason, targetSessionFile) => {
422
+ nativeShutdownReason = reason;
423
+ return nativeHandle.shutdown(reason, targetSessionFile);
424
+ };
425
+ bindNative(native);
426
+ const startAbort = () => {
427
+ if (state !== "active" && state !== "suspending" && !(state === "resuming" && resumingActive))
428
+ return Promise.resolve();
429
+ return aborting ??= Promise.resolve().then(() => native.abort?.()).then(() => undefined).finally(() => { aborting = undefined; });
430
+ };
431
+ const suspend = async () => {
432
+ if (state === "disposing" || state === "disposed")
433
+ throw new WorkflowError("INTERNAL_ERROR", "Local workflow session is closing");
434
+ if (state === "suspended" || !native.sessionFile)
435
+ return;
436
+ if (state === "suspending") {
437
+ await suspendOperation;
438
+ return;
439
+ }
440
+ if (state === "resuming") {
441
+ await resumeOperation;
442
+ return suspend();
443
+ }
444
+ const sessionFile = native.sessionFile;
445
+ state = "suspending";
446
+ suspendOperation = enqueue(async () => {
447
+ await Promise.allSettled(prompts);
448
+ if (state !== "suspending") {
449
+ if (isClosing())
450
+ throw new WorkflowError("INTERNAL_ERROR", "Local workflow session is closing");
451
+ return;
452
+ }
453
+ unbindNative();
454
+ try {
455
+ await shutdownNative("resume", sessionFile);
456
+ }
457
+ catch (error) {
458
+ if (!isClosing())
459
+ state = "suspended";
460
+ throw error;
461
+ }
462
+ if (!isClosing())
463
+ state = "suspended";
464
+ });
465
+ await suspendOperation;
466
+ };
467
+ const isActive = () => state === "active";
468
+ const isClosing = () => state === "disposing" || state === "disposed";
469
+ const resume = async () => {
470
+ if (state === "suspending") {
471
+ await suspendOperation;
472
+ return resume();
473
+ }
474
+ if (state === "resuming") {
475
+ if (resumeOperation)
476
+ await resumeOperation;
477
+ return;
478
+ }
479
+ if (state === "disposing" || state === "disposed" || !native.sessionFile)
480
+ return;
481
+ const sessionFile = native.sessionFile;
482
+ const wasSuspended = state === "suspended";
483
+ resumingActive = !wasSuspended;
484
+ state = "resuming";
485
+ const operation = enqueue(async () => {
486
+ try {
487
+ await Promise.allSettled(prompts);
488
+ if (!wasSuspended) {
489
+ unbindNative();
490
+ await shutdownNative("resume", sessionFile);
491
+ }
492
+ if (isClosing())
493
+ return;
494
+ const nextHandle = await createLocalPiSessionHandle({ ...input, sessionPath: sessionFile }, { type: "session_start", reason: "resume", previousSessionFile: sessionFile, settings: input.settings ?? Object.freeze({}) });
495
+ nativeHandle = nextHandle;
496
+ native = nextHandle.session;
497
+ nativeShutdownReason = undefined;
498
+ if (isClosing()) {
499
+ await shutdownNative("quit");
500
+ return;
501
+ }
502
+ bindNative(native);
503
+ state = "active";
504
+ }
505
+ catch (error) {
506
+ if (state === "resuming")
507
+ state = "suspended";
508
+ throw error;
509
+ }
510
+ });
511
+ resumeOperation = operation;
512
+ try {
513
+ await operation;
514
+ }
515
+ finally {
516
+ if (resumeOperation === operation) {
517
+ resumeOperation = undefined;
518
+ resumingActive = false;
519
+ }
520
+ }
521
+ };
522
+ const disposeSession = async () => {
523
+ if (disposal) {
524
+ await disposal;
525
+ return;
526
+ }
527
+ if (state === "disposed")
528
+ return;
529
+ const abort = startAbort();
530
+ state = "disposing";
531
+ disposal = enqueue(async () => {
532
+ try {
533
+ try {
534
+ await abort;
535
+ }
536
+ catch { /* Abort failure must not prevent the prompt from settling. */ }
537
+ await Promise.allSettled(prompts);
538
+ unbindNative();
539
+ if (nativeShutdownReason === "resume") {
540
+ const sessionFile = native.sessionFile;
541
+ if (sessionFile) {
542
+ const nextHandle = await createLocalPiSessionHandle({ ...input, sessionPath: sessionFile }, { type: "session_start", reason: "resume", previousSessionFile: sessionFile, settings: input.settings ?? Object.freeze({}) });
543
+ nativeHandle = nextHandle;
544
+ native = nextHandle.session;
545
+ nativeShutdownReason = undefined;
546
+ }
547
+ }
548
+ if (nativeShutdownReason === undefined)
549
+ await shutdownNative("quit");
550
+ }
551
+ finally {
552
+ state = "disposed";
553
+ }
554
+ });
555
+ await disposal;
556
+ };
557
+ const reference = { transport: "local", sessionId: native.sessionId, ...(native.sessionFile ? { locator: { sessionFile: native.sessionFile } } : {}) };
558
+ const session = {
559
+ reference,
560
+ getHerdrResourcePaths: () => native.herdrResourcePaths,
561
+ getHerdrContextFiles: () => native.herdrContextFiles,
562
+ getHerdrModelContext: () => native.herdrModelContext,
563
+ getState: () => Object.freeze(workflowAgentState(native, prepared)),
564
+ getResourceInspection: () => native.getResourceInspection(),
565
+ getSessionStats: () => workflowAgentStats(native.getSessionStats()),
566
+ getLastAssistant: () => latestUsableAssistant(native.messages),
567
+ subscribe(listener) { listeners.add(listener); listener({ type: "state_changed", state: workflowAgentState(native, prepared) }); return () => listeners.delete(listener); },
568
+ subscribeAsync(listener) {
569
+ listeners.add(listener);
570
+ let notification;
571
+ try {
572
+ notification = Promise.resolve(listener({ type: "state_changed", state: workflowAgentState(native, prepared) }));
573
+ }
574
+ catch (error) {
575
+ notification = Promise.resolve().then(() => { throw error; });
576
+ }
577
+ trackNotification(notification, observationGeneration);
578
+ return () => listeners.delete(listener);
579
+ },
580
+ async prompt(text) {
581
+ if (!isActive())
582
+ throw new WorkflowError("INTERNAL_ERROR", "Local workflow session is not active");
583
+ const prompt = (async () => {
584
+ let promptFailure;
585
+ let promptFailed = false;
586
+ try {
587
+ await native.prompt(text);
588
+ }
589
+ catch (error) {
590
+ promptFailure = error;
591
+ promptFailed = true;
592
+ }
593
+ let notificationFailure;
594
+ let notificationFailed = false;
595
+ try {
596
+ await flushNotifications();
597
+ }
598
+ catch (error) {
599
+ notificationFailure = error;
600
+ notificationFailed = true;
601
+ }
602
+ if (promptFailed)
603
+ throw promptFailure;
604
+ if (notificationFailed)
605
+ throw notificationFailure;
606
+ const assistant = latestUsableAssistant(native.messages);
607
+ return assistant ? { assistant } : {};
608
+ })();
609
+ prompts.add(prompt);
610
+ try {
611
+ return await prompt;
612
+ }
613
+ finally {
614
+ prompts.delete(prompt);
615
+ }
616
+ },
617
+ async steer(text) {
618
+ if (!isActive())
619
+ throw new WorkflowError("INTERNAL_ERROR", "Local workflow session is not active");
620
+ if (!native.steer)
621
+ throw new WorkflowError("INTERNAL_ERROR", "Local workflow session does not support steering");
622
+ await native.steer(text);
623
+ },
624
+ async abort() { if (!isActive())
625
+ return; await startAbort(); },
626
+ suspendForHandoff: suspend,
627
+ resumeFromHandoff: resume,
628
+ dispose: disposeSession,
629
+ };
630
+ return session;
631
+ }
632
+ export const localAgentTransport = Object.freeze({ id: "local", createSession: createLocalWorkflowAgentSession });
633
+ function changedOption(options, baseline, key) { return JSON.stringify(options[key]) !== JSON.stringify(baseline[key]); }
634
+ function isChildAgentToolParams(value) {
635
+ if (!jsonObject(value) || typeof value.prompt !== "string" || typeof value.label !== "string")
636
+ return false;
637
+ if (value.tools !== undefined && (!Array.isArray(value.tools) || value.tools.some((tool) => typeof tool !== "string")))
638
+ return false;
639
+ if (value.skills !== undefined && (!Array.isArray(value.skills) || value.skills.some((skill) => typeof skill !== "string")))
640
+ return false;
641
+ if (value.extensions !== undefined && (!Array.isArray(value.extensions) || value.extensions.some((extension) => typeof extension !== "string")))
642
+ return false;
643
+ if (value.model !== undefined && typeof value.model !== "string")
644
+ return false;
645
+ if (value.role !== undefined && (typeof value.role !== "string" || !value.role.trim()))
646
+ return false;
647
+ if (value.contextFiles !== undefined && (!Array.isArray(value.contextFiles) || !value.contextFiles.every(isContextFileScope)))
648
+ return false;
649
+ if (value.outputSchema !== undefined && !jsonObject(value.outputSchema))
650
+ return false;
651
+ if (value.retries !== undefined && (typeof value.retries !== "number" || !Number.isInteger(value.retries) || value.retries < 0))
652
+ return false;
653
+ if (value.timeoutMs !== undefined && (value.timeoutMs !== null && (typeof value.timeoutMs !== "number" || !Number.isInteger(value.timeoutMs) || value.timeoutMs < 1)))
654
+ return false;
655
+ return true;
656
+ }
657
+ function fallbackSetupContext(root, options, signal) {
658
+ const identity = options.agentIdentity ?? { structuralPath: [], callSite: options.label, occurrence: 1 };
659
+ const run = root.runContext ?? Object.freeze({ cwd: root.cwd, sessionId: "", runId: "", workflow: Object.freeze({ name: options.workflowName }), args: null, signal });
660
+ return { run, identity: Object.freeze({ ...identity, structuralPath: Object.freeze([...identity.structuralPath]) }), ...(options.tuiIndex === undefined ? {} : { tuiIndex: options.tuiIndex }), ...(options.tuiLabel === undefined ? {} : { tuiLabel: options.tuiLabel }) };
661
+ }
662
+ function resourcePolicySummary(policy, tools = policy.selectedTools ?? []) {
663
+ return {
664
+ selectors: { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], tools: [...(policy.effective.tools ?? [])] },
665
+ skills: [...(policy.selectedSkills ?? [])], extensions: [...(policy.selectedExtensions ?? [])], tools: [...tools],
666
+ unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions], unmatchedTools: [...(policy.unmatchedTools ?? [])],
667
+ selectorSources: policy.selectorSources,
668
+ };
669
+ }
670
+ const RESOURCE_SELECTOR_KEYS = ["skills", "extensions", "tools"];
671
+ function resourceSelectorLayers(sources, key) {
672
+ return [sources.global[key], sources.project[key], sources.role?.[key], sources.call?.[key]];
673
+ }
674
+ function selectorListWidened(ceiling, candidate) {
675
+ let candidateIndex = 0;
676
+ for (const pattern of ceiling) {
677
+ const matchIndex = candidate.indexOf(pattern, candidateIndex);
678
+ if (matchIndex < candidateIndex)
679
+ return true;
680
+ if (candidate.slice(candidateIndex, matchIndex).some((entry) => !entry.startsWith("!")))
681
+ return true;
682
+ candidateIndex = matchIndex + 1;
683
+ }
684
+ return candidate.slice(candidateIndex).some((entry) => !entry.startsWith("!"));
685
+ }
686
+ function resourcePolicyWidened(ceiling, candidate) {
687
+ if (!ceiling)
688
+ return false;
689
+ if (!candidate)
690
+ return true;
691
+ if (!ceiling.projectTrusted && candidate.projectTrusted)
692
+ return true;
693
+ for (const key of RESOURCE_SELECTOR_KEYS) {
694
+ const ceilingLayers = resourceSelectorLayers(ceiling.selectorSources, key);
695
+ const candidateLayers = resourceSelectorLayers(candidate.selectorSources, key);
696
+ for (let index = 0; index < ceilingLayers.length; index += 1)
697
+ if (selectorListWidened(ceilingLayers[index] ?? [], candidateLayers[index] ?? []))
698
+ return true;
699
+ }
700
+ if (selectorListWidened(ceiling.effective.skills, candidate.effective.skills))
701
+ return true;
702
+ if (selectorListWidened(ceiling.effective.extensions, candidate.effective.extensions))
703
+ return true;
704
+ return selectorListWidened(ceiling.effective.tools ?? [], candidate.effective.tools ?? []);
705
+ }
706
+ function selectorNarrowing(ceiling, candidate) {
707
+ const narrowing = [];
708
+ let candidateIndex = 0;
709
+ for (const pattern of ceiling) {
710
+ const matchIndex = candidate.indexOf(pattern, candidateIndex);
711
+ if (matchIndex < candidateIndex)
712
+ return [];
713
+ for (const entry of candidate.slice(candidateIndex, matchIndex))
714
+ if (entry.startsWith("!"))
715
+ narrowing.push(entry);
716
+ candidateIndex = matchIndex + 1;
717
+ }
718
+ for (const entry of candidate.slice(candidateIndex))
719
+ if (entry.startsWith("!"))
720
+ narrowing.push(entry);
721
+ return narrowing;
722
+ }
723
+ function appendResourcePolicyNarrowing(ceiling, candidate) {
724
+ if (!ceiling || !candidate)
725
+ return;
726
+ const suffixes = {
727
+ skills: selectorNarrowing(ceiling.effective.skills, candidate.effective.skills),
728
+ extensions: selectorNarrowing(ceiling.effective.extensions, candidate.effective.extensions),
729
+ tools: selectorNarrowing(ceiling.effective.tools ?? [], candidate.effective.tools ?? []),
730
+ };
731
+ if (!suffixes.skills.length && !suffixes.extensions.length && !suffixes.tools.length)
732
+ return;
733
+ const call = candidate.selectorSources.call ?? {};
734
+ candidate.selectorSources = {
735
+ ...candidate.selectorSources,
736
+ call: {
737
+ ...call,
738
+ ...(suffixes.skills.length ? { skills: [...(call.skills ?? []), ...suffixes.skills] } : {}),
739
+ ...(suffixes.extensions.length ? { extensions: [...(call.extensions ?? []), ...suffixes.extensions] } : {}),
740
+ ...(suffixes.tools.length ? { tools: [...(call.tools ?? []), ...suffixes.tools] } : {}),
741
+ },
742
+ };
743
+ }
744
+ function packageRoot(start) {
745
+ let current = dirname(realpathSync(start));
746
+ for (;;) {
747
+ const candidates = [current, join(current, "..", "@earendil-works", "pi-coding-agent"), join(current, "..", "pi-coding-agent")];
748
+ for (const candidate of candidates) {
749
+ const packageJsonPath = join(candidate, "package.json");
750
+ if (!existsSync(packageJsonPath))
751
+ continue;
752
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
753
+ if (object(packageJson) && typeof packageJson.name === "string" && packageJson.name === "@earendil-works/pi-coding-agent")
754
+ return candidate;
755
+ }
756
+ const parent = dirname(current);
757
+ if (parent === current)
758
+ return undefined;
759
+ current = parent;
760
+ }
761
+ }
762
+ function piPackageEntrypoint(root) {
763
+ const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
764
+ if (!object(packageJson))
765
+ throw new Error("the @earendil-works/pi-coding-agent package does not declare a pi CLI");
766
+ const bin = packageJson.bin;
767
+ const cli = typeof bin === "string" ? bin : object(bin) && typeof bin.pi === "string" ? bin.pi : undefined;
768
+ if (!cli)
769
+ throw new Error("the @earendil-works/pi-coding-agent package does not declare a pi CLI");
770
+ const entrypoint = resolve(root, cli);
771
+ if (!existsSync(entrypoint))
772
+ throw new Error(`the originating Pi CLI entrypoint is unavailable: ${entrypoint}`);
773
+ return entrypoint;
774
+ }
775
+ async function resolvePiRuntime() {
776
+ const executableName = basename(process.execPath).toLowerCase();
777
+ const bunBinary = ["$bunfs", "~BUN", "%7EBUN"].some((marker) => import.meta.url.includes(marker)) || Boolean(process.versions.bun && !["bun", "bun.exe"].includes(executableName));
778
+ if (bunBinary)
779
+ return { runtime: Object.freeze({ executable: process.execPath }) };
780
+ const errors = [];
781
+ const packageDirectory = await loadPiPackageDirectory();
782
+ if (packageDirectory.error)
783
+ errors.push(packageDirectory.error);
784
+ if (packageDirectory.directory) {
785
+ try {
786
+ return { runtime: Object.freeze({ executable: process.execPath, entrypoint: piPackageEntrypoint(packageDirectory.directory) }) };
787
+ }
788
+ catch (error) {
789
+ errors.push(`package directory resolution failed: ${errorText(error)}`);
790
+ }
791
+ }
792
+ try {
793
+ const packageEntry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
794
+ const root = packageRoot(packageEntry);
795
+ if (!root)
796
+ throw new Error("could not locate the @earendil-works/pi-coding-agent package root");
797
+ return { runtime: Object.freeze({ executable: process.execPath, entrypoint: piPackageEntrypoint(root) }) };
798
+ }
799
+ catch (error) {
800
+ errors.push(`import.meta.resolve fallback failed: ${errorText(error)}`);
801
+ return { error: errors.join("; ") };
802
+ }
803
+ }
804
+ const piRuntimeResolution = resolvePiRuntime();
805
+ function workflowAgentPrompt(task, options, attempt, previousError) {
806
+ 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");
807
+ const instruction = options.budget?.instruction();
808
+ return `${basePrompt}\n\nTask:\n${task}${instruction ? `\n\n${instruction}` : ""}`;
809
+ }
810
+ async function preparedAgentSession(input, initialPrompt) {
811
+ const { runtime: piRuntime, error: piRuntimeError } = await piRuntimeResolution;
812
+ const systemPromptPath = input.systemPrompt === undefined ? workflowSystemPromptPath(input.cwd, input.agentDir ?? getAgentDir(), input.resourcePolicy?.projectTrusted ?? true) : undefined;
813
+ const prepared = {
814
+ cwd: input.cwd, model: Object.freeze({ ...input.model }), tools: Object.freeze([...input.tools]), sessionLabel: input.sessionLabel, ...(input.sessionPath ? { sessionPath: input.sessionPath } : {}), ...(initialPrompt === undefined ? {} : { initialPrompt }),
815
+ ...(input.agentDir ? { agentDir: input.agentDir } : {}), ...(input.customTools?.length ? { customTools: Object.freeze([...input.customTools]) } : {}), ...(input.resultTool ? { resultTool: input.resultTool } : {}), ...(input.options ? { options: Object.freeze(structuredClone(input.options)) } : {}),
816
+ ...(piRuntime ? { piRuntime } : {}), ...(piRuntimeError ? { piRuntimeError } : {}),
817
+ ...(input.systemPrompt === undefined ? {} : { systemPrompt: input.systemPrompt }), ...(systemPromptPath ? { systemPromptPath } : {}), ...(input.systemPromptAppend ? { systemPromptAppend: input.systemPromptAppend } : {}), ...(input.extensionFactories?.length ? { extensionFactories: Object.freeze([...input.extensionFactories]) } : {}), ...(input.additionalSkillPaths?.length ? { additionalSkillPaths: Object.freeze([...input.additionalSkillPaths]) } : {}), ...(input.contextFiles === undefined ? {} : { contextFiles: Object.freeze([...input.contextFiles]) }), ...(input.resourcePolicy ? { resourcePolicy: Object.freeze(structuredClone(input.resourcePolicy)) } : {}), ...(input.settings === undefined ? {} : { settings: Object.freeze(structuredClone(input.settings)) }),
818
+ };
819
+ return deepFreeze(prepared);
820
+ }
821
+ function agentSetupSummary(setup, hookNames) {
822
+ const model = setup.sessionInput.model;
823
+ return { hookNames: [...hookNames], model: { provider: model.provider, model: model.model, ...(model.thinking ? { thinking: model.thinking } : {}) }, tools: [...setup.sessionInput.tools, ...(setup.sessionInput.resultTool ? [setup.sessionInput.resultTool.name] : [])], cwd: setup.sessionInput.cwd, ...(setup.sessionInput.resourcePolicy ? { resourceSelectors: resourcePolicySummary(setup.sessionInput.resourcePolicy, setup.sessionInput.tools) } : {}) };
824
+ }
825
+ /** Each attempt opens its own copy of the turn input, so messages a failed attempt appended never re-enter the next one. */
826
+ async function attemptSessionInput(source, attempt) {
827
+ const target = join(dirname(source), `${basename(source, ".jsonl")}-attempt-${String(attempt)}.jsonl`);
828
+ await copyFile(source, target);
829
+ return target;
830
+ }
831
+ async function prepareAgentSetup(root, transport, task, options, resolved, cwd, attempt, signal, customTools, resultTool, inspection = false, previousError) {
832
+ const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
833
+ const baselineOptions = structuredClone(options.agentOptions ?? {});
834
+ const baseResourcePolicy = await root.agentResourcePolicy?.();
835
+ const roleName = typeof options.role === "string" ? options.role : undefined;
836
+ const roleDefinition = roleName ? root.agentDefinitions?.[roleName] : undefined;
837
+ const roleSelectors = roleDefinition ? {
838
+ ...(roleDefinition.skills === undefined ? {} : { skills: roleDefinition.skills }),
839
+ ...(roleDefinition.extensions === undefined ? {} : { extensions: roleDefinition.extensions.map((selector) => canonicalExtensionSelector(selector, cwd)) }),
840
+ ...(roleDefinition.tools === undefined ? {} : { tools: roleDefinition.tools }),
841
+ } : undefined;
842
+ const selectorValue = (key, fallback) => {
843
+ const value = baselineOptions[key];
844
+ if (Array.isArray(value) && value.every((entry) => typeof entry === "string"))
845
+ return value;
846
+ return options.agentOptions === undefined ? fallback : undefined;
847
+ };
848
+ const rawCallSkills = selectorValue("skills", options.skills);
849
+ const rawCallExtensions = selectorValue("extensions", options.extensions)?.map((selector) => canonicalExtensionSelector(selector, cwd));
850
+ const rawCallTools = selectorValue("tools", options.tools);
851
+ const callSelectors = { ...(rawCallSkills === undefined ? {} : { skills: rawCallSkills }), ...(rawCallExtensions === undefined ? {} : { extensions: rawCallExtensions }), ...(rawCallTools === undefined ? {} : { tools: rawCallTools }) };
852
+ const resourcePolicy = baseResourcePolicy ? {
853
+ ...baseResourcePolicy,
854
+ effective: {
855
+ skills: [...baseResourcePolicy.effective.skills, ...(roleSelectors?.skills ?? []), ...(rawCallSkills ?? [])],
856
+ extensions: [...baseResourcePolicy.effective.extensions, ...(roleSelectors?.extensions ?? []), ...(rawCallExtensions ?? [])],
857
+ ...(baseResourcePolicy.effective.tools === undefined && roleSelectors?.tools === undefined && rawCallTools === undefined ? {} : { tools: [...(baseResourcePolicy.effective.tools ?? []), ...(roleSelectors?.tools ?? []), ...(rawCallTools ?? [])] }),
858
+ },
859
+ selectorSources: { global: baseResourcePolicy.selectorSources.global, project: baseResourcePolicy.selectorSources.project, ...(roleSelectors ? { role: roleSelectors } : {}), ...(Object.keys(callSelectors).length ? { call: callSelectors } : {}) },
860
+ } : undefined;
861
+ if (resourcePolicy) {
862
+ resourcePolicy.selectedTools = resolved.tools;
863
+ resourcePolicy.unmatchedTools = unmatchedResourcePatterns(resourceSelectorLayers(resourcePolicy.selectorSources, "tools").flatMap((layer) => layer ?? []), [...root.tools]);
864
+ }
865
+ const resourcePolicyCeiling = resourcePolicy ? structuredClone(resourcePolicy) : undefined;
866
+ const sessionPath = options.sessionPath === undefined || inspection ? options.sessionPath : await attemptSessionInput(options.sessionPath, attempt);
867
+ const extensionSettings = mergeWorkflowExtensionSettings(options.inheritedExtensionSettings ?? root.extensionSettings, roleDefinition?.extensionSettings) ?? Object.freeze({});
868
+ 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 }) });
869
+ if (options.agentNodeId !== undefined)
870
+ root.onAgentSettings?.(options.agentNodeId, extensionSettings);
871
+ 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) };
872
+ const setup = { prompt: task, options: sessionInput.options ?? {}, sessionInput, prepared: await preparedAgentSession(sessionInput, task), transport };
873
+ const base = fallbackSetupContext(root, options, setupSignal);
874
+ 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" } : {}) });
875
+ const hookNames = [];
876
+ for (const hook of [...(root.agentSetupHooks ?? [])].sort(byPriorityThenName)) {
877
+ if (setupSignal.aborted)
878
+ return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error: new WorkflowError("CANCELLED", "Agent cancelled") } };
879
+ try {
880
+ await hook.setup(setup, context);
881
+ }
882
+ catch (error) {
883
+ setup.prepared = await preparedAgentSession(setup.sessionInput, task);
884
+ return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error: setupSignal.reason !== undefined ? new WorkflowError("CANCELLED", "Agent cancelled") : error, hook: hook.name } };
885
+ }
886
+ setup.prepared = await preparedAgentSession(setup.sessionInput, task);
887
+ hookNames.push(hook.name);
888
+ if (setupSignal.reason !== undefined)
889
+ return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error: new WorkflowError("CANCELLED", "Agent cancelled") } };
890
+ }
891
+ try {
892
+ if (resourcePolicyWidened(resourcePolicyCeiling, setup.sessionInput.resourcePolicy))
893
+ throw new WorkflowError("INVALID_METADATA", "Agent setup widened the prepared resource policy");
894
+ appendResourcePolicyNarrowing(resourcePolicyCeiling, setup.sessionInput.resourcePolicy);
895
+ setup.sessionInput.options = setup.options;
896
+ if (changedOption(setup.options, baselineOptions, "model") && typeof setup.options.model === "string")
897
+ setup.sessionInput.model = parseModel(setup.options.model, setup.sessionInput.model, root.modelAliases, root.knownModels ?? root.availableModels, root.settingsPath);
898
+ if (changedOption(setup.options, baselineOptions, "tools") && Array.isArray(setup.options.tools) && setup.options.tools.every((tool) => typeof tool === "string"))
899
+ setup.sessionInput.tools = [...setup.options.tools];
900
+ if (changedOption(setup.options, baselineOptions, "cwd") && typeof setup.options.cwd === "string")
901
+ setup.sessionInput.cwd = setup.options.cwd;
902
+ const customToolNames = new Set([...(setup.sessionInput.customTools ?? []).map(({ name }) => name), ...(setup.sessionInput.resultTool ? [setup.sessionInput.resultTool.name] : [])]);
903
+ const widened = setup.sessionInput.tools.find((tool) => !resolved.tools.includes(tool) && !customToolNames.has(tool));
904
+ const outsideTool = widened ?? setup.sessionInput.tools.find((tool) => !root.tools.has(tool) && !customToolNames.has(tool));
905
+ if (outsideTool)
906
+ throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the prepared agent policy: ${outsideTool}`);
907
+ }
908
+ catch (error) {
909
+ setup.prepared = await preparedAgentSession(setup.sessionInput, task);
910
+ return { setup, summary: agentSetupSummary(setup, hookNames), failure: { error } };
911
+ }
912
+ setup.prepared = await preparedAgentSession(setup.sessionInput, inspection ? task : workflowAgentPrompt(setup.prompt, options, attempt, previousError));
913
+ return { setup, summary: agentSetupSummary(setup, hookNames) };
914
+ }
915
+ export async function prepareAgentSetupForInspection(root, task, options, transport) {
916
+ const executor = new WorkflowAgentExecutor(root);
917
+ const resolved = executor.resolve(options);
918
+ return prepareAgentSetup(root, transport, task, options, resolved, root.cwd, 1, root.runContext?.signal, [], undefined, true);
919
+ }
920
+ function attemptRecord(transport, attempt, session, setup, stats, result, error) {
921
+ return { attempt, transport, session: session.reference, ...(result === undefined ? {} : { result }), ...(error ? { error } : {}), accounting: stats, setup };
922
+ }
923
+ const agentAttemptsByError = new WeakMap();
924
+ function errorWithAttempts(error, attempts) {
925
+ const typed = error instanceof Error ? error : new Error(typeof error === "string" ? error : String(error));
926
+ const annotated = Object.assign(typed, { attempts });
927
+ agentAttemptsByError.set(typed, attempts);
928
+ return annotated;
929
+ }
930
+ export function getAgentAttempts(error) {
931
+ return error instanceof Error ? agentAttemptsByError.get(error) : undefined;
932
+ }
933
+ export class WorkflowAgentExecutor {
934
+ root;
935
+ transport;
936
+ warnedRoleTools = new Set();
937
+ constructor(root, transport = localAgentTransport) {
938
+ this.root = root;
939
+ this.transport = transport;
940
+ }
941
+ setRunContext(runContext) { this.root.runContext = runContext; }
942
+ resolve(options, inheritedTools) {
943
+ const roleName = typeof options.role === "string" ? options.role : undefined;
944
+ const roleDefinition = roleName ? this.root.agentDefinitions?.[roleName] : undefined;
945
+ if (roleName && !roleDefinition)
946
+ throw new WorkflowError("UNKNOWN_AGENT_TYPE", `Unknown agent role: ${roleName}`);
947
+ const candidateTools = inheritedTools !== undefined ? [...inheritedTools] : [...this.root.tools];
948
+ if (inheritedTools === undefined)
949
+ for (const pattern of roleDefinition?.tools ?? []) {
950
+ const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
951
+ if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !this.root.tools.has(body)) {
952
+ const key = `${roleName ?? "agent"}:${body}`;
953
+ if (!this.warnedRoleTools.has(key)) {
954
+ this.warnedRoleTools.add(key);
955
+ this.root.onResourceWarning?.(`Tool not available in this session for role ${roleName ?? "agent"}: ${body}. The agent runs without it.`);
956
+ }
957
+ }
958
+ }
959
+ const selectorLayers = [
960
+ this.root.resourceSelectors?.tools,
961
+ roleDefinition?.tools,
962
+ options.tools,
963
+ ];
964
+ for (const pattern of options.tools ?? []) {
965
+ const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
966
+ if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !candidateTools.includes(body))
967
+ throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${body}`);
968
+ }
969
+ const outsideEffectiveTool = options.effectiveTools?.find((tool) => !candidateTools.includes(tool));
970
+ if (outsideEffectiveTool)
971
+ throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${outsideEffectiveTool}`);
972
+ const requested = options.effectiveTools ?? selectResourcesByLayers(selectorLayers, candidateTools);
973
+ const forbidden = requested.find((tool) => !this.root.tools.has(tool));
974
+ if (forbidden)
975
+ throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${forbidden}`);
976
+ const requestedModel = options.model ?? (roleDefinition?.model && roleDefinition.thinking && !roleDefinition.model.includes(":") ? `${roleDefinition.model}:${roleDefinition.thinking}` : roleDefinition?.model);
977
+ const alias = requestedModel === undefined ? undefined : modelAliasName(requestedModel, this.root.modelAliases ?? {});
978
+ const blockedAlias = requestedModel?.split(":", 1)[0];
979
+ if (requestedModel !== undefined && blockedAlias && this.root.blockedAliases?.has(blockedAlias) && !alias) {
980
+ const target = this.root.blockedAliasTargets?.[blockedAlias];
981
+ throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${requestedModel}${target ? ` resolved to ${target}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
982
+ }
983
+ const model = options.modelOverride ?? parseModel(requestedModel, this.root.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
984
+ const availableModels = this.root.knownModels ?? this.root.availableModels ?? new Set([modelCapability(this.root.model)]);
985
+ if (!availableModels.has(modelCapability(model)))
986
+ throw new WorkflowError("UNKNOWN_MODEL", `Unknown model${requestedModel ? ` ${requestedModel} resolved to ${modelCapability(model)}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
987
+ const overrideSystemPrompt = roleDefinition?.overrideSystemPrompt === true;
988
+ const contextFiles = options.contextFiles ?? roleDefinition?.contextFiles;
989
+ return { model, ...(alias && requestedModel ? { requestedModel } : {}), tools: requested, ...(overrideSystemPrompt ? { systemPrompt: roleDefinition.prompt ?? "" } : {}), systemPromptAppend: overrideSystemPrompt ? "" : roleDefinition?.prompt ?? "", ...(contextFiles === undefined ? {} : { contextFiles: [...contextFiles] }) };
990
+ }
991
+ async execute(task, options, signal, customTools = [], setSteer, beforeRetry) {
992
+ const executionSignal = signal ?? this.root.runContext?.signal;
993
+ if (!Number.isInteger(options.retries ?? 0) || (options.retries ?? 0) < 0)
994
+ throw new WorkflowError("INVALID_METADATA", "retries must be a non-negative integer");
995
+ if (options.timeoutMs !== undefined && options.timeoutMs !== null && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0))
996
+ throw new WorkflowError("INVALID_METADATA", "timeoutMs must be null or a positive integer");
997
+ let resolved = this.resolve(options);
998
+ let recoveryModel;
999
+ let cwd;
1000
+ if (options.parent) {
1001
+ if (!options.cwd)
1002
+ throw new WorkflowError("INVALID_METADATA", "Child agents require their parent cwd");
1003
+ if (options.worktreeOwner) {
1004
+ if (!this.root.runStore)
1005
+ throw new WorkflowError("WORKTREE_FAILED", "Worktree inheritance requires a persisted run");
1006
+ cwd = (await this.root.runStore.validateWorktree(options.worktreeOwner, options.cwd)).cwd;
1007
+ }
1008
+ else {
1009
+ if (options.cwd !== this.root.cwd)
1010
+ throw new WorkflowError("INVALID_METADATA", "Shared-tree children must inherit the root cwd");
1011
+ cwd = this.root.cwd;
1012
+ }
1013
+ }
1014
+ else if (options.worktreeOwner) {
1015
+ if (!this.root.runStore)
1016
+ throw new WorkflowError("WORKTREE_FAILED", "Worktree scope requires a persisted run");
1017
+ const worktree = await this.root.runStore.worktree(options.worktreeOwner);
1018
+ if (options.cwd && resolvePath(options.cwd) !== resolvePath(worktree.cwd))
1019
+ throw new WorkflowError("WORKTREE_FAILED", "Agent cwd does not match its owned worktree");
1020
+ cwd = worktree.cwd;
1021
+ }
1022
+ else {
1023
+ if (options.cwd)
1024
+ throw new WorkflowError("INVALID_METADATA", "Only child agents or worktree scopes may provide a cwd");
1025
+ cwd = this.root.cwd;
1026
+ }
1027
+ const attempts = [];
1028
+ let maxAttempts = (options.retries ?? 0) + 1;
1029
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1030
+ const attemptSignal = executionSignal ?? new AbortController().signal;
1031
+ if (recoveryModel)
1032
+ resolved = this.resolve({ ...options, modelOverride: recoveryModel });
1033
+ options.budget?.beforeAttempt();
1034
+ let session;
1035
+ let handoff;
1036
+ let handoffAbort;
1037
+ const releaseHandoff = (reason) => {
1038
+ handoff?.release(reason);
1039
+ if (handoffAbort) {
1040
+ attemptSignal.removeEventListener("abort", handoffAbort);
1041
+ handoffAbort = undefined;
1042
+ }
1043
+ };
1044
+ const releaseIfAttemptCancelled = () => { if (attemptSignal.aborted)
1045
+ releaseHandoff("attempt cancelled"); };
1046
+ let setup;
1047
+ let setupSummary = { hookNames: [], model: { ...resolved.model }, tools: [...resolved.tools, "workflow_result"], cwd };
1048
+ let setupFailed = false;
1049
+ let completedAttempt;
1050
+ let attemptCallbackFailure;
1051
+ const resultSchema = Compile(options.schema ?? defaultWorkflowResultSchema);
1052
+ let resultAccepted = false;
1053
+ const resultTool = defineTool({
1054
+ name: "workflow_result", label: "Workflow Result", description: "Submit the terminal workflow result", parameters: resultSchema.Type(),
1055
+ async execute(_id, value) {
1056
+ if (!resultSchema.Check(value) || !jsonValue(value))
1057
+ return { content: [{ type: "text", text: "Result does not match the required schema." }], details: {}, isError: true };
1058
+ if (resultAccepted)
1059
+ return { content: [{ type: "text", text: "Result has already been accepted." }], details: {}, isError: true };
1060
+ resultAccepted = true;
1061
+ const currentSession = session;
1062
+ if (currentSession)
1063
+ void currentSession.abort().catch(() => undefined);
1064
+ return { content: [{ type: "text", text: "Result accepted." }], details: {} };
1065
+ },
1066
+ });
1067
+ let lastKnownAccounting = zeroAccounting();
1068
+ const accountingFromRuntime = (usage, active) => {
1069
+ const input = usage.input;
1070
+ const output = usage.output;
1071
+ const cacheRead = usage.cacheRead;
1072
+ const cacheWrite = usage.cacheWrite;
1073
+ const cost = usage.costUsd;
1074
+ if (usage.availability === "complete" && typeof input === "number" && Number.isFinite(input) && typeof output === "number" && Number.isFinite(output) && typeof cacheRead === "number" && Number.isFinite(cacheRead) && typeof cacheWrite === "number" && Number.isFinite(cacheWrite) && typeof cost === "number" && Number.isFinite(cost))
1075
+ return { input, output, cacheRead, cacheWrite, cost };
1076
+ return accounting(active.getSessionStats());
1077
+ };
1078
+ const progressHandler = options.onProgress;
1079
+ const reportProgress = progressHandler ? async (value) => {
1080
+ const values = [value.usage.input, value.usage.output, value.usage.cacheRead, value.usage.cacheWrite, value.usage.costUsd];
1081
+ const complete = value.usage.availability === "complete" && values.every((entry) => typeof entry === "number" && Number.isFinite(entry));
1082
+ const reportable = complete ? value : { ...value, usage: { availability: "complete", input: lastKnownAccounting.input, output: lastKnownAccounting.output, cacheRead: lastKnownAccounting.cacheRead, cacheWrite: lastKnownAccounting.cacheWrite, costUsd: lastKnownAccounting.cost } };
1083
+ const update = runtimeProgressToAgentProgress(reportable);
1084
+ if (complete)
1085
+ lastKnownAccounting = { ...update.accounting };
1086
+ await progressHandler(update);
1087
+ } : undefined;
1088
+ try {
1089
+ setupFailed = true;
1090
+ const prepared = await prepareAgentSetup(this.root, this.transport, task, options, resolved, cwd, attempt, attemptSignal, customTools, resultTool, false, attempts.at(-1)?.error?.message);
1091
+ setup = prepared.setup;
1092
+ const attemptSetup = prepared.setup;
1093
+ setupSummary = prepared.summary;
1094
+ if (prepared.failure)
1095
+ throw prepared.failure.error;
1096
+ setupFailed = false;
1097
+ if (attemptSignal.aborted)
1098
+ throw new WorkflowError("CANCELLED", "Agent cancelled");
1099
+ await options.onAttempt?.({ attempt, transport: attemptSetup.transport.id, accounting: zeroAccounting(), setup: setupSummary });
1100
+ const transportBase = fallbackSetupContext(this.root, options, attemptSignal);
1101
+ const transportContext = Object.freeze({ run: transportBase.run, identity: transportBase.identity, attempt, signal: attemptSignal, settings: setup.prepared.settings ?? Object.freeze({}), ...(transportBase.tuiIndex === undefined ? {} : { tuiIndex: transportBase.tuiIndex }), ...(transportBase.tuiLabel === undefined ? {} : { tuiLabel: transportBase.tuiLabel }) });
1102
+ handoff = createLiveSessionHandoff();
1103
+ handoffAbort = () => { releaseHandoff("attempt cancelled"); };
1104
+ attemptSignal.addEventListener("abort", handoffAbort, { once: true });
1105
+ releaseIfAttemptCancelled();
1106
+ const runStore = this.root.runStore;
1107
+ const runner = createPiRuntimeAgentRunner({
1108
+ transport: setup.transport,
1109
+ prepared: setup.prepared,
1110
+ context: transportContext,
1111
+ handoff,
1112
+ callbacks: {
1113
+ onSession: async (createdSession, createdHandoff, createdPrepared) => {
1114
+ session = createdSession;
1115
+ releaseIfAttemptCancelled();
1116
+ const activeAttempt = { attempt, transport: attemptSetup.transport.id, session: createdSession.reference, liveSession: createdSession, prepared: createdPrepared, handoff: createdHandoff, accounting: zeroAccounting(), setup: setupSummary };
1117
+ await options.onAttempt?.(activeAttempt);
1118
+ },
1119
+ onSessionReady: async () => {
1120
+ const policy = attemptSetup.sessionInput.resourcePolicy;
1121
+ const inspection = session?.getResourceInspection?.();
1122
+ if (policy && inspection) {
1123
+ try {
1124
+ policy.selectedSkills = [...inspection.skills];
1125
+ policy.selectedExtensions = [...inspection.extensions];
1126
+ }
1127
+ catch { /* Frozen prepared policies still receive the inspection overlay below. */ }
1128
+ }
1129
+ if (policy)
1130
+ setupSummary = { ...setupSummary, resourceSelectors: { ...resourcePolicySummary(policy, attemptSetup.sessionInput.tools), ...(inspection ? { skills: [...inspection.skills], extensions: [...inspection.extensions] } : {}) } };
1131
+ },
1132
+ onBeforeComplete: async () => {
1133
+ if (options.worktreeOwner)
1134
+ await runStore?.snapshotWorktree(options.worktreeOwner);
1135
+ },
1136
+ ...(runStore ? { onSystemPrompt: (entry) => runStore.recordSystemPrompt({ ...entry, attempt }) } : {}),
1137
+ onComplete: async (result, completedSession) => {
1138
+ const completedAccounting = accountingFromRuntime(result.usage, completedSession);
1139
+ completedAttempt = attemptRecord(attemptSetup.transport.id, attempt, completedSession, setupSummary, completedAccounting, result.value);
1140
+ attempts.push(completedAttempt);
1141
+ try {
1142
+ await options.onAttempt?.(completedAttempt);
1143
+ }
1144
+ catch (error) {
1145
+ attemptCallbackFailure = error;
1146
+ throw error;
1147
+ }
1148
+ finally {
1149
+ releaseHandoff("session completed");
1150
+ }
1151
+ },
1152
+ onFailure: async (failure, failedSession) => {
1153
+ releaseHandoff("attempt failed");
1154
+ const failedAccounting = accounting(failedSession.getSessionStats());
1155
+ const failedAttempt = attemptRecord(attemptSetup.transport.id, attempt, failedSession, setupSummary, failedAccounting, undefined, { code: failure.code, message: failure.message });
1156
+ attempts.push(failedAttempt);
1157
+ try {
1158
+ await options.onAttempt?.(failedAttempt);
1159
+ }
1160
+ catch (error) {
1161
+ attemptCallbackFailure = error;
1162
+ throw error;
1163
+ }
1164
+ },
1165
+ },
1166
+ });
1167
+ const runtimeRun = { id: transportBase.run.runId, namespaceId: transportBase.run.sessionId, workflowName: transportBase.run.workflow.name };
1168
+ const providerRecovery = options.providerErrorRecovery;
1169
+ const runtimeAgent = { id: transportBase.identity.callSite, structuralPath: [...transportBase.identity.structuralPath], ...(options.parent ? { parentId: options.parent } : {}) };
1170
+ const runtimeRequest = {
1171
+ task: setup.prepared.initialPrompt ?? setup.prompt,
1172
+ cwd: setup.prepared.cwd,
1173
+ model: { ...setup.prepared.model },
1174
+ enabledTools: [...setup.prepared.tools],
1175
+ // Pi sessions execute the prepared native definitions; the executor does not duplicate them as neutral request tools.
1176
+ customTools: [],
1177
+ ...(options.schema ? { resultSchema: options.schema } : {}),
1178
+ run: runtimeRun,
1179
+ agent: runtimeAgent,
1180
+ signal: attemptSignal,
1181
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
1182
+ ...(reportProgress ? { onProgress: reportProgress } : {}),
1183
+ ...(setSteer ? { onControl: async (control) => { setSteer((message) => control.steer(message)); } } : {}),
1184
+ ...(options.budget ? { turnPolicy: {
1185
+ beforeTurn: () => {
1186
+ try {
1187
+ options.budget?.beforeTurn();
1188
+ }
1189
+ catch (error) {
1190
+ throw error instanceof WorkflowError ? error : new WorkflowError("BUDGET_EXHAUSTED", errorText(error));
1191
+ }
1192
+ },
1193
+ afterTurn: (usage, final, requestInstruction) => {
1194
+ const active = session;
1195
+ if (!active)
1196
+ return undefined;
1197
+ options.budget?.afterTurn(accountingFromRuntime(usage, active), final);
1198
+ return requestInstruction && !final ? options.budget?.instruction() : undefined;
1199
+ },
1200
+ } } : {}),
1201
+ ...(providerRecovery ? { onProviderError: (failure) => providerRecovery({ label: options.label, ...failure }) } : {}),
1202
+ ...(this.root.providerPause ? { onProviderLimit: this.root.providerPause } : {}),
1203
+ };
1204
+ const result = await runner.run(runtimeRequest);
1205
+ if (attemptCallbackFailure)
1206
+ throw errorWithAttempts(attemptCallbackFailure, attempts);
1207
+ return { value: result.value, attempts, cwd: setupSummary.cwd };
1208
+ }
1209
+ catch (error) {
1210
+ if (attemptCallbackFailure || completedAttempt)
1211
+ throw errorWithAttempts(attemptCallbackFailure ?? error, attempts);
1212
+ const providerError = isRuntimeAgentProviderError(error) ? error : undefined;
1213
+ const typed = normalizePiRuntimeError(error, attemptSignal, setupFailed);
1214
+ releaseHandoff("attempt failed");
1215
+ if (!session) {
1216
+ const failedAttempt = { attempt, transport: setup?.transport.id ?? this.transport.id, accounting: zeroAccounting(), error: { code: typed.code, message: typed.message }, setup: setupSummary };
1217
+ attempts.push(failedAttempt);
1218
+ try {
1219
+ await options.onAttempt?.(failedAttempt);
1220
+ }
1221
+ catch (persistenceError) {
1222
+ throw errorWithAttempts(persistenceError, attempts);
1223
+ }
1224
+ }
1225
+ if (options.worktreeOwner && typed.code !== "WORKTREE_FAILED")
1226
+ await this.root.runStore?.snapshotWorktree(options.worktreeOwner).catch(() => undefined);
1227
+ if (providerError?.handled) {
1228
+ if (typeof providerError.recovery === "object" && typeof providerError.recovery.model === "string") {
1229
+ try {
1230
+ const selected = resolveModelReference(providerError.recovery.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
1231
+ recoveryModel = selected.thinking === undefined && resolved.model.thinking ? { ...selected, thinking: resolved.model.thinking } : selected;
1232
+ }
1233
+ catch {
1234
+ throw errorWithAttempts(typed, attempts);
1235
+ }
1236
+ maxAttempts += 1;
1237
+ beforeRetry?.();
1238
+ continue;
1239
+ }
1240
+ throw errorWithAttempts(typed, attempts);
1241
+ }
1242
+ if (attempt === maxAttempts || setupFailed || typed.code === "CANCELLED" || typed.code === "WORKTREE_FAILED" || typed.code === "RESUME_INCOMPATIBLE" || typed.code === "RESULT_INVALID")
1243
+ throw errorWithAttempts(typed, attempts);
1244
+ beforeRetry?.();
1245
+ }
1246
+ }
1247
+ throw new WorkflowError("AGENT_FAILED", "Agent execution failed");
1248
+ }
1249
+ }
1250
+ export class FairAgentScheduler {
1251
+ runner;
1252
+ sessionLimit;
1253
+ writeOwnership;
1254
+ #runs = new Map();
1255
+ #nodes = new Map();
1256
+ #runOrder = [];
1257
+ #cursor = 0;
1258
+ #active = 0;
1259
+ #nextId = 0;
1260
+ #persistence = Promise.resolve();
1261
+ #persistenceErrors = new Map();
1262
+ constructor(runner, sessionLimit = 16, writeOwnership) {
1263
+ this.runner = runner;
1264
+ this.sessionLimit = sessionLimit;
1265
+ this.writeOwnership = writeOwnership;
1266
+ if (!Number.isInteger(sessionLimit) || sessionLimit < 1 || sessionLimit > 16)
1267
+ throw new WorkflowError("INVALID_SETTINGS", "Session concurrency must be an integer from 1 to 16");
1268
+ }
1269
+ addRun(runId, limit = 8, beforeLaunch, extensionSettings) {
1270
+ if (this.#runs.has(runId))
1271
+ throw new WorkflowError("DUPLICATE_NAME", `Scheduler run already exists: ${runId}`);
1272
+ if (!Number.isInteger(limit) || limit < 1 || limit > this.sessionLimit)
1273
+ throw new WorkflowError("INVALID_SETTINGS", "Invalid run concurrency");
1274
+ this.#runs.set(runId, { limit, ...(beforeLaunch ? { beforeLaunch } : {}), ...(extensionSettings === undefined ? {} : { extensionSettings: deepFreeze(structuredClone(extensionSettings)) }), logical: 0, active: 0, nextIndex: 0, queue: [] });
1275
+ this.#runOrder.push(runId);
1276
+ }
1277
+ updateRunLimit(runId, limit) {
1278
+ const run = this.#runs.get(runId);
1279
+ if (!run)
1280
+ throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
1281
+ if (!Number.isInteger(limit) || limit < 1 || limit > this.sessionLimit)
1282
+ throw new WorkflowError("INVALID_SETTINGS", "Invalid run concurrency");
1283
+ run.limit = limit;
1284
+ this.#dispatch();
1285
+ }
1286
+ setRunExtensionSettings(runId, settings) {
1287
+ const run = this.#runs.get(runId);
1288
+ if (!run)
1289
+ throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
1290
+ run.extensionSettings = settings === undefined ? undefined : deepFreeze(structuredClone(settings));
1291
+ }
1292
+ setExtensionSettings(agentId, settings) {
1293
+ const node = this.#node(agentId);
1294
+ node.options = Object.freeze({ ...node.options, extensionSettings: deepFreeze(structuredClone(settings)) });
1295
+ this.#persist(node.runId);
1296
+ }
1297
+ spawn(runId, prompt, options, parentId) {
1298
+ const run = this.#runs.get(runId);
1299
+ if (!run)
1300
+ throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
1301
+ const parent = parentId ? this.#nodes.get(parentId) : undefined;
1302
+ if (parentId && (!parent || parent.runId !== runId))
1303
+ throw new WorkflowError("UNKNOWN_AGENT_TYPE", "Parent agent is not owned by this run");
1304
+ const effective = this.#inherit(run, parent, options);
1305
+ const id = `${runId}:${String(++this.#nextId)}`;
1306
+ const tuiIndex = ++run.nextIndex;
1307
+ let resolveResult = () => undefined;
1308
+ const promise = new Promise((resolve) => { resolveResult = resolve; });
1309
+ let resolveCompletion = () => undefined;
1310
+ const completion = new Promise((resolve) => { resolveCompletion = resolve; });
1311
+ const node = { id, runId, ...(parentId ? { parentId } : {}), prompt, options: effective, children: new Set(), collected: false, collecting: false, resultQueue: Promise.resolve(), state: "queued", controller: new AbortController(), promise, resolve: resolveResult, completion, resolveCompletion, task: async () => undefined, restored: false };
1312
+ node.task = async () => {
1313
+ if (node.controller.signal.aborted) {
1314
+ this.#release(node.runId);
1315
+ return;
1316
+ }
1317
+ node.state = "running";
1318
+ this.#persist(runId);
1319
+ try {
1320
+ const value = await this.runner({ id, runId, tuiIndex, ...(parentId ? { parentId } : {}), prompt, options: effective, signal: node.controller.signal, setSteer: (handler) => { node.steer = handler; } });
1321
+ this.#settle(node, { id, ok: true, value });
1322
+ }
1323
+ catch (error) {
1324
+ const typed = error instanceof WorkflowError ? error : new WorkflowError("AGENT_FAILED", errorText(error));
1325
+ this.#settle(node, { id, ok: false, error: { code: typed.code, message: typed.message } });
1326
+ }
1327
+ };
1328
+ this.#nodes.set(id, node);
1329
+ parent?.children.add(id);
1330
+ this.#persist(runId);
1331
+ this.#enqueue(runId, node, () => { void node.task(); });
1332
+ return { id, result: promise };
1333
+ }
1334
+ async result(parentId, childId) {
1335
+ const parent = this.#node(parentId);
1336
+ const child = this.#node(childId);
1337
+ if (child.parentId !== parentId)
1338
+ throw new WorkflowError("UNKNOWN_AGENT_TYPE", "Results are scoped to direct children");
1339
+ if (child.collected || !child.promise)
1340
+ throw new WorkflowError("AGENT_RESULT_COLLECTED", "Child result has already been collected; nested results are one-shot");
1341
+ if (child.collecting)
1342
+ throw new WorkflowError("AGENT_FAILED", "Child result is already being collected");
1343
+ child.collecting = true;
1344
+ const childPromise = child.promise;
1345
+ const previousCollection = parent.resultQueue;
1346
+ let releaseCollection = () => undefined;
1347
+ parent.resultQueue = new Promise((resolve) => { releaseCollection = resolve; });
1348
+ try {
1349
+ await previousCollection;
1350
+ // A queued collection may resume after the parent was cancelled or settled; it must not touch parent state or permits.
1351
+ if (this.#cancelledOrSettled(parent))
1352
+ throw new WorkflowError("CANCELLED", "Parent agent cancelled");
1353
+ parent.state = "waiting_for_child";
1354
+ this.#persist(parent.runId);
1355
+ this.#release(parent.runId);
1356
+ const outcome = await childPromise;
1357
+ await new Promise((resolve) => { this.#enqueue(parent.runId, undefined, () => { resolve(); }); });
1358
+ if (this.#cancelledOrSettled(parent)) {
1359
+ this.#release(parent.runId);
1360
+ throw new WorkflowError("CANCELLED", "Parent agent cancelled");
1361
+ }
1362
+ parent.state = "running";
1363
+ child.collected = true;
1364
+ child.collecting = false;
1365
+ this.releaseResult(child.id);
1366
+ this.#persist(parent.runId);
1367
+ return outcome;
1368
+ }
1369
+ catch (error) {
1370
+ child.collecting = false;
1371
+ throw error;
1372
+ }
1373
+ finally {
1374
+ releaseCollection();
1375
+ }
1376
+ }
1377
+ async steer(parentId, childId, message) {
1378
+ const child = this.#node(childId);
1379
+ if (child.parentId !== parentId)
1380
+ throw new WorkflowError("UNKNOWN_AGENT_TYPE", "Steering is scoped to direct children");
1381
+ if (child.state !== "running" && child.state !== "waiting_for_child")
1382
+ throw new WorkflowError("AGENT_FAILED", "Child is not running");
1383
+ if (!child.steer)
1384
+ throw new WorkflowError("AGENT_FAILED", "Child has not registered a steering handler");
1385
+ await child.steer(message);
1386
+ }
1387
+ cancel(id) { this.#cancelTree(this.#node(id)); }
1388
+ releaseResult(id) {
1389
+ const node = this.#nodes.get(id);
1390
+ if (!node || !node.promise)
1391
+ return;
1392
+ if (!SETTLED_AGENT_STATES.has(node.state))
1393
+ throw new WorkflowError("INTERNAL_ERROR", `Cannot release active agent result: ${id}`);
1394
+ node.promise = undefined;
1395
+ node.task = async () => undefined;
1396
+ }
1397
+ cancelChildren(id) {
1398
+ for (const childId of this.#node(id).children) {
1399
+ const child = this.#nodes.get(childId);
1400
+ if (child)
1401
+ this.#cancelTree(child);
1402
+ }
1403
+ }
1404
+ retry(id) {
1405
+ const node = this.#node(id);
1406
+ if (node.state === "running") {
1407
+ node.state = "retrying";
1408
+ this.#persist(node.runId);
1409
+ }
1410
+ }
1411
+ attemptStarted(id) {
1412
+ const node = this.#node(id);
1413
+ if (node.state === "retrying") {
1414
+ node.state = "running";
1415
+ this.#persist(node.runId);
1416
+ }
1417
+ }
1418
+ async cancelRun(runId) {
1419
+ const run = this.#runs.get(runId);
1420
+ if (!run)
1421
+ throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
1422
+ const nodes = [...this.#nodes.values()].filter((node) => node.runId === runId);
1423
+ for (const node of nodes)
1424
+ if (!node.parentId)
1425
+ this.#cancelTree(node);
1426
+ await Promise.all(nodes.map(({ completion }) => completion));
1427
+ if (nodes.every(({ restored }) => restored))
1428
+ run.logical = 0;
1429
+ }
1430
+ removeRun(runId) {
1431
+ const run = this.#runs.get(runId);
1432
+ if (!run)
1433
+ return;
1434
+ const nodes = [...this.#nodes.values()].filter((node) => node.runId === runId);
1435
+ if (run.active > 0 || nodes.some(({ state }) => !SETTLED_AGENT_STATES.has(state)))
1436
+ throw new WorkflowError("INTERNAL_ERROR", `Cannot remove active scheduler run: ${runId}`);
1437
+ for (const { id } of nodes)
1438
+ this.#nodes.delete(id);
1439
+ this.#runs.delete(runId);
1440
+ const index = this.#runOrder.indexOf(runId);
1441
+ if (index >= 0) {
1442
+ this.#runOrder.splice(index, 1);
1443
+ if (index < this.#cursor)
1444
+ this.#cursor -= 1;
1445
+ if (this.#cursor >= this.#runOrder.length)
1446
+ this.#cursor = 0;
1447
+ }
1448
+ this.#dispatch();
1449
+ }
1450
+ toolsFor(parentId, resolveTools) {
1451
+ const parent = this.#node(parentId);
1452
+ if (!parent.options.tools.includes("agent"))
1453
+ return [];
1454
+ const agentTool = defineTool({
1455
+ name: "agent", label: "Child Agent", description: "Start a direct child agent",
1456
+ 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 }),
1457
+ execute: async (_id, params) => {
1458
+ if (!isChildAgentToolParams(params))
1459
+ throw new WorkflowError("INVALID_METADATA", "Invalid child agent parameters");
1460
+ validateAgentOptions(params);
1461
+ const outputSchema = params.outputSchema;
1462
+ if (outputSchema !== undefined)
1463
+ validateSchema(outputSchema, "agent outputSchema");
1464
+ 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;
1465
+ const agentOptions = { ...params };
1466
+ Reflect.deleteProperty(agentOptions, "prompt");
1467
+ const options = { label: params.label, requestedLabel: params.label, cwd: parent.options.cwd, tools, agentOptions, ...(params.skills ? { skills: params.skills } : {}), ...(params.extensions ? { extensions: params.extensions } : {}), ...(params.model ? { model: params.model } : {}), ...(params.role ? { role: params.role } : {}), ...(params.contextFiles ? { contextFiles: params.contextFiles } : {}), ...(outputSchema === undefined ? {} : { schema: outputSchema }), ...(params.retries === undefined ? {} : { retries: params.retries }), ...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }) };
1468
+ const child = this.spawn(parent.runId, params.prompt, options, parentId);
1469
+ return { content: [{ type: "text", text: JSON.stringify({ id: child.id }) }], details: { id: child.id } };
1470
+ },
1471
+ });
1472
+ const resultTool = defineTool({
1473
+ name: "get_subagent_result", label: "Child Result", description: "Wait for a direct child and return its result once; repeated retrieval fails with AGENT_RESULT_COLLECTED",
1474
+ parameters: Type.Object({ id: Type.String() }),
1475
+ execute: async (_id, params) => { const value = await this.result(parentId, params.id); if (!value.ok && value.error.code === "BUDGET_EXHAUSTED")
1476
+ throw new WorkflowError("BUDGET_EXHAUSTED", value.error.message); return { content: [{ type: "text", text: JSON.stringify(value) }], details: value }; }
1477
+ });
1478
+ const steerTool = defineTool({
1479
+ name: "steer_subagent", label: "Steer Child", description: "Steer a running direct child",
1480
+ parameters: Type.Object({ id: Type.String(), message: Type.String() }),
1481
+ execute: async (_id, params) => { await this.steer(parentId, params.id, params.message); return { content: [{ type: "text", text: "Steering delivered." }], details: {} }; },
1482
+ });
1483
+ return [agentTool, resultTool, steerTool];
1484
+ }
1485
+ snapshot() {
1486
+ return [...this.#nodes.values()].map(({ id, parentId, prompt, options, state }) => ({ id, ...(parentId ? { parentId } : {}), ...(prompt === undefined ? {} : { prompt }), label: options.label, state, options }));
1487
+ }
1488
+ restoreRun(runId, limit, ownership, beforeLaunch, extensionSettings) {
1489
+ this.addRun(runId, limit, beforeLaunch, extensionSettings);
1490
+ const run = this.#runs.get(runId);
1491
+ for (const record of ownership) {
1492
+ if (record.id.split(":").slice(0, -1).join(":") !== runId)
1493
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Persisted agent belongs to another run: ${record.id}`);
1494
+ let resolveResult = () => undefined;
1495
+ const promise = new Promise((resolve) => { resolveResult = resolve; });
1496
+ let resolveCompletion = () => undefined;
1497
+ const completion = new Promise((resolve) => { resolveCompletion = resolve; });
1498
+ const node = { id: record.id, runId, ...(record.parentId ? { parentId: record.parentId } : {}), ...(record.prompt === undefined ? {} : { prompt: record.prompt }), options: this.#inherit(run, undefined, record.options), children: new Set(), collected: false, collecting: false, resultQueue: Promise.resolve(), state: record.state, controller: new AbortController(), promise, resolve: resolveResult, completion, resolveCompletion, task: async () => undefined, restored: true };
1499
+ this.#nodes.set(node.id, node);
1500
+ run.logical += 1;
1501
+ this.#nextId = Math.max(this.#nextId, Number(node.id.slice(node.id.lastIndexOf(":") + 1)) || 0);
1502
+ if (record.state === "completed") {
1503
+ resolveResult({ id: node.id, ok: true, value: null });
1504
+ resolveCompletion();
1505
+ }
1506
+ else if (record.state === "failed" || record.state === "cancelled") {
1507
+ resolveResult({ id: node.id, ok: false, error: { code: record.state === "cancelled" ? "CANCELLED" : "AGENT_FAILED", message: `Persisted agent ${record.state}` } });
1508
+ resolveCompletion();
1509
+ }
1510
+ }
1511
+ for (const node of this.#nodes.values())
1512
+ if (node.runId === runId && node.parentId)
1513
+ this.#nodes.get(node.parentId)?.children.add(node.id);
1514
+ }
1515
+ async flush(runId) {
1516
+ await this.#persistence;
1517
+ // Errors are keyed per run so one run's ownership-write failure is never thrown at another
1518
+ // run's synchronization point. A flush without a runId (shutdown) reports any run's failure.
1519
+ const key = runId ?? this.#persistenceErrors.keys().next().value;
1520
+ if (key === undefined)
1521
+ return;
1522
+ const error = this.#persistenceErrors.get(key);
1523
+ if (error === undefined)
1524
+ return;
1525
+ this.#persistenceErrors.delete(key);
1526
+ throw error;
1527
+ }
1528
+ #inherit(run, parent, options) {
1529
+ if (!options.label.trim() || !options.cwd || !Array.isArray(options.tools))
1530
+ throw new WorkflowError("INVALID_METADATA", "Agents require label, cwd, and tools");
1531
+ const inheritedTools = options.tools;
1532
+ const extensionSettings = options.extensionSettings ?? parent?.options.extensionSettings ?? run.extensionSettings;
1533
+ if (!parent)
1534
+ 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]) }) } : {}) });
1535
+ if (options.cwd !== parent.options.cwd)
1536
+ throw new WorkflowError("UNKNOWN_TOOL", "Child cwd cannot differ from its parent");
1537
+ const forbidden = inheritedTools.find((tool) => !parent.options.tools.includes(tool));
1538
+ if (forbidden)
1539
+ throw new WorkflowError("UNKNOWN_TOOL", `Child tool escalates parent boundary: ${forbidden}`);
1540
+ const identity = options.agentIdentity ?? parent.options.agentIdentity;
1541
+ 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 } : {}) });
1542
+ }
1543
+ #enqueue(runId, node, start) { this.#runs.get(runId)?.queue.push({ ...(node ? { node } : {}), start }); this.#dispatch(); }
1544
+ #dispatch() {
1545
+ while (this.#active < this.sessionLimit && this.#runOrder.length) {
1546
+ let selected;
1547
+ for (let checked = 0; checked < this.#runOrder.length; checked += 1) {
1548
+ const index = (this.#cursor + checked) % this.#runOrder.length;
1549
+ const id = this.#runOrder[index];
1550
+ const run = id ? this.#runs.get(id) : undefined;
1551
+ if (id && run && run.active < run.limit && run.queue.length) {
1552
+ selected = id;
1553
+ this.#cursor = (index + 1) % this.#runOrder.length;
1554
+ break;
1555
+ }
1556
+ }
1557
+ if (!selected)
1558
+ return;
1559
+ const run = this.#runs.get(selected);
1560
+ const item = run.queue.shift();
1561
+ if (item.node) {
1562
+ try {
1563
+ run.beforeLaunch?.();
1564
+ }
1565
+ catch (error) {
1566
+ const typed = error instanceof WorkflowError ? error : new WorkflowError("AGENT_FAILED", errorText(error));
1567
+ this.#settle(item.node, { id: item.node.id, ok: false, error: { code: typed.code, message: typed.message } });
1568
+ continue;
1569
+ }
1570
+ }
1571
+ run.active += 1;
1572
+ this.#active += 1;
1573
+ item.start();
1574
+ }
1575
+ }
1576
+ #release(runId) {
1577
+ const run = this.#runs.get(runId);
1578
+ if (run && run.active > 0) {
1579
+ run.active -= 1;
1580
+ this.#active -= 1;
1581
+ this.#dispatch();
1582
+ }
1583
+ }
1584
+ #settle(node, result) {
1585
+ if (SETTLED_AGENT_STATES.has(node.state))
1586
+ return;
1587
+ const heldPermit = node.state === "running" || node.state === "retrying";
1588
+ node.state = result.ok ? "completed" : result.error.code === "CANCELLED" ? "cancelled" : "failed";
1589
+ Reflect.deleteProperty(node, "steer");
1590
+ this.#persist(node.runId);
1591
+ if (heldPermit)
1592
+ this.#release(node.runId);
1593
+ for (const childId of node.children) {
1594
+ const child = this.#nodes.get(childId);
1595
+ if (child && !child.collected)
1596
+ this.#cancelTree(child);
1597
+ }
1598
+ const resolve = node.resolve;
1599
+ node.resolve = undefined;
1600
+ resolve?.(result);
1601
+ node.resolveCompletion();
1602
+ }
1603
+ #cancelTree(node) {
1604
+ if (SETTLED_AGENT_STATES.has(node.state))
1605
+ return;
1606
+ node.controller.abort();
1607
+ for (const childId of node.children) {
1608
+ const child = this.#nodes.get(childId);
1609
+ if (child)
1610
+ this.#cancelTree(child);
1611
+ }
1612
+ if (node.state === "queued" || node.restored)
1613
+ this.#settle(node, { id: node.id, ok: false, error: { code: "CANCELLED", message: "Agent cancelled" } });
1614
+ }
1615
+ #cancelledOrSettled(node) { return node.controller.signal.aborted || SETTLED_AGENT_STATES.has(node.state); }
1616
+ #node(id) {
1617
+ const node = this.#nodes.get(id);
1618
+ if (!node)
1619
+ throw new WorkflowError("UNKNOWN_AGENT_TYPE", `Unknown owned agent: ${id}`);
1620
+ return node;
1621
+ }
1622
+ #persist(runId) {
1623
+ if (!this.writeOwnership)
1624
+ return;
1625
+ const ownership = this.snapshot().filter(({ id }) => id.startsWith(`${runId}:`));
1626
+ // An ownership write is scheduled, never awaited: the rejection handler must be attached here,
1627
+ // or a failed write becomes an unhandled rejection that terminates the host process. flush() is
1628
+ // the one place that observes it, so the failure is reported instead of discarded.
1629
+ this.#persistence = this.#persistence.then(() => this.writeOwnership?.(runId, ownership)).then(() => undefined, (error) => { if (!this.#persistenceErrors.has(runId))
1630
+ this.#persistenceErrors.set(runId, error instanceof Error ? error : new WorkflowError("INTERNAL_ERROR", String(error))); });
1631
+ }
1632
+ }
1633
+ function resolvePath(path) { return path.replace(/[\\/]+$/, ""); }