@xalia/agent 0.5.8 → 0.6.1

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.
Files changed (185) hide show
  1. package/README.md +23 -8
  2. package/dist/agent/src/agent/agent.js +173 -96
  3. package/dist/agent/src/agent/agentUtils.js +82 -53
  4. package/dist/agent/src/agent/compressingContextManager.js +102 -0
  5. package/dist/agent/src/agent/context.js +189 -0
  6. package/dist/agent/src/agent/dummyLLM.js +46 -5
  7. package/dist/agent/src/agent/iAgentEventHandler.js +2 -0
  8. package/dist/agent/src/agent/mcpServerManager.js +22 -23
  9. package/dist/agent/src/agent/nullAgentEventHandler.js +21 -0
  10. package/dist/agent/src/agent/nullPlatform.js +14 -0
  11. package/dist/agent/src/agent/openAILLMStreaming.js +12 -7
  12. package/dist/agent/src/agent/promptProvider.js +63 -0
  13. package/dist/agent/src/agent/repeatLLM.js +5 -5
  14. package/dist/agent/src/agent/sudoMcpServerManager.js +11 -9
  15. package/dist/agent/src/agent/tokenAuth.js +7 -7
  16. package/dist/agent/src/agent/tools.js +1 -1
  17. package/dist/agent/src/chat/client/chatClient.js +733 -0
  18. package/dist/agent/src/chat/client/connection.js +209 -0
  19. package/dist/agent/src/chat/client/connection.test.js +188 -0
  20. package/dist/agent/src/chat/client/constants.js +5 -0
  21. package/dist/agent/src/chat/client/index.js +15 -0
  22. package/dist/agent/src/chat/client/interfaces.js +2 -0
  23. package/dist/agent/src/chat/client/responseHandler.js +105 -0
  24. package/dist/agent/src/chat/client/sessionClient.js +331 -0
  25. package/dist/agent/src/chat/client/teamManager.js +2 -0
  26. package/dist/agent/src/chat/{apiKeyManager.js → data/apiKeyManager.js} +4 -0
  27. package/dist/agent/src/chat/data/dataModels.js +2 -0
  28. package/dist/agent/src/chat/data/database.js +749 -0
  29. package/dist/agent/src/chat/data/dbMcpServerConfigs.js +47 -0
  30. package/dist/agent/src/chat/protocol/connectionMessages.js +5 -0
  31. package/dist/agent/src/chat/protocol/constants.js +50 -0
  32. package/dist/agent/src/chat/protocol/errors.js +22 -0
  33. package/dist/agent/src/chat/protocol/messages.js +110 -0
  34. package/dist/agent/src/chat/server/chatContextManager.js +405 -0
  35. package/dist/agent/src/chat/server/connectionManager.js +352 -0
  36. package/dist/agent/src/chat/server/connectionManager.test.js +159 -0
  37. package/dist/agent/src/chat/server/conversation.js +198 -0
  38. package/dist/agent/src/chat/server/errorUtils.js +23 -0
  39. package/dist/agent/src/chat/server/openSession.js +869 -0
  40. package/dist/agent/src/chat/server/server.js +177 -0
  41. package/dist/agent/src/chat/server/sessionFileManager.js +161 -0
  42. package/dist/agent/src/chat/server/sessionRegistry.js +700 -0
  43. package/dist/agent/src/chat/server/sessionRegistry.test.js +97 -0
  44. package/dist/agent/src/chat/server/test-utils/mockFactories.js +307 -0
  45. package/dist/agent/src/chat/server/tools.js +243 -0
  46. package/dist/agent/src/chat/utils/agentSessionMap.js +66 -0
  47. package/dist/agent/src/chat/utils/approvalManager.js +85 -0
  48. package/dist/agent/src/{utils → chat/utils}/asyncLock.js +3 -3
  49. package/dist/agent/src/chat/{asyncQueue.js → utils/asyncQueue.js} +12 -2
  50. package/dist/agent/src/chat/utils/htmlToText.js +84 -0
  51. package/dist/agent/src/chat/utils/multiAsyncQueue.js +42 -0
  52. package/dist/agent/src/chat/utils/search.js +145 -0
  53. package/dist/agent/src/chat/utils/userResolver.js +46 -0
  54. package/dist/agent/src/chat/{websocket.js → utils/websocket.js} +2 -0
  55. package/dist/agent/src/test/agent.test.js +332 -0
  56. package/dist/agent/src/test/approvalManager.test.js +58 -0
  57. package/dist/agent/src/test/chatContextManager.test.js +392 -0
  58. package/dist/agent/src/test/clientServerConnection.test.js +158 -0
  59. package/dist/agent/src/test/compressingContextManager.test.js +65 -0
  60. package/dist/agent/src/test/context.test.js +83 -0
  61. package/dist/agent/src/test/conversation.test.js +89 -0
  62. package/dist/agent/src/test/db.test.js +262 -90
  63. package/dist/agent/src/test/dbMcpServerConfigs.test.js +72 -0
  64. package/dist/agent/src/test/dbTestTools.js +99 -0
  65. package/dist/agent/src/test/imageLoad.test.js +8 -7
  66. package/dist/agent/src/test/mcpServerManager.test.js +21 -18
  67. package/dist/agent/src/test/multiAsyncQueue.test.js +101 -0
  68. package/dist/agent/src/test/openaiStreaming.test.js +12 -11
  69. package/dist/agent/src/test/prompt.test.js +5 -4
  70. package/dist/agent/src/test/promptProvider.test.js +28 -0
  71. package/dist/agent/src/test/responseHandler.test.js +61 -0
  72. package/dist/agent/src/test/sudoMcpServerManager.test.js +14 -12
  73. package/dist/agent/src/test/testTools.js +109 -0
  74. package/dist/agent/src/test/tools.test.js +31 -0
  75. package/dist/agent/src/tool/agentChat.js +21 -10
  76. package/dist/agent/src/tool/agentMain.js +1 -1
  77. package/dist/agent/src/tool/chatMain.js +235 -58
  78. package/dist/agent/src/tool/commandPrompt.js +15 -9
  79. package/dist/agent/src/tool/files.js +20 -16
  80. package/dist/agent/src/tool/nodePlatform.js +47 -3
  81. package/dist/agent/src/tool/options.js +4 -4
  82. package/dist/agent/src/tool/prompt.js +19 -13
  83. package/eslint.config.mjs +14 -1
  84. package/package.json +14 -6
  85. package/scripts/chat_server +8 -0
  86. package/scripts/setup_chat +7 -2
  87. package/scripts/shutdown_chat_server +3 -0
  88. package/scripts/test_chat +135 -17
  89. package/src/agent/agent.ts +270 -135
  90. package/src/agent/agentUtils.ts +136 -95
  91. package/src/agent/compressingContextManager.ts +164 -0
  92. package/src/agent/context.ts +268 -0
  93. package/src/agent/dummyLLM.ts +76 -8
  94. package/src/agent/iAgentEventHandler.ts +54 -0
  95. package/src/agent/iplatform.ts +1 -0
  96. package/src/agent/mcpServerManager.ts +32 -30
  97. package/src/agent/nullAgentEventHandler.ts +20 -0
  98. package/src/agent/nullPlatform.ts +13 -0
  99. package/src/agent/openAILLMStreaming.ts +12 -6
  100. package/src/agent/promptProvider.ts +87 -0
  101. package/src/agent/repeatLLM.ts +5 -5
  102. package/src/agent/sudoMcpServerManager.ts +13 -11
  103. package/src/agent/tokenAuth.ts +7 -7
  104. package/src/agent/tools.ts +3 -1
  105. package/src/chat/client/chatClient.ts +900 -0
  106. package/src/chat/client/connection.test.ts +241 -0
  107. package/src/chat/client/connection.ts +276 -0
  108. package/src/chat/client/constants.ts +3 -0
  109. package/src/chat/client/index.ts +18 -0
  110. package/src/chat/client/interfaces.ts +34 -0
  111. package/src/chat/client/responseHandler.ts +131 -0
  112. package/src/chat/client/sessionClient.ts +443 -0
  113. package/src/chat/client/teamManager.ts +29 -0
  114. package/src/chat/{apiKeyManager.ts → data/apiKeyManager.ts} +6 -2
  115. package/src/chat/data/dataModels.ts +85 -0
  116. package/src/chat/data/database.ts +982 -0
  117. package/src/chat/data/dbMcpServerConfigs.ts +59 -0
  118. package/src/chat/protocol/connectionMessages.ts +49 -0
  119. package/src/chat/protocol/constants.ts +55 -0
  120. package/src/chat/protocol/errors.ts +16 -0
  121. package/src/chat/protocol/messages.ts +682 -0
  122. package/src/chat/server/README.md +127 -0
  123. package/src/chat/server/chatContextManager.ts +612 -0
  124. package/src/chat/server/connectionManager.test.ts +266 -0
  125. package/src/chat/server/connectionManager.ts +541 -0
  126. package/src/chat/server/conversation.ts +269 -0
  127. package/src/chat/server/errorUtils.ts +28 -0
  128. package/src/chat/server/openSession.ts +1332 -0
  129. package/src/chat/server/server.ts +177 -0
  130. package/src/chat/server/sessionFileManager.ts +239 -0
  131. package/src/chat/server/sessionRegistry.test.ts +138 -0
  132. package/src/chat/server/sessionRegistry.ts +1064 -0
  133. package/src/chat/server/test-utils/mockFactories.ts +422 -0
  134. package/src/chat/server/tools.ts +265 -0
  135. package/src/chat/utils/agentSessionMap.ts +76 -0
  136. package/src/chat/utils/approvalManager.ts +111 -0
  137. package/src/{utils → chat/utils}/asyncLock.ts +3 -3
  138. package/src/chat/{asyncQueue.ts → utils/asyncQueue.ts} +14 -3
  139. package/src/chat/utils/htmlToText.ts +61 -0
  140. package/src/chat/utils/multiAsyncQueue.ts +52 -0
  141. package/src/chat/utils/search.ts +139 -0
  142. package/src/chat/utils/userResolver.ts +48 -0
  143. package/src/chat/{websocket.ts → utils/websocket.ts} +2 -0
  144. package/src/test/agent.test.ts +487 -0
  145. package/src/test/approvalManager.test.ts +73 -0
  146. package/src/test/chatContextManager.test.ts +521 -0
  147. package/src/test/clientServerConnection.test.ts +207 -0
  148. package/src/test/compressingContextManager.test.ts +82 -0
  149. package/src/test/context.test.ts +105 -0
  150. package/src/test/conversation.test.ts +109 -0
  151. package/src/test/db.test.ts +351 -103
  152. package/src/test/dbMcpServerConfigs.test.ts +112 -0
  153. package/src/test/dbTestTools.ts +153 -0
  154. package/src/test/imageLoad.test.ts +7 -6
  155. package/src/test/mcpServerManager.test.ts +19 -14
  156. package/src/test/multiAsyncQueue.test.ts +125 -0
  157. package/src/test/openaiStreaming.test.ts +11 -10
  158. package/src/test/prompt.test.ts +4 -3
  159. package/src/test/promptProvider.test.ts +33 -0
  160. package/src/test/responseHandler.test.ts +78 -0
  161. package/src/test/sudoMcpServerManager.test.ts +22 -15
  162. package/src/test/testTools.ts +146 -0
  163. package/src/test/tools.test.ts +39 -0
  164. package/src/tool/agentChat.ts +26 -12
  165. package/src/tool/agentMain.ts +1 -1
  166. package/src/tool/chatMain.ts +283 -100
  167. package/src/tool/commandPrompt.ts +25 -9
  168. package/src/tool/files.ts +25 -19
  169. package/src/tool/nodePlatform.ts +52 -3
  170. package/src/tool/options.ts +4 -2
  171. package/src/tool/prompt.ts +22 -15
  172. package/test_data/dummyllm_script_crash.json +32 -0
  173. package/test_data/frog.png.b64 +1 -0
  174. package/vitest.config.ts +39 -0
  175. package/dist/agent/src/chat/client.js +0 -310
  176. package/dist/agent/src/chat/conversationManager.js +0 -502
  177. package/dist/agent/src/chat/db.js +0 -218
  178. package/dist/agent/src/chat/messages.js +0 -29
  179. package/dist/agent/src/chat/server.js +0 -158
  180. package/src/chat/client.ts +0 -445
  181. package/src/chat/conversationManager.ts +0 -730
  182. package/src/chat/db.ts +0 -304
  183. package/src/chat/messages.ts +0 -266
  184. package/src/chat/server.ts +0 -177
  185. /package/{frog.png → test_data/frog.png} +0 -0
