@agentscope-ai/agentscope 0.0.2

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 (136) hide show
  1. package/dist/agent/index.d.mts +234 -0
  2. package/dist/agent/index.d.ts +234 -0
  3. package/dist/agent/index.js +1412 -0
  4. package/dist/agent/index.js.map +1 -0
  5. package/dist/agent/index.mjs +1375 -0
  6. package/dist/agent/index.mjs.map +1 -0
  7. package/dist/base-BOx3UzOl.d.mts +41 -0
  8. package/dist/base-BoIps2RL.d.ts +41 -0
  9. package/dist/base-C7jwyH4Z.d.mts +52 -0
  10. package/dist/base-Cwi4bjze.d.ts +127 -0
  11. package/dist/base-DYlBMCy_.d.mts +127 -0
  12. package/dist/base-NX-knWOv.d.ts +52 -0
  13. package/dist/block-VsnHrllL.d.mts +48 -0
  14. package/dist/block-VsnHrllL.d.ts +48 -0
  15. package/dist/event/index.d.mts +181 -0
  16. package/dist/event/index.d.ts +181 -0
  17. package/dist/event/index.js +58 -0
  18. package/dist/event/index.js.map +1 -0
  19. package/dist/event/index.mjs +33 -0
  20. package/dist/event/index.mjs.map +1 -0
  21. package/dist/formatter/index.d.mts +187 -0
  22. package/dist/formatter/index.d.ts +187 -0
  23. package/dist/formatter/index.js +647 -0
  24. package/dist/formatter/index.js.map +1 -0
  25. package/dist/formatter/index.mjs +616 -0
  26. package/dist/formatter/index.mjs.map +1 -0
  27. package/dist/index-BTJDlKvQ.d.mts +195 -0
  28. package/dist/index-BcatlwXQ.d.ts +195 -0
  29. package/dist/index-CAxQAkiP.d.mts +21 -0
  30. package/dist/index-CAxQAkiP.d.ts +21 -0
  31. package/dist/mcp/index.d.mts +9 -0
  32. package/dist/mcp/index.d.ts +9 -0
  33. package/dist/mcp/index.js +432 -0
  34. package/dist/mcp/index.js.map +1 -0
  35. package/dist/mcp/index.mjs +408 -0
  36. package/dist/mcp/index.mjs.map +1 -0
  37. package/dist/message/index.d.mts +10 -0
  38. package/dist/message/index.d.ts +10 -0
  39. package/dist/message/index.js +67 -0
  40. package/dist/message/index.js.map +1 -0
  41. package/dist/message/index.mjs +37 -0
  42. package/dist/message/index.mjs.map +1 -0
  43. package/dist/message-CkN21KaY.d.mts +99 -0
  44. package/dist/message-CzLeTlua.d.ts +99 -0
  45. package/dist/model/index.d.mts +377 -0
  46. package/dist/model/index.d.ts +377 -0
  47. package/dist/model/index.js +1880 -0
  48. package/dist/model/index.js.map +1 -0
  49. package/dist/model/index.mjs +1849 -0
  50. package/dist/model/index.mjs.map +1 -0
  51. package/dist/storage/index.d.mts +68 -0
  52. package/dist/storage/index.d.ts +68 -0
  53. package/dist/storage/index.js +250 -0
  54. package/dist/storage/index.js.map +1 -0
  55. package/dist/storage/index.mjs +212 -0
  56. package/dist/storage/index.mjs.map +1 -0
  57. package/dist/tool/index.d.mts +311 -0
  58. package/dist/tool/index.d.ts +311 -0
  59. package/dist/tool/index.js +1494 -0
  60. package/dist/tool/index.js.map +1 -0
  61. package/dist/tool/index.mjs +1447 -0
  62. package/dist/tool/index.mjs.map +1 -0
  63. package/dist/toolkit-CEpulFi0.d.ts +99 -0
  64. package/dist/toolkit-CGEZSZPa.d.mts +99 -0
  65. package/jest.config.js +11 -0
  66. package/package.json +92 -0
  67. package/src/_utils/common.ts +104 -0
  68. package/src/_utils/index.ts +1 -0
  69. package/src/agent/agent-base.ts +0 -0
  70. package/src/agent/agent.test.ts +1028 -0
  71. package/src/agent/agent.ts +1032 -0
  72. package/src/agent/index.ts +2 -0
  73. package/src/agent/interfaces.ts +23 -0
  74. package/src/agent/test-compression.ts +72 -0
  75. package/src/event/index.ts +250 -0
  76. package/src/formatter/base.ts +133 -0
  77. package/src/formatter/dashscope-chat-formatter.test.ts +372 -0
  78. package/src/formatter/dashscope-chat-formatter.ts +163 -0
  79. package/src/formatter/deepseek-chat-formatter.ts +130 -0
  80. package/src/formatter/index.ts +5 -0
  81. package/src/formatter/ollama-chat-formatter.ts +67 -0
  82. package/src/formatter/openai-chat-formatter.test.ts +263 -0
  83. package/src/formatter/openai-chat-formatter.ts +301 -0
  84. package/src/formatter/openai.md +767 -0
  85. package/src/mcp/base.ts +114 -0
  86. package/src/mcp/http.test.ts +303 -0
  87. package/src/mcp/http.ts +224 -0
  88. package/src/mcp/index.ts +2 -0
  89. package/src/mcp/stdio.test.ts +91 -0
  90. package/src/mcp/stdio.ts +119 -0
  91. package/src/message/block.ts +60 -0
  92. package/src/message/enums.ts +4 -0
  93. package/src/message/index.ts +12 -0
  94. package/src/message/message.test.ts +80 -0
  95. package/src/message/message.ts +131 -0
  96. package/src/model/base.ts +226 -0
  97. package/src/model/dashscope-model.test.ts +335 -0
  98. package/src/model/dashscope-model.ts +441 -0
  99. package/src/model/deepseek-model.test.ts +279 -0
  100. package/src/model/deepseek-model.ts +401 -0
  101. package/src/model/index.ts +7 -0
  102. package/src/model/ollama-model.test.ts +307 -0
  103. package/src/model/ollama-model.ts +356 -0
  104. package/src/model/openai-model.ts +327 -0
  105. package/src/model/response.ts +22 -0
  106. package/src/model/usage.ts +12 -0
  107. package/src/storage/base.ts +52 -0
  108. package/src/storage/file-system.test.ts +587 -0
  109. package/src/storage/file-system.ts +269 -0
  110. package/src/storage/index.ts +2 -0
  111. package/src/tool/base.ts +23 -0
  112. package/src/tool/bash.test.ts +174 -0
  113. package/src/tool/bash.ts +152 -0
  114. package/src/tool/edit.test.ts +83 -0
  115. package/src/tool/edit.ts +95 -0
  116. package/src/tool/glob.test.ts +63 -0
  117. package/src/tool/glob.ts +166 -0
  118. package/src/tool/grep.test.ts +74 -0
  119. package/src/tool/grep.ts +256 -0
  120. package/src/tool/index.ts +10 -0
  121. package/src/tool/read.test.ts +77 -0
  122. package/src/tool/read.ts +117 -0
  123. package/src/tool/response.ts +82 -0
  124. package/src/tool/task.test.ts +299 -0
  125. package/src/tool/task.ts +399 -0
  126. package/src/tool/toolkit.test.ts +636 -0
  127. package/src/tool/toolkit.ts +601 -0
  128. package/src/tool/write.test.ts +52 -0
  129. package/src/tool/write.ts +57 -0
  130. package/src/type/index.ts +52 -0
  131. package/tsconfig.build.json +4 -0
  132. package/tsconfig.cjs.json +11 -0
  133. package/tsconfig.esm.json +10 -0
  134. package/tsconfig.json +14 -0
  135. package/tsup.config.ts +20 -0
  136. package/typedoc.json +52 -0
