@librechat/agents 3.7.0 → 3.7.1

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.
Files changed (87) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +11 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +7 -2
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/llm/invoke.cjs +56 -9
  6. package/dist/cjs/llm/invoke.cjs.map +1 -1
  7. package/dist/cjs/main.cjs +11 -1
  8. package/dist/cjs/messages/content.cjs +8 -5
  9. package/dist/cjs/messages/content.cjs.map +1 -1
  10. package/dist/cjs/messages/format.cjs +15 -5
  11. package/dist/cjs/messages/format.cjs.map +1 -1
  12. package/dist/cjs/messages/index.cjs +2 -1
  13. package/dist/cjs/messages/projectionInvariant.cjs +74 -0
  14. package/dist/cjs/messages/projectionInvariant.cjs.map +1 -0
  15. package/dist/cjs/messages/provenance.cjs +30 -8
  16. package/dist/cjs/messages/provenance.cjs.map +1 -1
  17. package/dist/cjs/messages/recency.cjs +202 -7
  18. package/dist/cjs/messages/recency.cjs.map +1 -1
  19. package/dist/cjs/messages/toolResultTypes.cjs +1 -0
  20. package/dist/cjs/session/AgentSession.cjs +4 -23
  21. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  22. package/dist/cjs/session/deriveMessages.cjs +25 -0
  23. package/dist/cjs/session/deriveMessages.cjs.map +1 -0
  24. package/dist/cjs/session/index.cjs +1 -0
  25. package/dist/cjs/summarization/node.cjs +13 -6
  26. package/dist/cjs/summarization/node.cjs.map +1 -1
  27. package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs +65 -8
  28. package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs.map +1 -1
  29. package/dist/esm/agents/AgentContext.mjs +11 -2
  30. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  31. package/dist/esm/graphs/Graph.mjs +8 -3
  32. package/dist/esm/graphs/Graph.mjs.map +1 -1
  33. package/dist/esm/llm/invoke.mjs +56 -9
  34. package/dist/esm/llm/invoke.mjs.map +1 -1
  35. package/dist/esm/main.mjs +6 -4
  36. package/dist/esm/messages/content.mjs +8 -6
  37. package/dist/esm/messages/content.mjs.map +1 -1
  38. package/dist/esm/messages/format.mjs +16 -6
  39. package/dist/esm/messages/format.mjs.map +1 -1
  40. package/dist/esm/messages/index.mjs +2 -1
  41. package/dist/esm/messages/projectionInvariant.mjs +72 -0
  42. package/dist/esm/messages/projectionInvariant.mjs.map +1 -0
  43. package/dist/esm/messages/provenance.mjs +30 -9
  44. package/dist/esm/messages/provenance.mjs.map +1 -1
  45. package/dist/esm/messages/recency.mjs +201 -8
  46. package/dist/esm/messages/recency.mjs.map +1 -1
  47. package/dist/esm/messages/toolResultTypes.mjs +1 -1
  48. package/dist/esm/session/AgentSession.mjs +4 -23
  49. package/dist/esm/session/AgentSession.mjs.map +1 -1
  50. package/dist/esm/session/deriveMessages.mjs +25 -0
  51. package/dist/esm/session/deriveMessages.mjs.map +1 -0
  52. package/dist/esm/session/index.mjs +1 -0
  53. package/dist/esm/summarization/node.mjs +14 -7
  54. package/dist/esm/summarization/node.mjs.map +1 -1
  55. package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs +65 -8
  56. package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs.map +1 -1
  57. package/dist/types/agents/AgentContext.d.ts +6 -1
  58. package/dist/types/messages/content.d.ts +4 -1
  59. package/dist/types/messages/format.d.ts +6 -0
  60. package/dist/types/messages/index.d.ts +1 -0
  61. package/dist/types/messages/projectionInvariant.d.ts +25 -0
  62. package/dist/types/messages/provenance.d.ts +10 -0
  63. package/dist/types/messages/recency.d.ts +30 -18
  64. package/dist/types/session/deriveMessages.d.ts +11 -0
  65. package/dist/types/session/index.d.ts +2 -0
  66. package/dist/types/tools/subagent/InMemorySubagentTaskStore.d.ts +13 -1
  67. package/dist/types/types/graph.d.ts +1 -1
  68. package/dist/types/types/subagentTasks.d.ts +22 -0
  69. package/dist/types/types/summarize.d.ts +15 -13
  70. package/package.json +3 -1
  71. package/src/agents/AgentContext.ts +19 -5
  72. package/src/graphs/Graph.ts +9 -1
  73. package/src/llm/invoke.ts +102 -23
  74. package/src/messages/content.ts +20 -10
  75. package/src/messages/format.ts +29 -5
  76. package/src/messages/index.ts +1 -0
  77. package/src/messages/projectionInvariant.ts +134 -0
  78. package/src/messages/provenance.ts +60 -18
  79. package/src/messages/recency.ts +429 -27
  80. package/src/session/AgentSession.ts +4 -30
  81. package/src/session/deriveMessages.ts +37 -0
  82. package/src/session/index.ts +2 -0
  83. package/src/summarization/node.ts +37 -15
  84. package/src/tools/subagent/InMemorySubagentTaskStore.ts +139 -6
  85. package/src/types/graph.ts +1 -0
  86. package/src/types/subagentTasks.ts +28 -0
  87. package/src/types/summarize.ts +15 -13
