@peebles-group/agentlib-js 2.0.0 → 2.1.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/index.js CHANGED
@@ -1,3 +1,6 @@
1
- export { LLMService } from './src/llmService.js';
1
+ export { LLMService } from './src/LLMService.js';
2
2
  export { Agent } from './src/Agent.js';
3
- export { PromptLoader } from './src/prompt-loader/promptLoader.js';
3
+ export { PromptLoader } from './src/prompt-loader/promptLoader.js';
4
+ export { ToolLoader } from "./src/ToolLoader.js";
5
+ export { startA2AServer } from "./src/a2a/A2AServer.js";
6
+ export { createRemoteAgentTool } from "./src/a2a/RemoteAgentTool.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peebles-group/agentlib-js",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "A minimal JavaScript library implementing concurrent async agents for illustrating multi-agent systems and other agentic design patterns including recursive ones purely through function calling loops.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -11,12 +11,15 @@
11
11
  "author": "Peebles Group",
12
12
  "license": "MIT",
13
13
  "dependencies": {
14
+ "@a2a-js/sdk": "^0.3.10",
14
15
  "@google/genai": "^1.16.0",
15
16
  "@modelcontextprotocol/sdk": "^1.18.2",
16
- "openai": "^6.0.0",
17
+ "express": "^5.2.1",
17
18
  "js-yaml": "^4.1.0",
19
+ "openai": "^6.0.0",
20
+ "path": "^0.12.7",
18
21
  "sqlite3": "^5.1.7",
19
- "path": "^0.12.7"
22
+ "uuid": "^13.0.0"
20
23
  },
21
24
  "devDependencies": {
22
25
  "@peebles-group/agentlib-js": "^2.0.0",
@@ -34,4 +37,4 @@
34
37
  "README.md",
35
38
  "LICENSE"
36
39
  ]
37
- }
40
+ }
package/src/Agent.js CHANGED
@@ -1,5 +1,5 @@
1
- import { defaultModel } from "./config.js";
2
- import { MCPManager } from "./mcp/MCPManager.js";
1
+ import { defaultOpenaiModel } from "./config.js";
2
+ import { ToolLoader } from "./ToolLoader.js";
3
3
  import { PromptLoader } from "./prompt-loader/promptLoader.js";
4
4
  import { fileURLToPath } from 'url';
5
5
  import path, { dirname } from 'path';
@@ -7,8 +7,6 @@ import path, { dirname } from 'path';
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = dirname(__filename);
9
9
 
10
- const promptLoader = await PromptLoader.create(path.join(__dirname, 'prompts', 'agentPrompts.yml'));
11
-
12
10
  /**
13
11
  * Represents an LLM-based agent capable of tool calling.
14
12
  */
