mini-coder 0.5.12 → 0.5.14
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/BENCHMARK.md +15 -316
- package/PROGRESS.md +3 -3
- package/README.md +54 -21
- package/benchmark-baseline.sh +15 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +526 -13
- package/src/assistant-output.ts +73 -0
- package/src/cli.ts +2 -1
- package/src/delegation.ts +238 -0
- package/src/headless.ts +90 -44
- package/src/index.ts +267 -102
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +11 -13
- package/src/session-message.ts +57 -65
- package/src/session.ts +389 -42
- package/src/settings.ts +199 -7
- package/src/skills.ts +12 -3
- package/src/submit.ts +24 -2
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +2 -0
- package/src/tool-delegate.ts +125 -0
- package/src/tool-shell.ts +190 -8
- package/src/tools.ts +335 -6
- package/src/ui/agent.ts +10 -0
- package/src/ui/commands.test.ts +525 -10
- package/src/ui/commands.ts +224 -8
- package/src/ui/conversation.test.ts +252 -27
- package/src/ui/conversation.ts +468 -390
- package/src/ui/help.ts +27 -11
- package/src/ui.ts +230 -75
- package/src/plugins.ts +0 -183
package/src/agent.ts
CHANGED
|
@@ -21,9 +21,33 @@ import type {
|
|
|
21
21
|
UserMessage,
|
|
22
22
|
} from "@mariozechner/pi-ai";
|
|
23
23
|
import { streamSimple } from "@mariozechner/pi-ai";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
appendMessage,
|
|
26
|
+
appendSessionCompaction,
|
|
27
|
+
computeContextTokens,
|
|
28
|
+
computeSessionStats,
|
|
29
|
+
loadCompactedModelMessages,
|
|
30
|
+
loadMessages,
|
|
31
|
+
loadUncompactedModelMessages,
|
|
32
|
+
} from "./session.ts";
|
|
33
|
+
import { collapseWhitespaceToNull, joinTextBlocks } from "./text.ts";
|
|
25
34
|
import { getTodoItems, type ToolExecResult } from "./tools.ts";
|
|
26
35
|
|
|
36
|
+
const CONTEXT_COMPACTION_THRESHOLD = 0.9;
|
|
37
|
+
const CONTEXT_COMPACTION_FRACTION = 0.4;
|
|
38
|
+
const DEFAULT_CURRENT_USER_REQUEST =
|
|
39
|
+
"Continue the current task using the earlier conversation context.";
|
|
40
|
+
const COMPACTION_RECOVERY_PREFIX =
|
|
41
|
+
"These earlier messages were compacted out of the active context.";
|
|
42
|
+
|
|
43
|
+
const COMPACTION_SUMMARY_SYSTEM_PROMPT = [
|
|
44
|
+
"You summarize earlier conversation context for a coding agent.",
|
|
45
|
+
"Write a concise replacement summary for the provided earlier messages so the agent can continue the current request after those messages are removed from active context.",
|
|
46
|
+
"Keep only concrete facts that may still matter: user goals, decisions, constraints, files or paths, commands run, tool results, errors, and unfinished work.",
|
|
47
|
+
"Prefer short bullet points.",
|
|
48
|
+
"Do not invent details or mention information that is not supported by the provided messages.",
|
|
49
|
+
].join("\n");
|
|
50
|
+
|
|
27
51
|
// ---------------------------------------------------------------------------
|
|
28
52
|
// Types
|
|
29
53
|
// ---------------------------------------------------------------------------
|
|
@@ -101,6 +125,11 @@ export type AgentEvent =
|
|
|
101
125
|
result: ToolExecResult;
|
|
102
126
|
}
|
|
103
127
|
| { type: "tool_result"; message: ToolResultMessage }
|
|
128
|
+
| {
|
|
129
|
+
type: "context_compacted";
|
|
130
|
+
contextTokens: number;
|
|
131
|
+
stats: ReturnType<typeof computeSessionStats>;
|
|
132
|
+
}
|
|
104
133
|
| { type: "done"; message: AssistantMessage }
|
|
105
134
|
| { type: "error"; message: AssistantMessage }
|
|
106
135
|
| { type: "aborted"; message: AssistantMessage };
|
|
@@ -260,14 +289,103 @@ function unknownToolResult(name: string): ToolExecResult {
|
|
|
260
289
|
};
|
|
261
290
|
}
|
|
262
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
|
+
|
|
263
376
|
function buildAgentContext(
|
|
264
377
|
systemPrompt: string,
|
|
265
378
|
messages: Message[],
|
|
266
379
|
tools: Tool[],
|
|
380
|
+
modelSupportsImages: boolean,
|
|
267
381
|
) {
|
|
382
|
+
const contextMessages = sanitizeContextMessages(
|
|
383
|
+
messages,
|
|
384
|
+
modelSupportsImages,
|
|
385
|
+
).messages;
|
|
268
386
|
return tools.length > 0
|
|
269
|
-
? { systemPrompt, messages, tools }
|
|
270
|
-
: { systemPrompt, messages };
|
|
387
|
+
? { systemPrompt, messages: contextMessages, tools }
|
|
388
|
+
: { systemPrompt, messages: contextMessages };
|
|
271
389
|
}
|
|
272
390
|
|
|
273
391
|
function buildStreamOptions(
|
|
@@ -282,6 +400,323 @@ function buildStreamOptions(
|
|
|
282
400
|
};
|
|
283
401
|
}
|
|
284
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];
|
|
452
|
+
if (
|
|
453
|
+
!message ||
|
|
454
|
+
message.role !== "user" ||
|
|
455
|
+
isSystemReminderMessage(message) ||
|
|
456
|
+
isCompactionSummaryMessage(message)
|
|
457
|
+
) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
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
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return boundaries;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function computeSafeCompactionMessageCount(
|
|
584
|
+
messages: readonly Message[],
|
|
585
|
+
): number {
|
|
586
|
+
const targetCount = computeCompactionMessageCount(messages.length);
|
|
587
|
+
if (targetCount === 0) {
|
|
588
|
+
return 0;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const safeBoundaryCounts = collectCompactionBoundaryCounts(messages);
|
|
592
|
+
if (safeBoundaryCounts.length === 0) {
|
|
593
|
+
return 0;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
let nearestLowerBoundary = 0;
|
|
597
|
+
for (const boundaryCount of safeBoundaryCounts) {
|
|
598
|
+
if (boundaryCount === targetCount) {
|
|
599
|
+
return boundaryCount;
|
|
600
|
+
}
|
|
601
|
+
if (boundaryCount < targetCount) {
|
|
602
|
+
nearestLowerBoundary = boundaryCount;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
return nearestLowerBoundary > 0 ? nearestLowerBoundary : boundaryCount;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return nearestLowerBoundary;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
interface CompactionSummaryResult {
|
|
612
|
+
/** Replacement summary text for the compacted context prefix. */
|
|
613
|
+
text: string;
|
|
614
|
+
/** Usage consumed by the hidden summarization request. */
|
|
615
|
+
usage: AssistantMessage["usage"];
|
|
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,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function maybeCompactContext(
|
|
655
|
+
messages: Message[],
|
|
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;
|
|
664
|
+
|
|
665
|
+
while (!opts.signal?.aborted) {
|
|
666
|
+
const contextTokens = computeRequestContextTokens(
|
|
667
|
+
[...compactedMessages, ...pendingContextMessages],
|
|
668
|
+
opts.model.input.includes("image"),
|
|
669
|
+
);
|
|
670
|
+
if (
|
|
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),
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return compactedMessages;
|
|
718
|
+
}
|
|
719
|
+
|
|
285
720
|
interface StreamedToolCallEventPayload {
|
|
286
721
|
toolCallId: string;
|
|
287
722
|
name: string;
|
|
@@ -418,6 +853,19 @@ function buildIncompleteAssistantMessage(
|
|
|
418
853
|
};
|
|
419
854
|
}
|
|
420
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
|
+
|
|
421
869
|
async function streamAssistantMessage(
|
|
422
870
|
opts: Pick<
|
|
423
871
|
RunAgentOpts,
|
|
@@ -433,7 +881,12 @@ async function streamAssistantMessage(
|
|
|
433
881
|
): Promise<AssistantMessage> {
|
|
434
882
|
const eventStream = streamSimple(
|
|
435
883
|
opts.model,
|
|
436
|
-
buildAgentContext(
|
|
884
|
+
buildAgentContext(
|
|
885
|
+
opts.systemPrompt,
|
|
886
|
+
opts.messages,
|
|
887
|
+
opts.tools,
|
|
888
|
+
opts.model.input.includes("image"),
|
|
889
|
+
),
|
|
437
890
|
buildStreamOptions(opts.apiKey, opts.effort, opts.signal),
|
|
438
891
|
);
|
|
439
892
|
const streamResult = eventStream.result();
|
|
@@ -455,7 +908,13 @@ async function streamAssistantMessage(
|
|
|
455
908
|
}
|
|
456
909
|
|
|
457
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.
|
|
458
913
|
await Promise.resolve();
|
|
914
|
+
if (!assistantMessage && !settledStreamResult) {
|
|
915
|
+
settledStreamResult = await resolveStreamResultSoon(streamResult);
|
|
916
|
+
}
|
|
917
|
+
|
|
459
918
|
const finalAssistantMessage =
|
|
460
919
|
assistantMessage ??
|
|
461
920
|
settledStreamResult ??
|
|
@@ -466,17 +925,27 @@ async function streamAssistantMessage(
|
|
|
466
925
|
return mergeAssistantMessage(partialAssistantMessage, finalAssistantMessage);
|
|
467
926
|
}
|
|
468
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
|
+
|
|
469
935
|
function appendUserMessage(
|
|
470
936
|
db: Database,
|
|
471
937
|
sessionId: string,
|
|
472
938
|
messages: Message[],
|
|
473
939
|
userMessage: UserMessage,
|
|
474
940
|
onEvent: RunAgentOpts["onEvent"],
|
|
475
|
-
):
|
|
941
|
+
): AppendedUserTurn {
|
|
476
942
|
messages.push(userMessage);
|
|
477
943
|
const turn = appendMessage(db, sessionId, userMessage);
|
|
478
944
|
onEvent?.({ type: "user_message", message: userMessage });
|
|
479
|
-
return
|
|
945
|
+
return {
|
|
946
|
+
turn,
|
|
947
|
+
currentUserRequest: resolveCurrentUserRequest(userMessage),
|
|
948
|
+
};
|
|
480
949
|
}
|
|
481
950
|
|
|
482
951
|
function appendAssistantMessage(
|
|
@@ -516,7 +985,7 @@ function consumeQueuedUserMessage(
|
|
|
516
985
|
messages: Message[],
|
|
517
986
|
takeQueuedUserMessage: RunAgentOpts["takeQueuedUserMessage"],
|
|
518
987
|
onEvent: RunAgentOpts["onEvent"],
|
|
519
|
-
):
|
|
988
|
+
): AppendedUserTurn | null {
|
|
520
989
|
const queuedUserMessage = takeQueuedUserMessage?.();
|
|
521
990
|
if (!queuedUserMessage) {
|
|
522
991
|
return null;
|
|
@@ -596,6 +1065,7 @@ function appendToolResultMessage(
|
|
|
596
1065
|
toolCallId: toolCall.id,
|
|
597
1066
|
toolName: toolCall.name,
|
|
598
1067
|
content: result.content,
|
|
1068
|
+
...(result.details !== undefined ? { details: result.details } : {}),
|
|
599
1069
|
isError: result.isError,
|
|
600
1070
|
timestamp: Date.now(),
|
|
601
1071
|
};
|
|
@@ -605,8 +1075,14 @@ function appendToolResultMessage(
|
|
|
605
1075
|
onEvent?.({ type: "tool_result", message: toolResultMessage });
|
|
606
1076
|
}
|
|
607
1077
|
|
|
608
|
-
function getIncompleteTodos(
|
|
609
|
-
|
|
1078
|
+
function getIncompleteTodos(
|
|
1079
|
+
db: Database,
|
|
1080
|
+
sessionId: string,
|
|
1081
|
+
messages: readonly Message[],
|
|
1082
|
+
) {
|
|
1083
|
+
return getTodoItems([...loadMessages(db, sessionId), ...messages]).filter(
|
|
1084
|
+
(todo) => todo.status !== "completed",
|
|
1085
|
+
);
|
|
610
1086
|
}
|
|
611
1087
|
|
|
612
1088
|
function getTodoReminderSignature(
|
|
@@ -628,10 +1104,12 @@ function wrapSystemReminder(content: string): string {
|
|
|
628
1104
|
}
|
|
629
1105
|
|
|
630
1106
|
function createTodoReminderMessage(
|
|
1107
|
+
db: Database,
|
|
1108
|
+
sessionId: string,
|
|
631
1109
|
messages: readonly Message[],
|
|
632
1110
|
remindedTodoSignatures: Set<string>,
|
|
633
1111
|
): UserMessage | null {
|
|
634
|
-
const incompleteTodos = getIncompleteTodos(messages);
|
|
1112
|
+
const incompleteTodos = getIncompleteTodos(db, sessionId, messages);
|
|
635
1113
|
if (incompleteTodos.length === 0) {
|
|
636
1114
|
return null;
|
|
637
1115
|
}
|
|
@@ -663,6 +1141,8 @@ function createTodoReminderMessage(
|
|
|
663
1141
|
interface StoppedAssistantResolution {
|
|
664
1142
|
/** Next turn number when a queued steering message was consumed. */
|
|
665
1143
|
nextTurn: number | null;
|
|
1144
|
+
/** Updated current user request when a queued steering message starts a new turn. */
|
|
1145
|
+
nextCurrentUserRequest: string | null;
|
|
666
1146
|
/** Ephemeral context messages to include on the next model request. */
|
|
667
1147
|
pendingContextMessages: Message[];
|
|
668
1148
|
/** Final loop result when the turn should stop immediately. */
|
|
@@ -687,19 +1167,23 @@ function resolveStoppedAssistantMessage(
|
|
|
687
1167
|
);
|
|
688
1168
|
if (queuedTurn !== null) {
|
|
689
1169
|
return {
|
|
690
|
-
nextTurn: queuedTurn,
|
|
1170
|
+
nextTurn: queuedTurn.turn,
|
|
1171
|
+
nextCurrentUserRequest: queuedTurn.currentUserRequest,
|
|
691
1172
|
pendingContextMessages: [],
|
|
692
1173
|
finalResult: null,
|
|
693
1174
|
};
|
|
694
1175
|
}
|
|
695
1176
|
|
|
696
1177
|
const todoReminder = createTodoReminderMessage(
|
|
1178
|
+
opts.db,
|
|
1179
|
+
opts.sessionId,
|
|
697
1180
|
opts.messages,
|
|
698
1181
|
remindedTodoSignatures,
|
|
699
1182
|
);
|
|
700
1183
|
if (todoReminder) {
|
|
701
1184
|
return {
|
|
702
1185
|
nextTurn: null,
|
|
1186
|
+
nextCurrentUserRequest: null,
|
|
703
1187
|
pendingContextMessages: [todoReminder],
|
|
704
1188
|
finalResult: null,
|
|
705
1189
|
};
|
|
@@ -708,6 +1192,7 @@ function resolveStoppedAssistantMessage(
|
|
|
708
1192
|
opts.onEvent?.({ type: "done", message: assistantMessage });
|
|
709
1193
|
return {
|
|
710
1194
|
nextTurn: null,
|
|
1195
|
+
nextCurrentUserRequest: null,
|
|
711
1196
|
pendingContextMessages: [],
|
|
712
1197
|
finalResult: {
|
|
713
1198
|
messages: opts.messages,
|
|
@@ -763,6 +1248,8 @@ interface AgentIterationOutcome {
|
|
|
763
1248
|
finalResult: AgentLoopResult | null;
|
|
764
1249
|
/** Next turn number when a queued steering message starts a new turn. */
|
|
765
1250
|
nextTurn: number;
|
|
1251
|
+
/** Current user request text that should anchor any future compactions. */
|
|
1252
|
+
nextCurrentUserRequest: string;
|
|
766
1253
|
/** Ephemeral context messages for the next model request. */
|
|
767
1254
|
pendingContextMessages: Message[];
|
|
768
1255
|
}
|
|
@@ -770,6 +1257,7 @@ interface AgentIterationOutcome {
|
|
|
770
1257
|
async function resolveAgentIteration(
|
|
771
1258
|
assistantMessage: AssistantMessage,
|
|
772
1259
|
currentTurn: number,
|
|
1260
|
+
currentUserRequest: string,
|
|
773
1261
|
remindedTodoSignatures: Set<string>,
|
|
774
1262
|
opts: Pick<
|
|
775
1263
|
RunAgentOpts,
|
|
@@ -793,6 +1281,7 @@ async function resolveAgentIteration(
|
|
|
793
1281
|
return {
|
|
794
1282
|
finalResult: stopResult,
|
|
795
1283
|
nextTurn: currentTurn,
|
|
1284
|
+
nextCurrentUserRequest: currentUserRequest,
|
|
796
1285
|
pendingContextMessages: [],
|
|
797
1286
|
};
|
|
798
1287
|
}
|
|
@@ -816,6 +1305,8 @@ async function resolveAgentIteration(
|
|
|
816
1305
|
return {
|
|
817
1306
|
finalResult: stopResolution.finalResult,
|
|
818
1307
|
nextTurn: stopResolution.nextTurn ?? currentTurn,
|
|
1308
|
+
nextCurrentUserRequest:
|
|
1309
|
+
stopResolution.nextCurrentUserRequest ?? currentUserRequest,
|
|
819
1310
|
pendingContextMessages: stopResolution.pendingContextMessages,
|
|
820
1311
|
};
|
|
821
1312
|
}
|
|
@@ -829,6 +1320,7 @@ async function resolveAgentIteration(
|
|
|
829
1320
|
return {
|
|
830
1321
|
finalResult: toolStopResult,
|
|
831
1322
|
nextTurn: currentTurn,
|
|
1323
|
+
nextCurrentUserRequest: currentUserRequest,
|
|
832
1324
|
pendingContextMessages: [],
|
|
833
1325
|
};
|
|
834
1326
|
}
|
|
@@ -842,7 +1334,9 @@ async function resolveAgentIteration(
|
|
|
842
1334
|
);
|
|
843
1335
|
return {
|
|
844
1336
|
finalResult: null,
|
|
845
|
-
nextTurn: queuedTurn ?? currentTurn,
|
|
1337
|
+
nextTurn: queuedTurn?.turn ?? currentTurn,
|
|
1338
|
+
nextCurrentUserRequest:
|
|
1339
|
+
queuedTurn?.currentUserRequest ?? currentUserRequest,
|
|
846
1340
|
pendingContextMessages: [],
|
|
847
1341
|
};
|
|
848
1342
|
}
|
|
@@ -870,18 +1364,35 @@ export async function runAgentLoop(
|
|
|
870
1364
|
sessionId,
|
|
871
1365
|
turn,
|
|
872
1366
|
model,
|
|
873
|
-
messages,
|
|
1367
|
+
messages: initialMessages,
|
|
874
1368
|
signal,
|
|
875
1369
|
onEvent,
|
|
876
1370
|
toolHandlers,
|
|
877
1371
|
cwd,
|
|
878
1372
|
takeQueuedUserMessage,
|
|
879
1373
|
} = opts;
|
|
1374
|
+
let messages = initialMessages;
|
|
880
1375
|
let currentTurn = turn;
|
|
1376
|
+
let currentUserRequest = extractCurrentUserRequest(initialMessages);
|
|
881
1377
|
let pendingContextMessages: Message[] = [];
|
|
882
1378
|
const remindedTodoSignatures = new Set<string>();
|
|
883
1379
|
|
|
884
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
|
+
|
|
885
1396
|
const assistantMessage = await streamAssistantMessage({
|
|
886
1397
|
...opts,
|
|
887
1398
|
messages:
|
|
@@ -902,6 +1413,7 @@ export async function runAgentLoop(
|
|
|
902
1413
|
const iterationOutcome = await resolveAgentIteration(
|
|
903
1414
|
assistantMessage,
|
|
904
1415
|
currentTurn,
|
|
1416
|
+
currentUserRequest,
|
|
905
1417
|
remindedTodoSignatures,
|
|
906
1418
|
{
|
|
907
1419
|
db,
|
|
@@ -920,6 +1432,7 @@ export async function runAgentLoop(
|
|
|
920
1432
|
}
|
|
921
1433
|
|
|
922
1434
|
currentTurn = iterationOutcome.nextTurn;
|
|
1435
|
+
currentUserRequest = iterationOutcome.nextCurrentUserRequest;
|
|
923
1436
|
pendingContextMessages = iterationOutcome.pendingContextMessages;
|
|
924
1437
|
}
|
|
925
1438
|
}
|