@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 +5 -2
- package/package.json +7 -4
- package/src/Agent.js +14 -114
- package/src/ToolLoader.js +149 -0
- package/src/a2a/A2AServer.js +84 -0
- package/src/a2a/AgentExecutorAdapter.js +109 -0
- package/src/a2a/RemoteAgentTool.js +71 -0
- package/src/config.js +2 -1
- package/src/llmService.js +21 -6
- package/src/providers/gemini.js +122 -10
- package/src/providers/openai.js +17 -7
package/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
export { LLMService } from './src/
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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 {
|
|
2
|
-
import {
|
|
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 =
|
|
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.
|
|
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.
|
|
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:
|
|
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.
|
|
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
|
|
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 =
|
|
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
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
}
|
package/src/providers/gemini.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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;
|
package/src/providers/openai.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
2
|
import { zodTextFormat } from "openai/helpers/zod";
|
|
3
|
-
import {
|
|
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:
|
|
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;
|