@librechat/agents 3.7.6 → 3.7.8

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 (92) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +23 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/constants.cjs +10 -0
  4. package/dist/cjs/common/constants.cjs.map +1 -1
  5. package/dist/cjs/eventActor/EventActorExecutor.cjs +305 -11
  6. package/dist/cjs/eventActor/EventActorExecutor.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +16 -5
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/langfuseTraceShaping.cjs +75 -0
  10. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/toolCache.cjs +1 -1
  12. package/dist/cjs/llm/fake.cjs +9 -5
  13. package/dist/cjs/llm/fake.cjs.map +1 -1
  14. package/dist/cjs/llm/invoke.cjs +1 -1
  15. package/dist/cjs/llm/openrouter/toolCache.cjs +1 -1
  16. package/dist/cjs/llm/prepareProviderRequest.cjs +2 -2
  17. package/dist/cjs/llm/providers.cjs +1 -1
  18. package/dist/cjs/llm/truncation.cjs +1 -0
  19. package/dist/cjs/main.cjs +3 -2
  20. package/dist/cjs/messages/format.cjs +298 -3
  21. package/dist/cjs/messages/format.cjs.map +1 -1
  22. package/dist/cjs/messages/prune.cjs +1 -1
  23. package/dist/cjs/run.cjs +25 -15
  24. package/dist/cjs/run.cjs.map +1 -1
  25. package/dist/cjs/stream.cjs +1 -1
  26. package/dist/cjs/summarization/node.cjs +43 -9
  27. package/dist/cjs/summarization/node.cjs.map +1 -1
  28. package/dist/cjs/summarization/semanticIndex.cjs +362 -0
  29. package/dist/cjs/summarization/semanticIndex.cjs.map +1 -0
  30. package/dist/cjs/tools/subagent/childGraphConfig.cjs +2 -1
  31. package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -1
  32. package/dist/cjs/utils/index.cjs +1 -1
  33. package/dist/esm/agents/AgentContext.mjs +23 -3
  34. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  35. package/dist/esm/common/constants.mjs +10 -1
  36. package/dist/esm/common/constants.mjs.map +1 -1
  37. package/dist/esm/eventActor/EventActorExecutor.mjs +305 -11
  38. package/dist/esm/eventActor/EventActorExecutor.mjs.map +1 -1
  39. package/dist/esm/graphs/Graph.mjs +16 -5
  40. package/dist/esm/graphs/Graph.mjs.map +1 -1
  41. package/dist/esm/langfuseTraceShaping.mjs +75 -0
  42. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  43. package/dist/esm/llm/bedrock/toolCache.mjs +1 -1
  44. package/dist/esm/llm/fake.mjs +9 -5
  45. package/dist/esm/llm/fake.mjs.map +1 -1
  46. package/dist/esm/llm/invoke.mjs +1 -1
  47. package/dist/esm/llm/openrouter/toolCache.mjs +1 -1
  48. package/dist/esm/llm/prepareProviderRequest.mjs +2 -2
  49. package/dist/esm/llm/providers.mjs +1 -1
  50. package/dist/esm/llm/truncation.mjs +1 -1
  51. package/dist/esm/main.mjs +4 -4
  52. package/dist/esm/messages/format.mjs +298 -3
  53. package/dist/esm/messages/format.mjs.map +1 -1
  54. package/dist/esm/messages/prune.mjs +1 -1
  55. package/dist/esm/run.mjs +25 -15
  56. package/dist/esm/run.mjs.map +1 -1
  57. package/dist/esm/stream.mjs +1 -1
  58. package/dist/esm/summarization/node.mjs +43 -9
  59. package/dist/esm/summarization/node.mjs.map +1 -1
  60. package/dist/esm/summarization/semanticIndex.mjs +360 -0
  61. package/dist/esm/summarization/semanticIndex.mjs.map +1 -0
  62. package/dist/esm/tools/subagent/childGraphConfig.mjs +2 -1
  63. package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -1
  64. package/dist/esm/utils/index.mjs +1 -1
  65. package/dist/types/agents/AgentContext.d.ts +27 -1
  66. package/dist/types/common/constants.d.ts +10 -0
  67. package/dist/types/eventActor/EventActorExecutor.d.ts +3 -1
  68. package/dist/types/eventActor/index.d.ts +1 -1
  69. package/dist/types/eventActor/types.d.ts +149 -5
  70. package/dist/types/graphs/Graph.d.ts +15 -0
  71. package/dist/types/llm/fake.d.ts +12 -2
  72. package/dist/types/messages/format.d.ts +10 -1
  73. package/dist/types/run.d.ts +17 -5
  74. package/dist/types/summarization/semanticIndex.d.ts +19 -0
  75. package/dist/types/types/graph.d.ts +9 -1
  76. package/dist/types/types/summarize.d.ts +38 -0
  77. package/package.json +2 -1
  78. package/src/agents/AgentContext.ts +61 -1
  79. package/src/common/constants.ts +11 -0
  80. package/src/eventActor/EventActorExecutor.ts +545 -16
  81. package/src/eventActor/index.ts +17 -0
  82. package/src/eventActor/types.ts +182 -3
  83. package/src/graphs/Graph.ts +41 -8
  84. package/src/langfuseTraceShaping.ts +94 -0
  85. package/src/llm/fake.ts +35 -4
  86. package/src/messages/format.ts +652 -4
  87. package/src/run.ts +36 -10
  88. package/src/summarization/node.ts +90 -33
  89. package/src/summarization/semanticIndex.ts +653 -0
  90. package/src/tools/subagent/childGraphConfig.ts +3 -0
  91. package/src/types/graph.ts +9 -0
  92. package/src/types/summarize.ts +50 -0
