Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
091fa4a
🪟 fix: Summarize the Pruned Turn Window Safely
lia-librechat Oct 27, 2026
ddb8ed5
🪟 fix: Preserve Compaction State Across Retries and Sessions
lia-librechat Oct 27, 2026
80f101d
🪟 fix: Preserve Paired Tool Windows Within Compaction Budgets
lia-librechat Oct 27, 2026
18fb9dc
🪟 fix: Size Compaction Requests for Each Fallback
lia-librechat Oct 27, 2026
cba605d
🪟 fix: Request Text Instead of Tools During Compaction
lia-librechat Oct 27, 2026
8cca79a
🪟 test: Assert Compaction Disables Tool Calls
lia-librechat Oct 27, 2026
55d95d3
🪟 fix: Preserve Compaction Fallback and Cancellation Invariants
lia-librechat Oct 27, 2026
a2a62da
🪟 fix: Retry Reasoning-Only Compaction Responses
lia-librechat Oct 27, 2026
4fb264e
🪟 fix: Attribute Completed Summaries to the Serving Fallback
lia-librechat Oct 27, 2026
d96547d
🪟 fix: Honor Native Provider Stops During Compaction
lia-librechat Oct 27, 2026
bb4253f
🪟 fix: Unify Compaction Recovery and Attempt Accounting
lia-librechat Oct 27, 2026
ab0c2d6
🪟 fix: Require Nonempty Reasoning Before Compaction Retries
lia-librechat Oct 27, 2026
d725ca1
🪟 fix: Close Compaction Reasoning and Accounting Boundaries
lia-librechat Oct 27, 2026
242a672
🪟 fix: Coordinate Compaction Retry and Cache Policies
lia-librechat Oct 27, 2026
cab79ae
🪟 fix: Bound Moonshot Compaction Transport Retries
lia-librechat Oct 27, 2026
5e0f96e
🪟 fix: Align Compaction Budgets and Prefix Reuse
lia-librechat Oct 27, 2026
c709cfa
🪟 fix: Preserve Compaction Coverage Under Input Pressure
lia-librechat Oct 28, 2026
92f32b3
🪟 fix: Keep Overflow Corrections in Input-Budget Units
lia-librechat Oct 28, 2026
7260b06
🪟 fix: Keep Compaction Budget Units With Their Source
lia-librechat Oct 28, 2026
e5d4d07
🪟 test: Reserve Checkpoint Headroom in Tight OpenAI Fixtures
lia-librechat Oct 28, 2026
d443094
🪟 fix: Preserve Output Room for Reasoning Summarizers
lia-librechat Oct 29, 2026
0d25cde
🪟 fix: Normalize Reasoning Summary Output-Limit Aliases
lia-librechat Oct 29, 2026
ed29ef3
🪟 fix: Canonicalize Native Summary Output Overrides
lia-librechat Oct 29, 2026
22dbb0a
🪟 fix: Preserve Canonical Compaction Tails and Fallback Budget Units
lia-librechat Oct 29, 2026
ca2ab8e
🪟 fix: Preserve Uncapped Reasoning During Compaction
lia-librechat Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/agents/AgentContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,11 @@ export class AgentContext {
* disturbing a `maxContextTokens` that no correction ever touched.
*/
private _preOverflowMaxContextTokens?: number;
private correctedContextBudgetMode?: t.ContextBudgetMode;

get summarizationBudgetMode(): t.ContextBudgetMode {
return this.correctedContextBudgetMode ?? this.summarizationConfig?.contextBudgetMode ?? 'input';
}
/**
* Prompt size, normalized into the local counter's uncalibrated units, at
* the last overflow correction. Keeping both measurements in the same units
Expand Down Expand Up @@ -1772,6 +1777,7 @@ export class AgentContext {
}
if (budgetTokens != null) {
this.maxContextTokens = budgetTokens;
this.correctedContextBudgetMode = 'input';
}
this.pruneMessages = undefined;
this.clearProviderProjection(true);
Expand Down Expand Up @@ -1981,6 +1987,7 @@ export class AgentContext {
}
this.maxContextTokens = this._preOverflowMaxContextTokens;
this._preOverflowMaxContextTokens = undefined;
this.correctedContextBudgetMode = undefined;
this._lastOverflowPromptTokens = undefined;
this._overflowRecoveryAttempts = 0;
}
Expand Down
24 changes: 23 additions & 1 deletion src/agents/__tests__/AgentContext.overflow.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { AIMessage, HumanMessage } from '@langchain/core/messages';
import type * as t from '@/types';
import { messagesStateReducer } from '@/messages/reducer';
import { AgentContext } from '@/agents/AgentContext';
import { Providers } from '@/common';
import { messagesStateReducer } from '@/messages/reducer';

/**
* The overflow-recovery bookkeeping on AgentContext: the budget correction,
Expand All @@ -28,6 +28,28 @@ describe('AgentContext overflow recovery state', () => {
expect(context.pruneMessages).toBeUndefined();
});

it('tracks input-ceiling units after correction and restores configured units on reset', () => {
const context = createContext(32_000);
context.summarizationConfig = {contextBudgetMode: 'window'};
expect(context.summarizationBudgetMode).toBe('window');
context.applyContextBudgetCorrection(26_600);
expect(context.maxContextTokens).toBe(26_600);
expect(context.summarizationBudgetMode).toBe('input');
expect(context.summarizationConfig.contextBudgetMode).toBe('window');
context.reset();
expect(context.maxContextTokens).toBe(32_000);
expect(context.summarizationBudgetMode).toBe('window');
});

it('does not change budget units when an overflow supplies no numeric correction', () => {
const context = createContext(32_000);
context.summarizationConfig = {contextBudgetMode: 'window'};
context.applyContextBudgetCorrection(undefined);
expect(context.summarizationBudgetMode).toBe('window');
context.reset();
expect(context.summarizationBudgetMode).toBe('window');
});

it('keeps one provider projection per run and rebuilds it after overflow', () => {
const context = AgentContext.fromConfig(
{
Expand Down
11 changes: 11 additions & 0 deletions src/graphs/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3133,6 +3133,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
config,
originalToolContent,
estimatedPromptTokens,
windowMessages,
}: {
recovery: OverflowRecoveryPlan;
agentContext: AgentContext;
Expand All @@ -3142,6 +3143,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
originalToolContent?: Map<number, string>;
/** Size of the rejected prompt, recorded to detect a correction that changed nothing. */
estimatedPromptTokens?: number;
windowMessages: BaseMessage[];
}): Partial<t.AgentSubgraphState> {
const previousBudget = agentContext.maxContextTokens;
/**
Expand Down Expand Up @@ -3189,6 +3191,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
remainingContextTokens: 0,
agentId: agentId || agentContext.agentId,
reason: 'overflow',
windowMessages,
allowSummarization,
},
};
Expand Down Expand Up @@ -3225,12 +3228,14 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
agentContext,
agentId,
messageCount,
windowMessages,
contextUsage,
config,
}: {
agentContext: AgentContext;
agentId: string;
messageCount: number;
windowMessages: BaseMessage[];
contextUsage: t.ContextUsageEvent | null;
config: RunnableConfig;
}): Promise<Partial<t.AgentSubgraphState> | null> {
Expand Down Expand Up @@ -3262,6 +3267,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
remainingContextTokens: contextUsage?.remainingContextTokens ?? 0,
agentId: agentId || agentContext.agentId,
reason: 'manual',
windowMessages,
},
/** A checkpointed thread may still hold the previous compaction's
* summary; only the one this run produces may be reported. */
Expand Down Expand Up @@ -3484,6 +3490,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
agentContext,
agentId,
messageCount: messages.length,
windowMessages: context,
contextUsage,
config,
});
Expand Down Expand Up @@ -3541,6 +3548,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
return {
summarizationRequest: {
remainingContextTokens: remainingContextTokens ?? 0,
windowMessages: context,
agentId: agentId || agentContext.agentId,
},
};
Expand Down Expand Up @@ -3570,6 +3578,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
agentContext,
agentId,
messageCount: messages.length,
windowMessages: messagesToUse,
contextUsage: null,
config,
});
Expand Down Expand Up @@ -4596,6 +4605,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
agentId,
config,
originalToolContent: prunedOriginalToolContent,
windowMessages: messagesToUse,
estimatedPromptTokens: recoveryPromptEstimate,
});
}
Expand Down Expand Up @@ -4830,6 +4840,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
agentId,
config,
originalToolContent: prunedOriginalToolContent,
windowMessages: messagesToUse,
estimatedPromptTokens: fallbackRecoveryPromptEstimate,
});
}
Expand Down
49 changes: 49 additions & 0 deletions src/graphs/__tests__/Graph.compactionWindow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Runnable } from '@langchain/core/runnables';
import { AIMessage, AIMessageChunk, HumanMessage } from '@langchain/core/messages';
import type { BaseMessage } from '@langchain/core/messages';
import type * as t from '@/types';
import { StandardGraph } from '@/graphs/Graph';
import { Providers } from '@/common';

