Skip to content

fix: Keep Per-Conversation Context and the Summary at a Fixed System Position - #613

Open
Shabonasar wants to merge 6 commits into
LibreChat-AI:mainfrom
Shabonasar:fix/prompt-cache-stable-dynamic-placement
Open

Shabonasar wants to merge 6 commits into
LibreChat-AI:mainfrom
Shabonasar:fix/prompt-cache-stable-dynamic-placement

Conversation

@Shabonasar

@Shabonasar Shabonasar commented Oct 15, 2026 •

Copy link
Copy Markdown

Summary

With Anthropic or OpenRouter prompt caching, AgentContext moves the dynamic system part into a user message before the newest human message. Context that is fixed for a whole conversation (start time, memory, file lists) moves with it on every turn, so the previous exchange is written to the cache again. A summary created by a compaction inside a run is sent as a user message, while the next run renders the same summary in system, so every compaction costs a second prefix rebuild.

On Bedrock with promptCache the dynamic part stays at the end of the system text, in front of the whole history, so a per-turn change there misses every message cache point (reported in LibreChat-AI/LibreChat#16877).

This PR adds AgentInputs.conversation_instructions, kept in the system message right after instructions and never moved, and gives the summary one fixed placement when the field is set. On Bedrock with promptCache the dynamic tail moves behind the cached history, as it already does for Anthropic, within four cache points. A tool result keeps the same bytes when the tail cache marker moves on. As a result, request N+1 starts with request N up to the per-turn part, and after a compaction the run and the next run send the same prefix.

Related to LibreChat-AI/LibreChat#16877 (Bedrock part) and LibreChat-AI/LibreChat#17102 (tracking; design discussion LibreChat-AI/LibreChat#17481). Base: main, no dependency on other PRs.

How it works

S static instructions, T per-conversation context, D per-turn context, bp cache breakpoint, cp Bedrock cache point.

before (T in additional_instructions, moved under prompt caching):
  turn 3:  [tools][S|bp] u1 a1 u2 a2 [T D] u3                 T shifts every turn
  Bedrock: [tools][S|cp][D] u1 a1 u2 a2 u3                    D changes in front of the history

after:
  turn 3:  [tools][S|bp][T|bp] u1 a1 u2 a2 [D3] u3            request N+1 starts with request N
  Bedrock: [tools|cp][S|cp][T] u1 a1 u2 a2|cp [D3] u3|cp      at most 4 cache points
  compaction: [tools][S][T][summary] retained turns ...       same bytes in the run and in the next run
field lifetime Anthropic / OpenRouter with promptCache Bedrock with promptCache other providers
instructions every conversation of the agent system block with a breakpoint system block + cache point system text
conversation_instructions (new) one conversation next system block with a breakpoint next system block, + cache point while one is left appended to the system text
additional_instructions one turn user message ahead of the newest human message message ahead of the newest human message (was: end of system) appended to the system text
  • The conversation block gets its own breakpoint (cache point on Bedrock) only while one of the four is left after tools, the stable block and the markers already in the messages; otherwise the history breakpoint after it covers it.
  • With the field set, the summary (created in the run or restored as initialSummary) is one user message right after the system message, stable until the next compaction, and counts as instruction overhead. setSummary's precedesMessages is ignored then.
  • Tool results: an Anthropic tool_result that is one plain text block is sent as its string; OpenRouter sends every non-empty tool message as one text block. Both match what the tail cache marker produces, so the bytes stay the same before and after the marker moves.

Type of change

  • Bug fix
  • Feature

Testing

Tested environments/configuration:

scenario cached share misses notes
tools with thinking 87.0% 0
G2: tools toggled mid-chat 85.5% 0
G3: compaction, max context 12000, 4 summarizations 44.7% 8 all explained: new or rewritten summary 3, tool set switched 4, truncated old tool result 1; uncached prompt tokens 67479 (73-82k before)

Automated tests:

npx jest src/messages src/llm src/graphs src/tools src/agents src/summarization
npx tsc --noEmit
  • This PR: 4804 passed. 3 suites fail because they need API keys (anthropic, google, vertexai llm.spec.ts).
  • AgentContext.test.ts: Bedrock tail moved out of system, history marked at both ends of the moved tail, the conversation block's cache point only while one of four is left, a compaction summary and an initial summary rendered as the same first message. tool-id-normalization.test.ts, messages.test.ts: tool result bytes stay the same when the marker moves.
  • All five SDK PRs together: 6987 passed. Failing: the 3 suites above, plus durability-checkpoint.integration.test.ts and persistent-handoffs.test.ts, which need MongoDB and fail on main too.
  • LibreChat agent suites against all five together: packages/api src/agents 6913 passed (1 failure in handlers.shutdown.spec.ts, a shutdown timing test); api server/controllers/agents 1371 passed.

Risk / compatibility

Checklist

  • I reviewed my own changes
  • Relevant tests have been added or updated
  • Existing relevant tests pass
  • The change does not introduce new warnings or errors
  • User-facing or complex behavior is documented where necessary
  • Required dependency changes have been merged/published (none)

…Position

Adds conversation_instructions: context that is stable within a conversation.
It stays in the system message right after the static instructions and is never
moved: its own content block with a cache breakpoint for Anthropic and
OpenRouter, a block followed by a cache point for Bedrock, appended text for
other providers. additional_instructions keeps its per-turn behaviour.

A host that passes conversation_instructions gets the compaction summary, both
when created inside a run and when restored as initialSummary, as one stable
user message right after the system message, with a breakpoint of its own while
the request has one left. Token accounting counts it as instruction overhead.
Both paths render the same carrier text, so the requests after a compaction
form one continuous prefix. Hosts that do not pass the field keep the earlier
placement and accounting.

Adds a prompt cache harness (src/specs/prompt-cache) that runs declarative
scenarios through Run and each provider class, captures the serialized request
before the network, and asserts the cache invariants per provider.
With prompt caching on Bedrock, additional_instructions was appended to the system
message after its last cachePoint. Bedrock reads tools, system and messages in that
order, so a tail that changes between turns sits in front of the whole history and
every message checkpoint misses.

The tail now moves to a message right before the newest user message, as for
Anthropic and OpenRouter, with one cachePoint at the end of the history in front of it
and one at the end of the request. The conversation block keeps its own cachePoint
only while the four-checkpoint limit leaves room for it (tools, stable block, history,
request end come first). Without prompt caching the tail stays in the system message.
…Marks Two Messages

The graph can place a second message checkpoint for the latest human message. Beside the
system checkpoints and the tool list that overran the limit of four in two places:
- the conversation block kept its own cachePoint only on the moved-tail path; the Bedrock
  system message now drops it whenever the checkpoints already in the messages leave no
  room, with or without a moved tail;
- the opening message of the history in front of a moved tail kept the graph's checkpoint
  on top of the two the tail path places; it is stripped now.
@Shabonasar

Copy link
Copy Markdown
Author

Added: Bedrock tail placement (reported for Bedrock in LibreChat-AI/LibreChat#16877).

With promptCache on Bedrock, a non-empty dynamic tail was appended to the system text after the last cachePoint, so every message checkpoint cached the prefix together with it and the whole history missed whenever the tail changed. Now the tail goes as a message in front of the newest user message, as for Anthropic; checkpoints: tools, stable system, end of the history before the tail, end of the request (Bedrock limit of 4). The conversation block keeps its own checkpoint when the budget allows. Without promptCache nothing changes.

Compatibility with #631: the system block budgets its checkpoints against every checkpoint already placed in the messages, so #613 + #631 stays within 4 with or without a moved tail. With a moved tail #613 places its own two message checkpoints and never marks the tail; #631's second checkpoint applies to requests without one. Note: getBedrockMessageCachePointBudget runs before the system message is prepended, so it cannot see the system checkpoints on its own.

Delta: Shabonasar/agents@7676461...83eea26 — 872 tests pass on this branch, 1935 with #631 merged on top (4 failed the at-most-4 check before the fix). Not run against a live Bedrock endpoint.

…he cached prefix

Per-conversation instructions keep a fixed system position, the compaction summary has one placement, and the Bedrock dynamic tail moves in front of the newest user message within four cachePoints. Tool results keep the same bytes when the tail marker moves.
@Shabonasar

Copy link
Copy Markdown
Author

Reduced to simplify dependencies and the review: based on main only, no dependency on other PRs, +1796 -> +450 lines.

No file changes (tree of main c56fc5e). Joins an earlier snapshot that the branches of
the series share (83eea26), so any two of them have a single merge base and merge cleanly.
@Shabonasar

Copy link
Copy Markdown
Author

Branch updated with a merge commit (no file changes) so that the agents PRs of LibreChat-AI/LibreChat#17102 merge together with a plain git merge; the PR diff is unchanged. All five together: integration/prompt-cache.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant