agentfootprint 2.3.0 → 2.5.0

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 (212) hide show
  1. package/README.md +293 -247
  2. package/dist/adapters/llm/AnthropicProvider.js +2 -2
  3. package/dist/adapters/llm/AnthropicProvider.js.map +1 -1
  4. package/dist/adapters/llm/BedrockProvider.js +2 -2
  5. package/dist/adapters/llm/BedrockProvider.js.map +1 -1
  6. package/dist/adapters/llm/BrowserAnthropicProvider.js +96 -28
  7. package/dist/adapters/llm/BrowserAnthropicProvider.js.map +1 -1
  8. package/dist/adapters/llm/OpenAIProvider.js +2 -2
  9. package/dist/adapters/llm/OpenAIProvider.js.map +1 -1
  10. package/dist/adapters/memory/agentcore.js +9 -3
  11. package/dist/adapters/memory/agentcore.js.map +1 -1
  12. package/dist/adapters/memory/redis.js +9 -3
  13. package/dist/adapters/memory/redis.js.map +1 -1
  14. package/dist/core/Agent.js +493 -79
  15. package/dist/core/Agent.js.map +1 -1
  16. package/dist/core/flowchartAsTool.js +188 -0
  17. package/dist/core/flowchartAsTool.js.map +1 -0
  18. package/dist/core/outputSchema.js +119 -0
  19. package/dist/core/outputSchema.js.map +1 -0
  20. package/dist/core/slots/buildSystemPromptSlot.js +8 -0
  21. package/dist/core/slots/buildSystemPromptSlot.js.map +1 -1
  22. package/dist/core/slots/buildToolsSlot.js +56 -5
  23. package/dist/core/slots/buildToolsSlot.js.map +1 -1
  24. package/dist/esm/adapters/llm/AnthropicProvider.js +2 -2
  25. package/dist/esm/adapters/llm/AnthropicProvider.js.map +1 -1
  26. package/dist/esm/adapters/llm/BedrockProvider.js +2 -2
  27. package/dist/esm/adapters/llm/BedrockProvider.js.map +1 -1
  28. package/dist/esm/adapters/llm/BrowserAnthropicProvider.js +96 -28
  29. package/dist/esm/adapters/llm/BrowserAnthropicProvider.js.map +1 -1
  30. package/dist/esm/adapters/llm/OpenAIProvider.js +2 -2
  31. package/dist/esm/adapters/llm/OpenAIProvider.js.map +1 -1
  32. package/dist/esm/adapters/memory/agentcore.js +9 -3
  33. package/dist/esm/adapters/memory/agentcore.js.map +1 -1
  34. package/dist/esm/adapters/memory/redis.js +9 -3
  35. package/dist/esm/adapters/memory/redis.js.map +1 -1
  36. package/dist/esm/core/Agent.js +492 -78
  37. package/dist/esm/core/Agent.js.map +1 -1
  38. package/dist/esm/core/flowchartAsTool.js +184 -0
  39. package/dist/esm/core/flowchartAsTool.js.map +1 -0
  40. package/dist/esm/core/outputSchema.js +113 -0
  41. package/dist/esm/core/outputSchema.js.map +1 -0
  42. package/dist/esm/core/slots/buildSystemPromptSlot.js +8 -0
  43. package/dist/esm/core/slots/buildSystemPromptSlot.js.map +1 -1
  44. package/dist/esm/core/slots/buildToolsSlot.js +56 -5
  45. package/dist/esm/core/slots/buildToolsSlot.js.map +1 -1
  46. package/dist/esm/index.js +34 -5
  47. package/dist/esm/index.js.map +1 -1
  48. package/dist/esm/lib/injection-engine/SkillRegistry.js +181 -0
  49. package/dist/esm/lib/injection-engine/SkillRegistry.js.map +1 -0
  50. package/dist/esm/lib/injection-engine/factories/defineSkill.js +35 -0
  51. package/dist/esm/lib/injection-engine/factories/defineSkill.js.map +1 -1
  52. package/dist/esm/lib/injection-engine/index.js +9 -1
  53. package/dist/esm/lib/injection-engine/index.js.map +1 -1
  54. package/dist/esm/lib/injection-engine/skillTools.js +125 -0
  55. package/dist/esm/lib/injection-engine/skillTools.js.map +1 -0
  56. package/dist/esm/lib/injection-engine/types.js +8 -0
  57. package/dist/esm/lib/injection-engine/types.js.map +1 -1
  58. package/dist/esm/lib/lazyRequire.js +33 -0
  59. package/dist/esm/lib/lazyRequire.js.map +1 -0
  60. package/dist/esm/lib/mcp/mcpClient.js +4 -6
  61. package/dist/esm/lib/mcp/mcpClient.js.map +1 -1
  62. package/dist/esm/llm-providers.js +31 -0
  63. package/dist/esm/llm-providers.js.map +1 -0
  64. package/dist/esm/locales/index.js +144 -0
  65. package/dist/esm/locales/index.js.map +1 -0
  66. package/dist/esm/memory-providers.js +44 -0
  67. package/dist/esm/memory-providers.js.map +1 -0
  68. package/dist/esm/providers.js +11 -0
  69. package/dist/esm/providers.js.map +1 -1
  70. package/dist/esm/recorders/core/contextEngineering.js +155 -0
  71. package/dist/esm/recorders/core/contextEngineering.js.map +1 -0
  72. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js +6 -1
  73. package/dist/esm/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  74. package/dist/esm/security/PermissionPolicy.js +135 -0
  75. package/dist/esm/security/PermissionPolicy.js.map +1 -0
  76. package/dist/esm/security/index.js +38 -0
  77. package/dist/esm/security/index.js.map +1 -0
  78. package/dist/esm/tool-providers/gatedTools.js +52 -0
  79. package/dist/esm/tool-providers/gatedTools.js.map +1 -0
  80. package/dist/esm/tool-providers/index.js +43 -0
  81. package/dist/esm/tool-providers/index.js.map +1 -0
  82. package/dist/esm/tool-providers/skillScopedTools.js +63 -0
  83. package/dist/esm/tool-providers/skillScopedTools.js.map +1 -0
  84. package/dist/esm/tool-providers/staticTools.js +33 -0
  85. package/dist/esm/tool-providers/staticTools.js.map +1 -0
  86. package/dist/esm/tool-providers/types.js +53 -0
  87. package/dist/esm/tool-providers/types.js.map +1 -0
  88. package/dist/index.js +57 -12
  89. package/dist/index.js.map +1 -1
  90. package/dist/lib/injection-engine/SkillRegistry.js +185 -0
  91. package/dist/lib/injection-engine/SkillRegistry.js.map +1 -0
  92. package/dist/lib/injection-engine/factories/defineSkill.js +37 -1
  93. package/dist/lib/injection-engine/factories/defineSkill.js.map +1 -1
  94. package/dist/lib/injection-engine/index.js +14 -1
  95. package/dist/lib/injection-engine/index.js.map +1 -1
  96. package/dist/lib/injection-engine/skillTools.js +130 -0
  97. package/dist/lib/injection-engine/skillTools.js.map +1 -0
  98. package/dist/lib/injection-engine/types.js +8 -0
  99. package/dist/lib/injection-engine/types.js.map +1 -1
  100. package/dist/lib/lazyRequire.js +37 -0
  101. package/dist/lib/lazyRequire.js.map +1 -0
  102. package/dist/lib/mcp/mcpClient.js +4 -6
  103. package/dist/lib/mcp/mcpClient.js.map +1 -1
  104. package/dist/llm-providers.js +47 -0
  105. package/dist/llm-providers.js.map +1 -0
  106. package/dist/locales/index.js +149 -0
  107. package/dist/locales/index.js.map +1 -0
  108. package/dist/memory-providers.js +49 -0
  109. package/dist/memory-providers.js.map +1 -0
  110. package/dist/providers.js +11 -0
  111. package/dist/providers.js.map +1 -1
  112. package/dist/recorders/core/contextEngineering.js +161 -0
  113. package/dist/recorders/core/contextEngineering.js.map +1 -0
  114. package/dist/recorders/observability/commentary/commentaryTemplates.js +6 -1
  115. package/dist/recorders/observability/commentary/commentaryTemplates.js.map +1 -1
  116. package/dist/security/PermissionPolicy.js +139 -0
  117. package/dist/security/PermissionPolicy.js.map +1 -0
  118. package/dist/security/index.js +42 -0
  119. package/dist/security/index.js.map +1 -0
  120. package/dist/tool-providers/gatedTools.js +56 -0
  121. package/dist/tool-providers/gatedTools.js.map +1 -0
  122. package/dist/tool-providers/index.js +51 -0
  123. package/dist/tool-providers/index.js.map +1 -0
  124. package/dist/tool-providers/skillScopedTools.js +67 -0
  125. package/dist/tool-providers/skillScopedTools.js.map +1 -0
  126. package/dist/tool-providers/staticTools.js +37 -0
  127. package/dist/tool-providers/staticTools.js.map +1 -0
  128. package/dist/tool-providers/types.js +54 -0
  129. package/dist/tool-providers/types.js.map +1 -0
  130. package/dist/types/adapters/llm/AnthropicProvider.d.ts.map +1 -1
  131. package/dist/types/adapters/llm/BedrockProvider.d.ts.map +1 -1
  132. package/dist/types/adapters/llm/BrowserAnthropicProvider.d.ts.map +1 -1
  133. package/dist/types/adapters/llm/OpenAIProvider.d.ts.map +1 -1
  134. package/dist/types/adapters/memory/agentcore.d.ts +7 -1
  135. package/dist/types/adapters/memory/agentcore.d.ts.map +1 -1
  136. package/dist/types/adapters/memory/redis.d.ts +7 -1
  137. package/dist/types/adapters/memory/redis.d.ts.map +1 -1
  138. package/dist/types/core/Agent.d.ts +216 -2
  139. package/dist/types/core/Agent.d.ts.map +1 -1
  140. package/dist/types/core/flowchartAsTool.d.ts +161 -0
  141. package/dist/types/core/flowchartAsTool.d.ts.map +1 -0
  142. package/dist/types/core/outputSchema.d.ts +128 -0
  143. package/dist/types/core/outputSchema.d.ts.map +1 -0
  144. package/dist/types/core/slots/buildSystemPromptSlot.d.ts.map +1 -1
  145. package/dist/types/core/slots/buildToolsSlot.d.ts +10 -0
  146. package/dist/types/core/slots/buildToolsSlot.d.ts.map +1 -1
  147. package/dist/types/index.d.ts +8 -4
  148. package/dist/types/index.d.ts.map +1 -1
  149. package/dist/types/lib/injection-engine/SkillRegistry.d.ts +148 -0
  150. package/dist/types/lib/injection-engine/SkillRegistry.d.ts.map +1 -0
  151. package/dist/types/lib/injection-engine/factories/defineSkill.d.ts +99 -0
  152. package/dist/types/lib/injection-engine/factories/defineSkill.d.ts.map +1 -1
  153. package/dist/types/lib/injection-engine/index.d.ts +5 -2
  154. package/dist/types/lib/injection-engine/index.d.ts.map +1 -1
  155. package/dist/types/lib/injection-engine/skillTools.d.ts +73 -0
  156. package/dist/types/lib/injection-engine/skillTools.d.ts.map +1 -0
  157. package/dist/types/lib/injection-engine/types.d.ts +28 -0
  158. package/dist/types/lib/injection-engine/types.d.ts.map +1 -1
  159. package/dist/types/lib/lazyRequire.d.ts +30 -0
  160. package/dist/types/lib/lazyRequire.d.ts.map +1 -0
  161. package/dist/types/lib/mcp/mcpClient.d.ts.map +1 -1
  162. package/dist/types/llm-providers.d.ts +31 -0
  163. package/dist/types/llm-providers.d.ts.map +1 -0
  164. package/dist/types/locales/index.d.ts +133 -0
  165. package/dist/types/locales/index.d.ts.map +1 -0
  166. package/dist/types/memory-providers.d.ts +41 -0
  167. package/dist/types/memory-providers.d.ts.map +1 -0
  168. package/dist/types/providers.d.ts +11 -0
  169. package/dist/types/providers.d.ts.map +1 -1
  170. package/dist/types/recorders/core/contextEngineering.d.ts +137 -0
  171. package/dist/types/recorders/core/contextEngineering.d.ts.map +1 -0
  172. package/dist/types/recorders/observability/commentary/commentaryTemplates.d.ts.map +1 -1
  173. package/dist/types/security/PermissionPolicy.d.ts +125 -0
  174. package/dist/types/security/PermissionPolicy.d.ts.map +1 -0
  175. package/dist/types/security/index.d.ts +40 -0
  176. package/dist/types/security/index.d.ts.map +1 -0
  177. package/dist/types/tool-providers/gatedTools.d.ts +37 -0
  178. package/dist/types/tool-providers/gatedTools.d.ts.map +1 -0
  179. package/dist/types/tool-providers/index.d.ts +42 -0
  180. package/dist/types/tool-providers/index.d.ts.map +1 -0
  181. package/dist/types/tool-providers/skillScopedTools.d.ts +46 -0
  182. package/dist/types/tool-providers/skillScopedTools.d.ts.map +1 -0
  183. package/dist/types/tool-providers/staticTools.d.ts +22 -0
  184. package/dist/types/tool-providers/staticTools.d.ts.map +1 -0
  185. package/dist/types/tool-providers/types.d.ts +103 -0
  186. package/dist/types/tool-providers/types.d.ts.map +1 -0
  187. package/package.json +62 -9
  188. package/README.proposed.md +0 -258
  189. package/dist/instructions.js +0 -21
  190. package/dist/instructions.js.map +0 -1
  191. package/dist/lib/instructions/defineInstruction.js +0 -35
  192. package/dist/lib/instructions/defineInstruction.js.map +0 -1
  193. package/dist/lib/instructions/evaluator.js +0 -38
  194. package/dist/lib/instructions/evaluator.js.map +0 -1
  195. package/dist/lib/instructions/index.js +0 -48
  196. package/dist/lib/instructions/index.js.map +0 -1
  197. package/dist/lib/instructions/types.js +0 -22
  198. package/dist/lib/instructions/types.js.map +0 -1
  199. package/dist/memory/conversationHelpers.js +0 -39
  200. package/dist/memory/conversationHelpers.js.map +0 -1
  201. package/dist/types/instructions.d.ts +0 -5
  202. package/dist/types/instructions.d.ts.map +0 -1
  203. package/dist/types/lib/instructions/defineInstruction.d.ts +0 -22
  204. package/dist/types/lib/instructions/defineInstruction.d.ts.map +0 -1
  205. package/dist/types/lib/instructions/evaluator.d.ts +0 -11
  206. package/dist/types/lib/instructions/evaluator.d.ts.map +0 -1
  207. package/dist/types/lib/instructions/index.d.ts +0 -44
  208. package/dist/types/lib/instructions/index.d.ts.map +0 -1
  209. package/dist/types/lib/instructions/types.d.ts +0 -100
  210. package/dist/types/lib/instructions/types.d.ts.map +0 -1
  211. package/dist/types/memory/conversationHelpers.d.ts +0 -19
  212. package/dist/types/memory/conversationHelpers.d.ts.map +0 -1
