@xalia/agent 0.5.7 → 0.6.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.
Files changed (186) hide show
  1. package/README.md +23 -8
  2. package/dist/agent/src/agent/agent.js +176 -96
  3. package/dist/agent/src/agent/agentUtils.js +82 -59
  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/mcpServerManager.js +23 -24
  8. package/dist/agent/src/agent/nullAgentEventHandler.js +21 -0
  9. package/dist/agent/src/agent/nullPlatform.js +14 -0
  10. package/dist/agent/src/agent/openAILLMStreaming.js +26 -14
  11. package/dist/agent/src/agent/promptProvider.js +63 -0
  12. package/dist/agent/src/agent/repeatLLM.js +5 -5
  13. package/dist/agent/src/agent/sudoMcpServerManager.js +23 -21
  14. package/dist/agent/src/agent/tokenAuth.js +7 -7
  15. package/dist/agent/src/agent/tools.js +1 -1
  16. package/dist/agent/src/chat/client/chatClient.js +733 -0
  17. package/dist/agent/src/chat/client/connection.js +209 -0
  18. package/dist/agent/src/chat/client/connection.test.js +188 -0
  19. package/dist/agent/src/chat/client/constants.js +5 -0
  20. package/dist/agent/src/chat/client/index.js +15 -0
  21. package/dist/agent/src/chat/client/interfaces.js +2 -0
  22. package/dist/agent/src/chat/client/responseHandler.js +105 -0
  23. package/dist/agent/src/chat/client/sessionClient.js +331 -0
  24. package/dist/agent/src/chat/client/teamManager.js +2 -0
  25. package/dist/agent/src/chat/{apiKeyManager.js → data/apiKeyManager.js} +4 -0
  26. package/dist/agent/src/chat/data/dataModels.js +2 -0
  27. package/dist/agent/src/chat/data/database.js +749 -0
  28. package/dist/agent/src/chat/data/dbMcpServerConfigs.js +47 -0
  29. package/dist/agent/src/chat/protocol/connectionMessages.js +5 -0
  30. package/dist/agent/src/chat/protocol/constants.js +50 -0
  31. package/dist/agent/src/chat/protocol/errors.js +22 -0
  32. package/dist/agent/src/chat/protocol/messages.js +110 -0
  33. package/dist/agent/src/chat/server/chatContextManager.js +405 -0
  34. package/dist/agent/src/chat/server/connectionManager.js +352 -0
  35. package/dist/agent/src/chat/server/connectionManager.test.js +159 -0
  36. package/dist/agent/src/chat/server/conversation.js +198 -0
  37. package/dist/agent/src/chat/server/errorUtils.js +23 -0
  38. package/dist/agent/src/chat/server/openSession.js +869 -0
  39. package/dist/agent/src/chat/server/server.js +177 -0
  40. package/dist/agent/src/chat/server/sessionFileManager.js +161 -0
  41. package/dist/agent/src/chat/server/sessionRegistry.js +700 -0
  42. package/dist/agent/src/chat/server/sessionRegistry.test.js +97 -0
  43. package/dist/agent/src/chat/server/test-utils/mockFactories.js +307 -0
  44. package/dist/agent/src/chat/server/tools.js +243 -0
  45. package/dist/agent/src/chat/utils/agentSessionMap.js +66 -0
  46. package/dist/agent/src/chat/utils/approvalManager.js +85 -0
  47. package/dist/agent/src/{utils → chat/utils}/asyncLock.js +3 -3
  48. package/dist/agent/src/chat/{asyncQueue.js → utils/asyncQueue.js} +12 -2
  49. package/dist/agent/src/chat/utils/htmlToText.js +84 -0
  50. package/dist/agent/src/chat/utils/multiAsyncQueue.js +42 -0
  51. package/dist/agent/src/chat/utils/search.js +145 -0
  52. package/dist/agent/src/chat/utils/userResolver.js +46 -0
  53. package/dist/agent/src/chat/utils/websocket.js +16 -0
  54. package/dist/agent/src/test/agent.test.js +332 -0
  55. package/dist/agent/src/test/approvalManager.test.js +58 -0
  56. package/dist/agent/src/test/chatContextManager.test.js +392 -0
  57. package/dist/agent/src/test/clientServerConnection.test.js +158 -0
  58. package/dist/agent/src/test/compressingContextManager.test.js +65 -0
  59. package/dist/agent/src/test/context.test.js +83 -0
  60. package/dist/agent/src/test/conversation.test.js +89 -0
  61. package/dist/agent/src/test/db.test.js +271 -83
  62. package/dist/agent/src/test/dbMcpServerConfigs.test.js +72 -0
  63. package/dist/agent/src/test/dbTestTools.js +99 -0
  64. package/dist/agent/src/test/imageLoad.test.js +8 -7
  65. package/dist/agent/src/test/mcpServerManager.test.js +23 -20
  66. package/dist/agent/src/test/multiAsyncQueue.test.js +101 -0
  67. package/dist/agent/src/test/openaiStreaming.test.js +64 -35
  68. package/dist/agent/src/test/prompt.test.js +5 -4
  69. package/dist/agent/src/test/promptProvider.test.js +28 -0
  70. package/dist/agent/src/test/responseHandler.test.js +61 -0
  71. package/dist/agent/src/test/sudoMcpServerManager.test.js +24 -25
  72. package/dist/agent/src/test/testTools.js +109 -0
  73. package/dist/agent/src/test/tools.test.js +31 -0
  74. package/dist/agent/src/tool/agentChat.js +21 -10
  75. package/dist/agent/src/tool/agentMain.js +1 -1
  76. package/dist/agent/src/tool/chatMain.js +241 -58
  77. package/dist/agent/src/tool/commandPrompt.js +22 -17
  78. package/dist/agent/src/tool/files.js +20 -16
  79. package/dist/agent/src/tool/nodePlatform.js +47 -3
  80. package/dist/agent/src/tool/options.js +4 -4
  81. package/dist/agent/src/tool/prompt.js +19 -13
  82. package/eslint.config.mjs +14 -1
  83. package/package.json +14 -6
  84. package/scripts/chat_server +8 -0
  85. package/scripts/setup_chat +7 -2
  86. package/scripts/shutdown_chat_server +3 -0
  87. package/scripts/test_chat +135 -17
  88. package/src/agent/agent.ts +283 -138
  89. package/src/agent/agentUtils.ts +143 -108
  90. package/src/agent/compressingContextManager.ts +164 -0
  91. package/src/agent/context.ts +268 -0
  92. package/src/agent/dummyLLM.ts +76 -8
  93. package/src/agent/iAgentEventHandler.ts +54 -0
  94. package/src/agent/iplatform.ts +1 -0
  95. package/src/agent/mcpServerManager.ts +35 -31
  96. package/src/agent/nullAgentEventHandler.ts +20 -0
  97. package/src/agent/nullPlatform.ts +13 -0
  98. package/src/agent/openAILLMStreaming.ts +26 -13
  99. package/src/agent/promptProvider.ts +87 -0
  100. package/src/agent/repeatLLM.ts +5 -5
  101. package/src/agent/sudoMcpServerManager.ts +30 -29
  102. package/src/agent/tokenAuth.ts +7 -7
  103. package/src/agent/tools.ts +3 -1
  104. package/src/chat/client/chatClient.ts +900 -0
  105. package/src/chat/client/connection.test.ts +241 -0
  106. package/src/chat/client/connection.ts +276 -0
  107. package/src/chat/client/constants.ts +3 -0
  108. package/src/chat/client/index.ts +18 -0
  109. package/src/chat/client/interfaces.ts +34 -0
  110. package/src/chat/client/responseHandler.ts +131 -0
  111. package/src/chat/client/sessionClient.ts +443 -0
  112. package/src/chat/client/teamManager.ts +29 -0
  113. package/src/chat/{apiKeyManager.ts → data/apiKeyManager.ts} +6 -2
  114. package/src/chat/data/dataModels.ts +85 -0
  115. package/src/chat/data/database.ts +982 -0
  116. package/src/chat/data/dbMcpServerConfigs.ts +59 -0
  117. package/src/chat/protocol/connectionMessages.ts +49 -0
  118. package/src/chat/protocol/constants.ts +55 -0
  119. package/src/chat/protocol/errors.ts +16 -0
  120. package/src/chat/protocol/messages.ts +682 -0
  121. package/src/chat/server/README.md +127 -0
  122. package/src/chat/server/chatContextManager.ts +612 -0
  123. package/src/chat/server/connectionManager.test.ts +266 -0
  124. package/src/chat/server/connectionManager.ts +541 -0
  125. package/src/chat/server/conversation.ts +269 -0
  126. package/src/chat/server/errorUtils.ts +28 -0
  127. package/src/chat/server/openSession.ts +1332 -0
  128. package/src/chat/server/server.ts +177 -0
  129. package/src/chat/server/sessionFileManager.ts +239 -0
  130. package/src/chat/server/sessionRegistry.test.ts +138 -0
  131. package/src/chat/server/sessionRegistry.ts +1064 -0
  132. package/src/chat/server/test-utils/mockFactories.ts +422 -0
  133. package/src/chat/server/tools.ts +265 -0
  134. package/src/chat/utils/agentSessionMap.ts +76 -0
  135. package/src/chat/utils/approvalManager.ts +111 -0
  136. package/src/{utils → chat/utils}/asyncLock.ts +3 -3
  137. package/src/chat/{asyncQueue.ts → utils/asyncQueue.ts} +14 -3
  138. package/src/chat/utils/htmlToText.ts +61 -0
  139. package/src/chat/utils/multiAsyncQueue.ts +52 -0
  140. package/src/chat/utils/search.ts +139 -0
  141. package/src/chat/utils/userResolver.ts +48 -0
  142. package/src/chat/utils/websocket.ts +16 -0
  143. package/src/test/agent.test.ts +487 -0
  144. package/src/test/approvalManager.test.ts +73 -0
  145. package/src/test/chatContextManager.test.ts +521 -0
  146. package/src/test/clientServerConnection.test.ts +207 -0
  147. package/src/test/compressingContextManager.test.ts +82 -0
  148. package/src/test/context.test.ts +105 -0
  149. package/src/test/conversation.test.ts +109 -0
  150. package/src/test/db.test.ts +358 -89
  151. package/src/test/dbMcpServerConfigs.test.ts +112 -0
  152. package/src/test/dbTestTools.ts +153 -0
  153. package/src/test/imageLoad.test.ts +7 -6
  154. package/src/test/mcpServerManager.test.ts +21 -16
  155. package/src/test/multiAsyncQueue.test.ts +125 -0
  156. package/src/test/openaiStreaming.test.ts +71 -36
  157. package/src/test/prompt.test.ts +4 -3
  158. package/src/test/promptProvider.test.ts +33 -0
  159. package/src/test/responseHandler.test.ts +78 -0
  160. package/src/test/sudoMcpServerManager.test.ts +32 -30
  161. package/src/test/testTools.ts +146 -0
  162. package/src/test/tools.test.ts +39 -0
  163. package/src/tool/agentChat.ts +26 -12
  164. package/src/tool/agentMain.ts +1 -1
  165. package/src/tool/chatMain.ts +292 -100
  166. package/src/tool/commandPrompt.ts +28 -19
  167. package/src/tool/files.ts +25 -19
  168. package/src/tool/nodePlatform.ts +52 -3
  169. package/src/tool/options.ts +4 -2
  170. package/src/tool/prompt.ts +22 -15
  171. package/test_data/dummyllm_script_crash.json +32 -0
  172. package/test_data/frog.png.b64 +1 -0
  173. package/vitest.config.ts +39 -0
  174. package/dist/agent/src/chat/client.js +0 -349
  175. package/dist/agent/src/chat/conversationManager.js +0 -392
  176. package/dist/agent/src/chat/db.js +0 -209
  177. package/dist/agent/src/chat/frontendClient.js +0 -74
  178. package/dist/agent/src/chat/server.js +0 -158
  179. package/src/chat/client.ts +0 -455
  180. package/src/chat/conversationManager.ts +0 -595
  181. package/src/chat/db.ts +0 -290
  182. package/src/chat/frontendClient.ts +0 -123
  183. package/src/chat/messages.ts +0 -235
  184. package/src/chat/server.ts +0 -177
  185. /package/dist/agent/src/{chat/messages.js → agent/iAgentEventHandler.js} +0 -0
  186. /package/{frog.png → test_data/frog.png} +0 -0
