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.
@@ -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
+ }
@@ -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;