@@ -17,6 +17,13 @@
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.AgentBuilder = exports.Agent = void 0;
19
19
  const footprintjs_1 = require("footprintjs");
20
+ // ArrayMergeMode lives on footprintjs's `advanced` subpath, not its
21
+ // main barrel. Used to set `arrayMerge: Replace` on subflow output
22
+ // mapping for the Tools slot — the slot's deduped tool list must
23
+ // REPLACE the parent's `dynamicToolSchemas` rather than concatenate
24
+ // with it (default behavior re-introduces duplicate tool names that
25
+ // LLM providers reject).
26
+ const advanced_1 = require("footprintjs/advanced");
20
27
  const pause_js_1 = require("./pause.js");
21
28
  const cost_js_1 = require("./cost.js");
22
29
  const conventions_js_1 = require("../conventions.js");
@@ -38,8 +45,10 @@ const buildSystemPromptSlot_js_1 = require("./slots/buildSystemPromptSlot.js");
38
45
  const buildMessagesSlot_js_1 = require("./slots/buildMessagesSlot.js");
39
46
  const buildToolsSlot_js_1 = require("./slots/buildToolsSlot.js");
40
47
  const buildInjectionEngineSubflow_js_1 = require("../lib/injection-engine/buildInjectionEngineSubflow.js");
