@introspection-ai/recipes 0.13.0 → 0.14.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 (222) hide show
  1. package/README.md +46 -57
  2. package/dist/{agent-tool.d.ts → agents.d.ts} +4 -1
  3. package/dist/agents.d.ts.map +1 -0
  4. package/dist/{agent-tool.js → agents.js} +1 -1
  5. package/dist/agents.js.map +1 -0
  6. package/dist/api/extensions.d.ts +3 -0
  7. package/dist/api/extensions.d.ts.map +1 -0
  8. package/dist/api/extensions.js +2 -0
  9. package/dist/api/extensions.js.map +1 -0
  10. package/dist/api/mcp.d.ts +4 -0
  11. package/dist/api/mcp.d.ts.map +1 -0
  12. package/dist/api/mcp.js +2 -0
  13. package/dist/api/mcp.js.map +1 -0
  14. package/dist/api/session.d.ts +3 -0
  15. package/dist/api/session.d.ts.map +1 -0
  16. package/dist/api/session.js +2 -0
  17. package/dist/api/session.js.map +1 -0
  18. package/dist/child-agent.d.ts +5 -4
  19. package/dist/child-agent.d.ts.map +1 -1
  20. package/dist/child-agent.js +26 -117
  21. package/dist/child-agent.js.map +1 -1
  22. package/dist/child-session.d.ts +23 -0
  23. package/dist/child-session.d.ts.map +1 -0
  24. package/dist/child-session.js +48 -0
  25. package/dist/child-session.js.map +1 -0
  26. package/dist/extensions.d.ts +29 -0
  27. package/dist/extensions.d.ts.map +1 -0
  28. package/dist/extensions.js +119 -0
  29. package/dist/extensions.js.map +1 -0
  30. package/dist/index.d.ts +4 -13
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -13
  33. package/dist/index.js.map +1 -1
  34. package/dist/inspect.d.ts +38 -5
  35. package/dist/inspect.d.ts.map +1 -1
  36. package/dist/inspect.js +92 -41
  37. package/dist/inspect.js.map +1 -1
  38. package/dist/interactions.d.ts +18 -2
  39. package/dist/interactions.d.ts.map +1 -1
  40. package/dist/interactions.js +41 -10
  41. package/dist/interactions.js.map +1 -1
  42. package/dist/mcp-catalog.d.ts +1 -0
  43. package/dist/mcp-catalog.d.ts.map +1 -1
  44. package/dist/mcp-catalog.js +14 -6
  45. package/dist/mcp-catalog.js.map +1 -1
  46. package/dist/mcp-chunks/auth-command-ZLGYIEUY.js +273 -0
  47. package/dist/mcp-chunks/call-arguments-LU3W6D3K.js +13 -0
  48. package/dist/mcp-chunks/call-command-LKTKSYTH.js +612 -0
  49. package/dist/mcp-chunks/chunk-2U4BSDC4.js +3951 -0
  50. package/dist/mcp-chunks/chunk-2XL7MG7R.js +272 -0
  51. package/dist/mcp-chunks/chunk-3BJA27PY.js +2321 -0
  52. package/dist/mcp-chunks/chunk-4SQKWYFF.js +15 -0
  53. package/dist/mcp-chunks/chunk-5QOSCSQB.js +290 -0
  54. package/dist/mcp-chunks/chunk-6W5QFASN.js +77 -0
  55. package/dist/mcp-chunks/chunk-6XCZGU3G.js +1004 -0
  56. package/dist/mcp-chunks/chunk-7FHEBT5G.js +149 -0
  57. package/dist/mcp-chunks/chunk-7KA2VJVZ.js +114 -0
  58. package/dist/mcp-chunks/chunk-7UDVSOOF.js +138 -0
  59. package/dist/mcp-chunks/chunk-BNV3TPE4.js +66 -0
  60. package/dist/mcp-chunks/chunk-BTLAWHTA.js +129 -0
  61. package/dist/mcp-chunks/chunk-BV56DXPW.js +13 -0
  62. package/dist/mcp-chunks/chunk-BWHIA4PC.js +90 -0
  63. package/dist/mcp-chunks/chunk-BZIQOWE6.js +267 -0
  64. package/dist/mcp-chunks/chunk-D3MRAZ6H.js +16140 -0
  65. package/dist/mcp-chunks/chunk-DHBE7UBH.js +750 -0
  66. package/dist/mcp-chunks/chunk-E22DYOW5.js +251 -0
  67. package/dist/mcp-chunks/chunk-ECIVB7P4.js +139 -0
  68. package/dist/mcp-chunks/chunk-HKRJ6O36.js +1076 -0
  69. package/dist/mcp-chunks/chunk-IABRYROW.js +44 -0
  70. package/dist/mcp-chunks/chunk-J6LYIZXN.js +45 -0
  71. package/dist/mcp-chunks/chunk-L7NTQVYN.js +161 -0
  72. package/dist/mcp-chunks/chunk-LEZSACDE.js +141 -0
  73. package/dist/mcp-chunks/chunk-LKIBAYKU.js +208 -0
  74. package/dist/mcp-chunks/chunk-LNH5LAJD.js +152 -0
  75. package/dist/mcp-chunks/chunk-LQQEYD76.js +124 -0
  76. package/dist/mcp-chunks/chunk-LRKZP7DJ.js +47 -0
  77. package/dist/mcp-chunks/chunk-LS7PMWSR.js +109 -0
  78. package/dist/mcp-chunks/chunk-M3TPODXI.js +95 -0
  79. package/dist/mcp-chunks/chunk-MZ2NARG7.js +400 -0
  80. package/dist/mcp-chunks/chunk-N46NEFAF.js +45 -0
  81. package/dist/mcp-chunks/chunk-NNOEGEUZ.js +17273 -0
  82. package/dist/mcp-chunks/chunk-NRAJX5E4.js +69 -0
  83. package/dist/mcp-chunks/chunk-P3PSC54P.js +869 -0
  84. package/dist/mcp-chunks/chunk-PIIYJR44.js +6341 -0
  85. package/dist/mcp-chunks/chunk-PWYFKGKG.js +385 -0
  86. package/dist/mcp-chunks/chunk-SJCCBNZ4.js +45 -0
  87. package/dist/mcp-chunks/chunk-TATVT5IT.js +194 -0
  88. package/dist/mcp-chunks/chunk-TEUKFFQL.js +29 -0
  89. package/dist/mcp-chunks/chunk-UKYNQGNE.js +150 -0
  90. package/dist/mcp-chunks/chunk-UNKB4SBV.js +37 -0
  91. package/dist/mcp-chunks/chunk-WD6BOD24.js +138 -0
  92. package/dist/mcp-chunks/chunk-WQJZMCJN.js +73 -0
  93. package/dist/mcp-chunks/chunk-XANPGTLI.js +364 -0
  94. package/dist/mcp-chunks/chunk-XQK2JSW4.js +35 -0
  95. package/dist/mcp-chunks/chunk-YHQUTU6L.js +571 -0
  96. package/dist/mcp-chunks/chunk-YVZKMV5H.js +57 -0
  97. package/dist/mcp-chunks/cli-UV4QJZZM.js +929 -0
  98. package/dist/mcp-chunks/client-S7RQUG5U.js +13 -0
  99. package/dist/mcp-chunks/config-command-NLAGKID5.js +1101 -0
  100. package/dist/mcp-chunks/daemon-command-U242Z6VY.js +26 -0
  101. package/dist/mcp-chunks/dist-LK3SHAMX.js +6478 -0
  102. package/dist/mcp-chunks/emit-ts-command-VBXVXO6V.js +386 -0
  103. package/dist/mcp-chunks/generate-cli-runner-6CAZZA3P.js +1426 -0
  104. package/dist/mcp-chunks/inspect-cli-command-3ZORTOR7.js +111 -0
  105. package/dist/mcp-chunks/launch-YKRXHXJI.js +10 -0
  106. package/dist/mcp-chunks/lifecycle-XVKRHPH3.js +16 -0
  107. package/dist/mcp-chunks/list-command-2MEGGNFQ.js +4190 -0
  108. package/dist/mcp-chunks/output-utils-NKESLAJ6.js +12 -0
  109. package/dist/mcp-chunks/prompt-B1Yc1NPt-VGOTWJMN.js +918 -0
  110. package/dist/mcp-chunks/record-command-YXHTTTT5.js +17 -0
  111. package/dist/mcp-chunks/replay-command-25HJ6AQU.js +85 -0
  112. package/dist/mcp-chunks/resource-command-OO4NYF5C.js +97 -0
  113. package/dist/mcp-chunks/result-utils-MOHHLNM7.js +15 -0
  114. package/dist/mcp-chunks/runtime-5WJ3G25Y.js +33 -0
  115. package/dist/mcp-chunks/runtime-wrapper-RPWDIDBF.js +10 -0
  116. package/dist/mcp-chunks/serve-command-DHXJZMMN.js +3302 -0
  117. package/dist/mcp-chunks/timeouts-W2XWCCXR.js +18 -0
  118. package/dist/mcp-chunks/vault-command-PIYIUEZL.js +154 -0
  119. package/dist/mcp-daemon-client.d.ts +6 -1
  120. package/dist/mcp-daemon-client.d.ts.map +1 -1
  121. package/dist/mcp-daemon-client.js +119 -2
  122. package/dist/mcp-daemon-client.js.map +1 -1
  123. package/dist/mcp-daemon-protocol.d.ts +18 -1
  124. package/dist/mcp-daemon-protocol.d.ts.map +1 -1
  125. package/dist/mcp-daemon-protocol.js +31 -0
  126. package/dist/mcp-daemon-protocol.js.map +1 -1
  127. package/dist/mcp-daemon.js +169 -76602
  128. package/dist/mcp-daemon.js.map +1 -1
  129. package/dist/mcp-policy.d.ts +11 -0
  130. package/dist/mcp-policy.d.ts.map +1 -0
  131. package/dist/mcp-policy.js +27 -0
  132. package/dist/mcp-policy.js.map +1 -0
  133. package/dist/mcp-run-worker.js +38 -76540
  134. package/dist/mcp-tools.d.ts +39 -0
  135. package/dist/mcp-tools.d.ts.map +1 -0
  136. package/dist/mcp-tools.js +493 -0
  137. package/dist/mcp-tools.js.map +1 -0
  138. package/dist/mcp.d.ts +21 -9
  139. package/dist/mcp.d.ts.map +1 -1
  140. package/dist/mcp.js +101 -31
  141. package/dist/mcp.js.map +1 -1
  142. package/dist/model-binding.d.ts +39 -0
  143. package/dist/model-binding.d.ts.map +1 -0
  144. package/dist/model-binding.js +103 -0
  145. package/dist/model-binding.js.map +1 -0
  146. package/dist/pi-extension.d.ts +2 -22
  147. package/dist/pi-extension.d.ts.map +1 -1
  148. package/dist/pi-extension.js +296 -71
  149. package/dist/pi-extension.js.map +1 -1
  150. package/dist/recipe/resolve.d.ts +43 -20
  151. package/dist/recipe/resolve.d.ts.map +1 -1
  152. package/dist/recipe/resolve.js +215 -70
  153. package/dist/recipe/resolve.js.map +1 -1
  154. package/dist/recipe-agent.d.ts +24 -27
  155. package/dist/recipe-agent.d.ts.map +1 -1
  156. package/dist/recipe-agent.js +278 -230
  157. package/dist/recipe-agent.js.map +1 -1
  158. package/dist/recipe-check.d.ts +18 -0
  159. package/dist/recipe-check.d.ts.map +1 -0
  160. package/dist/recipe-check.js +261 -0
  161. package/dist/recipe-check.js.map +1 -0
  162. package/dist/recipe-extensions.d.ts.map +1 -1
  163. package/dist/recipe-extensions.js +0 -3
  164. package/dist/recipe-extensions.js.map +1 -1
  165. package/dist/recipe-model.d.ts +3 -1
  166. package/dist/recipe-model.d.ts.map +1 -1
  167. package/dist/recipe-model.js +15 -12
  168. package/dist/recipe-model.js.map +1 -1
  169. package/dist/recipe-package.d.ts +6 -28
  170. package/dist/recipe-package.d.ts.map +1 -1
  171. package/dist/recipe-package.js +248 -191
  172. package/dist/recipe-package.js.map +1 -1
  173. package/dist/recipe-skills.d.ts.map +1 -1
  174. package/dist/recipe-skills.js +42 -29
  175. package/dist/recipe-skills.js.map +1 -1
  176. package/dist/run-controller.d.ts +10 -9
  177. package/dist/run-controller.d.ts.map +1 -1
  178. package/dist/run-controller.js +28 -21
  179. package/dist/run-controller.js.map +1 -1
  180. package/dist/session.d.ts +40 -53
  181. package/dist/session.d.ts.map +1 -1
  182. package/dist/session.js +312 -173
  183. package/dist/session.js.map +1 -1
  184. package/dist/test-utils.d.ts +7 -7
  185. package/dist/test-utils.d.ts.map +1 -1
  186. package/dist/test-utils.js +32 -15
  187. package/dist/test-utils.js.map +1 -1
  188. package/docs/agent-composition.md +41 -35
  189. package/docs/host-api.md +201 -0
  190. package/docs/index.md +9 -14
  191. package/docs/interactions.md +12 -11
  192. package/docs/mcp-auth.md +6 -6
  193. package/docs/mcp-configuration.md +73 -25
  194. package/docs/pi-extension.md +39 -11
  195. package/docs/recipe-flow.md +9 -9
  196. package/docs/recipe-format.md +106 -41
  197. package/docs/recipe-judges.md +24 -36
  198. package/package.json +23 -17
  199. package/vendor/mcp-client/darwin-arm64/mcp-client +0 -0
  200. package/vendor/mcp-client/darwin-x64/mcp-client +0 -0
  201. package/vendor/mcp-client/linux-arm64/mcp-client +0 -0
  202. package/vendor/recipe-check/darwin-arm64/recipe-check +0 -0
  203. package/vendor/recipe-check/darwin-x64/recipe-check +0 -0
  204. package/vendor/recipe-check/linux-arm64/recipe-check +0 -0
  205. package/vendor/recipe-check/linux-x64/recipe-check +0 -0
  206. package/vendor/recipe-check/linux-x64-musl/recipe-check +0 -0
  207. package/vendor/recipe-check/win32-x64/recipe-check.exe +0 -0
  208. package/dist/agent-tool.d.ts.map +0 -1
  209. package/dist/agent-tool.js.map +0 -1
  210. package/dist/run.d.ts +0 -28
  211. package/dist/run.d.ts.map +0 -1
  212. package/dist/run.js +0 -84
  213. package/dist/run.js.map +0 -1
  214. package/dist/testing.d.ts +0 -25
  215. package/dist/testing.d.ts.map +0 -1
  216. package/dist/testing.js +0 -96
  217. package/dist/testing.js.map +0 -1
  218. package/docs/deployment-configuration.md +0 -65
  219. package/docs/migration.md +0 -70
  220. package/docs/python-bindings-release.md +0 -47
  221. package/docs/recipe-evals.md +0 -70
  222. package/docs/runtime-library.md +0 -171
