agentic-ai-framework 1.0.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 +626 -0
- package/index.js +84 -0
- package/package.json +38 -0
- package/src/agent/Agent.js +278 -0
- package/src/agent/AgentConfig.js +88 -0
- package/src/agent/AgentRunner.js +256 -0
- package/src/llm/BaseLLMProvider.js +78 -0
- package/src/llm/LLMRouter.js +80 -0
- package/src/llm/providers/ClaudeProvider.js +307 -0
- package/src/llm/providers/GrokProvider.js +208 -0
- package/src/llm/providers/OpenAIProvider.js +194 -0
- package/src/memory/FileStore.js +102 -0
- package/src/memory/MemoryManager.js +55 -0
- package/src/memory/SessionMemory.js +124 -0
- package/src/prompt/PromptBuilder.js +95 -0
- package/src/prompt/PromptTemplate.js +58 -0
- package/src/team/AgentTeam.js +308 -0
- package/src/team/TeamResult.js +60 -0
- package/src/tool/Tool.js +138 -0
- package/src/tool/ToolRegistry.js +81 -0
- package/src/utils/errors.js +46 -0
- package/src/utils/logger.js +33 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { Tool } from '../tool/Tool.js';
|
|
2
|
+
import { TeamResult } from './TeamResult.js';
|
|
3
|
+
import { createLogger } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Coordinates multiple Agent instances in a team.
|
|
7
|
+
*
|
|
8
|
+
* Two modes:
|
|
9
|
+
*
|
|
10
|
+
* 'router' (default) — The coordinator agent has all member agents registered
|
|
11
|
+
* as tools. It receives the user input and decides which member(s) to delegate
|
|
12
|
+
* to. The coordinator synthesizes member results into a final answer.
|
|
13
|
+
* This maps directly to the existing MasterAgent pattern.
|
|
14
|
+
*
|
|
15
|
+
* 'parallel' — All member agents are run simultaneously on the same user input
|
|
16
|
+
* via Promise.all(). The coordinator receives all results and synthesizes them.
|
|
17
|
+
* Useful when a question needs multiple specialists (e.g., SQL data + RAG docs).
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* const team = new AgentTeam({ coordinator, members: [sqlAgent, ragAgent], mode: 'router' });
|
|
21
|
+
* const result = await team.run('How many tickets last month?');
|
|
22
|
+
* console.log(result.final);
|
|
23
|
+
*
|
|
24
|
+
* Concurrency note:
|
|
25
|
+
* Same as Agent — create a fresh AgentTeam per request (cheap, no shared state).
|
|
26
|
+
*/
|
|
27
|
+
export class AgentTeam {
|
|
28
|
+
/**
|
|
29
|
+
* @param {Object} options
|
|
30
|
+
* @param {import('../agent/Agent.js').Agent} options.coordinator - Orchestrating agent
|
|
31
|
+
* @param {import('../agent/Agent.js').Agent[]} [options.members=[]] - Specialist agents
|
|
32
|
+
* @param {'router' | 'parallel'} [options.mode='router']
|
|
33
|
+
*/
|
|
34
|
+
constructor({ coordinator, members = [], mode = 'router' }) {
|
|
35
|
+
if (!coordinator) throw new Error('AgentTeam: coordinator is required');
|
|
36
|
+
if (!['router', 'parallel'].includes(mode)) {
|
|
37
|
+
throw new Error(`AgentTeam: mode must be 'router' or 'parallel', got "${mode}"`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this._coordinator = coordinator;
|
|
41
|
+
this._members = new Map(); // agentName → Agent
|
|
42
|
+
this._mode = mode;
|
|
43
|
+
this._log = createLogger('AgentTeam', { coordinator: coordinator.name, mode });
|
|
44
|
+
|
|
45
|
+
for (const member of members) {
|
|
46
|
+
this.addMember(member);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Member management ─────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Add a member agent to the team.
|
|
54
|
+
* In router mode, also registers the member as a tool on the coordinator.
|
|
55
|
+
* @param {import('../agent/Agent.js').Agent} agent
|
|
56
|
+
*/
|
|
57
|
+
addMember(agent) {
|
|
58
|
+
this._members.set(agent.name, agent);
|
|
59
|
+
if (this._mode === 'router') {
|
|
60
|
+
this._registerMemberTool(agent);
|
|
61
|
+
}
|
|
62
|
+
this._log.debug({ member: agent.name }, 'member added');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Remove a member agent from the team.
|
|
67
|
+
* @param {string} agentName
|
|
68
|
+
*/
|
|
69
|
+
removeMember(agentName) {
|
|
70
|
+
this._members.delete(agentName);
|
|
71
|
+
if (this._mode === 'router') {
|
|
72
|
+
this._coordinator.getToolRegistry().unregister(`delegate_to_${agentName}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get all member names.
|
|
78
|
+
* @returns {string[]}
|
|
79
|
+
*/
|
|
80
|
+
getMembers() {
|
|
81
|
+
return [...this._members.keys()];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Tool registration (router mode) ───────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
_registerMembersAsTools() {
|
|
87
|
+
for (const agent of this._members.values()) {
|
|
88
|
+
this._registerMemberTool(agent);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_registerMemberTool(agent) {
|
|
93
|
+
const toolName = `delegate_to_${agent.name}`;
|
|
94
|
+
|
|
95
|
+
// Don't register twice
|
|
96
|
+
if (this._coordinator.getToolRegistry().has(toolName)) {
|
|
97
|
+
this._coordinator.getToolRegistry().unregister(toolName);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this._coordinator.getToolRegistry().registerOrReplace(new Tool({
|
|
101
|
+
name: toolName,
|
|
102
|
+
description: agent.description
|
|
103
|
+
? `Delegate to ${agent.name}: ${agent.description}`
|
|
104
|
+
: `Delegate this question to the ${agent.name} specialist agent`,
|
|
105
|
+
parameters: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
question: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
description: 'The question or task to delegate to this agent',
|
|
111
|
+
},
|
|
112
|
+
context: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
description: 'Optional additional context to include',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
required: ['question'],
|
|
118
|
+
},
|
|
119
|
+
handler: async ({ question, context }) => {
|
|
120
|
+
const input = context ? `${question}\n\nAdditional context: ${context}` : question;
|
|
121
|
+
const result = await agent.run(input, { appendToHistory: false });
|
|
122
|
+
if (!result.success) {
|
|
123
|
+
return JSON.stringify({ _delegateError: true, error: result.error, agent: agent.name });
|
|
124
|
+
}
|
|
125
|
+
return result.text || result.content || 'No response';
|
|
126
|
+
},
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Main execution ────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Run the team on a user input.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} userInput
|
|
136
|
+
* @param {Object} [opts={}]
|
|
137
|
+
* @param {Record<string, string>} [opts.templateVars={}] - Passed to coordinator
|
|
138
|
+
* @param {boolean} [opts.appendToHistory=false] - Whether to update coordinator history
|
|
139
|
+
* @returns {Promise<TeamResult>}
|
|
140
|
+
*/
|
|
141
|
+
async run(userInput, opts = {}) {
|
|
142
|
+
const { templateVars = {}, appendToHistory = false } = opts;
|
|
143
|
+
|
|
144
|
+
this._log.info({ mode: this._mode, input: userInput.slice(0, 100) }, 'team.run()');
|
|
145
|
+
|
|
146
|
+
if (this._mode === 'router') {
|
|
147
|
+
return this._runRouter(userInput, { templateVars, appendToHistory });
|
|
148
|
+
} else {
|
|
149
|
+
return this._runParallel(userInput, { templateVars, appendToHistory });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Router mode ───────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Router mode: coordinator decides which member(s) to call via tool-calling.
|
|
157
|
+
* Members are already registered as tools — this is just a normal agent.run().
|
|
158
|
+
*/
|
|
159
|
+
async _runRouter(userInput, { templateVars, appendToHistory }) {
|
|
160
|
+
try {
|
|
161
|
+
// Inject member list into coordinator's context for awareness
|
|
162
|
+
const memberList = [...this._members.values()]
|
|
163
|
+
.map(a => `- ${a.name}: ${a.description || 'specialist agent'}`)
|
|
164
|
+
.join('\n');
|
|
165
|
+
|
|
166
|
+
const coordinatorResult = await this._coordinator.run(userInput, {
|
|
167
|
+
templateVars: { ...templateVars, teamMembers: memberList },
|
|
168
|
+
appendToHistory,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (!coordinatorResult.success) {
|
|
172
|
+
return TeamResult.failure(coordinatorResult.error, 'router');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Extract per-member results from tool call history
|
|
176
|
+
const memberResults = this._extractMemberResultsFromHistory(coordinatorResult.toolCallHistory);
|
|
177
|
+
|
|
178
|
+
return new TeamResult({
|
|
179
|
+
success: true,
|
|
180
|
+
final: coordinatorResult.text || coordinatorResult.content || '',
|
|
181
|
+
memberResults,
|
|
182
|
+
coordinatorResult,
|
|
183
|
+
mode: 'router',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
} catch (err) {
|
|
187
|
+
this._log.error({ err: err.message }, 'router mode failed');
|
|
188
|
+
return TeamResult.failure(err.message, 'router');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Extract member results keyed by agent name from the coordinator's tool call history.
|
|
194
|
+
*/
|
|
195
|
+
_extractMemberResultsFromHistory(toolCallHistory = []) {
|
|
196
|
+
const memberResults = {};
|
|
197
|
+
for (const call of toolCallHistory) {
|
|
198
|
+
const match = call.name.match(/^delegate_to_(.+)$/);
|
|
199
|
+
if (match) {
|
|
200
|
+
const agentName = match[1];
|
|
201
|
+
// Detect structured error marker from the handler
|
|
202
|
+
let isError = false;
|
|
203
|
+
let resultText = typeof call.result === 'string' ? call.result : String(call.result ?? '');
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse(resultText);
|
|
206
|
+
if (parsed?._delegateError) {
|
|
207
|
+
isError = true;
|
|
208
|
+
resultText = parsed.error || resultText;
|
|
209
|
+
}
|
|
210
|
+
} catch { /* not JSON — treat as success text */ }
|
|
211
|
+
memberResults[agentName] = {
|
|
212
|
+
success: !isError,
|
|
213
|
+
text: resultText,
|
|
214
|
+
content: resultText,
|
|
215
|
+
toolCallHistory: [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return memberResults;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Parallel mode ─────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Parallel mode: all members run simultaneously, coordinator synthesizes.
|
|
226
|
+
*/
|
|
227
|
+
async _runParallel(userInput, { templateVars, appendToHistory }) {
|
|
228
|
+
const members = [...this._members.values()];
|
|
229
|
+
|
|
230
|
+
if (members.length === 0) {
|
|
231
|
+
return TeamResult.failure('AgentTeam: no members registered', 'parallel');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Run all members in parallel
|
|
235
|
+
const memberEntries = await Promise.all(
|
|
236
|
+
members.map(async agent => {
|
|
237
|
+
try {
|
|
238
|
+
const result = await agent.run(userInput, { appendToHistory: false });
|
|
239
|
+
return [agent.name, result];
|
|
240
|
+
} catch (err) {
|
|
241
|
+
return [agent.name, {
|
|
242
|
+
success: false,
|
|
243
|
+
error: err.message,
|
|
244
|
+
text: `Error: ${err.message}`,
|
|
245
|
+
toolCallHistory: [],
|
|
246
|
+
iterations: 0,
|
|
247
|
+
}];
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const memberResults = Object.fromEntries(memberEntries);
|
|
253
|
+
|
|
254
|
+
// Build a synthesis prompt for the coordinator
|
|
255
|
+
const memberSummaries = memberEntries.map(([name, result]) => {
|
|
256
|
+
return `### ${name}\n${result.success ? result.text : `Error: ${result.error}`}`;
|
|
257
|
+
}).join('\n\n');
|
|
258
|
+
|
|
259
|
+
const synthesisInput = `The following specialist agents have answered the user's question. Synthesize their responses into a single, comprehensive answer.
|
|
260
|
+
|
|
261
|
+
User question: ${userInput}
|
|
262
|
+
|
|
263
|
+
Specialist responses:
|
|
264
|
+
${memberSummaries}
|
|
265
|
+
|
|
266
|
+
Provide a unified answer that incorporates the most relevant information from each specialist.`;
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const coordinatorResult = await this._coordinator.run(synthesisInput, {
|
|
270
|
+
templateVars,
|
|
271
|
+
appendToHistory,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return new TeamResult({
|
|
275
|
+
success: coordinatorResult.success,
|
|
276
|
+
final: coordinatorResult.text || coordinatorResult.content || '',
|
|
277
|
+
memberResults,
|
|
278
|
+
coordinatorResult,
|
|
279
|
+
mode: 'parallel',
|
|
280
|
+
error: coordinatorResult.success ? undefined : coordinatorResult.error,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
} catch (err) {
|
|
284
|
+
this._log.error({ err: err.message }, 'parallel synthesis failed');
|
|
285
|
+
// Return partial success with member results even if coordinator failed
|
|
286
|
+
return new TeamResult({
|
|
287
|
+
success: false,
|
|
288
|
+
final: '',
|
|
289
|
+
memberResults,
|
|
290
|
+
coordinatorResult: null,
|
|
291
|
+
mode: 'parallel',
|
|
292
|
+
error: `Coordinator synthesis failed: ${err.message}`,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get team info.
|
|
299
|
+
* @returns {Object}
|
|
300
|
+
*/
|
|
301
|
+
getInfo() {
|
|
302
|
+
return {
|
|
303
|
+
coordinator: this._coordinator.getInfo(),
|
|
304
|
+
members: [...this._members.values()].map(a => a.getInfo()),
|
|
305
|
+
mode: this._mode,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured result returned by AgentTeam.run().
|
|
3
|
+
*
|
|
4
|
+
* @typedef {Object} TeamResult
|
|
5
|
+
* @property {boolean} success
|
|
6
|
+
* @property {string} final - Final synthesized answer from coordinator
|
|
7
|
+
* @property {Object} memberResults - Map of agentName → AgentResult for each member
|
|
8
|
+
* @property {import('../agent/AgentRunner.js').AgentResult} coordinatorResult - Coordinator's result
|
|
9
|
+
* @property {string} mode - 'router' | 'parallel'
|
|
10
|
+
* @property {string} [error] - Present when success=false
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class TeamResult {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} data
|
|
16
|
+
* @param {boolean} data.success
|
|
17
|
+
* @param {string} data.final
|
|
18
|
+
* @param {Record<string, Object>} data.memberResults
|
|
19
|
+
* @param {Object} data.coordinatorResult
|
|
20
|
+
* @param {string} data.mode
|
|
21
|
+
* @param {string} [data.error]
|
|
22
|
+
*/
|
|
23
|
+
constructor({ success, final, memberResults, coordinatorResult, mode, error }) {
|
|
24
|
+
this.success = success;
|
|
25
|
+
this.final = final ?? '';
|
|
26
|
+
this.memberResults = memberResults ?? {};
|
|
27
|
+
this.coordinatorResult = coordinatorResult ?? null;
|
|
28
|
+
this.mode = mode;
|
|
29
|
+
this.error = error ?? null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the result for a specific team member.
|
|
34
|
+
* @param {string} agentName
|
|
35
|
+
* @returns {Object | null}
|
|
36
|
+
*/
|
|
37
|
+
getMemberResult(agentName) {
|
|
38
|
+
return this.memberResults[agentName] ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* List the names of members that successfully returned results.
|
|
43
|
+
* @returns {string[]}
|
|
44
|
+
*/
|
|
45
|
+
getSuccessfulMembers() {
|
|
46
|
+
return Object.entries(this.memberResults)
|
|
47
|
+
.filter(([, result]) => result.success)
|
|
48
|
+
.map(([name]) => name);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a failure TeamResult.
|
|
53
|
+
* @param {string} error
|
|
54
|
+
* @param {string} mode
|
|
55
|
+
* @returns {TeamResult}
|
|
56
|
+
*/
|
|
57
|
+
static failure(error, mode) {
|
|
58
|
+
return new TeamResult({ success: false, final: '', memberResults: {}, coordinatorResult: null, mode, error });
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/tool/Tool.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ToolError } from '../utils/errors.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a tool definition (name, description, parameters) and its handler.
|
|
6
|
+
*
|
|
7
|
+
* The parameters field accepts either:
|
|
8
|
+
* - A plain JSON Schema object: { type: 'object', properties: {...}, required: [...] }
|
|
9
|
+
* - A Zod schema (z.object(...)): automatically converted to JSON Schema for the LLM
|
|
10
|
+
*
|
|
11
|
+
* Handler contract:
|
|
12
|
+
* async (args: Record<string, any>) => string | object
|
|
13
|
+
* - Receives the parsed arguments from the LLM
|
|
14
|
+
* - Returns a string or any JSON-serializable value
|
|
15
|
+
* - Throwing an error causes AgentRunner to report the error to the LLM as the tool result
|
|
16
|
+
*/
|
|
17
|
+
export class Tool {
|
|
18
|
+
/**
|
|
19
|
+
* @param {Object} definition
|
|
20
|
+
* @param {string} definition.name - Unique tool name (snake_case recommended)
|
|
21
|
+
* @param {string} definition.description - Description shown to the LLM
|
|
22
|
+
* @param {Object | import('zod').ZodObject<any>} [definition.parameters] - Parameter schema
|
|
23
|
+
* @param {Function} definition.handler - async (args) => string | object
|
|
24
|
+
*/
|
|
25
|
+
constructor({ name, description, parameters, handler }) {
|
|
26
|
+
if (!name || typeof name !== 'string') {
|
|
27
|
+
throw new ToolError('Tool: name must be a non-empty string');
|
|
28
|
+
}
|
|
29
|
+
if (typeof handler !== 'function') {
|
|
30
|
+
throw new ToolError(`Tool "${name}": handler must be a function`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.name = name;
|
|
34
|
+
this.description = description ?? '';
|
|
35
|
+
this._handler = handler;
|
|
36
|
+
|
|
37
|
+
// Normalize parameters to plain JSON Schema
|
|
38
|
+
if (parameters && typeof parameters._def !== 'undefined') {
|
|
39
|
+
// Zod schema detected — convert to JSON Schema
|
|
40
|
+
this.parameters = zodToJsonSchema(parameters);
|
|
41
|
+
} else {
|
|
42
|
+
this.parameters = parameters ?? { type: 'object', properties: {} };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns the tool definition object sent to the LLM (without the handler).
|
|
48
|
+
* @returns {{ name: string, description: string, parameters: Object }}
|
|
49
|
+
*/
|
|
50
|
+
toDefinition() {
|
|
51
|
+
return {
|
|
52
|
+
name: this.name,
|
|
53
|
+
description: this.description,
|
|
54
|
+
parameters: this.parameters,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Execute the tool with the given arguments.
|
|
60
|
+
* @param {Record<string, any>} args
|
|
61
|
+
* @returns {Promise<string | object>}
|
|
62
|
+
*/
|
|
63
|
+
async execute(args) {
|
|
64
|
+
try {
|
|
65
|
+
return await this._handler(args);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
throw new ToolError(`Tool "${this.name}" execution failed: ${err.message}`, {
|
|
68
|
+
cause: err,
|
|
69
|
+
toolName: this.name,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Minimal Zod → JSON Schema conversion for common types.
|
|
77
|
+
* Handles z.object(), z.string(), z.number(), z.boolean(), z.array(), z.enum().
|
|
78
|
+
* For complex schemas, pass a plain JSON Schema object instead.
|
|
79
|
+
*
|
|
80
|
+
* @param {import('zod').ZodTypeAny} zodSchema
|
|
81
|
+
* @returns {Object} JSON Schema
|
|
82
|
+
*/
|
|
83
|
+
function zodToJsonSchema(zodSchema) {
|
|
84
|
+
const def = zodSchema._def;
|
|
85
|
+
|
|
86
|
+
switch (def.typeName) {
|
|
87
|
+
case z.ZodFirstPartyTypeKind.ZodObject: {
|
|
88
|
+
const properties = {};
|
|
89
|
+
const required = [];
|
|
90
|
+
for (const [key, value] of Object.entries(def.shape())) {
|
|
91
|
+
properties[key] = zodToJsonSchema(value);
|
|
92
|
+
if (!(value._def.typeName === z.ZodFirstPartyTypeKind.ZodOptional)) {
|
|
93
|
+
required.push(key);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const schema = { type: 'object', properties };
|
|
97
|
+
if (required.length > 0) schema.required = required;
|
|
98
|
+
if (def.description) schema.description = def.description;
|
|
99
|
+
return schema;
|
|
100
|
+
}
|
|
101
|
+
case z.ZodFirstPartyTypeKind.ZodOptional:
|
|
102
|
+
return zodToJsonSchema(def.innerType);
|
|
103
|
+
case z.ZodFirstPartyTypeKind.ZodString: {
|
|
104
|
+
const s = { type: 'string' };
|
|
105
|
+
if (def.description) s.description = def.description;
|
|
106
|
+
return s;
|
|
107
|
+
}
|
|
108
|
+
case z.ZodFirstPartyTypeKind.ZodNumber: {
|
|
109
|
+
const s = { type: 'number' };
|
|
110
|
+
if (def.description) s.description = def.description;
|
|
111
|
+
return s;
|
|
112
|
+
}
|
|
113
|
+
case z.ZodFirstPartyTypeKind.ZodBoolean: {
|
|
114
|
+
const s = { type: 'boolean' };
|
|
115
|
+
if (def.description) s.description = def.description;
|
|
116
|
+
return s;
|
|
117
|
+
}
|
|
118
|
+
case z.ZodFirstPartyTypeKind.ZodArray: {
|
|
119
|
+
const s = { type: 'array', items: zodToJsonSchema(def.type) };
|
|
120
|
+
if (def.description) s.description = def.description;
|
|
121
|
+
return s;
|
|
122
|
+
}
|
|
123
|
+
case z.ZodFirstPartyTypeKind.ZodEnum: {
|
|
124
|
+
const s = { type: 'string', enum: def.values };
|
|
125
|
+
if (def.description) s.description = def.description;
|
|
126
|
+
return s;
|
|
127
|
+
}
|
|
128
|
+
case z.ZodFirstPartyTypeKind.ZodNullable:
|
|
129
|
+
return zodToJsonSchema(def.innerType);
|
|
130
|
+
case z.ZodFirstPartyTypeKind.ZodDefault:
|
|
131
|
+
return zodToJsonSchema(def.innerType);
|
|
132
|
+
default:
|
|
133
|
+
throw new ToolError(
|
|
134
|
+
`zodToJsonSchema: unsupported Zod type "${def.typeName}". ` +
|
|
135
|
+
'Use a plain JSON Schema object for complex types.'
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ToolError } from '../utils/errors.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages a collection of Tool instances.
|
|
5
|
+
* Provides registration, lookup, definition export, and execution.
|
|
6
|
+
*/
|
|
7
|
+
export class ToolRegistry {
|
|
8
|
+
constructor() {
|
|
9
|
+
this._tools = new Map(); // name → Tool
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Register a tool. Throws if a tool with the same name already exists.
|
|
14
|
+
* @param {import('./Tool.js').Tool} tool
|
|
15
|
+
*/
|
|
16
|
+
register(tool) {
|
|
17
|
+
if (this._tools.has(tool.name)) {
|
|
18
|
+
throw new ToolError(`Tool "${tool.name}" is already registered`);
|
|
19
|
+
}
|
|
20
|
+
this._tools.set(tool.name, tool);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Register or replace a tool by name.
|
|
25
|
+
* @param {import('./Tool.js').Tool} tool
|
|
26
|
+
*/
|
|
27
|
+
registerOrReplace(tool) {
|
|
28
|
+
this._tools.set(tool.name, tool);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Remove a tool by name.
|
|
33
|
+
* @param {string} name
|
|
34
|
+
*/
|
|
35
|
+
unregister(name) {
|
|
36
|
+
this._tools.delete(name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get tool definitions to send to the LLM (no handlers).
|
|
41
|
+
* Returns an empty array if no tools are registered.
|
|
42
|
+
* @returns {Array<{ name: string, description: string, parameters: Object }>}
|
|
43
|
+
*/
|
|
44
|
+
getDefinitions() {
|
|
45
|
+
return [...this._tools.values()].map(t => t.toDefinition());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* List registered tool names.
|
|
50
|
+
* @returns {string[]}
|
|
51
|
+
*/
|
|
52
|
+
listNames() {
|
|
53
|
+
return [...this._tools.keys()];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a tool is registered.
|
|
58
|
+
* @param {string} name
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
has(name) {
|
|
62
|
+
return this._tools.has(name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Execute a tool by name with the given arguments.
|
|
67
|
+
* Always returns a string (serializes objects to JSON).
|
|
68
|
+
*
|
|
69
|
+
* @param {string} name
|
|
70
|
+
* @param {Record<string, any>} args
|
|
71
|
+
* @returns {Promise<string>}
|
|
72
|
+
*/
|
|
73
|
+
async execute(name, args) {
|
|
74
|
+
const tool = this._tools.get(name);
|
|
75
|
+
if (!tool) {
|
|
76
|
+
return `Error: Tool "${name}" is not registered. Available tools: ${this.listNames().join(', ') || 'none'}`;
|
|
77
|
+
}
|
|
78
|
+
const result = await tool.execute(args);
|
|
79
|
+
return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Framework-specific error types
|
|
2
|
+
|
|
3
|
+
export class AgentError extends Error {
|
|
4
|
+
constructor(message, { cause, agentName } = {}) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'AgentError';
|
|
7
|
+
this.agentName = agentName ?? null;
|
|
8
|
+
if (cause) this.cause = cause;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class ToolError extends Error {
|
|
13
|
+
constructor(message, { cause, toolName } = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'ToolError';
|
|
16
|
+
this.toolName = toolName ?? null;
|
|
17
|
+
if (cause) this.cause = cause;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class LLMError extends Error {
|
|
22
|
+
constructor(message, { cause, provider } = {}) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'LLMError';
|
|
25
|
+
this.provider = provider ?? null;
|
|
26
|
+
if (cause) this.cause = cause;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ConfigError extends Error {
|
|
31
|
+
constructor(message, { cause, field } = {}) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'ConfigError';
|
|
34
|
+
this.field = field ?? null;
|
|
35
|
+
if (cause) this.cause = cause;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class MemoryError extends Error {
|
|
40
|
+
constructor(message, { cause, sessionId } = {}) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'MemoryError';
|
|
43
|
+
this.sessionId = sessionId ?? null;
|
|
44
|
+
if (cause) this.cause = cause;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
|
|
3
|
+
// Check if pino-pretty is available (optional dev dependency)
|
|
4
|
+
let prettyTransport;
|
|
5
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
6
|
+
try {
|
|
7
|
+
// Verify the module can be resolved before configuring it as a transport
|
|
8
|
+
await import('pino-pretty');
|
|
9
|
+
prettyTransport = { target: 'pino-pretty', options: { colorize: true, translateTime: 'HH:MM:ss' } };
|
|
10
|
+
} catch {
|
|
11
|
+
// pino-pretty not installed — fall back to default JSON output
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Single shared logger instance for the framework
|
|
16
|
+
// Uses pino for structured JSON output with log levels
|
|
17
|
+
const logger = pino({
|
|
18
|
+
name: 'agent-framework',
|
|
19
|
+
level: process.env.AGENT_LOG_LEVEL ?? 'info',
|
|
20
|
+
transport: prettyTransport,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a child logger scoped to a specific component (agent name, team name, etc.)
|
|
25
|
+
* @param {string} component
|
|
26
|
+
* @param {Object} [bindings] - Additional context to bind
|
|
27
|
+
* @returns {pino.Logger}
|
|
28
|
+
*/
|
|
29
|
+
export function createLogger(component, bindings = {}) {
|
|
30
|
+
return logger.child({ component, ...bindings });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default logger;
|