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/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 { appendMessage } from "./session.ts";
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(opts.systemPrompt, opts.messages, opts.tools),
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
- ): number {
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 turn;
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
- ): number | null {
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(messages: readonly Message[]) {
609
- return getTodoItems(messages).filter((todo) => todo.status !== "completed");
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
  }