@@ -1,16 +1,20 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { existsSync } from "node:fs";
3
+ import { rm } from "node:fs/promises";
3
4
  import { basename, resolve } from "node:path";
4
5
  import { Text } from "@earendil-works/pi-tui";
5
6
  import { createRecipeChildAgentRunner, promptResultText, } from "./child-agent.js";
6
7
  import { loadRecipeExtensionFactory } from "./recipe-extensions.js";
8
+ import { bindRecipeExtensionFactory, createRecipeExtensionRegistrationRegistry, } from "./extensions.js";
7
9
  import { ChildAgentRunStore, } from "./child-agent-store.js";
8
10
  import { ChildCompletionQueue, envelopeFromRun, renderCompletionNotice, } from "./child-agent-completions.js";
9
- import { clearMcpSession, configureMcpLocalConfigPath, formatMcpConfigurationDiagnostics, materializeMcpSession, materializeSessionMcpCli, resolveAgentMcpSelections, stopMcpDaemon, } from "./mcp.js";
11
+ import { clearMcpSession, clearSessionMcpCli, configureMcpLocalConfigPath, createIsolatedMcpEnvironment, formatMcpConfigurationDiagnostics, materializeMcpSession, materializeSessionMcpCli, resolveAgentMcpSelections, stopMcpDaemon, } from "./mcp.js";
10
12
  import { clearMcpCatalogPreload, preloadMcpCatalogs, } from "./mcp-catalog.js";