@@ -17,6 +15,7 @@ export class Agent {
17
15
  * @param {object} llmService - The LLM service used for communication with LLM client.
18
16
  * @param {object} [options] - Configuration options for the agent.
19
17
  * @param {string} [options.model=defaultModel] - The model identifier to use.
18
+ * @param {ToolLoader} [options.toolLoader=null] - Optional ToolLoader instance.
20
19
  * @param {Array<object>} [options.tools=[]] - Array of native tools available to the agent.
21
20
  * @param {zod object|null} [options.inputSchema=null] - Zod schema for validating input messages.
22
21
  * @param {zod object|null} [options.outputSchema=null] - Zod schema for expected final output format.
@@ -24,13 +23,12 @@ export class Agent {
24
23
  * @param {boolean} [options.redundantToolInfo=true] - Whether to include tool descriptions in the system prompt.
25
24
  * @param {object} [options...] - Additional options passed to the LLM service.
26
25
  */
27
- constructor(llmService, { model = defaultModel, tools = [], inputSchema = null, outputSchema = null, enableMCP = false, redundantToolInfo = true, ...options } = {}) {
26
+ constructor(llmService, { model = defaultOpenaiModel, toolLoader = null, inputSchema = null, outputSchema = null, enableMCP = false, redundantToolInfo = true, ...options } = {}) {
28
27
  this.llmService = llmService;
29
28
  this.model = model;
30
- this.nativeTools = tools;
29
+ this.toolLoader = toolLoader || new ToolLoader(enableMCP);
31
30
  this.inputSchema = inputSchema;
32
31
  this.outputSchema = outputSchema;
33
- this.mcpManager = enableMCP ? new MCPManager() : null;
34
32
  this.redundantToolInfo = redundantToolInfo;
35
33
  this.additionalOptions = options;
36
34
  this.input = [];
@@ -40,105 +38,16 @@ export class Agent {
40
38
  }
41
39
  }
42
40
 
43
- /**
44
- * Adds a new MCP server for remote tool access.
45
- * @param {string} serverName - A unique name for the server.
46
- * @param {object} config - Configuration details for the MCP server connection.
47
- * @returns {Promise<object>} The result of adding the server.
48
- * @throws {Error} If MCP is not enabled.
49
- */
50
- async addMCPServer(serverName, config) {
51
- if (!this.mcpManager) {
52
- throw new Error("MCP is not enabled for this agent");
53
- }
54
-
55
- const result = await this.mcpManager.addServer(serverName, config);
56
- if (this.redundantToolInfo) {
57
- this.updateSystemPrompt();
58
- }
59
- return result;
60
- }
61
-
62
- /**
63
- * Removes an existing MCP server.
64
- * @param {string} serverName - The unique name of the server to remove.
65
- * @returns {Promise<boolean>} True if the server was successfully removed, false otherwise.
66
- */
67
- async removeMCPServer(serverName) {
68
- if (!this.mcpManager) return false;
69
-
70
- const result = await this.mcpManager.removeServer(serverName);
71
- if (result && this.redundantToolInfo) {
72
- this.updateSystemPrompt();
73
- }
74
- return result;
75
- }
76
-
77
- /**
78
- * Adds a native tool to the agent's array of tools.
79
- * @param {object} tool - The tool object.
80
- * @param {string} tool.name - The unique name of the tool.
81
- * @param {function} tool.func - The function to execute when the tool is called.
82
- * @param {string} [tool.description=''] - A description for the LLM on when to use the tool.
83
- * @returns {object} The added tool object.
84
- * @throws {Error} If the tool is invalid or a name collision occurs.
85
- */
86
- addTool(tool) {
87
- if (!tool || typeof tool !== 'object') {
88
- throw new Error('Invalid tool: expected an object');
89
- }
90
-
91
- const { name, func } = tool;
92
- if (typeof name !== 'string' || name.trim() === '') {
93
- throw new Error("Invalid tool: missing valid 'name' (string)");
94
- }
95
- if (typeof func !== 'function') {
96
- throw new Error("Invalid tool: missing 'func' (function)");
97
- }
98
-
99
- const nameExistsInNative = this.nativeTools.some(t => t && t.name === name);
100
- const nameExistsInMCP = this.mcpManager ? this.mcpManager.getAllTools().some(t => t && t.name === name) : false;
101
- if (nameExistsInNative || nameExistsInMCP) {
102
- throw new Error(`Tool with name '${name}' already exists`);
103
- }
104
-
105
- if (typeof tool.description !== 'string') {
106
- tool.description = '';
107
- }
108
-
109
- this.nativeTools.push(tool);
110
- if (this.redundantToolInfo) {
111
- this.updateSystemPrompt();
112
- }
113
- return tool;
114
- }
115
-
116
- /**
117
- * Retrieves all available tools, including native and MCP tools.
118
- * @returns {Array<object>} An array of all tools.
119
- */
120
- getAllTools() {
121
- const mcpTools = this.mcpManager ? this.mcpManager.getAllTools() : [];
122
- return [...this.nativeTools, ...mcpTools];
123
- }
124
-
125
- /**
126
- * Gets status information about the MCP manager.
127
- * @returns {object} Information about the MCP manager, or { enabled: false } if disabled.
128
- */
129
- getMCPInfo() {
130
- return this.mcpManager ? this.mcpManager.getServerInfo() : { enabled: false };
131
- }
132
-
133
41
  /**
134
42
  * Updates the system prompt with descriptions of all currently available tools.
135
43
  */
136
44
  updateSystemPrompt() {
137
- const allTools = this.getAllTools();
45
+ const allTools = this.toolLoader.getTools();
138
46
  const toolDescriptions = allTools.map(tool => `${tool.name}: ${tool.description}`).join('; ');
139
47
  this.input = [{
140
48
  role: 'system',
141
- content: promptLoader.getPrompt('systemPrompt').format({ toolDescriptions })
49
+ content: `You are a tool-calling agent. You have access to the following tools: ${toolDescriptions}.
50
+ Use these tools to answer the user's questions.`
142
51
  }];
143
52
  }
144
53
 
@@ -159,7 +68,7 @@ export class Agent {
159
68
  * @returns {Promise<object>} The final response object from the LLM, including execution details.
160
69
  */
161
70
  async run() {
162
- const allTools = this.getAllTools();
71
+ const allTools = this.toolLoader.getTools() || [];
163
72
  const executed = []
164
73
 
165
74
  let response = await this.llmService.chat(this.input, {
@@ -174,7 +83,8 @@ export class Agent {
174
83
  rawResponse.output.forEach(item => {
175
84
  if (item.type === "function_call") {
176
85
  const { parsed_arguments, ...rest } = item;
177
- const cleanedItem = { ...rest, arguments: JSON.stringify(item.arguments) };
86
+ const args = typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments);
87
+ const cleanedItem = { ...rest, arguments: args };
178
88
  this.addInput(cleanedItem);
179
89
  } else {
180
90
  this.addInput(item);
@@ -190,7 +100,7 @@ export class Agent {
190
100
  call.arguments = args
191
101
  executed.push(call)
192
102
 
193
- const tool = allTools.find(t => t.name === call.name);
103
+ const tool = this.toolLoader.findTool(call.name);
194
104
  if (!tool || !tool.func) {
195
105
  throw new Error(`Tool ${call.name} not found or missing implementation.`);
196
106
  }
@@ -198,12 +108,12 @@ export class Agent {
198
108
  const result = await tool.func(args);
199
109
 
200
110
  this.input.push({
111
+ ...call,
201
112
  type: "function_call_output",
202
- call_id: call.call_id,
203
113
  output: JSON.stringify(result),
204
114
  });
205
115
  }
206
-
116
+
207
117
  // Step 6: send updated input back to model for final response
208
118
  response = await this.llmService.chat(this.input, {
209
119
  tools: allTools,
@@ -215,14 +125,4 @@ export class Agent {
215
125
  response.executed = executed;
216
126
  return response;
217
127
  }
218
-
219
- /**
220
- * Performs cleanup operations, primarily closing MCP server connections.
221
- * @returns {Promise<void>}
222
- */
223
- async cleanup() {
224
- if (this.mcpManager) {
225
- await this.mcpManager.cleanup();
226
- }
227
- }
228
128
  }
@@ -0,0 +1,149 @@
1
+ import { MCPManager } from "./mcp/MCPManager.js";
2
+
3
+ /**
4
+ * Manages the lifecycle, storage, and retrieval of the agent's tools.
5
+ */
6
+ export class ToolLoader {
7
+ /**
8
+ * @param {boolean} [enableMCP=false] - Whether to initialize the MCP manager.
9
+ */
10
+ constructor(enableMCP = false) {
11
+ this.nativeTools = new Map();
12
+ this.mcpManager = enableMCP ? new MCPManager() : null;
13
+ }
14
+
15
+ /**
16
+ * @returns {Array<Object>} An array of tool objects containing name, description, and schemas.
17
+ */
18
+ getTools() {
19
+ const native = Array.from(this.nativeTools.values());
20
+ const mcp = this.mcpManager ? this.mcpManager.getAllTools() : [];
21
+ return [...native, ...mcp];
22
+ }
23
+
24
+ /**
25
+ * Finds a tool by name, searching Native tools first, then MCP tools.
26
+ * @param {string} name - The name of the tool to find.
27
+ * @returns {Object|null} The tool object including the executable function, or null if not found.
28
+ */
29
+ findTool(name) {
30
+ if (this.nativeTools.has(name)) {
31
+ return this.nativeTools.get(name);
32
+ }
33
+ if (this.mcpManager) {
34
+ return this.mcpManager.getAllTools().find(t => t.name === name);
35
+ }
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * Registers a new tool.
41
+ * @param {Object} tool - The tool definition.
42
+ * @param {string} tool.name - The unique name of the tool.
43
+ * @param {Function} tool.func - The function to execute when the tool is called.
44
+ * @param {string} [tool.description] - A description of what the tool does.
45
+ * @throws {Error} If the tool structure is invalid or the name is a duplicate.
46
+ */
47
+ addTool(tool) {
48
+ this._validateToolStructure(tool);
49
+
50
+ if (this._isDuplicate(tool.name)) {
51
+ throw new Error(`Tool with name '${tool.name}' already exists.`);
52
+ }
53
+
54
+ this.nativeTools.set(tool.name, {
55
+ description: '',
56
+ ...tool
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Registers multiple tools.
62
+ * @param {Array<Object>} tools - An array of tool definitions.
63
+ * @throws {Error} If any tool structure is invalid or if any name is a duplicate.
64
+ */
65
+ addTools(tools) {
66
+ tools.forEach(tool => this.addTool(tool));
67
+ }
68
+
69
+ /**
70
+ * Adds an MCP server configuration to the manager.
71
+ * @param {string} serverName - A unique identifier for this MCP server.
72
+ * @param {Object} config - The configuration object for the MCP server connection.
73
+ * @returns {Promise<Object>} The result of the connection attempt.
74
+ * @throws {Error} If MCP is not enabled for this registry.
75
+ */
76
+ async addMCPServer(serverName, config) {
77
+ if (!this.mcpManager) {
78
+ throw new Error("MCP is disabled.");
79
+ }
80
+ return this.mcpManager.addServer(serverName, config);
81
+ }
82
+
83
+ /**
84
+ * Removes an MCP server and cleans up its resources.
85
+ * @param {string} serverName - The identifier of the server to remove.
86
+ * @returns {Promise<boolean>} True if removed successfully, false otherwise.
87
+ */
88
+ async removeMCPServer(serverName) {
89
+ if (!this.mcpManager) return false;
90
+ return this.mcpManager.removeServer(serverName);
91
+ }
92
+
93
+ /**
94
+ * Generates a text snippet describing available tools for the System Prompt.
95
+ * @returns {string} A formatted string describing all tools, or an empty string if no tools exist.
96
+ */
97
+ getSystemPromptSnippet() {
98
+ const tools = this.getTools();
99
+ if (tools.length === 0) return "";
100
+
101
+ const descriptions = tools
102
+ .map(t => `${t.name}: ${t.description}`)
103
+ .join('; ');
104
+
105
+ return `You are a tool-calling agent. You have access to the following tools: ${descriptions}. Use these tools to answer the user's questions.`;
106
+ }
107
+
108
+ /**
109
+ * Gets status information about the MCP manager.
110
+ * @returns {Object} Information about the MCP manager, or { enabled: false } if disabled.
111
+ */
112
+ getMCPInfo() {
113
+ return this.mcpManager ? this.mcpManager.getServerInfo() : { enabled: false };
114
+ }
115
+
116
+ /**
117
+ * Cleans up resources, specifically closing MCP connections.
118
+ * @returns {Promise<void>}
119
+ */
120
+ async cleanup() {
121
+ if (this.mcpManager) await this.mcpManager.cleanup();
122
+ }
123
+
124
+ // --- Internals ---
125
+
126
+ /**
127
+ * Validates that a tool object matches the required schema.
128
+ * @private
129
+ * @param {Object} tool - The tool to validate.
130
+ * @throws {Error} If the tool is missing required properties.
131
+ */
132
+ _validateToolStructure(tool) {
133
+ if (!tool || typeof tool !== 'object') throw new Error("Invalid tool object");
134
+ if (typeof tool.name !== 'string' || !tool.name.trim()) throw new Error("Tool missing name");
135
+ if (typeof tool.func !== 'function') throw new Error("Tool missing func");
136
+ }
137
+
138
+ /**
139
+ * Checks if a tool name already exists in either Native or MCP registries.
140
+ * @private
141
+ * @param {string} name - The tool name to check.
142
+ * @returns {boolean} True if the name exists, false otherwise.
143
+ */
144
+ _isDuplicate(name) {
145
+ const existsNative = this.nativeTools.has(name);
146
+ const existsMCP = this.mcpManager?.getAllTools().some(t => t.name === name);
147
+ return existsNative || existsMCP;
148
+ }
149
+ }
@@ -0,0 +1,84 @@
1
+
2
+ import express from 'express';
3
+ import {
4
+ AGENT_CARD_PATH
5
+ } from '@a2a-js/sdk';
6
+ import {
7
+ DefaultRequestHandler,
8
+ InMemoryTaskStore,
9
+ } from '@a2a-js/sdk/server';
10
+ import {
11
+ agentCardHandler,
12
+ jsonRpcHandler,
13
+ restHandler,
14
+ UserBuilder
15
+ } from '@a2a-js/sdk/server/express';
16
+ import { AgentExecutorAdapter } from './AgentExecutorAdapter.js';
17
+
18
+ /**
19
+ * Starts an A2A-compliant server for the given agent.
20
+ * @param {import('../Agent').Agent} agent - The agent to expose.
21
+ * @param {object} options
22
+ * @param {number} [options.port=4000] - The port to listen on.
23
+ * @param {string} [options.name] - Agent name override.
24
+ * @param {string} [options.baseUrl] - The public URL (e.g., http://localhost:4000).
25
+ */
26
+ export function startA2AServer(agent, { port = 4000, name, baseUrl = `http://localhost:${port}` } = {}) {
27
+
28
+ // 1. Generate Agent Card
29
+ const tools = agent.toolLoader.getTools();
30
+
31
+ // Convert tools to A2A skills
32
+ const skills = tools.map(tool => ({
33
+ id: tool.name,
34
+ name: tool.name,
35
+ description: tool.description,
36
+ tags: ['tool']
37
+ }));
38
+
39
+ const agentCard = {
40
+ name: name || "AgentLib Agent",
41
+ description: "An agent exposed via agentlib A2A.",
42
+ protocolVersion: '0.3.0',
43
+ version: '1.0.0',
44
+ url: `${baseUrl}/a2a/jsonrpc`,
45
+ skills: skills,
46
+ capabilities: {
47
+ pushNotifications: false,
48
+ },
49
+ defaultInputModes: ['text'],
50
+ defaultOutputModes: ['text'],
51
+ additionalInterfaces: [
52
+ { url: `${baseUrl}/a2a/jsonrpc`, transport: 'JSONRPC' },
53
+ { url: `${baseUrl}/a2a/rest`, transport: 'HTTP+JSON' },
54
+ ],
55
+ };
56
+
57
+ // 2. Setup Executor and Handlers
58
+ const executor = new AgentExecutorAdapter(agent);
59
+ const taskStore = new InMemoryTaskStore();
60
+ const requestHandler = new DefaultRequestHandler(
61
+ agentCard,
62
+ taskStore,
63
+ executor
64
+ );
65
+
66
+ // 3. Setup Express
67
+ const app = express();
68
+
69
+ // Agent Card Endpoint
70
+ app.use(`/${AGENT_CARD_PATH}`, agentCardHandler({ agentCardProvider: requestHandler }));
71
+
72
+ // Communication Endpoints
73
+ // Note: Using UserBuilder.noAuthentication for easy testing.
74
+ app.use('/a2a/jsonrpc', jsonRpcHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }));
75
+ app.use('/a2a/rest', restHandler({ requestHandler, userBuilder: UserBuilder.noAuthentication }));
76
+
77
+ // Start Server
78
+ const server = app.listen(port, () => {
79
+ console.log(` A2A Server started on ${baseUrl}`);
80
+ console.log(` Card: ${baseUrl}/${AGENT_CARD_PATH}`);
81
+ });
82
+
83
+ return server;
84
+ }
@@ -0,0 +1,109 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+
3
+ /**
4
+ * Adapts an agentlib Agent to the A2A AgentExecutor interface.
5
+ * This allows the agent to be run by the A2A server.
6
+ */
7
+ export class AgentExecutorAdapter {
8
+ /**
9
+ * @param {import('../Agent').Agent} agent - The agent to adapt.
10
+ */
11
+ constructor(agent) {
12
+ this.agent = agent;
13
+ this.cancelledTasks = new Set();
14
+ }
15
+
16
+ /**
17
+ * Executes the agent logic for a given task.
18
+ * @param {import('@a2a-js/sdk/server').RequestContext} requestContext
19
+ * @param {import('@a2a-js/sdk/server').ExecutionEventBus} eventBus
20
+ */
21
+ async execute(requestContext, eventBus) {
22
+ const { taskId, contextId, userMessage, task } = requestContext;
23
+
24
+ // Publish initial task state if not already present
25
+ if (!task) {
26
+ eventBus.publish({
27
+ kind: 'task',
28
+ id: taskId,
29
+ contextId: contextId,
30
+ status: { state: 'submitted', timestamp: new Date().toISOString() },
31
+ history: [userMessage],
32
+ });
33
+ }
34
+
35
+ // Publish 'working' status
36
+ eventBus.publish({
37
+ kind: 'status-update',
38
+ taskId,
39
+ contextId,
40
+ status: { state: 'working', timestamp: new Date().toISOString() },
41
+ final: false,
42
+ });
43
+
44
+ try {
45
+ // Extract text content from the A2A message
46
+ // A2A messages have parts, usually 'text' kind.
47
+ const textPart = userMessage.parts.find(p => p.kind === 'text');
48
+ const inputContent = textPart ? textPart.text : JSON.stringify(userMessage.parts);
49
+
50
+ // Add to agent's input
51
+ this.agent.addInput({ role: 'user', content: inputContent });
52
+
53
+ // Run the agent
54
+ const response = await this.agent.run();
55
+
56
+ // The response from agentlib is typically { rawResponse, output: [...messages], ... }
57
+ // We want the final text response.
58
+ // Depending on the Agent implementation, output might be the full history or just the new messages.
59
+ // Based on Agent.js, run() returns `response` which has `executed`.
60
+ // The last message in `this.agent.input` should be the assistant's response.
61
+
62
+ const lastMessage = this.agent.input[this.agent.input.length - 1];
63
+ let responseText = "No response generated";
64
+
65
+ if (lastMessage && lastMessage.role === 'assistant') {
66
+ responseText = lastMessage.content;
67
+ } else if (response.rawResponse && response.rawResponse.content) {
68
+ // Fallback if not added to input yet
69
+ responseText = response.rawResponse.content;
70
+ }
71
+
72
+ // Publish the response message
73
+ eventBus.publish({
74
+ kind: 'message',
75
+ messageId: uuidv4(),
76
+ role: 'agent',
77
+ parts: [{ kind: 'text', text: responseText }],
78
+ contextId: contextId,
79
+ });
80
+
81
+ // Mark as completed
82
+ eventBus.publish({
83
+ kind: 'status-update',
84
+ taskId,
85
+ contextId,
86
+ status: { state: 'completed', timestamp: new Date().toISOString() },
87
+ final: true,
88
+ });
89
+
90
+ } catch (error) {
91
+ console.error("Error executing agent:", error);
92
+ eventBus.publish({
93
+ kind: 'status-update',
94
+ taskId,
95
+ contextId,
96
+ status: { state: 'failed', timestamp: new Date().toISOString(), details: error.message },
97
+ final: true,
98
+ });
99
+ } finally {
100
+ eventBus.finished();
101
+ }
102
+ }
103
+
104
+ async cancelTask(taskId, eventBus) {
105
+ this.cancelledTasks.add(taskId);
106
+ // Note: To fully support cancellation, Agent.js would need to check this flag during its execution loop.
107
+ // For now, this just marks it.
108
+ }
109
+ }
@@ -0,0 +1,71 @@
1
+ import { ClientFactory } from '@a2a-js/sdk/client';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ /**
5
+ * Creates a tool function that calls a remote A2A agent.
6
+ * This can be added to a local agent's tool set.
7
+ *
8
+ * @param {string} remoteUrl - The URL of the remote agent (e.g. http://localhost:4000)
9
+ * @param {string} [toolName='remote_agent'] - The name of the tool to register.
10
+ * @param {string} [description] - Description for the tool. Use this to tell the agent when to use it.
11
+ * @returns {object} A tool definition compatible with ToolLoader.
12
+ */
13
+ export async function createRemoteAgentTool(remoteUrl, toolName = 'remote_agent', description = "Ask a remote agent for help.") {
14
+ const factory = new ClientFactory();
15
+ let client;
16
+
17
+ try {
18
+ client = await factory.createFromUrl(remoteUrl);
19
+ } catch (err) {
20
+ console.warn(`Failed to connect to remote agent at ${remoteUrl}:`, err.message);
21
+ // We return the tool anyway, but it might fail at runtime if not fixed.
22
+ }
23
+
24
+ return {
25
+ name: toolName,
26
+ type: 'function',
27
+ description: description,
28
+ func: async ({ request }) => {
29
+ if (!client) {
30
+ // Try connecting again if initially failed
31
+ client = await factory.createFromUrl(remoteUrl);
32
+ }
33
+
34
+ const sendParams = {
35
+ message: {
36
+ messageId: uuidv4(),
37
+ role: 'user',
38
+ parts: [{ kind: 'text', text: request }],
39
+ kind: 'message',
40
+ },
41
+ };
42
+
43
+ try {
44
+ const response = await client.sendMessage(sendParams);
45
+
46
+ if (response.kind === 'task') {
47
+ // It returned a task object, check artifacts or wait?
48
+ // Simple case: just report the status
49
+ return `Remote task started: ${response.id} - Status: ${response.status.state}`;
50
+ } else if (response.kind === 'message') {
51
+ // Return the text
52
+ return response.parts.map(p => p.text).join('\n');
53
+ }
54
+ return "Unknown response type from remote agent.";
55
+
56
+ } catch (error) {
57
+ return `Error communicating with remote agent: ${error.message}`;
58
+ }
59
+ },
60
+ parameters: {
61
+ type: "object",
62
+ properties: {
63
+ request: {
64
+ type: "string",
65
+ description: "The natural language request to send to the remote agent."
66
+ }
67
+ },
68
+ required: ["request"]
69
+ }
70
+ };
71
+ }
package/src/config.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export const defaultProvider = 'openai';
2
- export const defaultModel = 'gpt-5';
2
+ export const defaultOpenaiModel = 'gpt-5';
3
+ export const defaultGeminiModel = 'gemini-3-pro-preview';
package/src/llmService.js CHANGED
@@ -18,11 +18,26 @@ export class LLMService {
18
18
  return this.providerNamespace.createClient(this.apiKey);
19
19
  }
20
20
 
21
- async chat(input, {inputSchema = null, outputSchema = null, ...options} = {}) {
22
- return this.providerNamespace.chat(this.client, input, {
23
- inputSchema,
24
- outputSchema,
25
- ...options
26
- });
21
+ async chat(input, { inputSchema = null, outputSchema = null, maxRetries = 3, initialDelay = 1000, ...options } = {}) {
22
+ let attempt = 0;
23
+ let delay = initialDelay;
24
+
25
+ while (true) {
26
+ try {
27
+ return await this.providerNamespace.chat(this.client, input, {
28
+ inputSchema,
29
+ outputSchema,
30
+ ...options
31
+ });
32
+ } catch (error) {
33
+ attempt++;
34
+ if (attempt > maxRetries) {
35
+ throw error;
36
+ }
37
+ console.warn(`LLM call failed (attempt ${attempt}/${maxRetries}). Retrying in ${delay}ms... Error: ${error.message}`);
38
+ await new Promise(resolve => setTimeout(resolve, delay));
39
+ delay *= 2; // Exponential backoff
40
+ }
41
+ }
27
42
  }
28
43
  }
@@ -1,20 +1,132 @@
1
1
  import { GoogleGenAI } from "@google/genai";
2
+ import { defaultGeminiModel } from "../config.js";
3
+ import { zodToJsonSchema } from "zod-to-json-schema";
2
4
 
3
5
  export function createClient(apiKey) {
4
- return new GoogleGenAI(apiKey);
6
+ return new GoogleGenAI({ apiKey });
5
7
  }
6
8
 
7
- export async function chat(client, input, { inputSchema, outputSchema, ...options }) {
8
- // TODO: Add translation of input to provide
9
- const defaultOptions = { model: 'gemini-2.5-flash-lite' };
10
- const finalOptions = { ...defaultOptions, ...options };
9
+ function _convertInput(input) {
10
+ const contents = [];
11
+ const systemParts = [];
12
+ for (const object of input) {
13
+ if (object.role === 'user') {
14
+ contents.push({
15
+ role: "user",
16
+ parts: [{ text: object.content }]
17
+ })
18
+ } else if (object.type === 'function_call') {
19
+ contents.push({
20
+ role: "model",
21
+ parts: [{
22
+ functionCall: {
23
+ name: object.name,
24
+ args: object.args || (typeof object.arguments === 'string' ? JSON.parse(object.arguments) : object.arguments),
25
+ },
26
+ thoughtSignature: object.thoughtSignature
27
+ }],
28
+ })
29
+ } else if (object.type === 'function_call_output') {
30
+ contents.push({
31
+ role: 'user',
32
+ parts: [{ functionResponse: { name: object.name, response: { result: JSON.parse(object.output) } } }]
33
+ })
34
+ } else if (object.role === 'system') {
35
+ systemParts.push({
36
+ text: object.content
37
+ })
38
+ }
39
+ }
40
+ return {
41
+ contents,
42
+ systemParts
43
+ }
44
+ }
45
+
46
+ function _convertCandidateParts(parts) {
47
+ const output = [];
48
+ for (const part of parts) {
49
+ if (part.functionCall) {
50
+ output.push({
51
+ type: "function_call",
52
+ name: part.functionCall.name,
53
+ arguments: JSON.stringify(part.functionCall.args),
54
+ thoughtSignature: part.thoughtSignature
55
+ })
56
+ }
57
+ else {
58
+ output.push({
59
+ type: "message",
60
+ content: {
61
+ text: part.text
62
+ }
63
+ })
64
+ }
65
+ }
66
+ return output;
67
+ }
11
68
 
69
+ function _convertResponse(response, output) {
70
+ return {
71
+ output: output,
72
+ rawResponse: {
73
+ output: _convertCandidateParts(response.candidates[0].content.parts),
74
+ model: response.modelVersion,
75
+ id: response.responseId,
76
+ usage: response.usageMetadata,
77
+ originalFormat: response
78
+ }
79
+ };
80
+ }
81
+
82
+ export async function chat(client, input, { inputSchema, outputSchema, tools, ...options }) {
12
83
  try {
13
- const response = await client.models.generateContent({
14
- contents: input,
15
- ...finalOptions,
16
- });
17
- return response;
84
+ let response, output;
85
+ const formattedInput = _convertInput(input);
86
+ const config = {
87
+ systemInstruction: {
88
+ parts: formattedInput.systemParts || []
89
+ },
90
+ tools: tools ? [{
91
+ functionDeclarations: tools.map(tool => ({
92
+ name: tool.name,
93
+ description: tool.description,
94
+ parameters: tool.parameters,
95
+ func: tool.func
96
+ }))
97
+ }] : [],
98
+ ...options
99
+ }
100
+
101
+ if (outputSchema) {
102
+ response = await client.models.generateContent({
103
+ model: defaultGeminiModel,
104
+ contents: formattedInput.contents,
105
+ config: {
106
+ ...config,
107
+ responseMimeType: "application/json",
108
+ responseJsonSchema: zodToJsonSchema(outputSchema),
109
+ }
110
+ });
111
+
112
+ const candidates = response.candidates;
113
+ const hasFunctionCall = candidates && candidates[0] && candidates[0].content && candidates[0].content.parts.some(p => p.functionCall);
114
+
115
+ if (hasFunctionCall) {
116
+ output = null;
117
+ } else {
118
+ const text = response.text ? (typeof response.text === 'function' ? response.text() : response.text) : null;
119
+ output = text ? outputSchema.parse(JSON.parse(text)) : null;
120
+ }
121
+ } else {
122
+ response = await client.models.generateContent({
123
+ model: defaultGeminiModel,
124
+ contents: formattedInput.contents,
125
+ config: config,
126
+ });
127
+ output = response.text ? (typeof response.text === 'function' ? response.text() : response.text) : null;
128
+ }
129
+ return _convertResponse(response, output);
18
130
  } catch (error) {
19
131
  console.error(`Error during Gemini chat completion:`, error);
20
132
  throw error;
@@ -1,15 +1,25 @@
1
1
  import OpenAI from 'openai';
2
2
  import { zodTextFormat } from "openai/helpers/zod";
3
- import { defaultModel } from "../config.js";
3
+ import { defaultOpenaiModel } from "../config.js";
4
4
 
5
5
  // Factory function to create client
6
6
  export function createClient(apiKey) {
7
7
  return new OpenAI({ apiKey });
8
8
  }
9
9
 
10
+ function _convertInput(input) {
11
+ return input.map((item) => {
12
+ if (item.type === 'function_call_output') {
13
+ return { type: 'function_call_output', call_id: item.call_id, output: JSON.stringify(item.output) };
14
+ } else {
15
+ return item;
16
+ }
17
+ });
18
+ }
19
+
10
20
  // Now accepts the client as first parameter
11
21
  export async function chat(client, input, { inputSchema, outputSchema, ...options }) {
12
- const defaultOptions = { model: defaultModel };
22
+ const defaultOptions = { model: defaultOpenaiModel };
13
23
  const finalOptions = { ...defaultOptions, ...options };
14
24
 
15
25
  if (inputSchema) {
@@ -20,21 +30,21 @@ export async function chat(client, input, { inputSchema, outputSchema, ...option
20
30
  let response, output;
21
31
  if (outputSchema) {
22
32
  response = await client.responses.parse({
23
- input: input,
24
- text: {
25
- format: zodTextFormat(outputSchema, "output")
33
+ input: _convertInput(input),
34
+ text: {
35
+ format: zodTextFormat(outputSchema, "output")
26
36
  },
27
37
  ...finalOptions,
28
38
  });
29
39
  output = response.output_parsed;
30
40
  } else {
31
41
  response = await client.responses.create({
32
- input: input,
42
+ input: _convertInput(input),
33
43
  ...finalOptions,
34
44
  });
35
45
  output = response.output_text;
36
46
  }
37
- return {output: output, rawResponse: response};
47
+ return { output: output, rawResponse: response };
38
48
  } catch (error) {
39
49
  console.error(`Error during OpenAI chat response creation:`, error);
40
50
  throw error;