@xalia/agent 0.6.9 → 0.6.10

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 (127) hide show
  1. package/.env.development +6 -1
  2. package/.env.test +7 -0
  3. package/README.md +11 -0
  4. package/context_system.md +498 -0
  5. package/dist/agent/src/agent/agent.js +77 -18
  6. package/dist/agent/src/agent/agentUtils.js +3 -2
  7. package/dist/agent/src/agent/documentSummarizer.js +126 -0
  8. package/dist/agent/src/agent/dummyLLM.js +25 -22
  9. package/dist/agent/src/agent/imageGenLLM.js +22 -19
  10. package/dist/agent/src/agent/llm.js +1 -1
  11. package/dist/agent/src/agent/openAILLM.js +15 -12
  12. package/dist/agent/src/agent/openAILLMStreaming.js +68 -37
  13. package/dist/agent/src/agent/repeatLLM.js +16 -7
  14. package/dist/agent/src/agent/tokenCounter.js +390 -0
  15. package/dist/agent/src/agent/tokenCounter.test.js +206 -0
  16. package/dist/agent/src/agent/toolSettings.js +17 -0
  17. package/dist/agent/src/agent/tools/calculatorTool.js +45 -0
  18. package/dist/agent/src/agent/tools/contentExtractors/pdfToText.js +55 -0
  19. package/dist/agent/src/agent/tools/datetimeTool.js +38 -0
  20. package/dist/agent/src/agent/tools/fileManager/fileManagerTool.js +156 -0
  21. package/dist/agent/src/agent/tools/fileManager/index.js +31 -0
  22. package/dist/agent/src/agent/tools/fileManager/memoryFileManager.js +102 -0
  23. package/dist/agent/src/{chat/data → agent/tools/fileManager}/mimeTypes.js +3 -1
  24. package/dist/agent/src/agent/tools/fileManager/prompt.js +33 -0
  25. package/dist/agent/src/{chat/data/dbSessionFileModels.js → agent/tools/fileManager/types.js} +7 -0
  26. package/dist/agent/src/agent/tools/index.js +64 -0
  27. package/dist/agent/src/agent/tools/openUrlTool.js +57 -0
  28. package/dist/agent/src/agent/tools/renderTool.js +89 -0
  29. package/dist/agent/src/agent/tools/utils.js +61 -0
  30. package/dist/agent/src/{chat/utils/search.js → agent/tools/webSearch.js} +1 -2
  31. package/dist/agent/src/agent/tools/webSearchTool.js +40 -0
  32. package/dist/agent/src/chat/client/chatClient.js +28 -0
  33. package/dist/agent/src/chat/client/index.js +4 -1
  34. package/dist/agent/src/chat/client/sessionClient.js +28 -2
  35. package/dist/agent/src/chat/constants.js +8 -0
  36. package/dist/agent/src/chat/data/dbSessionFiles.js +11 -6
  37. package/dist/agent/src/chat/protocol/messages.js +5 -0
  38. package/dist/agent/src/chat/server/chatContextManager.js +45 -25
  39. package/dist/agent/src/chat/server/conversation.js +3 -0
  40. package/dist/agent/src/chat/server/imageGeneratorTools.js +20 -8
  41. package/dist/agent/src/chat/server/openAIRouterLLM.js +0 -3
  42. package/dist/agent/src/chat/server/openSession.js +218 -55
  43. package/dist/agent/src/chat/server/promptRefiner.js +86 -0
  44. package/dist/agent/src/chat/server/server.js +5 -1
  45. package/dist/agent/src/chat/server/sessionFileManager.js +22 -221
  46. package/dist/agent/src/chat/server/sessionRegistry.js +87 -0
  47. package/dist/agent/src/chat/server/titleGenerator.js +112 -0
  48. package/dist/agent/src/chat/server/titleGenerator.test.js +113 -0
  49. package/dist/agent/src/chat/server/tools.js +63 -287
  50. package/dist/agent/src/chat/utils/approvalManager.js +6 -3
  51. package/dist/agent/src/chat/utils/multiAsyncQueue.js +3 -0
  52. package/dist/agent/src/test/agent.test.js +16 -17
  53. package/dist/agent/src/test/chatContextManager.test.js +15 -3
  54. package/dist/agent/src/test/dbMcpServerConfigs.test.js +4 -4
  55. package/dist/agent/src/test/dbSessionFiles.test.js +17 -17
  56. package/dist/agent/src/test/testTools.js +6 -1
  57. package/dist/agent/src/test/tools.test.js +27 -9
  58. package/dist/agent/src/tool/agentChat.js +5 -2
  59. package/dist/agent/src/tool/chatMain.js +34 -7
  60. package/dist/agent/src/tool/commandPrompt.js +2 -2
  61. package/dist/agent/src/tool/files.js +7 -8
  62. package/package.json +4 -1
  63. package/scripts/test_chat +195 -176
  64. package/src/agent/agent.ts +98 -23
  65. package/src/agent/agentUtils.ts +3 -2
  66. package/src/agent/documentSummarizer.ts +157 -0
  67. package/src/agent/dummyLLM.ts +27 -23
  68. package/src/agent/imageGenLLM.ts +28 -24
  69. package/src/agent/llm.ts +2 -2
  70. package/src/agent/openAILLM.ts +17 -13
  71. package/src/agent/openAILLMStreaming.ts +80 -41
  72. package/src/agent/repeatLLM.ts +19 -7
  73. package/src/agent/test_data/harrypotter.txt +6065 -0
  74. package/src/agent/tokenCounter.test.ts +243 -0
  75. package/src/agent/tokenCounter.ts +483 -0
  76. package/src/agent/toolSettings.ts +24 -0
  77. package/src/agent/tools/calculatorTool.ts +50 -0
  78. package/src/agent/tools/contentExtractors/pdfToText.ts +60 -0
  79. package/src/agent/tools/datetimeTool.ts +41 -0
  80. package/src/agent/tools/fileManager/fileManagerTool.ts +199 -0
  81. package/src/agent/tools/fileManager/index.ts +50 -0
  82. package/src/agent/tools/fileManager/memoryFileManager.ts +120 -0
  83. package/src/{chat/data → agent/tools/fileManager}/mimeTypes.ts +3 -1
  84. package/src/agent/tools/fileManager/prompt.ts +38 -0
  85. package/src/{chat/data/dbSessionFileModels.ts → agent/tools/fileManager/types.ts} +76 -0
  86. package/src/agent/tools/index.ts +49 -0
  87. package/src/agent/tools/openUrlTool.ts +62 -0
  88. package/src/agent/tools/renderTool.ts +92 -0
  89. package/src/agent/tools/utils.ts +74 -0
  90. package/src/{chat/utils/search.ts → agent/tools/webSearch.ts} +0 -1
  91. package/src/agent/tools/webSearchTool.ts +44 -0
  92. package/src/chat/client/chatClient.ts +45 -0
  93. package/src/chat/client/index.ts +3 -0
  94. package/src/chat/client/sessionClient.ts +40 -3
  95. package/src/chat/client/sessionFiles.ts +1 -1
  96. package/src/chat/constants.ts +6 -0
  97. package/src/chat/data/dataModels.ts +6 -0
  98. package/src/chat/data/dbSessionFiles.ts +12 -4
  99. package/src/chat/protocol/messages.ts +60 -7
  100. package/src/chat/server/chatContextManager.ts +58 -37
  101. package/src/chat/server/conversation.ts +3 -0
  102. package/src/chat/server/imageGeneratorTools.ts +31 -12
  103. package/src/chat/server/openAIRouterLLM.ts +1 -4
  104. package/src/chat/server/openSession.ts +323 -67
  105. package/src/chat/server/promptRefiner.ts +106 -0
  106. package/src/chat/server/server.ts +4 -1
  107. package/src/chat/server/sessionFileManager.ts +35 -306
  108. package/src/chat/server/sessionRegistry.ts +128 -0
  109. package/src/chat/server/titleGenerator.test.ts +103 -0
  110. package/src/chat/server/titleGenerator.ts +143 -0
  111. package/src/chat/server/tools.ts +77 -304
  112. package/src/chat/utils/approvalManager.ts +9 -3
  113. package/src/chat/utils/multiAsyncQueue.ts +4 -0
  114. package/src/test/agent.test.ts +17 -23
  115. package/src/test/chatContextManager.test.ts +29 -4
  116. package/src/test/dbMcpServerConfigs.test.ts +4 -4
  117. package/src/test/dbSessionFiles.test.ts +16 -16
  118. package/src/test/testTools.ts +8 -3
  119. package/src/test/tools.test.ts +30 -5
  120. package/src/tool/agentChat.ts +12 -3
  121. package/src/tool/chatMain.ts +33 -6
  122. package/src/tool/commandPrompt.ts +2 -2
  123. package/src/tool/files.ts +1 -3
  124. package/dist/agent/src/agent/tools.js +0 -44
  125. package/src/agent/tools.ts +0 -57
  126. /package/dist/agent/src/{chat/utils → agent/tools/contentExtractors}/htmlToText.js +0 -0
  127. /package/src/{chat/utils → agent/tools/contentExtractors}/htmlToText.ts +0 -0