11
- import { applyRecipeAgentModelConfigToModel } from "./recipe-model.js";
13
+ import { createMcpToolSet } from "./mcp-tools.js";
14
+ import { applyRecipeAgentModelConfigToModel, cloneModelForRecipe, } from "./recipe-model.js";
15
+ import { checkRecipeAtLoad, formatRecipeDiagnostics, } from "./recipe-check.js";
12
16
  import { resolveRecipe, } from "./recipe/resolve.js";
13
- import { createAgentTool, } from "./agent-tool.js";
17
+ import { createAgentTool, } from "./agents.js";
14
18
  /** Mid-turn completion delivery retries on this cadence until the session idles. */
15
19
  const COMPLETION_DELIVERY_RETRY_MS = 100;
16
20
  const require = createRequire(import.meta.url);
@@ -53,8 +57,8 @@ function visibleSubagents(state) {
53
57
  function mcpSelectionsForAgent(agent) {
54
58
  return resolveAgentMcpSelections(agent.mcp);
55
59
  }
56
- function scopedMcpSelections(state) {
57
- return [state.resolved.agent, ...visibleSubagents(state)].flatMap(mcpSelectionsForAgent);
60
+ function resolvedMcpMode(state) {
61
+ return state.resolved.mcp?.mode ?? "cli";
58
62
  }
59
63
  function textResult(text, details) {
60
64
  return {
@@ -102,7 +106,7 @@ function formatAgentCall(args, theme) {
102
106
  const label = args.label ? ` ${themeFg(theme, "muted", `(${args.label})`)}` : "";
103
107
  return `${themeFg(theme, "toolTitle", themeBold(theme, `agent ${action}`))} ${themeFg(theme, "accent", agent)}${label}`;
104
108
  }
105
- // Keep the legacy wire value so resumed transcripts retain their renderer.
109
+ // Stable wire value used by persisted transcripts and the renderer.
106
110
  const AGENT_COMPLETIONS_TYPE = "recipe-agent-completions";
107
111
  function formatDurationMs(ms) {
108
112
  const seconds = Math.round(ms / 1000);
@@ -196,7 +200,10 @@ function bulletList(items) {
196
200
  }
197
201
  function activeRecipeTools(state, activeTools) {
198
202
  const active = new Set(activeTools);
199
- const recipeTools = new Set(state.resolved.tools);
203
+ const recipeTools = new Set([
204
+ ...state.resolved.tools,
205
+ ...(state.resolved.subagents.size > 0 ? ["agent"] : []),
206
+ ]);
200
207
  return [...recipeTools]
201
208
  .filter((tool) => active.has(tool))
202
209
  .sort();
@@ -207,14 +214,18 @@ function recipeSummary(state, activeTools) {
207
214
  "Active Recipe",
208
215
  `Name: ${state.resolved.manifest.name}@${state.resolved.manifest.version}`,
209
216
  state.resolved.manifest.description ? `Description: ${state.resolved.manifest.description}` : undefined,
210
- `Agent: ${state.resolved.agentName}`,
211
- `Model: ${state.resolved.agent.model?.name ?? "(session default)"}`,
212
- `Thinking level: ${state.resolved.agent.model?.thinkingLevel ?? "(session default)"}`,
217
+ `Agent: ${state.resolved.name}`,
218
+ `Model: ${state.resolved.definition.model?.name ?? "(session default)"}`,
219
+ `Thinking level: ${state.resolved.definition.model?.thinkingLevel ?? "(session default)"}`,
213
220
  `Subagents: ${nameList(subagents)}`,
214
221
  "",
215
222
  "Active recipe tools:",
216
223
  ...bulletList(activeRecipeTools(state, activeTools)),
217
224
  "",
225
+ `Package extensions: ${state.resolved.extensionPaths.length}`,
226
+ `Package prompts: ${state.resolved.promptPaths.length}`,
227
+ "Host layer: trusted ambient Pi hooks and settings may also apply",
228
+ "",
218
229
  `Directory: ${state.resolved.recipeDir}`,
219
230
  `Workspace: ${state.cwd}`,
220
231
  ].filter((line) => line !== undefined).join("\n");
@@ -414,10 +425,32 @@ export function createRecipesExtension(opts = {}) {
414
425
  childRuns.delete(id);
415
426
  return controllerSummary(run, "closed");
416
427
  },
428
+ async shutdown() {
429
+ await closeAllChildRuns();
430
+ },
417
431
  };
418
432
  async function closeAllChildRuns() {
419
433
  await Promise.all([...childRuns.keys()].map((id) => localRunController.close(id)));
420
434
  }
435
+ async function closeRootMcpRuntime() {
436
+ const privateEnv = state?.mcpPrivateEnv;
437
+ const privateDirectory = state?.mcpPrivateDirectory;
438
+ if (privateEnv) {
439
+ clearMcpCatalogPreload(privateEnv);
440
+ await stopMcpDaemon(privateEnv);
441
+ }
442
+ if (privateDirectory) {
443
+ await rm(privateDirectory, { recursive: true, force: true });
444
+ }
445
+ if (state) {
446
+ state.mcpPrivateEnv = undefined;
447
+ state.mcpPrivateDirectory = undefined;
448
+ if (state.mcpAmbientMcporterConfig) {
449
+ env.MCPORTER_CONFIG = state.mcpAmbientMcporterConfig;
450
+ state.mcpAmbientMcporterConfig = undefined;
451
+ }
452
+ }
453
+ }
421
454
  function archivedControlError(id) {
422
455
  return {
423
456
  ...textResult(`Agent run ${id} belongs to a previous Pi session and cannot be controlled; only status and wait are available.`, { id }),
@@ -429,6 +462,8 @@ export function createRecipesExtension(opts = {}) {
429
462
  // later session load in the same process.
430
463
  const launchRecipeDir = stringFlag(env.PI_RECIPE_DIR);
431
464
  const launchAgentName = stringFlag(env.PI_AGENT_NAME);
465
+ const originalRecipeDirEnv = env.PI_RECIPE_DIR;
466
+ const originalAgentNameEnv = env.PI_AGENT_NAME;
432
467
  function recipeFlag(pi) {
433
468
  return stringFlag(pi.getFlag("recipe")) ?? launchRecipeDir;
434
469
  }
@@ -447,12 +482,11 @@ export function createRecipesExtension(opts = {}) {
447
482
  const key = [cwd, recipeDir, requestedAgentName ?? ""].join("\0");
448
483
  if (state?.key === key)
449
484
  return state;
485
+ let resolvedRecipe;
450
486
  let resolved;
451
487
  try {
452
- resolved = resolveRecipe({
453
- recipeDir,
454
- agentName: requestedAgentName,
455
- });
488
+ resolvedRecipe = resolveRecipe({ recipeDir });
489
+ resolved = resolvedRecipe.selectAgent(requestedAgentName);
456
490
  }
457
491
  catch (err) {
458
492
  throw new RecipeLaunchError(recipeLoadErrorMessage(flag, err instanceof Error ? err.message : String(err)));
@@ -461,13 +495,22 @@ export function createRecipesExtension(opts = {}) {
461
495
  // recipe-authored instructions. In production `env` is process.env, so
462
496
  // built-in shell tools and child agents inherit these resolved values.
463
497
  env.PI_RECIPE_DIR = recipeDir;
464
- env.PI_AGENT_NAME = resolved.agentName;
498
+ env.PI_AGENT_NAME = resolved.name;
465
499
  state = {
466
500
  key,
467
501
  cwd,
502
+ resolvedRecipe,
468
503
  resolved,
504
+ extensionRegistrations: createRecipeExtensionRegistrationRegistry(),
469
505
  extensionsLoaded: false,
470
506
  configured: false,
507
+ mcpConfigured: false,
508
+ agentMcpMode: "cli",
509
+ initialMcpToolNames: [],
510
+ extensionAllowedToolNames: new Set([
511
+ ...resolved.tools,
512
+ ...(resolved.subagents.size > 0 ? ["agent"] : []),
513
+ ]),
471
514
  };
472
515
  return state;
473
516
  }
@@ -479,29 +522,57 @@ export function createRecipesExtension(opts = {}) {
479
522
  if (!(err instanceof RecipeLaunchError))
480
523
  throw err;
481
524
  const key = [cwd, recipeFlag(pi) ?? "", err.message].join("\0");
482
- if (ctx && lastLaunchErrorKey !== key) {
483
- ctx.ui.notify(err.message, "warning");
525
+ if (lastLaunchErrorKey !== key) {
526
+ failRecipeSession(pi, err.message, ctx);
484
527
  lastLaunchErrorKey = key;
485
528
  }
529
+ else {
530
+ sessionConfigurationError = err.message;
531
+ pi.setActiveTools([]);
532
+ }
486
533
  return null;
487
534
  }
488
535
  }
536
+ function failRecipeSession(pi, message, ctx) {
537
+ sessionConfigurationError = message;
538
+ pi.setActiveTools([]);
539
+ ctx?.ui.notify(`Recipe session cannot start: ${message}`, "warning");
540
+ if (ctx?.mode === "json" || ctx?.mode === "print") {
541
+ process.exitCode = 1;
542
+ }
543
+ }
489
544
  async function loadRecipeExtensions(pi, ctx, launchState) {
545
+ if (launchState.extensionFailure) {
546
+ throw new Error(launchState.extensionFailure);
547
+ }
490
548
  if (launchState.extensionsLoaded)
491
549
  return;
550
+ // Pi cannot unload registrations made by an earlier factory if a later
551
+ // factory fails. Mark the closure attempted before execution so the same
552
+ // live runner never retries a partially installed closure.
553
+ launchState.extensionsLoaded = true;
554
+ for (const tool of pi.getAllTools()) {
555
+ launchState.extensionRegistrations.claim("tool", tool.name, "<host>");
556
+ }
492
557
  let loadedCount = 0;
493
- for (const extensionPath of launchState.resolved.extensionPaths) {
494
- try {
495
- const factory = await loadRecipeExtensionFactory(launchState.resolved.recipeDir, extensionPath);
558
+ try {
559
+ for (const extensionPath of launchState.resolved.extensionPaths) {
560
+ const factory = bindRecipeExtensionFactory(await loadRecipeExtensionFactory(launchState.resolved.recipeDir, extensionPath), Object.freeze({
561
+ recipe: Object.freeze({ name: launchState.resolved.manifest.name }),
562
+ agent: Object.freeze({ name: launchState.resolved.name }),
563
+ session: Object.freeze({
564
+ role: "root",
565
+ }),
566
+ }), launchState.extensionRegistrations, extensionPath, launchState.extensionAllowedToolNames);
496
567
  await factory(pi);
497
568
  loadedCount += 1;
498
569
  }
499
- catch (err) {
500
- const message = err instanceof Error ? err.message : String(err);
501
- ctx.ui.notify(`Recipe extension failed to load: ${extensionPath}\n${message}`, "warning");
502
- }
503
570
  }
504
- launchState.extensionsLoaded = true;
571
+ catch (error) {
572
+ launchState.extensionFailure =
573
+ error instanceof Error ? error.message : String(error);
574
+ throw error;
575
+ }
505
576
  if (launchState.resolved.extensionPaths.length > 0) {
506
577
  ctx.ui.notify(`Recipe extensions: ${loadedCount}/${launchState.resolved.extensionPaths.length} loaded`, "info");
507
578
  }
@@ -511,15 +582,16 @@ export function createRecipesExtension(opts = {}) {
511
582
  return;
512
583
  const labelParts = [
513
584
  `${launchState.resolved.manifest.name}@${launchState.resolved.manifest.version}`,
514
- `agent:${launchState.resolved.agentName}`,
585
+ `agent:${launchState.resolved.name}`,
515
586
  ].filter(Boolean);
516
587
  pi.setSessionName(labelParts.join(" "));
517
588
  const { provider, model } = modelParts(launchState.resolved.modelSpec);
518
589
  const lookupProvider = provider === "gemini" ? "google" : provider;
519
- const resolvedModel = ctx.modelRegistry.find(lookupProvider, model);
520
- if (!resolvedModel) {
590
+ const registeredModel = ctx.modelRegistry.find(lookupProvider, model);
591
+ if (!registeredModel) {
521
592
  throw new Error(`Recipe model is not available: ${launchState.resolved.modelSpec}`);
522
593
  }
594
+ const resolvedModel = cloneModelForRecipe(registeredModel);
523
595
  applyRecipeAgentModelConfigToModel(resolvedModel, launchState.resolved.modelConfig);
524
596
  const ok = await pi.setModel(resolvedModel);
525
597
  if (!ok) {
@@ -528,54 +600,167 @@ export function createRecipesExtension(opts = {}) {
528
600
  if (launchState.resolved.thinkingLevel) {
529
601
  pi.setThinkingLevel(launchState.resolved.thinkingLevel);
530
602
  }
531
- const activeTools = new Set(launchState.resolved.tools);
603
+ const activeTools = new Set([
604
+ ...launchState.resolved.tools,
605
+ ...(launchState.resolved.subagents.size > 0 ? ["agent"] : []),
606
+ ...launchState.initialMcpToolNames,
607
+ ...(launchState.mcpSearchToolName
608
+ ? [launchState.mcpSearchToolName]
609
+ : []),
610
+ ]);
611
+ const registeredTools = new Set(pi.getAllTools().map((tool) => tool.name));
612
+ const missingTools = [...activeTools].filter((name) => !registeredTools.has(name));
613
+ if (missingTools.length > 0) {
614
+ throw new Error(`Recipe agent "${launchState.resolved.name}" declares unavailable tool(s): ${missingTools.join(", ")}`);
615
+ }
532
616
  pi.setActiveTools([...activeTools]);
533
617
  launchState.configured = true;
534
618
  sessionConfigurationError = null;
535
619
  }
536
- async function configureMcp(launchState, ctx) {
537
- const mcpSelections = scopedMcpSelections(launchState);
538
- if (mcpSelections.length === 0) {
539
- await clearMcpSession(env, launchState.cwd);
620
+ async function configureMcp(pi, launchState, ctx) {
621
+ if (launchState.mcpConfigured)
540
622
  return;
541
- }
623
+ launchState.agentMcpMode = resolvedMcpMode(launchState);
624
+ launchState.initialMcpToolNames = [];
625
+ launchState.mcpSearchToolName = undefined;
542
626
  configureMcpLocalConfigPath({
543
627
  cwd: launchState.cwd,
544
628
  recipeDir: launchState.resolved.recipeDir,
545
629
  env,
546
630
  });
547
- const [, session] = await Promise.all([
548
- materializeSessionMcpCli({
549
- cwd: launchState.cwd,
550
- env,
551
- }),
552
- materializeMcpSession({
553
- cwd: launchState.cwd,
554
- manifest: launchState.resolved.manifest,
555
- agentMcp: mcpSelections,
556
- env,
557
- }),
558
- ]);
559
- if (session.servers.length > 0) {
560
- void preloadMcpCatalogs({ env }).catch((error) => {
561
- console.warn(`Recipe MCP catalog preload failed: ${error instanceof Error ? error.message : String(error)}`);
562
- });
563
- ctx.ui.notify(`Recipe MCP: ${session.servers.length} server(s) configured; runtime warming in background`, "info");
631
+ if (launchState.agentMcpMode === "cli") {
632
+ const mcpSelections = resolveAgentMcpSelections(launchState.resolved.mcp);
633
+ if (mcpSelections.length === 0) {
634
+ await clearMcpSession(env, launchState.cwd);
635
+ launchState.mcpConfigured = true;
636
+ return;
637
+ }
638
+ const [, session] = await Promise.all([
639
+ materializeSessionMcpCli({
640
+ cwd: launchState.cwd,
641
+ env,
642
+ }),
643
+ materializeMcpSession({
644
+ cwd: launchState.cwd,
645
+ manifest: launchState.resolved.manifest,
646
+ agentMcp: mcpSelections,
647
+ env,
648
+ }),
649
+ ]);
650
+ if (session.servers.length > 0) {
651
+ void preloadMcpCatalogs({ env }).catch((error) => {
652
+ console.warn(`Recipe MCP catalog preload failed: ${error instanceof Error ? error.message : String(error)}`);
653
+ });
654
+ ctx.ui.notify(`Recipe MCP: ${session.servers.length} server(s) configured; runtime warming in background`, "info");
655
+ }
656
+ else {
657
+ const detail = formatMcpConfigurationDiagnostics(session.diagnostics ?? []);
658
+ ctx.ui.notify([
659
+ "Recipe MCP: no servers are available to this agent. Check package policy and .pi/mcp.local.json.",
660
+ ...(detail ? ["", detail] : []),
661
+ ].join("\n"), "warning");
662
+ }
564
663
  const detail = formatMcpConfigurationDiagnostics(session.diagnostics ?? []);
565
- if (detail) {
664
+ if (session.servers.length > 0 && detail) {
566
665
  ctx.ui.notify([
567
666
  "Recipe MCP: some configured servers or tools were filtered.",
568
667
  "",
569
668
  detail,
570
669
  ].join("\n"), "warning");
571
670
  }
671
+ launchState.mcpConfigured = true;
672
+ return;
572
673
  }
573
- else {
574
- const detail = formatMcpConfigurationDiagnostics(session.diagnostics ?? []);
575
- ctx.ui.notify([
576
- "Recipe MCP: no servers are available to this agent. Check package policy and .pi/mcp.local.json.",
577
- ...(detail ? ["", detail] : []),
578
- ].join("\n"), "warning");
674
+ const clearedCli = await clearSessionMcpCli(env, launchState.cwd);
675
+ launchState.mcpAmbientMcporterConfig =
676
+ clearedCli.previousMcporterConfig;
677
+ const rootMcp = launchState.resolved.mcp;
678
+ const rootSelections = mcpSelectionsForAgent(launchState.resolved.definition);
679
+ if (!rootMcp || rootSelections.length === 0) {
680
+ launchState.mcpConfigured = true;
681
+ return;
682
+ }
683
+ const privateRuntime = await createIsolatedMcpEnvironment(env);
684
+ launchState.mcpPrivateEnv = privateRuntime.env;
685
+ launchState.mcpPrivateDirectory = privateRuntime.directory;
686
+ try {
687
+ const session = await materializeMcpSession({
688
+ cwd: launchState.cwd,
689
+ manifest: launchState.resolved.manifest,
690
+ agentMcp: rootSelections,
691
+ env: privateRuntime.env,
692
+ mcporterConfigPath: privateRuntime.mcporterConfigPath,
693
+ });
694
+ if (session.servers.length === 0) {
695
+ const detail = formatMcpConfigurationDiagnostics(session.diagnostics ?? []);
696
+ ctx.ui.notify([
697
+ "Recipe MCP tools: no servers are available to the root agent.",
698
+ ...(detail ? ["", detail] : []),
699
+ ].join("\n"), "warning");
700
+ launchState.mcpConfigured = true;
701
+ return;
702
+ }
703
+ const catalogs = await preloadMcpCatalogs({
704
+ env: privateRuntime.env,
705
+ allowPartial: true,
706
+ });
707
+ const materialized = createMcpToolSet({
708
+ session,
709
+ catalogs,
710
+ mcp: rootMcp,
711
+ env: privateRuntime.env,
712
+ activation: {
713
+ getActiveTools: () => pi.getActiveTools(),
714
+ setActiveTools: (names) => pi.setActiveTools(names),
715
+ },
716
+ });
717
+ const existing = new Set(pi.getAllTools().map((tool) => tool.name));
718
+ for (const tool of materialized.tools) {
719
+ if (existing.has(tool.name)) {
720
+ throw new Error(`Cannot register MCP tool '${tool.name}': the Pi tool name is already in use.`);
721
+ }
722
+ }
723
+ for (const tool of materialized.tools)
724
+ pi.registerTool(tool);
725
+ const registered = new Set(pi.getAllTools().map((tool) => tool.name));
726
+ const missing = materialized.tools
727
+ .map((tool) => tool.name)
728
+ .filter((name) => !registered.has(name));
729
+ if (missing.length > 0) {
730
+ throw new Error(`Pi did not register MCP tool(s): ${missing.join(", ")}`);
731
+ }
732
+ launchState.initialMcpToolNames =
733
+ materialized.initialActiveToolNames;
734
+ launchState.mcpSearchToolName = materialized.searchToolName;
735
+ for (const toolName of materialized.toolNames) {
736
+ launchState.extensionAllowedToolNames.add(toolName);
737
+ }
738
+ if (materialized.searchToolName) {
739
+ launchState.extensionAllowedToolNames.add(materialized.searchToolName);
740
+ }
741
+ ctx.ui.notify(`Recipe MCP tools: ${materialized.toolNames.length} registered, ${materialized.initialActiveToolNames.length} initially active${materialized.searchToolName ? "; deferred search enabled" : ""}`, "info");
742
+ const diagnostics = [
743
+ ...formatMcpConfigurationDiagnostics(session.diagnostics ?? [])
744
+ .split("\n")
745
+ .filter(Boolean),
746
+ ...materialized.diagnostics,
747
+ ];
748
+ if (diagnostics.length > 0) {
749
+ ctx.ui.notify([
750
+ "Recipe MCP: some configured tools were unavailable.",
751
+ "",
752
+ ...diagnostics,
753
+ ].join("\n"), "warning");
754
+ }
755
+ launchState.mcpConfigured = true;
756
+ }
757
+ catch (error) {
758
+ clearMcpCatalogPreload(privateRuntime.env);
759
+ await stopMcpDaemon(privateRuntime.env);
760
+ await rm(privateRuntime.directory, { recursive: true, force: true });
761
+ launchState.mcpPrivateEnv = undefined;
762
+ launchState.mcpPrivateDirectory = undefined;
763
+ throw error;
579
764
  }
580
765
  }
581
766
  async function runChildAgent(launchState, agentName, prompt, label, ctx, onUpdate) {
@@ -593,7 +778,7 @@ export function createRecipesExtension(opts = {}) {
593
778
  }
594
779
  };
595
780
  const runner = createChildAgentRunner({
596
- recipeDir: launchState.resolved.recipeDir,
781
+ recipe: launchState.resolvedRecipe,
597
782
  workspaceDir: launchState.cwd,
598
783
  env,
599
784
  agentName,
@@ -763,6 +948,7 @@ export function createRecipesExtension(opts = {}) {
763
948
  return;
764
949
  }
765
950
  await closeAllChildRuns();
951
+ await closeRootMcpRuntime();
766
952
  state = null;
767
953
  archivedRuns.clear();
768
954
  completions.clear();
@@ -798,37 +984,65 @@ export function createRecipesExtension(opts = {}) {
798
984
  pi.on("session_start", async (_event, ctx) => {
799
985
  sessionCtx = ctx;
800
986
  localAgentContext = ctx;
987
+ const selectedRecipe = recipeFlag(pi);
988
+ if (selectedRecipe) {
989
+ const recipeDir = resolve(ctx.cwd, selectedRecipe);
990
+ if (existsSync(recipeDir)) {
991
+ try {
992
+ const report = await checkRecipeAtLoad(recipeDir, env);
993
+ if (!report.valid) {
994
+ const message = formatRecipeDiagnostics(report.diagnostics);
995
+ failRecipeSession(pi, `validation failed\n${message}`, ctx);
996
+ return;
997
+ }
998
+ }
999
+ catch (error) {
1000
+ const message = error instanceof Error ? error.message : String(error);
1001
+ failRecipeSession(pi, `validation failed\n${message}`, ctx);
1002
+ return;
1003
+ }
1004
+ }
1005
+ }
801
1006
  const launchState = safeLoadState(pi, ctx.cwd, ctx);
802
1007
  if (!launchState)
803
1008
  return;
1009
+ launchState.configured = false;
804
1010
  visibleAgentDefinitions.clear();
805
1011
  for (const [name, definition] of launchState.resolved.subagents) {
806
1012
  visibleAgentDefinitions.set(name, definition);
807
1013
  }
808
- await loadRecipeExtensions(pi, ctx, launchState);
809
1014
  try {
810
- await configureMcp(launchState, ctx);
1015
+ await loadRecipeExtensions(pi, ctx, launchState);
811
1016
  }
812
1017
  catch (err) {
813
- ctx.ui.notify(`Recipe MCP failed to configure: ${err instanceof Error ? err.message : String(err)}`, "warning");
1018
+ const message = err instanceof Error ? err.message : String(err);
1019
+ failRecipeSession(pi, `extension startup failed\n${message}`, ctx);
814
1020
  return;
815
1021
  }
816
1022
  try {
817
- await configureSession(pi, ctx, launchState);
1023
+ await configureMcp(pi, launchState, ctx);
818
1024
  }
819
1025
  catch (err) {
820
1026
  const message = err instanceof Error ? err.message : String(err);
821
- // A recipe's declared model is part of its behavior contract. Continuing
822
- // with Pi's previously active model can produce plausible but invalid
823
- // results, so stop the session instead of silently falling back.
824
1027
  sessionConfigurationError = message;
825
1028
  pi.setActiveTools([]);
826
- ctx.ui.notify(`Recipe session cannot start: ${message}`, "warning");
1029
+ ctx.ui.notify(`Recipe MCP failed to configure: ${message}`, "warning");
827
1030
  if (ctx.mode === "json" || ctx.mode === "print") {
828
1031
  process.exitCode = 1;
829
1032
  }
830
1033
  return;
831
1034
  }
1035
+ try {
1036
+ await configureSession(pi, ctx, launchState);
1037
+ }
1038
+ catch (err) {
1039
+ const message = err instanceof Error ? err.message : String(err);
1040
+ // A recipe's declared model is part of its behavior contract. Continuing
1041
+ // with Pi's previously active model can produce plausible but invalid
1042
+ // results, so stop the session instead of silently falling back.
1043
+ failRecipeSession(pi, message, ctx);
1044
+ return;
1045
+ }
832
1046
  // Restore run snapshots persisted by a previous Pi process so run ids
833
1047
  // referenced in a resumed conversation stay resolvable (read-only).
834
1048
  try {
@@ -844,8 +1058,21 @@ export function createRecipesExtension(opts = {}) {
844
1058
  });
845
1059
  pi.on("session_shutdown", async () => {
846
1060
  await closeAllChildRuns();
1061
+ await closeRootMcpRuntime();
847
1062
  clearMcpCatalogPreload(env);
848
1063
  await stopMcpDaemon(env);
1064
+ if (originalRecipeDirEnv === undefined) {
1065
+ delete env.PI_RECIPE_DIR;
1066
+ }
1067
+ else {
1068
+ env.PI_RECIPE_DIR = originalRecipeDirEnv;
1069
+ }
1070
+ if (originalAgentNameEnv === undefined) {
1071
+ delete env.PI_AGENT_NAME;
1072
+ }
1073
+ else {
1074
+ env.PI_AGENT_NAME = originalAgentNameEnv;
1075
+ }
849
1076
  });
850
1077
  pi.on("agent_start", (_event, ctx) => {
851
1078
  if (!sessionConfigurationError)
@@ -860,8 +1087,8 @@ export function createRecipesExtension(opts = {}) {
860
1087
  if (!launchState)
861
1088
  return {};
862
1089
  return {
863
- skillPaths: launchState.resolved.skillPaths,
864
- promptPaths: launchState.resolved.promptPaths,
1090
+ skillPaths: [...launchState.resolved.skillPaths],
1091
+ promptPaths: [...launchState.resolved.promptPaths],
865
1092
  };
866
1093
  });
867
1094
  pi.on("before_agent_start", (event, ctx) => {
@@ -874,7 +1101,5 @@ export function createRecipesExtension(opts = {}) {
874
1101
  });
875
1102
  };
876
1103
  }
877
- /** @deprecated Use `createRecipesExtension`. */
878
- export const createPiRecipesExtension = createRecipesExtension;
879
1104
  export default createRecipesExtension();
880
1105
  //# sourceMappingURL=pi-extension.js.map