mini-coder 0.5.14 → 0.6.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/README.md +25 -109
- package/bin/mc.ts +8 -11
- package/bun.lock +79 -269
- package/package.json +17 -22
- package/src/agent.ts +237 -1403
- package/src/args.ts +289 -0
- package/src/headless.ts +43 -358
- package/src/index.ts +29 -1016
- package/src/oauth.ts +117 -0
- package/src/prompt.ts +227 -284
- package/src/session.ts +55 -1306
- package/src/shared.ts +117 -38
- package/src/tool-bash.ts +110 -0
- package/src/tool-edit.ts +133 -0
- package/src/tool-task.ts +114 -0
- package/src/tui-components.ts +150 -0
- package/src/tui-conversation.ts +262 -0
- package/src/tui-editor.ts +29 -0
- package/src/tui-overlay.ts +403 -0
- package/src/tui.ts +236 -0
- package/src/types.ts +160 -0
- package/tsconfig.json +17 -0
- package/BENCHMARK.md +0 -107
- package/LICENSE +0 -9
- package/PROGRESS.md +0 -5
- package/assets/icon-1-minimal.svg +0 -31
- package/assets/icon-2-dark-terminal.svg +0 -48
- package/assets/icon-3-gradient-modern.svg +0 -45
- package/assets/icon-4-filled-bold.svg +0 -54
- package/assets/icon-5-community-badge.svg +0 -63
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/assets/preview-0-5-0.png +0 -0
- package/assets/preview.gif +0 -0
- package/benchmark-baseline.sh +0 -15
- package/benchmark-loop.sh +0 -19
- package/skills-lock.json +0 -15
- package/src/assistant-output.ts +0 -73
- package/src/cli.ts +0 -134
- package/src/delegation.ts +0 -238
- package/src/errors.ts +0 -15
- package/src/git.ts +0 -247
- package/src/input.ts +0 -168
- package/src/mcp.ts +0 -609
- package/src/paths.ts +0 -37
- package/src/session-message.ts +0 -385
- package/src/settings.ts +0 -449
- package/src/skills.ts +0 -271
- package/src/submit.ts +0 -376
- package/src/text.ts +0 -71
- package/src/theme.ts +0 -330
- package/src/tool-common.ts +0 -93
- package/src/tool-delegate.ts +0 -125
- package/src/tool-grep.ts +0 -606
- package/src/tool-read.ts +0 -313
- package/src/tool-shell.ts +0 -1051
- package/src/tools.ts +0 -1179
- package/src/ui/agent.ts +0 -320
- package/src/ui/commands.test.ts +0 -957
- package/src/ui/commands.ts +0 -848
- package/src/ui/conversation.test.ts +0 -585
- package/src/ui/conversation.ts +0 -1836
- package/src/ui/help.ts +0 -158
- package/src/ui/input.test.ts +0 -64
- package/src/ui/input.ts +0 -138
- package/src/ui/overlay.ts +0 -59
- package/src/ui/runtime.ts +0 -69
- package/src/ui/status.ts +0 -220
- package/src/ui.ts +0 -1190
- package/src/version.ts +0 -48
package/src/agent.ts
CHANGED
|
@@ -1,1438 +1,272 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core agent loop.
|
|
3
|
-
*
|
|
4
|
-
* Streams LLM responses, executes tool calls, appends messages to the
|
|
5
|
-
* session history, and loops until the model stops or the user interrupts.
|
|
6
|
-
* Uses pi-ai's {@link streamSimple} for model communication.
|
|
7
|
-
*
|
|
8
|
-
* @module
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { Database } from "bun:sqlite";
|
|
12
|
-
import type {
|
|
13
|
-
AssistantMessage,
|
|
14
|
-
AssistantMessageEvent,
|
|
15
|
-
Message,
|
|
16
|
-
Model,
|
|
17
|
-
ThinkingLevel,
|
|
18
|
-
Tool,
|
|
19
|
-
ToolCall,
|
|
20
|
-
ToolResultMessage,
|
|
21
|
-
UserMessage,
|
|
22
|
-
} from "@mariozechner/pi-ai";
|
|
23
|
-
import { streamSimple } from "@mariozechner/pi-ai";
|
|
24
1
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from "
|
|
33
|
-
import {
|
|
34
|
-
import
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* A tool execution handler.
|
|
57
|
-
*
|
|
58
|
-
* Called by the agent loop when the model invokes a tool. Arguments are
|
|
59
|
-
* the JSON object parsed by pi-ai from the model's tool call.
|
|
60
|
-
*/
|
|
61
|
-
/** Callback used by tool handlers to report progressive output updates. */
|
|
62
|
-
export type ToolUpdateCallback = (result: ToolExecResult) => void;
|
|
63
|
-
|
|
64
|
-
export type ToolHandler = (
|
|
65
|
-
args: Record<string, unknown>,
|
|
66
|
-
cwd: string,
|
|
67
|
-
signal?: AbortSignal,
|
|
68
|
-
onUpdate?: ToolUpdateCallback,
|
|
69
|
-
) => Promise<ToolExecResult> | ToolExecResult;
|
|
70
|
-
|
|
71
|
-
export type { ToolExecResult };
|
|
72
|
-
|
|
73
|
-
/** Events emitted during the agent loop for UI updates. */
|
|
74
|
-
export type AgentEvent =
|
|
75
|
-
| {
|
|
76
|
-
type: "text_delta";
|
|
77
|
-
delta: string;
|
|
78
|
-
content: AssistantMessage["content"];
|
|
79
|
-
}
|
|
80
|
-
| {
|
|
81
|
-
type: "thinking_delta";
|
|
82
|
-
delta: string;
|
|
83
|
-
content: AssistantMessage["content"];
|
|
84
|
-
}
|
|
85
|
-
| {
|
|
86
|
-
type: "toolcall_start";
|
|
87
|
-
toolCallId: string;
|
|
88
|
-
name: string;
|
|
89
|
-
args: Record<string, unknown>;
|
|
90
|
-
content: AssistantMessage["content"];
|
|
91
|
-
}
|
|
92
|
-
| {
|
|
93
|
-
type: "toolcall_delta";
|
|
94
|
-
toolCallId: string;
|
|
95
|
-
name: string;
|
|
96
|
-
args: Record<string, unknown>;
|
|
97
|
-
delta: string;
|
|
98
|
-
content: AssistantMessage["content"];
|
|
99
|
-
}
|
|
100
|
-
| {
|
|
101
|
-
type: "toolcall_end";
|
|
102
|
-
toolCallId: string;
|
|
103
|
-
name: string;
|
|
104
|
-
args: Record<string, unknown>;
|
|
105
|
-
content: AssistantMessage["content"];
|
|
106
|
-
}
|
|
107
|
-
| { type: "user_message"; message: UserMessage }
|
|
108
|
-
| { type: "assistant_message"; message: AssistantMessage }
|
|
109
|
-
| {
|
|
110
|
-
type: "tool_start";
|
|
111
|
-
toolCallId: string;
|
|
112
|
-
name: string;
|
|
113
|
-
args: Record<string, unknown>;
|
|
114
|
-
}
|
|
115
|
-
| {
|
|
116
|
-
type: "tool_delta";
|
|
117
|
-
toolCallId: string;
|
|
118
|
-
name: string;
|
|
119
|
-
result: ToolExecResult;
|
|
120
|
-
}
|
|
121
|
-
| {
|
|
122
|
-
type: "tool_end";
|
|
123
|
-
toolCallId: string;
|
|
124
|
-
name: string;
|
|
125
|
-
result: ToolExecResult;
|
|
126
|
-
}
|
|
127
|
-
| { type: "tool_result"; message: ToolResultMessage }
|
|
128
|
-
| {
|
|
129
|
-
type: "context_compacted";
|
|
130
|
-
contextTokens: number;
|
|
131
|
-
stats: ReturnType<typeof computeSessionStats>;
|
|
132
|
-
}
|
|
133
|
-
| { type: "done"; message: AssistantMessage }
|
|
134
|
-
| { type: "error"; message: AssistantMessage }
|
|
135
|
-
| { type: "aborted"; message: AssistantMessage };
|
|
136
|
-
|
|
137
|
-
/** Options for the agent loop. */
|
|
138
|
-
interface RunAgentOpts {
|
|
139
|
-
/** Open database handle. */
|
|
140
|
-
db: Database;
|
|
141
|
-
/** Current session ID. */
|
|
142
|
-
sessionId: string;
|
|
143
|
-
/** Turn number for this agent loop (all messages share this turn). */
|
|
144
|
-
turn: number;
|
|
145
|
-
/** The model to stream with. */
|
|
146
|
-
model: Model<string>;
|
|
147
|
-
/** The assembled system prompt. */
|
|
148
|
-
systemPrompt: string;
|
|
149
|
-
/** Tool definitions sent to the model. */
|
|
150
|
-
tools: Tool[];
|
|
151
|
-
/** Tool name → handler map for executing tool calls. */
|
|
152
|
-
toolHandlers: Map<string, ToolHandler>;
|
|
153
|
-
/** Current message history (mutated in-place as messages are appended). */
|
|
154
|
-
messages: Message[];
|
|
155
|
-
/** Working directory for tool execution. */
|
|
156
|
-
cwd: string;
|
|
157
|
-
/** API key for the provider. */
|
|
158
|
-
apiKey?: string;
|
|
159
|
-
/** Reasoning effort level (e.g. "low", "medium", "high", "xhigh"). */
|
|
160
|
-
effort?: ThinkingLevel;
|
|
161
|
-
/** Abort signal for interruption. */
|
|
162
|
-
signal?: AbortSignal;
|
|
163
|
-
/** Callback for UI events. */
|
|
164
|
-
onEvent?: (event: AgentEvent) => void;
|
|
165
|
-
/** Dequeue the next queued steering message, if one is waiting. */
|
|
166
|
-
takeQueuedUserMessage?: () => UserMessage | null;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Result of the agent loop. */
|
|
170
|
-
interface AgentLoopResult {
|
|
171
|
-
/** The updated message history. */
|
|
172
|
-
messages: Message[];
|
|
173
|
-
/** How the loop ended. */
|
|
174
|
-
stopReason: "stop" | "length" | "error" | "aborted";
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function cloneAssistantContent(
|
|
178
|
-
content: AssistantMessage["content"],
|
|
179
|
-
): AssistantMessage["content"] {
|
|
180
|
-
return content.map((block) => {
|
|
181
|
-
if (block.type === "toolCall") {
|
|
182
|
-
return {
|
|
183
|
-
...block,
|
|
184
|
-
arguments: structuredClone(block.arguments),
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
return { ...block };
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
interface MergedAssistantBlock {
|
|
192
|
-
block: AssistantMessage["content"][number];
|
|
193
|
-
partialStep: number;
|
|
194
|
-
finalStep: number;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function mergeAssistantBlocks(
|
|
198
|
-
partialBlock: AssistantMessage["content"][number] | undefined,
|
|
199
|
-
finalBlock: AssistantMessage["content"][number] | undefined,
|
|
200
|
-
): MergedAssistantBlock | null {
|
|
201
|
-
if (!partialBlock && !finalBlock) {
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
if (!partialBlock && finalBlock) {
|
|
205
|
-
return { block: finalBlock, partialStep: 0, finalStep: 1 };
|
|
206
|
-
}
|
|
207
|
-
if (partialBlock && !finalBlock) {
|
|
208
|
-
return { block: partialBlock, partialStep: 1, finalStep: 0 };
|
|
209
|
-
}
|
|
210
|
-
if (!partialBlock || !finalBlock) {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
if (partialBlock.type === finalBlock.type) {
|
|
214
|
-
const shouldPreservePartialThinking =
|
|
215
|
-
partialBlock.type === "thinking" &&
|
|
216
|
-
finalBlock.type === "thinking" &&
|
|
217
|
-
partialBlock.thinking &&
|
|
218
|
-
!finalBlock.thinking;
|
|
219
|
-
return {
|
|
220
|
-
block: shouldPreservePartialThinking ? partialBlock : finalBlock,
|
|
221
|
-
partialStep: 1,
|
|
222
|
-
finalStep: 1,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
if (partialBlock.type === "thinking") {
|
|
226
|
-
return { block: partialBlock, partialStep: 1, finalStep: 0 };
|
|
227
|
-
}
|
|
228
|
-
return { block: finalBlock, partialStep: 1, finalStep: 1 };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** Merge streamed partial assistant content into the final message content. */
|
|
232
|
-
function mergeAssistantContent(
|
|
233
|
-
partialContent: AssistantMessage["content"],
|
|
234
|
-
finalContent: AssistantMessage["content"],
|
|
235
|
-
): AssistantMessage["content"] {
|
|
236
|
-
if (partialContent.length === 0) return finalContent;
|
|
237
|
-
if (finalContent.length === 0) return partialContent;
|
|
238
|
-
|
|
239
|
-
const merged: AssistantMessage["content"] = [];
|
|
240
|
-
let partialIndex = 0;
|
|
241
|
-
let finalIndex = 0;
|
|
242
|
-
|
|
243
|
-
while (
|
|
244
|
-
partialIndex < partialContent.length ||
|
|
245
|
-
finalIndex < finalContent.length
|
|
246
|
-
) {
|
|
247
|
-
const nextBlock = mergeAssistantBlocks(
|
|
248
|
-
partialContent[partialIndex],
|
|
249
|
-
finalContent[finalIndex],
|
|
250
|
-
);
|
|
251
|
-
if (!nextBlock) {
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
merged.push(nextBlock.block);
|
|
255
|
-
partialIndex += nextBlock.partialStep;
|
|
256
|
-
finalIndex += nextBlock.finalStep;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return merged;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** Merge a final assistant message with the richest streamed partial content seen. */
|
|
263
|
-
function mergeAssistantMessage(
|
|
264
|
-
partialMessage: AssistantMessage,
|
|
265
|
-
finalMessage: AssistantMessage,
|
|
266
|
-
): AssistantMessage {
|
|
267
|
-
return {
|
|
268
|
-
...finalMessage,
|
|
269
|
-
content: mergeAssistantContent(
|
|
270
|
-
partialMessage.content,
|
|
271
|
-
finalMessage.content,
|
|
272
|
-
),
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function toolErrorResult(name: string, error: unknown): ToolExecResult {
|
|
277
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
278
|
-
|
|
279
|
-
return {
|
|
280
|
-
content: [{ type: "text", text: `Tool ${name} failed: ${message}` }],
|
|
281
|
-
isError: true,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function unknownToolResult(name: string): ToolExecResult {
|
|
286
|
-
return {
|
|
287
|
-
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
288
|
-
isError: true,
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const MAX_CONTEXT_IMAGE_BLOCKS = 12;
|
|
293
|
-
const OMITTED_CONTEXT_IMAGE_PLACEHOLDER =
|
|
294
|
-
"[Earlier image omitted from model context to stay within image-input limits.]";
|
|
295
|
-
const UNSUPPORTED_CONTEXT_IMAGE_PLACEHOLDER =
|
|
296
|
-
"[Image omitted from model context because the active model does not support image input.]";
|
|
297
|
-
|
|
298
|
-
function sanitizeMessageContentForContext(
|
|
299
|
-
message: Message,
|
|
300
|
-
imageBudget: { remaining: number; supportsImages: boolean },
|
|
301
|
-
): Message {
|
|
302
|
-
if (message.role === "assistant" || !Array.isArray(message.content)) {
|
|
303
|
-
return message;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
let changed = false;
|
|
307
|
-
const sanitized: typeof message.content = [];
|
|
308
|
-
|
|
309
|
-
for (const block of message.content) {
|
|
310
|
-
if (block.type !== "image") {
|
|
311
|
-
sanitized.push(block);
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const placeholder = imageBudget.supportsImages
|
|
316
|
-
? OMITTED_CONTEXT_IMAGE_PLACEHOLDER
|
|
317
|
-
: UNSUPPORTED_CONTEXT_IMAGE_PLACEHOLDER;
|
|
318
|
-
|
|
319
|
-
if (imageBudget.supportsImages && imageBudget.remaining > 0) {
|
|
320
|
-
imageBudget.remaining -= 1;
|
|
321
|
-
sanitized.push(block);
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
changed = true;
|
|
326
|
-
const lastBlock = sanitized.at(-1);
|
|
327
|
-
if (lastBlock?.type === "text" && lastBlock.text === placeholder) {
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
sanitized.push({ type: "text", text: placeholder });
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (!changed) {
|
|
335
|
-
return message;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return { ...message, content: sanitized };
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function sanitizeContextMessages(
|
|
342
|
-
messages: Message[],
|
|
343
|
-
modelSupportsImages: boolean,
|
|
344
|
-
): { messages: Message[]; changed: boolean } {
|
|
345
|
-
const imageBudget = {
|
|
346
|
-
remaining: modelSupportsImages ? MAX_CONTEXT_IMAGE_BLOCKS : 0,
|
|
347
|
-
supportsImages: modelSupportsImages,
|
|
348
|
-
};
|
|
349
|
-
const sanitized = new Array<Message>(messages.length);
|
|
350
|
-
let changed = false;
|
|
351
|
-
|
|
352
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
353
|
-
const message = sanitizeMessageContentForContext(
|
|
354
|
-
messages[index]!,
|
|
355
|
-
imageBudget,
|
|
356
|
-
);
|
|
357
|
-
if (message !== messages[index]) {
|
|
358
|
-
changed = true;
|
|
359
|
-
}
|
|
360
|
-
sanitized[index] = message;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return { messages: sanitized, changed };
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function computeRequestContextTokens(
|
|
367
|
-
messages: Message[],
|
|
368
|
-
modelSupportsImages: boolean,
|
|
369
|
-
): number {
|
|
370
|
-
const sanitized = sanitizeContextMessages(messages, modelSupportsImages);
|
|
371
|
-
return computeContextTokens(sanitized.messages, {
|
|
372
|
-
ignoreAssistantUsage: sanitized.changed,
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function buildAgentContext(
|
|
377
|
-
systemPrompt: string,
|
|
378
|
-
messages: Message[],
|
|
379
|
-
tools: Tool[],
|
|
380
|
-
modelSupportsImages: boolean,
|
|
381
|
-
) {
|
|
382
|
-
const contextMessages = sanitizeContextMessages(
|
|
383
|
-
messages,
|
|
384
|
-
modelSupportsImages,
|
|
385
|
-
).messages;
|
|
386
|
-
return tools.length > 0
|
|
387
|
-
? { systemPrompt, messages: contextMessages, tools }
|
|
388
|
-
: { systemPrompt, messages: contextMessages };
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function buildStreamOptions(
|
|
392
|
-
apiKey: string | undefined,
|
|
393
|
-
effort: ThinkingLevel | undefined,
|
|
394
|
-
signal: AbortSignal | undefined,
|
|
395
|
-
) {
|
|
396
|
-
return {
|
|
397
|
-
...(apiKey ? { apiKey } : {}),
|
|
398
|
-
...(effort ? { reasoning: effort } : {}),
|
|
399
|
-
...(signal ? { signal } : {}),
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function stripSyntheticSystemMessageBlock(text: string): string {
|
|
404
|
-
return text.replace(/\n?<system-message>[\s\S]*?<\/system-message>/g, "");
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function describeUserContent(content: UserMessage["content"]): string | null {
|
|
408
|
-
if (typeof content === "string") {
|
|
409
|
-
return collapseWhitespaceToNull(stripSyntheticSystemMessageBlock(content));
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const text = collapseWhitespaceToNull(joinTextBlocks(content));
|
|
413
|
-
const imageCount = content.filter((block) => block.type === "image").length;
|
|
414
|
-
const parts = [
|
|
415
|
-
text,
|
|
416
|
-
imageCount > 0
|
|
417
|
-
? `[${imageCount} image${imageCount === 1 ? "" : "s"} attached]`
|
|
418
|
-
: null,
|
|
419
|
-
].filter(
|
|
420
|
-
(part): part is string => typeof part === "string" && part.length > 0,
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
return parts.length > 0 ? parts.join("\n") : null;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function resolveCurrentUserRequest(userMessage: UserMessage): string {
|
|
427
|
-
return (
|
|
428
|
-
describeUserContent(userMessage.content) ?? DEFAULT_CURRENT_USER_REQUEST
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function isSystemReminderMessage(message: Message): boolean {
|
|
433
|
-
return (
|
|
434
|
-
message.role === "user" &&
|
|
435
|
-
typeof message.content === "string" &&
|
|
436
|
-
message.content.startsWith("<system_reminder>")
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function isCompactionSummaryMessage(message: Message): boolean {
|
|
441
|
-
return (
|
|
442
|
-
message.role === "user" &&
|
|
443
|
-
typeof message.content === "string" &&
|
|
444
|
-
message.content.includes("<system-message>") &&
|
|
445
|
-
message.content.includes(COMPACTION_RECOVERY_PREFIX)
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function extractCurrentUserRequest(messages: readonly Message[]): string {
|
|
450
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
451
|
-
const message = messages[index];
|
|
2
|
+
type AssistantMessage,
|
|
3
|
+
type Context,
|
|
4
|
+
type Message,
|
|
5
|
+
streamSimple,
|
|
6
|
+
type TextContent,
|
|
7
|
+
type ToolCall,
|
|
8
|
+
type ToolResultMessage,
|
|
9
|
+
} from "@mariozechner/pi-ai";
|
|
10
|
+
import { getApiKey } from "./oauth";
|
|
11
|
+
import type {
|
|
12
|
+
AgentContex,
|
|
13
|
+
AgentEvent,
|
|
14
|
+
AgentToolEvent,
|
|
15
|
+
ToolAndRunner,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
// ### JetBrains Junie: Observation Masking
|
|
19
|
+
// Published research found that **simply hiding old tool outputs** matched the quality of full LLM summarization with **zero extra compute**:
|
|
20
|
+
// https://blog.jetbrains.com/research/2025/12/efficient-context-management/
|
|
21
|
+
export function compactContext(messages: Message[]) {
|
|
22
|
+
// TODO: Preserve SKILL.md contents, and exclude them from compaction.
|
|
23
|
+
// Problem: How do we know? we need to check each message result againt it's arguments
|
|
24
|
+
// and then find if by chance it has a read skill command... This is a mess. We we.
|
|
25
|
+
// A better way would be to first add a read(path, lines, offset). And then we know
|
|
26
|
+
// which files are read using it, and can match by path if it's a SKILL.md ending.
|
|
27
|
+
const KEEP_OBSERVATIONS = 10;
|
|
28
|
+
|
|
29
|
+
const toolResultIndices: number[] = [];
|
|
30
|
+
for (let i = 0; i < messages.length; i++) {
|
|
31
|
+
if (messages[i].role !== "toolResult") continue;
|
|
32
|
+
const content = (messages[i] as ToolResultMessage).content;
|
|
452
33
|
if (
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
isCompactionSummaryMessage(message)
|
|
34
|
+
content.length === 1 &&
|
|
35
|
+
content[0].type === "text" &&
|
|
36
|
+
content[0].text.startsWith("Old environment output:")
|
|
457
37
|
) {
|
|
458
38
|
continue;
|
|
459
39
|
}
|
|
460
|
-
|
|
461
|
-
const content = describeUserContent(message.content);
|
|
462
|
-
if (content) {
|
|
463
|
-
return content;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return DEFAULT_CURRENT_USER_REQUEST;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function createCompactionInstructionMessage(
|
|
471
|
-
currentUserRequest: string,
|
|
472
|
-
): UserMessage {
|
|
473
|
-
return {
|
|
474
|
-
role: "user",
|
|
475
|
-
content: [
|
|
476
|
-
"Current user request:",
|
|
477
|
-
currentUserRequest,
|
|
478
|
-
"",
|
|
479
|
-
"Summarize the earlier conversation messages above so the coding agent can continue the current request after those messages are removed from active context.",
|
|
480
|
-
].join("\n"),
|
|
481
|
-
timestamp: Date.now(),
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function extractAssistantTextContent(message: AssistantMessage): string | null {
|
|
486
|
-
const text = message.content
|
|
487
|
-
.flatMap((block) => {
|
|
488
|
-
return block.type === "text" ? [block.text] : [];
|
|
489
|
-
})
|
|
490
|
-
.join("\n")
|
|
491
|
-
.trim();
|
|
492
|
-
return text.length > 0 ? text : null;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function buildCompactionSummaryMessage(
|
|
496
|
-
summaryText: string,
|
|
497
|
-
dbPath: string,
|
|
498
|
-
sessionId: string,
|
|
499
|
-
): UserMessage {
|
|
500
|
-
return {
|
|
501
|
-
role: "user",
|
|
502
|
-
content: [
|
|
503
|
-
summaryText,
|
|
504
|
-
"",
|
|
505
|
-
"<system-message>",
|
|
506
|
-
`${COMPACTION_RECOVERY_PREFIX} If you need the exact originals again, read them from the SQLite database at ${dbPath} for session ${sessionId}.`,
|
|
507
|
-
"</system-message>",
|
|
508
|
-
].join("\n"),
|
|
509
|
-
timestamp: Date.now(),
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function computeCompactionMessageCount(messageCount: number): number {
|
|
514
|
-
if (messageCount <= 1) {
|
|
515
|
-
return 0;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
return Math.max(
|
|
519
|
-
1,
|
|
520
|
-
Math.min(
|
|
521
|
-
messageCount - 1,
|
|
522
|
-
Math.floor(messageCount * CONTEXT_COMPACTION_FRACTION),
|
|
523
|
-
),
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function advanceCompleteContextSpan(
|
|
528
|
-
messages: readonly Message[],
|
|
529
|
-
startIndex: number,
|
|
530
|
-
): number | null {
|
|
531
|
-
const message = messages[startIndex];
|
|
532
|
-
if (!message) {
|
|
533
|
-
return null;
|
|
534
|
-
}
|
|
535
|
-
if (message.role === "toolResult") {
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
|
-
if (message.role !== "assistant") {
|
|
539
|
-
return startIndex + 1;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const toolCalls = getAssistantToolCalls(message);
|
|
543
|
-
if (toolCalls.length === 0) {
|
|
544
|
-
return startIndex + 1;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
let cursor = startIndex + 1;
|
|
548
|
-
const pendingToolCallIds = new Set(toolCalls.map((toolCall) => toolCall.id));
|
|
549
|
-
while (pendingToolCallIds.size > 0) {
|
|
550
|
-
const nextMessage = messages[cursor];
|
|
551
|
-
if (!nextMessage || nextMessage.role !== "toolResult") {
|
|
552
|
-
return null;
|
|
553
|
-
}
|
|
554
|
-
if (!pendingToolCallIds.delete(nextMessage.toolCallId)) {
|
|
555
|
-
return null;
|
|
556
|
-
}
|
|
557
|
-
cursor += 1;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
return cursor;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function collectCompactionBoundaryCounts(
|
|
564
|
-
messages: readonly Message[],
|
|
565
|
-
): number[] {
|
|
566
|
-
const boundaries: number[] = [];
|
|
567
|
-
let index = 0;
|
|
568
|
-
|
|
569
|
-
while (index < messages.length) {
|
|
570
|
-
const nextIndex = advanceCompleteContextSpan(messages, index);
|
|
571
|
-
if (nextIndex === null) {
|
|
572
|
-
return [];
|
|
573
|
-
}
|
|
574
|
-
index = nextIndex;
|
|
575
|
-
if (index < messages.length) {
|
|
576
|
-
boundaries.push(index);
|
|
577
|
-
}
|
|
40
|
+
toolResultIndices.push(i);
|
|
578
41
|
}
|
|
579
42
|
|
|
580
|
-
return
|
|
581
|
-
}
|
|
43
|
+
if (toolResultIndices.length <= KEEP_OBSERVATIONS) return;
|
|
582
44
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
let nearestLowerBoundary = 0;
|
|
597
|
-
for (const boundaryCount of safeBoundaryCounts) {
|
|
598
|
-
if (boundaryCount === targetCount) {
|
|
599
|
-
return boundaryCount;
|
|
45
|
+
for (let i = 0; i < toolResultIndices.length - KEEP_OBSERVATIONS; i++) {
|
|
46
|
+
const idx = toolResultIndices[i];
|
|
47
|
+
const msg = messages[idx] as ToolResultMessage;
|
|
48
|
+
let lines = 0;
|
|
49
|
+
let images = 0;
|
|
50
|
+
for (const c of msg.content) {
|
|
51
|
+
if (c.type === "text") {
|
|
52
|
+
lines += c.text.split(/\r?\n/).length;
|
|
53
|
+
} else if (c.type === "image") {
|
|
54
|
+
images++;
|
|
55
|
+
}
|
|
600
56
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
57
|
+
let text = `Old environment output: (${lines} lines omitted)`;
|
|
58
|
+
if (images > 0) {
|
|
59
|
+
text += ` (${images} image${images > 1 ? "s" : ""} omitted)`;
|
|
604
60
|
}
|
|
605
|
-
|
|
61
|
+
msg.content = [{ type: "text", text }];
|
|
606
62
|
}
|
|
607
|
-
|
|
608
|
-
return nearestLowerBoundary;
|
|
609
63
|
}
|
|
610
64
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
async function summarizeCompactionChunk(
|
|
619
|
-
messages: Message[],
|
|
620
|
-
currentUserRequest: string,
|
|
621
|
-
opts: Pick<RunAgentOpts, "model" | "apiKey" | "effort" | "signal">,
|
|
622
|
-
): Promise<CompactionSummaryResult | null> {
|
|
623
|
-
const summaryMessage = await streamAssistantMessage({
|
|
624
|
-
model: opts.model,
|
|
625
|
-
systemPrompt: COMPACTION_SUMMARY_SYSTEM_PROMPT,
|
|
626
|
-
tools: [],
|
|
627
|
-
messages: [
|
|
628
|
-
...messages,
|
|
629
|
-
createCompactionInstructionMessage(currentUserRequest),
|
|
630
|
-
],
|
|
631
|
-
apiKey: opts.apiKey,
|
|
632
|
-
effort: opts.effort,
|
|
633
|
-
signal: opts.signal,
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
if (
|
|
637
|
-
summaryMessage.stopReason === "error" ||
|
|
638
|
-
summaryMessage.stopReason === "aborted"
|
|
639
|
-
) {
|
|
640
|
-
return null;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const text = extractAssistantTextContent(summaryMessage);
|
|
644
|
-
if (!text) {
|
|
645
|
-
return null;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
return {
|
|
649
|
-
text,
|
|
650
|
-
usage: summaryMessage.usage,
|
|
65
|
+
export async function* streamAgent(
|
|
66
|
+
agentCtx: AgentContex,
|
|
67
|
+
): AsyncGenerator<AgentEvent> {
|
|
68
|
+
const llmCtx: Context = {
|
|
69
|
+
systemPrompt: agentCtx.systemPrompt,
|
|
70
|
+
tools: agentCtx.tools.map((t) => t.tool),
|
|
71
|
+
messages: agentCtx.messages,
|
|
651
72
|
};
|
|
652
|
-
}
|
|
653
73
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
pendingContextMessages: readonly Message[],
|
|
657
|
-
currentUserRequest: string,
|
|
658
|
-
opts: Pick<
|
|
659
|
-
RunAgentOpts,
|
|
660
|
-
"db" | "sessionId" | "model" | "apiKey" | "effort" | "signal" | "onEvent"
|
|
661
|
-
>,
|
|
662
|
-
): Promise<Message[]> {
|
|
663
|
-
let compactedMessages = messages;
|
|
74
|
+
// Important for refreshing tokens.
|
|
75
|
+
const apiKey = await getApiKey(agentCtx.options);
|
|
664
76
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
contextTokens / opts.model.contextWindow <
|
|
672
|
-
CONTEXT_COMPACTION_THRESHOLD
|
|
673
|
-
) {
|
|
674
|
-
return compactedMessages;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
const candidateRows = loadUncompactedModelMessages(opts.db, opts.sessionId);
|
|
678
|
-
const compactCount = computeSafeCompactionMessageCount(
|
|
679
|
-
candidateRows.map((row) => row.message),
|
|
680
|
-
);
|
|
681
|
-
if (compactCount === 0) {
|
|
682
|
-
return compactedMessages;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const summaryResult = await summarizeCompactionChunk(
|
|
686
|
-
candidateRows.slice(0, compactCount).map((row) => row.message),
|
|
687
|
-
currentUserRequest,
|
|
688
|
-
opts,
|
|
689
|
-
);
|
|
690
|
-
if (!summaryResult) {
|
|
691
|
-
return compactedMessages;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
appendSessionCompaction(
|
|
695
|
-
opts.db,
|
|
696
|
-
opts.sessionId,
|
|
697
|
-
candidateRows[compactCount - 1]!.id,
|
|
698
|
-
buildCompactionSummaryMessage(
|
|
699
|
-
summaryResult.text,
|
|
700
|
-
opts.db.filename,
|
|
701
|
-
opts.sessionId,
|
|
702
|
-
),
|
|
703
|
-
summaryResult.usage,
|
|
704
|
-
);
|
|
705
|
-
|
|
706
|
-
compactedMessages = loadCompactedModelMessages(opts.db, opts.sessionId);
|
|
707
|
-
opts.onEvent?.({
|
|
708
|
-
type: "context_compacted",
|
|
709
|
-
contextTokens: computeRequestContextTokens(
|
|
710
|
-
[...compactedMessages, ...pendingContextMessages],
|
|
711
|
-
opts.model.input.includes("image"),
|
|
712
|
-
),
|
|
713
|
-
stats: computeSessionStats(opts.db, opts.sessionId),
|
|
77
|
+
// Main agent loop, continues until llm sends a response other than toolCall or has no tool calls.
|
|
78
|
+
while (true) {
|
|
79
|
+
const s = streamSimple(agentCtx.options.model, llmCtx, {
|
|
80
|
+
reasoning: agentCtx.options.effort,
|
|
81
|
+
signal: agentCtx.signal,
|
|
82
|
+
apiKey,
|
|
714
83
|
});
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
return compactedMessages;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
interface StreamedToolCallEventPayload {
|
|
721
|
-
toolCallId: string;
|
|
722
|
-
name: string;
|
|
723
|
-
args: Record<string, unknown>;
|
|
724
|
-
content: AssistantMessage["content"];
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
function buildStreamedToolCallEventPayload(
|
|
728
|
-
partial: AssistantMessage,
|
|
729
|
-
contentIndex: number,
|
|
730
|
-
toolCall?: ToolCall,
|
|
731
|
-
): StreamedToolCallEventPayload | null {
|
|
732
|
-
const block = toolCall ?? partial.content[contentIndex];
|
|
733
|
-
if (!block || block.type !== "toolCall") {
|
|
734
|
-
return null;
|
|
735
|
-
}
|
|
736
84
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
85
|
+
let partial: AssistantMessage | null = null;
|
|
86
|
+
let added = false;
|
|
87
|
+
|
|
88
|
+
for await (const e of s) {
|
|
89
|
+
switch (e.type) {
|
|
90
|
+
case "start":
|
|
91
|
+
partial = e.partial;
|
|
92
|
+
llmCtx.messages.push(e.partial);
|
|
93
|
+
added = true;
|
|
94
|
+
yield { type: "message_start", partial };
|
|
95
|
+
break;
|
|
96
|
+
|
|
97
|
+
case "text_start":
|
|
98
|
+
case "text_delta":
|
|
99
|
+
case "text_end":
|
|
100
|
+
case "thinking_start":
|
|
101
|
+
case "thinking_delta":
|
|
102
|
+
case "thinking_end":
|
|
103
|
+
case "toolcall_start":
|
|
104
|
+
case "toolcall_delta":
|
|
105
|
+
case "toolcall_end":
|
|
106
|
+
if (partial) {
|
|
107
|
+
partial = e.partial;
|
|
108
|
+
llmCtx.messages[llmCtx.messages.length - 1] = partial;
|
|
109
|
+
yield { type: "message_update", partial };
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case "error": {
|
|
114
|
+
const finalMessage = await s.result();
|
|
115
|
+
if (added) {
|
|
116
|
+
llmCtx.messages[llmCtx.messages.length - 1] = finalMessage;
|
|
117
|
+
} else {
|
|
118
|
+
llmCtx.messages.push(finalMessage);
|
|
119
|
+
yield { type: "message_start", partial: { ...finalMessage } };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
yield { type: "message_end", message: finalMessage };
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
774
125
|
}
|
|
775
|
-
return null;
|
|
776
|
-
}
|
|
777
|
-
case "toolcall_delta": {
|
|
778
|
-
const payload = buildStreamedToolCallEventPayload(
|
|
779
|
-
event.partial,
|
|
780
|
-
event.contentIndex,
|
|
781
|
-
);
|
|
782
|
-
if (payload) {
|
|
783
|
-
onEvent?.({
|
|
784
|
-
type: "toolcall_delta",
|
|
785
|
-
delta: event.delta,
|
|
786
|
-
...payload,
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
return null;
|
|
790
|
-
}
|
|
791
|
-
case "toolcall_end": {
|
|
792
|
-
const payload = buildStreamedToolCallEventPayload(
|
|
793
|
-
event.partial,
|
|
794
|
-
event.contentIndex,
|
|
795
|
-
event.toolCall,
|
|
796
|
-
);
|
|
797
|
-
if (payload) {
|
|
798
|
-
onEvent?.({
|
|
799
|
-
type: "toolcall_end",
|
|
800
|
-
...payload,
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
return null;
|
|
804
|
-
}
|
|
805
|
-
case "done":
|
|
806
|
-
return event.message;
|
|
807
|
-
case "error":
|
|
808
|
-
return event.error;
|
|
809
|
-
case "start":
|
|
810
|
-
case "text_start":
|
|
811
|
-
case "text_end":
|
|
812
|
-
case "thinking_start":
|
|
813
|
-
case "thinking_end":
|
|
814
|
-
return null;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function buildIncompleteAssistantMessage(
|
|
819
|
-
opts: Pick<RunAgentOpts, "model" | "signal">,
|
|
820
|
-
partialAssistantMessage?: AssistantMessage,
|
|
821
|
-
): AssistantMessage {
|
|
822
|
-
const stopReason = opts.signal?.aborted ? "aborted" : "error";
|
|
823
|
-
const errorMessage =
|
|
824
|
-
stopReason === "aborted"
|
|
825
|
-
? "Request was aborted"
|
|
826
|
-
: "Stream ended without a final assistant message";
|
|
827
|
-
|
|
828
|
-
return {
|
|
829
|
-
role: "assistant",
|
|
830
|
-
content: partialAssistantMessage
|
|
831
|
-
? cloneAssistantContent(partialAssistantMessage.content)
|
|
832
|
-
: [],
|
|
833
|
-
api: partialAssistantMessage?.api ?? opts.model.api,
|
|
834
|
-
provider: partialAssistantMessage?.provider ?? opts.model.provider,
|
|
835
|
-
model: partialAssistantMessage?.model ?? opts.model.id,
|
|
836
|
-
usage: partialAssistantMessage?.usage ?? {
|
|
837
|
-
input: 0,
|
|
838
|
-
output: 0,
|
|
839
|
-
cacheRead: 0,
|
|
840
|
-
cacheWrite: 0,
|
|
841
|
-
totalTokens: 0,
|
|
842
|
-
cost: {
|
|
843
|
-
input: 0,
|
|
844
|
-
output: 0,
|
|
845
|
-
cacheRead: 0,
|
|
846
|
-
cacheWrite: 0,
|
|
847
|
-
total: 0,
|
|
848
|
-
},
|
|
849
|
-
},
|
|
850
|
-
stopReason,
|
|
851
|
-
errorMessage,
|
|
852
|
-
timestamp: Date.now(),
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
async function resolveStreamResultSoon(
|
|
857
|
-
streamResult: Promise<AssistantMessage>,
|
|
858
|
-
): Promise<AssistantMessage | undefined> {
|
|
859
|
-
const pending = Symbol("pending");
|
|
860
|
-
const result = await Promise.race([
|
|
861
|
-
streamResult,
|
|
862
|
-
new Promise<typeof pending>((resolve) => {
|
|
863
|
-
setTimeout(() => resolve(pending), 0);
|
|
864
|
-
}),
|
|
865
|
-
]);
|
|
866
|
-
return result === pending ? undefined : result;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
async function streamAssistantMessage(
|
|
870
|
-
opts: Pick<
|
|
871
|
-
RunAgentOpts,
|
|
872
|
-
| "model"
|
|
873
|
-
| "systemPrompt"
|
|
874
|
-
| "tools"
|
|
875
|
-
| "messages"
|
|
876
|
-
| "apiKey"
|
|
877
|
-
| "effort"
|
|
878
|
-
| "signal"
|
|
879
|
-
| "onEvent"
|
|
880
|
-
>,
|
|
881
|
-
): Promise<AssistantMessage> {
|
|
882
|
-
const eventStream = streamSimple(
|
|
883
|
-
opts.model,
|
|
884
|
-
buildAgentContext(
|
|
885
|
-
opts.systemPrompt,
|
|
886
|
-
opts.messages,
|
|
887
|
-
opts.tools,
|
|
888
|
-
opts.model.input.includes("image"),
|
|
889
|
-
),
|
|
890
|
-
buildStreamOptions(opts.apiKey, opts.effort, opts.signal),
|
|
891
|
-
);
|
|
892
|
-
const streamResult = eventStream.result();
|
|
893
|
-
let settledStreamResult: AssistantMessage | undefined;
|
|
894
|
-
void streamResult.then((message) => {
|
|
895
|
-
settledStreamResult = message;
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
let assistantMessage: AssistantMessage | undefined;
|
|
899
|
-
let partialAssistantMessage: AssistantMessage | undefined;
|
|
900
|
-
|
|
901
|
-
for await (const event of eventStream) {
|
|
902
|
-
if ("partial" in event) {
|
|
903
|
-
partialAssistantMessage = event.partial;
|
|
904
126
|
}
|
|
905
127
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
// `end(result)` resolves the final result without emitting a terminal event.
|
|
911
|
-
// Some wrappers settle that promise on the next task, so give it one more
|
|
912
|
-
// turn before treating the stream as incomplete.
|
|
913
|
-
await Promise.resolve();
|
|
914
|
-
if (!assistantMessage && !settledStreamResult) {
|
|
915
|
-
settledStreamResult = await resolveStreamResultSoon(streamResult);
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
const finalAssistantMessage =
|
|
919
|
-
assistantMessage ??
|
|
920
|
-
settledStreamResult ??
|
|
921
|
-
buildIncompleteAssistantMessage(opts, partialAssistantMessage);
|
|
922
|
-
if (!partialAssistantMessage) {
|
|
923
|
-
return finalAssistantMessage;
|
|
924
|
-
}
|
|
925
|
-
return mergeAssistantMessage(partialAssistantMessage, finalAssistantMessage);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
interface AppendedUserTurn {
|
|
929
|
-
/** Turn number assigned to the appended user message. */
|
|
930
|
-
turn: number;
|
|
931
|
-
/** Actual current user request text to keep anchoring future compactions. */
|
|
932
|
-
currentUserRequest: string;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
function appendUserMessage(
|
|
936
|
-
db: Database,
|
|
937
|
-
sessionId: string,
|
|
938
|
-
messages: Message[],
|
|
939
|
-
userMessage: UserMessage,
|
|
940
|
-
onEvent: RunAgentOpts["onEvent"],
|
|
941
|
-
): AppendedUserTurn {
|
|
942
|
-
messages.push(userMessage);
|
|
943
|
-
const turn = appendMessage(db, sessionId, userMessage);
|
|
944
|
-
onEvent?.({ type: "user_message", message: userMessage });
|
|
945
|
-
return {
|
|
946
|
-
turn,
|
|
947
|
-
currentUserRequest: resolveCurrentUserRequest(userMessage),
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
function appendAssistantMessage(
|
|
952
|
-
db: Database,
|
|
953
|
-
sessionId: string,
|
|
954
|
-
messages: Message[],
|
|
955
|
-
assistantMessage: AssistantMessage,
|
|
956
|
-
turn: number,
|
|
957
|
-
onEvent: RunAgentOpts["onEvent"],
|
|
958
|
-
): void {
|
|
959
|
-
messages.push(assistantMessage);
|
|
960
|
-
appendMessage(db, sessionId, assistantMessage, turn);
|
|
961
|
-
onEvent?.({ type: "assistant_message", message: assistantMessage });
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function resolveLoopStopReason(
|
|
965
|
-
assistantMessage: AssistantMessage,
|
|
966
|
-
messages: Message[],
|
|
967
|
-
onEvent: RunAgentOpts["onEvent"],
|
|
968
|
-
): AgentLoopResult | null {
|
|
969
|
-
if (
|
|
970
|
-
assistantMessage.stopReason === "error" ||
|
|
971
|
-
assistantMessage.stopReason === "aborted"
|
|
972
|
-
) {
|
|
973
|
-
const eventType =
|
|
974
|
-
assistantMessage.stopReason === "error" ? "error" : "aborted";
|
|
975
|
-
onEvent?.({ type: eventType, message: assistantMessage });
|
|
976
|
-
return { messages, stopReason: assistantMessage.stopReason };
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
return null;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
function consumeQueuedUserMessage(
|
|
983
|
-
db: Database,
|
|
984
|
-
sessionId: string,
|
|
985
|
-
messages: Message[],
|
|
986
|
-
takeQueuedUserMessage: RunAgentOpts["takeQueuedUserMessage"],
|
|
987
|
-
onEvent: RunAgentOpts["onEvent"],
|
|
988
|
-
): AppendedUserTurn | null {
|
|
989
|
-
const queuedUserMessage = takeQueuedUserMessage?.();
|
|
990
|
-
if (!queuedUserMessage) {
|
|
991
|
-
return null;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
return appendUserMessage(db, sessionId, messages, queuedUserMessage, onEvent);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
function getAssistantToolCalls(message: AssistantMessage): ToolCall[] {
|
|
998
|
-
return message.content.filter((content): content is ToolCall => {
|
|
999
|
-
return content.type === "toolCall";
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
async function executeToolCall(
|
|
1004
|
-
toolCall: ToolCall,
|
|
1005
|
-
opts: Pick<RunAgentOpts, "toolHandlers" | "cwd" | "signal" | "onEvent">,
|
|
1006
|
-
): Promise<ToolExecResult> {
|
|
1007
|
-
const handler = opts.toolHandlers.get(toolCall.name);
|
|
1008
|
-
if (!handler) {
|
|
1009
|
-
return unknownToolResult(toolCall.name);
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
opts.onEvent?.({
|
|
1013
|
-
type: "tool_start",
|
|
1014
|
-
toolCallId: toolCall.id,
|
|
1015
|
-
name: toolCall.name,
|
|
1016
|
-
args: toolCall.arguments,
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
let result: ToolExecResult;
|
|
1020
|
-
try {
|
|
1021
|
-
if (opts.signal?.aborted) {
|
|
1022
|
-
result = toolErrorResult(
|
|
1023
|
-
toolCall.name,
|
|
1024
|
-
new Error("This operation was aborted"),
|
|
1025
|
-
);
|
|
128
|
+
const message = await s.result();
|
|
129
|
+
if (added) {
|
|
130
|
+
llmCtx.messages[llmCtx.messages.length - 1] = message;
|
|
1026
131
|
} else {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
opts.cwd,
|
|
1030
|
-
opts.signal,
|
|
1031
|
-
(partial) => {
|
|
1032
|
-
opts.onEvent?.({
|
|
1033
|
-
type: "tool_delta",
|
|
1034
|
-
toolCallId: toolCall.id,
|
|
1035
|
-
name: toolCall.name,
|
|
1036
|
-
result: partial,
|
|
1037
|
-
});
|
|
1038
|
-
},
|
|
1039
|
-
);
|
|
132
|
+
llmCtx.messages.push(message);
|
|
133
|
+
yield { type: "message_start", partial: { ...message } };
|
|
1040
134
|
}
|
|
1041
|
-
|
|
1042
|
-
result = toolErrorResult(toolCall.name, error);
|
|
1043
|
-
}
|
|
135
|
+
yield { type: "message_end", message };
|
|
1044
136
|
|
|
1045
|
-
|
|
1046
|
-
type: "tool_end",
|
|
1047
|
-
toolCallId: toolCall.id,
|
|
1048
|
-
name: toolCall.name,
|
|
1049
|
-
result,
|
|
1050
|
-
});
|
|
1051
|
-
return result;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
function appendToolResultMessage(
|
|
1055
|
-
db: Database,
|
|
1056
|
-
sessionId: string,
|
|
1057
|
-
messages: Message[],
|
|
1058
|
-
toolCall: ToolCall,
|
|
1059
|
-
result: ToolExecResult,
|
|
1060
|
-
turn: number,
|
|
1061
|
-
onEvent: RunAgentOpts["onEvent"],
|
|
1062
|
-
): void {
|
|
1063
|
-
const toolResultMessage: ToolResultMessage = {
|
|
1064
|
-
role: "toolResult",
|
|
1065
|
-
toolCallId: toolCall.id,
|
|
1066
|
-
toolName: toolCall.name,
|
|
1067
|
-
content: result.content,
|
|
1068
|
-
...(result.details !== undefined ? { details: result.details } : {}),
|
|
1069
|
-
isError: result.isError,
|
|
1070
|
-
timestamp: Date.now(),
|
|
1071
|
-
};
|
|
1072
|
-
|
|
1073
|
-
messages.push(toolResultMessage);
|
|
1074
|
-
appendMessage(db, sessionId, toolResultMessage, turn);
|
|
1075
|
-
onEvent?.({ type: "tool_result", message: toolResultMessage });
|
|
1076
|
-
}
|
|
137
|
+
const toolCalls = message.content.filter((c) => c.type === "toolCall");
|
|
1077
138
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
) {
|
|
1083
|
-
return getTodoItems([...loadMessages(db, sessionId), ...messages]).filter(
|
|
1084
|
-
(todo) => todo.status !== "completed",
|
|
1085
|
-
);
|
|
1086
|
-
}
|
|
139
|
+
// Stop on errors or no tools to call.
|
|
140
|
+
if (message.stopReason !== "toolUse" || toolCalls.length === 0) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
1087
143
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
144
|
+
const seenToolIds = new Map<string, number>();
|
|
145
|
+
if (toolCalls.length > 0) {
|
|
146
|
+
const ts = toolRunner(toolCalls, agentCtx.tools, agentCtx.signal);
|
|
147
|
+
|
|
148
|
+
for await (const e of ts) {
|
|
149
|
+
if (e.type === "tool_update") {
|
|
150
|
+
// Update contex with update or add new.
|
|
151
|
+
const existing = seenToolIds.get(e.partial.toolCallId);
|
|
152
|
+
if (existing && existing >= 0) {
|
|
153
|
+
const m = llmCtx.messages[existing];
|
|
154
|
+
if (m.role === "toolResult") {
|
|
155
|
+
llmCtx.messages[existing] = {
|
|
156
|
+
...m,
|
|
157
|
+
content: [...m.content, ...e.partial.content],
|
|
158
|
+
isError: e.partial.isError,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
yield {
|
|
163
|
+
type: "tool_message_update",
|
|
164
|
+
partial: e.partial,
|
|
165
|
+
};
|
|
166
|
+
} else {
|
|
167
|
+
llmCtx.messages.push(e.partial);
|
|
168
|
+
seenToolIds.set(e.partial.toolCallId, llmCtx.messages.length - 1);
|
|
169
|
+
|
|
170
|
+
yield {
|
|
171
|
+
type: "tool_message_start",
|
|
172
|
+
partial: e.partial,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
} else if (e.type === "tool_result") {
|
|
176
|
+
// Update context with full message and yield
|
|
177
|
+
const idx = llmCtx.messages.findIndex(
|
|
178
|
+
(m) =>
|
|
179
|
+
m.role === "toolResult" && m.toolCallId === e.message.toolCallId,
|
|
180
|
+
);
|
|
181
|
+
if (idx >= 0) {
|
|
182
|
+
llmCtx.messages[idx] = e.message;
|
|
183
|
+
} else {
|
|
184
|
+
llmCtx.messages.push(e.message);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
yield {
|
|
188
|
+
type: "tool_message_end",
|
|
189
|
+
message: e.message,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
1096
192
|
}
|
|
1097
|
-
return left.status.localeCompare(right.status);
|
|
1098
|
-
}),
|
|
1099
|
-
);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
function wrapSystemReminder(content: string): string {
|
|
1103
|
-
return `<system_reminder>\n${content}\n</system_reminder>`;
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
function createTodoReminderMessage(
|
|
1107
|
-
db: Database,
|
|
1108
|
-
sessionId: string,
|
|
1109
|
-
messages: readonly Message[],
|
|
1110
|
-
remindedTodoSignatures: Set<string>,
|
|
1111
|
-
): UserMessage | null {
|
|
1112
|
-
const incompleteTodos = getIncompleteTodos(db, sessionId, messages);
|
|
1113
|
-
if (incompleteTodos.length === 0) {
|
|
1114
|
-
return null;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
const signature = getTodoReminderSignature(incompleteTodos);
|
|
1118
|
-
if (remindedTodoSignatures.has(signature)) {
|
|
1119
|
-
return null;
|
|
1120
|
-
}
|
|
1121
|
-
remindedTodoSignatures.add(signature);
|
|
1122
|
-
|
|
1123
|
-
const lines = [
|
|
1124
|
-
"You have pending todo items that must be completed before finishing the task:",
|
|
1125
|
-
"",
|
|
1126
|
-
...incompleteTodos.map((todo) => {
|
|
1127
|
-
const status = todo.status === "in_progress" ? "IN_PROGRESS" : "PENDING";
|
|
1128
|
-
return `- [${status}] ${todo.content}`;
|
|
1129
|
-
}),
|
|
1130
|
-
"",
|
|
1131
|
-
"Please complete all pending items before finishing.",
|
|
1132
|
-
];
|
|
1133
|
-
|
|
1134
|
-
return {
|
|
1135
|
-
role: "user",
|
|
1136
|
-
content: wrapSystemReminder(lines.join("\n")),
|
|
1137
|
-
timestamp: Date.now(),
|
|
1138
|
-
};
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
interface StoppedAssistantResolution {
|
|
1142
|
-
/** Next turn number when a queued steering message was consumed. */
|
|
1143
|
-
nextTurn: number | null;
|
|
1144
|
-
/** Updated current user request when a queued steering message starts a new turn. */
|
|
1145
|
-
nextCurrentUserRequest: string | null;
|
|
1146
|
-
/** Ephemeral context messages to include on the next model request. */
|
|
1147
|
-
pendingContextMessages: Message[];
|
|
1148
|
-
/** Final loop result when the turn should stop immediately. */
|
|
1149
|
-
finalResult: AgentLoopResult | null;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
function resolveStoppedAssistantMessage(
|
|
1153
|
-
assistantMessage: AssistantMessage,
|
|
1154
|
-
stopReason: "stop" | "length",
|
|
1155
|
-
opts: Pick<
|
|
1156
|
-
RunAgentOpts,
|
|
1157
|
-
"db" | "sessionId" | "messages" | "takeQueuedUserMessage" | "onEvent"
|
|
1158
|
-
>,
|
|
1159
|
-
remindedTodoSignatures: Set<string>,
|
|
1160
|
-
): StoppedAssistantResolution {
|
|
1161
|
-
const queuedTurn = consumeQueuedUserMessage(
|
|
1162
|
-
opts.db,
|
|
1163
|
-
opts.sessionId,
|
|
1164
|
-
opts.messages,
|
|
1165
|
-
opts.takeQueuedUserMessage,
|
|
1166
|
-
opts.onEvent,
|
|
1167
|
-
);
|
|
1168
|
-
if (queuedTurn !== null) {
|
|
1169
|
-
return {
|
|
1170
|
-
nextTurn: queuedTurn.turn,
|
|
1171
|
-
nextCurrentUserRequest: queuedTurn.currentUserRequest,
|
|
1172
|
-
pendingContextMessages: [],
|
|
1173
|
-
finalResult: null,
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
const todoReminder = createTodoReminderMessage(
|
|
1178
|
-
opts.db,
|
|
1179
|
-
opts.sessionId,
|
|
1180
|
-
opts.messages,
|
|
1181
|
-
remindedTodoSignatures,
|
|
1182
|
-
);
|
|
1183
|
-
if (todoReminder) {
|
|
1184
|
-
return {
|
|
1185
|
-
nextTurn: null,
|
|
1186
|
-
nextCurrentUserRequest: null,
|
|
1187
|
-
pendingContextMessages: [todoReminder],
|
|
1188
|
-
finalResult: null,
|
|
1189
|
-
};
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
opts.onEvent?.({ type: "done", message: assistantMessage });
|
|
1193
|
-
return {
|
|
1194
|
-
nextTurn: null,
|
|
1195
|
-
nextCurrentUserRequest: null,
|
|
1196
|
-
pendingContextMessages: [],
|
|
1197
|
-
finalResult: {
|
|
1198
|
-
messages: opts.messages,
|
|
1199
|
-
stopReason,
|
|
1200
|
-
},
|
|
1201
|
-
};
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
async function executeAssistantToolCalls(
|
|
1205
|
-
assistantMessage: AssistantMessage,
|
|
1206
|
-
opts: Pick<
|
|
1207
|
-
RunAgentOpts,
|
|
1208
|
-
| "db"
|
|
1209
|
-
| "sessionId"
|
|
1210
|
-
| "messages"
|
|
1211
|
-
| "toolHandlers"
|
|
1212
|
-
| "cwd"
|
|
1213
|
-
| "signal"
|
|
1214
|
-
| "onEvent"
|
|
1215
|
-
| "model"
|
|
1216
|
-
>,
|
|
1217
|
-
turn: number,
|
|
1218
|
-
): Promise<AgentLoopResult | null> {
|
|
1219
|
-
for (const toolCall of getAssistantToolCalls(assistantMessage)) {
|
|
1220
|
-
const result = await executeToolCall(toolCall, opts);
|
|
1221
|
-
appendToolResultMessage(
|
|
1222
|
-
opts.db,
|
|
1223
|
-
opts.sessionId,
|
|
1224
|
-
opts.messages,
|
|
1225
|
-
toolCall,
|
|
1226
|
-
result,
|
|
1227
|
-
turn,
|
|
1228
|
-
opts.onEvent,
|
|
1229
|
-
);
|
|
1230
|
-
|
|
1231
|
-
if (opts.signal?.aborted) {
|
|
1232
|
-
opts.onEvent?.({
|
|
1233
|
-
type: "aborted",
|
|
1234
|
-
message: buildIncompleteAssistantMessage(
|
|
1235
|
-
{ model: opts.model, signal: opts.signal },
|
|
1236
|
-
assistantMessage,
|
|
1237
|
-
),
|
|
1238
|
-
});
|
|
1239
|
-
return { messages: opts.messages, stopReason: "aborted" };
|
|
1240
193
|
}
|
|
1241
194
|
}
|
|
1242
|
-
|
|
1243
|
-
return null;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
interface AgentIterationOutcome {
|
|
1247
|
-
/** Final loop result when the run should stop immediately. */
|
|
1248
|
-
finalResult: AgentLoopResult | null;
|
|
1249
|
-
/** Next turn number when a queued steering message starts a new turn. */
|
|
1250
|
-
nextTurn: number;
|
|
1251
|
-
/** Current user request text that should anchor any future compactions. */
|
|
1252
|
-
nextCurrentUserRequest: string;
|
|
1253
|
-
/** Ephemeral context messages for the next model request. */
|
|
1254
|
-
pendingContextMessages: Message[];
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
async function resolveAgentIteration(
|
|
1258
|
-
assistantMessage: AssistantMessage,
|
|
1259
|
-
currentTurn: number,
|
|
1260
|
-
currentUserRequest: string,
|
|
1261
|
-
remindedTodoSignatures: Set<string>,
|
|
1262
|
-
opts: Pick<
|
|
1263
|
-
RunAgentOpts,
|
|
1264
|
-
| "db"
|
|
1265
|
-
| "sessionId"
|
|
1266
|
-
| "messages"
|
|
1267
|
-
| "toolHandlers"
|
|
1268
|
-
| "cwd"
|
|
1269
|
-
| "signal"
|
|
1270
|
-
| "onEvent"
|
|
1271
|
-
| "model"
|
|
1272
|
-
| "takeQueuedUserMessage"
|
|
1273
|
-
>,
|
|
1274
|
-
): Promise<AgentIterationOutcome> {
|
|
1275
|
-
const stopResult = resolveLoopStopReason(
|
|
1276
|
-
assistantMessage,
|
|
1277
|
-
opts.messages,
|
|
1278
|
-
opts.onEvent,
|
|
1279
|
-
);
|
|
1280
|
-
if (stopResult) {
|
|
1281
|
-
return {
|
|
1282
|
-
finalResult: stopResult,
|
|
1283
|
-
nextTurn: currentTurn,
|
|
1284
|
-
nextCurrentUserRequest: currentUserRequest,
|
|
1285
|
-
pendingContextMessages: [],
|
|
1286
|
-
};
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
if (
|
|
1290
|
-
assistantMessage.stopReason === "stop" ||
|
|
1291
|
-
assistantMessage.stopReason === "length"
|
|
1292
|
-
) {
|
|
1293
|
-
const stopResolution = resolveStoppedAssistantMessage(
|
|
1294
|
-
assistantMessage,
|
|
1295
|
-
assistantMessage.stopReason,
|
|
1296
|
-
{
|
|
1297
|
-
db: opts.db,
|
|
1298
|
-
sessionId: opts.sessionId,
|
|
1299
|
-
messages: opts.messages,
|
|
1300
|
-
takeQueuedUserMessage: opts.takeQueuedUserMessage,
|
|
1301
|
-
onEvent: opts.onEvent,
|
|
1302
|
-
},
|
|
1303
|
-
remindedTodoSignatures,
|
|
1304
|
-
);
|
|
1305
|
-
return {
|
|
1306
|
-
finalResult: stopResolution.finalResult,
|
|
1307
|
-
nextTurn: stopResolution.nextTurn ?? currentTurn,
|
|
1308
|
-
nextCurrentUserRequest:
|
|
1309
|
-
stopResolution.nextCurrentUserRequest ?? currentUserRequest,
|
|
1310
|
-
pendingContextMessages: stopResolution.pendingContextMessages,
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
const toolStopResult = await executeAssistantToolCalls(
|
|
1315
|
-
assistantMessage,
|
|
1316
|
-
opts,
|
|
1317
|
-
currentTurn,
|
|
1318
|
-
);
|
|
1319
|
-
if (toolStopResult) {
|
|
1320
|
-
return {
|
|
1321
|
-
finalResult: toolStopResult,
|
|
1322
|
-
nextTurn: currentTurn,
|
|
1323
|
-
nextCurrentUserRequest: currentUserRequest,
|
|
1324
|
-
pendingContextMessages: [],
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
const queuedTurn = consumeQueuedUserMessage(
|
|
1329
|
-
opts.db,
|
|
1330
|
-
opts.sessionId,
|
|
1331
|
-
opts.messages,
|
|
1332
|
-
opts.takeQueuedUserMessage,
|
|
1333
|
-
opts.onEvent,
|
|
1334
|
-
);
|
|
1335
|
-
return {
|
|
1336
|
-
finalResult: null,
|
|
1337
|
-
nextTurn: queuedTurn?.turn ?? currentTurn,
|
|
1338
|
-
nextCurrentUserRequest:
|
|
1339
|
-
queuedTurn?.currentUserRequest ?? currentUserRequest,
|
|
1340
|
-
pendingContextMessages: [],
|
|
1341
|
-
};
|
|
1342
195
|
}
|
|
1343
196
|
|
|
1344
|
-
//
|
|
1345
|
-
//
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
signal,
|
|
1369
|
-
onEvent,
|
|
1370
|
-
toolHandlers,
|
|
1371
|
-
cwd,
|
|
1372
|
-
takeQueuedUserMessage,
|
|
1373
|
-
} = opts;
|
|
1374
|
-
let messages = initialMessages;
|
|
1375
|
-
let currentTurn = turn;
|
|
1376
|
-
let currentUserRequest = extractCurrentUserRequest(initialMessages);
|
|
1377
|
-
let pendingContextMessages: Message[] = [];
|
|
1378
|
-
const remindedTodoSignatures = new Set<string>();
|
|
1379
|
-
|
|
1380
|
-
while (true) {
|
|
1381
|
-
messages = await maybeCompactContext(
|
|
1382
|
-
messages,
|
|
1383
|
-
pendingContextMessages,
|
|
1384
|
-
currentUserRequest,
|
|
1385
|
-
{
|
|
1386
|
-
db,
|
|
1387
|
-
sessionId,
|
|
1388
|
-
model,
|
|
1389
|
-
apiKey: opts.apiKey,
|
|
1390
|
-
effort: opts.effort,
|
|
1391
|
-
signal,
|
|
1392
|
-
onEvent,
|
|
1393
|
-
},
|
|
1394
|
-
);
|
|
1395
|
-
|
|
1396
|
-
const assistantMessage = await streamAssistantMessage({
|
|
1397
|
-
...opts,
|
|
1398
|
-
messages:
|
|
1399
|
-
pendingContextMessages.length > 0
|
|
1400
|
-
? [...messages, ...pendingContextMessages]
|
|
1401
|
-
: messages,
|
|
1402
|
-
});
|
|
1403
|
-
pendingContextMessages = [];
|
|
1404
|
-
appendAssistantMessage(
|
|
1405
|
-
db,
|
|
1406
|
-
sessionId,
|
|
1407
|
-
messages,
|
|
1408
|
-
assistantMessage,
|
|
1409
|
-
currentTurn,
|
|
1410
|
-
onEvent,
|
|
1411
|
-
);
|
|
1412
|
-
|
|
1413
|
-
const iterationOutcome = await resolveAgentIteration(
|
|
1414
|
-
assistantMessage,
|
|
1415
|
-
currentTurn,
|
|
1416
|
-
currentUserRequest,
|
|
1417
|
-
remindedTodoSignatures,
|
|
1418
|
-
{
|
|
1419
|
-
db,
|
|
1420
|
-
sessionId,
|
|
1421
|
-
messages,
|
|
1422
|
-
toolHandlers,
|
|
1423
|
-
cwd,
|
|
1424
|
-
signal,
|
|
1425
|
-
onEvent,
|
|
1426
|
-
model,
|
|
1427
|
-
takeQueuedUserMessage,
|
|
1428
|
-
},
|
|
1429
|
-
);
|
|
1430
|
-
if (iterationOutcome.finalResult) {
|
|
1431
|
-
return iterationOutcome.finalResult;
|
|
197
|
+
// This should stay stateless, don't accumulate anything at this layer, just proxy
|
|
198
|
+
// wrapped runners events.
|
|
199
|
+
async function* toolRunner(
|
|
200
|
+
toolCalls: ToolCall[],
|
|
201
|
+
tools: ToolAndRunner[],
|
|
202
|
+
signal?: AbortSignal,
|
|
203
|
+
): AsyncGenerator<AgentToolEvent> {
|
|
204
|
+
for (const call of toolCalls) {
|
|
205
|
+
const timestamp = Date.now();
|
|
206
|
+
const tool = tools.find((t) => t.tool.name === call.name);
|
|
207
|
+
|
|
208
|
+
if (!tool) {
|
|
209
|
+
yield {
|
|
210
|
+
type: "tool_result",
|
|
211
|
+
message: {
|
|
212
|
+
role: "toolResult",
|
|
213
|
+
toolCallId: call.id,
|
|
214
|
+
toolName: call.name,
|
|
215
|
+
content: [{ type: "text", text: "Error: Tool not found" }],
|
|
216
|
+
isError: true,
|
|
217
|
+
timestamp,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
continue;
|
|
1432
221
|
}
|
|
1433
222
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
223
|
+
try {
|
|
224
|
+
for await (const e of tool.runner(call.arguments, signal)) {
|
|
225
|
+
// yield deltas for output, full output on result.
|
|
226
|
+
const content: TextContent = { type: "text", text: e.text };
|
|
227
|
+
|
|
228
|
+
if (e.type === "output") {
|
|
229
|
+
// handle deltas
|
|
230
|
+
yield {
|
|
231
|
+
type: "tool_update",
|
|
232
|
+
partial: {
|
|
233
|
+
role: "toolResult",
|
|
234
|
+
toolCallId: call.id,
|
|
235
|
+
toolName: call.name,
|
|
236
|
+
content: [content],
|
|
237
|
+
isError: false,
|
|
238
|
+
timestamp,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
} else if (e.type === "result") {
|
|
242
|
+
// handle final message
|
|
243
|
+
yield {
|
|
244
|
+
type: "tool_result",
|
|
245
|
+
message: {
|
|
246
|
+
role: "toolResult",
|
|
247
|
+
toolCallId: call.id,
|
|
248
|
+
toolName: call.name,
|
|
249
|
+
content: [content],
|
|
250
|
+
isError: false,
|
|
251
|
+
timestamp,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} catch (err) {
|
|
257
|
+
// We don't validate arguments, so it's better to show the errors.
|
|
258
|
+
const error = err instanceof Error ? err.message : "Unknown error";
|
|
259
|
+
yield {
|
|
260
|
+
type: "tool_result",
|
|
261
|
+
message: {
|
|
262
|
+
role: "toolResult",
|
|
263
|
+
toolCallId: call.id,
|
|
264
|
+
toolName: call.name,
|
|
265
|
+
content: [{ type: "text", text: `Error: ${error}` }],
|
|
266
|
+
isError: true,
|
|
267
|
+
timestamp,
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
1437
271
|
}
|
|
1438
272
|
}
|