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