@@ -29,6 +29,7 @@ import {
29
29
  } from '@/messages/cache';
30
30
  import {
31
31
  DEFAULT_RETAIN_RECENT_TURNS,
32
+ resolveIntraTurnRetainTokens,
32
33
  splitAtRecencyBoundary,
33
34
  } from '@/messages/recency';
34
35
  import {
@@ -1091,14 +1092,22 @@ export function createSummarizeNode({
1091
1092
  const runnableConfig = config ?? graph.config;
1092
1093
 
1093
1094
  const retainRecent = agentContext.summarizationConfig?.retainRecent;
1094
- const { head: messagesToRefine, tailStartIndex } = splitAtRecencyBoundary(
1095
- restoredMessages,
1096
- {
1097
- turns: retainRecent?.turns ?? DEFAULT_RETAIN_RECENT_TURNS,
1095
+ const recencyTokenCounter =
1096
+ agentContext.contextPressureTokenCounts?.count ??
1097
+ agentContext.tokenCounter;
1098
+ const {
1099
+ head: messagesToRefine,
1100
+ tailStartIndex,
1101
+ usedIntraTurnFallback,
1102
+ } = splitAtRecencyBoundary(restoredMessages, {
1103
+ turns: retainRecent?.turns ?? DEFAULT_RETAIN_RECENT_TURNS,
1104
+ tokens: retainRecent?.tokens,
1105
+ tokenCounter: recencyTokenCounter,
1106
+ intraTurnTokens: resolveIntraTurnRetainTokens({
1098
1107
  tokens: retainRecent?.tokens,
1099
- tokenCounter: agentContext.tokenCounter,
1100
- }
1101
- );
1108
+ maxContextTokens: agentContext.maxContextTokens,
1109
+ }),
1110
+ });
1102
1111
  /**
1103
1112
  * Use the *masked* messages for the retained tail so that any
1104
1113
  * truncation prune applied to oversized ToolMessage content stays
@@ -1305,10 +1314,17 @@ export function createSummarizeNode({
1305
1314
  * supposed to preserve. Leave state untouched and let the provider error
1306
1315
  * surface instead.
1307
1316
  */
1308
- if (usedMetadataStub === true && request.reason === 'overflow') {
1317
+ if (
1318
+ usedMetadataStub === true &&
1319
+ (request.reason === 'overflow' || usedIntraTurnFallback)
1320
+ ) {
1321
+ const preservationReason =
1322
+ request.reason === 'overflow'
1323
+ ? 'overflow recovery'
1324
+ : 'intra-turn compaction';
1309
1325
  log(
1310
1326
  'warn',
1311
- 'Overflow summarization failed; keeping history rather than replacing it with a metadata stub'
1327
+ `Summarization failed during ${preservationReason}; keeping history rather than replacing it with a metadata stub`
1312
1328
  );
1313
1329
  agentContext.markSummarizationTriggered(state.messages.length);
1314
1330
  /**
@@ -1330,8 +1346,7 @@ export function createSummarizeNode({
1330
1346
  {
1331
1347
  id: stepId,
1332
1348
  agentId: request.agentId,
1333
- error:
1334
- 'Summarization failed during overflow recovery; conversation history was preserved',
1349
+ error: `Summarization failed during ${preservationReason}; conversation history was preserved`,
1335
1350
  } satisfies t.SummarizeCompleteEvent,
1336
1351
  runnableConfig
1337
1352
  );
@@ -1364,7 +1379,13 @@ export function createSummarizeNode({
1364
1379
  agentContext.tokenCounter
1365
1380
  );
1366
1381
 
1367
- agentContext.setSummary(summaryText, tokenCount);
1382
+ if (usedIntraTurnFallback) {
1383
+ agentContext.setSummary(summaryText, tokenCount, {
1384
+ precedesMessages: true,
1385
+ });
1386
+ } else {
1387
+ agentContext.setSummary(summaryText, tokenCount);
1388
+ }
1368
1389
 
1369
1390
  log('info', 'Summary persisted');
1370
1391
  log('debug', 'Summary details', {
@@ -1596,9 +1617,10 @@ function traceConfig(
1596
1617
 
1597
1618
  /**
1598
1619
  * Cache-friendly compaction: sends raw conversation messages with the
1599
- * summarization instruction appended as the final HumanMessage.
1600
- * Providers with prompt caching get a cache hit on the system prompt +
1601
- * tool definitions prefix.
1620
+ * summarization instruction appended as the final HumanMessage. Bound tool
1621
+ * definitions can reuse their cache prefix. Exact replay of the main request's
1622
+ * system + tools + messages prefix requires a routed-request projection that
1623
+ * this summarization path does not currently own.
1602
1624
  */
1603
1625
  async function summarizeWithCacheHit({
1604
1626
  model,
@@ -4,6 +4,7 @@ import type {
4
4
  SubagentTaskBoundary,
5
5
  SubagentTaskClaim,
6
6
  SubagentTaskControlCommand,
7
+ SubagentTaskControlReceipt,
7
8
  SubagentTaskControlResult,
8
9
  SubagentTaskProgress,
9
10
  SubagentTaskRuntime,
@@ -20,6 +21,7 @@ const DEFAULT_MAX_ERROR_CHARS = 4 * 1024;
20
21
  const DEFAULT_MAX_MESSAGE_CHARS = 64 * 1024;
21
22
  const DEFAULT_TASK_TIMEOUT_MS = 30 * 60 * 1000;
22
23
  const DEFAULT_MAX_CONTROLS = 32;
24
+ const DEFAULT_MAX_CONTROL_RECEIPTS = 64;
23
25
  const DEFAULT_MAX_RESULT_CHARS = 100_000;
24
26
  const DEFAULT_MAX_RUNNING_PER_SCOPE = 10;
25
27
  const DEFAULT_MAX_RUNNING_TOTAL = 100;
@@ -30,6 +32,7 @@ export interface InMemorySubagentTaskStoreOptions {
30
32
  completedTtlMs?: number;
31
33
  maxControlMessageChars?: number;
32
34
  maxControlsPerTask?: number;
35
+ maxControlReceiptsPerTask?: number;
33
36
  maxErrorChars?: number;
34
37
  maxResultChars?: number;
35
38
  maxRunningPerScope?: number;
@@ -57,6 +60,7 @@ type StoredTask = {
57
60
  updatedAt: number;
58
61
  controller: AbortController;
59
62
  controls: PendingControl[];
63
+ controlReceipts: Map<string, SubagentTaskControlReceipt>;
60
64
  progressEvents: number;
61
65
  resultClaimed: boolean;
62
66
  acceptingControls: boolean;
@@ -107,6 +111,13 @@ function resolveOptions(
107
111
  options.maxControlsPerTask,
108
112
  DEFAULT_MAX_CONTROLS
109
113
  ),
114
+ maxControlReceiptsPerTask: Math.max(
115
+ resolvePositiveInteger(
116
+ options.maxControlReceiptsPerTask,
117
+ DEFAULT_MAX_CONTROL_RECEIPTS
118
+ ),
119
+ resolvePositiveInteger(options.maxControlsPerTask, DEFAULT_MAX_CONTROLS)
120
+ ),
110
121
  maxErrorChars: resolvePositiveInteger(
111
122
  options.maxErrorChars,
112
123
  DEFAULT_MAX_ERROR_CHARS
@@ -177,11 +188,20 @@ function snapshot(task: StoredTask): SubagentTaskSnapshot {
177
188
  task.status === 'completed' && task.result != null && !task.resultClaimed,
178
189
  resultClaimed: task.resultClaimed,
179
190
  pendingControls: task.controls.length,
191
+ controlReceipts: [...task.controlReceipts.values()].map((receipt) => ({
192
+ ...receipt,
193
+ })),
180
194
  ...(task.progress == null ? {} : { progress: { ...task.progress } }),
181
195
  ...(task.error == null ? {} : { error: task.error }),
182
196
  };
183
197
  }
184
198
 
199
+ function cloneControlReceipt(
200
+ receipt: SubagentTaskControlReceipt
201
+ ): SubagentTaskControlReceipt {
202
+ return { ...receipt };
203
+ }
204
+
185
205
  function abortReason(signal: AbortSignal): Error {
186
206
  return signal.reason instanceof Error
187
207
  ? signal.reason
@@ -206,6 +226,34 @@ export class InMemorySubagentTaskStore implements SubagentTaskStore {
206
226
  this.options = resolveOptions(options);
207
227
  }
208
228
 
229
+ /**
230
+ * Payload-free transition seam for hosts that durably project authoritative
231
+ * receipts. Implementations must return synchronously, must not reproduce
232
+ * task-store transition rules, and cannot veto task-store state transitions:
233
+ * hook failures are deliberately isolated by the caller.
234
+ */
235
+ protected onControlReceipt(
236
+ _scopeId: string,
237
+ _taskId: string,
238
+ _receipt: SubagentTaskControlReceipt
239
+ ): void {}
240
+
241
+ private emitControlReceipt(
242
+ task: StoredTask,
243
+ receipt: SubagentTaskControlReceipt
244
+ ): void {
245
+ try {
246
+ this.onControlReceipt(
247
+ task.scopeId,
248
+ task.id,
249
+ cloneControlReceipt(receipt)
250
+ );
251
+ } catch {
252
+ // A host projection is observability only. Task admission, draining, and
253
+ // settlement must remain correct when that projection is unavailable.
254
+ }
255
+ }
256
+
209
257
  start(request: SubagentTaskStartRequest): SubagentTaskStartResult {
210
258
  const scopeId = request.scopeId.trim();
211
259
  const idempotencyKey = request.idempotencyKey.trim();
@@ -272,6 +320,7 @@ export class InMemorySubagentTaskStore implements SubagentTaskStore {
272
320
  updatedAt: now,
273
321
  controller: new AbortController(),
274
322
  controls: [],
323
+ controlReceipts: new Map(),
275
324
  progressEvents: 0,
276
325
  resultClaimed: false,
277
326
  acceptingControls: true,
@@ -312,6 +361,7 @@ export class InMemorySubagentTaskStore implements SubagentTaskStore {
312
361
  }
313
362
  task.status = 'completed';
314
363
  task.acceptingControls = false;
364
+ this.failPendingControls(task, 'task_completed');
315
365
  task.controls.length = 0;
316
366
  task.result = truncateMiddle(
317
367
  result.content,
@@ -399,9 +449,16 @@ export class InMemorySubagentTaskStore implements SubagentTaskStore {
399
449
  if (index < 0) {
400
450
  return { status: 'control_not_found', task: snapshot(task) };
401
451
  }
402
- task.controls.splice(index, 1);
452
+ const [control] = task.controls.splice(index, 1);
403
453
  task.updatedAt = Date.now();
404
- return { status: 'accepted', task: snapshot(task) };
454
+ this.transitionControl(task, control.id, 'rejected', {
455
+ reason: 'withdrawn',
456
+ });
457
+ return {
458
+ status: 'accepted',
459
+ task: snapshot(task),
460
+ controlId: control.id,
461
+ };
405
462
  }
406
463
  const message = command.message.trim();
407
464
  if (message === '') {
@@ -419,6 +476,12 @@ export class InMemorySubagentTaskStore implements SubagentTaskStore {
419
476
  message: `Task already has ${this.options.maxControlsPerTask} pending messages.`,
420
477
  };
421
478
  }
479
+ if (!this.makeRoomForControlReceipt(task)) {
480
+ return {
481
+ status: 'invalid',
482
+ message: 'Task control receipt capacity is unavailable.',
483
+ };
484
+ }
422
485
  const control: PendingControl = {
423
486
  id: nanoid(),
424
487
  action: command.action,
@@ -426,6 +489,15 @@ export class InMemorySubagentTaskStore implements SubagentTaskStore {
426
489
  };
427
490
  task.controls.push(control);
428
491
  task.updatedAt = Date.now();
492
+ const receipt: SubagentTaskControlReceipt = {
493
+ controlId: control.id,
494
+ action: control.action,
495
+ status: 'accepted',
496
+ createdAt: task.updatedAt,
497
+ updatedAt: task.updatedAt,
498
+ };
499
+ task.controlReceipts.set(control.id, receipt);
500
+ this.emitControlReceipt(task, receipt);
429
501
  return {
430
502
  status: 'accepted',
431
503
  task: snapshot(task),
@@ -525,6 +597,59 @@ export class InMemorySubagentTaskStore implements SubagentTaskStore {
525
597
  }
526
598
  }
527
599
 
600
+ private makeRoomForControlReceipt(task: StoredTask): boolean {
601
+ while (
602
+ task.controlReceipts.size >= this.options.maxControlReceiptsPerTask
603
+ ) {
604
+ const terminal = [...task.controlReceipts].find(
605
+ ([, receipt]) => receipt.status !== 'accepted'
606
+ );
607
+ if (terminal == null) {
608
+ return false;
609
+ }
610
+ task.controlReceipts.delete(terminal[0]);
611
+ }
612
+ return true;
613
+ }
614
+
615
+ private transitionControl(
616
+ task: StoredTask,
617
+ controlId: string,
618
+ status: Exclude<SubagentTaskControlReceipt['status'], 'accepted'>,
619
+ detail: Pick<SubagentTaskControlReceipt, 'boundary' | 'reason'> = {}
620
+ ): void {
621
+ const receipt = task.controlReceipts.get(controlId);
622
+ if (receipt == null || receipt.status !== 'accepted') {
623
+ return;
624
+ }
625
+ const transitioned: SubagentTaskControlReceipt = {
626
+ ...receipt,
627
+ status,
628
+ updatedAt: Date.now(),
629
+ ...(detail.boundary == null ? {} : { boundary: detail.boundary }),
630
+ ...(detail.reason == null ? {} : { reason: detail.reason }),
631
+ };
632
+ task.controlReceipts.set(controlId, transitioned);
633
+ this.emitControlReceipt(task, transitioned);
634
+ }
635
+
636
+ private failPendingControls(
637
+ task: StoredTask,
638
+ reason: Extract<
639
+ NonNullable<SubagentTaskControlReceipt['reason']>,
640
+ 'task_completed' | 'task_cancelled' | 'task_failed'
641
+ >
642
+ ): void {
643
+ for (const control of task.controls) {
644
+ this.transitionControl(
645
+ task,
646
+ control.id,
647
+ reason === 'task_failed' ? 'failed' : 'rejected',
648
+ { reason }
649
+ );
650
+ }
651
+ }
652
+
528
653
  private scheduleExpiry(task: StoredTask): void {
529
654
  this.clearTaskTimeout(task);
530
655
  this.clearTaskExpiry(task);
@@ -566,6 +691,10 @@ export class InMemorySubagentTaskStore implements SubagentTaskStore {
566
691
  task.status = status;
567
692
  this.runningTasks -= 1;
568
693
  task.acceptingControls = false;
694
+ this.failPendingControls(
695
+ task,
696
+ status === 'cancelled' ? 'task_cancelled' : 'task_failed'
697
+ );
569
698
  task.controls.length = 0;
570
699
  task.error = message;
571
700
  task.updatedAt = Date.now();
@@ -575,6 +704,7 @@ export class InMemorySubagentTaskStore implements SubagentTaskStore {
575
704
 
576
705
  private createRuntime(task: StoredTask): SubagentTaskRuntime {
577
706
  const take = (
707
+ boundary: SubagentTaskBoundary,
578
708
  accept: (control: PendingControl) => boolean
579
709
  ): InjectedMessage[] => {
580
710
  if (task.status !== 'running') {
@@ -588,6 +718,9 @@ export class InMemorySubagentTaskStore implements SubagentTaskStore {
588
718
  task.controls = retained;
589
719
  if (selected.length > 0) {
590
720
  task.updatedAt = Date.now();
721
+ for (const control of selected) {
722
+ this.transitionControl(task, control.id, 'applied', { boundary });
723
+ }
591
724
  }
592
725
  return selected.map(toInjectedMessage);
593
726
  };
@@ -599,15 +732,15 @@ export class InMemorySubagentTaskStore implements SubagentTaskStore {
599
732
  task.controls.some((control) => control.action === 'interrupt'),
600
733
  drain: (boundary: SubagentTaskBoundary): InjectedMessage[] => {
601
734
  if (boundary === 'preempt') {
602
- return take((control) => control.action === 'interrupt');
735
+ return take(boundary, (control) => control.action === 'interrupt');
603
736
  }
604
737
  if (boundary === 'tool') {
605
- return take((control) => control.action !== 'queue');
738
+ return take(boundary, (control) => control.action !== 'queue');
606
739
  }
607
- return take(() => true);
740
+ return take(boundary, () => true);
608
741
  },
609
742
  closeTurn: (): { closed: boolean; messages: InjectedMessage[] } => {
610
- const messages = take(() => true);
743
+ const messages = take('turn', () => true);
611
744
  if (messages.length > 0) {
612
745
  return { closed: false, messages };
613
746
  }
@@ -614,6 +614,7 @@ export type SubagentUpdatePhase =
614
614
  | 'run_step_closed'
615
615
  | 'message_delta'
616
616
  | 'reasoning_delta'
617
+ | 'control'
617
618
  | 'stop'
618
619
  | 'error';
619
620
 
@@ -12,6 +12,28 @@ export type SubagentTaskStatus =
12
12
  /** Where a pending parent message may enter the child run. */
13
13
  export type SubagentTaskBoundary = 'preempt' | 'tool' | 'turn';
14
14
 
15
+ /**
16
+ * Lifecycle of one parent-to-child message after the task store accepts it.
17
+ * Hosts may render a transient `submitted` state before this authoritative
18
+ * receipt exists; that transport state is intentionally not persisted here.
19
+ */
20
+ export type SubagentTaskControlReceiptStatus =
21
+ | 'accepted'
22
+ | 'applied'
23
+ | 'rejected'
24
+ | 'failed';
25
+
26
+ /** Bounded authoritative receipt for one steer, queue, or interrupt command. */
27
+ export interface SubagentTaskControlReceipt {
28
+ controlId: string;
29
+ action: 'steer' | 'queue' | 'interrupt';
30
+ status: SubagentTaskControlReceiptStatus;
31
+ createdAt: number;
32
+ updatedAt: number;
33
+ boundary?: SubagentTaskBoundary;
34
+ reason?: 'withdrawn' | 'task_completed' | 'task_cancelled' | 'task_failed';
35
+ }
36
+
15
37
  /** Parent-to-child control operations accepted while a task is running. */
16
38
  export type SubagentTaskControlCommand =
17
39
  | { action: 'steer' | 'queue' | 'interrupt'; message: string }
@@ -42,6 +64,12 @@ export interface SubagentTaskSnapshot {
42
64
  resultAvailable: boolean;
43
65
  resultClaimed: boolean;
44
66
  pendingControls: number;
67
+ /**
68
+ * Bounded receipts emitted by stores that support authoritative control
69
+ * tracking. Optional so legacy and custom stores remain compatible during
70
+ * rolling upgrades.
71
+ */
72
+ controlReceipts?: SubagentTaskControlReceipt[];
45
73
  progress?: SubagentTaskProgress;
46
74
  error?: string;
47
75
  }
@@ -11,25 +11,27 @@ export type SummarizationTrigger = {
11
11
  };
12
12
 
13
13
  /**
14
- * Controls how many recent messages are preserved verbatim during
15
- * compaction. The most recent user-led turn is always preserved
16
- * regardless of these caps, so a single oversized first message is
17
- * never destroyed by summarization.
14
+ * Controls how much recent context is preserved verbatim during compaction.
15
+ * User-turn boundaries are preferred. Under context pressure, older closed
16
+ * tool units inside an otherwise indivisible turn may be summarized while a
17
+ * token-priced recent tail is retained. A lone user payload stays intact.
18
18
  */
19
19
  export type RetainRecentConfig = {
20
20
  /**
21
- * Maximum number of recent user-led turns to keep in the tail. A turn
22
- * begins at a HumanMessage and includes every following AIMessage and
23
- * ToolMessage up to (but not including) the next HumanMessage. Cutting
24
- * at turn boundaries guarantees tool_use / tool_result pairs are never
25
- * split. Set to `0` to disable the recency window (legacy behavior:
26
- * summarize everything). Defaults to `2`.
21
+ * Maximum number of recent user-led turns to keep in the tail. A turn begins
22
+ * at a user-authored HumanMessage and includes every following AIMessage and
23
+ * tool result up to the next user-authored HumanMessage. Provider-native
24
+ * HumanMessages containing only tool results remain in the current turn.
25
+ * Set to `0` to disable the recency window (legacy behavior: summarize
26
+ * everything). Defaults to `2`.
27
27
  */
28
28
  turns?: number;
29
29
  /**
30
- * Optional cap on retained-recent tokens beyond the most recent turn.
31
- * Older turns are added whole only while cumulative tokens stay below
32
- * the cap. Defaults to undefined (no cap; bounded only by `turns`).
30
+ * Optional retained-recent token budget. Older turns are added whole only
31
+ * while cumulative tokens stay below the cap. If a tool-heavy history has
32
+ * no compactable turn-level head, this is also the minimum recent tail kept
33
+ * behind a pairing-balanced intra-turn cut. When omitted, that fallback
34
+ * retains 16% of the configured context window.
33
35
  */
34
36
  tokens?: number;
35
37
  };