@@ -1,5 +1,6 @@
1
1
  import { getLogger } from "@xalia/xmcp/sdk";
2
- import { Agent, AgentProfile, OnMessageCB, OnToolCallCB } from "./agent";
2
+ import { Agent, AgentProfile } from "./agent";
3
+ import { IAgentEventHandler } from "./iAgentEventHandler";
3
4
  import { IPlatform } from "./iplatform";
4
5
  import { SkillManager } from "./sudoMcpServerManager";
5
6
  import OpenAI from "openai";
@@ -10,154 +11,159 @@ import { DummyLLM } from "./dummyLLM";
10
11
  import { ILLM } from "./llm";
11
12
  import { strict as assert } from "assert";
12
13
  import { RepeatLLM } from "./repeatLLM";
13
- import { McpServerManager } from "./mcpServerManager";
14
+ import { ContextManager, IContextManager } from "./context";
14
15
 
15
16
  const logger = getLogger();
16
17
 
17
18
  export const DEFAULT_LLM_URL = "http://localhost:5001/v1";
18
- export const DEFAULT_LLM_MODEL = "gpt-4o";
19
+ // uses openrouter
20
+ export const DEFAULT_LLM_MODEL =
21
+ process.env["DEFAULT_LLM_MODEL"] || "openai/gpt-4o";
19
22
 