class RecordingModel extends Runnable<BaseMessage[], AIMessageChunk> {
lc_namespace = ['tests'];
calls: BaseMessage[][] = [];
async invoke(messages: BaseMessage[]): Promise<AIMessageChunk> {
this.calls.push(messages);
return new AIMessageChunk('answer');
}
}

const count: t.TokenCounter = () => 10;

function makeGraph(mode: 'normal' | 'manual' | 'trigger'): StandardGraph {
return new StandardGraph({
runId: mode, tokenCounter: count,
agents: [{
agentId: 'agent', provider: Providers.OPENAI,
instructions: 'Same system prompt', maxContextTokens: 1_000,
summarizeOnly: mode === 'manual', summarizationEnabled: true,
summarizationConfig: {trigger: {type: 'messages_to_refine', value: mode === 'trigger' ? 1 : 10_000}},
clientOptions: {disableStreaming: true},
}],
});
}

it('manual and automatic requests carry the normal turn’s pruned window, not 720 stored messages', async () => {
const messages: BaseMessage[] = [];
for (let i = 0; i < 360; i++) messages.push(new HumanMessage(`q${i}`), new AIMessage(`a${i}`));
const normal = makeGraph('normal');
const model = new RecordingModel();
normal.overrideModel = model;
const config = {metadata: {langgraph_node: 'agent=agent', langgraph_step: 1}, configurable: {thread_id: 'window'}};
await normal.createCallModel('agent')({messages}, config);
expect(model.calls).toHaveLength(1);
const pruned = model.calls[0].filter(message => message.getType() !== 'system');
expect(pruned.length).toBeLessThan(720);
expect(pruned.length).toBeGreaterThan(0);
for (const mode of ['manual', 'trigger'] as const) {
const graph = makeGraph(mode);
const result = await graph.createCallModel('agent')({messages}, config);
expect(result.summarizationRequest?.windowMessages).toEqual(pruned);
}
});
Loading
Loading