@@ -0,0 +1,1028 @@
1
+ import { Agent } from './agent';
2
+ import { AgentEvent, EventType, UserConfirmResultEvent } from '../event';
3
+ import { ContentBlock, Msg } from '../message';
4
+ import { ChatModelBase, ChatResponse } from '../model';
5
+ import { ChatModelRequestOptions } from '../model/base';
6
+ import { Bash, Edit, Glob, Grep, Read, Toolkit, Write } from '../tool';
7
+ import { ToolChoice, ToolSchema } from '../type';
8
+
9
+ /**
10
+ * A mock chat model for testing purposes.
11
+ */
12
+ class MockChatModel extends ChatModelBase {
13
+ /**
14
+ * Mock implementations
15
+ * @param _tools
16
+ */
17
+ _formatToolSchemas(_tools: ToolSchema[]): unknown[] {
18
+ throw new Error('Method not implemented.');
19
+ }
20
+ public mockContent: ContentBlock[];
21
+ /**
22
+ * Initialize a new instance of the MockChatModel class.
23
+ */
24
+ constructor() {
25
+ super({ modelName: 'mock-model' });
26
+ this.mockContent = [];
27
+ this.stream = false;
28
+ }
29
+
30
+ /**
31
+ * Simulate calling the API and return a ChatResponse with the mock content.
32
+ * @param _modelName
33
+ * @param _options
34
+ * @returns A promise that resolves to a ChatResponse containing the mock content.
35
+ */
36
+ async _callAPI(
37
+ _modelName: string,
38
+ _options: ChatModelRequestOptions<unknown>
39
+ ): Promise<ChatResponse> {
40
+ return {
41
+ type: 'chat',
42
+ id: 'mock-id',
43
+ createdAt: new Date().toISOString(),
44
+ content: [...this.mockContent],
45
+ } as ChatResponse;
46
+ }
47
+
48
+ /**
49
+ * Simulate formatting the tool choice. This method is not implemented in this mock model.
50
+ * @param _toolChoice
51
+ */
52
+ _formatToolChoice(_toolChoice: ToolChoice): unknown {
53
+ throw new Error('Method not implemented.');
54
+ }
55
+ }
56
+
57
+ describe('Human-in-the-loop', () => {
58
+ test('user confirm', async () => {
59
+ // Prepare tools and agent
60
+ const toolkit = new Toolkit({
61
+ tools: [Bash(), Glob(), Grep(), Read(), Write(), Edit()],
62
+ });
63
+
64
+ const model = new MockChatModel();
65
+ const agent = new Agent({
66
+ name: 'Friday',
67
+ sysPrompt: 'You are a helpful assistant named Friday.',
68
+ model: model,
69
+ toolkit,
70
+ });
71
+
72
+ // Set mock content to simulate model output with tool calls
73
+ model.mockContent = [
74
+ {
75
+ type: 'tool_call',
76
+ id: '1',
77
+ name: 'Bash',
78
+ input: `{"command": "echo Hello"}`,
79
+ },
80
+ {
81
+ type: 'tool_call',
82
+ id: '2',
83
+ name: 'Bash',
84
+ input: `{"command": "echo World"}`,
85
+ },
86
+ ];
87
+
88
+ // Record the last event emitted by the agent
89
+ let lastEvent: AgentEvent | null = null;
90
+ for await (const event of agent.replyStream({})) {
91
+ lastEvent = event;
92
+ }
93
+
94
+ expect(lastEvent).toMatchObject({
95
+ type: EventType.REQUIRE_USER_CONFIRM,
96
+ toolCalls: [
97
+ {
98
+ type: 'tool_call',
99
+ id: '1',
100
+ name: 'Bash',
101
+ input: '{"command": "echo Hello"}',
102
+ awaitUserConfirmation: true,
103
+ },
104
+ {
105
+ type: 'tool_call',
106
+ id: '2',
107
+ name: 'Bash',
108
+ input: '{"command": "echo World"}',
109
+ awaitUserConfirmation: true,
110
+ },
111
+ ],
112
+ });
113
+
114
+ expect(await agent.toJSON()).toMatchObject({
115
+ replyId: expect.any(String),
116
+ confirmedToolCallIds: [],
117
+ curIter: 0,
118
+ });
119
+
120
+ // Ensure the agent context state
121
+ expect(agent.context).toEqual([
122
+ {
123
+ content: [
124
+ {
125
+ id: '1',
126
+ input: '{"command": "echo Hello"}',
127
+ name: 'Bash',
128
+ type: 'tool_call',
129
+ awaitUserConfirmation: true,
130
+ },
131
+ {
132
+ id: '2',
133
+ input: '{"command": "echo World"}',
134
+ name: 'Bash',
135
+ type: 'tool_call',
136
+ awaitUserConfirmation: true,
137
+ },
138
+ ],
139
+ id: expect.any(String),
140
+ metadata: {},
141
+ name: 'Friday',
142
+ role: 'assistant',
143
+ timestamp: expect.any(String),
144
+ },
145
+ ]);
146
+
147
+ // Simulate user confirmation result for the first tool call
148
+ for await (const event of agent.replyStream({
149
+ event: {
150
+ id: 'xxx',
151
+ createdAt: new Date().toISOString(),
152
+ type: EventType.USER_CONFIRM_RESULT,
153
+ replyId: agent.replyId,
154
+ confirmResults: [
155
+ {
156
+ confirmed: true,
157
+ toolCall: {
158
+ type: 'tool_call',
159
+ id: '1',
160
+ name: 'Bash',
161
+ input: '{"command": "echo Hello"}',
162
+ },
163
+ },
164
+ ],
165
+ } as UserConfirmResultEvent,
166
+ })) {
167
+ lastEvent = event;
168
+ }
169
+
170
+ // Verify the agent still yields user confirmation for the second tool call
171
+ expect(lastEvent).toMatchObject({
172
+ type: EventType.REQUIRE_USER_CONFIRM,
173
+ replyId: expect.any(String),
174
+ toolCalls: [
175
+ {
176
+ type: 'tool_call',
177
+ id: '2',
178
+ name: 'Bash',
179
+ input: '{"command": "echo World"}',
180
+ },
181
+ ],
182
+ });
183
+
184
+ // Verify the current agent context
185
+ expect(agent.context.map(msg => msg.content)).toEqual([
186
+ [
187
+ {
188
+ id: '1',
189
+ input: '{"command": "echo Hello"}',
190
+ name: 'Bash',
191
+ type: 'tool_call',
192
+ },
193
+ {
194
+ id: '2',
195
+ input: '{"command": "echo World"}',
196
+ name: 'Bash',
197
+ type: 'tool_call',
198
+ awaitUserConfirmation: true,
199
+ },
200
+ {
201
+ id: '1',
202
+ name: 'Bash',
203
+ output: [
204
+ {
205
+ id: expect.any(String),
206
+ text: 'Hello\n',
207
+ type: 'text',
208
+ },
209
+ ],
210
+ type: 'tool_result',
211
+ state: 'success',
212
+ },
213
+ ],
214
+ ]);
215
+
216
+ model.mockContent = [{ type: 'text', text: 'Finished', id: expect.any(String) }];
217
+
218
+ // Reject the second tool call by simulating user confirmation result
219
+ const res = agent.replyStream({
220
+ event: {
221
+ id: 'xxx',
222
+ createdAt: new Date().toISOString(),
223
+ type: EventType.USER_CONFIRM_RESULT,
224
+ replyId: agent.replyId,
225
+ confirmResults: [
226
+ {
227
+ confirmed: false,
228
+ toolCall: {
229
+ type: 'tool_call',
230
+ id: '2',
231
+ name: 'Bash',
232
+ input: '{"command": "echo World"}',
233
+ },
234
+ },
235
+ ],
236
+ },
237
+ });
238
+
239
+ let replyMsg: Msg;
240
+ while (true) {
241
+ const { value, done } = await res.next();
242
+ if (done) {
243
+ replyMsg = value as Msg;
244
+ break;
245
+ }
246
+ lastEvent = value;
247
+ }
248
+
249
+ // Verify the lastEvent
250
+ expect(lastEvent).toMatchObject({
251
+ id: expect.any(String),
252
+ type: EventType.RUN_FINISHED,
253
+ createdAt: expect.any(String),
254
+ replyId: agent.replyId,
255
+ });
256
+
257
+ // Verify the final agent reply msg
258
+ expect(replyMsg).toMatchObject({
259
+ content: [
260
+ {
261
+ id: expect.any(String),
262
+ type: 'text',
263
+ text: 'Finished',
264
+ },
265
+ ],
266
+ id: expect.any(String),
267
+ metadata: {},
268
+ name: 'Friday',
269
+ role: 'assistant',
270
+ timestamp: expect.any(String),
271
+ });
272
+ });
273
+
274
+ test('external execution', async () => {
275
+ // Prepare tools and agent with external execution tools
276
+ const externalTool1 = {
277
+ name: 'ExternalTool1',
278
+ description: 'A tool that requires external execution',
279
+ inputSchema: {
280
+ type: 'object' as const,
281
+ properties: { query: { type: 'string' as const } },
282
+ },
283
+ // No call method means it requires external execution
284
+ };
285
+
286
+ const externalTool2 = {
287
+ name: 'ExternalTool2',
288
+ description: 'Another tool that requires external execution',
289
+ inputSchema: {
290
+ type: 'object' as const,
291
+ properties: { data: { type: 'string' as const } },
292
+ },
293
+ // No call method means it requires external execution
294
+ };
295
+
296
+ const toolkit = new Toolkit({
297
+ tools: [externalTool1, externalTool2],
298
+ });
299
+
300
+ const model = new MockChatModel();
301
+ const agent = new Agent({
302
+ name: 'Friday',
303
+ sysPrompt: 'You are a helpful assistant named Friday.',
304
+ model: model,
305
+ toolkit,
306
+ });
307
+
308
+ // Set mock content to simulate model output with external tool calls
309
+ model.mockContent = [
310
+ {
311
+ type: 'tool_call',
312
+ id: '1',
313
+ name: 'ExternalTool1',
314
+ input: `{"query": "test query"}`,
315
+ },
316
+ {
317
+ type: 'tool_call',
318
+ id: '2',
319
+ name: 'ExternalTool2',
320
+ input: `{"data": "test data"}`,
321
+ },
322
+ ];
323
+
324
+ // Record the last event emitted by the agent
325
+ let lastEvent: AgentEvent | null = null;
326
+ for await (const event of agent.replyStream({})) {
327
+ lastEvent = event;
328
+ }
329
+
330
+ // Verify the agent emits REQUIRE_EXTERNAL_EXECUTION event
331
+ expect(lastEvent).toMatchObject({
332
+ type: EventType.REQUIRE_EXTERNAL_EXECUTION,
333
+ toolCalls: [
334
+ {
335
+ type: 'tool_call',
336
+ id: '1',
337
+ name: 'ExternalTool1',
338
+ input: '{"query": "test query"}',
339
+ },
340
+ {
341
+ type: 'tool_call',
342
+ id: '2',
343
+ name: 'ExternalTool2',
344
+ input: '{"data": "test data"}',
345
+ },
346
+ ],
347
+ });
348
+
349
+ // Verify agent state
350
+ expect(await agent.toJSON()).toMatchObject({
351
+ replyId: expect.any(String),
352
+ confirmedToolCallIds: [],
353
+ curIter: 0,
354
+ });
355
+
356
+ // Verify agent context
357
+ expect(agent.context).toEqual([
358
+ {
359
+ content: [
360
+ {
361
+ id: '1',
362
+ input: '{"query": "test query"}',
363
+ name: 'ExternalTool1',
364
+ type: 'tool_call',
365
+ },
366
+ {
367
+ id: '2',
368
+ input: '{"data": "test data"}',
369
+ name: 'ExternalTool2',
370
+ type: 'tool_call',
371
+ },
372
+ ],
373
+ id: expect.any(String),
374
+ metadata: {},
375
+ name: 'Friday',
376
+ role: 'assistant',
377
+ timestamp: expect.any(String),
378
+ },
379
+ ]);
380
+
381
+ // Provide execution result for the first tool call
382
+ for await (const event of agent.replyStream({
383
+ event: {
384
+ id: 'xxx',
385
+ createdAt: new Date().toISOString(),
386
+ type: EventType.EXTERNAL_EXECUTION_RESULT,
387
+ replyId: agent.replyId,
388
+ executionResults: [
389
+ {
390
+ type: 'tool_result',
391
+ id: '1',
392
+ name: 'ExternalTool1',
393
+ output: [
394
+ {
395
+ id: 'output-1',
396
+ type: 'text',
397
+ text: 'Result from ExternalTool1',
398
+ },
399
+ ],
400
+ state: 'success',
401
+ },
402
+ ],
403
+ },
404
+ })) {
405
+ lastEvent = event;
406
+ }
407
+
408
+ // Verify the agent still requires external execution for the second tool
409
+ expect(lastEvent).toMatchObject({
410
+ type: EventType.REQUIRE_EXTERNAL_EXECUTION,
411
+ replyId: expect.any(String),
412
+ toolCalls: [
413
+ {
414
+ type: 'tool_call',
415
+ id: '2',
416
+ name: 'ExternalTool2',
417
+ input: '{"data": "test data"}',
418
+ },
419
+ ],
420
+ });
421
+
422
+ // Verify the current agent context
423
+ expect(agent.context.map(msg => msg.content)).toEqual([
424
+ [
425
+ {
426
+ id: '1',
427
+ input: '{"query": "test query"}',
428
+ name: 'ExternalTool1',
429
+ type: 'tool_call',
430
+ },
431
+ {
432
+ id: '2',
433
+ input: '{"data": "test data"}',
434
+ name: 'ExternalTool2',
435
+ type: 'tool_call',
436
+ },
437
+ {
438
+ id: '1',
439
+ name: 'ExternalTool1',
440
+ output: [
441
+ {
442
+ id: expect.any(String),
443
+ text: 'Result from ExternalTool1',
444
+ type: 'text',
445
+ },
446
+ ],
447
+ type: 'tool_result',
448
+ state: 'success',
449
+ },
450
+ ],
451
+ ]);
452
+
453
+ model.mockContent = [{ type: 'text', text: 'All tools executed', id: expect.any(String) }];
454
+
455
+ // Provide execution result for the second tool call
456
+ const res = agent.replyStream({
457
+ event: {
458
+ id: 'xxx',
459
+ createdAt: new Date().toISOString(),
460
+ type: EventType.EXTERNAL_EXECUTION_RESULT,
461
+ replyId: agent.replyId,
462
+ executionResults: [
463
+ {
464
+ type: 'tool_result',
465
+ id: '2',
466
+ name: 'ExternalTool2',
467
+ output: [
468
+ {
469
+ id: expect.any(String),
470
+ type: 'text',
471
+ text: 'Result from ExternalTool2',
472
+ },
473
+ ],
474
+ state: 'success',
475
+ },
476
+ ],
477
+ },
478
+ });
479
+
480
+ let replyMsg: Msg;
481
+ while (true) {
482
+ const { value, done } = await res.next();
483
+ if (done) {
484
+ replyMsg = value as Msg;
485
+ break;
486
+ }
487
+ lastEvent = value;
488
+ }
489
+
490
+ // Verify the lastEvent is RUN_FINISHED
491
+ expect(lastEvent).toMatchObject({
492
+ id: expect.any(String),
493
+ type: EventType.RUN_FINISHED,
494
+ createdAt: expect.any(String),
495
+ replyId: agent.replyId,
496
+ });
497
+
498
+ // Verify the final agent reply msg
499
+ expect(replyMsg).toMatchObject({
500
+ content: [
501
+ {
502
+ id: expect.any(String),
503
+ type: 'text',
504
+ text: 'All tools executed',
505
+ },
506
+ ],
507
+ id: expect.any(String),
508
+ metadata: {},
509
+ name: 'Friday',
510
+ role: 'assistant',
511
+ timestamp: expect.any(String),
512
+ });
513
+ });
514
+
515
+ test('mixed tool calls', async () => {
516
+ // Create three tools: external execution, user confirm, and normal execution
517
+ const externalTool = {
518
+ name: 'ExternalTool',
519
+ description: 'A tool that requires external execution',
520
+ inputSchema: {
521
+ type: 'object' as const,
522
+ properties: { query: { type: 'string' as const } },
523
+ },
524
+ // No call method means it requires external execution
525
+ };
526
+
527
+ const confirmTool = {
528
+ name: 'ConfirmTool',
529
+ description: 'A tool that requires user confirmation',
530
+ inputSchema: {
531
+ type: 'object' as const,
532
+ properties: { action: { type: 'string' as const } },
533
+ },
534
+ requireUserConfirm: true,
535
+ call: async (input: { action: string }) => {
536
+ return `Executed action: ${input.action}`;
537
+ },
538
+ };
539
+
540
+ const normalTool = {
541
+ name: 'NormalTool',
542
+ description: 'A normal tool',
543
+ inputSchema: {
544
+ type: 'object' as const,
545
+ properties: { data: { type: 'string' as const } },
546
+ },
547
+ call: async (input: { data: string }) => {
548
+ return `Processed data: ${input.data}`;
549
+ },
550
+ };
551
+
552
+ const toolkit = new Toolkit({
553
+ tools: [
554
+ externalTool,
555
+ confirmTool,
556
+ normalTool,
557
+ Bash(),
558
+ Glob(),
559
+ Grep(),
560
+ Read(),
561
+ Write(),
562
+ Edit(),
563
+ ],
564
+ });
565
+
566
+ const model = new MockChatModel();
567
+ const agent = new Agent({
568
+ name: 'Friday',
569
+ sysPrompt: 'You are a helpful assistant named Friday.',
570
+ model: model,
571
+ toolkit,
572
+ });
573
+
574
+ // Set mock content to simulate model output with three different tool calls
575
+ model.mockContent = [
576
+ {
577
+ type: 'tool_call',
578
+ id: '1',
579
+ name: 'ExternalTool',
580
+ input: `{"query": "external query"}`,
581
+ },
582
+ {
583
+ type: 'tool_call',
584
+ id: '2',
585
+ name: 'ConfirmTool',
586
+ input: `{"action": "delete file"}`,
587
+ },
588
+ {
589
+ type: 'tool_call',
590
+ id: '3',
591
+ name: 'NormalTool',
592
+ input: `{"data": "normal data"}`,
593
+ },
594
+ ];
595
+
596
+ // Record the last event emitted by the agent
597
+ let lastEvent: AgentEvent | null = null;
598
+ for await (const event of agent.replyStream({})) {
599
+ lastEvent = event;
600
+ }
601
+
602
+ // Verify the agent emits REQUIRE_EXTERNAL_EXECUTION event for the first tool
603
+ expect(lastEvent).toMatchObject({
604
+ type: EventType.REQUIRE_EXTERNAL_EXECUTION,
605
+ toolCalls: [
606
+ {
607
+ type: 'tool_call',
608
+ id: '1',
609
+ name: 'ExternalTool',
610
+ input: '{"query": "external query"}',
611
+ },
612
+ ],
613
+ });
614
+
615
+ // Verify agent state
616
+ expect(await agent.toJSON()).toMatchObject({
617
+ replyId: expect.any(String),
618
+ confirmedToolCallIds: [],
619
+ curIter: 0,
620
+ });
621
+
622
+ // Provide execution result for the external tool
623
+ for await (const event of agent.replyStream({
624
+ event: {
625
+ id: 'xxx',
626
+ createdAt: new Date().toISOString(),
627
+ type: EventType.EXTERNAL_EXECUTION_RESULT,
628
+ replyId: agent.replyId,
629
+ executionResults: [
630
+ {
631
+ type: 'tool_result',
632
+ id: '1',
633
+ name: 'ExternalTool',
634
+ output: [
635
+ {
636
+ id: expect.any(String),
637
+ type: 'text',
638
+ text: 'External execution result',
639
+ },
640
+ ],
641
+ state: 'success',
642
+ },
643
+ ],
644
+ },
645
+ })) {
646
+ lastEvent = event;
647
+ }
648
+
649
+ // Verify the agent now requires user confirmation for the second tool
650
+ expect(lastEvent).toMatchObject({
651
+ type: EventType.REQUIRE_USER_CONFIRM,
652
+ replyId: expect.any(String),
653
+ toolCalls: [
654
+ {
655
+ type: 'tool_call',
656
+ id: '2',
657
+ name: 'ConfirmTool',
658
+ input: '{"action": "delete file"}',
659
+ awaitUserConfirmation: true,
660
+ },
661
+ ],
662
+ });
663
+
664
+ // Verify agent state after external execution
665
+ expect(await agent.toJSON()).toMatchObject({
666
+ replyId: expect.any(String),
667
+ confirmedToolCallIds: [],
668
+ curIter: 0,
669
+ });
670
+
671
+ // Update mock content to return final text response
672
+ model.mockContent = [
673
+ {
674
+ type: 'text',
675
+ text: 'All tools completed successfully',
676
+ id: expect.any(String),
677
+ },
678
+ ];
679
+
680
+ // Provide user confirmation for the second tool
681
+ for await (const event of agent.replyStream({
682
+ event: {
683
+ id: 'xxx',
684
+ createdAt: new Date().toISOString(),
685
+ type: EventType.USER_CONFIRM_RESULT,
686
+ replyId: agent.replyId,
687
+ confirmResults: [
688
+ {
689
+ confirmed: true,
690
+ toolCall: {
691
+ type: 'tool_call',
692
+ id: '2',
693
+ name: 'ConfirmTool',
694
+ input: '{"action": "delete file"}',
695
+ },
696
+ },
697
+ ],
698
+ },
699
+ })) {
700
+ lastEvent = event;
701
+ }
702
+
703
+ // Verify the lastEvent is RUN_FINISHED
704
+ expect(lastEvent).toMatchObject({
705
+ id: expect.any(String),
706
+ type: EventType.RUN_FINISHED,
707
+ createdAt: expect.any(String),
708
+ replyId: agent.replyId,
709
+ });
710
+
711
+ // Verify the agent context includes all three tool calls, their results, and the final text
712
+ expect(agent.context.map(msg => msg.content)).toEqual([
713
+ [
714
+ {
715
+ id: '1',
716
+ input: '{"query": "external query"}',
717
+ name: 'ExternalTool',
718
+ type: 'tool_call',
719
+ },
720
+ {
721
+ id: '2',
722
+ input: '{"action": "delete file"}',
723
+ name: 'ConfirmTool',
724
+ type: 'tool_call',
725
+ },
726
+ {
727
+ id: '3',
728
+ input: '{"data": "normal data"}',
729
+ name: 'NormalTool',
730
+ type: 'tool_call',
731
+ },
732
+ {
733
+ id: '1',
734
+ name: 'ExternalTool',
735
+ output: [
736
+ {
737
+ id: expect.any(String),
738
+ text: 'External execution result',
739
+ type: 'text',
740
+ },
741
+ ],
742
+ type: 'tool_result',
743
+ state: 'success',
744
+ },
745
+ {
746
+ id: '2',
747
+ name: 'ConfirmTool',
748
+ output: [
749
+ {
750
+ id: expect.any(String),
751
+ text: 'Executed action: delete file',
752
+ type: 'text',
753
+ },
754
+ ],
755
+ type: 'tool_result',
756
+ state: 'success',
757
+ },
758
+ {
759
+ id: '3',
760
+ name: 'NormalTool',
761
+ output: [
762
+ {
763
+ id: expect.any(String),
764
+ text: 'Processed data: normal data',
765
+ type: 'text',
766
+ },
767
+ ],
768
+ type: 'tool_result',
769
+ state: 'success',
770
+ },
771
+ {
772
+ id: expect.any(String),
773
+ type: 'text',
774
+ text: 'All tools completed successfully',
775
+ },
776
+ ],
777
+ ]);
778
+ });
779
+
780
+ test('a tool requires both external execution and user confirmation', async () => {
781
+ // Create two tools: one requires both external execution and user confirmation,
782
+ // another requires only user confirmation
783
+ const externalAndConfirmTool = {
784
+ name: 'ExternalAndConfirmTool',
785
+ description: 'A tool that requires both external execution and user confirmation',
786
+ inputSchema: {
787
+ type: 'object' as const,
788
+ properties: { command: { type: 'string' as const } },
789
+ },
790
+ requireUserConfirm: true,
791
+ // No call method means it requires external execution
792
+ };
793
+
794
+ const confirmOnlyTool = {
795
+ name: 'ConfirmOnlyTool',
796
+ description: 'A tool that requires only user confirmation',
797
+ inputSchema: {
798
+ type: 'object' as const,
799
+ properties: { action: { type: 'string' as const } },
800
+ },
801
+ requireUserConfirm: true,
802
+ call: async (input: { action: string }) => {
803
+ return `Executed action: ${input.action}`;
804
+ },
805
+ };
806
+
807
+ const toolkit = new Toolkit({
808
+ tools: [externalAndConfirmTool, confirmOnlyTool],
809
+ });
810
+
811
+ const model = new MockChatModel();
812
+ const agent = new Agent({
813
+ name: 'Friday',
814
+ sysPrompt: 'You are a helpful assistant named Friday.',
815
+ model: model,
816
+ toolkit,
817
+ });
818
+
819
+ // Set mock content to simulate model output with two tool calls
820
+ model.mockContent = [
821
+ {
822
+ type: 'tool_call',
823
+ id: '1',
824
+ name: 'ExternalAndConfirmTool',
825
+ input: `{"command": "rm -rf /"}`,
826
+ },
827
+ {
828
+ type: 'tool_call',
829
+ id: '2',
830
+ name: 'ConfirmOnlyTool',
831
+ input: `{"action": "delete database"}`,
832
+ },
833
+ ];
834
+
835
+ // Record the last event emitted by the agent
836
+ let lastEvent: AgentEvent | null = null;
837
+ for await (const event of agent.replyStream({})) {
838
+ lastEvent = event;
839
+ }
840
+
841
+ // Verify the agent emits REQUIRE_USER_CONFIRM event for both tools
842
+ expect(lastEvent).toMatchObject({
843
+ type: EventType.REQUIRE_USER_CONFIRM,
844
+ toolCalls: [
845
+ {
846
+ type: 'tool_call',
847
+ id: '1',
848
+ name: 'ExternalAndConfirmTool',
849
+ input: '{"command": "rm -rf /"}',
850
+ awaitUserConfirmation: true,
851
+ },
852
+ {
853
+ type: 'tool_call',
854
+ id: '2',
855
+ name: 'ConfirmOnlyTool',
856
+ input: '{"action": "delete database"}',
857
+ awaitUserConfirmation: true,
858
+ },
859
+ ],
860
+ });
861
+
862
+ // Verify agent state
863
+ expect(await agent.toJSON()).toMatchObject({
864
+ replyId: expect.any(String),
865
+ confirmedToolCallIds: [],
866
+ curIter: 0,
867
+ });
868
+
869
+ // Provide user confirmation for both tools
870
+ for await (const event of agent.replyStream({
871
+ event: {
872
+ id: 'xxx',
873
+ createdAt: new Date().toISOString(),
874
+ type: EventType.USER_CONFIRM_RESULT,
875
+ replyId: agent.replyId,
876
+ confirmResults: [
877
+ {
878
+ confirmed: true,
879
+ toolCall: {
880
+ type: 'tool_call',
881
+ id: '1',
882
+ name: 'ExternalAndConfirmTool',
883
+ input: '{"command": "rm -rf /"}',
884
+ },
885
+ },
886
+ {
887
+ confirmed: true,
888
+ toolCall: {
889
+ type: 'tool_call',
890
+ id: '2',
891
+ name: 'ConfirmOnlyTool',
892
+ input: '{"action": "delete database"}',
893
+ },
894
+ },
895
+ ],
896
+ },
897
+ })) {
898
+ lastEvent = event;
899
+ }
900
+
901
+ // After user confirmation, the first tool requires external execution
902
+ expect(lastEvent).toMatchObject({
903
+ type: EventType.REQUIRE_EXTERNAL_EXECUTION,
904
+ replyId: expect.any(String),
905
+ toolCalls: [
906
+ {
907
+ type: 'tool_call',
908
+ id: '1',
909
+ name: 'ExternalAndConfirmTool',
910
+ input: '{"command": "rm -rf /"}',
911
+ },
912
+ ],
913
+ });
914
+
915
+ // Verify agent state after user confirmation
916
+ expect(await agent.toJSON()).toMatchObject({
917
+ replyId: expect.any(String),
918
+ confirmedToolCallIds: ['1', '2'],
919
+ curIter: 0,
920
+ });
921
+
922
+ // Verify the current agent context
923
+ expect(agent.context.map(msg => msg.content)).toEqual([
924
+ [
925
+ {
926
+ id: '1',
927
+ input: '{"command": "rm -rf /"}',
928
+ name: 'ExternalAndConfirmTool',
929
+ type: 'tool_call',
930
+ },
931
+ {
932
+ id: '2',
933
+ input: '{"action": "delete database"}',
934
+ name: 'ConfirmOnlyTool',
935
+ type: 'tool_call',
936
+ },
937
+ ],
938
+ ]);
939
+
940
+ // Update mock content to return final text response
941
+ model.mockContent = [{ type: 'text', text: 'All operations completed', id: 'abc' }];
942
+
943
+ // Provide external execution result for the first tool
944
+ for await (const event of agent.replyStream({
945
+ event: {
946
+ id: 'xxx',
947
+ createdAt: new Date().toISOString(),
948
+ type: EventType.EXTERNAL_EXECUTION_RESULT,
949
+ replyId: agent.replyId,
950
+ executionResults: [
951
+ {
952
+ type: 'tool_result',
953
+ id: '1',
954
+ name: 'ExternalAndConfirmTool',
955
+ output: [
956
+ {
957
+ id: expect.any(String),
958
+ type: 'text',
959
+ text: 'External command executed',
960
+ },
961
+ ],
962
+ state: 'success',
963
+ },
964
+ ],
965
+ },
966
+ })) {
967
+ lastEvent = event;
968
+ }
969
+
970
+ // After external execution, the second tool should execute directly
971
+ // because it was already confirmed in the previous step
972
+ expect(lastEvent).toMatchObject({
973
+ id: expect.any(String),
974
+ type: EventType.RUN_FINISHED,
975
+ createdAt: expect.any(String),
976
+ replyId: agent.replyId,
977
+ });
978
+
979
+ // Verify the final agent context includes all tool calls, their results, and the final text
980
+ expect(agent.context.map(msg => msg.content)).toEqual([
981
+ [
982
+ {
983
+ id: '1',
984
+ input: '{"command": "rm -rf /"}',
985
+ name: 'ExternalAndConfirmTool',
986
+ type: 'tool_call',
987
+ },
988
+ {
989
+ id: '2',
990
+ input: '{"action": "delete database"}',
991
+ name: 'ConfirmOnlyTool',
992
+ type: 'tool_call',
993
+ },
994
+ {
995
+ id: '1',
996
+ name: 'ExternalAndConfirmTool',
997
+ output: [
998
+ {
999
+ id: expect.any(String),
1000
+ text: 'External command executed',
1001
+ type: 'text',
1002
+ },
1003
+ ],
1004
+ type: 'tool_result',
1005
+ state: 'success',
1006
+ },
1007
+ {
1008
+ id: '2',
1009
+ name: 'ConfirmOnlyTool',
1010
+ output: [
1011
+ {
1012
+ id: expect.any(String),
1013
+ text: 'Executed action: delete database',
1014
+ type: 'text',
1015
+ },
1016
+ ],
1017
+ type: 'tool_result',
1018
+ state: 'success',
1019
+ },
1020
+ {
1021
+ id: expect.any(String),
1022
+ type: 'text',
1023
+ text: 'All operations completed',
1024
+ },
1025
+ ],
1026
+ ]);
1027
+ });
1028
+ });