@asm-agent/agent 0.8.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/README.md +502 -0
- package/dist/agent-loop.d.ts +24 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +656 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent.d.ts +109 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +442 -0
- package/dist/agent.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy.d.ts +59 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +268 -0
- package/dist/proxy.js.map +1 -0
- package/dist/types.d.ts +392 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent loop that works with AgentMessage throughout.
|
|
3
|
+
* Transforms to Message[] only at the LLM call boundary.
|
|
4
|
+
*/
|
|
5
|
+
import { EventStream, streamSimple, validateToolArguments, } from "@asm-agent/ai";
|
|
6
|
+
const ABORT_ERROR_MESSAGE = "Request was aborted";
|
|
7
|
+
const EMPTY_USAGE = {
|
|
8
|
+
input: 0,
|
|
9
|
+
output: 0,
|
|
10
|
+
cacheRead: 0,
|
|
11
|
+
cacheWrite: 0,
|
|
12
|
+
totalTokens: 0,
|
|
13
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
14
|
+
};
|
|
15
|
+
function createAbortError() {
|
|
16
|
+
return new Error(ABORT_ERROR_MESSAGE);
|
|
17
|
+
}
|
|
18
|
+
function throwIfAborted(signal) {
|
|
19
|
+
if (signal?.aborted) {
|
|
20
|
+
throw createAbortError();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function raceWithAbort(operation, signal, onAbort) {
|
|
24
|
+
if (!signal) {
|
|
25
|
+
return operation;
|
|
26
|
+
}
|
|
27
|
+
if (signal.aborted) {
|
|
28
|
+
onAbort?.();
|
|
29
|
+
void operation.catch(() => undefined);
|
|
30
|
+
return Promise.reject(createAbortError());
|
|
31
|
+
}
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
let settled = false;
|
|
34
|
+
const cleanup = () => {
|
|
35
|
+
signal.removeEventListener("abort", abort);
|
|
36
|
+
};
|
|
37
|
+
const abort = () => {
|
|
38
|
+
if (settled) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
settled = true;
|
|
42
|
+
cleanup();
|
|
43
|
+
onAbort?.();
|
|
44
|
+
reject(createAbortError());
|
|
45
|
+
};
|
|
46
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
47
|
+
operation.then((value) => {
|
|
48
|
+
if (settled) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
settled = true;
|
|
52
|
+
cleanup();
|
|
53
|
+
resolve(value);
|
|
54
|
+
}, (error) => {
|
|
55
|
+
if (settled) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
settled = true;
|
|
59
|
+
cleanup();
|
|
60
|
+
reject(error);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function maybePromiseWithAbort(operation, signal, onAbort) {
|
|
65
|
+
return raceWithAbort(Promise.resolve(operation), signal, onAbort);
|
|
66
|
+
}
|
|
67
|
+
function isAbortError(error) {
|
|
68
|
+
return error instanceof Error && (error.message === ABORT_ERROR_MESSAGE || error.name === "AbortError");
|
|
69
|
+
}
|
|
70
|
+
async function settlePostTurn(operation, signal) {
|
|
71
|
+
try {
|
|
72
|
+
return { status: "completed", value: await operation };
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (signal?.aborted && isAbortError(error)) {
|
|
76
|
+
return { status: "aborted" };
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function cloneAssistantContent(content) {
|
|
82
|
+
return content.map((part) => {
|
|
83
|
+
if (part.type === "toolCall") {
|
|
84
|
+
return { ...part, arguments: { ...part.arguments } };
|
|
85
|
+
}
|
|
86
|
+
return { ...part };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function cloneUsage(usage) {
|
|
90
|
+
return { ...usage, cost: { ...usage.cost } };
|
|
91
|
+
}
|
|
92
|
+
function createAbortedAssistantMessage(config, partialMessage) {
|
|
93
|
+
return {
|
|
94
|
+
role: "assistant",
|
|
95
|
+
content: partialMessage ? cloneAssistantContent(partialMessage.content) : [{ type: "text", text: "" }],
|
|
96
|
+
api: partialMessage?.api ?? config.model.api,
|
|
97
|
+
provider: partialMessage?.provider ?? config.model.provider,
|
|
98
|
+
model: partialMessage?.model ?? config.model.id,
|
|
99
|
+
usage: cloneUsage(partialMessage?.usage ?? EMPTY_USAGE),
|
|
100
|
+
stopReason: "aborted",
|
|
101
|
+
errorMessage: ABORT_ERROR_MESSAGE,
|
|
102
|
+
timestamp: Date.now(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function getTerminalMessage(event) {
|
|
106
|
+
return event.type === "done" ? event.message : event.error;
|
|
107
|
+
}
|
|
108
|
+
function endAgentStreamOnError(stream, promise) {
|
|
109
|
+
void promise.then((messages) => {
|
|
110
|
+
stream.end(messages);
|
|
111
|
+
}, () => {
|
|
112
|
+
stream.end([]);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async function pollMessagesUnlessAborted(poll, signal) {
|
|
116
|
+
if (!poll || signal?.aborted) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
return (await maybePromiseWithAbort(poll(), signal)) || [];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Start an agent loop with a new prompt message.
|
|
123
|
+
* The prompt is added to the context and events are emitted for it.
|
|
124
|
+
*/
|
|
125
|
+
export function agentLoop(prompts, context, config, signal, streamFn) {
|
|
126
|
+
const stream = createAgentStream();
|
|
127
|
+
endAgentStreamOnError(stream, runAgentLoop(prompts, context, config, async (event) => {
|
|
128
|
+
stream.push(event);
|
|
129
|
+
}, signal, streamFn));
|
|
130
|
+
return stream;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Continue an agent loop from the current context without adding a new message.
|
|
134
|
+
* Used for retries - context already has user message or tool results.
|
|
135
|
+
*
|
|
136
|
+
* **Important:** The last message in context must convert to a `user` or `toolResult` message
|
|
137
|
+
* via `convertToLlm`. If it doesn't, the LLM provider will reject the request.
|
|
138
|
+
* This cannot be validated here since `convertToLlm` is only called once per turn.
|
|
139
|
+
*/
|
|
140
|
+
export function agentLoopContinue(context, config, signal, streamFn) {
|
|
141
|
+
if (context.messages.length === 0) {
|
|
142
|
+
throw new Error("Cannot continue: no messages in context");
|
|
143
|
+
}
|
|
144
|
+
if (context.messages[context.messages.length - 1].role === "assistant") {
|
|
145
|
+
throw new Error("Cannot continue from message role: assistant");
|
|
146
|
+
}
|
|
147
|
+
const stream = createAgentStream();
|
|
148
|
+
endAgentStreamOnError(stream, runAgentLoopContinue(context, config, async (event) => {
|
|
149
|
+
stream.push(event);
|
|
150
|
+
}, signal, streamFn));
|
|
151
|
+
return stream;
|
|
152
|
+
}
|
|
153
|
+
export async function runAgentLoop(prompts, context, config, emit, signal, streamFn) {
|
|
154
|
+
const newMessages = [...prompts];
|
|
155
|
+
const currentContext = {
|
|
156
|
+
...context,
|
|
157
|
+
messages: [...context.messages, ...prompts],
|
|
158
|
+
};
|
|
159
|
+
await emit({ type: "agent_start" });
|
|
160
|
+
await emit({ type: "turn_start" });
|
|
161
|
+
for (const prompt of prompts) {
|
|
162
|
+
await emit({ type: "message_start", message: prompt });
|
|
163
|
+
await emit({ type: "message_end", message: prompt });
|
|
164
|
+
}
|
|
165
|
+
await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
|
|
166
|
+
return newMessages;
|
|
167
|
+
}
|
|
168
|
+
export async function runAgentLoopContinue(context, config, emit, signal, streamFn) {
|
|
169
|
+
if (context.messages.length === 0) {
|
|
170
|
+
throw new Error("Cannot continue: no messages in context");
|
|
171
|
+
}
|
|
172
|
+
if (context.messages[context.messages.length - 1].role === "assistant") {
|
|
173
|
+
throw new Error("Cannot continue from message role: assistant");
|
|
174
|
+
}
|
|
175
|
+
const newMessages = [];
|
|
176
|
+
const currentContext = { ...context };
|
|
177
|
+
await emit({ type: "agent_start" });
|
|
178
|
+
await emit({ type: "turn_start" });
|
|
179
|
+
await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
|
|
180
|
+
return newMessages;
|
|
181
|
+
}
|
|
182
|
+
function createAgentStream() {
|
|
183
|
+
return new EventStream((event) => event.type === "agent_end", (event) => (event.type === "agent_end" ? event.messages : []));
|
|
184
|
+
}
|
|
185
|
+
async function runLoop(currentContext, newMessages, config, signal, emit, streamFn) {
|
|
186
|
+
let firstTurn = true;
|
|
187
|
+
let lastTurn;
|
|
188
|
+
let pendingMessages = await pollMessagesUnlessAborted(config.getSteeringMessages, signal);
|
|
189
|
+
const shouldStopBeforeTurn = () => !firstTurn && (config.shouldStopBeforeTurn?.() ?? false);
|
|
190
|
+
while (true) {
|
|
191
|
+
throwIfAborted(signal);
|
|
192
|
+
let hasMoreToolCalls = true;
|
|
193
|
+
while (hasMoreToolCalls || pendingMessages.length > 0) {
|
|
194
|
+
throwIfAborted(signal);
|
|
195
|
+
if (!firstTurn) {
|
|
196
|
+
await emit({ type: "turn_start" });
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
firstTurn = false;
|
|
200
|
+
}
|
|
201
|
+
if (pendingMessages.length > 0) {
|
|
202
|
+
for (const message of pendingMessages) {
|
|
203
|
+
await emit({ type: "message_start", message });
|
|
204
|
+
await emit({ type: "message_end", message });
|
|
205
|
+
currentContext.messages.push(message);
|
|
206
|
+
newMessages.push(message);
|
|
207
|
+
}
|
|
208
|
+
pendingMessages = [];
|
|
209
|
+
}
|
|
210
|
+
const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
|
|
211
|
+
newMessages.push(message);
|
|
212
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
213
|
+
await emit({ type: "turn_end", message, toolResults: [] });
|
|
214
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const toolCalls = message.content.filter((c) => c.type === "toolCall");
|
|
218
|
+
const toolResults = [];
|
|
219
|
+
hasMoreToolCalls = false;
|
|
220
|
+
if (toolCalls.length > 0) {
|
|
221
|
+
const executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit);
|
|
222
|
+
toolResults.push(...executedToolBatch.messages);
|
|
223
|
+
hasMoreToolCalls = !executedToolBatch.terminate;
|
|
224
|
+
for (const result of toolResults) {
|
|
225
|
+
currentContext.messages.push(result);
|
|
226
|
+
newMessages.push(result);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
await emit({ type: "turn_end", message, toolResults });
|
|
230
|
+
if (signal?.aborted) {
|
|
231
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
lastTurn = {
|
|
235
|
+
message,
|
|
236
|
+
toolResults,
|
|
237
|
+
context: currentContext,
|
|
238
|
+
newMessages,
|
|
239
|
+
};
|
|
240
|
+
const shouldStopResult = await settlePostTurn(maybePromiseWithAbort(config.shouldStopAfterTurn?.({
|
|
241
|
+
message,
|
|
242
|
+
toolResults,
|
|
243
|
+
context: currentContext,
|
|
244
|
+
newMessages,
|
|
245
|
+
}) ?? false, signal), signal);
|
|
246
|
+
if (shouldStopResult.status === "aborted") {
|
|
247
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (shouldStopResult.value || shouldStopBeforeTurn()) {
|
|
251
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const steeringMessagesResult = await settlePostTurn(pollMessagesUnlessAborted(config.getSteeringMessages, signal), signal);
|
|
255
|
+
if (steeringMessagesResult.status === "aborted") {
|
|
256
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
pendingMessages = steeringMessagesResult.value;
|
|
260
|
+
// Steering drained by this poll owns the turn boundary; stop only when it was empty.
|
|
261
|
+
if (pendingMessages.length === 0 && shouldStopBeforeTurn()) {
|
|
262
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (shouldStopBeforeTurn())
|
|
267
|
+
break;
|
|
268
|
+
const followUpMessagesResult = await settlePostTurn(pollMessagesUnlessAborted(config.getFollowUpMessages, signal), signal);
|
|
269
|
+
if (followUpMessagesResult.status === "aborted") {
|
|
270
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const followUpMessages = followUpMessagesResult.value;
|
|
274
|
+
if (followUpMessages.length > 0) {
|
|
275
|
+
pendingMessages = followUpMessages;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (shouldStopBeforeTurn())
|
|
279
|
+
break;
|
|
280
|
+
const continuationMessagesResult = lastTurn
|
|
281
|
+
? await settlePostTurn(maybePromiseWithAbort(config.getContinuationMessages?.(lastTurn, signal) ?? [], signal), signal)
|
|
282
|
+
: { status: "completed", value: [] };
|
|
283
|
+
if (continuationMessagesResult.status === "aborted") {
|
|
284
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const continuationMessages = continuationMessagesResult.value || [];
|
|
288
|
+
if (continuationMessages.length > 0) {
|
|
289
|
+
pendingMessages = continuationMessages;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
295
|
+
}
|
|
296
|
+
async function streamAssistantResponse(context, config, signal, emit, streamFn) {
|
|
297
|
+
let partialMessage = null;
|
|
298
|
+
let addedPartial = false;
|
|
299
|
+
const finishAbortedMessage = async () => {
|
|
300
|
+
const finalMessage = createAbortedAssistantMessage(config, partialMessage);
|
|
301
|
+
if (addedPartial) {
|
|
302
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
context.messages.push(finalMessage);
|
|
306
|
+
await emit({ type: "message_start", message: { ...finalMessage } });
|
|
307
|
+
}
|
|
308
|
+
await emit({ type: "message_end", message: finalMessage });
|
|
309
|
+
return finalMessage;
|
|
310
|
+
};
|
|
311
|
+
try {
|
|
312
|
+
throwIfAborted(signal);
|
|
313
|
+
let messages = context.messages;
|
|
314
|
+
if (config.transformContext) {
|
|
315
|
+
messages = await maybePromiseWithAbort(config.transformContext(messages, signal), signal);
|
|
316
|
+
}
|
|
317
|
+
const llmMessages = await maybePromiseWithAbort(config.convertToLlm(messages), signal);
|
|
318
|
+
const streamFunction = streamFn || streamSimple;
|
|
319
|
+
const resolvedApiKey = (config.getApiKey
|
|
320
|
+
? await maybePromiseWithAbort(config.getApiKey(config.model.provider), signal)
|
|
321
|
+
: undefined) || config.apiKey;
|
|
322
|
+
const llmContext = {
|
|
323
|
+
systemPrompt: config.getSystemPrompt?.() ?? context.systemPrompt,
|
|
324
|
+
messages: llmMessages,
|
|
325
|
+
tools: context.tools,
|
|
326
|
+
};
|
|
327
|
+
const response = await maybePromiseWithAbort(streamFunction(config.model, llmContext, {
|
|
328
|
+
...config,
|
|
329
|
+
apiKey: resolvedApiKey,
|
|
330
|
+
signal,
|
|
331
|
+
}), signal);
|
|
332
|
+
const iterator = response[Symbol.asyncIterator]();
|
|
333
|
+
const closeIterator = () => {
|
|
334
|
+
void Promise.resolve(iterator.return?.()).catch(() => undefined);
|
|
335
|
+
};
|
|
336
|
+
while (true) {
|
|
337
|
+
const next = await raceWithAbort(iterator.next(), signal, closeIterator);
|
|
338
|
+
if (next.done) {
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
const event = next.value;
|
|
342
|
+
switch (event.type) {
|
|
343
|
+
case "start":
|
|
344
|
+
partialMessage = event.partial;
|
|
345
|
+
context.messages.push(partialMessage);
|
|
346
|
+
addedPartial = true;
|
|
347
|
+
await emit({ type: "message_start", message: { ...partialMessage } });
|
|
348
|
+
break;
|
|
349
|
+
case "text_start":
|
|
350
|
+
case "text_delta":
|
|
351
|
+
case "text_end":
|
|
352
|
+
case "thinking_start":
|
|
353
|
+
case "thinking_delta":
|
|
354
|
+
case "thinking_end":
|
|
355
|
+
case "toolcall_start":
|
|
356
|
+
case "toolcall_delta":
|
|
357
|
+
case "toolcall_end":
|
|
358
|
+
if (partialMessage) {
|
|
359
|
+
partialMessage = event.partial;
|
|
360
|
+
context.messages[context.messages.length - 1] = partialMessage;
|
|
361
|
+
await emit({
|
|
362
|
+
type: "message_update",
|
|
363
|
+
assistantMessageEvent: event,
|
|
364
|
+
message: { ...partialMessage },
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
case "done":
|
|
369
|
+
case "error": {
|
|
370
|
+
let finalMessage = getTerminalMessage(event);
|
|
371
|
+
try {
|
|
372
|
+
finalMessage = await maybePromiseWithAbort(response.result(), signal);
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (!signal?.aborted || !isAbortError(error)) {
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (addedPartial) {
|
|
380
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
context.messages.push(finalMessage);
|
|
384
|
+
}
|
|
385
|
+
if (!addedPartial) {
|
|
386
|
+
await emit({ type: "message_start", message: { ...finalMessage } });
|
|
387
|
+
}
|
|
388
|
+
await emit({ type: "message_end", message: finalMessage });
|
|
389
|
+
return finalMessage;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const finalMessage = await maybePromiseWithAbort(response.result(), signal);
|
|
394
|
+
if (addedPartial) {
|
|
395
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
context.messages.push(finalMessage);
|
|
399
|
+
await emit({ type: "message_start", message: { ...finalMessage } });
|
|
400
|
+
}
|
|
401
|
+
await emit({ type: "message_end", message: finalMessage });
|
|
402
|
+
return finalMessage;
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
if (signal?.aborted && isAbortError(error)) {
|
|
406
|
+
return finishAbortedMessage();
|
|
407
|
+
}
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function executeToolCalls(currentContext, assistantMessage, config, signal, emit) {
|
|
412
|
+
const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
|
|
413
|
+
const hasSequentialToolCall = toolCalls.some((tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === "sequential");
|
|
414
|
+
if (config.toolExecution === "sequential" || hasSequentialToolCall) {
|
|
415
|
+
return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit);
|
|
416
|
+
}
|
|
417
|
+
return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);
|
|
418
|
+
}
|
|
419
|
+
async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit) {
|
|
420
|
+
const finalizedCalls = [];
|
|
421
|
+
const messages = [];
|
|
422
|
+
for (const toolCall of toolCalls) {
|
|
423
|
+
if (signal?.aborted) {
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
await emit({
|
|
427
|
+
type: "tool_execution_start",
|
|
428
|
+
toolCallId: toolCall.id,
|
|
429
|
+
toolName: toolCall.name,
|
|
430
|
+
args: toolCall.arguments,
|
|
431
|
+
});
|
|
432
|
+
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
|
|
433
|
+
let finalized;
|
|
434
|
+
if (preparation.kind === "immediate") {
|
|
435
|
+
finalized = {
|
|
436
|
+
toolCall,
|
|
437
|
+
result: preparation.result,
|
|
438
|
+
isError: preparation.isError,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
const executed = await executePreparedToolCall(preparation, signal, emit);
|
|
443
|
+
finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
|
|
444
|
+
}
|
|
445
|
+
await emitToolExecutionEnd(finalized, emit);
|
|
446
|
+
const toolResultMessage = createToolResultMessage(finalized);
|
|
447
|
+
await emitToolResultMessage(toolResultMessage, emit);
|
|
448
|
+
finalizedCalls.push(finalized);
|
|
449
|
+
messages.push(toolResultMessage);
|
|
450
|
+
if (signal?.aborted) {
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
messages,
|
|
456
|
+
terminate: shouldTerminateToolBatch(finalizedCalls),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
async function executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit) {
|
|
460
|
+
const finalizedCalls = [];
|
|
461
|
+
for (const toolCall of toolCalls) {
|
|
462
|
+
await emit({
|
|
463
|
+
type: "tool_execution_start",
|
|
464
|
+
toolCallId: toolCall.id,
|
|
465
|
+
toolName: toolCall.name,
|
|
466
|
+
args: toolCall.arguments,
|
|
467
|
+
});
|
|
468
|
+
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
|
|
469
|
+
if (preparation.kind === "immediate") {
|
|
470
|
+
const finalized = {
|
|
471
|
+
toolCall,
|
|
472
|
+
result: preparation.result,
|
|
473
|
+
isError: preparation.isError,
|
|
474
|
+
};
|
|
475
|
+
await emitToolExecutionEnd(finalized, emit);
|
|
476
|
+
finalizedCalls.push(finalized);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
finalizedCalls.push(async () => {
|
|
480
|
+
const executed = await executePreparedToolCall(preparation, signal, emit);
|
|
481
|
+
const finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
|
|
482
|
+
await emitToolExecutionEnd(finalized, emit);
|
|
483
|
+
return finalized;
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
const orderedFinalizedCalls = await Promise.all(finalizedCalls.map((entry) => (typeof entry === "function" ? entry() : Promise.resolve(entry))));
|
|
487
|
+
const messages = [];
|
|
488
|
+
for (const finalized of orderedFinalizedCalls) {
|
|
489
|
+
const toolResultMessage = createToolResultMessage(finalized);
|
|
490
|
+
await emitToolResultMessage(toolResultMessage, emit);
|
|
491
|
+
messages.push(toolResultMessage);
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
messages,
|
|
495
|
+
terminate: shouldTerminateToolBatch(orderedFinalizedCalls),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function shouldTerminateToolBatch(finalizedCalls) {
|
|
499
|
+
return finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);
|
|
500
|
+
}
|
|
501
|
+
function prepareToolCallArguments(tool, toolCall) {
|
|
502
|
+
if (!tool.prepareArguments) {
|
|
503
|
+
return toolCall;
|
|
504
|
+
}
|
|
505
|
+
const preparedArguments = tool.prepareArguments(toolCall.arguments);
|
|
506
|
+
if (preparedArguments === toolCall.arguments) {
|
|
507
|
+
return toolCall;
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
...toolCall,
|
|
511
|
+
arguments: preparedArguments,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
async function prepareToolCall(currentContext, assistantMessage, toolCall, config, signal) {
|
|
515
|
+
const tool = currentContext.tools?.find((t) => t.name === toolCall.name);
|
|
516
|
+
if (!tool) {
|
|
517
|
+
return {
|
|
518
|
+
kind: "immediate",
|
|
519
|
+
result: createErrorToolResult(`Tool ${toolCall.name} not found`),
|
|
520
|
+
isError: true,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
const preparedToolCall = prepareToolCallArguments(tool, toolCall);
|
|
525
|
+
const validatedArgs = validateToolArguments(tool, preparedToolCall);
|
|
526
|
+
if (config.beforeToolCall) {
|
|
527
|
+
const beforeResult = await maybePromiseWithAbort(config.beforeToolCall({
|
|
528
|
+
assistantMessage,
|
|
529
|
+
toolCall,
|
|
530
|
+
args: validatedArgs,
|
|
531
|
+
context: currentContext,
|
|
532
|
+
}, signal), signal);
|
|
533
|
+
if (beforeResult?.block) {
|
|
534
|
+
return {
|
|
535
|
+
kind: "immediate",
|
|
536
|
+
result: createErrorToolResult(beforeResult.reason || "Tool execution was blocked"),
|
|
537
|
+
isError: true,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
kind: "prepared",
|
|
543
|
+
toolCall,
|
|
544
|
+
tool,
|
|
545
|
+
args: validatedArgs,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
return {
|
|
550
|
+
kind: "immediate",
|
|
551
|
+
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
|
552
|
+
isError: true,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async function executePreparedToolCall(prepared, signal, emit) {
|
|
557
|
+
const updateEvents = [];
|
|
558
|
+
let acceptingUpdates = true;
|
|
559
|
+
try {
|
|
560
|
+
throwIfAborted(signal);
|
|
561
|
+
const result = await raceWithAbort(prepared.tool.execute(prepared.toolCall.id, prepared.args, signal, (partialResult) => {
|
|
562
|
+
if (!acceptingUpdates || signal?.aborted) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
updateEvents.push(Promise.resolve(emit({
|
|
566
|
+
type: "tool_execution_update",
|
|
567
|
+
toolCallId: prepared.toolCall.id,
|
|
568
|
+
toolName: prepared.toolCall.name,
|
|
569
|
+
args: prepared.toolCall.arguments,
|
|
570
|
+
partialResult,
|
|
571
|
+
})));
|
|
572
|
+
}), signal);
|
|
573
|
+
acceptingUpdates = false;
|
|
574
|
+
try {
|
|
575
|
+
await raceWithAbort(Promise.all(updateEvents).then(() => undefined), signal);
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
if (!signal?.aborted || !isAbortError(error)) {
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return { result, isError: false };
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
acceptingUpdates = false;
|
|
586
|
+
await raceWithAbort(Promise.all(updateEvents).then(() => undefined), signal).catch(() => undefined);
|
|
587
|
+
return {
|
|
588
|
+
result: createErrorToolResult(signal?.aborted ? "Tool execution aborted" : error instanceof Error ? error.message : String(error)),
|
|
589
|
+
isError: true,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, signal) {
|
|
594
|
+
let result = executed.result;
|
|
595
|
+
let isError = executed.isError;
|
|
596
|
+
if (config.afterToolCall) {
|
|
597
|
+
try {
|
|
598
|
+
const afterResult = await maybePromiseWithAbort(config.afterToolCall({
|
|
599
|
+
assistantMessage,
|
|
600
|
+
toolCall: prepared.toolCall,
|
|
601
|
+
args: prepared.args,
|
|
602
|
+
result,
|
|
603
|
+
isError,
|
|
604
|
+
context: currentContext,
|
|
605
|
+
}, signal), signal);
|
|
606
|
+
if (afterResult) {
|
|
607
|
+
result = {
|
|
608
|
+
content: afterResult.content ?? result.content,
|
|
609
|
+
details: afterResult.details ?? result.details,
|
|
610
|
+
terminate: afterResult.terminate ?? result.terminate,
|
|
611
|
+
};
|
|
612
|
+
isError = afterResult.isError ?? isError;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
result = createErrorToolResult(error instanceof Error ? error.message : String(error));
|
|
617
|
+
isError = true;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
toolCall: prepared.toolCall,
|
|
622
|
+
result,
|
|
623
|
+
isError,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function createErrorToolResult(message) {
|
|
627
|
+
return {
|
|
628
|
+
content: [{ type: "text", text: message }],
|
|
629
|
+
details: {},
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
async function emitToolExecutionEnd(finalized, emit) {
|
|
633
|
+
await emit({
|
|
634
|
+
type: "tool_execution_end",
|
|
635
|
+
toolCallId: finalized.toolCall.id,
|
|
636
|
+
toolName: finalized.toolCall.name,
|
|
637
|
+
result: finalized.result,
|
|
638
|
+
isError: finalized.isError,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function createToolResultMessage(finalized) {
|
|
642
|
+
return {
|
|
643
|
+
role: "toolResult",
|
|
644
|
+
toolCallId: finalized.toolCall.id,
|
|
645
|
+
toolName: finalized.toolCall.name,
|
|
646
|
+
content: finalized.result.content,
|
|
647
|
+
details: finalized.result.details,
|
|
648
|
+
isError: finalized.isError,
|
|
649
|
+
timestamp: Date.now(),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
async function emitToolResultMessage(toolResultMessage, emit) {
|
|
653
|
+
await emit({ type: "message_start", message: toolResultMessage });
|
|
654
|
+
await emit({ type: "message_end", message: toolResultMessage });
|
|
655
|
+
}
|
|
656
|
+
//# sourceMappingURL=agent-loop.js.map
|