Repository navigation
agents/context: cache the system prompt by default and restore the fluent withContext API #2772
Copy link
Copy link
Open
Labels
enhancementNew feature or requestNew feature or requestpriority: highHigh impact: fix soonHigh impact: fix soon
Description
ContextBlocks from agents/context replaced Session.create(this).withContext(...).withCachedPrompt() when sessions moved into a Lifecycle capability (#2196). The new API is harder to use on a plain Durable Object, and it doesn't cache the system prompt unless you know to pass a positional argument.
I hit this while rewriting the Agent Memory get started guide (cloudflare/cloudflare-docs#34730) for a plain DurableObject with Lifecycle. I ended up dropping agents/context from the guide and using a plain AI SDK tool, because ContextBlocks took more explaining than the feature it added.
Before
session = Session.create(this)
.withContext("soul", { provider: { get: async () => "You are helpful." } })
.withContext("memory", { description: "Learned facts", maxTokens: 1100 })
.withCachedPrompt();
const system = await this.session.freezeSystemPrompt();
const tools = await this.session.tools();- Blocks were declared by name, one call each.
- A block without a provider was backed by the agent's SQLite.
.withCachedPrompt()said what it did: persist the frozen prompt.- Compaction refreshed the prompt for you.
Now
readonly sessions = new Sessions();
readonly lifecycle = Lifecycle.install(this).use(this.sessions);
readonly session = this.sessions.session();
readonly context = new ContextBlocks(
[
{ label: "soul", provider: { get: async () => "You are helpful." } },
{ label: "memory", description: "Learned facts", maxTokens: 1100 },
],
new AgentContextProvider(this.lifecycle, "_system_prompt"), // the cache
(label) => new AgentContextProvider(this.lifecycle, label), // blocks without a provider
);What's wrong with it:
- The prompt isn't cached by default. Without the second argument,
freezeSystemPrompt()keeps the prompt in memory only. After an eviction it renders again, and any block whose content changed since (a searchable block's entry count, a writable memory block) produces a different prompt and busts the provider's prefix cache. That's the thingwithCachedPrompt()existed to prevent, and nothing warns you that it's off. - Caching is a positional argument with a magic label.
new AgentContextProvider(this.lifecycle, "_system_prompt")doesn't say "cache my prompt". It works becauselifecycle.sqlhappens to match theSqlProvidershape. Think hard-codes the same_system_promptlabel internally. - Blocks without a provider need a third positional factory. Before,
withContext("memory", { maxTokens })was backed by SQLite automatically. Now it silently has no provider unless you passdefaultProvider. - It isn't a capability.
Sessions,Scheduler, andWebSocketsattach with.use().ContextBlocksis a loose object that reaches into Lifecycle storage, so it gets no startup hook, nolifecycle.ready(), and a different construction style from everything around it. - Compaction no longer refreshes the prompt.
Sessionused to callrefreshSystemPrompt()after compacting. Think does that itself now, but a plain Durable Object usingSessionsandContextBlockshas to wire it by hand, and the docs don't say so. - Tool names are implicit. A provider with
search()becomes asearch_contexttool that never appears in user code, yet the system prompt has to name it.
Proposal
- Make
ContextBlocks(or aContextcapability) cache the frozen prompt in the object's SQLite by default when installed on a Lifecycle, with an opt-out. No magic label in user code. - Bring back the fluent shape:
Blocks without a provider are SQLite-backed, as before.
readonly context = new Context() .withBlock("soul", { provider: { get: async () => "You are helpful." } }) .withBlock("memory", { description: "Learned facts", maxTokens: 1100 }); readonly lifecycle = Lifecycle.install(this).use(this.sessions).use(this.context);
- Install it with
.use()so it gets storage,ready(), and startup like other capabilities. - Offer a one-line link to a session so compaction refreshes the prompt, for example
context.refreshOn(session), or document the manual wiring. - Let users name the generated tools, or at least export the names as constants, so prompts don't hard-code
search_context.
agents/context is experimental, so this is the time to change it.
Reacted by github-actions
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or requestpriority: highHigh impact: fix soonHigh impact: fix soon
Activity
github-actions commentedon Oct 29, 2026
Thanks for the detailed write-up. I triaged this against
main.Verified: in
packages/agents/src/context/blocks.ts,ContextBlocksonly persists the frozen prompt when apromptStoreis passed (freezeSystemPrompt). Blocks with no provider get no storage unlessdefaultProvideris passed. Without a store, an eviction re-renders the prompt and can bust the prefix cache.Recommendation: make it a capability installed with
.use(). It would cache the prompt in Lifecycle SQLite by default, with an opt-out, and back provider-less blocks with SQLite. Keep the existing block config shape and add a documented compaction refresh hook. Skip the fluentwithBlockrebuild and tool renaming for now: they add API surface for little gain.Next: a maintainer decides. Adding
gardener-fixwill have this built.mattzcarey commentedon Oct 29, 2026
#2773 takes a different route from the one in this issue. Context stays separate from
Sessions, but it's bound to a session, and the transcript records what the model was sent:storageoption), as before.ThinkHarnesstakes acontextoption, and Think's context runs on the same class.SET_CONTEXT_TOOLandSEARCH_CONTEXT_TOOLare exported fromagents/context.feat(sessions): declare context blocks on the session handle