@@ -1,13 +1,51 @@
1
1
  import * as dotenv from "dotenv";
2
2
  import { OpenAI } from "openai";
3
3
  import { McpServerManager } from "./mcpServerManager";
4
- import { ChatCompletionContentPart } from "openai/resources.mjs";
5
4
  import { strict as assert } from "assert";
6
5
  import { ILLM } from "./llm";
7
6
  import { AgentProfile, getLogger } from "@xalia/xmcp/sdk";
7
+ import { IAgentEventHandler } from "./iAgentEventHandler";
8
+ import { IContextManager } from "./context";
8
9
  export { AgentProfile } from "@xalia/xmcp/sdk";
9
10
 
10
- export type ToolHandler = (args: unknown) => string;
11
+ const MAX_TOOL_CALL_RESPONSE_LENGTH = 4000;
12
+
13
+ export interface IAgentToolProvider {
14
+ /**
15
+ * Any initial setup to be performed by the tool (loading data, etc). This
16
+ * function is responsible for registering any tools that this provider
17
+ * exposes.
18
+ */
19
+ setup(agent: Agent): Promise<void>;
20
+ }
21
+
22
+ export type ToolCallResult = {
23
+ /**
24
+ * Response to pass to the LLM
25
+ */
26
+ response: string;
27
+
28
+ /**
29
+ * If set, `response` is used in the next round of the Agent loop (if any),
30
+ * but `overwriteResponse` is passed to the ContextManager to be stored, and
31
+ * to be used for future LLM invocations.
32
+ */
33
+ overwriteResponse?: string;
34
+
35
+ /**
36
+ * If set, the arguments to this tool all, as stored in the context and used
37
+ * for future LLM invocations, will be overwritten by this value. This is
38
+ * intended for the case where the LLM generates a large amount of data to
39
+ * be saved which we do not want to appear in the context at every
40
+ * iteration.
41
+ */
42
+ overwriteArgs?: string;
43
+ };
44
+
45
+ export type ToolHandler = (
46
+ agent: Agent,
47
+ args: unknown
48
+ ) => Promise<ToolCallResult>;
11
49
 