48
+ const skillTools_js_1 = require("../lib/injection-engine/skillTools.js");
49
+ const defineInstruction_js_1 = require("../lib/injection-engine/factories/defineInstruction.js");
50
+ const outputSchema_js_1 = require("./outputSchema.js");
41
51
  const RunnerBase_js_1 = require("./RunnerBase.js");
42
- const tools_js_1 = require("./tools.js");
43
52
  class Agent extends RunnerBase_js_1.RunnerBase {
44
53
  name;
45
54
  id;
@@ -74,6 +83,21 @@ class Agent extends RunnerBase_js_1.RunnerBase {
74
83
  runId: 'pending',
75
84
  compositionPath: [],
76
85
  };
86
+ /**
87
+ * Reference to the most recent executor. Set on every `createExecutor()`
88
+ * call (i.e., every `run()` and `resume()`); read by `getLastSnapshot()`
89
+ * / `getLastNarrativeEntries()` so post-run UIs (Lens Trace tab,
90
+ * ExplainableShell) can pull execution state without intercepting the
91
+ * call. `undefined` until the first run.
92
+ */
93
+ lastExecutor;
94
+ /**
95
+ * Reference to the FlowChart compiled for the most recent run. Cached
96
+ * here rather than recomputed via `buildChart()` so `getSpec()` returns
97
+ * the SAME spec the executor traced — important when the spec is used
98
+ * to reconcile `getLastSnapshot()` for ExplainableShell.
99
+ */
100
+ lastFlowChart;
77
101
  /**
78
102
  * Memory subsystems registered via `.memory()`. Each definition mounts
79
103
  * its `read` subflow before the InjectionEngine on every turn; per-id
@@ -81,7 +105,24 @@ class Agent extends RunnerBase_js_1.RunnerBase {
81
105
  * collision-free.
82
106
  */
83
107
  memories;
84
- constructor(opts, systemPromptValue, registry, voice, injections = [], memories = []) {
108
+ /**
109
+ * Optional terminal contract. Set via the builder's `.outputSchema()`.
110
+ * When present, `agent.runTyped()` parses + validates the final
111
+ * answer against this parser. `agent.run()` keeps returning the
112
+ * raw string; consumers opt into typed mode explicitly.
113
+ */
114
+ outputSchemaParser;
115
+ /**
116
+ * Optional `ToolProvider` set via the builder's `.toolProvider()`.
117
+ * When present, the Tools slot subflow consults it per iteration
118
+ * (Block A5 follow-up) — the provider's tools land alongside any
119
+ * tools registered statically via `.tool()` / `.tools()`. The
120
+ * tool-call dispatcher also consults it for per-iteration execute
121
+ * lookup so dynamic chains (`gatedTools`, `skillScopedTools`)
122
+ * dispatch correctly when their visible-set changes mid-turn.
123
+ */
124
+ externalToolProvider;
125
+ constructor(opts, systemPromptValue, registry, voice, injections = [], memories = [], outputSchemaParser, toolProvider) {
85
126
  super();
86
127
  this.provider = opts.provider;
87
128
  this.name = opts.name ?? 'Agent';
@@ -94,6 +135,8 @@ class Agent extends RunnerBase_js_1.RunnerBase {
94
135
  this.registry = registry;
95
136
  this.injections = injections;
96
137
  this.memories = memories;
138
+ this.outputSchemaParser = outputSchemaParser;
139
+ this.externalToolProvider = toolProvider;
97
140
  // Eager validation: tool names must be unique across .tool() +
98
141
  // every Skill.inject.tools — the LLM dispatches by name. Runs in
99
142
  // constructor so `Agent.build()` throws immediately on collision,
@@ -118,6 +161,77 @@ class Agent extends RunnerBase_js_1.RunnerBase {
118
161
  toFlowChart() {
119
162
  return this.buildChart();
120
163
  }
164
+ /**
165
+ * The footprintjs `RuntimeSnapshot` from the most recent `run()` /
166
+ * `resume()`. Feeds Lens's Trace tab (ExplainableShell `runtimeSnapshot`
167
+ * prop) so consumers can scrub the execution timeline post-run without
168
+ * threading a recorder through the call site.
169
+ *
170
+ * Returns `undefined` before the first run completes. Returns the
171
+ * snapshot of the most recent run on every call after — including
172
+ * across multiple turns of the same Agent instance.
173
+ */
174
+ getLastSnapshot() {
175
+ return this.lastExecutor?.getSnapshot();
176
+ }
177
+ /**
178
+ * Structured narrative entries from the most recent run. Pairs with
179
+ * `getLastSnapshot()` for ExplainableShell's `narrativeEntries` prop.
180
+ * Empty array (not `undefined`) when no run has completed — matches
181
+ * the prop's expected shape so consumers can wire it directly without
182
+ * a defensive guard.
183
+ */
184
+ getLastNarrativeEntries() {
185
+ return this.lastExecutor?.getNarrativeEntries() ?? [];
186
+ }
187
+ /**
188
+ * The FlowChart compiled for the most recent run (or a freshly-built
189
+ * one if no run has happened yet). Feeds ExplainableShell's `spec`
190
+ * prop. Returning the cached chart matters: the spec must match what
191
+ * `getLastSnapshot()` traced, otherwise the Trace view's stage tree
192
+ * desyncs from the snapshot's runtime tree.
193
+ */
194
+ getSpec() {
195
+ return this.lastFlowChart ?? this.buildChart();
196
+ }
197
+ /**
198
+ * Parse + validate a raw agent answer against the agent's
199
+ * `outputSchema` parser. Throws `OutputSchemaError` on JSON parse
200
+ * or schema validation failure (the rawOutput is preserved on the
201
+ * error for triage). Throws a plain `Error` if the agent has no
202
+ * outputSchema set.
203
+ *
204
+ * Use this when you need to keep `agent.run()` returning the raw
205
+ * string for logging/observability and validate at a different
206
+ * layer; otherwise prefer `agent.runTyped()`.
207
+ */
208
+ parseOutput(raw) {
209
+ if (!this.outputSchemaParser) {
210
+ throw new Error(`Agent.parseOutput: this agent has no outputSchema. Use ` +
211
+ `Agent.create({...}).outputSchema(parser).build() to enable typed output.`);
212
+ }
213
+ return (0, outputSchema_js_1.applyOutputSchema)(raw, this.outputSchemaParser);
214
+ }
215
+ /**
216
+ * Run the agent and return the schema-validated typed output.
217
+ * Convenience over `parseOutput(await agent.run({...}))`.
218
+ *
219
+ * Throws `OutputSchemaError` on parse / validation failure.
220
+ * Throws if the agent has no outputSchema set or if the run
221
+ * pauses (use `run()` directly when pauses are expected).
222
+ */
223
+ async runTyped(input, options) {
224
+ if (!this.outputSchemaParser) {
225
+ throw new Error(`Agent.runTyped: this agent has no outputSchema. Use ` +
226
+ `Agent.create({...}).outputSchema(parser).build() to enable typed output.`);
227
+ }
228
+ const out = await this.run(input, options);
229
+ if (typeof out !== 'string') {
230
+ throw new Error('Agent.runTyped: run paused — typed mode does not support pauses. ' +
231
+ 'Use agent.run() + agent.parseOutput(...) after resume.');
232
+ }
233
+ return this.parseOutput(out);
234
+ }
121
235
  async run(input, options) {
122
236
  const executor = this.createExecutor();
123
237
  const result = await executor.run({
@@ -147,6 +261,12 @@ class Agent extends RunnerBase_js_1.RunnerBase {
147
261
  };
148
262
  const chart = this.buildChart();
149
263
  const executor = new footprintjs_1.FlowChartExecutor(chart);
264
+ // Enable structured narrative so `getLastNarrativeEntries()` can
265
+ // hand a populated array to consumer Trace views (ExplainableShell).
266
+ // Cheap when no consumer reads it; the recorder accumulates only.
267
+ executor.enableNarrative();
268
+ this.lastExecutor = executor;
269
+ this.lastFlowChart = chart;
150
270
  const dispatcher = this.getDispatcher();
151
271
  const getRunCtx = () => this.currentRunContext;
152
272
  executor.attachCombinedRecorder(new ContextRecorder_js_1.ContextRecorder({ dispatcher, getRunContext: getRunCtx }));
@@ -252,31 +372,86 @@ class Agent extends RunnerBase_js_1.RunnerBase {
252
372
  // ability to call the right tool. Throw early instead of subtly
253
373
  // shadowing.
254
374
  const skills = this.injections.filter((i) => i.flavor === 'skill');
375
+ // Collect skill tools, deduping by name when the SAME Tool reference
376
+ // is shared across skills. Different Tool implementations under the
377
+ // same name throws (already validated upstream by
378
+ // validateToolNameUniqueness) — we keep the runtime check as
379
+ // belt-and-suspenders.
380
+ //
381
+ // Block C runtime — `autoActivate: 'currentSkill'` semantics:
382
+ // When a skill's `defineSkill({ autoActivate: 'currentSkill' })`
383
+ // is set, its tools are EXCLUDED from the static registry. They
384
+ // flow into the LLM's tool list ONLY through `dynamicSchemas`
385
+ // (the buildToolsSlot path that reads activeInjections), which
386
+ // means they're visible ONLY on iterations after the skill is
387
+ // activated by `read_skill('id')`. Without this, the LLM sees
388
+ // every skill's tools on every iteration and the
389
+ // per-skill-narrowing autoActivate promised in `defineSkill`
390
+ // doesn't actually narrow anything. Skills WITHOUT autoActivate
391
+ // keep the v2.4 behavior (tools always visible) for back-compat.
255
392
  const skillToolEntries = [];
393
+ const sharedSkillTools = new Map();
256
394
  for (const skill of skills) {
395
+ const meta = skill.metadata;
396
+ const isAutoActivate = meta?.autoActivate === 'currentSkill';
257
397
  const toolsFromSkill = skill.inject.tools ?? [];
258
398
  for (const tool of toolsFromSkill) {
259
- skillToolEntries.push({ name: tool.schema.name, tool });
399
+ const name = tool.schema.name;
400
+ const existing = sharedSkillTools.get(name);
401
+ if (existing) {
402
+ if (existing !== tool) {
403
+ throw new Error(`Agent: tool name '${name}' is declared by multiple skills with different ` +
404
+ `Tool implementations. Skills MAY share the SAME Tool reference; they may ` +
405
+ `NOT register different functions under the same name.`);
406
+ }
407
+ continue; // dedupe — same reference already added
408
+ }
409
+ sharedSkillTools.set(name, tool);
410
+ // autoActivate skills: their tools come ONLY through
411
+ // dynamicSchemas (buildToolsSlot.ts pulls them from
412
+ // activeInjections.inject.tools when the skill is active).
413
+ // Don't pre-load them in the static registry.
414
+ if (isAutoActivate)
415
+ continue;
416
+ skillToolEntries.push({ name, tool });
260
417
  }
261
418
  }
262
- const readSkillEntries = skills.length > 0 ? [{ name: 'read_skill', tool: buildReadSkillTool(skills) }] : [];
419
+ // buildReadSkillTool returns undefined when skills is empty; the
420
+ // length check below short-circuits so the non-null assertion is safe.
421
+ const readSkillEntries = skills.length > 0 ? [{ name: 'read_skill', tool: (0, skillTools_js_1.buildReadSkillTool)(skills) }] : [];
263
422
  const augmentedRegistry = [
264
423
  ...registry,
265
424
  ...readSkillEntries,
266
425
  ...skillToolEntries,
267
426
  ];
268
- // Validate: tool names must be unique across registry + skills.
269
- // The LLM dispatches by name; collisions silently shadow.
427
+ // Final cross-source name-uniqueness check: static .tool() vs
428
+ // read_skill vs (deduped) skill tools. After the dedupe above this
429
+ // catches collisions BETWEEN sources (e.g., a static .tool('foo')
430
+ // colliding with a Skill's foo) which are real bugs.
270
431
  const seenNames = new Set();
271
432
  for (const entry of augmentedRegistry) {
272
433
  if (seenNames.has(entry.name)) {
273
434
  throw new Error(`Agent: duplicate tool name '${entry.name}'. Tool names must be unique ` +
274
- `across .tool() registrations and all Skills' inject.tools (the LLM ` +
275
- `dispatches by name; collisions break tool routing).`);
435
+ `across .tool() registrations and Skills' inject.tools (after deduping ` +
436
+ `same-reference shares across skills). The LLM dispatches by name; ` +
437
+ `collisions break tool routing.`);
276
438
  }
277
439
  seenNames.add(entry.name);
278
440
  }
279
441
  const registryByName = new Map(augmentedRegistry.map((e) => [e.name, e.tool]));
442
+ // Block C runtime — autoActivate skill tools live OUTSIDE the LLM-
443
+ // visible registry (so they don't pollute the per-iteration tool
444
+ // list before the skill activates), but they MUST still be findable
445
+ // by the dispatch handler — the LLM calls them by name once the
446
+ // skill is active, and dispatch looks up by name. Add them to the
447
+ // dispatch map so `lookupTool` resolves correctly. Using the Map
448
+ // backing the static registryByName means autoActivate tools share
449
+ // the same `.execute` wiring as normal tools — no special path.
450
+ for (const [name, tool] of sharedSkillTools.entries()) {
451
+ if (!registryByName.has(name)) {
452
+ registryByName.set(name, tool);
453
+ }
454
+ }
280
455
  const toolSchemas = augmentedRegistry.map((e) => e.tool.schema);
281
456
  const injectionEngineSubflow = (0, buildInjectionEngineSubflow_js_1.buildInjectionEngineSubflow)({
282
457
  injections: this.injections,
@@ -286,7 +461,10 @@ class Agent extends RunnerBase_js_1.RunnerBase {
286
461
  reason: 'Agent.system()',
287
462
  });
288
463
  const messagesSubflow = (0, buildMessagesSlot_js_1.buildMessagesSlot)();
289
- const toolsSubflow = (0, buildToolsSlot_js_1.buildToolsSlot)({ tools: toolSchemas });
464
+ const toolsSubflow = (0, buildToolsSlot_js_1.buildToolsSlot)({
465
+ tools: toolSchemas,
466
+ ...(this.externalToolProvider && { toolProvider: this.externalToolProvider }),
467
+ });
290
468
  const iterationStart = (scope) => {
291
469
  (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.agent.iteration_start', {
292
470
  turnIndex: 0,
@@ -295,19 +473,22 @@ class Agent extends RunnerBase_js_1.RunnerBase {
295
473
  };
296
474
  const callLLM = async (scope) => {
297
475
  const systemPromptInjections = scope.systemPromptInjections ?? [];
298
- const messagesInjections = scope.messagesInjections ?? [];
476
+ // `scope.messagesInjections` is read by ContextRecorder for
477
+ // observability; the LLM-wire path now reads scope.history
478
+ // directly (see below for rationale).
299
479
  const iteration = scope.iteration;
300
480
  const systemPrompt = systemPromptInjections
301
481
  .map((r) => r.rawContent ?? '')
302
482
  .filter((s) => s.length > 0)
303
483
  .join('\n\n');
304
- const messages = messagesInjections
305
- .map((r) => ({
306
- role: r.asRole ?? 'user',
307
- content: r.rawContent ?? r.contentSummary,
308
- ...(r.sourceId !== undefined && { toolCallId: r.sourceId }),
309
- }))
310
- .filter((m) => m.content.length > 0);
484
+ // Read the LLM message stream from `scope.history` directly.
485
+ // The `messagesInjections` projection is for observability
486
+ // (ContextRecorder, Lens) — it flattens InjectionRecords for
487
+ // event reporting and doesn't carry the full LLM-protocol
488
+ // shape (assistant `toolCalls[]`, etc.). For Anthropic's API
489
+ // contract we need the original LLMMessage with `toolCalls`
490
+ // intact so tool_use → tool_result correlation survives.
491
+ const messages = scope.history ?? [];
311
492
  (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.stream.llm_start', {
312
493
  iteration,
313
494
  provider: provider.name,
@@ -426,8 +607,32 @@ class Agent extends RunnerBase_js_1.RunnerBase {
426
607
  ...(toolCalls.length > 0 && { toolCalls }),
427
608
  });
428
609
  }
610
+ // Resolve a tool by name, consulting the external ToolProvider
611
+ // if one was wired via `.toolProvider()` and the static
612
+ // registry doesn't carry the tool. The provider sees the same
613
+ // ctx the Tools slot used, so dispatch + visibility stay
614
+ // consistent within the iteration.
615
+ const externalToolProvider = this.externalToolProvider;
616
+ const lookupTool = (toolName) => {
617
+ const fromRegistry = registryByName.get(toolName);
618
+ if (fromRegistry)
619
+ return fromRegistry;
620
+ if (!externalToolProvider)
621
+ return undefined;
622
+ const activatedIds = scope.activatedInjectionIds ?? [];
623
+ const identity = scope.runIdentity;
624
+ const ctx = {
625
+ iteration: scope.iteration,
626
+ ...(activatedIds.length > 0 && {
627
+ activeSkillId: activatedIds[activatedIds.length - 1],
628
+ }),
629
+ ...(identity && { identity }),
630
+ };
631
+ const visible = externalToolProvider.list(ctx);
632
+ return visible.find((t) => t.schema.name === toolName);
633
+ };
429
634
  for (const tc of toolCalls) {
430
- const tool = registryByName.get(tc.name);
635
+ const tool = lookupTool(tc.name);
431
636
  (0, typedEmit_js_1.typedEmit)(scope, 'agentfootprint.stream.tool_start', {
432
637
  toolName: tc.name,
433
638
  toolCallId: tc.id,
@@ -691,6 +896,11 @@ class Agent extends RunnerBase_js_1.RunnerBase {
691
896
  activeInjections: parent.activeInjections,
692
897
  }),
693
898
  outputMapper: (sf) => ({ systemPromptInjections: sf.systemPromptInjections }),
899
+ // See Tools-subflow comment below — same array-concat hazard.
900
+ // Without Replace, iter N+1's systemPromptInjections gets
901
+ // CONCATENATED with iter N's, multiplying the system prompt
902
+ // each iteration.
903
+ arrayMerge: advanced_1.ArrayMergeMode.Replace,
694
904
  })
695
905
  .addSubFlowChartNext(conventions_js_1.SUBFLOW_IDS.MESSAGES, messagesSubflow, 'Messages', {
696
906
  inputMapper: (parent) => ({
@@ -699,11 +909,26 @@ class Agent extends RunnerBase_js_1.RunnerBase {
699
909
  activeInjections: parent.activeInjections,
700
910
  }),
701
911
  outputMapper: (sf) => ({ messagesInjections: sf.messagesInjections }),
912
+ // Same array-concat hazard. messagesInjections is consumer-
913
+ // facing observability metadata (ContextRecorder, Lens) — must
914
+ // reflect THIS iteration's history, not be appended to last
915
+ // iteration's. CallLLM no longer reads this for the wire
916
+ // request (uses scope.history directly), so the LLM-protocol
917
+ // bug is fixed independently — but consumers of the
918
+ // messagesInjections stream still expect the per-iteration
919
+ // semantics.
920
+ arrayMerge: advanced_1.ArrayMergeMode.Replace,
702
921
  })
703
922
  .addSubFlowChartNext(conventions_js_1.SUBFLOW_IDS.TOOLS, toolsSubflow, 'Tools', {
704
923
  inputMapper: (parent) => ({
705
924
  iteration: parent.iteration,
706
925
  activeInjections: parent.activeInjections,
926
+ // The slot subflow reads these to build the per-iteration
927
+ // ToolDispatchContext when an external `.toolProvider()` is
928
+ // configured. Without them the provider sees activeSkillId
929
+ // = undefined every iteration, breaking skillScopedTools etc.
930
+ activatedInjectionIds: parent.activatedInjectionIds,
931
+ runIdentity: parent.runIdentity,
707
932
  }),
708
933
  outputMapper: (sf) => ({
709
934
  toolsInjections: sf.toolsInjections,
@@ -711,6 +936,16 @@ class Agent extends RunnerBase_js_1.RunnerBase {
711
936
  // back up so callLLM uses the right list for THIS iteration.
712
937
  dynamicToolSchemas: sf.toolSchemas,
713
938
  }),
939
+ // CRITICAL: footprintjs's default `applyOutputMapping`
940
+ // CONCATENATES arrays from subflow output with the parent's
941
+ // existing array values. Without `Replace`, the parent's
942
+ // `dynamicToolSchemas` (carrying the iter N value) gets
943
+ // concatenated with the slot's iter N+1 deduped list,
944
+ // re-introducing duplicate tool names that Anthropic's API
945
+ // rejects with "tools: Tool names must be unique." The slot's
946
+ // toolSchemas IS the authoritative list — replace, don't
947
+ // concatenate.
948
+ arrayMerge: advanced_1.ArrayMergeMode.Replace,
714
949
  })
715
950
  .addFunction('IterationStart', iterationStart, 'iteration-start', 'Iteration begin marker')
716
951
  .addFunction('CallLLM', callLLM, conventions_js_1.STAGE_IDS.CALL_LLM, 'LLM invocation')
@@ -736,7 +971,18 @@ class Agent extends RunnerBase_js_1.RunnerBase {
736
971
  })
737
972
  .setDefault('final')
738
973
  .end()
739
- .loopTo(conventions_js_1.SUBFLOW_IDS.MESSAGES);
974
+ // Dynamic ReAct: loop back to the InjectionEngine so EVERY iteration
975
+ // re-evaluates triggers (rule predicates, on-tool-return, llm-activated)
976
+ // against the freshest context (the just-appended tool result).
977
+ // Without this, the InjectionEngine runs ONCE per turn and:
978
+ // - on-tool-return predicates never fire on iter 2+
979
+ // - read_skill('X') activations are never picked up next iteration
980
+ // - autoActivate per-skill tool gating is structurally impossible
981
+ // - tools / system-prompt slots stay frozen at iter 1 content
982
+ // The v2.4 default of loopTo(MESSAGES) bypassed all four — quietly
983
+ // breaking the framework's "Dynamic ReAct" claim. v2.5 restores the
984
+ // v1 behavior that documents promise.
985
+ .loopTo(conventions_js_1.SUBFLOW_IDS.INJECTION_ENGINE);
740
986
  return builder.build();
741
987
  }
742
988
  }
@@ -751,6 +997,29 @@ class AgentBuilder {
751
997
  registry = [];
752
998
  injectionList = [];
753
999
  memoryList = [];
1000
+ /**
1001
+ * Optional terminal contract — see `outputSchema()`. Stored on the
1002
+ * builder, propagated to the Agent at `.build()` time.
1003
+ */
1004
+ outputSchemaParser;
1005
+ /**
1006
+ * Optional `ToolProvider` set via `.toolProvider()`. Propagated to
1007
+ * the Agent's Tools slot subflow + tool-call dispatcher; consulted
1008
+ * per iteration so dynamic chains (`gatedTools`, `skillScopedTools`)
1009
+ * react to current activation state.
1010
+ */
1011
+ toolProviderRef;
1012
+ /**
1013
+ * Optional override for `AgentOptions.maxIterations`. When set via
1014
+ * the `.maxIterations()` builder method, takes precedence over the
1015
+ * value passed to `Agent.create({ maxIterations })`.
1016
+ */
1017
+ maxIterationsOverride;
1018
+ /**
1019
+ * Recorders collected via `.recorder()`. Attached to the built Agent
1020
+ * before `build()` returns (each via `agent.attach(rec)`).
1021
+ */
1022
+ recorderList = [];
754
1023
  // Voice config — defaults until the consumer calls .appName() /
755
1024
  // .commentaryTemplates() / .thinkingTemplates(). Stored as plain
756
1025
  // dicts (Record<string, string>) so the builder doesn't depend on
@@ -785,6 +1054,76 @@ class AgentBuilder {
785
1054
  this.tool(t);
786
1055
  return this;
787
1056
  }
1057
+ /**
1058
+ * Wire a chainable `ToolProvider` (from `agentfootprint/tool-providers`)
1059
+ * as the agent's per-iteration tool source.
1060
+ *
1061
+ * The provider is consulted EVERY iteration via `provider.list(ctx)`
1062
+ * with `ctx = { iteration, activeSkillId, identity }`. Tools the
1063
+ * provider emits flow into the Tools slot alongside any static
1064
+ * tools registered via `.tool()` / `.tools()`. The tool-call
1065
+ * dispatcher also consults the provider so dynamic chains
1066
+ * (`gatedTools`, `skillScopedTools`) dispatch correctly when their
1067
+ * visible-set changes mid-turn.
1068
+ *
1069
+ * Throws if called more than once on the same builder (avoids
1070
+ * silent override surprises).
1071
+ *
1072
+ * @example Permission-gated baseline
1073
+ * import { gatedTools, staticTools } from 'agentfootprint/tool-providers';
1074
+ * import { PermissionPolicy } from 'agentfootprint/security';
1075
+ *
1076
+ * const policy = PermissionPolicy.fromRoles({
1077
+ * readonly: ['lookup', 'list_skills', 'read_skill'],
1078
+ * admin: ['lookup', 'list_skills', 'read_skill', 'delete'],
1079
+ * }, 'readonly');
1080
+ *
1081
+ * const provider = gatedTools(
1082
+ * staticTools(allTools),
1083
+ * (toolName) => policy.isAllowed(toolName),
1084
+ * );
1085
+ *
1086
+ * const agent = Agent.create({ provider: llm, model })
1087
+ * .system('You answer.')
1088
+ * .toolProvider(provider)
1089
+ * .build();
1090
+ */
1091
+ toolProvider(provider) {
1092
+ if (this.toolProviderRef) {
1093
+ throw new Error('AgentBuilder.toolProvider: already set. Each agent has at most one external ToolProvider.');
1094
+ }
1095
+ this.toolProviderRef = provider;
1096
+ return this;
1097
+ }
1098
+ /**
1099
+ * Override the ReAct iteration cap set via `Agent.create({
1100
+ * maxIterations })`. Convenience for builder-style code that prefers
1101
+ * fluent setters over constructor opts. Last call wins.
1102
+ *
1103
+ * Throws if `n` is not a positive integer or exceeds the hard cap
1104
+ * (`clampIterations`'s upper bound).
1105
+ */
1106
+ maxIterations(n) {
1107
+ if (!Number.isInteger(n) || n <= 0) {
1108
+ throw new Error(`AgentBuilder.maxIterations: expected a positive integer, got ${n}.`);
1109
+ }
1110
+ this.maxIterationsOverride = n;
1111
+ return this;
1112
+ }
1113
+ /**
1114
+ * Attach a footprintjs `CombinedRecorder` to the built Agent. Wired
1115
+ * via `agent.attach(rec)` immediately after construction, so the
1116
+ * recorder sees every event from the very first run.
1117
+ *
1118
+ * Equivalent to calling `agent.attach(rec)` post-build; the builder
1119
+ * method is a convenience for codebases that prefer fully-fluent
1120
+ * agent assembly. Multiple recorders are supported (each gets its
1121
+ * own `attach()` call).
1122
+ */
1123
+ recorder(rec) {
1124
+ this.recorderList.push(rec);
1125
+ return this;
1126
+ }
788
1127
  /**
789
1128
  * Set the agent's display name — substituted as `{{appName}}` in
790
1129
  * commentary + thinking templates. Same place to brand a tenant
@@ -845,6 +1184,22 @@ class AgentBuilder {
845
1184
  skill(injection) {
846
1185
  return this.injection(injection);
847
1186
  }
1187
+ /**
1188
+ * Bulk-register every Skill in a `SkillRegistry`. Use for shared
1189
+ * skill catalogs across multiple Agents — register skills once on
1190
+ * the registry; attach the same registry to every consumer Agent.
1191
+ *
1192
+ * @example
1193
+ * const registry = new SkillRegistry();
1194
+ * registry.register(billingSkill).register(refundSkill);
1195
+ * const supportAgent = Agent.create({ provider }).skills(registry).build();
1196
+ * const escalationAgent = Agent.create({ provider }).skills(registry).build();
1197
+ */
1198
+ skills(registry) {
1199
+ for (const skill of registry.list())
1200
+ this.injection(skill);
1201
+ return this;
1202
+ }
848
1203
  /**
849
1204
  * Register a Steering doc — always-on system-prompt rule.
850
1205
  * Use for invariant guidance: output format, persona, safety policies.
@@ -860,6 +1215,18 @@ class AgentBuilder {
860
1215
  instruction(injection) {
861
1216
  return this.injection(injection);
862
1217
  }
1218
+ /**
1219
+ * Bulk-register many instructions at once. Convenience for consumer
1220
+ * code that organizes its instruction set in a flat array (`const
1221
+ * instructions = [outputFormat, dataRouting, ...]`). Each element
1222
+ * is registered via `.instruction()` so duplicate-id checks still
1223
+ * fire per-entry.
1224
+ */
1225
+ instructions(injections) {
1226
+ for (const i of injections)
1227
+ this.instruction(i);
1228
+ return this;
1229
+ }
863
1230
  /**
864
1231
  * Register a Fact — developer-supplied data the LLM should see.
865
1232
  * User profile, env info, computed summary, current time, …
@@ -920,6 +1287,53 @@ class AgentBuilder {
920
1287
  rag(definition) {
921
1288
  return this.memory(definition);
922
1289
  }
1290
+ /**
1291
+ * Declarative terminal contract. The agent's final answer must be
1292
+ * JSON matching `parser`. Auto-injects a system-prompt instruction
1293
+ * telling the LLM the shape, and exposes `agent.runTyped()` /
1294
+ * `agent.parseOutput()` for parse + validate at the call site.
1295
+ *
1296
+ * The `parser` is duck-typed: any object with a `parse(unknown): T`
1297
+ * method works (Zod, Valibot, ArkType, hand-written). The optional
1298
+ * `description` field on the parser drives the auto-generated
1299
+ * instruction; consumers can also override via `opts.instruction`.
1300
+ *
1301
+ * Throws if called more than once on the same builder (avoids
1302
+ * silent override surprises).
1303
+ *
1304
+ * @param parser Validation strategy that throws on shape failure.
1305
+ * @param opts Optional `{ name, instruction }` to customize.
1306
+ *
1307
+ * @example
1308
+ * import { z } from 'zod';
1309
+ * const Output = z.object({
1310
+ * status: z.enum(['ok', 'err']),
1311
+ * items: z.array(z.string()),
1312
+ * }).describe('A status enum + an array of strings.');
1313
+ *
1314
+ * const agent = Agent.create({...})
1315
+ * .outputSchema(Output)
1316
+ * .build();
1317
+ *
1318
+ * const typed = await agent.runTyped({ message: '...' });
1319
+ * typed.status; // narrowed to 'ok' | 'err'
1320
+ */
1321
+ outputSchema(parser, opts) {
1322
+ if (this.outputSchemaParser) {
1323
+ throw new Error('AgentBuilder.outputSchema: already set. Each agent has at most one terminal contract.');
1324
+ }
1325
+ this.outputSchemaParser = parser;
1326
+ const instructionText = opts?.instruction ?? (0, outputSchema_js_1.buildDefaultInstruction)(parser);
1327
+ const id = opts?.name ?? 'output-schema';
1328
+ // Always-on system-slot instruction. Activates every iteration so
1329
+ // long runs keep the contract present (recency-first redundancy).
1330
+ this.injectionList.push((0, defineInstruction_js_1.defineInstruction)({
1331
+ id,
1332
+ activeWhen: () => true,
1333
+ prompt: instructionText,
1334
+ }));
1335
+ return this;
1336
+ }
923
1337
  build() {
924
1338
  // Resolve the voice config: bundled defaults + consumer overrides.
925
1339
  // Templates flow through the same barrel exports the rest of the
@@ -929,7 +1343,17 @@ class AgentBuilder {
929
1343
  commentaryTemplates: { ...commentaryTemplates_js_1.defaultCommentaryTemplates, ...this.commentaryOverrides },
930
1344
  thinkingTemplates: { ...thinkingTemplates_js_1.defaultThinkingTemplates, ...this.thinkingOverrides },
931
1345
  };
932
- return new Agent(this.opts, this.systemPromptValue, this.registry, voice, this.injectionList, this.memoryList);
1346
+ const opts = this.maxIterationsOverride !== undefined
1347
+ ? { ...this.opts, maxIterations: this.maxIterationsOverride }
1348
+ : this.opts;
1349
+ const agent = new Agent(opts, this.systemPromptValue, this.registry, voice, this.injectionList, this.memoryList, this.outputSchemaParser, this.toolProviderRef);
1350
+ // Attach builder-collected recorders so they receive events from
1351
+ // the very first run. Mirrors what consumers would do post-build
1352
+ // via `agent.attach(rec)`; the builder method is purely sugar.
1353
+ for (const rec of this.recorderList) {
1354
+ agent.attach(rec);
1355
+ }
1356
+ return agent;
933
1357
  }
934
1358
  }
935
1359
  exports.AgentBuilder = AgentBuilder;
@@ -962,72 +1386,62 @@ function clampIterations(n) {
962
1386
  * with consumer tools throw.
963
1387
  */
964
1388
  function validateToolNameUniqueness(registry, injections) {
965
- const seen = new Set();
966
- const claim = (name, sourceLabel) => {
967
- if (seen.has(name)) {
968
- throw new Error(`Agent: duplicate tool name '${name}' (${sourceLabel}). Tool names must be ` +
969
- `unique across .tool() registrations and all Skills' inject.tools — the LLM ` +
970
- `dispatches by name; collisions break tool routing.`);
1389
+ // Static registry: unique within itself. The Agent.tool() builder
1390
+ // method already throws on per-call duplicates; this is the
1391
+ // belt-and-suspenders check at build time.
1392
+ const staticNames = new Set();
1393
+ for (const entry of registry) {
1394
+ if (staticNames.has(entry.name)) {
1395
+ throw new Error(`Agent: duplicate tool name '${entry.name}' in .tool() registry. ` +
1396
+ `Tool names must be unique within the static registry.`);
971
1397
  }
972
- seen.add(name);
973
- };
974
- for (const entry of registry)
975
- claim(entry.name, '.tool()');
1398
+ staticNames.add(entry.name);
1399
+ }
1400
+ // `read_skill` is reserved when any Skill is registered. Collisions
1401
+ // with consumer-supplied tools break the auto-attach path.
976
1402
  const skills = injections.filter((i) => i.flavor === 'skill');
977
- if (skills.length > 0)
978
- claim('read_skill', 'auto-attached for Skills');
1403
+ if (skills.length > 0 && staticNames.has('read_skill')) {
1404
+ throw new Error(`Agent: tool name 'read_skill' is reserved when ≥1 Skill is registered. ` +
1405
+ `Rename your custom 'read_skill' tool or unregister it.`);
1406
+ }
1407
+ // Per-skill check: a skill's `inject.tools` array must be internally
1408
+ // unique (no duplicate names within the same skill — that's a
1409
+ // skill authoring bug). Across skills, sharing a Tool reference is
1410
+ // EXPECTED and supported — common tools (e.g., a `flogi_lookup`
1411
+ // used by multiple investigation skills) appear in multiple skills'
1412
+ // tool arrays. Only one skill is active at a time (or, when several
1413
+ // are active, deduped by name + reference at runtime). Sharing the
1414
+ // same Tool object across skills is the supported pattern; sharing
1415
+ // a Tool NAME with a DIFFERENT execute function is the actual bug —
1416
+ // we detect that here too.
1417
+ const seenByName = new Map();
979
1418
  for (const skill of skills) {
1419
+ const intraSkill = new Set();
980
1420
  for (const tool of skill.inject.tools ?? []) {
981
- claim(tool.schema.name, `from Skill '${skill.id}'`);
1421
+ const name = tool.schema.name;
1422
+ if (intraSkill.has(name)) {
1423
+ throw new Error(`Agent: skill '${skill.id}' lists tool '${name}' more than once in its ` +
1424
+ `inject.tools array. Each skill's tools must be unique within itself.`);
1425
+ }
1426
+ intraSkill.add(name);
1427
+ // Skill tools collide with the static .tool() registry → ambiguous dispatch
1428
+ if (staticNames.has(name)) {
1429
+ throw new Error(`Agent: skill '${skill.id}' tool '${name}' collides with the static .tool() ` +
1430
+ `registry. Either rename the skill's tool or remove the static registration.`);
1431
+ }
1432
+ // Same name across skills with DIFFERENT Tool objects = ambiguous when
1433
+ // both skills active. Same name + SAME Tool reference = supported sharing.
1434
+ const prior = seenByName.get(name);
1435
+ if (prior && prior !== tool) {
1436
+ throw new Error(`Agent: tool name '${name}' is declared by multiple skills with different ` +
1437
+ `Tool implementations. Skills MAY share the SAME Tool reference across ` +
1438
+ `their inject.tools arrays (deduped at dispatch); they may NOT register ` +
1439
+ `different functions under the same name (ambiguous dispatch).`);
1440
+ }
1441
+ seenByName.set(name, tool);
982
1442
  }
983
1443
  }
984
1444
  }
985
- /**
986
- * Build the auto-attached `read_skill` tool from a list of Skill
987
- * Injections. The LLM picks WHICH skill via the `id` argument.
988
- *
989
- * Tool execute() does the bookkeeping: appends the requested skill id
990
- * to `scope.activatedInjectionIds`. The next iteration's
991
- * InjectionEngine matches Skills with `trigger.kind: 'llm-activated'`
992
- * by id and includes them in the active set; slot subflows then
993
- * inject the body + tools.
994
- *
995
- * The tool's description lists each Skill's `id` + `description` so
996
- * the LLM can choose meaningfully.
997
- */
998
- function buildReadSkillTool(skills) {
999
- const skillIds = skills.map((s) => s.id);
1000
- const skillCatalog = skills
1001
- .map((s) => ` - ${s.id}: ${s.description ?? '(no description)'}`)
1002
- .join('\n');
1003
- return (0, tools_js_1.defineTool)({
1004
- name: 'read_skill',
1005
- description: `Activate a skill for the next iteration. Available skills:\n${skillCatalog}\n\n` +
1006
- `Pass the skill's id. The skill's body becomes part of the system prompt and any ` +
1007
- `gated tools become available on the next call.`,
1008
- inputSchema: {
1009
- type: 'object',
1010
- properties: {
1011
- id: {
1012
- type: 'string',
1013
- enum: skillIds,
1014
- description: 'The skill id to activate.',
1015
- },
1016
- },
1017
- required: ['id'],
1018
- },
1019
- execute: ({ id }) => {
1020
- // Bookkeeping is handled by the Agent's tool-calls subflow,
1021
- // which inspects `read_skill` returns and updates
1022
- // `scope.activatedInjectionIds` before the next iteration.
1023
- // The tool itself returns a confirmation string for the LLM.
1024
- if (!skillIds.includes(id)) {
1025
- return `Unknown skill '${id}'. Available: ${skillIds.join(', ')}`;
1026
- }
1027
- return `Skill '${id}' activated for the next iteration.`;
1028
- },
1029
- });
1030
- }
1031
1445
  /**
1032
1446
  * JSON.stringify with circular-ref protection. Tool results are untrusted —
1033
1447
  * a hostile/buggy tool returning a cyclic object must not crash the run.