@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,1032 @@
1
+ import { z } from 'zod';
2
+
3
+ import {
4
+ ContentBlock,
5
+ createMsg,
6
+ getContentBlocks,
7
+ Msg,
8
+ ToolCallBlock,
9
+ ToolResultBlock,
10
+ } from '../message';
11
+ import { ChatModelBase, ChatResponse, ChatUsage } from '../model';
12
+ import { Toolkit, ToolResponse } from '../tool';
13
+ import { ActingOptions, ObserveOptions, ReasoningOptions, ReplyOptions } from './interfaces';
14
+ import {
15
+ AgentEvent,
16
+ EventType,
17
+ ModelCallEndedEvent,
18
+ ModelCallStartedEvent,
19
+ RunFinishedEvent,
20
+ RunStartedEvent,
21
+ TextBlockDeltaEvent,
22
+ TextBlockEndEvent,
23
+ TextBlockStartEvent,
24
+ ThinkingBlockDeltaEvent,
25
+ ThinkingBlockEndEvent,
26
+ ThinkingBlockStartEvent,
27
+ ToolCallDeltaEvent,
28
+ ToolCallEndEvent,
29
+ ToolCallStartEvent,
30
+ ToolResultBinaryDeltaEvent,
31
+ ToolResultEndEvent,
32
+ ToolResultStartEvent,
33
+ ToolResultTextDeltaEvent,
34
+ } from '../event';
35
+ import { StorageBase } from '../storage';
36
+
37
+ const DEFAULT_COMPRESSION_PROMPT =
38
+ '<system-hint>You have been working on the task described above but have not yet completed it. ' +
39
+ 'Now write a continuation summary that will allow you to resume work efficiently in a future context window ' +
40
+ 'where the conversation history will be replaced with this summary. ' +
41
+ 'Your summary should be structured, concise, and actionable.</system-hint>';
42
+
43
+ const DEFAULT_SUMMARY_SCHEMA = z.object({
44
+ task_overview: z
45
+ .string()
46
+ .max(300)
47
+ .describe(
48
+ "The user\'s core request and success criteria. Any clarifications or constraints they specified"
49
+ ),
50
+ current_state: z
51
+ .string()
52
+ .max(300)
53
+ .describe(
54
+ 'What has been completed so far. File created, modified, or analyzed (with paths if relevant). Key outputs or artifacts produced.'
55
+ ),
56
+ important_discoveries: z
57
+ .string()
58
+ .max(300)
59
+ .describe(
60
+ "Technical constraints or requirements uncovered. Decisions made and their rationale. Errors encountered and how they were resolved. What approaches were tried that didn\'t work (and why)"
61
+ ),
62
+ next_steps: z
63
+ .string()
64
+ .max(200)
65
+ .describe(
66
+ 'Specific actions needed to complete the task. Any blockers or open questions to resolve. Priority order if multiple steps remain'
67
+ ),
68
+ context_to_preserve: z
69
+ .string()
70
+ .max(300)
71
+ .describe(
72
+ "User preferences or style requirements. Domain-specific details that aren\'t obvious. Any promises made to the user"
73
+ ),
74
+ });
75
+
76
+ export interface CompressionConfig {
77
+ /**
78
+ * Whether to enable memory compression.
79
+ */
80
+ enabled: boolean;
81
+ /**
82
+ * The token count threshold to trigger memory compression.
83
+ */
84
+ triggerThreshold: number;
85
+ /**
86
+ * The function to count the tokens of the messages in memory. If not provided, a heuristic token counting method will be used by default.
87
+ */
88
+ tokenCountFunc?: (msgs: Msg[]) => number;
89
+ /**
90
+ * The chat model used for compression. If not provided, the same model as the agent will be used by default.
91
+ */
92
+ compressionModel?: ChatModelBase;
93
+ /**
94
+ * The prompt template for memory compression. It should be designed to instruct the model to compress the input messages into a concise summary while preserving important information. If not provided, a default prompt will be used.
95
+ */
96
+ compressionPrompt?: string;
97
+ /**
98
+ * The JSON schema for the compressed summary. The model will be guided to compress the memory into a structured summary following this schema. If not provided, a default schema with a single text field will be used.
99
+ */
100
+ summarySchema?: z.ZodObject;
101
+ /**
102
+ * The number of recent messages to keep in the context without compression.
103
+ */
104
+ keepRecent?: number;
105
+ }
106
+
107
+ export interface AgentOptions {
108
+ name: string;
109
+ sysPrompt: string;
110
+ model: ChatModelBase;
111
+ maxIters?: number;
112
+ toolkit?: Toolkit;
113
+ storage?: StorageBase;
114
+ compressionConfig?: CompressionConfig;
115
+ }
116
+
117
+ /**
118
+ * The unified agent class in AgentScope library.
119
+ */
120
+ export class Agent {
121
+ // Agent configuration
122
+ name: string;
123
+ model: ChatModelBase;
124
+ maxIters: number;
125
+ toolkit: Toolkit;
126
+ storage?: StorageBase;
127
+ context: Msg[];
128
+ private _loaded: boolean;
129
+ private _sysPrompt: string;
130
+ compressionConfig?: CompressionConfig;
131
+
132
+ // Agent state
133
+ replyId: string;
134
+ curIter: number;
135
+ confirmedToolCallIds: string[];
136
+ curSummary: string;
137
+
138
+ /**
139
+ * Initialize an agent instance with the given parameters.
140
+ *
141
+ * @param options - The agent configuration options.
142
+ * @param options.name - The name of the agent.
143
+ * @param options.sysPrompt - The system prompt for the agent.
144
+ * @param options.model - The chat model to use.
145
+ * @param options.maxIters - Maximum iterations (default: 5).
146
+ * @param options.memory - Memory storage (default: InMemoryMemory).
147
+ * @param options.toolkit - Toolkit for tools (default: Toolkit).
148
+ */
149
+ constructor(options: AgentOptions) {
150
+ // Check maxIters mast be greater than 0
151
+ if (options.maxIters !== undefined && options.maxIters <= 0) {
152
+ throw new Error('maxIters must be greater than 0');
153
+ }
154
+
155
+ this.name = options.name;
156
+ this._sysPrompt = options.sysPrompt;
157
+ this.model = options.model;
158
+ this.maxIters = options.maxIters ?? 20;
159
+ this.context = [];
160
+ this.toolkit = options.toolkit ?? new Toolkit();
161
+ this.storage = options.storage;
162
+ this.compressionConfig = options.compressionConfig;
163
+
164
+ // Record if the agent state has been loaded from storage to avoid repeat loading
165
+ this._loaded = false;
166
+
167
+ // The states that tracks the current reply session
168
+ this.replyId = '';
169
+ this.curIter = 0;
170
+ this.confirmedToolCallIds = [];
171
+ this.curSummary = '';
172
+ }
173
+
174
+ /**
175
+ * Load the state from the storage if storage is provided and not loaded yet.
176
+ */
177
+ async loadState() {
178
+ if (this._loaded || !this.storage) return;
179
+ const { context, metadata } = await this.storage.loadAgentState({ agentId: this.name });
180
+ console.log(`Load state for agent "${this.name}" from storage:`, { context, metadata });
181
+ this.context = context;
182
+ this.replyId = (metadata.replyId as string) || '';
183
+ this.curIter = (metadata.curIter as number) || 0;
184
+ this.curSummary = (metadata.curSummary as string) || '';
185
+ this._loaded = true;
186
+ }
187
+
188
+ /**
189
+ * Save the state of the current reply session to storage if storage is provided.
190
+ */
191
+ async saveState() {
192
+ if (!this.storage) return;
193
+ await this.storage.saveAgentState({
194
+ agentId: this.name,
195
+ context: this.context,
196
+ metadata: {
197
+ replyId: this.replyId,
198
+ curIter: this.curIter,
199
+ curSummary: this.curSummary,
200
+ },
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Get the system prompt of the agent.
206
+ *
207
+ * @returns The system prompt string.
208
+ */
209
+ public get sysPrompt() {
210
+ const skillsPrompt = this.toolkit.getSkillsPrompt();
211
+ if (skillsPrompt.length > 0) {
212
+ return this._sysPrompt + '\n\n' + skillsPrompt;
213
+ }
214
+ return this._sysPrompt;
215
+ }
216
+
217
+ /**
218
+ * Reply to the given message and stream agent events as they are generated.
219
+ *
220
+ * @param options - The reply options containing the incoming message.
221
+ * @returns An async generator that yields agent events and resolves to the final reply message.
222
+ */
223
+ public async *replyStream(options: ReplyOptions): AsyncGenerator<AgentEvent, Msg> {
224
+ // Load the agent state from storage if not loaded yet
225
+ await this.loadState();
226
+ try {
227
+ // Yield the reply stream
228
+ return yield* this._reply(options);
229
+ } finally {
230
+ await this.saveState();
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Reply to the given message, consuming all streamed events internally.
236
+ *
237
+ * @param options - The reply options containing the incoming message.
238
+ * @param options.msgs - The incoming message(s) to reply to.
239
+ * @returns A promise that resolves to the final reply message.
240
+ */
241
+ public async reply(options: ReplyOptions): Promise<Msg> {
242
+ // Load the agent state from storage if not loaded yet
243
+ await this.loadState();
244
+ try {
245
+ const res = this._reply(options);
246
+ while (true) {
247
+ const { value, done } = await res.next();
248
+ if (done) {
249
+ return value as Msg;
250
+ }
251
+ }
252
+ } finally {
253
+ await this.saveState();
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Save the given content blocks into the context as a new block in the last assistant message,
259
+ * or create a new assistant message if the last message is not from the assistant or has a different name.
260
+ * @param blocks
261
+ * @param usage
262
+ */
263
+ protected _saveToContext(blocks: ContentBlock[], usage?: ChatUsage): void {
264
+ const lastMsg = this.context.at(-1);
265
+ if (this.context.length === 0) {
266
+ this.context.push(
267
+ createMsg({ name: this.name, content: blocks, role: 'assistant', usage })
268
+ );
269
+ } else if (lastMsg && lastMsg.role === 'assistant' && lastMsg.name === this.name) {
270
+ lastMsg.content.push(...blocks);
271
+ if (usage) {
272
+ if (!lastMsg.usage) {
273
+ lastMsg.usage = {
274
+ inputTokens: 0,
275
+ outputTokens: 0,
276
+ };
277
+ }
278
+ lastMsg.usage.inputTokens = lastMsg.usage.inputTokens + usage.inputTokens;
279
+ lastMsg.usage.outputTokens = lastMsg.usage.outputTokens + usage.outputTokens;
280
+ }
281
+ } else {
282
+ this.context.push(
283
+ createMsg({ name: this.name, content: blocks, role: 'assistant', usage })
284
+ );
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Get the pending tool calls that have no results yet in the context.
290
+ * @returns An array of pending `ToolCallBlock`s that are waiting for execution results.
291
+ */
292
+ protected _getPendingToolCalls(): ToolCallBlock[] {
293
+ if (this.context.length === 0) return [];
294
+
295
+ const lastMsg = this.context.at(-1);
296
+ if (!lastMsg) return [];
297
+ if (lastMsg.role === 'assistant') {
298
+ const toolCalls = getContentBlocks(lastMsg, 'tool_call');
299
+ const toolResults = getContentBlocks(lastMsg, 'tool_result');
300
+ return toolCalls.filter(toolCall => !toolResults.some(tr => tr.id === toolCall.id));
301
+ }
302
+ return [];
303
+ }
304
+
305
+ /**
306
+ * Get the awaiting tool calls that require user confirmation or external execution.
307
+ * @returns An array of `ToolCallBlock`s that are waiting for user confirmation or external execution.
308
+ */
309
+ protected _getAwaitingToolCalls(): {
310
+ awaitingType?: EventType.REQUIRE_USER_CONFIRM | EventType.REQUIRE_EXTERNAL_EXECUTION;
311
+ expectedEventType?: EventType.USER_CONFIRM_RESULT | EventType.EXTERNAL_EXECUTION_RESULT;
312
+ awaitingToolCalls: ToolCallBlock[];
313
+ preToolCalls: ToolCallBlock[];
314
+ } {
315
+ // If there is awaiting tool calls within the last assistant message in the context
316
+ const pendingToolCalls = this._getPendingToolCalls();
317
+
318
+ // The tool calls that should be executed before yield the (maybe have) user-confirm or external-execution event
319
+ const preToolCalls: ToolCallBlock[] = [];
320
+ for (const [index, toolCall] of pendingToolCalls.entries()) {
321
+ if (
322
+ this.toolkit.requireUserConfirm(toolCall.name) &&
323
+ !this.confirmedToolCallIds.includes(toolCall.id)
324
+ ) {
325
+ toolCall.awaitUserConfirmation = true;
326
+ // Find the continuous tool calls that require user confirmation
327
+ let i = index + 1;
328
+ for (; i < pendingToolCalls.length; i++) {
329
+ const nextToolCall = pendingToolCalls[i];
330
+ if (
331
+ !this.toolkit.requireUserConfirm(nextToolCall.name) ||
332
+ this.confirmedToolCallIds.includes(nextToolCall.id)
333
+ )
334
+ break;
335
+ nextToolCall.awaitUserConfirmation = true;
336
+ }
337
+ return {
338
+ awaitingType: EventType.REQUIRE_USER_CONFIRM,
339
+ expectedEventType: EventType.USER_CONFIRM_RESULT,
340
+ awaitingToolCalls: pendingToolCalls.slice(index, i),
341
+ preToolCalls,
342
+ };
343
+ }
344
+
345
+ if (this.toolkit.requireExternalExecution(toolCall.name)) {
346
+ // Find the continuous tool calls that require external execution
347
+ let i = index + 1;
348
+ for (; i < pendingToolCalls.length; i++) {
349
+ const nextToolCall = pendingToolCalls[i];
350
+ if (!this.toolkit.requireExternalExecution(nextToolCall.name)) break;
351
+ }
352
+ return {
353
+ awaitingType: EventType.REQUIRE_EXTERNAL_EXECUTION,
354
+ expectedEventType: EventType.EXTERNAL_EXECUTION_RESULT,
355
+ awaitingToolCalls: pendingToolCalls.slice(index, i),
356
+ preToolCalls,
357
+ };
358
+ }
359
+
360
+ preToolCalls.push(toolCall);
361
+ }
362
+ return { awaitingToolCalls: [], preToolCalls };
363
+ }
364
+
365
+ /**
366
+ * Core reply logic without middlewares. Observes the incoming message, runs
367
+ * reasoning/acting iterations up to `maxIters`, and returns the final response.
368
+ *
369
+ * @param options - The reply options containing the incoming message.
370
+ * @returns An async generator that yields agent events and resolves to the final reply message.
371
+ */
372
+ protected async *_reply(options?: ReplyOptions): AsyncGenerator<AgentEvent, Msg> {
373
+ const { expectedEventType } = this._getAwaitingToolCalls();
374
+ if (expectedEventType) {
375
+ // Checking
376
+ if (!options || !options.event || options.event.type !== expectedEventType) {
377
+ throw new Error(
378
+ `Agent is awaiting for '${expectedEventType}' confirmation, but received event of type '${options?.event?.type ?? 'none'}'.`
379
+ );
380
+ }
381
+
382
+ // handle the external execution result event
383
+ const event = options.event;
384
+ if (event.type === EventType.EXTERNAL_EXECUTION_RESULT) {
385
+ // Record the tool results into context and go on acting
386
+ this._saveToContext(event.executionResults);
387
+ } else if (event.type === EventType.USER_CONFIRM_RESULT) {
388
+ for (const result of event.confirmResults) {
389
+ if (result.confirmed) {
390
+ this.confirmedToolCallIds.push(result.toolCall.id);
391
+ } else {
392
+ // If user rejected, add a rejection result and handle the pending tool calls
393
+ const rejectionRes = `<system-info>**Note** the user rejected the execution of tool "${result.toolCall.name}"!</system-info>`;
394
+ yield {
395
+ id: crypto.randomUUID(),
396
+ createdAt: new Date().toISOString(),
397
+ type: EventType.TOOL_RESULT_START,
398
+ replyId: this.replyId,
399
+ toolCallId: result.toolCall.id,
400
+ } as ToolResultStartEvent;
401
+ yield {
402
+ id: crypto.randomUUID(),
403
+ createdAt: new Date().toISOString(),
404
+ type: EventType.TOOL_RESULT_TEXT_DELTA,
405
+ replyId: this.replyId,
406
+ toolCallId: result.toolCall.id,
407
+ delta: rejectionRes,
408
+ } as ToolResultTextDeltaEvent;
409
+ yield {
410
+ id: crypto.randomUUID(),
411
+ createdAt: new Date().toISOString(),
412
+ type: EventType.TOOL_RESULT_END,
413
+ replyId: this.replyId,
414
+ toolCallId: result.toolCall.id,
415
+ state: 'interrupted',
416
+ } as ToolResultEndEvent;
417
+ this._saveToContext([
418
+ {
419
+ type: 'tool_result',
420
+ id: result.toolCall.id,
421
+ name: result.toolCall.name,
422
+ output: [
423
+ {
424
+ id: crypto.randomUUID(),
425
+ type: 'text',
426
+ text: `<system-info>**Note** the user rejected the execution of tool "${result.toolCall.name}"!</system-info>`,
427
+ },
428
+ ],
429
+ state: 'interrupted',
430
+ },
431
+ ]);
432
+ }
433
+ }
434
+ // Remove the tool call from the awaiting state
435
+ const processedToolCallIds = event.confirmResults.map(result => result.toolCall.id);
436
+ // Set the awaitingUserConfirmation flag to undefined for UI update
437
+ this.context.at(-1)?.content.forEach(content => {
438
+ if (content.type === 'tool_call' && processedToolCallIds.includes(content.id)) {
439
+ delete content.awaitUserConfirmation;
440
+ }
441
+ });
442
+ }
443
+ } else {
444
+ // The normal reply flow starts without any external event
445
+ this.curIter = 0;
446
+ this.replyId = crypto.randomUUID();
447
+ this.confirmedToolCallIds = [];
448
+
449
+ // Yield the run started event
450
+ yield {
451
+ id: crypto.randomUUID(),
452
+ type: EventType.RUN_STARTED,
453
+ createdAt: new Date().toISOString(),
454
+ sessionId: '',
455
+ replyId: this.replyId,
456
+ name: this.name,
457
+ role: 'assistant',
458
+ } as RunStartedEvent;
459
+ }
460
+
461
+ // Store the incoming message into memory
462
+ if (Array.isArray(options?.msgs)) {
463
+ // await this.memory.add(options.msg);
464
+ this.context.push(...options.msgs);
465
+ } else if (options?.msgs) {
466
+ this.context.push(options.msgs);
467
+ }
468
+
469
+ while (this.curIter < this.maxIters) {
470
+ const pendingToolCalls = this._getPendingToolCalls();
471
+ if (pendingToolCalls.length === 0) {
472
+ await this.compressMemoryIfNeeded();
473
+ const reasoningResponse = yield* this._reasoning({ toolChoice: 'auto' });
474
+ this._saveToContext(reasoningResponse.content, reasoningResponse.usage);
475
+ }
476
+
477
+ // Extract the awaiting tool calls and those should be executed before yielding human-in-the-loop events
478
+ const { awaitingType, awaitingToolCalls, preToolCalls } = this._getAwaitingToolCalls();
479
+ // Execute the pre-tool calls before yielding the user-confirm or external-execution event if there is any
480
+ for (const toolCall of preToolCalls) {
481
+ const actingContent = yield* this._acting({ toolCall });
482
+ this._saveToContext([actingContent]);
483
+ // Consume the confirmation after execution
484
+ this.confirmedToolCallIds = this.confirmedToolCallIds.filter(
485
+ id => id !== toolCall.id
486
+ );
487
+ }
488
+
489
+ // yield the user-confirm or external-execution event if there is any awaiting tool calls
490
+ if (awaitingType) {
491
+ yield {
492
+ id: crypto.randomUUID(),
493
+ createdAt: new Date().toISOString(),
494
+ type: awaitingType,
495
+ replyId: this.replyId,
496
+ toolCalls: awaitingToolCalls,
497
+ };
498
+
499
+ return createMsg({
500
+ name: this.name,
501
+ content: [
502
+ {
503
+ id: crypto.randomUUID(),
504
+ type: 'text',
505
+ text:
506
+ awaitingType === EventType.REQUIRE_USER_CONFIRM
507
+ ? 'Waiting for user confirmation ...'
508
+ : 'Waiting for external execution ...',
509
+ },
510
+ ],
511
+ role: 'assistant',
512
+ });
513
+ }
514
+
515
+ // Break the loop if there is no tool call in the reasoning message
516
+ if (preToolCalls.length === 0) break;
517
+
518
+ this.curIter += 1;
519
+ }
520
+
521
+ // If exceed max iterations without text output
522
+ if (this.context.at(-1)?.content.at(-1)?.type !== 'text') {
523
+ // Generate a final response
524
+ const summaryResponse = yield* this._reasoning({ toolChoice: 'none' });
525
+ this._saveToContext(summaryResponse.content, summaryResponse.usage);
526
+ }
527
+
528
+ // Yield the run finished event
529
+ yield {
530
+ id: crypto.randomUUID(),
531
+ type: EventType.RUN_FINISHED,
532
+ createdAt: new Date().toISOString(),
533
+ sessionId: '',
534
+ replyId: this.replyId,
535
+ } as RunFinishedEvent;
536
+
537
+ return createMsg({
538
+ id: this.replyId,
539
+ name: this.name,
540
+ // Must be a string for the final output message
541
+ content: [this.context.at(-1)!.content.at(-1)!],
542
+ role: 'assistant',
543
+ });
544
+ }
545
+
546
+ /**
547
+ * Core reasoning logic without middlewares. Calls the model with the current
548
+ * memory and system prompt, converts the response to agent events, and saves
549
+ * the resulting message to memory.
550
+ *
551
+ * @param options - The reasoning options, including tool choice strategy.
552
+ * @returns An async generator that yields agent events and resolves to the content blocks of the model response.
553
+ */
554
+ protected async *_reasoning(
555
+ options: ReasoningOptions
556
+ ): AsyncGenerator<AgentEvent, ChatResponse> {
557
+ const tools = this.toolkit.getJSONSchemas();
558
+ yield {
559
+ id: crypto.randomUUID(),
560
+ createdAt: new Date().toISOString(),
561
+ type: EventType.MODEL_CALL_STARTED,
562
+ replyId: this.replyId,
563
+ modelName: this.model.modelName,
564
+ } as ModelCallStartedEvent;
565
+ const res = await this.model.call({
566
+ messages: [
567
+ createMsg({
568
+ name: 'system',
569
+ content: [{ type: 'text', text: this.sysPrompt, id: crypto.randomUUID() }],
570
+ role: 'system',
571
+ }),
572
+ ...(this.curSummary
573
+ ? [
574
+ createMsg({
575
+ name: 'user',
576
+ content: [
577
+ { type: 'text', text: this.curSummary, id: crypto.randomUUID() },
578
+ ],
579
+ role: 'user',
580
+ }),
581
+ ]
582
+ : []),
583
+ ...this.context,
584
+ ],
585
+ tools,
586
+ toolChoice: options.toolChoice,
587
+ });
588
+
589
+ let blockIds = {
590
+ textBlockId: null,
591
+ thinkingBlockId: null,
592
+ toolCallIds: [],
593
+ } as {
594
+ textBlockId: string | null;
595
+ thinkingBlockId: string | null;
596
+ toolCallIds: string[];
597
+ };
598
+ let completedResponse: ChatResponse;
599
+ if (this.model.stream) {
600
+ // Handle streaming response
601
+ while (true) {
602
+ const { value, done } = await (
603
+ res as AsyncGenerator<ChatResponse, ChatResponse>
604
+ ).next();
605
+ if (done) {
606
+ // The complete response is returned in the `value` when `done` is true
607
+ completedResponse = value as ChatResponse;
608
+ break;
609
+ }
610
+ const chunk = value as ChatResponse;
611
+ yield* this.convertChatResponseToEvent(blockIds, chunk);
612
+ }
613
+ } else {
614
+ // Handle non-streaming response, the res is a ChatResponse instance
615
+ completedResponse = res as ChatResponse;
616
+ yield* this.convertChatResponseToEvent(blockIds, res as ChatResponse);
617
+ }
618
+
619
+ // Send end events for text message, thinking message and tool calls
620
+ if (blockIds.textBlockId) {
621
+ yield {
622
+ id: crypto.randomUUID(),
623
+ createdAt: new Date().toISOString(),
624
+ type: EventType.TEXT_BLOCK_END,
625
+ replyId: this.replyId,
626
+ blockId: blockIds.textBlockId,
627
+ } as TextBlockEndEvent;
628
+ }
629
+ if (blockIds.thinkingBlockId) {
630
+ yield {
631
+ id: crypto.randomUUID(),
632
+ createdAt: new Date().toISOString(),
633
+ type: EventType.THINKING_BLOCK_END,
634
+ replyId: this.replyId,
635
+ blockId: blockIds.thinkingBlockId,
636
+ } as ThinkingBlockEndEvent;
637
+ }
638
+ if (blockIds.toolCallIds.length > 0) {
639
+ for (const toolCallId of blockIds.toolCallIds) {
640
+ yield {
641
+ id: crypto.randomUUID(),
642
+ createdAt: new Date().toISOString(),
643
+ type: EventType.TOOL_CALL_END,
644
+ replyId: this.replyId,
645
+ toolCallId,
646
+ } as ToolCallEndEvent;
647
+ }
648
+ }
649
+
650
+ yield {
651
+ id: crypto.randomUUID(),
652
+ createdAt: new Date().toISOString(),
653
+ type: EventType.MODEL_CALL_ENDED,
654
+ replyId: this.replyId,
655
+ inputTokens: completedResponse.usage?.inputTokens || 0,
656
+ outputTokens: completedResponse.usage?.outputTokens || 0,
657
+ } as ModelCallEndedEvent;
658
+
659
+ return completedResponse;
660
+ }
661
+
662
+ /**
663
+ * Core acting logic without middlewares. Executes the given tool call, streams
664
+ * intermediate tool result events, and saves the final tool response to memory.
665
+ *
666
+ * @param options - The acting options containing the tool call to execute.
667
+ * @returns An async generator that yields tool result events.
668
+ */
669
+ protected async *_acting(options: ActingOptions): AsyncGenerator<AgentEvent, ToolResultBlock> {
670
+ const res = this.toolkit.callToolFunction(options.toolCall);
671
+
672
+ yield {
673
+ type: EventType.TOOL_RESULT_START,
674
+ id: crypto.randomUUID(),
675
+ createdAt: new Date().toISOString(),
676
+ replyId: this.replyId,
677
+ toolCallId: options.toolCall.id,
678
+ toolCallName: options.toolCall.name,
679
+ } as ToolResultStartEvent;
680
+
681
+ while (true) {
682
+ const { value, done } = await res.next();
683
+ if (done) {
684
+ return {
685
+ type: 'tool_result',
686
+ id: options.toolCall.id,
687
+ name: options.toolCall.name,
688
+ output: value.content,
689
+ state: value.state,
690
+ } as ToolResultBlock;
691
+ }
692
+ yield* this.convertToolResponseToEvent(options.toolCall, value);
693
+ }
694
+ }
695
+
696
+ /**
697
+ * Receive external observation message(s) and save them into memory.
698
+ *
699
+ * @param options - The observe options containing the message to store.
700
+ * @returns A promise that resolves when the message has been saved to memory.
701
+ */
702
+ protected async _observe(options: ObserveOptions): Promise<void> {
703
+ // Load the agent state from storage if not loaded yet
704
+ await this.loadState();
705
+
706
+ if (Array.isArray(options.msg)) {
707
+ // await this.memory.add(options.msg);
708
+ this.context.push(...options.msg);
709
+ } else if (options.msg) {
710
+ this.context.push(options.msg);
711
+ }
712
+ }
713
+
714
+ /**
715
+ * Convert a `ChatResponse` chunk into a sequence of typed agent events.
716
+ * Tracks message IDs across calls via the mutable `responseId` object so that
717
+ * start/content/end events are correctly correlated.
718
+ *
719
+ * @param responseId - Mutable object tracking IDs for the current text, thinking, and tool-call messages.
720
+ * @param responseId.textMessageId - ID of the in-progress text message, or `null` if not yet started.
721
+ * @param responseId.thinkingMessageId - ID of the in-progress thinking message, or `null` if not yet started.
722
+ * @param responseId.textBlockId
723
+ * @param responseId.thinkingBlockId
724
+ * @param responseId.toolCallIds - List of tool-call IDs seen so far in this response.
725
+ * @param chunk - The chat response chunk to convert.
726
+ * @returns An async generator that yields the corresponding agent events.
727
+ */
728
+ protected async *convertChatResponseToEvent(
729
+ responseId: {
730
+ textBlockId: string | null;
731
+ thinkingBlockId: string | null;
732
+ toolCallIds: string[];
733
+ },
734
+ chunk: ChatResponse
735
+ ): AsyncGenerator<AgentEvent> {
736
+ for (const block of chunk.content) {
737
+ switch (block.type) {
738
+ case 'text':
739
+ if (responseId.textBlockId === null) {
740
+ // A new uuid
741
+ responseId.textBlockId = crypto.randomUUID();
742
+ yield {
743
+ id: crypto.randomUUID(),
744
+ createdAt: new Date().toISOString(),
745
+ type: EventType.TEXT_BLOCK_START,
746
+ replyId: this.replyId,
747
+ blockId: responseId.textBlockId,
748
+ } as TextBlockStartEvent;
749
+ }
750
+ yield {
751
+ id: crypto.randomUUID(),
752
+ createdAt: new Date().toISOString(),
753
+ type: EventType.TEXT_BLOCK_DELTA,
754
+ replyId: this.replyId,
755
+ blockId: responseId.textBlockId,
756
+ delta: block.text,
757
+ } as TextBlockDeltaEvent;
758
+ break;
759
+
760
+ case 'thinking':
761
+ if (responseId.thinkingBlockId === null) {
762
+ responseId.thinkingBlockId = crypto.randomUUID();
763
+ yield {
764
+ id: crypto.randomUUID(),
765
+ createdAt: new Date().toISOString(),
766
+ type: EventType.THINKING_BLOCK_START,
767
+ replyId: this.replyId,
768
+ blockId: responseId.thinkingBlockId,
769
+ } as ThinkingBlockStartEvent;
770
+ }
771
+ yield {
772
+ id: crypto.randomUUID(),
773
+ createdAt: new Date().toISOString(),
774
+ type: EventType.THINKING_BLOCK_DELTA,
775
+ replyId: this.replyId,
776
+ blockId: responseId.thinkingBlockId,
777
+ delta: block.thinking,
778
+ } as ThinkingBlockDeltaEvent;
779
+ break;
780
+
781
+ case 'tool_call':
782
+ if (!responseId.toolCallIds.includes(block.id)) {
783
+ responseId.toolCallIds.push(block.id);
784
+ yield {
785
+ id: crypto.randomUUID(),
786
+ type: EventType.TOOL_CALL_START,
787
+ createdAt: new Date().toISOString(),
788
+ replyId: this.replyId,
789
+ toolCallId: block.id,
790
+ toolCallName: block.name,
791
+ } as ToolCallStartEvent;
792
+ }
793
+ yield {
794
+ id: crypto.randomUUID(),
795
+ createdAt: new Date().toISOString(),
796
+ type: EventType.TOOL_CALL_DELTA,
797
+ delta: block.input,
798
+ replyId: this.replyId,
799
+ toolCallId: block.id,
800
+ } as ToolCallDeltaEvent;
801
+ }
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Convert a `ToolResponse` into a sequence of typed agent events, followed by
807
+ * a final `TOOL_RESULT_END` event.
808
+ *
809
+ * @param toolCall - The original tool-use block that triggered this response.
810
+ * @param toolRes - The tool response containing result content blocks.
811
+ * @returns An async generator that yields tool result events.
812
+ */
813
+ protected async *convertToolResponseToEvent(toolCall: ToolCallBlock, toolRes: ToolResponse) {
814
+ for (const block of toolRes.content) {
815
+ switch (block.type) {
816
+ case 'text':
817
+ yield {
818
+ id: crypto.randomUUID(),
819
+ createdAt: new Date().toISOString(),
820
+ type: EventType.TOOL_RESULT_TEXT_DELTA,
821
+ replyId: this.replyId,
822
+ toolCallId: toolCall.id,
823
+ delta: block.text,
824
+ } as ToolResultTextDeltaEvent;
825
+ break;
826
+
827
+ case 'data':
828
+ if (block.source.type === 'base64') {
829
+ yield {
830
+ id: crypto.randomUUID(),
831
+ createdAt: new Date().toISOString(),
832
+ type: EventType.TOOL_RESULT_BINARY_DELTA,
833
+ replyId: this.replyId,
834
+ toolCallId: toolCall.id,
835
+ mediaType: block.source.mediaType,
836
+ data: block.source.data,
837
+ } as ToolResultBinaryDeltaEvent;
838
+ } else if (block.source.type === 'url') {
839
+ yield {
840
+ id: crypto.randomUUID(),
841
+ createdAt: new Date().toISOString(),
842
+ type: EventType.TOOL_RESULT_BINARY_DELTA,
843
+ replyId: this.replyId,
844
+ toolCallId: toolCall.id,
845
+ mediaType: block.source.mediaType,
846
+ url: block.source.url,
847
+ } as ToolResultBinaryDeltaEvent;
848
+ }
849
+ break;
850
+ }
851
+ }
852
+ yield {
853
+ id: crypto.randomUUID(),
854
+ createdAt: new Date().toISOString(),
855
+ type: EventType.TOOL_RESULT_END,
856
+ replyId: this.replyId,
857
+ toolCallId: toolCall.id,
858
+ state: toolRes.state,
859
+ } as ToolResultEndEvent;
860
+ }
861
+
862
+ /**
863
+ * Convert the agent instance to a JSON-serializable object.
864
+ * @returns An object containing the agent's name and system prompt.
865
+ */
866
+ public async toJSON() {
867
+ return {
868
+ replyId: this.replyId,
869
+ confirmedToolCallIds: this.confirmedToolCallIds,
870
+ curIter: this.curIter,
871
+ };
872
+ }
873
+
874
+ /**
875
+ * Split the current context into two parts: one part that needs to be compressed and another part that should be reserved based on the compression configuration. The method calculates how many recent "units" (blocks or tool call pairs) to keep uncompressed according to the `keepRecent` setting in the compression configuration, and ensures that tool calls and their corresponding results are not separated during the split.
876
+ * @returns An object containing the `toCompressedContext` which includes the messages that need to be compressed, and the `reservedContext` which includes the recent messages that should be kept uncompressed.
877
+ */
878
+ protected _splitContextForCompression() {
879
+ let toCompressedContext: Msg[] = [];
880
+ let reservedContext: Msg[] = [];
881
+
882
+ // Calculate which messages need to be compressed
883
+ // keepRecent specifies the number of recent "units" to keep uncompressed
884
+ // A unit is either: a single block (text/thinking), or a tool_call + tool_result pair
885
+ const keepRecent = this.compressionConfig!.keepRecent ?? 0;
886
+
887
+ const nBlocks = this.context.map(msg => msg.content.length).reduce((a, b) => a + b, 0);
888
+ const toCompressedBlockNumber = nBlocks - keepRecent > 0 ? nBlocks - keepRecent : 0;
889
+
890
+ let currentCompressedBlocks = 0;
891
+ for (const [index, msg] of this.context.entries()) {
892
+ if (currentCompressedBlocks + msg.content.length <= toCompressedBlockNumber) {
893
+ toCompressedContext.push(msg);
894
+ currentCompressedBlocks += msg.content.length;
895
+ } else {
896
+ // The blocks that should be reserved according to the keepRecent count
897
+ const reservedBlocks = msg.content.slice(
898
+ toCompressedBlockNumber - currentCompressedBlocks
899
+ );
900
+ // Check if the reserved blocks contain an unpaired tool_call or tool_result
901
+ const unPairedToolResultIds = new Set<string>();
902
+ for (const block of reservedBlocks) {
903
+ if (block.type == 'tool_call') {
904
+ unPairedToolResultIds.add(block.id);
905
+ } else if (block.type == 'tool_result') {
906
+ if (unPairedToolResultIds.has(block.id)) {
907
+ unPairedToolResultIds.delete(block.id);
908
+ }
909
+ }
910
+ }
911
+ // If there are unpaired tool calls, we need to move them to the reserved blocks
912
+ let i = toCompressedBlockNumber - currentCompressedBlocks - 1;
913
+ for (; i >= 0; i--) {
914
+ const block = msg.content[i];
915
+ if (block.type === 'tool_call' && unPairedToolResultIds.has(block.id)) {
916
+ unPairedToolResultIds.delete(block.id);
917
+ }
918
+ if (unPairedToolResultIds.size === 0) break;
919
+ }
920
+ // All contents in this message should be reserved if i
921
+ if (i <= 0) {
922
+ reservedContext.push(msg);
923
+ break;
924
+ }
925
+
926
+ // Slice the message content and push the reserved part to the compressed context
927
+ const lastMsg = { ...msg };
928
+ lastMsg.content = msg.content.slice(0, i);
929
+ toCompressedContext.push(lastMsg);
930
+
931
+ const reservedMsg = { ...msg };
932
+ reservedMsg.content = msg.content.slice(i);
933
+ reservedContext.push(reservedMsg);
934
+
935
+ // The rest messages should be reserved
936
+ reservedContext.push(...this.context.slice(index + 1));
937
+ break;
938
+ }
939
+ }
940
+ return { toCompressedContext, reservedContext };
941
+ }
942
+
943
+ /**
944
+ * Compress the agent's memory using the specified compression model (if provided) or the original model.
945
+ */
946
+ protected async compressMemoryIfNeeded() {
947
+ // The tool call and result pair must be kept or removed together
948
+ if (!this.compressionConfig || !this.compressionConfig.enabled) return;
949
+
950
+ const { toCompressedContext, reservedContext } = this._splitContextForCompression();
951
+
952
+ // Compress the toCompressedContext
953
+ if (
954
+ toCompressedContext.length <= 0 ||
955
+ (toCompressedContext.length === 1 && toCompressedContext.at(0)?.content.length === 1)
956
+ )
957
+ return;
958
+
959
+ // Compute if the context exceed the threshold
960
+ const messages = [
961
+ createMsg({
962
+ name: 'system',
963
+ content: [{ type: 'text', text: this.sysPrompt, id: crypto.randomUUID() }],
964
+ role: 'system',
965
+ }),
966
+ ...toCompressedContext,
967
+ // instructions to compress the context into a summary
968
+ createMsg({
969
+ name: 'user',
970
+ content: [
971
+ {
972
+ id: crypto.randomUUID(),
973
+ type: 'text',
974
+ text:
975
+ this.compressionConfig.compressionPrompt || DEFAULT_COMPRESSION_PROMPT,
976
+ },
977
+ ],
978
+ role: 'user',
979
+ }),
980
+ ];
981
+
982
+ const nTokens = await this.model.countTokens({
983
+ messages,
984
+ tools: this.toolkit.getJSONSchemas(),
985
+ });
986
+ console.debug(`[AGENT ${this.name}] Current context token count: ${nTokens}.`);
987
+ if (nTokens <= this.compressionConfig.triggerThreshold) return;
988
+
989
+ console.log(
990
+ `[AGENT ${this.name}] Compressing memory with ${toCompressedContext.length} messages.`
991
+ );
992
+ // Generate the summary structured content
993
+ const res = await this.model.callStructured({
994
+ messages: [
995
+ createMsg({
996
+ name: 'system',
997
+ content: [{ type: 'text', text: this.sysPrompt, id: crypto.randomUUID() }],
998
+ role: 'system',
999
+ }),
1000
+ ...toCompressedContext,
1001
+ // instructions to compress the context into a summary
1002
+ createMsg({
1003
+ name: 'user',
1004
+ content: [
1005
+ {
1006
+ id: crypto.randomUUID(),
1007
+ type: 'text',
1008
+ text:
1009
+ this.compressionConfig.compressionPrompt ||
1010
+ DEFAULT_COMPRESSION_PROMPT,
1011
+ },
1012
+ ],
1013
+ role: 'user',
1014
+ }),
1015
+ ],
1016
+ schema: this.compressionConfig.summarySchema || DEFAULT_SUMMARY_SCHEMA,
1017
+ });
1018
+
1019
+ // Make the compression summary
1020
+ let summaryText = '<system-reminder>Here is a summary of your previous work\n';
1021
+ for (const [key, value] of Object.entries(res.content)) {
1022
+ summaryText += `# ${key}\n${value}\n`;
1023
+ }
1024
+ summaryText += '</system-reminder>';
1025
+
1026
+ console.debug(`[AGENT ${this.name}] Compression summary: ${summaryText}`);
1027
+
1028
+ // Update the context with the compression summary and the reserved recent blocks
1029
+ this.context = reservedContext;
1030
+ this.curSummary = summaryText;
1031
+ }
1032
+ }