12
50
  export type McpServerUrls = (name: string) => string;
13
51
 
@@ -22,69 +60,64 @@ export type ChatCompletionAssistantMessageParam =
22
60
  export type ChatCompletionUserMessageParam =
23
61
  OpenAI.ChatCompletionUserMessageParam;
24
62
 
25
- // Role: If content, give it to UI
26
- export type OnMessageCB = {
27
- (msg: string, msgEnd: boolean): Promise<void>;
28
- };
63
+ export type ChatCompletionToolMessageParam =
64
+ OpenAI.ChatCompletionToolMessageParam;
29
65
 
30
- // Role: If tool calls, prompt for permission to handle them
31
- export type OnToolCallCB = {
32
- (msg: ChatCompletionMessageToolCall): Promise<boolean>;
33
- };
66
+ dotenv.config();
67
+ const logger = getLogger();
34
68
 
35
69
  export interface IConversation {
36
70
  userMessage(msg?: string, imageB64?: string): void;
37
71
  getConversation(): ChatCompletionMessageParam[];
38
72
 
39
- // TODO: remove?
40
- resetConversation(): void;
41
-
42
73
  getAgentProfile(): AgentProfile;
43
- getSystemPrompt(): string;
74
+
44
75
  setSystemPrompt(systemPrompt: string): void;
45
- getModel(): string;
46
76
  setModel(model: string): void;
47
77
 
48
78
  shutdown(): Promise<void>;
49
79
  }