20
23
  export const XALIA_APP_HEADER = {
21
24
  "HTTP-Referer": "xalia.ai",
22
25
  "X-Title": "Xalia",
23
26
  };
24
27
 
25
- /**
26
- * Util function to create an Agent from some config information.
27
- */
28
- async function createAgent(
29
- llmUrl: string | undefined,
30
- model: string | undefined,
31
- systemPrompt: string,
32
- onMessage: OnMessageCB,
33
- onToolCall: OnToolCallCB,
28
+ export async function createAgentWithoutSkills(
29
+ llmUrl: string,
30
+ agentProfile: AgentProfile,
31
+ eventHandler: IAgentEventHandler,
34
32
  platform: IPlatform,
35
- openaiApiKey: string | undefined,
36
- stream: boolean = false,
37
- mcpServerManager?: McpServerManager
38
- ): Promise<Agent> {
39
- let llm: ILLM | undefined;
40
-
41
- if (model && model.startsWith("dummy:")) {
42
- // Dummy Agent
43
- const llmUrl = model.slice(6);
44
- if (llmUrl.length === 0) {
45
- throw "malformed dummy:<script>";
46
- }
47
- const script = await platform.load(llmUrl);
48
- logger.debug(` script: ${script}`);
49
- const responses: OpenAI.ChatCompletion.Choice[] = JSON.parse(script);
50
- logger.debug(`Initializing Dummy Agent: ${llmUrl}`);
51
- llm = new DummyLLM(responses);
52
- } else if (model === "repeat") {
53
- llm = new RepeatLLM();
54
- } else {
55
- // Regular Agent
56
- if (!openaiApiKey) {
57
- throw "Missing OpenAI API Key";
58
- }
59
-
60
- logger.debug(`Initializing Agent: ${llmUrl} - ${model}`);
61
- if (stream) {
62
- llm = new OpenAILLMStreaming(openaiApiKey, llmUrl, model);
63
- } else {
64
- llm = new OpenAILLM(openaiApiKey, llmUrl, model);
65
- }
66
- }
33
+ contextManager: IContextManager,
34
+ llmApiKey: string | undefined,
35
+ sudomcpConfig: SudoMcpConfiguration,
36
+ authorizedUrl: string | undefined,
37
+ stream: boolean = false
38
+ ): Promise<[Agent, SkillManager]> {
39
+ // Init SudoMcpServerManager
40
+ logger.debug("[createAgentWithSkills] creating SudoMcpServerManager.");
41
+ const sudoMcpServerManager = await SkillManager.initialize(
42
+ (url: string, authResultP: Promise<boolean>, displayName: string) => {
43
+ platform.openUrl(url, authResultP, displayName);
44
+ },
45
+ sudomcpConfig.backend_url,
46
+ sudomcpConfig.api_key,
47
+ authorizedUrl
48
+ );
49
+ logger.debug(
50
+ "[createAgentWithoutSkills] restore mcp settings:" +
51
+ JSON.stringify(agentProfile.mcp_settings)
52
+ );
67
53
 
68
- assert(llm);
69
- return Agent.initializeWithLLM(
70
- onMessage,
71
- onToolCall,
72
- systemPrompt,
73
- llm,
74
- mcpServerManager
54
+ // Create agent using the event handler
55
+ const agent = await createAgentFromSkillManager(
56
+ llmUrl,
57
+ agentProfile,
58
+ eventHandler,
59
+ platform,
60
+ contextManager,
61
+ llmApiKey,
62
+ sudoMcpServerManager,
63
+ stream
75
64
  );
65
+
66
+ return [agent, sudoMcpServerManager];
76
67
  }
77
68
 
78
69
  /**
79
- * Util function to create and initialize an Agent given an AgentProfile.
70
+ * Create and initialize an Agent given an AgentProfile using the
71
+ * IAgentEventHandler interface. This is the preferred way to create
72
+ * agents.
80
73
  */
81
74
  export async function createAgentWithSkills(
82
75
  llmUrl: string,
83
76
  agentProfile: AgentProfile,
84
- onMessage: OnMessageCB,
85
- onToolCall: OnToolCallCB,
77
+ eventHandler: IAgentEventHandler,
86
78
  platform: IPlatform,
79
+ contextManager: IContextManager,
87
80
  llmApiKey: string | undefined,
88
81
  sudomcpConfig: SudoMcpConfiguration,
89
82
  authorizedUrl: string | undefined,
90
- conversation: OpenAI.ChatCompletionMessageParam[] | undefined,
91
83
  stream: boolean = false
92
84
  ): Promise<[Agent, SkillManager]> {
93
- // Create agent
94
- logger.debug("[createAgentAndSudoMcpServerManager] creating agent ...");
95
- const agent = await createAgent(
85
+ const [agent, sudoMcpServerManager] = await createAgentWithoutSkills(
96
86
  llmUrl,
97
- agentProfile.model,
98
- agentProfile.system_prompt,
99
- onMessage,
100
- onToolCall,
87
+ agentProfile,
88
+ eventHandler,
101
89
  platform,
90
+ contextManager,
102
91
  llmApiKey,
92
+ sudomcpConfig,
93
+ authorizedUrl,
103
94
  stream
104
95
  );
105
- if (conversation) {
106
- agent.setConversation(conversation);
107
- }
108
96
 
109
- // Init SudoMcpServerManager
110
- logger.debug("[createAgentWithSkills] creating SudoMcpServerManager.");
111
- const sudoMcpServerManager = await SkillManager.initialize(
112
- agent.getMcpServerManager(),
113
- platform.openUrl,
114
- sudomcpConfig.backend_url,
115
- sudomcpConfig.api_key,
116
- authorizedUrl
117
- );
118
- logger.debug(
119
- "[createAgentWithSkills] restore mcp settings:" +
120
- JSON.stringify(agentProfile.mcp_settings)
121
- );
97
+ logger.debug("[createAgentWithSkills] restoring skills");
122
98
  await sudoMcpServerManager.restoreMcpSettings(agentProfile.mcp_settings);
123
99
 
124
- logger.debug("[createAgentWithSkills] done");
125
100
  return [agent, sudoMcpServerManager];
126
101
  }
127
102
 
128
103
  export async function createAgentFromSkillManager(
129
104
  llmUrl: string,
130
105
  agentProfile: AgentProfile,
131
- onMessage: OnMessageCB,
132
- onToolCall: OnToolCallCB,
106
+ eventHandler: IAgentEventHandler,
133
107
  platform: IPlatform,
108
+ contextManager: IContextManager,
134
109
  llmApiKey: string | undefined,
135
110
  skillManager: SkillManager,
136
- conversation: OpenAI.ChatCompletionMessageParam[] | undefined,
137
111
  stream: boolean = false
138
112
  ): Promise<Agent> {
139
113
  // Create agent
140
- logger.debug("[createAgentAndSudoMcpServerManager] creating agent ...");
141
- const mcpServerManager = skillManager.getMcpServerManager();
142
- const agent = await createAgent(
114
+ logger.debug("[createAgentFromSkillManager] creating agent ...");
115
+ const llm = await createLLM(
143
116
  llmUrl,
144
- agentProfile.model,
145
- agentProfile.system_prompt,
146
- onMessage,
147
- onToolCall,
148
- platform,
149
117
  llmApiKey,
118
+ agentProfile.model,
150
119
  stream,
151
- mcpServerManager
120
+ platform
121
+ );
122
+ contextManager.setAgentPrompt(agentProfile.system_prompt);
123
+ const agent = Agent.initializeWithLLM(
124
+ eventHandler,
125
+ llm,
126
+ contextManager,
127
+ skillManager
152
128
  );
153
- if (conversation) {
154
- agent.setConversation(conversation);
155
- }
156
129
 
157
- logger.debug("[createAgentWithSkills] done");
130
+ logger.debug("[createAgentFromSkillManager] done");
158
131
  return agent;
159
132
  }
160
133
 
134
+ export async function createLLM(
135
+ llmUrl: string | undefined,
136
+ llmApiKey: string | undefined,
137
+ model: string | undefined,
138
+ stream: boolean = false,
139
+ platform: IPlatform
140
+ ): Promise<ILLM> {
141
+ let llm: ILLM | undefined;
142
+
143
+ if (model && model.startsWith("dummy:")) {
144
+ llm = await DummyLLM.initFromModelUrl(model, platform);
145
+ } else if (model === "repeat") {
146
+ llm = new RepeatLLM();
147
+ } else {
148
+ // Regular Agent
149
+ if (!llmApiKey) {
150
+ throw new Error("Missing OpenAI API Key");
151
+ }
152
+
153
+ logger.debug(
154
+ `Initializing Agent: ${llmUrl ?? "unknown"} - ${model ?? "unknown"}`
155
+ );
156
+ if (stream) {
157
+ llm = new OpenAILLMStreaming(llmApiKey, llmUrl, model);
158
+ } else {
159
+ llm = new OpenAILLM(llmApiKey, llmUrl, model);
160
+ }
161
+ }
162
+
163
+ assert(llm);
164
+ return llm;
165
+ }
166
+
161
167
  /**
162
168
  * An "non-interactive" agent is one which is not intended to be used
163
169
  * interactively (settings cannot be dyanmically adjusted, intermediate
@@ -174,25 +180,33 @@ export async function createNonInteractiveAgent(
174
180
  approveToolsUpTo: number
175
181
  ): Promise<Agent> {
176
182
  let remainingToolCalls = approveToolsUpTo;
177
- const onMessage = async () => {};
178
- const onToolCall = async () => {
179
- if (remainingToolCalls !== 0) {
180
- --remainingToolCalls;
181
- return true;
182
- }
183
- return false;
183
+ const eventHandler: IAgentEventHandler = {
184
+ onCompletion: () => {},
185
+ onAgentMessage: async () => {},
186
+ // eslint-disable-next-line @typescript-eslint/require-await
187
+ onToolCall: async () => {
188
+ if (remainingToolCalls !== 0) {
189
+ --remainingToolCalls;
190
+ return true;
191
+ }
192
+ return false;
193
+ },
194
+ onToolCallResult: () => {},
184
195
  };
185
196
 
197
+ const contextManager = new ContextManager(
198
+ agentProfile.system_prompt,
199
+ conversation || []
200
+ );
186
201
  const [agent, _] = await createAgentWithSkills(
187
202
  url,
188
203
  agentProfile,
189
- onMessage,
190
- onToolCall,
204
+ eventHandler,
191
205
  platform,
206
+ contextManager,
192
207
  openaiApiKey,
193
208
  sudomcpConfig,
194
- undefined,
195
- conversation
209
+ undefined
196
210
  );
197
211
 
198
212
  return agent;
@@ -236,11 +250,32 @@ export async function runOneShot(
236
250
  logger.debug("[runOneShot]: shutdown done");
237
251
 
238
252
  if (!response) {
239
- throw "No message returned from agent";
253
+ throw new Error("No message returned from agent");
254
+ }
255
+
256
+ // Handle different content types
257
+ let responseText = "";
258
+ if (typeof response.content === "string") {
259
+ responseText = response.content;
260
+ } else if (response.content === null || response.content === undefined) {
261
+ responseText = "";
262
+ } else if (Array.isArray(response.content)) {
263
+ // Handle array of content parts
264
+ responseText = response.content
265
+ .map((part) => {
266
+ if ("text" in part) {
267
+ return part.text;
268
+ }
269
+ return "";
270
+ })
271
+ .join("");
272
+ } else {
273
+ // Fallback for other types
274
+ responseText = String(response.content);
240
275
  }
241
276
 
242
277
  return {
243
- response: "" + response.content,
278
+ response: responseText,
244
279
  conversation: agent.getConversation(),
245
280
  };
246
281
  }
@@ -0,0 +1,164 @@
1
+ import { strict as assert } from "assert";
2
+ import { getLogger } from "@xalia/xmcp/sdk";
3
+
4
+ import {
5
+ Agent,
6
+ ChatCompletionUserMessageParam,
7
+ ChatCompletionMessageParam,
8
+ } from "./agent";
9
+ import { NULL_PLATFORM } from "./nullPlatform";
10
+ import { createLLM } from "./agentUtils";
11
+ import { ContextManager, ContextManagerWithCommit } from "./context";
12
+ import { NULL_AGENT_EVENT_HANDLER } from "./nullAgentEventHandler";
13
+
14
+ const logger = getLogger();
15
+
16
+ /**
17
+ * System prompt used to generate a conversation summary.
18
+ */
19
+ const COMPRESSION_SYSTEM_PROMPT =
20
+ // eslint-disable-next-line max-len
21
+ "You are a context summarizer, creating MINIMAL conversation digests which can be used to CONTINUE the conversation at a later date. TOKEN EFFICIENCY is a HIGH PRIORITY. Summaries will only be seen by LLMs and should USE ANY AND ALL ABBREVIATIONS to keep them as CONCISE as possible, while PRESERVING IMPORTANT DETAILS of the CONVERSATION.";
22
+
23
+ /**
24
+ * Text prepended to a summary to create the checkpoint message
25
+ */
26
+ const CHECKPOINT_MESSAGE_PREFIX =
27
+ // eslint-disable-next-line max-len
28
+ "We are continuing an earlier conversation. The remainder of this message is a summary of the conversation so far: ";
29
+
30
+ export function createCheckpointMessage(
31
+ summary: string
32
+ ): ChatCompletionUserMessageParam {
33
+ return {
34
+ role: "user",
35
+ content: CHECKPOINT_MESSAGE_PREFIX + summary,
36
+ };
37
+ }
38
+
39
+ export async function createCompressionAgent(
40
+ compressionAgentUrl: string,
41
+ compressionAgentModel: string,
42
+ compressionAgentApiKey: string
43
+ ): Promise<Agent> {
44
+ const llm = await createLLM(
45
+ compressionAgentUrl,
46
+ compressionAgentApiKey,
47
+ compressionAgentModel,
48
+ false /* stream */,
49
+ NULL_PLATFORM
50
+ );
51
+
52
+ return Agent.initializeWithLLM(
53
+ NULL_AGENT_EVENT_HANDLER,
54
+ llm,
55
+ new ContextManager(COMPRESSION_SYSTEM_PROMPT, [])
56
+ );
57
+ }
58
+
59
+ export async function createSummary(
60
+ compressionAgentUrl: string,
61
+ compressionAgentModel: string,
62
+ compressionAgentApiKey: string,
63
+ conversation: ChatCompletionMessageParam[]
64
+ ): Promise<string> {
65
+ const agent = await createCompressionAgent(
66
+ compressionAgentUrl,
67
+ compressionAgentModel,
68
+ compressionAgentApiKey
69
+ );
70
+
71
+ const resp = await agent.userMessageEx(JSON.stringify(conversation));
72
+ if (!resp) {
73
+ throw new Error("compression agent returned null");
74
+ }
75
+
76
+ assert(resp.role === "assistant");
77
+ assert(
78
+ typeof resp.content === "string",
79
+ "expected string content from compression agent"
80
+ );
81
+
82
+ return resp.content;
83
+ }
84
+
85
+ /**
86
+ * Can perform compression on the committed part of the context. Caller (not
87
+ * the Agent) is responsible for committing the conversation and triggering
88
+ * compression.
89
+ */
90
+ export class CompressingContextManager extends ContextManagerWithCommit {
91
+ compressionAgentUrl: string;
92
+ compressionAgentModel: string;
93
+ compressionAgentApiKey: string;
94
+ compressingMessages: number | undefined;
95
+
96
+ constructor(
97
+ systemPrompt: string,
98
+ messages: ChatCompletionMessageParam[],
99
+ compressionAgentUrl: string,
100
+ compressionAgentModel: string,
101
+ compressionAgentApiKey: string
102
+ ) {
103
+ super(systemPrompt, messages);
104
+ this.compressionAgentUrl = compressionAgentUrl;
105
+ this.compressionAgentModel = compressionAgentModel;
106
+ this.compressionAgentApiKey = compressionAgentApiKey;
107
+ this.compressingMessages = undefined;
108
+
109
+ // Sanity check the conversation form.
110
+ //
111
+ // Ordinarily, the committed context should end with an "assistant"
112
+ // message (i.e. user messages, an agent loop terminating in a final agent
113
+ // response). However, if a conversation has been compressed, we may have
114
+ // only the summary, which is a "user" message. In this case, this should
115
+ // be the only message.
116
+
117
+ const numMessages = this.numMessages();
118
+ const lastMessage = this.lastMessage();
119
+ if (lastMessage) {
120
+ const finalRole = lastMessage.role;
121
+ if (finalRole === "user") {
122
+ assert(numMessages === 1);
123
+ } else {
124
+ assert(finalRole === "assistant", `unexpected final role ${finalRole}`);
125
+ }
126
+ }
127
+ }
128
+
129
+ async compress(): Promise<string> {
130
+ // Only select messages for compression if they have been committed.
131
+ const numToCompress = this.getCommittedLength();
132
+ const messagesToCompress = this.leadingMessages(numToCompress);
133
+ assert(messagesToCompress.length === numToCompress);
134
+ this.compressingMessages = numToCompress;
135
+ assert(this.compressingMessages > 1, "<2 messages commited in the context");
136
+
137
+ logger.debug(
138
+ `[CompressingContextManager] start (${String(this.compressingMessages)})`
139
+ );
140
+
141
+ try {
142
+ const summary = await createSummary(
143
+ this.compressionAgentUrl,
144
+ this.compressionAgentModel,
145
+ this.compressionAgentApiKey,
146
+ messagesToCompress
147
+ );
148
+
149
+ logger.debug(`[CompressingContextManager] summary: ${summary}`);
150
+
151
+ // Replace the context `messages` and update `lastCommittedMessage`
152
+ // index.
153
+
154
+ const checkpointMessage = createCheckpointMessage(summary);
155
+ assert(typeof checkpointMessage.content === "string");
156
+ this.replaceLeadingMessages(numToCompress, checkpointMessage);
157
+
158
+ return summary;
159
+ } finally {
160
+ this.compressingMessages = undefined;
161
+ logger.debug(`[CompressingContextManager] compression done`);
162
+ }
163
+ }
164
+ }