@@ -62,12 +62,129 @@ export type EventActorTerminalResult<TResult extends EventActorEvent> = {
62
62
  status: 'completed_no_action';
63
63
  result?: TResult;
64
64
  };
65
+ /** JSON-safe interrupt descriptor retained with a suspended invocation fork. */
66
+ export interface EventActorInterrupt<TPayload extends EventActorEvent = EventActorEvent> {
67
+ id: string;
68
+ payload: TPayload;
69
+ }
70
+ /**
71
+ * Nonterminal adapter outcome. The checkpoint must already contain the pause;
72
+ * the SDK publishes its authority through `suspend` before exposing it.
73
+ */
74
+ export interface EventActorAdapterSuspendedResult<TPayload extends EventActorEvent = EventActorEvent> {
75
+ status: 'suspended';
76
+ checkpoint: EventActorCheckpointFork;
77
+ interrupt: EventActorInterrupt<TPayload>;
78
+ }
79
+ export type EventActorAdapterInvocationResult<TResult extends EventActorEvent, TPayload extends EventActorEvent = EventActorEvent> = EventActorTerminalResult<TResult> | EventActorAdapterSuspendedResult<TPayload>;
80
+ /**
81
+ * Authenticated, versioned, JSON-safe evidence for one nonterminal invocation.
82
+ * Integrity does not make this evidence one-shot; the host's durable current
83
+ * suspension record is the replay and ownership fence. Hosts must capability-
84
+ * route each exact version during rolling deploys and drain or migrate current
85
+ * evidence before rotating away from its signing key.
86
+ */
87
+ export interface EventActorSuspension<TPayload extends EventActorEvent = EventActorEvent> {
88
+ version: 1;
89
+ suspensionId: string;
90
+ attempt: number;
91
+ issuedAt: number;
92
+ expiresAt: number;
93
+ invocation: EventActorInvocationReference;
94
+ checkpoint: EventActorCheckpointFork;
95
+ interrupt: EventActorInterrupt<TPayload>;
96
+ suspensionDigest: string;
97
+ }
98
+ export interface EventActorSuspendedResult<TPayload extends EventActorEvent = EventActorEvent> {
99
+ status: 'suspended';
100
+ suspension: EventActorSuspension<TPayload>;
101
+ }
102
+ export interface EventActorSuspendRequest {
103
+ suspension: EventActorSuspension;
104
+ /** CAS predecessor required when a claimed resume pauses again. */
105
+ previous?: {
106
+ suspensionId: string;
107
+ attempt: number;
108
+ resumeAttemptId: string;
109
+ };
110
+ }
111
+ export type EventActorSuspendResult = {
112
+ status: 'stored';
113
+ } | {
114
+ status: 'stale';
115
+ };
116
+ export interface EventActorAdapterResumeRequest {
117
+ suspension: EventActorSuspension;
118
+ resumeAttemptId: string;
119
+ value: EventActorEvent;
120
+ }
121
+ export type EventActorAdapterResumeResult<TResult extends EventActorEvent> = {
122
+ status: 'claimed';
123
+ result: EventActorAdapterInvocationResult<TResult>;
124
+ } | {
125
+ /** The host proved no action before returning this claimed failure. */
126
+ status: 'claimed_failed';
127
+ error: Error;
128
+ } | {
129
+ status: 'stale';
130
+ };
131
+ export interface EventActorResumeRequest {
132
+ suspension: EventActorSuspension;
133
+ resumeAttemptId: string;
134
+ value: EventActorEvent;
135
+ signal?: AbortSignal;
136
+ }
137
+ export interface EventActorCancelSuspensionRequest {
138
+ suspension: EventActorSuspension;
139
+ cancelAttemptId: string;
140
+ reason?: 'cancelled' | 'expired';
141
+ signal?: AbortSignal;
142
+ }
143
+ export interface EventActorAdapterCancelSuspensionRequest {
144
+ suspension: EventActorSuspension;
145
+ cancelAttemptId: string;
146
+ reason: 'cancelled' | 'expired';
147
+ }
148
+ export type EventActorCancelSuspensionResult = {
149
+ status: 'cancelled';
150
+ } | EventActorIndeterminateResult<EventActorEvent>;
151
+ export type EventActorAdapterCancelSuspensionResult = {
152
+ status: 'cancelled';
153
+ } | {
154
+ status: 'stale';
155
+ };
156
+ export interface EventActorSettleSuspensionRequest {
157
+ suspensionId: string;
158
+ attempt: number;
159
+ resumeAttemptId: string;
160
+ status: 'completed_no_action' | 'failed';
161
+ }
162
+ export type EventActorSettleSuspensionResult = {
163
+ status: 'settled';
164
+ } | {
165
+ status: 'stale';
166
+ };
65
167
  export type EventActorAppliedResult<TResult extends EventActorEvent> = Extract<EventActorTerminalResult<TResult>, {
66
168
  status: 'applied';
67
169
  }> & {
68
- /** Executor-issued one-shot settlement for the invocation that produced this action. */
170
+ /** Executor-issued settlement for the invocation that produced this action. */
69
171
  invocation: EventActorInvocationReference;
172
+ /** Authenticated cross-executor authority for a resumed terminal action. */
173
+ settlementAuthority?: EventActorSettlementAuthority;
70
174
  };
