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/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;
@@ -446,7 +881,12 @@ async function streamAssistantMessage(
446
881
  ): Promise<AssistantMessage> {
447
882
  const eventStream = streamSimple(
448
883
  opts.model,
449
- 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
+ ),
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
- ): number {
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 turn;
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
- ): number | null {
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(messages: readonly Message[]) {
629
- 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
+ );
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
  }