@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.
- package/dist/agent/index.d.mts +234 -0
- package/dist/agent/index.d.ts +234 -0
- package/dist/agent/index.js +1412 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/index.mjs +1375 -0
- package/dist/agent/index.mjs.map +1 -0
- package/dist/base-BOx3UzOl.d.mts +41 -0
- package/dist/base-BoIps2RL.d.ts +41 -0
- package/dist/base-C7jwyH4Z.d.mts +52 -0
- package/dist/base-Cwi4bjze.d.ts +127 -0
- package/dist/base-DYlBMCy_.d.mts +127 -0
- package/dist/base-NX-knWOv.d.ts +52 -0
- package/dist/block-VsnHrllL.d.mts +48 -0
- package/dist/block-VsnHrllL.d.ts +48 -0
- package/dist/event/index.d.mts +181 -0
- package/dist/event/index.d.ts +181 -0
- package/dist/event/index.js +58 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/index.mjs +33 -0
- package/dist/event/index.mjs.map +1 -0
- package/dist/formatter/index.d.mts +187 -0
- package/dist/formatter/index.d.ts +187 -0
- package/dist/formatter/index.js +647 -0
- package/dist/formatter/index.js.map +1 -0
- package/dist/formatter/index.mjs +616 -0
- package/dist/formatter/index.mjs.map +1 -0
- package/dist/index-BTJDlKvQ.d.mts +195 -0
- package/dist/index-BcatlwXQ.d.ts +195 -0
- package/dist/index-CAxQAkiP.d.mts +21 -0
- package/dist/index-CAxQAkiP.d.ts +21 -0
- package/dist/mcp/index.d.mts +9 -0
- package/dist/mcp/index.d.ts +9 -0
- package/dist/mcp/index.js +432 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/index.mjs +408 -0
- package/dist/mcp/index.mjs.map +1 -0
- package/dist/message/index.d.mts +10 -0
- package/dist/message/index.d.ts +10 -0
- package/dist/message/index.js +67 -0
- package/dist/message/index.js.map +1 -0
- package/dist/message/index.mjs +37 -0
- package/dist/message/index.mjs.map +1 -0
- package/dist/message-CkN21KaY.d.mts +99 -0
- package/dist/message-CzLeTlua.d.ts +99 -0
- package/dist/model/index.d.mts +377 -0
- package/dist/model/index.d.ts +377 -0
- package/dist/model/index.js +1880 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/index.mjs +1849 -0
- package/dist/model/index.mjs.map +1 -0
- package/dist/storage/index.d.mts +68 -0
- package/dist/storage/index.d.ts +68 -0
- package/dist/storage/index.js +250 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/index.mjs +212 -0
- package/dist/storage/index.mjs.map +1 -0
- package/dist/tool/index.d.mts +311 -0
- package/dist/tool/index.d.ts +311 -0
- package/dist/tool/index.js +1494 -0
- package/dist/tool/index.js.map +1 -0
- package/dist/tool/index.mjs +1447 -0
- package/dist/tool/index.mjs.map +1 -0
- package/dist/toolkit-CEpulFi0.d.ts +99 -0
- package/dist/toolkit-CGEZSZPa.d.mts +99 -0
- package/jest.config.js +11 -0
- package/package.json +92 -0
- package/src/_utils/common.ts +104 -0
- package/src/_utils/index.ts +1 -0
- package/src/agent/agent-base.ts +0 -0
- package/src/agent/agent.test.ts +1028 -0
- package/src/agent/agent.ts +1032 -0
- package/src/agent/index.ts +2 -0
- package/src/agent/interfaces.ts +23 -0
- package/src/agent/test-compression.ts +72 -0
- package/src/event/index.ts +250 -0
- package/src/formatter/base.ts +133 -0
- package/src/formatter/dashscope-chat-formatter.test.ts +372 -0
- package/src/formatter/dashscope-chat-formatter.ts +163 -0
- package/src/formatter/deepseek-chat-formatter.ts +130 -0
- package/src/formatter/index.ts +5 -0
- package/src/formatter/ollama-chat-formatter.ts +67 -0
- package/src/formatter/openai-chat-formatter.test.ts +263 -0
- package/src/formatter/openai-chat-formatter.ts +301 -0
- package/src/formatter/openai.md +767 -0
- package/src/mcp/base.ts +114 -0
- package/src/mcp/http.test.ts +303 -0
- package/src/mcp/http.ts +224 -0
- package/src/mcp/index.ts +2 -0
- package/src/mcp/stdio.test.ts +91 -0
- package/src/mcp/stdio.ts +119 -0
- package/src/message/block.ts +60 -0
- package/src/message/enums.ts +4 -0
- package/src/message/index.ts +12 -0
- package/src/message/message.test.ts +80 -0
- package/src/message/message.ts +131 -0
- package/src/model/base.ts +226 -0
- package/src/model/dashscope-model.test.ts +335 -0
- package/src/model/dashscope-model.ts +441 -0
- package/src/model/deepseek-model.test.ts +279 -0
- package/src/model/deepseek-model.ts +401 -0
- package/src/model/index.ts +7 -0
- package/src/model/ollama-model.test.ts +307 -0
- package/src/model/ollama-model.ts +356 -0
- package/src/model/openai-model.ts +327 -0
- package/src/model/response.ts +22 -0
- package/src/model/usage.ts +12 -0
- package/src/storage/base.ts +52 -0
- package/src/storage/file-system.test.ts +587 -0
- package/src/storage/file-system.ts +269 -0
- package/src/storage/index.ts +2 -0
- package/src/tool/base.ts +23 -0
- package/src/tool/bash.test.ts +174 -0
- package/src/tool/bash.ts +152 -0
- package/src/tool/edit.test.ts +83 -0
- package/src/tool/edit.ts +95 -0
- package/src/tool/glob.test.ts +63 -0
- package/src/tool/glob.ts +166 -0
- package/src/tool/grep.test.ts +74 -0
- package/src/tool/grep.ts +256 -0
- package/src/tool/index.ts +10 -0
- package/src/tool/read.test.ts +77 -0
- package/src/tool/read.ts +117 -0
- package/src/tool/response.ts +82 -0
- package/src/tool/task.test.ts +299 -0
- package/src/tool/task.ts +399 -0
- package/src/tool/toolkit.test.ts +636 -0
- package/src/tool/toolkit.ts +601 -0
- package/src/tool/write.test.ts +52 -0
- package/src/tool/write.ts +57 -0
- package/src/type/index.ts +52 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.cjs.json +11 -0
- package/tsconfig.esm.json +10 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +20 -0
- 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
|
+
}
|