@code-yeongyu/senpi-agent-core 2026.7.25-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 +508 -0
- package/dist/agent-loop.d.ts +31 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +851 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent.d.ts +140 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +464 -0
- package/dist/agent.js.map +1 -0
- package/dist/harness/agent-harness.d.ts +94 -0
- package/dist/harness/agent-harness.d.ts.map +1 -0
- package/dist/harness/agent-harness.js +988 -0
- package/dist/harness/agent-harness.js.map +1 -0
- package/dist/harness/compaction/branch-summarization.d.ts +55 -0
- package/dist/harness/compaction/branch-summarization.d.ts.map +1 -0
- package/dist/harness/compaction/branch-summarization.js +172 -0
- package/dist/harness/compaction/branch-summarization.js.map +1 -0
- package/dist/harness/compaction/compaction.d.ts +107 -0
- package/dist/harness/compaction/compaction.d.ts.map +1 -0
- package/dist/harness/compaction/compaction.js +588 -0
- package/dist/harness/compaction/compaction.js.map +1 -0
- package/dist/harness/compaction/utils.d.ts +36 -0
- package/dist/harness/compaction/utils.d.ts.map +1 -0
- package/dist/harness/compaction/utils.js +136 -0
- package/dist/harness/compaction/utils.js.map +1 -0
- package/dist/harness/env/nodejs.d.ts +45 -0
- package/dist/harness/env/nodejs.d.ts.map +1 -0
- package/dist/harness/env/nodejs.js +620 -0
- package/dist/harness/env/nodejs.js.map +1 -0
- package/dist/harness/messages.d.ts +52 -0
- package/dist/harness/messages.d.ts.map +1 -0
- package/dist/harness/messages.js +102 -0
- package/dist/harness/messages.js.map +1 -0
- package/dist/harness/prompt-templates.d.ts +48 -0
- package/dist/harness/prompt-templates.d.ts.map +1 -0
- package/dist/harness/prompt-templates.js +230 -0
- package/dist/harness/prompt-templates.js.map +1 -0
- package/dist/harness/session/jsonl-repo.d.ts +26 -0
- package/dist/harness/session/jsonl-repo.d.ts.map +1 -0
- package/dist/harness/session/jsonl-repo.js +100 -0
- package/dist/harness/session/jsonl-repo.js.map +1 -0
- package/dist/harness/session/jsonl-storage.d.ts +42 -0
- package/dist/harness/session/jsonl-storage.d.ts.map +1 -0
- package/dist/harness/session/jsonl-storage.js +286 -0
- package/dist/harness/session/jsonl-storage.js.map +1 -0
- package/dist/harness/session/memory-repo.d.ts +18 -0
- package/dist/harness/session/memory-repo.d.ts.map +1 -0
- package/dist/harness/session/memory-repo.js +44 -0
- package/dist/harness/session/memory-repo.js.map +1 -0
- package/dist/harness/session/memory-storage.d.ts +33 -0
- package/dist/harness/session/memory-storage.d.ts.map +1 -0
- package/dist/harness/session/memory-storage.js +163 -0
- package/dist/harness/session/memory-storage.js.map +1 -0
- package/dist/harness/session/repo-utils.d.ts +11 -0
- package/dist/harness/session/repo-utils.d.ts.map +1 -0
- package/dist/harness/session/repo-utils.js +39 -0
- package/dist/harness/session/repo-utils.js.map +1 -0
- package/dist/harness/session/session.d.ts +49 -0
- package/dist/harness/session/session.d.ts.map +1 -0
- package/dist/harness/session/session.js +261 -0
- package/dist/harness/session/session.js.map +1 -0
- package/dist/harness/skills.d.ts +44 -0
- package/dist/harness/skills.d.ts.map +1 -0
- package/dist/harness/skills.js +311 -0
- package/dist/harness/skills.js.map +1 -0
- package/dist/harness/system-prompt.d.ts +3 -0
- package/dist/harness/system-prompt.d.ts.map +1 -0
- package/dist/harness/system-prompt.js +30 -0
- package/dist/harness/system-prompt.js.map +1 -0
- package/dist/harness/tools/bash.d.ts +27 -0
- package/dist/harness/tools/bash.d.ts.map +1 -0
- package/dist/harness/tools/bash.js +132 -0
- package/dist/harness/tools/bash.js.map +1 -0
- package/dist/harness/tools/edit-diff.d.ts +89 -0
- package/dist/harness/tools/edit-diff.d.ts.map +1 -0
- package/dist/harness/tools/edit-diff.js +386 -0
- package/dist/harness/tools/edit-diff.js.map +1 -0
- package/dist/harness/tools/edit.d.ts +19 -0
- package/dist/harness/tools/edit.d.ts.map +1 -0
- package/dist/harness/tools/edit.js +95 -0
- package/dist/harness/tools/edit.js.map +1 -0
- package/dist/harness/tools/file-mutation-queue.d.ts +4 -0
- package/dist/harness/tools/file-mutation-queue.d.ts.map +1 -0
- package/dist/harness/tools/file-mutation-queue.js +46 -0
- package/dist/harness/tools/file-mutation-queue.js.map +1 -0
- package/dist/harness/tools/image.d.ts +3 -0
- package/dist/harness/tools/image.d.ts.map +1 -0
- package/dist/harness/tools/image.js +106 -0
- package/dist/harness/tools/image.js.map +1 -0
- package/dist/harness/tools/index.d.ts +6 -0
- package/dist/harness/tools/index.d.ts.map +1 -0
- package/dist/harness/tools/index.js +5 -0
- package/dist/harness/tools/index.js.map +1 -0
- package/dist/harness/tools/path-utils.d.ts +4 -0
- package/dist/harness/tools/path-utils.d.ts.map +1 -0
- package/dist/harness/tools/path-utils.js +26 -0
- package/dist/harness/tools/path-utils.js.map +1 -0
- package/dist/harness/tools/read.d.ts +34 -0
- package/dist/harness/tools/read.d.ts.map +1 -0
- package/dist/harness/tools/read.js +110 -0
- package/dist/harness/tools/read.js.map +1 -0
- package/dist/harness/tools/tool-context.d.ts +6 -0
- package/dist/harness/tools/tool-context.d.ts.map +1 -0
- package/dist/harness/tools/tool-context.js +2 -0
- package/dist/harness/tools/tool-context.js.map +1 -0
- package/dist/harness/tools/write.d.ts +11 -0
- package/dist/harness/tools/write.d.ts.map +1 -0
- package/dist/harness/tools/write.js +31 -0
- package/dist/harness/tools/write.js.map +1 -0
- package/dist/harness/types.d.ts +682 -0
- package/dist/harness/types.d.ts.map +1 -0
- package/dist/harness/types.js +83 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/utils/shell-output.d.ts +22 -0
- package/dist/harness/utils/shell-output.d.ts.map +1 -0
- package/dist/harness/utils/shell-output.js +181 -0
- package/dist/harness/utils/shell-output.js.map +1 -0
- package/dist/harness/utils/truncate.d.ts +70 -0
- package/dist/harness/utils/truncate.d.ts.map +1 -0
- package/dist/harness/utils/truncate.js +296 -0
- package/dist/harness/utils/truncate.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +3 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +3 -0
- package/dist/node.js.map +1 -0
- package/dist/proxy.d.ts +71 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +280 -0
- package/dist/proxy.js.map +1 -0
- package/dist/stream-fn.d.ts +10 -0
- package/dist/stream-fn.d.ts.map +1 -0
- package/dist/stream-fn.js +17 -0
- package/dist/stream-fn.js.map +1 -0
- package/dist/types.d.ts +415 -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 +61 -0
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent loop that works with AgentMessage throughout.
|
|
3
|
+
* Transforms to Message[] only at the LLM call boundary.
|
|
4
|
+
*/
|
|
5
|
+
import { EventStream, validateToolArguments, } from "@earendil-works/pi-ai";
|
|
6
|
+
import { getDefaultStreamFn } from "./stream-fn.js";
|
|
7
|
+
/**
|
|
8
|
+
* Start an agent loop with a new prompt message.
|
|
9
|
+
* The prompt is added to the context and events are emitted for it.
|
|
10
|
+
*/
|
|
11
|
+
export function agentLoop(prompts, context, config, signal, streamFn) {
|
|
12
|
+
const stream = createAgentStream();
|
|
13
|
+
void runAgentLoop(prompts, context, config, async (event) => {
|
|
14
|
+
stream.push(event);
|
|
15
|
+
}, signal, streamFn).then((messages) => {
|
|
16
|
+
stream.end(messages);
|
|
17
|
+
});
|
|
18
|
+
return stream;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Continue an agent loop from the current context without adding a new message.
|
|
22
|
+
* Used for retries - context already has user message or tool results.
|
|
23
|
+
*
|
|
24
|
+
* **Important:** The last message in context must convert to a `user` or `toolResult` message
|
|
25
|
+
* via `convertToLlm`. If it doesn't, the LLM provider will reject the request.
|
|
26
|
+
* This cannot be validated here since `convertToLlm` is only called once per turn.
|
|
27
|
+
*/
|
|
28
|
+
export function agentLoopContinue(context, config, signal, streamFn) {
|
|
29
|
+
if (context.messages.length === 0) {
|
|
30
|
+
throw new Error("Cannot continue: no messages in context");
|
|
31
|
+
}
|
|
32
|
+
if (context.messages[context.messages.length - 1].role === "assistant") {
|
|
33
|
+
throw new Error("Cannot continue from message role: assistant");
|
|
34
|
+
}
|
|
35
|
+
const stream = createAgentStream();
|
|
36
|
+
void runAgentLoopContinue(context, config, async (event) => {
|
|
37
|
+
stream.push(event);
|
|
38
|
+
}, signal, streamFn).then((messages) => {
|
|
39
|
+
stream.end(messages);
|
|
40
|
+
});
|
|
41
|
+
return stream;
|
|
42
|
+
}
|
|
43
|
+
export async function runAgentLoop(prompts, context, config, emit, signal, streamFn) {
|
|
44
|
+
const newMessages = [...prompts];
|
|
45
|
+
const currentContext = {
|
|
46
|
+
...context,
|
|
47
|
+
messages: [...context.messages, ...prompts],
|
|
48
|
+
};
|
|
49
|
+
await emit({ type: "agent_start" });
|
|
50
|
+
await emit({ type: "turn_start" });
|
|
51
|
+
for (const prompt of prompts) {
|
|
52
|
+
await emit({ type: "message_start", message: prompt });
|
|
53
|
+
await emit({ type: "message_end", message: prompt });
|
|
54
|
+
}
|
|
55
|
+
await runLoop(currentContext, newMessages, config, signal, emit, streamFn ?? getDefaultStreamFn());
|
|
56
|
+
return newMessages;
|
|
57
|
+
}
|
|
58
|
+
export async function runAgentLoopContinue(context, config, emit, signal, streamFn) {
|
|
59
|
+
if (context.messages.length === 0) {
|
|
60
|
+
throw new Error("Cannot continue: no messages in context");
|
|
61
|
+
}
|
|
62
|
+
if (context.messages[context.messages.length - 1].role === "assistant") {
|
|
63
|
+
throw new Error("Cannot continue from message role: assistant");
|
|
64
|
+
}
|
|
65
|
+
const newMessages = [];
|
|
66
|
+
const currentContext = { ...context };
|
|
67
|
+
await emit({ type: "agent_start" });
|
|
68
|
+
await emit({ type: "turn_start" });
|
|
69
|
+
await runLoop(currentContext, newMessages, config, signal, emit, streamFn ?? getDefaultStreamFn());
|
|
70
|
+
return newMessages;
|
|
71
|
+
}
|
|
72
|
+
function createAgentStream() {
|
|
73
|
+
return new EventStream((event) => event.type === "agent_end", (event) => (event.type === "agent_end" ? event.messages : []));
|
|
74
|
+
}
|
|
75
|
+
const EMPTY_USAGE = {
|
|
76
|
+
input: 0,
|
|
77
|
+
output: 0,
|
|
78
|
+
cacheRead: 0,
|
|
79
|
+
cacheWrite: 0,
|
|
80
|
+
totalTokens: 0,
|
|
81
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
82
|
+
};
|
|
83
|
+
class StreamIdleTimeoutError extends Error {
|
|
84
|
+
constructor(timeoutMs) {
|
|
85
|
+
super(`Idle timeout waiting for provider stream after ${timeoutMs}ms`);
|
|
86
|
+
this.name = "StreamIdleTimeoutError";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Main loop logic shared by agentLoop and agentLoopContinue.
|
|
91
|
+
*/
|
|
92
|
+
async function runLoop(initialContext, newMessages, initialConfig, signal, emit, streamFunction) {
|
|
93
|
+
let currentContext = initialContext;
|
|
94
|
+
let config = initialConfig;
|
|
95
|
+
let firstTurn = true;
|
|
96
|
+
// Check for steering messages at start (user may have typed while waiting)
|
|
97
|
+
let pendingMessages = (await config.getSteeringMessages?.()) || [];
|
|
98
|
+
let drainedTerminatingQueue;
|
|
99
|
+
const refreshTerminatingQueueDrain = async () => {
|
|
100
|
+
if (!drainedTerminatingQueue || !config.restorePendingMessages)
|
|
101
|
+
return;
|
|
102
|
+
await config.restorePendingMessages(drainedTerminatingQueue, pendingMessages);
|
|
103
|
+
pendingMessages = (await config.getSteeringMessages?.()) || [];
|
|
104
|
+
drainedTerminatingQueue = pendingMessages.length > 0 ? "steering" : undefined;
|
|
105
|
+
if (pendingMessages.length === 0) {
|
|
106
|
+
pendingMessages = (await config.getFollowUpMessages?.()) || [];
|
|
107
|
+
drainedTerminatingQueue = pendingMessages.length > 0 ? "followUp" : undefined;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
// Outer loop: continues when queued follow-up messages arrive after agent would stop
|
|
111
|
+
while (true) {
|
|
112
|
+
let hasMoreToolCalls = true;
|
|
113
|
+
// Inner loop: process tool calls and steering messages
|
|
114
|
+
while (hasMoreToolCalls || pendingMessages.length > 0) {
|
|
115
|
+
if (!firstTurn) {
|
|
116
|
+
await emit({ type: "turn_start" });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
firstTurn = false;
|
|
120
|
+
}
|
|
121
|
+
if (drainedTerminatingQueue) {
|
|
122
|
+
await refreshTerminatingQueueDrain();
|
|
123
|
+
if (pendingMessages.length === 0) {
|
|
124
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
drainedTerminatingQueue = undefined;
|
|
128
|
+
}
|
|
129
|
+
// Process pending messages (inject before next assistant response)
|
|
130
|
+
if (pendingMessages.length > 0) {
|
|
131
|
+
for (const message of pendingMessages) {
|
|
132
|
+
await emit({ type: "message_start", message });
|
|
133
|
+
await emit({ type: "message_end", message });
|
|
134
|
+
currentContext.messages.push(message);
|
|
135
|
+
newMessages.push(message);
|
|
136
|
+
}
|
|
137
|
+
pendingMessages = [];
|
|
138
|
+
}
|
|
139
|
+
// Stream assistant response
|
|
140
|
+
const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFunction);
|
|
141
|
+
newMessages.push(message);
|
|
142
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
143
|
+
await emit({ type: "turn_end", message, toolResults: [] });
|
|
144
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Check for tool calls
|
|
148
|
+
const toolCalls = message.content.filter((c) => c.type === "toolCall");
|
|
149
|
+
const toolResults = [];
|
|
150
|
+
hasMoreToolCalls = false;
|
|
151
|
+
let toolBatchTerminated = false;
|
|
152
|
+
if (toolCalls.length > 0) {
|
|
153
|
+
// A native "length" stop means the output was cut off by the token limit,
|
|
154
|
+
// so every tool call in the message may carry truncated arguments. Text
|
|
155
|
+
// tool-call middleware finalizes its calls as "toolUse", leaving only
|
|
156
|
+
// native, unwrapped length responses for this message-wide safeguard.
|
|
157
|
+
const executedToolBatch = message.stopReason === "length"
|
|
158
|
+
? await failToolCallsFromTruncatedMessage(toolCalls, emit)
|
|
159
|
+
: await executeToolCalls(currentContext, message, config, signal, emit);
|
|
160
|
+
toolResults.push(...executedToolBatch.messages);
|
|
161
|
+
toolBatchTerminated = executedToolBatch.terminate;
|
|
162
|
+
hasMoreToolCalls = !executedToolBatch.terminate;
|
|
163
|
+
for (const result of toolResults) {
|
|
164
|
+
currentContext.messages.push(result);
|
|
165
|
+
newMessages.push(result);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
await emit({ type: "turn_end", message, toolResults });
|
|
169
|
+
if (signal?.aborted) {
|
|
170
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const nextTurnContext = {
|
|
174
|
+
message,
|
|
175
|
+
toolResults,
|
|
176
|
+
context: currentContext,
|
|
177
|
+
newMessages,
|
|
178
|
+
};
|
|
179
|
+
if (toolBatchTerminated) {
|
|
180
|
+
if (await config.shouldStopAfterTurn?.(nextTurnContext)) {
|
|
181
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
pendingMessages = (await config.getSteeringMessages?.()) || [];
|
|
185
|
+
if (pendingMessages.length > 0) {
|
|
186
|
+
drainedTerminatingQueue = "steering";
|
|
187
|
+
}
|
|
188
|
+
if (pendingMessages.length === 0) {
|
|
189
|
+
pendingMessages = (await config.getFollowUpMessages?.()) || [];
|
|
190
|
+
if (pendingMessages.length > 0) {
|
|
191
|
+
drainedTerminatingQueue = "followUp";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (pendingMessages.length === 0) {
|
|
195
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
let nextTurnSnapshot;
|
|
200
|
+
try {
|
|
201
|
+
nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (drainedTerminatingQueue) {
|
|
205
|
+
await config.restorePendingMessages?.(drainedTerminatingQueue, pendingMessages);
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
if (nextTurnSnapshot) {
|
|
210
|
+
currentContext = nextTurnSnapshot.context ?? currentContext;
|
|
211
|
+
config = {
|
|
212
|
+
...config,
|
|
213
|
+
model: nextTurnSnapshot.model ?? config.model,
|
|
214
|
+
reasoning: nextTurnSnapshot.thinkingLevel === undefined
|
|
215
|
+
? config.reasoning
|
|
216
|
+
: nextTurnSnapshot.thinkingLevel === "off"
|
|
217
|
+
? undefined
|
|
218
|
+
: nextTurnSnapshot.thinkingLevel,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (signal?.aborted) {
|
|
222
|
+
if (drainedTerminatingQueue) {
|
|
223
|
+
await config.restorePendingMessages?.(drainedTerminatingQueue, pendingMessages);
|
|
224
|
+
}
|
|
225
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (drainedTerminatingQueue) {
|
|
229
|
+
await refreshTerminatingQueueDrain();
|
|
230
|
+
if (pendingMessages.length === 0) {
|
|
231
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!toolBatchTerminated &&
|
|
236
|
+
(await config.shouldStopAfterTurn?.({
|
|
237
|
+
message,
|
|
238
|
+
toolResults,
|
|
239
|
+
context: currentContext,
|
|
240
|
+
newMessages,
|
|
241
|
+
}))) {
|
|
242
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (!toolBatchTerminated) {
|
|
246
|
+
pendingMessages = (await config.getSteeringMessages?.()) || [];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Agent would stop here. Check for follow-up messages.
|
|
250
|
+
const followUpMessages = (await config.getFollowUpMessages?.()) || [];
|
|
251
|
+
if (followUpMessages.length > 0) {
|
|
252
|
+
// Set as pending so inner loop processes them
|
|
253
|
+
pendingMessages = followUpMessages;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
// No more messages, exit
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
await emit({ type: "agent_end", messages: newMessages });
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Stream an assistant response from the LLM.
|
|
263
|
+
* This is where AgentMessage[] gets transformed to Message[] for the LLM.
|
|
264
|
+
*/
|
|
265
|
+
async function streamAssistantResponse(context, config, signal, emit, streamFunction) {
|
|
266
|
+
let partialMessage = null;
|
|
267
|
+
let addedPartial = false;
|
|
268
|
+
const thinkingTiming = new Map();
|
|
269
|
+
function propagateThinkingTiming(finalMessage) {
|
|
270
|
+
for (const timing of thinkingTiming.values()) {
|
|
271
|
+
if (timing.endedAt === undefined)
|
|
272
|
+
timing.endedAt = Date.now();
|
|
273
|
+
}
|
|
274
|
+
for (const [contentIndex, timing] of thinkingTiming) {
|
|
275
|
+
const block = finalMessage.content[contentIndex];
|
|
276
|
+
if (block?.type !== "thinking")
|
|
277
|
+
continue;
|
|
278
|
+
block.startedAt = timing.startedAt;
|
|
279
|
+
block.endedAt = timing.endedAt;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Dedicated controller for the provider request so the loop can tear the
|
|
283
|
+
// request down itself (idle timeout), not only when the caller aborts.
|
|
284
|
+
const requestAbortController = new AbortController();
|
|
285
|
+
let detachCallerAbort;
|
|
286
|
+
if (signal !== undefined) {
|
|
287
|
+
if (signal.aborted) {
|
|
288
|
+
requestAbortController.abort(signal.reason);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const onCallerAbort = () => requestAbortController.abort(signal.reason);
|
|
292
|
+
signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
293
|
+
detachCallerAbort = () => signal.removeEventListener("abort", onCallerAbort);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
// Apply context transform if configured (AgentMessage[] → AgentMessage[])
|
|
298
|
+
let messages = context.messages;
|
|
299
|
+
if (config.transformContext) {
|
|
300
|
+
messages = await config.transformContext(messages, signal);
|
|
301
|
+
}
|
|
302
|
+
// Convert to LLM-compatible messages (AgentMessage[] → Message[])
|
|
303
|
+
const llmMessages = await config.convertToLlm(messages);
|
|
304
|
+
// Build LLM context
|
|
305
|
+
const llmContext = {
|
|
306
|
+
systemPrompt: context.systemPrompt,
|
|
307
|
+
messages: llmMessages,
|
|
308
|
+
tools: context.tools,
|
|
309
|
+
};
|
|
310
|
+
// Resolve API key (important for expiring tokens)
|
|
311
|
+
const resolvedApiKey = (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
|
|
312
|
+
const response = await streamFunction(config.model, llmContext, {
|
|
313
|
+
...config,
|
|
314
|
+
apiKey: resolvedApiKey,
|
|
315
|
+
signal: requestAbortController.signal,
|
|
316
|
+
});
|
|
317
|
+
const iterator = response[Symbol.asyncIterator]();
|
|
318
|
+
const eventReader = createAssistantEventReader(iterator, config.timeoutMs, requestAbortController.signal, (error) => requestAbortController.abort(error));
|
|
319
|
+
try {
|
|
320
|
+
while (true) {
|
|
321
|
+
const next = await eventReader.next();
|
|
322
|
+
if (next.done)
|
|
323
|
+
break;
|
|
324
|
+
const event = next.value;
|
|
325
|
+
switch (event.type) {
|
|
326
|
+
case "start":
|
|
327
|
+
partialMessage = event.partial;
|
|
328
|
+
context.messages.push(partialMessage);
|
|
329
|
+
addedPartial = true;
|
|
330
|
+
await emit({ type: "message_start", message: { ...partialMessage } });
|
|
331
|
+
break;
|
|
332
|
+
case "text_start":
|
|
333
|
+
case "text_delta":
|
|
334
|
+
case "text_end":
|
|
335
|
+
case "thinking_start":
|
|
336
|
+
case "thinking_delta":
|
|
337
|
+
case "thinking_end":
|
|
338
|
+
case "toolcall_start":
|
|
339
|
+
case "toolcall_delta":
|
|
340
|
+
case "toolcall_end":
|
|
341
|
+
if (partialMessage) {
|
|
342
|
+
partialMessage = event.partial;
|
|
343
|
+
if (event.type === "thinking_start" ||
|
|
344
|
+
event.type === "thinking_delta" ||
|
|
345
|
+
event.type === "thinking_end") {
|
|
346
|
+
let timing = thinkingTiming.get(event.contentIndex);
|
|
347
|
+
if (event.type === "thinking_start" && timing === undefined) {
|
|
348
|
+
timing = { startedAt: Date.now() };
|
|
349
|
+
thinkingTiming.set(event.contentIndex, timing);
|
|
350
|
+
}
|
|
351
|
+
if (event.type === "thinking_end" && timing !== undefined)
|
|
352
|
+
timing.endedAt = Date.now();
|
|
353
|
+
const block = partialMessage.content[event.contentIndex];
|
|
354
|
+
if (block?.type === "thinking" && timing !== undefined) {
|
|
355
|
+
block.startedAt = timing.startedAt;
|
|
356
|
+
if (timing.endedAt !== undefined)
|
|
357
|
+
block.endedAt = timing.endedAt;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
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
|
+
const finalMessage = normalizeTerminalAssistantMessage(await response.result(), event);
|
|
371
|
+
propagateThinkingTiming(finalMessage);
|
|
372
|
+
if (addedPartial) {
|
|
373
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
context.messages.push(finalMessage);
|
|
377
|
+
}
|
|
378
|
+
if (!addedPartial) {
|
|
379
|
+
await emit({ type: "message_start", message: { ...finalMessage } });
|
|
380
|
+
}
|
|
381
|
+
await emit({ type: "message_end", message: finalMessage });
|
|
382
|
+
return finalMessage;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
eventReader.dispose();
|
|
389
|
+
}
|
|
390
|
+
const finalMessage = await response.result();
|
|
391
|
+
propagateThinkingTiming(finalMessage);
|
|
392
|
+
if (addedPartial) {
|
|
393
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
context.messages.push(finalMessage);
|
|
397
|
+
await emit({ type: "message_start", message: { ...finalMessage } });
|
|
398
|
+
}
|
|
399
|
+
await emit({ type: "message_end", message: finalMessage });
|
|
400
|
+
return finalMessage;
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
const finalMessage = createTerminalFailureAssistantMessage(config.model, signal?.aborted ? "aborted" : "error", error, partialMessage);
|
|
404
|
+
propagateThinkingTiming(finalMessage);
|
|
405
|
+
if (addedPartial) {
|
|
406
|
+
context.messages[context.messages.length - 1] = finalMessage;
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
context.messages.push(finalMessage);
|
|
410
|
+
await emit({ type: "message_start", message: { ...finalMessage } });
|
|
411
|
+
}
|
|
412
|
+
await emit({ type: "message_end", message: finalMessage });
|
|
413
|
+
return finalMessage;
|
|
414
|
+
}
|
|
415
|
+
finally {
|
|
416
|
+
detachCallerAbort?.();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const ABORTED = Symbol("aborted");
|
|
420
|
+
function createAssistantEventReader(iterator, timeoutMs, signal, onIdleTimeout) {
|
|
421
|
+
const idleTimeoutMs = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
|
|
422
|
+
let removeAbortListener;
|
|
423
|
+
let abortPromise;
|
|
424
|
+
if (signal !== undefined) {
|
|
425
|
+
if (signal.aborted) {
|
|
426
|
+
abortPromise = Promise.resolve(ABORTED);
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
abortPromise = new Promise((resolve) => {
|
|
430
|
+
const abortHandler = () => resolve(ABORTED);
|
|
431
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
432
|
+
removeAbortListener = () => signal.removeEventListener("abort", abortHandler);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
next: () => {
|
|
438
|
+
if (signal?.aborted) {
|
|
439
|
+
void iterator.return?.();
|
|
440
|
+
return Promise.reject(new Error("Request was aborted"));
|
|
441
|
+
}
|
|
442
|
+
return readNextAssistantEvent(iterator, idleTimeoutMs, abortPromise, onIdleTimeout);
|
|
443
|
+
},
|
|
444
|
+
dispose: () => removeAbortListener?.(),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async function readNextAssistantEvent(iterator, idleTimeoutMs, abortPromise, onIdleTimeout) {
|
|
448
|
+
if (idleTimeoutMs === undefined && abortPromise === undefined) {
|
|
449
|
+
return iterator.next();
|
|
450
|
+
}
|
|
451
|
+
let timeout;
|
|
452
|
+
let settled = false;
|
|
453
|
+
return new Promise((resolve, reject) => {
|
|
454
|
+
const settle = (complete) => {
|
|
455
|
+
if (settled)
|
|
456
|
+
return;
|
|
457
|
+
settled = true;
|
|
458
|
+
if (timeout !== undefined) {
|
|
459
|
+
clearTimeout(timeout);
|
|
460
|
+
}
|
|
461
|
+
complete();
|
|
462
|
+
};
|
|
463
|
+
if (idleTimeoutMs !== undefined) {
|
|
464
|
+
timeout = setTimeout(() => {
|
|
465
|
+
const error = new StreamIdleTimeoutError(idleTimeoutMs);
|
|
466
|
+
void iterator.return?.();
|
|
467
|
+
settle(() => reject(error));
|
|
468
|
+
// Abort after settling so the failure surfaces as an idle timeout,
|
|
469
|
+
// not as a generic abort, while the dead request still gets torn down.
|
|
470
|
+
onIdleTimeout?.(error);
|
|
471
|
+
}, idleTimeoutMs);
|
|
472
|
+
}
|
|
473
|
+
const next = abortPromise ? Promise.race([iterator.next(), abortPromise]) : iterator.next();
|
|
474
|
+
void next.then((result) => {
|
|
475
|
+
if (result === ABORTED) {
|
|
476
|
+
void iterator.return?.();
|
|
477
|
+
settle(() => reject(new Error("Request was aborted")));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
settle(() => resolve(result));
|
|
481
|
+
}, (error) => settle(() => reject(error)));
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
function createIncompleteToolCallErrorMessage(toolName, errorMessage) {
|
|
485
|
+
if (errorMessage !== undefined) {
|
|
486
|
+
return `${errorMessage}${errorMessage.endsWith(".") ? "" : "."} Re-issue the tool call with complete arguments.`;
|
|
487
|
+
}
|
|
488
|
+
return `Tool call "${toolName}" was not executed: the response ended before the tool call was complete because it hit the output token limit. Re-issue the tool call with complete arguments.`;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Fail all tool calls from an assistant message that was truncated by the
|
|
492
|
+
* output token limit. Streamed tool-call arguments are finalized with a
|
|
493
|
+
* best-effort JSON salvage parser, so a truncated message can yield tool calls
|
|
494
|
+
* whose arguments parse and validate but are silently incomplete. None of them
|
|
495
|
+
* are safe to execute; report each as an error so the model can re-issue them.
|
|
496
|
+
*/
|
|
497
|
+
async function failToolCallsFromTruncatedMessage(toolCalls, emit) {
|
|
498
|
+
const messages = [];
|
|
499
|
+
for (const toolCall of toolCalls) {
|
|
500
|
+
await emit({
|
|
501
|
+
type: "tool_execution_start",
|
|
502
|
+
toolCallId: toolCall.id,
|
|
503
|
+
toolName: toolCall.name,
|
|
504
|
+
args: toolCall.arguments,
|
|
505
|
+
});
|
|
506
|
+
const finalized = {
|
|
507
|
+
toolCall,
|
|
508
|
+
result: createErrorToolResult(createIncompleteToolCallErrorMessage(toolCall.name)),
|
|
509
|
+
isError: true,
|
|
510
|
+
};
|
|
511
|
+
await emitToolExecutionEnd(finalized, emit);
|
|
512
|
+
const toolResultMessage = createToolResultMessage(finalized);
|
|
513
|
+
await emitToolResultMessage(toolResultMessage, emit);
|
|
514
|
+
messages.push(toolResultMessage);
|
|
515
|
+
}
|
|
516
|
+
return { messages, terminate: false };
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Execute tool calls from an assistant message.
|
|
520
|
+
*/
|
|
521
|
+
async function executeToolCalls(currentContext, assistantMessage, config, signal, emit) {
|
|
522
|
+
const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
|
|
523
|
+
if (config.toolExecution === "sequential") {
|
|
524
|
+
return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit);
|
|
525
|
+
}
|
|
526
|
+
return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);
|
|
527
|
+
}
|
|
528
|
+
function createTerminalFailureAssistantMessage(model, reason, error, partialMessage) {
|
|
529
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
530
|
+
return {
|
|
531
|
+
role: "assistant",
|
|
532
|
+
content: partialMessage?.content ?? [{ type: "text", text: "" }],
|
|
533
|
+
api: partialMessage?.api ?? model.api,
|
|
534
|
+
provider: partialMessage?.provider ?? model.provider,
|
|
535
|
+
model: partialMessage?.model ?? model.id,
|
|
536
|
+
responseModel: partialMessage?.responseModel,
|
|
537
|
+
responseId: partialMessage?.responseId,
|
|
538
|
+
diagnostics: partialMessage?.diagnostics,
|
|
539
|
+
usage: partialMessage?.usage ?? EMPTY_USAGE,
|
|
540
|
+
stopReason: reason,
|
|
541
|
+
errorMessage: errorMessage || (reason === "aborted" ? "Request was aborted" : "Error"),
|
|
542
|
+
timestamp: partialMessage?.timestamp ?? Date.now(),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function normalizeTerminalAssistantMessage(message, event) {
|
|
546
|
+
if (event.type === "done") {
|
|
547
|
+
return message;
|
|
548
|
+
}
|
|
549
|
+
const errorMessage = message.errorMessage ?? (event.reason === "aborted" ? "Request was aborted" : "Error");
|
|
550
|
+
if (message.stopReason === event.reason && message.errorMessage === errorMessage) {
|
|
551
|
+
return message;
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
...message,
|
|
555
|
+
stopReason: event.reason,
|
|
556
|
+
errorMessage,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
async function executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit) {
|
|
560
|
+
const finalizedCalls = [];
|
|
561
|
+
const messages = [];
|
|
562
|
+
for (const toolCall of toolCalls) {
|
|
563
|
+
await emit({
|
|
564
|
+
type: "tool_execution_start",
|
|
565
|
+
toolCallId: toolCall.id,
|
|
566
|
+
toolName: toolCall.name,
|
|
567
|
+
args: toolCall.arguments,
|
|
568
|
+
});
|
|
569
|
+
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
|
|
570
|
+
let finalized;
|
|
571
|
+
if (preparation.kind === "immediate") {
|
|
572
|
+
finalized = {
|
|
573
|
+
toolCall,
|
|
574
|
+
result: preparation.result,
|
|
575
|
+
isError: preparation.isError,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
const executed = await executePreparedToolCall(preparation, signal, emit);
|
|
580
|
+
finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
|
|
581
|
+
}
|
|
582
|
+
await emitToolExecutionEnd(finalized, emit);
|
|
583
|
+
const toolResultMessage = createToolResultMessage(finalized);
|
|
584
|
+
await emitToolResultMessage(toolResultMessage, emit);
|
|
585
|
+
finalizedCalls.push(finalized);
|
|
586
|
+
messages.push(toolResultMessage);
|
|
587
|
+
if (signal?.aborted) {
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
messages,
|
|
593
|
+
terminate: shouldTerminateToolBatch(finalizedCalls),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
async function executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit) {
|
|
597
|
+
const finalizedCalls = [];
|
|
598
|
+
let lastSequentialCall;
|
|
599
|
+
let currentParallelWave = [];
|
|
600
|
+
for (const toolCall of toolCalls) {
|
|
601
|
+
await emit({
|
|
602
|
+
type: "tool_execution_start",
|
|
603
|
+
toolCallId: toolCall.id,
|
|
604
|
+
toolName: toolCall.name,
|
|
605
|
+
args: toolCall.arguments,
|
|
606
|
+
});
|
|
607
|
+
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
|
|
608
|
+
const isSequential = isSequentialToolCall(currentContext, toolCall);
|
|
609
|
+
const dependencies = isSequential
|
|
610
|
+
? [...(lastSequentialCall ? [lastSequentialCall] : []), ...currentParallelWave]
|
|
611
|
+
: lastSequentialCall
|
|
612
|
+
? [lastSequentialCall]
|
|
613
|
+
: [];
|
|
614
|
+
const finalizedCall = (async () => {
|
|
615
|
+
await Promise.all(dependencies);
|
|
616
|
+
const finalized = await runPreparedToolCall(currentContext, assistantMessage, preparation, config, signal, emit);
|
|
617
|
+
await emitToolExecutionEnd(finalized, emit);
|
|
618
|
+
return finalized;
|
|
619
|
+
})();
|
|
620
|
+
finalizedCalls.push(finalizedCall);
|
|
621
|
+
if (isSequential) {
|
|
622
|
+
lastSequentialCall = finalizedCall;
|
|
623
|
+
currentParallelWave = [];
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
currentParallelWave.push(finalizedCall);
|
|
627
|
+
}
|
|
628
|
+
if (signal?.aborted) {
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const orderedFinalizedCalls = await Promise.all(finalizedCalls);
|
|
633
|
+
const messages = [];
|
|
634
|
+
for (const finalized of orderedFinalizedCalls) {
|
|
635
|
+
const toolResultMessage = createToolResultMessage(finalized);
|
|
636
|
+
await emitToolResultMessage(toolResultMessage, emit);
|
|
637
|
+
messages.push(toolResultMessage);
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
messages,
|
|
641
|
+
terminate: shouldTerminateToolBatch(orderedFinalizedCalls),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
async function runPreparedToolCall(currentContext, assistantMessage, preparation, config, signal, emit) {
|
|
645
|
+
if (preparation.kind === "immediate") {
|
|
646
|
+
return {
|
|
647
|
+
toolCall: preparation.toolCall,
|
|
648
|
+
result: preparation.result,
|
|
649
|
+
isError: preparation.isError,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
const executed = await executePreparedToolCall(preparation, signal, emit);
|
|
653
|
+
return finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
|
|
654
|
+
}
|
|
655
|
+
function isSequentialToolCall(currentContext, toolCall) {
|
|
656
|
+
return currentContext.tools?.find((tool) => tool.name === toolCall.name)?.executionMode === "sequential";
|
|
657
|
+
}
|
|
658
|
+
function shouldTerminateToolBatch(finalizedCalls) {
|
|
659
|
+
return finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);
|
|
660
|
+
}
|
|
661
|
+
export function prepareAgentToolCallArguments(tool, toolCall) {
|
|
662
|
+
if (!tool.prepareArguments) {
|
|
663
|
+
return toolCall;
|
|
664
|
+
}
|
|
665
|
+
const preparedArguments = tool.prepareArguments(toolCall.arguments);
|
|
666
|
+
if (preparedArguments === toolCall.arguments) {
|
|
667
|
+
return toolCall;
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
...toolCall,
|
|
671
|
+
arguments: preparedArguments,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
export function prepareAgentToolCall(tool, toolCall) {
|
|
675
|
+
const preparedToolCall = prepareAgentToolCallArguments(tool, toolCall);
|
|
676
|
+
return {
|
|
677
|
+
toolCall: preparedToolCall,
|
|
678
|
+
tool,
|
|
679
|
+
args: validateToolArguments(tool, preparedToolCall),
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
async function prepareToolCall(currentContext, assistantMessage, toolCall, config, signal) {
|
|
683
|
+
if (toolCall.incomplete === true) {
|
|
684
|
+
return {
|
|
685
|
+
kind: "immediate",
|
|
686
|
+
toolCall,
|
|
687
|
+
result: createErrorToolResult(createIncompleteToolCallErrorMessage(toolCall.name, toolCall.errorMessage)),
|
|
688
|
+
isError: true,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
const tool = currentContext.tools?.find((t) => t.name === toolCall.name);
|
|
692
|
+
if (!tool) {
|
|
693
|
+
return {
|
|
694
|
+
kind: "immediate",
|
|
695
|
+
toolCall,
|
|
696
|
+
result: createErrorToolResult(`Tool ${toolCall.name} not found`),
|
|
697
|
+
isError: true,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
const preparedToolCall = prepareAgentToolCall(tool, toolCall);
|
|
702
|
+
const validatedArgs = preparedToolCall.args;
|
|
703
|
+
if (config.beforeToolCall) {
|
|
704
|
+
const beforeResult = await config.beforeToolCall({
|
|
705
|
+
assistantMessage,
|
|
706
|
+
toolCall: preparedToolCall.toolCall,
|
|
707
|
+
args: validatedArgs,
|
|
708
|
+
context: currentContext,
|
|
709
|
+
}, signal);
|
|
710
|
+
if (signal?.aborted) {
|
|
711
|
+
return {
|
|
712
|
+
kind: "immediate",
|
|
713
|
+
toolCall,
|
|
714
|
+
result: createErrorToolResult("Operation aborted"),
|
|
715
|
+
isError: true,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
if (beforeResult?.block) {
|
|
719
|
+
return {
|
|
720
|
+
kind: "immediate",
|
|
721
|
+
toolCall,
|
|
722
|
+
result: createErrorToolResult(beforeResult.reason || "Tool execution was blocked"),
|
|
723
|
+
isError: true,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (signal?.aborted) {
|
|
728
|
+
return {
|
|
729
|
+
kind: "immediate",
|
|
730
|
+
toolCall,
|
|
731
|
+
result: createErrorToolResult("Operation aborted"),
|
|
732
|
+
isError: true,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
kind: "prepared",
|
|
737
|
+
toolCall: preparedToolCall.toolCall,
|
|
738
|
+
tool,
|
|
739
|
+
args: validatedArgs,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
return {
|
|
744
|
+
kind: "immediate",
|
|
745
|
+
toolCall,
|
|
746
|
+
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
|
747
|
+
isError: true,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async function executePreparedToolCall(prepared, signal, emit) {
|
|
752
|
+
const updateEvents = [];
|
|
753
|
+
let acceptingUpdates = true;
|
|
754
|
+
try {
|
|
755
|
+
const result = await prepared.tool.execute(prepared.toolCall.id, prepared.args, signal, (partialResult) => {
|
|
756
|
+
if (!acceptingUpdates)
|
|
757
|
+
return;
|
|
758
|
+
updateEvents.push(Promise.resolve(emit({
|
|
759
|
+
type: "tool_execution_update",
|
|
760
|
+
toolCallId: prepared.toolCall.id,
|
|
761
|
+
toolName: prepared.toolCall.name,
|
|
762
|
+
args: prepared.toolCall.arguments,
|
|
763
|
+
partialResult,
|
|
764
|
+
})));
|
|
765
|
+
});
|
|
766
|
+
acceptingUpdates = false;
|
|
767
|
+
await Promise.all(updateEvents);
|
|
768
|
+
return { result, isError: false };
|
|
769
|
+
}
|
|
770
|
+
catch (error) {
|
|
771
|
+
acceptingUpdates = false;
|
|
772
|
+
await Promise.all(updateEvents);
|
|
773
|
+
return {
|
|
774
|
+
result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
|
|
775
|
+
isError: true,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
finally {
|
|
779
|
+
acceptingUpdates = false;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, signal) {
|
|
783
|
+
let result = executed.result;
|
|
784
|
+
let isError = executed.isError;
|
|
785
|
+
if (config.afterToolCall) {
|
|
786
|
+
try {
|
|
787
|
+
const afterResult = await config.afterToolCall({
|
|
788
|
+
assistantMessage,
|
|
789
|
+
toolCall: prepared.toolCall,
|
|
790
|
+
args: prepared.args,
|
|
791
|
+
result,
|
|
792
|
+
isError,
|
|
793
|
+
context: currentContext,
|
|
794
|
+
}, signal);
|
|
795
|
+
if (afterResult) {
|
|
796
|
+
result = {
|
|
797
|
+
...result,
|
|
798
|
+
content: afterResult.content ?? result.content,
|
|
799
|
+
details: afterResult.details ?? result.details,
|
|
800
|
+
usage: afterResult.usage ?? result.usage,
|
|
801
|
+
terminate: afterResult.terminate ?? result.terminate,
|
|
802
|
+
};
|
|
803
|
+
isError = afterResult.isError ?? isError;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
result = createErrorToolResult(error instanceof Error ? error.message : String(error));
|
|
808
|
+
isError = true;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
toolCall: prepared.toolCall,
|
|
813
|
+
result,
|
|
814
|
+
isError,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
function createErrorToolResult(message) {
|
|
818
|
+
return {
|
|
819
|
+
content: [{ type: "text", text: message }],
|
|
820
|
+
details: {},
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
async function emitToolExecutionEnd(finalized, emit) {
|
|
824
|
+
await emit({
|
|
825
|
+
type: "tool_execution_end",
|
|
826
|
+
toolCallId: finalized.toolCall.id,
|
|
827
|
+
toolName: finalized.toolCall.name,
|
|
828
|
+
result: finalized.result,
|
|
829
|
+
isError: finalized.isError,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
function createToolResultMessage(finalized) {
|
|
833
|
+
return {
|
|
834
|
+
role: "toolResult",
|
|
835
|
+
toolCallId: finalized.toolCall.id,
|
|
836
|
+
toolName: finalized.toolCall.name,
|
|
837
|
+
// Untyped tools (JS extensions) can return results without content; normalize
|
|
838
|
+
// so the null never enters session history or provider payloads.
|
|
839
|
+
content: finalized.result.content ?? [],
|
|
840
|
+
details: finalized.result.details,
|
|
841
|
+
usage: finalized.result.usage,
|
|
842
|
+
...(finalized.result.addedToolNames?.length ? { addedToolNames: finalized.result.addedToolNames } : {}),
|
|
843
|
+
isError: finalized.isError,
|
|
844
|
+
timestamp: Date.now(),
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
async function emitToolResultMessage(toolResultMessage, emit) {
|
|
848
|
+
await emit({ type: "message_start", message: toolResultMessage });
|
|
849
|
+
await emit({ type: "message_end", message: toolResultMessage });
|
|
850
|
+
}
|
|
851
|
+
//# sourceMappingURL=agent-loop.js.map
|