package/scripts/test_chat CHANGED
@@ -45,8 +45,9 @@ function stop_chat_server() {
45
45
  stop_chat_server
46
46
 
47
47
  # Start the server with full logging and short heartbeat timeout.
48
- export DEFAULT_LLM_MODEL=repeat
49
- export DEVELOPMENT=1
48
+ export DEFAULT_LLM_MODEL="repeat:Main agent message"
49
+ export RACE_MODE_MODEL="repeat:Agent B message"
50
+ export TEST=1
50
51
  export GEN_IMAGE_MODEL=dummy:test_data/dummyllm_script_image_gen.json
51
52
 
52
53
  LOG_LEVEL=debug \
@@ -99,27 +100,27 @@ pushd _test_chat
99
100
  ${client} agent-profile set profile0 --profile agent_profile.json | jq -r .uuid > agent_profile.uuid
100
101
  agent_profile_id=`cat agent_profile.uuid`
101
102
 
102
- # User 0 tries to join with invalid API key - fails with "invalid api key" error
103
+ # # User 0 tries to join with invalid API key - fails with "invalid api key" error
103
104
 
104
- invalid_apikey0="invalid_api_key"
105
- ${agent} chat client --session-id no_such_session --api-key "${invalid_apikey0}" 2>&1 | tee invalid_apikey_test.log | grep -i "invalid api key" || \
106
- (echo "Expected 'invalid api key' error message"; cat invalid_apikey_test.log; exit 1)
105
+ # invalid_apikey0="invalid_api_key"
106
+ # ${agent} chat client --session-id no_such_session --api-key "${invalid_apikey0}" 2>&1 | tee invalid_apikey_test.log | grep -i "invalid api key" || \
107
+ # (echo "Expected 'invalid api key' error message"; cat invalid_apikey_test.log; exit 1)
107
108
 
108
- # User 0 tries to join non-existant session - fails
109
+ # # User 0 tries to join non-existant session - fails
109
110
 
110
- ${agent} chat clear-sessions
111
- ${agent} chat client --session-id no_such_session && \
112
- (echo "Should not be able to join"; exit 1)
111
+ # ${agent} chat clear-sessions
112
+ # ${agent} chat client --session-id no_such_session && \
113
+ # (echo "Should not be able to join"; exit 1)
113
114
 
114
- # User 0 creates a new session with `crash_agent_profile` and interacts with
115
- # it, exercising LLM error conditions.
115
+ # # User 0 creates a new session with `crash_agent_profile` and interacts with
116
+ # # it, exercising LLM error conditions.
116
117
 
117
- echo ':si crash_session.id' > crash_script
118
- echo 'Hello' >> crash_script
119
- echo 'Hello again' >> crash_script
120
- ${agent} chat client --session-title crash_test_session \
121
- --agent-profile-id ${crash_agent_profile_id} \
122
- --script crash_script
118
+ # echo ':si crash_session.id' > crash_script
119
+ # echo 'Hello' >> crash_script
120
+ # echo 'Hello again' >> crash_script
121
+ # ${agent} chat client --session-title crash_test_session \
122
+ # --agent-profile-id ${crash_agent_profile_id} \
123
+ # --script crash_script
123
124
 
124
125
  # User 0 creates empty session, which should not appear in the DB.
125
126
 
@@ -133,165 +134,183 @@ pushd _test_chat
133
134
 
134
135
  # User 0 creates a user session, sends message and writes session id to file
135
136
 
136
- echo ':pause-agent 1' > init_session
137
- echo 'tell me a joke' >> init_session
138
- echo ':si session.id' >> init_session
139
- echo ':share-session guest.key' >> init_session
140
- echo ':pause-agent 0' >> init_session
141
- echo 'now please' >> init_session
142
- ${agent} chat client --session-title test_session \
143
- --agent-profile-id ${agent_profile_id} \
144
- --script init_session
145
- session_id=`cat session.id`
146
- guest_key=`cat guest.key`
147
-
148
- # Check that the session got saved with the updated title
149
-
150
- ${agent} chat list-sessions | grep '"title":"tell me a joke"'
151
-
152
- # Check that a guest can join with the guest key
153
-
154
- echo ":sleep 1000" > guest_script
155
- echo "Hello from the guest" >> guest_script
137
+ echo ':si session.id' > race_mode
138
+ echo 'tell me a joke' > race_mode
139
+ echo 'race: tell me a longer joke' >> race_mode
140
+ echo ':sleep 10000' >> race_mode
141
+ echo 'B' >> race_mode
156
142
  ${agent} chat client \
157
- --session-id ${session_id} \
158
- --api-key ${guest_key} \
159
- --script guest_script | tee guest_output.txt
160
- grep "joke" guest_output.txt
161
- grep "[Guest]" guest_output.txt
162
-
163
- # Dummy session to invoke the image generation
164
-
165
- echo '{"system_prompt":"prompt","model":"dummy:test_data/dummyllm_script_invoke_image_gen_tool.json","mcp_settings":{}}' > invoke_gen_image_profile.json
166
- ${client} agent-profile set invoke_gen_image_agent_profile \
167
- --profile invoke_gen_image_profile.json \
168
- | jq -r .uuid > invoke_gen_image_profile.uuid
169
- invoke_gen_image_profile_id=`cat invoke_gen_image_profile.uuid`
170
-
171
- echo "Choose an animal and generate an image of it" > invoke_gen_image_script
172
- echo ":list-files" >> invoke_gen_image_script
173
- echo ":si invoke_gen_image.id" >> invoke_gen_image_script
174
- echo ':add-custom-mcp-server custom-mcp http://localhost:8002' >> invoke_gen_image_script
175
- echo ':list-custom-mcp-servers' >> invoke_gen_image_script
176
-
177
- LOG_LEVEL=debug \
178
- ${agent} chat client \
179
- --session-title invoke_gen_image_session \
180
- --agent-profile-id ${invoke_gen_image_profile_id} \
181
- --script invoke_gen_image_script | tee invoke_gen_image_output.txt
182
- grep 'frog.png' invoke_gen_image_output.txt
183
- grep '"xalia/fileMimeType":"image/png"' invoke_gen_image_output.txt
184
- grep 'structuredContent example' invoke_gen_image_output.txt
185
- grep '_meta example' invoke_gen_image_output.txt
186
- grep 'custom-mcp": ' invoke_gen_image_output.txt
187
-
188
- # User 1 tries to join the session. Should be rejected.
189
-
190
- ${agent} chat client --session-id ${session_id} --api-key ${apikey1} \
191
- --script init_session > join_unauthorized.log 2>&1 && \
192
- (echo "Should exit with error when joining invalid session"; exit 1)
193
-
194
- grep -i "not authorized" join_unauthorized.log || \
195
- (echo "Should include error message"; exit 1)
196
-
197
- # User 0 create a team, and write team id to a file
198
-
199
- echo ':ct team0' > script_create_team
200
- echo ':ci team0.id' >> script_create_team
201
- ${agent} chat client \
202
- --session-id ${session_id} \
203
- --script script_create_team
204
- team_id=`cat team0.id`
205
-
206
- # User 0 create a new team session (including a new paused agent), and write
207
- # session id to a file. Extract the agent_profile UUID.
208
-
209
- echo ':pause-agent 1' > script_team_chat
210
- echo 'tell me a joke' >> script_team_chat
211
- echo ':si team_session.id' >> script_team_chat
212
- ${agent} chat client --session-title test_team_session \
213
- --team-id ${team_id} \
214
- --script script_team_chat | tee team_chat_1
215
- team_session_id=`cat team_session.id`
216
-
217
- get_profile_id_query='.[] | select(.uuid=="'${team_session_id}'") | .agent_profile_uuid'
218
- ${agent} chat list-sessions | jq -r "${get_profile_id_query}" > team_agent.id
219
- team_agent_id=`cat team_agent.id`
220
-
221
- # Check that the agent has not said anything
222
-
223
- grep AGENT team_chat_1 && \
224
- (echo "Unexpected AGENT messages"; exit 1)
225
-
226
- # User 1 tries to join the team session. Should be rejected.
227
-
228
- ${agent} chat client --session-id ${team_session_id} --api-key ${apikey1} \
229
- --script init_session > join_unauthorized_team.log 2>&1 && \
230
- (echo "Should exit with error when joining invalid team session"; exit 1)
231
- grep -i "not authorized" join_unauthorized_team.log || \
232
- (echo "Should include error message (no auth team session)"; exit 1)
233
-
234
- # User0 sets up the session
235
- # add charuser1 to the team
236
- # add duckduckgo-search
237
- # add a session file (md file)
238
- # add an image to the workspace
239
- # unpause the agent
240
-
241
- echo ":ap ${team_id} chatuser1" > script_setup_team_session
242
- echo ':lp' >> script_setup_team_session
243
- echo ":as duckduckgo-search" >> script_setup_team_session
244
- echo ':put-file plan.md data:text/markdown,#PLAN\n##STEP1\n##STEP2\n' \
245
- >> script_setup_team_session
246
- echo ':sw file:../test_data/frog.png For context, this image represents our shared workspace' >> script_setup_team_session
247
- echo ':pause-agent 0' >> script_setup_team_session
248
- ${agent} chat client \
249
- --session-id ${team_session_id} \
250
- --script script_setup_team_session
251
-
252
- # checks the DB for chatuser2 participant and duckduckgo mcp server
253
-
254
- ${agent} chat list-participants --session ${team_session_id} | grep chatuser1
255
- ${client} agent-profile get ${team_agent_id} | grep duckduckgo
256
-
257
- # Check the state. Clean the workspace, file and mcp servers
258
-
259
- echo ':sleep 1000' > script_check_state
260
- echo ':list-files filelist.json' >> script_check_state
261
- echo 'Tell me about the image in our workspace?' >> script_check_state
262
- echo ':sw' >> script_check_state
263
- echo 'How many steps are in plan.md?' >> script_check_state
264
- echo ':sleep 5000' >> script_check_state
265
- echo ':delete-file plan.md' >> script_check_state
266
- echo ':rs duckduckgo-search' >> script_check_state
267
- ${agent} chat client \
268
- --session-id ${team_session_id} \
269
- --script script_check_state
270
-
271
- sleep 1
272
-
273
- # Check duckduckgo-search got removed
274
- (${client} agent-profile get ${team_agent_id} | grep duckduckgo) && ( \
275
- echo "ERROR: expected duckduckgo to be removed from agent profile" ; \
276
- exit 1 \
277
- )
278
-
279
- # Check the file manager list
280
- grep 'plan.md' filelist.json
281
-
282
- # Run both clients with a script that continues the conversation
283
-
284
- echo -e "why is that funny?\ntell me another.\nwhat is my name?\nhow old are you?\nwhere are your parents?" > script1
285
- ${agent} chat client --session-id ${team_session_id} --script script1 &
286
- pid0=$!
287
- ${agent} chat client --session-id ${team_session_id} --api-key ${apikey1} --script script1 >chatuser1.output
288
- wait ${pid0}
289
-
290
- # The name chatuser0 and AGENT should appear in chatuser1's session output
291
-
292
- grep "Chat User0" chatuser1.output
293
- grep "Chat User1" chatuser1.output
294
- grep AGENT chatuser1.output
143
+ --session-title test_session \
144
+ --agent-profile-id ${agent_profile_id} \
145
+ --script race_mode
146
+
147
+ # echo ':pause-agent 1' > init_session
148
+ # echo 'tell me a joke' >> init_session
149
+ # echo ':si session.id' >> init_session
150
+ # echo ':share-session guest.key' >> init_session
151
+ # echo ':pause-agent 0' >> init_session
152
+ # echo 'now please' >> init_session
153
+ # ${agent} chat client --session-title test_session \
154
+ # --agent-profile-id ${agent_profile_id} \
155
+ # --script init_session
156
+ # session_id=`cat session.id`
157
+ # guest_key=`cat guest.key`
158
+
159
+ # # Check that the session got saved with the updated title
160
+
161
+ # ${agent} chat list-sessions | grep '"title":"tell me a joke"'
162
+
163
+ # # Try race-mode
164
+ # echo 'race: tell me a better joke' > race_mode
165
+ # echo ':sleep 5000' >> race_mode
166
+ # echo 'B' >> race_mode
167
+ # ${agent} chat client \
168
+ # --session-id ${session_id} \
169
+ # --script race_mode | tee race_mode_output.txt
170
+
171
+ # # Check that a guest can join with the guest key
172
+
173
+ # echo ":sleep 1000" > guest_script
174
+ # echo "Hello from the guest" >> guest_script
175
+ # ${agent} chat client \
176
+ # --session-id ${session_id} \
177
+ # --api-key ${guest_key} \
178
+ # --script guest_script | tee guest_output.txt
179
+ # grep "joke" guest_output.txt
180
+ # grep "[Guest]" guest_output.txt
181
+
182
+ # # Dummy session to invoke the image generation
183
+
184
+ # echo '{"system_prompt":"prompt","model":"dummy:test_data/dummyllm_script_invoke_image_gen_tool.json","mcp_settings":{}}' > invoke_gen_image_profile.json
185
+ # ${client} agent-profile set invoke_gen_image_agent_profile \
186
+ # --profile invoke_gen_image_profile.json \
187
+ # | jq -r .uuid > invoke_gen_image_profile.uuid
188
+ # invoke_gen_image_profile_id=`cat invoke_gen_image_profile.uuid`
189
+
190
+ # echo "Choose an animal and generate an image of it" > invoke_gen_image_script
191
+ # echo ":list-files" >> invoke_gen_image_script
192
+ # echo ":si invoke_gen_image.id" >> invoke_gen_image_script
193
+ # echo ':add-custom-mcp-server custom-mcp http://localhost:8002' >> invoke_gen_image_script
194
+ # echo ':list-custom-mcp-servers' >> invoke_gen_image_script
195
+
196
+ # LOG_LEVEL=debug \
197
+ # ${agent} chat client \
198
+ # --session-title invoke_gen_image_session \
199
+ # --agent-profile-id ${invoke_gen_image_profile_id} \
200
+ # --script invoke_gen_image_script | tee invoke_gen_image_output.txt
201
+ # grep 'frog.png' invoke_gen_image_output.txt
202
+ # grep '"xalia/fileMimeType":"image/png"' invoke_gen_image_output.txt
203
+ # grep 'structuredContent example' invoke_gen_image_output.txt
204
+ # grep '_meta example' invoke_gen_image_output.txt
205
+ # grep 'custom-mcp": ' invoke_gen_image_output.txt
206
+
207
+ # # User 1 tries to join the session. Should be rejected.
208
+
209
+ # ${agent} chat client --session-id ${session_id} --api-key ${apikey1} \
210
+ # --script init_session > join_unauthorized.log 2>&1 && \
211
+ # (echo "Should exit with error when joining invalid session"; exit 1)
212
+
213
+ # grep -i "not authorized" join_unauthorized.log || \
214
+ # (echo "Should include error message"; exit 1)
215
+
216
+ # # User 0 create a team, and write team id to a file
217
+
218
+ # echo ':ct team0' > script_create_team
219
+ # echo ':ci team0.id' >> script_create_team
220
+ # ${agent} chat client \
221
+ # --session-id ${session_id} \
222
+ # --script script_create_team
223
+ # team_id=`cat team0.id`
224
+
225
+ # # User 0 create a new team session (including a new paused agent), and write
226
+ # # session id to a file. Extract the agent_profile UUID.
227
+
228
+ # echo ':pause-agent 1' > script_team_chat
229
+ # echo 'tell me a joke' >> script_team_chat
230
+ # echo ':si team_session.id' >> script_team_chat
231
+ # ${agent} chat client --session-title test_team_session \
232
+ # --team-id ${team_id} \
233
+ # --script script_team_chat | tee team_chat_1
234
+ # team_session_id=`cat team_session.id`
235
+
236
+ # get_profile_id_query='.[] | select(.uuid=="'${team_session_id}'") | .agent_profile_uuid'
237
+ # ${agent} chat list-sessions | jq -r "${get_profile_id_query}" > team_agent.id
238
+ # team_agent_id=`cat team_agent.id`
239
+
240
+ # # Check that the agent has not said anything
241
+
242
+ # grep AGENT team_chat_1 && \
243
+ # (echo "Unexpected AGENT messages"; exit 1)
244
+
245
+ # # User 1 tries to join the team session. Should be rejected.
246
+
247
+ # ${agent} chat client --session-id ${team_session_id} --api-key ${apikey1} \
248
+ # --script init_session > join_unauthorized_team.log 2>&1 && \
249
+ # (echo "Should exit with error when joining invalid team session"; exit 1)
250
+ # grep -i "not authorized" join_unauthorized_team.log || \
251
+ # (echo "Should include error message (no auth team session)"; exit 1)
252
+
253
+ # # User0 sets up the session
254
+ # # add charuser1 to the team
255
+ # # add duckduckgo-search
256
+ # # add a session file (md file)
257
+ # # add an image to the workspace
258
+ # # unpause the agent
259
+
260
+ # echo ":ap ${team_id} chatuser1" > script_setup_team_session
261
+ # echo ':lp' >> script_setup_team_session
262
+ # echo ":as duckduckgo-search" >> script_setup_team_session
263
+ # echo ':put-file plan.md data:text/markdown,#PLAN\n##STEP1\n##STEP2\n' \
264
+ # >> script_setup_team_session
265
+ # echo ':sw file:../test_data/frog.png For context, this image represents our shared workspace' >> script_setup_team_session
266
+ # echo ':pause-agent 0' >> script_setup_team_session
267
+ # ${agent} chat client \
268
+ # --session-id ${team_session_id} \
269
+ # --script script_setup_team_session
270
+
271
+ # # checks the DB for chatuser2 participant and duckduckgo mcp server
272
+
273
+ # ${agent} chat list-participants --session ${team_session_id} | grep chatuser1
274
+ # ${client} agent-profile get ${team_agent_id} | grep duckduckgo
275
+
276
+ # # Check the state. Clean the workspace, file and mcp servers
277
+
278
+ # echo ':sleep 1000' > script_check_state
279
+ # echo ':list-files filelist.json' >> script_check_state
280
+ # echo 'Tell me about the image in our workspace?' >> script_check_state
281
+ # echo ':sw' >> script_check_state
282
+ # echo 'How many steps are in plan.md?' >> script_check_state
283
+ # echo ':sleep 5000' >> script_check_state
284
+ # echo ':delete-file plan.md' >> script_check_state
285
+ # echo ':rs duckduckgo-search' >> script_check_state
286
+ # ${agent} chat client \
287
+ # --session-id ${team_session_id} \
288
+ # --script script_check_state
289
+
290
+ # sleep 1
291
+
292
+ # # Check duckduckgo-search got removed
293
+ # (${client} agent-profile get ${team_agent_id} | grep duckduckgo) && ( \
294
+ # echo "ERROR: expected duckduckgo to be removed from agent profile" ; \
295
+ # exit 1 \
296
+ # )
297
+
298
+ # # Check the file manager list
299
+ # grep 'plan.md' filelist.json
300
+
301
+ # # Run both clients with a script that continues the conversation
302
+
303
+ # echo -e "why is that funny?\ntell me another.\nwhat is my name?\nhow old are you?\nwhere are your parents?" > script1
304
+ # ${agent} chat client --session-id ${team_session_id} --script script1 &
305
+ # pid0=$!
306
+ # ${agent} chat client --session-id ${team_session_id} --api-key ${apikey1} --script script1 >chatuser1.output
307
+ # wait ${pid0}
308
+
309
+ # # The name chatuser0 and AGENT should appear in chatuser1's session output
310
+
311
+ # grep "Chat User0" chatuser1.output
312
+ # grep "Chat User1" chatuser1.output
313
+ # grep AGENT chatuser1.output
295
314
 
296
315
  popd
297
316
 
@@ -20,9 +20,15 @@ import { IAgentEventHandler } from "./iAgentEventHandler";
20
20
  import { IContextManager, IContextTransaction } from "./context";
21
21
  import { ChatCompletionContentPartImage } from "openai/resources";
22
22
 
23
+ import { MAX_TOOL_CALL_RESPONSE_LENGTH } from "./toolSettings";
24
+
23
25
  export const DEFAULT_LLM_URL = "http://localhost:5001/v1";
24
26
 
25
- const MAX_TOOL_CALL_RESPONSE_LENGTH = 4000;
27
+ /**
28
+ * The message to append to the agent output if the agent is interrupted by a
29
+ * signal from the user.
30
+ */
31
+ export const USER_STOP_MESSAGE = " AGENT INTERRUPTED";
26
32
 
27
33
  /**
28
34
  * An agent's response, with optional extra image data.
@@ -102,10 +108,21 @@ type RegisteredTools = {
102
108
  handler: ToolHandler;
103
109
  };
104
110
 
111
+ /**
112
+ * An agent attached to an ILLM which updates a context via an
113
+ * IContextTransaction interface (where IContextTransaction is like a DB tx or
114
+ * DB writer, for staging changes and reading back state as-if those changes
115
+ * were applied).
116
+ */
105
117
  export class AgentEx {
106
118
  mcpServerManager: McpServerManager;
107
119
  llm: ILLM;
108
120
 
121
+ /// Flag to stop the Agent loop.
122
+ stopFlag: boolean;
123
+ /// Function to stop the LLM (only present while it is active)
124
+ stopFn: ((msg: string) => void) | undefined;
125
+
109
126
  /// The full list of tools, ready to pass to the LLM
110
127
  private tools: ToolDescriptor[] = [];
111
128
 
@@ -116,12 +133,22 @@ export class AgentEx {
116
133
  constructor(mcpServerManager: McpServerManager, llm: ILLM) {
117
134
  this.mcpServerManager = mcpServerManager;
118
135
  this.llm = llm;
136
+ this.stopFlag = false;
137
+ this.stopFn = undefined;
119
138
  }
120
139
 
121
140
  public async shutdown(): Promise<void> {
141
+ this.stop("shutting down");
122
142
  return this.mcpServerManager.shutdown();
123
143
  }
124
144
 
145
+ public stop(msg?: string) {
146
+ this.stopFlag = true;
147
+ if (this.stopFn) {
148
+ this.stopFn(msg || USER_STOP_MESSAGE);
149
+ }
150
+ }
151
+
125
152
  public getMcpServerManager(): McpServerManager {
126
153
  return this.mcpServerManager;
127
154
  }
@@ -131,6 +158,8 @@ export class AgentEx {
131
158
  contextTx: IContextTransaction,
132
159
  eventHandler: IAgentEventHandler
133
160
  ): Promise<AssistantResponse | undefined> {
161
+ this.stopFlag = false;
162
+
134
163
  // New user messages have already been added to the `contextTx`.
135
164
 
136
165
  // Image and audio handling
@@ -157,6 +186,12 @@ export class AgentEx {
157
186
  // While there are tool calls to make, invoke them and loop
158
187
 
159
188
  while (message.tool_calls && message.tool_calls.length > 0) {
189
+ // Signal the event handler of the assistant message with tool calls
190
+ // BEFORE processing tool results. This ensures the order of messages
191
+ // in pendingMessages matches the order in the LLM context:
192
+ // [user, assistant(tool_calls), tool_result, assistant(final)]
193
+ eventHandler.onCompletion(message);
194
+
160
195
  // TODO: Execute all tool calls in parallel
161
196
 
162
197
  // [indexInContext, ToolCallResult][]
@@ -180,6 +215,12 @@ export class AgentEx {
180
215
  const toolResultHandle = contextTx.addMessage(toolResult);
181
216
  toolCallResults.push([toolResultHandle, result]);
182
217
 
218
+ // Immediately broadcast the tool result to the frontend for UI
219
+ // feedback. This ensures the frontend knows the tool executed
220
+ // successfully without waiting for the next LLM completion to
221
+ // finish streaming
222
+ eventHandler.onToolCallResult(toolResult);
223
+
183
224
  // If the tool call requested that its args be redacted, this can be
184
225
  // done now - before the next LLM invocation.
185
226
 
@@ -192,11 +233,6 @@ export class AgentEx {
192
233
  }
193
234
  }
194
235
 
195
- // Now that any args have been overwritten, signal the event handler of
196
- // the prevoius completion.
197
-
198
- eventHandler.onCompletion(message);
199
-
200
236
  // Get a new completion using the untouched tool call results. Note
201
237
  // that, since we are deferring the `onToolCallResult` calls (so they
202
238
  // can be redacted), we must take care that the errors in
@@ -212,19 +248,16 @@ export class AgentEx {
212
248
  contextTx.addMessage(message);
213
249
  } finally {
214
250
  // Now that the tool call results have been passed to the LLM, perform
215
- // any updates on them. Pass the (updated) tool-call-result LLM
216
- // messages to the event handler - note, we want to do this even if an
217
- // error occured, so that the caller has an up-to-date picture of the
218
- // context state when the error occured.
251
+ // any updates on them if overwriteResponse was requested. If so, send
252
+ // the updated tool result to the frontend to replace the original.
219
253
 
220
254
  toolCallResults.forEach(([handle, tcr]) => {
221
- const ctxMsg = contextTx.getMessage(handle);
222
255
  if (tcr.overwriteResponse) {
256
+ const ctxMsg = contextTx.getMessage(handle);
223
257
  ctxMsg.content = tcr.overwriteResponse;
258
+ assert(ctxMsg.role === "tool");
259
+ eventHandler.onToolCallResult(ctxMsg);
224
260
  }
225
-
226
- assert(ctxMsg.role === "tool");
227
- eventHandler.onToolCallResult(ctxMsg);
228
261
  });
229
262
 
230
263
  // Note, if an error DID occur, the ContextManager does not see any of
@@ -242,6 +275,27 @@ export class AgentEx {
242
275
  context: MessageParam[],
243
276
  eventHandler: IAgentEventHandler
244
277
  ): Promise<Completion> {
278
+ if (this.stopFlag) {
279
+ return {
280
+ id: "user_stopped",
281
+ choices: [
282
+ {
283
+ finish_reason: "stop",
284
+ index: 0,
285
+ message: {
286
+ content: USER_STOP_MESSAGE,
287
+ role: "assistant",
288
+ refusal: null,
289
+ },
290
+ logprobs: null,
291
+ },
292
+ ],
293
+ created: Date.now(),
294
+ model: this.llm.getModel(),
295
+ object: "chat.completion",
296
+ };
297
+ }
298
+
245
299
  // Compute the full list of available tools
246
300
 
247
301
  let tools: ToolDescriptor[] | undefined;
@@ -252,12 +306,27 @@ export class AgentEx {
252
306
  tools = enabledTools;
253
307
  }
254
308
  logger.debug(`[chatCompletion] tools: ${JSON.stringify(tools)}`);
255
- const completion = await this.llm.getConversationResponse(
256
- context,
257
- tools,
258
- eventHandler.onAgentMessage.bind(eventHandler),
259
- eventHandler.onReasoning.bind(eventHandler)
260
- );
309
+
310
+ // Log system prompt length
311
+ if (context.length > 0 && context[0].role === "system") {
312
+ const systemPrompt = context[0].content as string;
313
+ logger.info(
314
+ `[chatCompletion] System prompt length: ${String(systemPrompt.length)}`
315
+ );
316
+ }
317
+
318
+ const { stop, completion: completionP } =
319
+ await this.llm.getConversationResponse(
320
+ context,
321
+ tools,
322
+ eventHandler.onAgentMessage.bind(eventHandler),
323
+ eventHandler.onReasoning.bind(eventHandler)
324
+ );
325
+
326
+ this.stopFn = stop;
327
+ const completion = await completionP;
328
+ this.stopFn = undefined;
329
+
261
330
  logger.debug(`Received chat completion ${JSON.stringify(completion)}`);
262
331
  return completion;
263
332
  }
@@ -310,6 +379,10 @@ export class AgentEx {
310
379
  toolCall: MessageToolCall,
311
380
  eventHandler: IAgentEventHandler
312
381
  ): Promise<ToolCallResult> {
382
+ if (this.stopFlag) {
383
+ return { response: USER_STOP_MESSAGE };
384
+ }
385
+
313
386
  // If the tool is and "agent" (internal) tool, we can just execute it.
314
387
  // Otherwise, call the event handler to get permission and invoke the
315
388
  // external tool handler.
@@ -390,8 +463,10 @@ export class AgentEx {
390
463
  }
391
464
 
392
465
  /**
393
- * Higher-level abstraction over AgentEx, which abstracts out the transactions
394
- * to the context manager.
466
+ * Higher-level abstraction over AgentEx, which abstracts out the context
467
+ * transactions. A single agent is associated with an IContextManager and
468
+ * internally creates and commits transactions during each call to
469
+ * `userMessage*`.
395
470
  */
396
471
  export class Agent implements IConversation {
397
472
  private eventHandler: IAgentEventHandler;
@@ -564,7 +639,7 @@ export function createUserMessageEnsure(
564
639
  name?: string
565
640
  ): UserMessageParam {
566
641
  const userMsg = createUserMessage(msg, imageB64, name);
567
- assert(userMsg);
642
+ assert(userMsg, "createUserMessageEnsure");
568
643
  return userMsg;
569
644
  }
570
645
 
@@ -125,8 +125,9 @@ export async function createSpecializedLLM(
125
125
 
126
126
  if (model && model.startsWith("dummy:")) {
127
127
  llm = await DummyLLM.initFromModelUrl(model, platform);
128
- } else if (model === "repeat") {
129
- llm = new RepeatLLM();
128
+ } else if (model && model.startsWith("repeat")) {
129
+ const prefix = model.startsWith("repeat:") ? model.slice(7) : "";
130
+ llm = new RepeatLLM(prefix);
130
131
  }
131
132
  return llm;
132
133
  }