@ponythewhite/base-context-agent 1.0.0
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/LICENSE +23 -0
- package/NOTICE +22 -0
- package/README.md +551 -0
- package/dist/LICENSE +23 -0
- package/dist/NOTICE +22 -0
- package/dist/agent-loop.d.ts +24 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +897 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent.d.ts +144 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +569 -0
- package/dist/agent.js.map +1 -0
- package/dist/build-info.json +14 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/invocation-output.d.ts +30 -0
- package/dist/invocation-output.d.ts.map +1 -0
- package/dist/invocation-output.js +125 -0
- package/dist/invocation-output.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 +512 -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 +56 -0
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent loop that works with AgentMessage throughout.
|
|
3
|
+
* Transforms to Message[] only at the LLM call boundary.
|
|
4
|
+
*/
|
|
5
|
+
import { EventStream, isLocalRequestPreparationError, markLocalRequestPreparationError, streamSimple, validateToolArguments, } from "@ponythewhite/base-context-ai";
|
|
6
|
+
import { AgentOutputLimitError, InvocationOutput } from "./invocation-output.js";
|
|
7
|
+
const ABORT_ERROR_MESSAGE = "Request was aborted";
|
|
8
|
+
const EMPTY_USAGE = {
|
|
9
|
+
input: 0,
|
|
10
|
+
output: 0,
|
|
11
|
+
cacheRead: 0,
|
|
12
|
+
cacheWrite: 0,
|
|
13
|
+
totalTokens: 0,
|
|
14
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
15
|
+
};
|
|
16
|
+
function createAbortError() {
|
|
17
|
+
return new Error(ABORT_ERROR_MESSAGE);
|
|
18
|
+
}
|
|
19
|
+
function throwIfAborted(signal) {
|
|
20
|
+
if (signal?.aborted) {
|
|
21
|
+
throw createAbortError();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function raceWithAbort(operation, signal, onAbort) {
|
|
25
|
+
if (!signal) {
|
|
26
|
+
return operation;
|
|
27
|
+
}
|
|
28
|
+
if (signal.aborted) {
|
|
29
|
+
onAbort?.();
|
|
30
|
+
void operation.catch(() => undefined);
|
|
31
|
+
return Promise.reject(createAbortError());
|
|
32
|
+
}
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
let settled = false;
|
|
35
|
+
const cleanup = () => {
|
|
36
|
+
signal.removeEventListener("abort", abort);
|
|
37
|
+
};
|
|
38
|
+
const abort = () => {
|
|
39
|
+
if (settled) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
settled = true;
|
|
43
|
+
cleanup();
|
|
44
|
+
onAbort?.();
|
|
45
|
+
reject(createAbortError());
|
|
46
|
+
};
|
|
47
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
48
|
+
operation.then((value) => {
|
|
49
|
+
if (settled) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
settled = true;
|
|
53
|
+
cleanup();
|
|
54
|
+
resolve(value);
|
|
55
|
+
}, (error) => {
|
|
56
|
+
if (settled) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
settled = true;
|
|
60
|
+
cleanup();
|
|
61
|
+
reject(error);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function maybePromiseWithAbort(operation, signal, onAbort) {
|
|
66
|
+
return raceWithAbort(Promise.resolve(operation), signal, onAbort);
|
|
67
|
+
}
|
|
68
|
+
function isAbortError(error) {
|
|
69
|
+
return error instanceof Error && (error.message === ABORT_ERROR_MESSAGE || error.name === "AbortError");
|
|
70
|
+
}
|
|
71
|
+
async function settlePostTurn(operation, signal) {
|
|
72
|
+
try {
|
|
73
|
+
return { status: "completed", value: await operation };
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (signal?.aborted && isAbortError(error)) {
|
|
77
|
+
return { status: "aborted" };
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function cloneAssistantContent(content) {
|
|
83
|
+
return content.map((part) => {
|
|
84
|
+
if (part.type === "toolCall") {
|
|
85
|
+
return { ...part, arguments: { ...part.arguments } };
|
|
86
|
+
}
|
|
87
|
+
return { ...part };
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function cloneUsage(usage) {
|
|
91
|
+
return { ...usage, cost: { ...usage.cost } };
|
|
92
|
+
}
|
|
93
|
+
function createAbortedAssistantMessage(config, partialMessage) {
|
|
94
|
+
return {
|
|
95
|
+
role: "assistant",
|
|
96
|
+
content: partialMessage ? cloneAssistantContent(partialMessage.content) : [{ type: "text", text: "" }],
|
|
97
|
+
api: partialMessage?.api ?? config.model.api,
|
|
98
|
+
provider: partialMessage?.provider ?? config.model.provider,
|
|
99
|
+
model: partialMessage?.model ?? config.model.id,
|
|
100
|
+
usage: cloneUsage(partialMessage?.usage ?? EMPTY_USAGE),
|
|
101
|
+
stopReason: "aborted",
|
|
102
|
+
errorMessage: ABORT_ERROR_MESSAGE,
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function getTerminalMessage(event) {
|
|
107
|
+
return event.type === "done" ? event.message : event.error;
|
|
108
|
+
}
|
|
109
|
+
function endAgentStreamOnError(stream, promise) {
|
|
110
|
+
void promise.then((messages) => {
|
|
111
|
+
stream.end(messages);
|
|
112
|
+
}, (error) => {
|
|
113
|
+
if (error instanceof AgentOutputLimitError)
|
|
114
|
+
stream.fail(error);
|
|
115
|
+
else
|
|
116
|
+
stream.end([]);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async function pollMessagesUnlessAborted(poll, signal) {
|
|
120
|
+
if (!poll || signal?.aborted) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
return (await maybePromiseWithAbort(poll(), signal)) || [];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Start an agent loop with a new prompt message.
|
|
127
|
+
* The prompt is added to the context and events are emitted for it.
|
|
128
|
+
*/
|
|
129
|
+
export function agentLoop(prompts, context, config, signal, streamFn) {
|
|
130
|
+
const stream = createAgentStream();
|
|
131
|
+
endAgentStreamOnError(stream, runAgentLoop(prompts, context, config, async (event) => {
|
|
132
|
+
stream.push(event);
|
|
133
|
+
}, signal, streamFn));
|
|
134
|
+
return stream;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Continue an agent loop from the current context without adding a new message.
|
|
138
|
+
* Used for retries - context already has user message or tool results.
|
|
139
|
+
*
|
|
140
|
+
* **Important:** The last message in context must convert to a `user` or `toolResult` message
|
|
141
|
+
* via `convertToLlm`. If it doesn't, the LLM provider will reject the request.
|
|
142
|
+
* This cannot be validated here since `convertToLlm` is only called once per turn.
|
|
143
|
+
*/
|
|
144
|
+
export function agentLoopContinue(context, config, signal, streamFn) {
|
|
145
|
+
if (context.messages.length === 0) {
|
|
146
|
+
throw new Error("Cannot continue: no messages in context");
|
|
147
|
+
}
|
|
148
|
+
if (context.messages[context.messages.length - 1].role === "assistant") {
|
|
149
|
+
throw new Error("Cannot continue from message role: assistant");
|
|
150
|
+
}
|
|
151
|
+
const stream = createAgentStream();
|
|
152
|
+
endAgentStreamOnError(stream, runAgentLoopContinue(context, config, async (event) => {
|
|
153
|
+
stream.push(event);
|
|
154
|
+
}, signal, streamFn));
|
|
155
|
+
return stream;
|
|
156
|
+
}
|
|
157
|
+
async function withInvocationOutput(messages, config, emit, run) {
|
|
158
|
+
const output = config.outputPolicy ? new InvocationOutput(config.outputPolicy, messages) : undefined;
|
|
159
|
+
let releaseUpdates = output ? output.policy.bindUpdates?.(output.refresh.bind(output)) : undefined;
|
|
160
|
+
const stopUpdates = () => {
|
|
161
|
+
const release = releaseUpdates;
|
|
162
|
+
releaseUpdates = undefined;
|
|
163
|
+
release?.();
|
|
164
|
+
};
|
|
165
|
+
let updateSettlement;
|
|
166
|
+
const settleUpdates = () => {
|
|
167
|
+
if (!updateSettlement) {
|
|
168
|
+
try {
|
|
169
|
+
updateSettlement = output?.policy.settleUpdates?.() ?? Promise.resolve();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
updateSettlement = Promise.reject(error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return updateSettlement;
|
|
176
|
+
};
|
|
177
|
+
const ownedEmit = output
|
|
178
|
+
? async (event) => {
|
|
179
|
+
if (event.type === "agent_end") {
|
|
180
|
+
await settleUpdates();
|
|
181
|
+
output.throwIfRefused();
|
|
182
|
+
const finalizedMessages = output.copy();
|
|
183
|
+
stopUpdates();
|
|
184
|
+
await emit({ type: "agent_end", messages: finalizedMessages });
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
await emit(event);
|
|
188
|
+
// The native sink joins this message's replacement/persistence job. Keep its original subject.
|
|
189
|
+
if (event.type === "message_end")
|
|
190
|
+
output.capture(event.message);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
: emit;
|
|
194
|
+
try {
|
|
195
|
+
await run(ownedEmit, output);
|
|
196
|
+
return messages;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (!output)
|
|
200
|
+
throw error;
|
|
201
|
+
let failure = error;
|
|
202
|
+
try {
|
|
203
|
+
await settleUpdates();
|
|
204
|
+
}
|
|
205
|
+
catch (settlementError) {
|
|
206
|
+
if (settlementError !== failure)
|
|
207
|
+
failure = new AggregateError([failure, settlementError], "Invocation and accepted update settlement failed", { cause: failure });
|
|
208
|
+
}
|
|
209
|
+
if (!output.refusal)
|
|
210
|
+
throw failure;
|
|
211
|
+
let cause = failure instanceof AgentOutputLimitError ? failure.cause : failure;
|
|
212
|
+
stopUpdates();
|
|
213
|
+
try {
|
|
214
|
+
await emit({ type: "agent_end", refusal: { ...output.refusal } });
|
|
215
|
+
}
|
|
216
|
+
catch (notificationError) {
|
|
217
|
+
cause =
|
|
218
|
+
cause === undefined
|
|
219
|
+
? notificationError
|
|
220
|
+
: new AggregateError([cause, notificationError], "Output refusal and terminal notification failed");
|
|
221
|
+
}
|
|
222
|
+
throw new AgentOutputLimitError({ ...output.refusal }, cause === undefined ? undefined : { cause });
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
try {
|
|
226
|
+
stopUpdates();
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
output?.dispose();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export async function runAgentLoop(prompts, context, config, emit, signal, streamFn) {
|
|
234
|
+
const newMessages = config.outputPolicy ? [] : [...prompts];
|
|
235
|
+
return withInvocationOutput(newMessages, config, emit, async (ownedEmit, output) => {
|
|
236
|
+
if (output) {
|
|
237
|
+
output.checkRoom(prompts.length);
|
|
238
|
+
prompts = [...prompts];
|
|
239
|
+
}
|
|
240
|
+
// Keep only the working wrapper across awaits, not the initial snapshot.
|
|
241
|
+
context = { ...context, messages: [...context.messages, ...prompts] };
|
|
242
|
+
await ownedEmit({ type: "agent_start" });
|
|
243
|
+
await ownedEmit({ type: "turn_start" });
|
|
244
|
+
for (const prompt of prompts) {
|
|
245
|
+
await ownedEmit({ type: "message_start", message: prompt });
|
|
246
|
+
await ownedEmit({ type: "message_end", message: prompt });
|
|
247
|
+
output?.throwIfRefused();
|
|
248
|
+
}
|
|
249
|
+
await runLoop(context, newMessages, config, signal, ownedEmit, streamFn, output);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
export async function runAgentLoopContinue(context, config, emit, signal, streamFn) {
|
|
253
|
+
if (context.messages.length === 0) {
|
|
254
|
+
throw new Error("Cannot continue: no messages in context");
|
|
255
|
+
}
|
|
256
|
+
if (context.messages[context.messages.length - 1].role === "assistant") {
|
|
257
|
+
throw new Error("Cannot continue from message role: assistant");
|
|
258
|
+
}
|
|
259
|
+
const newMessages = [];
|
|
260
|
+
return withInvocationOutput(newMessages, config, emit, async (ownedEmit, output) => {
|
|
261
|
+
context = { ...context };
|
|
262
|
+
await ownedEmit({ type: "agent_start" });
|
|
263
|
+
output?.throwIfRefused();
|
|
264
|
+
await ownedEmit({ type: "turn_start" });
|
|
265
|
+
await runLoop(context, newMessages, config, signal, ownedEmit, streamFn, output);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function createAgentStream() {
|
|
269
|
+
return new EventStream((event) => event.type === "agent_end" && !event.refusal, (event) => (event.type === "agent_end" && !event.refusal ? event.messages : []));
|
|
270
|
+
}
|
|
271
|
+
async function runLoop(currentContext, newMessages, config, signal, emit, streamFn, output) {
|
|
272
|
+
let firstTurn = true;
|
|
273
|
+
let lastTurn;
|
|
274
|
+
let pendingMessages = await pollMessagesUnlessAborted(config.getSteeringMessages, signal);
|
|
275
|
+
const shouldStopBeforeTurn = () => !firstTurn && (config.shouldStopBeforeTurn?.() ?? false);
|
|
276
|
+
while (true) {
|
|
277
|
+
throwIfAborted(signal);
|
|
278
|
+
let hasMoreToolCalls = true;
|
|
279
|
+
while (hasMoreToolCalls || pendingMessages.length > 0) {
|
|
280
|
+
throwIfAborted(signal);
|
|
281
|
+
if (!firstTurn) {
|
|
282
|
+
await emit({ type: "turn_start" });
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
firstTurn = false;
|
|
286
|
+
}
|
|
287
|
+
if (pendingMessages.length > 0) {
|
|
288
|
+
output?.checkRoom(pendingMessages.length);
|
|
289
|
+
if (output)
|
|
290
|
+
pendingMessages = [...pendingMessages];
|
|
291
|
+
for (const message of pendingMessages) {
|
|
292
|
+
await emit({ type: "message_start", message });
|
|
293
|
+
await emit({ type: "message_end", message });
|
|
294
|
+
currentContext.messages.push(message);
|
|
295
|
+
if (!output)
|
|
296
|
+
newMessages.push(message);
|
|
297
|
+
output?.throwIfRefused();
|
|
298
|
+
}
|
|
299
|
+
pendingMessages = [];
|
|
300
|
+
}
|
|
301
|
+
output?.checkRoom(1);
|
|
302
|
+
const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
|
|
303
|
+
if (!output)
|
|
304
|
+
newMessages.push(message);
|
|
305
|
+
output?.throwIfRefused();
|
|
306
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
307
|
+
await emit({
|
|
308
|
+
type: "turn_end",
|
|
309
|
+
message,
|
|
310
|
+
toolResults: [],
|
|
311
|
+
toolExecution: config.toolExecution ?? "parallel",
|
|
312
|
+
exchanges: [],
|
|
313
|
+
});
|
|
314
|
+
output?.refresh(message);
|
|
315
|
+
if (message.stopReason === "error" &&
|
|
316
|
+
config.recoverProviderFailure &&
|
|
317
|
+
(await maybePromiseWithAbort(config.recoverProviderFailure(message, signal), signal))) {
|
|
318
|
+
// Retain and charge the failed output; only omit it from the next request.
|
|
319
|
+
currentContext.messages = currentContext.messages.filter((candidate) => candidate !== message);
|
|
320
|
+
hasMoreToolCalls = true;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const toolCalls = message.content.filter((c) => c.type === "toolCall");
|
|
327
|
+
const toolResults = [];
|
|
328
|
+
const exchanges = [];
|
|
329
|
+
let toolExecution = config.toolExecution ?? "parallel";
|
|
330
|
+
hasMoreToolCalls = false;
|
|
331
|
+
if (toolCalls.length > 0) {
|
|
332
|
+
output?.checkRoom(toolCalls.length);
|
|
333
|
+
const executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit, output);
|
|
334
|
+
toolResults.push(...executedToolBatch.messages);
|
|
335
|
+
exchanges.push(...executedToolBatch.exchanges);
|
|
336
|
+
toolExecution = executedToolBatch.toolExecution;
|
|
337
|
+
hasMoreToolCalls = !executedToolBatch.terminate;
|
|
338
|
+
for (const result of toolResults) {
|
|
339
|
+
currentContext.messages.push(result);
|
|
340
|
+
if (!output)
|
|
341
|
+
newMessages.push(result);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
output?.throwIfRefused();
|
|
345
|
+
await emit({ type: "turn_end", message, toolResults, toolExecution, exchanges });
|
|
346
|
+
output?.refresh(message);
|
|
347
|
+
for (const result of toolResults)
|
|
348
|
+
output?.refresh(result);
|
|
349
|
+
output?.throwIfRefused();
|
|
350
|
+
if (signal?.aborted) {
|
|
351
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
lastTurn = {
|
|
355
|
+
message,
|
|
356
|
+
toolResults,
|
|
357
|
+
context: currentContext,
|
|
358
|
+
newMessages,
|
|
359
|
+
};
|
|
360
|
+
const turnContext = { ...lastTurn, newMessages: output?.copy() ?? newMessages };
|
|
361
|
+
const shouldStopResult = await settlePostTurn(maybePromiseWithAbort(config.getTurnOutcome
|
|
362
|
+
? config.getTurnOutcome({ ...turnContext, hasMoreToolCalls }, signal)
|
|
363
|
+
: Promise.resolve(config.shouldStopAfterTurn?.(turnContext) ?? false).then((stop) => ({
|
|
364
|
+
kind: stop ? "finish" : "proceed",
|
|
365
|
+
})), signal), signal);
|
|
366
|
+
output?.refresh(message);
|
|
367
|
+
for (const result of toolResults)
|
|
368
|
+
output?.refresh(result);
|
|
369
|
+
output?.throwIfRefused();
|
|
370
|
+
if (shouldStopResult.status === "aborted") {
|
|
371
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (shouldStopResult.value.kind !== "proceed" || shouldStopBeforeTurn()) {
|
|
375
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const steeringMessagesResult = await settlePostTurn(pollMessagesUnlessAborted(config.getSteeringMessages, signal), signal);
|
|
379
|
+
if (steeringMessagesResult.status === "aborted") {
|
|
380
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
pendingMessages = steeringMessagesResult.value;
|
|
384
|
+
// Steering drained by this poll owns the turn boundary; stop only when it was empty.
|
|
385
|
+
if (pendingMessages.length === 0 && shouldStopBeforeTurn()) {
|
|
386
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (shouldStopBeforeTurn())
|
|
391
|
+
break;
|
|
392
|
+
const followUpMessagesResult = await settlePostTurn(pollMessagesUnlessAborted(config.getFollowUpMessages, signal), signal);
|
|
393
|
+
if (followUpMessagesResult.status === "aborted") {
|
|
394
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const followUpMessages = followUpMessagesResult.value;
|
|
398
|
+
if (followUpMessages.length > 0) {
|
|
399
|
+
pendingMessages = followUpMessages;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (shouldStopBeforeTurn())
|
|
403
|
+
break;
|
|
404
|
+
if (lastTurn && config.getContinuationOutcome) {
|
|
405
|
+
const outcome = await settlePostTurn(maybePromiseWithAbort(config.getContinuationOutcome(output ? { ...lastTurn, newMessages: output.copy() } : lastTurn, signal), signal), signal);
|
|
406
|
+
if (outcome.status === "completed" && outcome.value.kind === "continue") {
|
|
407
|
+
pendingMessages = outcome.value.messages;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
const continuationMessagesResult = lastTurn
|
|
413
|
+
? await settlePostTurn(maybePromiseWithAbort(config.getContinuationMessages?.(output ? { ...lastTurn, newMessages: output.copy() } : lastTurn, signal) ?? [], signal), signal)
|
|
414
|
+
: { status: "completed", value: [] };
|
|
415
|
+
if (continuationMessagesResult.status === "aborted") {
|
|
416
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const continuationMessages = continuationMessagesResult.value || [];
|
|
420
|
+
if (continuationMessages.length > 0) {
|
|
421
|
+
pendingMessages = continuationMessages;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
427
|
+
}
|
|
428
|
+
async function streamAssistantResponse(context, config, signal, emit, streamFn, allowRecovery = true) {
|
|
429
|
+
const build = { projection: undefined };
|
|
430
|
+
let addedPartial = false;
|
|
431
|
+
const runResponse = async () => {
|
|
432
|
+
let partialMessage = null;
|
|
433
|
+
const finishAbortedMessage = async () => {
|
|
434
|
+
const finalMessage = createAbortedAssistantMessage(config, partialMessage);
|
|
435
|
+
if (addedPartial) {
|
|
436
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
context.messages.push(finalMessage);
|
|
440
|
+
await emit({ type: "message_start", message: { ...finalMessage } });
|
|
441
|
+
}
|
|
442
|
+
await emit({ type: "message_end", message: finalMessage });
|
|
443
|
+
return finalMessage;
|
|
444
|
+
};
|
|
445
|
+
try {
|
|
446
|
+
throwIfAborted(signal);
|
|
447
|
+
// Do not race source persistence against cancellation; drain it before leaving this build.
|
|
448
|
+
const projection = await config.beforeContextBuild?.();
|
|
449
|
+
build.projection = projection;
|
|
450
|
+
if (projection && projection.adoptMessages === true) {
|
|
451
|
+
context.messages = projection.messages.slice();
|
|
452
|
+
await config.onContextAdopted?.(context.messages);
|
|
453
|
+
}
|
|
454
|
+
throwIfAborted(signal);
|
|
455
|
+
let messages = projection ? projection.messages.slice() : context.messages;
|
|
456
|
+
if (config.transformContext) {
|
|
457
|
+
messages = await maybePromiseWithAbort(config.transformContext(messages, signal), signal);
|
|
458
|
+
}
|
|
459
|
+
const llmMessages = await maybePromiseWithAbort(config.convertToLlm(messages), signal);
|
|
460
|
+
const streamFunction = streamFn || streamSimple;
|
|
461
|
+
const resolvedApiKey = (config.getApiKey
|
|
462
|
+
? await maybePromiseWithAbort(config.getApiKey(config.model.provider), signal)
|
|
463
|
+
: undefined) || config.apiKey;
|
|
464
|
+
const llmContext = {
|
|
465
|
+
systemPrompt: config.getSystemPrompt?.() ?? context.systemPrompt,
|
|
466
|
+
messages: llmMessages,
|
|
467
|
+
tools: context.tools,
|
|
468
|
+
};
|
|
469
|
+
const { beforeContextBuild: _beforeContextBuild, onContextAdopted: _onContextAdopted, recoverRequestPreparation: _recoverRequestPreparation, recoverProviderFailure: _recoverProviderFailure, outputPolicy: _outputPolicy, ownedStreamFn, ...streamOptions } = config;
|
|
470
|
+
const options = { ...streamOptions, apiKey: resolvedApiKey, signal };
|
|
471
|
+
const response = await maybePromiseWithAbort(ownedStreamFn
|
|
472
|
+
? ownedStreamFn(config.model, llmContext, options, projection ? projection.streamContext : undefined)
|
|
473
|
+
: streamFunction(config.model, llmContext, options), signal);
|
|
474
|
+
const iterator = response[Symbol.asyncIterator]();
|
|
475
|
+
const closeIterator = () => {
|
|
476
|
+
void Promise.resolve(iterator.return?.()).catch(() => undefined);
|
|
477
|
+
};
|
|
478
|
+
while (true) {
|
|
479
|
+
const next = await raceWithAbort(iterator.next(), signal, closeIterator);
|
|
480
|
+
if (next.done) {
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
const event = next.value;
|
|
484
|
+
switch (event.type) {
|
|
485
|
+
case "start":
|
|
486
|
+
partialMessage = event.partial;
|
|
487
|
+
context.messages.push(partialMessage);
|
|
488
|
+
addedPartial = true;
|
|
489
|
+
await emit({ type: "message_start", message: { ...partialMessage } });
|
|
490
|
+
break;
|
|
491
|
+
case "text_start":
|
|
492
|
+
case "text_delta":
|
|
493
|
+
case "text_end":
|
|
494
|
+
case "thinking_start":
|
|
495
|
+
case "thinking_delta":
|
|
496
|
+
case "thinking_end":
|
|
497
|
+
case "toolcall_start":
|
|
498
|
+
case "toolcall_delta":
|
|
499
|
+
case "toolcall_end":
|
|
500
|
+
if (partialMessage) {
|
|
501
|
+
partialMessage = event.partial;
|
|
502
|
+
context.messages[context.messages.length - 1] = partialMessage;
|
|
503
|
+
await emit({
|
|
504
|
+
type: "message_update",
|
|
505
|
+
assistantMessageEvent: event,
|
|
506
|
+
message: { ...partialMessage },
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
break;
|
|
510
|
+
case "done":
|
|
511
|
+
case "error": {
|
|
512
|
+
let finalMessage = getTerminalMessage(event);
|
|
513
|
+
try {
|
|
514
|
+
finalMessage = await maybePromiseWithAbort(response.result(), signal);
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
if (!signal?.aborted || !isAbortError(error)) {
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (addedPartial) {
|
|
522
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
context.messages.push(finalMessage);
|
|
526
|
+
}
|
|
527
|
+
if (!addedPartial) {
|
|
528
|
+
await emit({ type: "message_start", message: { ...finalMessage } });
|
|
529
|
+
}
|
|
530
|
+
await emit({ type: "message_end", message: finalMessage });
|
|
531
|
+
return finalMessage;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const finalMessage = await maybePromiseWithAbort(response.result(), signal);
|
|
536
|
+
if (addedPartial) {
|
|
537
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
context.messages.push(finalMessage);
|
|
541
|
+
await emit({ type: "message_start", message: { ...finalMessage } });
|
|
542
|
+
}
|
|
543
|
+
await emit({ type: "message_end", message: finalMessage });
|
|
544
|
+
return finalMessage;
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
if (signal?.aborted && isAbortError(error)) {
|
|
548
|
+
return await finishAbortedMessage();
|
|
549
|
+
}
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
let outcome;
|
|
554
|
+
try {
|
|
555
|
+
outcome = { message: await runResponse() };
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
outcome = { error };
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
if (build.projection)
|
|
562
|
+
await build.projection.release?.();
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
if ("error" in outcome) {
|
|
566
|
+
throw new AggregateError([outcome.error, error], `Agent inference and context cleanup failed: ${String(outcome.error)}`);
|
|
567
|
+
}
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
if ("error" in outcome) {
|
|
571
|
+
if (allowRecovery &&
|
|
572
|
+
!addedPartial &&
|
|
573
|
+
isLocalRequestPreparationError(outcome.error) &&
|
|
574
|
+
config.recoverRequestPreparation) {
|
|
575
|
+
let recovered;
|
|
576
|
+
try {
|
|
577
|
+
recovered = await config.recoverRequestPreparation(outcome.error, signal);
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
// Preserve the real owner error, including a post-ACK compaction failure.
|
|
581
|
+
markLocalRequestPreparationError(error);
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
if (recovered) {
|
|
585
|
+
// Rebuild/adopt the committed owner view. Keep this invocation's output collector,
|
|
586
|
+
// completed tools and emitted messages; only this unsent request gets one reprepare.
|
|
587
|
+
return streamAssistantResponse(context, config, signal, emit, streamFn, false);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
throw outcome.error;
|
|
591
|
+
}
|
|
592
|
+
return outcome.message;
|
|
593
|
+
}
|
|
594
|
+
async function executeToolCalls(currentContext, assistantMessage, config, signal, emit, output) {
|
|
595
|
+
const toolCalls = assistantMessage.content
|
|
596
|
+
.filter((c) => c.type === "toolCall")
|
|
597
|
+
.map((toolCall, sourceOrder) => ({
|
|
598
|
+
toolCall,
|
|
599
|
+
sourceOrder,
|
|
600
|
+
executionId: crypto.randomUUID(),
|
|
601
|
+
assistantMessage,
|
|
602
|
+
originalInput: structuredClone(toolCall.arguments),
|
|
603
|
+
}));
|
|
604
|
+
const hasSequentialToolCall = toolCalls.some(({ toolCall }) => currentContext.tools?.find((t) => t.name === toolCall.name)?.executionMode === "sequential");
|
|
605
|
+
if (config.toolExecution === "sequential" || hasSequentialToolCall) {
|
|
606
|
+
return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit, output);
|
|
607
|
+
}
|
|
608
|
+
return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);
|
|
609
|
+
}
|
|
610
|
+
async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit, output) {
|
|
611
|
+
const finalizedCalls = [];
|
|
612
|
+
const messages = [];
|
|
613
|
+
const exchanges = [];
|
|
614
|
+
for (const source of toolCalls) {
|
|
615
|
+
const { toolCall } = source;
|
|
616
|
+
if (signal?.aborted) {
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
await emit({
|
|
620
|
+
type: "tool_execution_start",
|
|
621
|
+
toolCallId: toolCall.id,
|
|
622
|
+
toolName: toolCall.name,
|
|
623
|
+
args: toolCall.arguments,
|
|
624
|
+
});
|
|
625
|
+
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
|
|
626
|
+
let finalized;
|
|
627
|
+
if (preparation.kind === "immediate") {
|
|
628
|
+
finalized = {
|
|
629
|
+
toolCall,
|
|
630
|
+
result: preparation.result,
|
|
631
|
+
isError: preparation.isError,
|
|
632
|
+
executionOutcome: "not_started",
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
const executed = await executePreparedToolCall(preparation, source, "sequential", config, signal, emit);
|
|
637
|
+
finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
|
|
638
|
+
}
|
|
639
|
+
const published = await publishToolExchange(finalized, source, "sequential", config, signal, emit);
|
|
640
|
+
await emitToolResultMessage(published.exchange.result, emit);
|
|
641
|
+
finalizedCalls.push(finalized);
|
|
642
|
+
messages.push(published.exchange.result);
|
|
643
|
+
exchanges.push(published.exchange);
|
|
644
|
+
if (signal?.aborted || output?.refusal) {
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
messages,
|
|
650
|
+
exchanges,
|
|
651
|
+
toolExecution: "sequential",
|
|
652
|
+
terminate: shouldTerminateToolBatch(finalizedCalls),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
async function executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit) {
|
|
656
|
+
const finalizedCalls = [];
|
|
657
|
+
for (const source of toolCalls) {
|
|
658
|
+
const { toolCall } = source;
|
|
659
|
+
await emit({
|
|
660
|
+
type: "tool_execution_start",
|
|
661
|
+
toolCallId: toolCall.id,
|
|
662
|
+
toolName: toolCall.name,
|
|
663
|
+
args: toolCall.arguments,
|
|
664
|
+
});
|
|
665
|
+
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
|
|
666
|
+
if (preparation.kind === "immediate") {
|
|
667
|
+
const finalized = {
|
|
668
|
+
toolCall,
|
|
669
|
+
result: preparation.result,
|
|
670
|
+
isError: preparation.isError,
|
|
671
|
+
executionOutcome: "not_started",
|
|
672
|
+
};
|
|
673
|
+
finalizedCalls.push(await publishToolExchange(finalized, source, "parallel", config, signal, emit));
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
finalizedCalls.push(async () => {
|
|
677
|
+
const executed = await executePreparedToolCall(preparation, source, "parallel", config, signal, emit);
|
|
678
|
+
const finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
|
|
679
|
+
return publishToolExchange(finalized, source, "parallel", config, signal, emit);
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
// An owner/publication failure must not abandon other invocations already started.
|
|
683
|
+
const settledCalls = await Promise.allSettled(finalizedCalls.map((entry) => (typeof entry === "function" ? entry() : Promise.resolve(entry))));
|
|
684
|
+
const orderedFinalizedCalls = settledCalls.flatMap((settled) => settled.status === "fulfilled" ? [settled.value] : []);
|
|
685
|
+
const failure = settledCalls.find((settled) => settled.status === "rejected");
|
|
686
|
+
const messages = [];
|
|
687
|
+
for (const finalized of orderedFinalizedCalls) {
|
|
688
|
+
const toolResultMessage = finalized.exchange.result;
|
|
689
|
+
await emitToolResultMessage(toolResultMessage, emit);
|
|
690
|
+
messages.push(toolResultMessage);
|
|
691
|
+
}
|
|
692
|
+
if (failure?.status === "rejected")
|
|
693
|
+
throw failure.reason;
|
|
694
|
+
return {
|
|
695
|
+
messages,
|
|
696
|
+
exchanges: orderedFinalizedCalls.map((finalized) => finalized.exchange),
|
|
697
|
+
toolExecution: "parallel",
|
|
698
|
+
terminate: shouldTerminateToolBatch(orderedFinalizedCalls),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function shouldTerminateToolBatch(finalizedCalls) {
|
|
702
|
+
return finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);
|
|
703
|
+
}
|
|
704
|
+
function prepareToolCallArguments(tool, toolCall) {
|
|
705
|
+
if (!tool.prepareArguments) {
|
|
706
|
+
return toolCall;
|
|
707
|
+
}
|
|
708
|
+
const preparedArguments = tool.prepareArguments(toolCall.arguments);
|
|
709
|
+
if (preparedArguments === toolCall.arguments) {
|
|
710
|
+
return toolCall;
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
...toolCall,
|
|
714
|
+
arguments: preparedArguments,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
async function prepareToolCall(currentContext, assistantMessage, toolCall, config, signal) {
|
|
718
|
+
const tool = currentContext.tools?.find((t) => t.name === toolCall.name);
|
|
719
|
+
if (!tool) {
|
|
720
|
+
return {
|
|
721
|
+
kind: "immediate",
|
|
722
|
+
result: createErrorToolResult(`Tool ${toolCall.name} not found`),
|
|
723
|
+
isError: true,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
const preparedToolCall = prepareToolCallArguments(tool, toolCall);
|
|
728
|
+
const validatedArgs = validateToolArguments(tool, preparedToolCall);
|
|
729
|
+
if (config.beforeToolCall) {
|
|
730
|
+
const beforeResult = await maybePromiseWithAbort(config.beforeToolCall({
|
|
731
|
+
assistantMessage,
|
|
732
|
+
toolCall,
|
|
733
|
+
args: validatedArgs,
|
|
734
|
+
context: currentContext,
|
|
735
|
+
}, signal), signal);
|
|
736
|
+
if (beforeResult?.block) {
|
|
737
|
+
return {
|
|
738
|
+
kind: "immediate",
|
|
739
|
+
result: createErrorToolResult(beforeResult.reason || "Tool execution was blocked"),
|
|
740
|
+
isError: true,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
kind: "prepared",
|
|
746
|
+
toolCall,
|
|
747
|
+
tool,
|
|
748
|
+
args: validatedArgs,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
catch (error) {
|
|
752
|
+
return {
|
|
753
|
+
kind: "immediate",
|
|
754
|
+
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
|
755
|
+
isError: true,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async function executePreparedToolCall(prepared, source, toolExecution, config, signal, emit) {
|
|
760
|
+
const invocationArgs = structuredClone(prepared.args);
|
|
761
|
+
const executedInput = structuredClone(invocationArgs);
|
|
762
|
+
const execute = prepared.tool.execute;
|
|
763
|
+
if (!signal?.aborted) {
|
|
764
|
+
// Admission failures must not become synthetic tool results or fall through to effects.
|
|
765
|
+
const owner = await config.onToolInvocationStarting?.({
|
|
766
|
+
executionId: source.executionId,
|
|
767
|
+
sourceOrder: source.sourceOrder,
|
|
768
|
+
toolCallId: prepared.toolCall.id,
|
|
769
|
+
toolName: prepared.toolCall.name,
|
|
770
|
+
originalInput: source.originalInput,
|
|
771
|
+
executedInput,
|
|
772
|
+
toolExecution,
|
|
773
|
+
}, signal, prepared.tool, execute, source.assistantMessage);
|
|
774
|
+
if (owner)
|
|
775
|
+
source.owner = owner;
|
|
776
|
+
}
|
|
777
|
+
const updateEvents = [];
|
|
778
|
+
let acceptingUpdates = true;
|
|
779
|
+
let executionStarted = false;
|
|
780
|
+
try {
|
|
781
|
+
throwIfAborted(signal);
|
|
782
|
+
executionStarted = true;
|
|
783
|
+
const run = () => execute.call(prepared.tool, prepared.toolCall.id, invocationArgs, signal, (partialResult) => {
|
|
784
|
+
if (!acceptingUpdates || signal?.aborted) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
updateEvents.push(Promise.resolve(emit({
|
|
788
|
+
type: "tool_execution_update",
|
|
789
|
+
toolCallId: prepared.toolCall.id,
|
|
790
|
+
toolName: prepared.toolCall.name,
|
|
791
|
+
args: prepared.toolCall.arguments,
|
|
792
|
+
partialResult,
|
|
793
|
+
})));
|
|
794
|
+
});
|
|
795
|
+
const result = await raceWithAbort(source.owner ? source.owner.run(run) : run(), signal);
|
|
796
|
+
acceptingUpdates = false;
|
|
797
|
+
try {
|
|
798
|
+
await raceWithAbort(Promise.all(updateEvents).then(() => undefined), signal);
|
|
799
|
+
}
|
|
800
|
+
catch (error) {
|
|
801
|
+
if (!signal?.aborted || !isAbortError(error)) {
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return { result, isError: false, executedInput, executionOutcome: "completed" };
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
acceptingUpdates = false;
|
|
809
|
+
await raceWithAbort(Promise.all(updateEvents).then(() => undefined), signal).catch(() => undefined);
|
|
810
|
+
return {
|
|
811
|
+
result: createErrorToolResult(signal?.aborted ? "Tool execution aborted" : error instanceof Error ? error.message : String(error)),
|
|
812
|
+
isError: true,
|
|
813
|
+
...(executionStarted ? { executedInput } : {}),
|
|
814
|
+
executionOutcome: !executionStarted ? "not_started" : signal?.aborted ? "outcome_unknown" : "failed",
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, signal) {
|
|
819
|
+
let result = executed.result;
|
|
820
|
+
let isError = executed.isError;
|
|
821
|
+
if (config.afterToolCall) {
|
|
822
|
+
try {
|
|
823
|
+
const afterResult = await maybePromiseWithAbort(config.afterToolCall({
|
|
824
|
+
assistantMessage,
|
|
825
|
+
toolCall: prepared.toolCall,
|
|
826
|
+
args: prepared.args,
|
|
827
|
+
result,
|
|
828
|
+
isError,
|
|
829
|
+
context: currentContext,
|
|
830
|
+
}, signal), signal);
|
|
831
|
+
if (afterResult) {
|
|
832
|
+
result = {
|
|
833
|
+
content: afterResult.content ?? result.content,
|
|
834
|
+
details: afterResult.details ?? result.details,
|
|
835
|
+
terminate: afterResult.terminate ?? result.terminate,
|
|
836
|
+
};
|
|
837
|
+
isError = afterResult.isError ?? isError;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
catch (error) {
|
|
841
|
+
result = createErrorToolResult(error instanceof Error ? error.message : String(error));
|
|
842
|
+
isError = true;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
...executed,
|
|
847
|
+
toolCall: prepared.toolCall,
|
|
848
|
+
result,
|
|
849
|
+
isError,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function createErrorToolResult(message) {
|
|
853
|
+
return {
|
|
854
|
+
content: [{ type: "text", text: message }],
|
|
855
|
+
details: {},
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
async function publishToolExchange(finalized, source, toolExecution, config, signal, emit) {
|
|
859
|
+
const exchange = {
|
|
860
|
+
executionId: source.executionId,
|
|
861
|
+
sourceOrder: source.sourceOrder,
|
|
862
|
+
toolCallId: finalized.toolCall.id,
|
|
863
|
+
toolName: finalized.toolCall.name,
|
|
864
|
+
originalInput: source.originalInput,
|
|
865
|
+
...(finalized.executionOutcome === "not_started" ? {} : { executedInput: finalized.executedInput }),
|
|
866
|
+
toolExecution,
|
|
867
|
+
executionOutcome: finalized.executionOutcome,
|
|
868
|
+
cancellationRequested: signal?.aborted ?? false,
|
|
869
|
+
result: createToolResultMessage(finalized),
|
|
870
|
+
};
|
|
871
|
+
await config.onToolExchangeFinalized?.(exchange, signal, source.owner);
|
|
872
|
+
await emit({
|
|
873
|
+
type: "tool_execution_end",
|
|
874
|
+
toolCallId: finalized.toolCall.id,
|
|
875
|
+
toolName: finalized.toolCall.name,
|
|
876
|
+
result: finalized.result,
|
|
877
|
+
isError: finalized.isError,
|
|
878
|
+
exchange,
|
|
879
|
+
});
|
|
880
|
+
return { ...finalized, exchange };
|
|
881
|
+
}
|
|
882
|
+
function createToolResultMessage(finalized) {
|
|
883
|
+
return {
|
|
884
|
+
role: "toolResult",
|
|
885
|
+
toolCallId: finalized.toolCall.id,
|
|
886
|
+
toolName: finalized.toolCall.name,
|
|
887
|
+
content: finalized.result.content,
|
|
888
|
+
details: finalized.result.details,
|
|
889
|
+
isError: finalized.isError,
|
|
890
|
+
timestamp: Date.now(),
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
async function emitToolResultMessage(toolResultMessage, emit) {
|
|
894
|
+
await emit({ type: "message_start", message: toolResultMessage });
|
|
895
|
+
await emit({ type: "message_end", message: toolResultMessage });
|
|
896
|
+
}
|
|
897
|
+
//# sourceMappingURL=agent-loop.js.map
|