50
80
 
51
- dotenv.config();
52
- const logger = getLogger();
81
+ type RegisteredTools = {
82
+ handler: ToolHandler;
83
+ };
53
84
 
54
85
  export class Agent implements IConversation {
55
- private toolHandlers: { [toolName: string]: ToolHandler } = {};
86
+ private eventHandler: IAgentEventHandler;
87
+ private mcpServerManager: McpServerManager;
88
+ private llm: ILLM;
89
+ private contextManager: IContextManager;
90
+
91
+ /// The full list of tools, ready to pass to the LLM
92
+ private tools: OpenAI.ChatCompletionTool[] = [];
93
+
94
+ /// Handlers for "agent" (or "built-in") tools. These do not require
95
+ /// approval from the user.
96
+ private agentTools = new Map<string, RegisteredTools>();
56
97
 
57
98
  private constructor(
58
- public onMessage: OnMessageCB,
59
- public onToolCall: OnToolCallCB,
60
- private messages: ChatCompletionMessageParam[],
61
- private mcpServerManager: McpServerManager,
62
- private tools: OpenAI.ChatCompletionTool[],
63
- private llm: ILLM
64
- ) {}
65
-
66
- public static async initializeWithLLM(
67
- onMessage: OnMessageCB,
68
- onToolCall: OnToolCallCB,
69
- systemPrompt: string | undefined,
99
+ eventHandler: IAgentEventHandler,
100
+ mcpServerManager: McpServerManager,
70
101
  llm: ILLM,
71
- mcpServerManager?: McpServerManager
72
- ): Promise<Agent> {
73
- // Initialize messages with system prompt
74
- const messages = [
75
- {
76
- role: "system",
77
- content: systemPrompt ?? "You are a helpful assistant",
78
- } as OpenAI.ChatCompletionMessageParam,
79
- ];
102
+ contextManager: IContextManager
103
+ ) {
104
+ this.eventHandler = eventHandler;
105
+ this.mcpServerManager = mcpServerManager;
106
+ this.llm = llm;
107
+ this.contextManager = contextManager;
108
+ }
80
109
 
110
+ public static initializeWithLLM(
111
+ eventHandler: IAgentEventHandler,
112
+ llm: ILLM,
113
+ contextManager: IContextManager,
114
+ mcpServerManager?: McpServerManager
115
+ ): Agent {
81
116
  return new Agent(
82
- onMessage,
83
- onToolCall,
84
- messages,
117
+ eventHandler,
85
118
  mcpServerManager ?? new McpServerManager(),
86
- [],
87
- llm
119
+ llm,
120
+ contextManager
88
121
  );
89
122
  }
90
123
 
@@ -101,24 +134,12 @@ export class Agent implements IConversation {
101
134
  }
102
135
 
103
136
  public getConversation(): ChatCompletionMessageParam[] {
137
+ const llmMessages = this.contextManager.getLLMContext();
104
138
  assert(
105
- this.messages[0].role == "system",
139
+ llmMessages[0].role === "system",
106
140
  "first message must have system role"
107
141
  );
108
- // Return a copy so future modifications to `this.messages` don't impact
109
- // the callers copy.
110
- return structuredClone(this.messages.slice(1));
111
- }
112
-
113
- public setConversation(messages: ChatCompletionMessageParam[]) {
114
- assert(this.messages[0].role == "system");
115
- assert(
116
- messages.length === 0 || messages[0].role != "system",
117
- "conversation contains system msg"
118
- );
119
-
120
- const newMessages: ChatCompletionMessageParam[] = [this.messages[0]];
121
- this.messages = newMessages.concat(structuredClone(messages));
142
+ return [...llmMessages.slice(1)];
122
143
  }
123
144
 
124
145
  public getMcpServerManager(): McpServerManager {
@@ -143,49 +164,103 @@ export class Agent implements IConversation {
143
164
  public async userMessageRaw(
144
165
  userMessage: ChatCompletionUserMessageParam
145
166
  ): Promise<ChatCompletionMessageParam | undefined> {
146
- this.messages.push(userMessage);
147
- let completion = await this.chatCompletion();
167
+ return this.userMessagesRaw([userMessage]);
168
+ }
169
+
170
+ public async userMessagesRaw(
171
+ userMessages: ChatCompletionUserMessageParam[]
172
+ ): Promise<ChatCompletionMessageParam | undefined> {
173
+ // Note: `getLLMContext` returns a copy to we can mutate this array
174
+ const context = this.contextManager.getLLMContext();
175
+ const newMessagesIdx = context.length;
176
+
177
+ // Add the new user messages
178
+ context.push(...userMessages);
148
179
 
180
+ let completion = await this.chatCompletion(context);
149
181
  let message = completion.choices[0].message;
150
- this.messages.push(message);
182
+ context.push(message);
151
183
 
152
- // While there are tool calls to make, make them and loop
184
+ // While there are tool calls to make, invoke them and loop
153
185
 
154
186
  while (message.tool_calls && message.tool_calls.length > 0) {
187
+ // TODO: Execute all tool calls in parallel
188
+
189
+ // [indexInContext, ToolCallResult][]
190
+ const toolCallResults: [number, ToolCallResult][] = [];
155
191
  for (const toolCall of message.tool_calls ?? []) {
156
- const approval = await this.onToolCall(toolCall);
157
- if (approval) {
158
- try {
159
- const result = await this.doToolCall(toolCall);
160
- logger.debug(`tool call result ${JSON.stringify(result)}`);
161
- this.messages.push(result);
162
- } catch (e) {
163
- logger.error(`tool call error: ${e}`);
164
- this.messages.push({
165
- role: "tool",
166
- tool_call_id: toolCall.id,
167
- content: "Tool call failed.",
168
- });
169
- }
170
- } else {
171
- this.messages.push({
172
- role: "tool",
173
- tool_call_id: toolCall.id,
174
- content: "User denied tool use request.",
175
- });
192
+ // Execute the tool call, add the result to the context as an LLM
193
+ // mesage, and record the index of the message alongside the result in
194
+ // `toolCallResults`.
195
+
196
+ const result = await this.doToolCall(toolCall);
197
+ toolCallResults.push([context.length, result]);
198
+ context.push({
199
+ role: "tool",
200
+ tool_call_id: toolCall.id,
201
+ content: result.response,
202
+ });
203
+
204
+ // If the tool call requested that its args be redacted, this can be
205
+ // done now - before the next LLM invocation.
206
+
207
+ if (result.overwriteArgs) {
208
+ logger.debug(
209
+ `updating args for toolcall ${toolCall.id}: ${result.overwriteArgs}`
210
+ );
211
+ toolCall.function.arguments = result.overwriteArgs;
212
+ logger.debug(`agent message after update ${JSON.stringify(message)}`);
176
213
  }
177
214
  }
178
215
 
179
- completion = await this.chatCompletion();
180
- message = completion.choices[0].message;
181
- this.messages.push(message);
216
+ // Now that any args have been overwritten, signal the event handler of
217
+ // the prevoius completion.
218
+
219
+ this.eventHandler.onCompletion(message);
220
+
221
+ // Get a new completion using the untouched tool call results. Note
222
+ // that, since we are deferring the `onToolCallResult` calls (so they
223
+ // can be redacted), we must take care that the errors in
224
+ // `chatCompletion` do not disrupt this, so the caller has a consistent
225
+ // view of the conversation state.
226
+
227
+ try {
228
+ completion = await this.chatCompletion(context); // CAN THROW
229
+ message = completion.choices[0].message;
230
+ context.push(message);
231
+ } finally {
232
+ // Now that the tool call results have been passed to the LLM, perform
233
+ // any updates on them. Pass the (updated) tool-call-result LLM
234
+ // messages to the event handler - note, we want to do this even if
235
+ // the an error occured, so that the caller has an up-to-date picture
236
+ // of the context state when the error occured.
237
+
238
+ toolCallResults.forEach(([indexInContext, tcr]) => {
239
+ const ctxMsg = context[indexInContext];
240
+ if (tcr.overwriteResponse) {
241
+ ctxMsg.content = tcr.overwriteResponse;
242
+ }
243
+
244
+ assert(ctxMsg.role === "tool");
245
+ this.eventHandler.onToolCallResult(ctxMsg);
246
+ });
247
+
248
+ // Note, if an error DID occur, the ContextManager does not see any of
249
+ // the new context.
250
+ }
182
251
  }
183
252
 
253
+ // Signal the event handler of the final completion.
254
+ this.eventHandler.onCompletion(message);
255
+
256
+ // Add all new new messages to the context
257
+ this.contextManager.addMessages(context.slice(newMessagesIdx));
258
+
184
259
  return completion.choices[0].message;
185
260
  }
186
261
 
187
262
  public userMessage(msg?: string, imageB64?: string): void {
188
- this.userMessageEx(msg, imageB64);
263
+ void this.userMessageEx(msg, imageB64);
189
264
  }
190
265
 
191
266
  public getModel(): string {
@@ -197,85 +272,135 @@ export class Agent implements IConversation {
197
272
  this.llm.setModel(model);
198
273
  }
199
274
 
200
- /**
201
- * Clear the conversation.
202
- */
203
- public resetConversation() {
204
- assert(this.messages.length > 0);
205
- // Keep only the system message
206
- this.messages.splice(1);
207
- }
208
-
209
275
  public getSystemPrompt(): string {
210
- assert(this.messages[0].role === "system");
211
- return this.messages[0].content as string;
276
+ return this.contextManager.getAgentPrompt();
212
277
  }
213
278
 
214
279
  /**
215
280
  * Set the system prompt
216
281
  */
217
282
  public setSystemPrompt(systemMsg: string) {
218
- assert(this.messages[0].role === "system");
219
- this.messages[0].content = systemMsg;
283
+ this.contextManager.setAgentPrompt(systemMsg);
220
284
  }
221
285
 
222
- async chatCompletion(): Promise<OpenAI.Chat.Completions.ChatCompletion> {
286
+ async chatCompletion(
287
+ context: ChatCompletionMessageParam[]
288
+ ): Promise<OpenAI.Chat.Completions.ChatCompletion> {
289
+ // Compute the full list of available tools
290
+
223
291
  let tools: OpenAI.ChatCompletionTool[] | undefined;
224
- const enabledTools = this.tools.concat(
225
- this.mcpServerManager.getOpenAITools()
226
- );
292
+ const mcpTools = this.mcpServerManager.getOpenAITools();
293
+ const enabledTools = this.tools.concat(mcpTools);
227
294
  if (enabledTools.length > 0) {
228
295
  tools = enabledTools;
229
296
  }
230
- // logger.debug(
231
- // `chatCompletion: tools: ${JSON.stringify(tools, undefined, 2)}`
232
- // );
233
297
  const completion = await this.llm.getConversationResponse(
234
- this.messages,
298
+ context,
235
299
  tools,
236
- this.onMessage
300
+ this.eventHandler.onAgentMessage.bind(this.eventHandler)
237
301
  );
238
302
  logger.debug(`Received chat completion ${JSON.stringify(completion)}`);
239
303
  return completion;
240
304
  }
241
305
 
242
- public toolNames(): string[] {
243
- return this.mcpServerManager
244
- .getOpenAITools()
245
- .map((tool) => tool.function.name);
306
+ public addAgentToolProvider(toolProvider: IAgentToolProvider): Promise<void> {
307
+ return toolProvider.setup(this);
246
308
  }
247
309
 
248
- public addTool(tool: OpenAI.ChatCompletionTool, handler: ToolHandler) {
310
+ public addAgentTool(tool: OpenAI.ChatCompletionTool, handler: ToolHandler) {
249
311
  const name = tool.function.name;
250
- if (this.toolHandlers[name]) {
251
- throw `tool ${name} already added`;
312
+ if (this.agentTools.has(name)) {
313
+ throw new Error(`tool ${name} already added`);
252
314
  }
253
315
 
254
316
  logger.debug(`Adding tool ${name}`);
255
317
 
256
318
  this.tools.push(tool);
257
- this.toolHandlers[name] = handler;
319
+ this.agentTools.set(name, { handler });
320
+ }
321
+
322
+ public removeAgentTool(name: string) {
323
+ if (!this.agentTools.has(name)) {
324
+ logger.warn(`[removeTool] tool ${name} not present`);
325
+ }
326
+
327
+ // Find idx of the tool in the list
328
+ const idx = (() => {
329
+ let idx = 0;
330
+ while (idx < this.tools.length) {
331
+ if (this.tools[idx].function.name === name) {
332
+ return idx;
333
+ }
334
+ idx++;
335
+ }
336
+ return -1;
337
+ })();
338
+ assert(idx > -1);
339
+
340
+ // Remove entries
341
+ this.tools.splice(idx, 1);
342
+ this.agentTools.delete(name);
258
343
  }
259
344
 
260
- async doToolCall(
345
+ /**
346
+ * Handle the details of getting approval (if required), invoking the tool
347
+ * handler, informing the IAgentEventHandler of the result, and returns the
348
+ * OpenAI.ChatCompletionToolMessageParam to be used in the conversation.
349
+ */
350
+ private async doToolCall(
261
351
  toolCall: ChatCompletionMessageToolCall
262
- ): Promise<OpenAI.ChatCompletionToolMessageParam> {
263
- const name = toolCall.function.name;
264
- const args = JSON.parse(toolCall.function.arguments);
265
-
266
- let result: string | undefined = undefined;
267
- const handler = this.toolHandlers[name];
268
- if (handler) {
269
- logger.debug(` found agent tool ${name} ...`);
270
- result = handler(args);
271
- } else {
272
- result = await this.mcpServerManager.invoke(name, args);
352
+ ): Promise<ToolCallResult> {
353
+ // If the tool is and "agent" (internal) tool, we can just execute it.
354
+ // Otherwise, call the event handler to get permission and invoke the
355
+ // external tool handler.
356
+
357
+ let result: ToolCallResult;
358
+ try {
359
+ const toolName = toolCall.function.name;
360
+ const agentTool = this.agentTools.get(toolName);
361
+ const isAgentTool = !!agentTool;
362
+ const approve = await this.eventHandler.onToolCall(toolCall, isAgentTool);
363
+ if (!approve) {
364
+ result = { response: "User denied tool request." };
365
+ } else if (isAgentTool) {
366
+ // Internal (agent) tool
367
+ const args: unknown = JSON.parse(toolCall.function.arguments);
368
+ result = await agentTool.handler(this, args);
369
+ } else {
370
+ // McpServer tool call (agentTool === undefined)
371
+ const args: unknown = JSON.parse(toolCall.function.arguments);
372
+ result = {
373
+ response: await this.mcpServerManager.invoke(toolName, args),
374
+ };
375
+ logger.debug(`tool call result ${JSON.stringify(result)}`);
376
+ }
377
+ } catch (e) {
378
+ let msg: string;
379
+ if (e instanceof Error) {
380
+ msg = e.message;
381
+ } else if (typeof e === "string") {
382
+ msg = e;
383
+ } else {
384
+ msg = String(e);
385
+ }
386
+ logger.error(`tool call error: ${msg}`);
387
+ result = {
388
+ response: `tool call error: ${msg}`,
389
+ };
390
+ }
391
+
392
+ // Final sanity check on the tool call response length.
393
+ if (result.response.length > MAX_TOOL_CALL_RESPONSE_LENGTH) {
394
+ logger.warn(
395
+ "[Agent.doToolCall]: truncating tool call result.response for call:\n" +
396
+ JSON.stringify(toolCall)
397
+ );
398
+ result.response =
399
+ result.response.slice(0, MAX_TOOL_CALL_RESPONSE_LENGTH) +
400
+ " ..truncated";
273
401
  }
274
- return {
275
- role: "tool",
276
- tool_call_id: toolCall.id,
277
- content: result.toString(),
278
- };
402
+
403
+ return result;
279
404
  }
280
405
  }
281
406
 
@@ -297,7 +422,7 @@ export function createUserMessage(
297
422
  return msg;
298
423
  }
299
424
 
300
- const content: ChatCompletionContentPart[] = [];
425
+ const content: OpenAI.ChatCompletionContentPart[] = [];
301
426
  if (msg) {
302
427
  content.push({
303
428
  type: "text",
@@ -325,3 +450,13 @@ export function createUserMessage(
325
450
  name,
326
451
  };
327
452
  }
453
+
454
+ export function createUserMessageEnsure(
455
+ msg?: string,
456
+ imageB64?: string,
457
+ name?: string
458
+ ): ChatCompletionUserMessageParam {
459
+ const userMsg = createUserMessage(msg, imageB64, name);
460
+ assert(userMsg);
461
+ return userMsg;
462
+ }