175
+ /**
176
+ * Authenticated fence that binds a resumed terminal result to its claimed
177
+ * suspension. The host must consume it atomically with the actor-head CAS.
178
+ */
179
+ export interface EventActorSettlementAuthority {
180
+ version: 1;
181
+ suspensionId: string;
182
+ attempt: number;
183
+ resumeAttemptId: string;
184
+ issuedAt: number;
185
+ expiresAt: number;
186
+ settlementDigest: string;
187
+ }
71
188
  export interface EventActorIndeterminateResult<TResult extends EventActorEvent> {
72
189
  /** Applied handling cannot be proven safe to retry; retain its fork. */
73
190
  status: 'commit_indeterminate';
@@ -75,7 +192,7 @@ export interface EventActorIndeterminateResult<TResult extends EventActorEvent>
75
192
  checkpoint: EventActorCheckpointFork;
76
193
  error: Error;
77
194
  }
78
- export type EventActorInvocationResult<TResult extends EventActorEvent> = EventActorAppliedResult<TResult> | EventActorIndeterminateResult<TResult> | Extract<EventActorTerminalResult<TResult>, {
195
+ export type EventActorInvocationResult<TResult extends EventActorEvent> = EventActorAppliedResult<TResult> | EventActorIndeterminateResult<TResult> | EventActorSuspendedResult | Extract<EventActorTerminalResult<TResult>, {
79
196
  status: 'completed_no_action';
80
197
  }>;
81
198
  export interface EventActorInvocationContext {
@@ -101,6 +218,8 @@ export interface EventActorCommitRequest<TResult extends EventActorEvent> {
101
218
  expectedHead: EventActorHead;
102
219
  checkpoint: EventActorCheckpointFork;
103
220
  result: TResult;
221
+ /** Host must consume this suspension fence atomically with the head CAS. */
222
+ settlementAuthority?: EventActorSettlementAuthority;
104
223
  retention: {
105
224
  committedCheckpoints: 2;
106
225
  dormantCheckpointTtlMs: number;
@@ -123,7 +242,11 @@ export interface EventActorDiscardRequest {
123
242
  /**
124
243
  * Host adapter for durable actor state and the concrete agent invocation.
125
244
  * `commit` must compare both the expected generation and checkpoint identity
126
- * atomically before advancing the logical actor head. The host mailbox
245
+ * atomically before advancing the logical actor head. When settlement authority
246
+ * is present, that same transaction must also verify and close the exact
247
+ * suspension/resume-attempt fence, including stale-head outcomes. Retrying an
248
+ * ambiguous acknowledgement must return the durable outcome rather than apply
249
+ * the same terminal transition again. The host mailbox
127
250
  * deduplicates the logical `invocationId` before entering this seam, while each
128
251
  * SDK execution attempt receives a distinct checkpoint namespace. Preparation
129
252
  * methods own rollback until they return a ready invocation and must treat the
@@ -137,11 +260,28 @@ export interface EventActorDiscardRequest {
137
260
  * fork: the SDK retains and surfaces it as `commit_conflict` for host
138
261
  * reconciliation. `discard` must be idempotent for the same invocation because
139
262
  * an ambiguous cleanup failure can be retried through the public lifecycle.
263
+ *
264
+ * Suspension-capable hosts implement all optional suspension methods. `suspend`
265
+ * publishes an initial suspension only while its logical invocation is current;
266
+ * with `previous`, it atomically replaces only the exact claimed predecessor.
267
+ * `resume` atomically claims the current suspension before applying its value to
268
+ * the declared interrupt and rejects duplicate or competing claims as `stale`.
269
+ * It must return `claimed_failed` only when it can prove no qualifying action;
270
+ * any failure after an action returns `applied`. `settleSuspension` atomically
271
+ * discards the claimed fork and closes its fence for definite no-action
272
+ * outcomes. `cancelSuspension`
273
+ * atomically claims, discards, and closes current state, including expired
274
+ * evidence; expiration alone never implies a safe action outcome.
140
275
  */
141
276
  export interface EventActorHostAdapter<TEvent extends EventActorEvent, TResult extends EventActorEvent> {
142
277
  prepare(request: EventActorAdapterPrepareRequest<TEvent>, context: EventActorPreparationContext): Promise<EventActorAdapterPreparation<TEvent>>;
143
278
  coldContinue(request: EventActorAdapterPrepareRequest<TEvent>, head: EventActorHead, context: EventActorPreparationContext): Promise<EventActorInvocation<TEvent>>;
144
- invoke(invocation: EventActorInvocation<TEvent>, context: EventActorInvocationContext): Promise<EventActorTerminalResult<TResult>>;
279
+ invoke(invocation: EventActorInvocation<TEvent>, context: EventActorInvocationContext): Promise<EventActorAdapterInvocationResult<TResult>>;
280
+ suspend?(request: EventActorSuspendRequest): Promise<EventActorSuspendResult>;
281
+ resume?(request: EventActorAdapterResumeRequest, context: EventActorInvocationContext): Promise<EventActorAdapterResumeResult<TResult>>;
282
+ /** Atomically claims, discards, and closes the current suspension. */
283
+ cancelSuspension?(request: EventActorAdapterCancelSuspensionRequest, context: EventActorPreparationContext): Promise<EventActorAdapterCancelSuspensionResult>;
284
+ settleSuspension?(request: EventActorSettleSuspensionRequest): Promise<EventActorSettleSuspensionResult>;
145
285
  commit(request: EventActorCommitRequest<TResult>): Promise<EventActorCommitResult>;
146
286
  discard(request: EventActorDiscardRequest): Promise<void>;
147
287
  }
@@ -165,7 +305,9 @@ export type EventActorExecutionResult<TResult extends EventActorEvent> = {
165
305
  } | {
166
306
  status: 'cancelled';
167
307
  continuation: 'warm' | 'cold';
168
- } | {
308
+ } | (EventActorSuspendedResult & {
309
+ continuation: 'warm' | 'cold';
310
+ }) | {
169
311
  /** The action happened, but another head won the CAS. Reconcile; do not retry. */
170
312
  status: 'commit_conflict';
171
313
  result: TResult;
@@ -190,4 +332,6 @@ export interface EventActorExecutorOptions {
190
332
  dormantCheckpointTtlMs?: number;
191
333
  /** Stable private key of at least 32 bytes for cross-lifetime handoffs. */
192
334
  preparationSigningKey?: string | Uint8Array;
335
+ /** Maximum UTF-8 byte size of canonical suspension evidence. */
336
+ maxSuspensionPayloadBytes?: number;
193
337
  }
@@ -374,6 +374,21 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
374
374
  * hosts persist it as unfinished rather than complete.
375
375
  */
376
376
  preemptIncomplete: boolean;
377
+ /**
378
+ * True when `routeMessage` sent a turn to `END` because the last AI
379
+ * message carries no tool call, AND the provider reports it stopped for
380
+ * hitting the output token ceiling (`getTruncationStopReason`). Plain-text
381
+ * and reasoning turns cut off this way carry no tool call for
382
+ * `assertNotTruncatedToolCall` to catch, so `toolsCondition` reads them as
383
+ * an ordinary finished turn otherwise — hosts read this flag to persist
384
+ * the turn as unfinished instead of a silently truncated "complete" answer.
385
+ *
386
+ * Deliberately separate from `preemptIncomplete`/`preemptHaltReason`: this
387
+ * has no interaction with the preempt/seal machinery (in particular the
388
+ * `preemptHaltReason` check at each model node's entry), so setting it
389
+ * cannot suppress an unrelated agent's turn in a multi-agent graph.
390
+ */
391
+ outputTruncatedIncomplete: boolean;
377
392
  /**
378
393
  * `stopReason` from a `PreemptBoundary` hook that halted the turn.
379
394
  *
@@ -11,15 +11,25 @@ export declare class FakeChatModel extends FakeListChatModel {
11
11
  private splitStrategy;
12
12
  private toolCalls;
13
13
  private addedToolCalls;
14
- constructor({ responses, sleep, emitCustomEvent, splitStrategy, toolCalls, }: {
14
+ /**
15
+ * Attached to the last streamed chunk of every response, mirroring a real
16
+ * provider's terminal `finish_reason`/`stop_reason` on `generationInfo`.
17
+ * Lets truncation-path tests (`getTruncationStopReason`) drive a genuine
18
+ * `Run`/`StandardGraph` without a live provider. `undefined` (the
19
+ * default) reproduces the previous behavior exactly: no metadata on any
20
+ * chunk.
21
+ */
22
+ private finalChunkGenerationInfo?;
23
+ constructor({ responses, sleep, emitCustomEvent, splitStrategy, toolCalls, finalChunkGenerationInfo, }: {
15
24
  responses: string[];
16
25
  sleep?: number;
17
26
  emitCustomEvent?: boolean;
18
27
  splitStrategy?: SplitStrategy;
19
28
  toolCalls?: ToolCall[];
29
+ finalChunkGenerationInfo?: Record<string, unknown>;
20
30
  });
21
31
  private splitText;
22
- _createResponseChunk(text: string, tool_call_chunks?: ToolCallChunk[]): ChatGenerationChunk;
32
+ _createResponseChunk(text: string, tool_call_chunks?: ToolCallChunk[], responseMetadata?: Record<string, unknown>): ChatGenerationChunk;
23
33
  _streamResponseChunks(_messages: BaseMessage[], options: this['ParsedCallOptions'], runManager?: CallbackManagerForLLMRun): AsyncGenerator<ChatGenerationChunk>;
24
34
  }
25
35
  export declare function createFakeStreamingLLM({ responses, sleep, splitStrategy, toolCalls, }: {
@@ -1,7 +1,7 @@
1
1
  import { AIMessage, ToolMessage, BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
2
2
  import type { MessageContentImageUrl } from '@langchain/core/messages';
3
3
  import type { RunnableConfig } from '@langchain/core/runnables';
4
- import type { MessageContentComplex, TPayload, ProviderName } from '@/types';
4
+ import type { MessageContentComplex, TPayload, ProviderName, CompactionSemanticIndex } from '@/types';
5
5
  import { Providers } from '@/common';
6
6
  interface MediaMessageParams {
7
7
  message: {
@@ -105,6 +105,13 @@ interface FormatAgentMessagesOptions {
105
105
  * historical `skill` tool_calls are not reconstructed into a HumanMessage,
106
106
  * so the same SKILL.md body is not injected twice in one request. */
107
107
  skipSkillBodyNames?: Set<string>;
108
+ /** Derive bounded compaction guidance during the formatter's existing
109
+ * persisted-content analysis. Tool intents are accepted only for names
110
+ * the host identifies as semantic-label fields; business `intent`
111
+ * parameters must remain ordinary tool input. */
112
+ compactionSemanticIndex?: {
113
+ intentToolNames?: ReadonlySet<string>;
114
+ };
108
115
  }
109
116
  /**
110
117
  * Groups content parts by agent and formats them with agent labels
@@ -148,6 +155,8 @@ export declare const formatAgentMessages: (payload: TPayload, indexTokenCountMap
148
155
  /** When a positional summary boundary sliced content from a message, the token
149
156
  * count was proportionally reduced. Returned so the caller can log it. */
150
157
  boundaryTokenAdjustment?: SummaryTokenAdjustment;
158
+ /** Bounded semantic guidance derived during persisted-content analysis. */
159
+ compactionSemanticIndex?: CompactionSemanticIndex;
151
160
  };
152
161
  /**
153
162
  * Adds a value at key 0 for system messages and shifts all key indices by one in an indexTokenCountMap.
@@ -134,6 +134,15 @@ export declare class Run<_T extends t.BaseGraphState> {
134
134
  */
135
135
  getPreemptStats(): t.PreemptStats;
136
136
  getToolCount(): number;
137
+ /**
138
+ * True when the run's last turn ended at `END` because the provider hit
139
+ * its output token ceiling while producing plain text/reasoning — no tool
140
+ * call, so `assertNotTruncatedToolCall` never sees it and the graph reads
141
+ * the turn as an ordinary completion. Hosts check this alongside
142
+ * `getPreemptStats()` / `getHaltReason()` to decide whether to persist the
143
+ * response as unfinished instead of a silently truncated "complete" one.
144
+ */
145
+ getOutputTruncated(): boolean;
137
146
  /**
138
147
  * Creates a custom event callback handler that intercepts custom events
139
148
  * and processes them through our handler registry instead of EventStreamCallbackHandler
@@ -175,13 +184,16 @@ export declare class Run<_T extends t.BaseGraphState> {
175
184
  */
176
185
  getInterrupt<TPayload = t.HumanInterruptPayload>(): t.RunInterruptResult<TPayload> | undefined;
177
186
  /**
178
- * Returns the reason a hook halted the run via
179
- * `preventContinuation: true`, or `undefined` if no hook halted.
187
+ * Returns why the run ended without a natural completion, or `undefined`
188
+ * when it completed normally. Reasons include hook- and prompt-driven
189
+ * halts, `preempt_incomplete` when a cooperative seal ended the turn
190
+ * without continuation content, and `output_truncated` when the provider
191
+ * stopped a plain-text/reasoning response at its output-token ceiling.
180
192
  *
181
193
  * Hosts inspect this after `processStream` returns to distinguish a
182
- * natural completion (`undefined`) from a hook-driven halt (a
183
- * truthy string). Independent from `getInterrupt()` — a halted run
184
- * has no interrupt; an interrupted run has no halt reason.
194
+ * natural completion from a terminal partial response. Independent from
195
+ * `getInterrupt()` — a halted run has no interrupt; an interrupted run has
196
+ * no halt reason.
185
197
  */
186
198
  getHaltReason(): string | undefined;
187
199
  /**
@@ -0,0 +1,19 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
2
+ import type { CompactionSemanticIndex } from '@/types';
3
+ export { COMPACTION_SEMANTIC_INDEX_LIMITS } from '@/common';
4
+ /** Records producer-side omissions without retaining the discarded entries. */
5
+ export declare function setCompactionSemanticIndexProvidedEntryCount(index: CompactionSemanticIndex, providedEntryCount: number): void;
6
+ export type RenderedCompactionSemanticIndex = {
7
+ appendix: string;
8
+ providedEntryCount: number;
9
+ entryCount: number;
10
+ charCount: number;
11
+ omittedEntryCount: number;
12
+ };
13
+ /** Captures caller-owned data before graph execution can cross an await. */
14
+ export declare function snapshotCompactionSemanticIndex(index: CompactionSemanticIndex | undefined): CompactionSemanticIndex | undefined;
15
+ /**
16
+ * Produces a deterministic, bounded compaction appendix. Invalid, stale,
17
+ * pending, redacted, conflicting, and out-of-range entries fail closed.
18
+ */
19
+ export declare function renderCompactionSemanticIndex(index: CompactionSemanticIndex | undefined, messagesToRefine: BaseMessage[]): RenderedCompactionSemanticIndex;
@@ -4,7 +4,7 @@ import type { START, StateGraph, StateGraphArgs } from '@langchain/langgraph';
4
4
  import type { RunnableConfig, Runnable } from '@langchain/core/runnables';
5
5
  import type { ChatGenerationChunk } from '@langchain/core/outputs';
6
6
  import type { GoogleAIToolType } from '@langchain/google-common';
7
- import type { SummarizationNodeInput, SummarizeCompleteEvent, SummarizationConfig, SummarizeStartEvent, SummarizeDeltaEvent } from '@/types/summarize';
7
+ import type { SummarizationNodeInput, SummarizeCompleteEvent, CompactionSemanticIndex, SummarizationConfig, SummarizeStartEvent, SummarizeDeltaEvent } from '@/types/summarize';
8
8
  import type { RunStep, RunStepDeltaEvent, RunStepResumeState, RunStepClosedEvent, MessageDeltaEvent, ReasoningDeltaEvent } from '@/types/stream';
9
9
  import type { ToolMap, ToolSessionMap, ToolEndEvent, GenericTool, LCTool, ToolExecutionConfig, ToolExecuteBatchRequest } from '@/types/tools';
10
10
  import type { TokenCounter, StreamLimits, StreamPreemption, TokenBudgetBreakdown } from '@/types/run';
@@ -693,6 +693,14 @@ interface AgentInputFields {
693
693
  discoveredTools?: string[];
694
694
  summarizationEnabled?: boolean;
695
695
  summarizationConfig?: SummarizationConfig;
696
+ /**
697
+ * Optional host-supplied, user-visible guidance for compaction. The SDK
698
+ * validates, bounds, and scopes entries to the messages being compacted;
699
+ * raw conversation messages remain authoritative. Captured when the
700
+ * AgentContext is constructed; labels committed later in the same run are
701
+ * outside this construction-time interface.
702
+ */
703
+ compactionSemanticIndex?: CompactionSemanticIndex;
696
704
  /** Cross-run summary from a previous run, forwarded from formatAgentMessages.
697
705
  * Injected into the dynamic system tail via AgentContext. */
698
706
  initialSummary?: {
@@ -29,6 +29,39 @@ export type RetainRecentConfig = {
29
29
  */
30
30
  tokens?: number;
31
31
  };
32
+ export type CompactionSemanticIndexStatus = 'committed' | 'pending';
33
+ type CompactionSemanticIndexEntryBase = {
34
+ /** Persisted message that owns the indexed content. */
35
+ sourceMessageId: string;
36
+ /** Zero-based content-part index within the persisted source message. */
37
+ sourceContentIndex: number;
38
+ /** Monotonic host revision for this logical entry. */
39
+ revision: number;
40
+ /** Only committed entries may guide compaction. */
41
+ status: CompactionSemanticIndexStatus;
42
+ /** User-visible semantic guidance. Hidden reasoning must never be supplied. */
43
+ text: string;
44
+ /** Omits the entry entirely when host policy redacts its source. */
45
+ redacted?: boolean;
46
+ };
47
+ export type CompactionToolSemanticIndexEntry = CompactionSemanticIndexEntryBase & {
48
+ type: 'tool_intent' | 'tool_outcome';
49
+ toolCallId: string;
50
+ };
51
+ export type CompactionActivitySemanticIndexEntry = CompactionSemanticIndexEntryBase & {
52
+ type: 'activity_phase';
53
+ };
54
+ export type CompactionReasoningSemanticIndexEntry = CompactionSemanticIndexEntryBase & {
55
+ type: 'reasoning_label';
56
+ /** Stable identity shared by every user-visible label revision. */
57
+ reasoningStepId: string;
58
+ };
59
+ /**
60
+ * Source-addressed navigation hints for the compaction model. Entries remain
61
+ * advisory: raw messages are always sent and remain authoritative.
62
+ */
63
+ export type CompactionSemanticIndexEntry = CompactionToolSemanticIndexEntry | CompactionActivitySemanticIndexEntry | CompactionReasoningSemanticIndexEntry;
64
+ export type CompactionSemanticIndex = readonly CompactionSemanticIndexEntry[];
32
65
  export type SummarizationConfig = {
33
66
  provider?: ProviderName;
34
67
  model?: string;
@@ -86,6 +119,10 @@ export interface SummarizeStartEvent {
86
119
  messagesToRefineCount: number;
87
120
  /** Which summarization cycle this is (1-based, increments each time summarization fires) */
88
121
  summaryVersion: number;
122
+ /** Committed, source-valid semantic hints included in the request. */
123
+ semanticIndexEntryCount?: number;
124
+ /** Serialized semantic-index characters included in the request. */
125
+ semanticIndexCharCount?: number;
89
126
  }
90
127
  export interface SummarizeDeltaEvent {
91
128
  id: string;
@@ -99,3 +136,4 @@ export interface SummarizeCompleteEvent {
99
136
  summary?: SummaryContentBlock;
100
137
  error?: string;
101
138
  }
139
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.7.6",
3
+ "version": "3.7.8",
4
4
  "reova": {
5
5
  "enabled": true,
6
6
  "endpoint": "https://telemetry.reo.dev/data"
@@ -221,6 +221,7 @@
221
221
  "bench:provider-derivation": "tsx ./src/scripts/bench-provider-derivation.ts",
222
222
  "bench:provider-projection": "tsx ./src/scripts/bench-provider-request-projection.ts",
223
223
  "bench:execution-world": "tsx ./src/scripts/bench-execution-world.ts",
224
+ "bench:compaction-semantic-index": "tsx ./src/scripts/bench-compaction-semantic-index.ts",
224
225
  "probe:overflow": "tsx -r dotenv/config ./src/scripts/context-overflow-probe.ts",
225
226
  "subagent": "tsx -r dotenv/config ./src/scripts/multi-agent-subagent.ts",
226
227
  "subagent:events": "tsx -r dotenv/config ./src/scripts/subagent-event-driven-debug.ts",
@@ -50,6 +50,7 @@ import {
50
50
  Providers,
51
51
  } from '@/common';
52
52
  import { isTokenCounterCacheCompatible } from '@/llm/tokenCounterCacheCompatibility';
53
+ import { snapshotCompactionSemanticIndex } from '@/summarization/semanticIndex';
53
54
  import { createExactTokenCountCache } from '@/llm/contextPressureMeter';
54
55
  import { createSchemaOnlyTools } from '@/tools/schema';
55
56
  import { apportionTokenCounts } from '@/utils/tokens';
@@ -74,6 +75,14 @@ type ProgrammaticToolInstructionTarget = {
74
75
  executesDirectly: boolean;
75
76
  };
76
77
 
78
+ /**
79
+ * Consecutive summarization attempts that may return no usable summary before
80
+ * the run stops asking. Every such attempt spends a full model call over the
81
+ * whole history and leaves the message set exactly as it was, so the cap
82
+ * bounds spend as much as it bounds the compaction loop.
83
+ */
84
+ const MAX_SUMMARIZATION_FAILURES = 3;
85
+
77
86
  /**
78
87
  * Encapsulates agent-specific state that can vary between agents in a multi-agent system
79
88
  */
@@ -108,6 +117,7 @@ export class AgentContext {
108
117
  discoveredTools,
109
118
  summarizationEnabled,
110
119
  summarizationConfig,
120
+ compactionSemanticIndex,
111
121
  initialSummary,
112
122
  contextPruningConfig,
113
123
  maxToolResultChars,
@@ -141,11 +151,18 @@ export class AgentContext {
141
151
  discoveredTools,
142
152
  summarizationEnabled,
143
153
  summarizationConfig,
154
+ compactionSemanticIndex,
144
155
  contextPruningConfig,
145
156
  maxToolResultChars,
146
157
  });
147
158
 
148
- agentContext._sourceInputs = agentConfig;
159
+ agentContext._sourceInputs =
160
+ compactionSemanticIndex == null
161
+ ? agentConfig
162
+ : {
163
+ ...agentConfig,
164
+ compactionSemanticIndex: agentContext.compactionSemanticIndex,
165
+ };
149
166
  agentContext.subagentConfigs = subagentConfigs;
150
167
  agentContext.maxSubagentDepth = maxSubagentDepth;
151
168
  /**
@@ -346,6 +363,8 @@ export class AgentContext {
346
363
  summarizationEnabled?: boolean;
347
364
  /** Summarization runtime settings used by graph pruning hooks */
348
365
  summarizationConfig?: t.SummarizationConfig;
366
+ /** Host-supplied advisory guidance consumed only when compaction runs. */
367
+ compactionSemanticIndex?: t.CompactionSemanticIndex;
349
368
  /** Current summary text produced by the summarize node, integrated into system message */
350
369
  private summaryText?: string;
351
370
  /** Token count of the current summary (tracked for token accounting) */
@@ -376,6 +395,13 @@ export class AgentContext {
376
395
  * Summarization is allowed to fire again only when new messages appear.
377
396
  */
378
397
  private _lastSummarizationMsgCount: number = 0;
398
+ /**
399
+ * Consecutive summarization attempts that produced no usable summary.
400
+ * An empty or failed summary leaves the message set exactly as it was, so
401
+ * the next prune cycle would ask again on identical state. Cleared by
402
+ * `setSummary` and by `reset()`.
403
+ */
404
+ private _summarizationFailures: number = 0;
379
405
  /**
380
406
  * Forced compactions performed after a provider rejected a prompt as too
381
407
  * large. Bounds the recovery loop so a model that keeps refusing cannot
@@ -430,6 +456,7 @@ export class AgentContext {
430
456
  discoveredTools,
431
457
  summarizationEnabled,
432
458
  summarizationConfig,
459
+ compactionSemanticIndex,
433
460
  contextPruningConfig,
434
461
  maxToolResultChars,
435
462
  }: {
@@ -456,6 +483,7 @@ export class AgentContext {
456
483
  discoveredTools?: string[];
457
484
  summarizationEnabled?: boolean;
458
485
  summarizationConfig?: t.SummarizationConfig;
486
+ compactionSemanticIndex?: t.CompactionSemanticIndex;
459
487
  contextPruningConfig?: t.ContextPruningConfig;
460
488
  maxToolResultChars?: number;
461
489
  }) {
@@ -495,6 +523,11 @@ export class AgentContext {
495
523
  this.useLegacyContent = useLegacyContent ?? false;
496
524
  this.summarizationEnabled = summarizationEnabled;
497
525
  this.summarizationConfig = summarizationConfig;
526
+ if (compactionSemanticIndex != null) {
527
+ this.compactionSemanticIndex = snapshotCompactionSemanticIndex(
528
+ compactionSemanticIndex
529
+ );
530
+ }
498
531
  this.contextPruningConfig = contextPruningConfig;
499
532
  this.maxToolResultChars = maxToolResultChars;
500
533
 
@@ -1209,6 +1242,7 @@ export class AgentContext {
1209
1242
  this.summaryTokenCount = this._durableSummaryTokenCount;
1210
1243
  this.summaryPrecedesMessages = this.durableSummaryPrecedesMessages;
1211
1244
  this._lastSummarizationMsgCount = 0;
1245
+ this._summarizationFailures = 0;
1212
1246
  this.lastCallUsage = undefined;
1213
1247
  this.totalTokensFresh = false;
1214
1248
  this.restoreContextBudgetAfterOverflow();
@@ -1478,6 +1512,7 @@ export class AgentContext {
1478
1512
  this._durableSummaryTokenCount = tokenCount;
1479
1513
  this.durableSummaryPrecedesMessages = this.summaryPrecedesMessages;
1480
1514
  this._summaryVersion += 1;
1515
+ this._summarizationFailures = 0;
1481
1516
  this.systemRunnableStale = true;
1482
1517
  this.pruneMessages = undefined;
1483
1518
  }
@@ -1547,6 +1582,31 @@ export class AgentContext {
1547
1582
  this._lastSummarizationMsgCount = msgCount;
1548
1583
  }
1549
1584
 
1585
+ /**
1586
+ * Records a summarization attempt that produced no usable summary — an
1587
+ * empty model response, or a provider failure the run declined to paper
1588
+ * over with a metadata stub. Cleared by the next successful summary.
1589
+ */
1590
+ recordSummarizationFailure(): void {
1591
+ this._summarizationFailures += 1;
1592
+ }
1593
+
1594
+ get summarizationFailures(): number {
1595
+ return this._summarizationFailures;
1596
+ }
1597
+
1598
+ /**
1599
+ * True once consecutive no-progress attempts reach {@link MAX_SUMMARIZATION_FAILURES}.
1600
+ * A summarizer that has returned nothing this many times in a row will keep
1601
+ * returning nothing: each empty result leaves the history unchanged, so the
1602
+ * next prune cycle re-triggers on the same state and the run burns its
1603
+ * recursion budget on empty summary steps. Summarization stays off for the
1604
+ * remainder of the run; `reset()` restores it for the next one.
1605
+ */
1606
+ get summarizationExhausted(): boolean {
1607
+ return this._summarizationFailures >= MAX_SUMMARIZATION_FAILURES;
1608
+ }
1609
+
1550
1610
  get overflowRecoveryAttempts(): number {
1551
1611
  return this._overflowRecoveryAttempts;
1552
1612
  }
@@ -42,3 +42,14 @@ export const ACTIVITY_LABEL_RUN_NAME = 'StepLabel';
42
42
  export const REASONING_LABEL_RUN_NAME = 'ReasoningLabel';
43
43
  export const ACTIVITY_PHASE_RUN_NAME = 'MultiStepLabel';
44
44
  export const ACTIVITY_PHASE_LABEL_RUN_NAME = 'MultiStepLabelGeneration';
45
+
46
+ /** Shared admission and rendering bounds for compaction navigation hints. */
47
+ export const COMPACTION_SEMANTIC_INDEX_LIMITS = Object.freeze({
48
+ maxInputEntries: 256,
49
+ maxEntries: 64,
50
+ maxEntryChars: 512,
51
+ maxTotalChars: 4_096,
52
+ maxInputTextChars: 4_096,
53
+ maxIdentityChars: 512,
54
+ maxSourceContentIndex: 4_095,
55
+ } as const);