mini-coder 0.5.13 → 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/PROGRESS.md +3 -2
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/agent.ts +506 -13
- package/src/assistant-output.ts +73 -0
- package/src/delegation.ts +238 -0
- package/src/headless.ts +12 -39
- package/src/index.ts +191 -11
- package/src/prompt.ts +9 -1
- package/src/session-message.ts +57 -65
- package/src/session.ts +389 -42
- package/src/submit.ts +7 -2
- package/src/tool-delegate.ts +125 -0
- package/src/tool-shell.ts +52 -2
- package/src/tools.ts +331 -6
- package/src/ui/agent.ts +3 -0
- package/src/ui/commands.test.ts +50 -6
- package/src/ui/commands.ts +14 -0
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;
|
|
@@ -446,7 +881,12 @@ async function streamAssistantMessage(
|
|
|
446
881
|
): Promise<AssistantMessage> {
|
|
447
882
|
const eventStream = streamSimple(
|
|
448
883
|
opts.model,
|
|
449
|
-
buildAgentContext(
|
|
884
|
+
buildAgentContext(
|
|
885
|
+
opts.systemPrompt,
|
|
886
|
+
opts.messages,
|
|
887
|
+
opts.tools,
|
|
888
|
+
opts.model.input.includes("image"),
|
|
889
|
+
),
|
|
450
890
|
buildStreamOptions(opts.apiKey, opts.effort, opts.signal),
|
|
451
891
|
);
|
|
452
892
|
const streamResult = eventStream.result();
|
|
@@ -485,17 +925,27 @@ async function streamAssistantMessage(
|
|
|
485
925
|
return mergeAssistantMessage(partialAssistantMessage, finalAssistantMessage);
|
|
486
926
|
}
|
|
487
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
|
+
|
|
488
935
|
function appendUserMessage(
|
|
489
936
|
db: Database,
|
|
490
937
|
sessionId: string,
|
|
491
938
|
messages: Message[],
|
|
492
939
|
userMessage: UserMessage,
|
|
493
940
|
onEvent: RunAgentOpts["onEvent"],
|
|
494
|
-
):
|
|
941
|
+
): AppendedUserTurn {
|
|
495
942
|
messages.push(userMessage);
|
|
496
943
|
const turn = appendMessage(db, sessionId, userMessage);
|
|
497
944
|
onEvent?.({ type: "user_message", message: userMessage });
|
|
498
|
-
return
|
|
945
|
+
return {
|
|
946
|
+
turn,
|
|
947
|
+
currentUserRequest: resolveCurrentUserRequest(userMessage),
|
|
948
|
+
};
|
|
499
949
|
}
|
|
500
950
|
|
|
501
951
|
function appendAssistantMessage(
|
|
@@ -535,7 +985,7 @@ function consumeQueuedUserMessage(
|
|
|
535
985
|
messages: Message[],
|
|
536
986
|
takeQueuedUserMessage: RunAgentOpts["takeQueuedUserMessage"],
|
|
537
987
|
onEvent: RunAgentOpts["onEvent"],
|
|
538
|
-
):
|
|
988
|
+
): AppendedUserTurn | null {
|
|
539
989
|
const queuedUserMessage = takeQueuedUserMessage?.();
|
|
540
990
|
if (!queuedUserMessage) {
|
|
541
991
|
return null;
|
|
@@ -625,8 +1075,14 @@ function appendToolResultMessage(
|
|
|
625
1075
|
onEvent?.({ type: "tool_result", message: toolResultMessage });
|
|
626
1076
|
}
|
|
627
1077
|
|
|
628
|
-
function getIncompleteTodos(
|
|
629
|
-
|
|
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
|
+
);
|
|
630
1086
|
}
|
|
631
1087
|
|
|
632
1088
|
function getTodoReminderSignature(
|
|
@@ -648,10 +1104,12 @@ function wrapSystemReminder(content: string): string {
|
|
|
648
1104
|
}
|
|
649
1105
|
|
|
650
1106
|
function createTodoReminderMessage(
|
|
1107
|
+
db: Database,
|
|
1108
|
+
sessionId: string,
|
|
651
1109
|
messages: readonly Message[],
|
|
652
1110
|
remindedTodoSignatures: Set<string>,
|
|
653
1111
|
): UserMessage | null {
|
|
654
|
-
const incompleteTodos = getIncompleteTodos(messages);
|
|
1112
|
+
const incompleteTodos = getIncompleteTodos(db, sessionId, messages);
|
|
655
1113
|
if (incompleteTodos.length === 0) {
|
|
656
1114
|
return null;
|
|
657
1115
|
}
|
|
@@ -683,6 +1141,8 @@ function createTodoReminderMessage(
|
|
|
683
1141
|
interface StoppedAssistantResolution {
|
|
684
1142
|
/** Next turn number when a queued steering message was consumed. */
|
|
685
1143
|
nextTurn: number | null;
|
|
1144
|
+
/** Updated current user request when a queued steering message starts a new turn. */
|
|
1145
|
+
nextCurrentUserRequest: string | null;
|
|
686
1146
|
/** Ephemeral context messages to include on the next model request. */
|
|
687
1147
|
pendingContextMessages: Message[];
|
|
688
1148
|
/** Final loop result when the turn should stop immediately. */
|
|
@@ -707,19 +1167,23 @@ function resolveStoppedAssistantMessage(
|
|
|
707
1167
|
);
|
|
708
1168
|
if (queuedTurn !== null) {
|
|
709
1169
|
return {
|
|
710
|
-
nextTurn: queuedTurn,
|
|
1170
|
+
nextTurn: queuedTurn.turn,
|
|
1171
|
+
nextCurrentUserRequest: queuedTurn.currentUserRequest,
|
|
711
1172
|
pendingContextMessages: [],
|
|
712
1173
|
finalResult: null,
|
|
713
1174
|
};
|
|
714
1175
|
}
|
|
715
1176
|
|
|
716
1177
|
const todoReminder = createTodoReminderMessage(
|
|
1178
|
+
opts.db,
|
|
1179
|
+
opts.sessionId,
|
|
717
1180
|
opts.messages,
|
|
718
1181
|
remindedTodoSignatures,
|
|
719
1182
|
);
|
|
720
1183
|
if (todoReminder) {
|
|
721
1184
|
return {
|
|
722
1185
|
nextTurn: null,
|
|
1186
|
+
nextCurrentUserRequest: null,
|
|
723
1187
|
pendingContextMessages: [todoReminder],
|
|
724
1188
|
finalResult: null,
|
|
725
1189
|
};
|
|
@@ -728,6 +1192,7 @@ function resolveStoppedAssistantMessage(
|
|
|
728
1192
|
opts.onEvent?.({ type: "done", message: assistantMessage });
|
|
729
1193
|
return {
|
|
730
1194
|
nextTurn: null,
|
|
1195
|
+
nextCurrentUserRequest: null,
|
|
731
1196
|
pendingContextMessages: [],
|
|
732
1197
|
finalResult: {
|
|
733
1198
|
messages: opts.messages,
|
|
@@ -783,6 +1248,8 @@ interface AgentIterationOutcome {
|
|
|
783
1248
|
finalResult: AgentLoopResult | null;
|
|
784
1249
|
/** Next turn number when a queued steering message starts a new turn. */
|
|
785
1250
|
nextTurn: number;
|
|
1251
|
+
/** Current user request text that should anchor any future compactions. */
|
|
1252
|
+
nextCurrentUserRequest: string;
|
|
786
1253
|
/** Ephemeral context messages for the next model request. */
|
|
787
1254
|
pendingContextMessages: Message[];
|
|
788
1255
|
}
|
|
@@ -790,6 +1257,7 @@ interface AgentIterationOutcome {
|
|
|
790
1257
|
async function resolveAgentIteration(
|
|
791
1258
|
assistantMessage: AssistantMessage,
|
|
792
1259
|
currentTurn: number,
|
|
1260
|
+
currentUserRequest: string,
|
|
793
1261
|
remindedTodoSignatures: Set<string>,
|
|
794
1262
|
opts: Pick<
|
|
795
1263
|
RunAgentOpts,
|
|
@@ -813,6 +1281,7 @@ async function resolveAgentIteration(
|
|
|
813
1281
|
return {
|
|
814
1282
|
finalResult: stopResult,
|
|
815
1283
|
nextTurn: currentTurn,
|
|
1284
|
+
nextCurrentUserRequest: currentUserRequest,
|
|
816
1285
|
pendingContextMessages: [],
|
|
817
1286
|
};
|
|
818
1287
|
}
|
|
@@ -836,6 +1305,8 @@ async function resolveAgentIteration(
|
|
|
836
1305
|
return {
|
|
837
1306
|
finalResult: stopResolution.finalResult,
|
|
838
1307
|
nextTurn: stopResolution.nextTurn ?? currentTurn,
|
|
1308
|
+
nextCurrentUserRequest:
|
|
1309
|
+
stopResolution.nextCurrentUserRequest ?? currentUserRequest,
|
|
839
1310
|
pendingContextMessages: stopResolution.pendingContextMessages,
|
|
840
1311
|
};
|
|
841
1312
|
}
|
|
@@ -849,6 +1320,7 @@ async function resolveAgentIteration(
|
|
|
849
1320
|
return {
|
|
850
1321
|
finalResult: toolStopResult,
|
|
851
1322
|
nextTurn: currentTurn,
|
|
1323
|
+
nextCurrentUserRequest: currentUserRequest,
|
|
852
1324
|
pendingContextMessages: [],
|
|
853
1325
|
};
|
|
854
1326
|
}
|
|
@@ -862,7 +1334,9 @@ async function resolveAgentIteration(
|
|
|
862
1334
|
);
|
|
863
1335
|
return {
|
|
864
1336
|
finalResult: null,
|
|
865
|
-
nextTurn: queuedTurn ?? currentTurn,
|
|
1337
|
+
nextTurn: queuedTurn?.turn ?? currentTurn,
|
|
1338
|
+
nextCurrentUserRequest:
|
|
1339
|
+
queuedTurn?.currentUserRequest ?? currentUserRequest,
|
|
866
1340
|
pendingContextMessages: [],
|
|
867
1341
|
};
|
|
868
1342
|
}
|
|
@@ -890,18 +1364,35 @@ export async function runAgentLoop(
|
|
|
890
1364
|
sessionId,
|
|
891
1365
|
turn,
|
|
892
1366
|
model,
|
|
893
|
-
messages,
|
|
1367
|
+
messages: initialMessages,
|
|
894
1368
|
signal,
|
|
895
1369
|
onEvent,
|
|
896
1370
|
toolHandlers,
|
|
897
1371
|
cwd,
|
|
898
1372
|
takeQueuedUserMessage,
|
|
899
1373
|
} = opts;
|
|
1374
|
+
let messages = initialMessages;
|
|
900
1375
|
let currentTurn = turn;
|
|
1376
|
+
let currentUserRequest = extractCurrentUserRequest(initialMessages);
|
|
901
1377
|
let pendingContextMessages: Message[] = [];
|
|
902
1378
|
const remindedTodoSignatures = new Set<string>();
|
|
903
1379
|
|
|
904
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
|
+
|
|
905
1396
|
const assistantMessage = await streamAssistantMessage({
|
|
906
1397
|
...opts,
|
|
907
1398
|
messages:
|
|
@@ -922,6 +1413,7 @@ export async function runAgentLoop(
|
|
|
922
1413
|
const iterationOutcome = await resolveAgentIteration(
|
|
923
1414
|
assistantMessage,
|
|
924
1415
|
currentTurn,
|
|
1416
|
+
currentUserRequest,
|
|
925
1417
|
remindedTodoSignatures,
|
|
926
1418
|
{
|
|
927
1419
|
db,
|
|
@@ -940,6 +1432,7 @@ export async function runAgentLoop(
|
|
|
940
1432
|
}
|
|
941
1433
|
|
|
942
1434
|
currentTurn = iterationOutcome.nextTurn;
|
|
1435
|
+
currentUserRequest = iterationOutcome.nextCurrentUserRequest;
|
|
943
1436
|
pendingContextMessages = iterationOutcome.pendingContextMessages;
|
|
944
1437
|
}
|
|
945
1438
|
}
|