Skip to content

feat(context): bind context blocks to a session, recorded in the transcript - #2773

Open
mattzcarey wants to merge 7 commits into
mainfrom
feat/session-context
Open

mattzcarey wants to merge 7 commits into
mainfrom
feat/session-context

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

agents/context gets Context (experimental): context blocks bound to a session. The transcript records the system prompt each turn was sent with and every context change delivered after it, and each model request is a projection of that record. Think's context now runs on it, ThinkHarness takes a context option, and Sessions is unchanged.

readonly context = new Context(this.session, {
  storage: this.ctx.storage,
  blocks: [
    { label: "soul", provider: { get: async () => "You are helpful." } },
    { label: "memory", description: "Learned facts", maxTokens: 1100 },
    { label: "environment", provider: environment, whenChanged: "remind" }
  ]
});

// each turn, once the user message is stored
const { system, messages } = await this.context.build();
const tools = await this.context.tools();

Context never rewrites what the model has already been sent:

  • the first turn records a snapshot of the blocks on its user message; every later request opens with that exact prompt until a refresh records a new one
  • a remind block that changed is delivered after the user message of the turn that noticed it, and recorded there, so later requests render it in the same place instead of moving it to the newest user message every turn
  • a user message that already has an answer (a regeneration) is never recorded on; the change rides on that request only
  • block values stay in their providers (the existing cf_agents_context_blocks rows), so nothing migrates; a small state row keeps a pending refresh and a pointer to the latest snapshot, so a refresh survives eviction and a bounded history window still opens with the recorded prompt

ContextBlocks keeps only blocks and tools: freezeSystemPrompt, refreshSystemPrompt, reminder, and the prompt store move to Context. ThinkHarness reserves the context metadata key, so client input cannot carry a record.

This replaces the _system_prompt row and the moving reminder (#2772). pi and Claude Code work the same way: context changes land at turn boundaries and stay where they were delivered. Think's prompt-cache test shows the result: a turn after a reminder now extends the previous request instead of losing the last user message from the cache.

Agents workers, Think workers, ThinkHarness, and ai-chat tests ran, plus pnpm run check. Context is covered by integration tests on a real Durable Object and session. Docs: cloudflare/cloudflare-docs#34718.

@changeset-bot

changeset-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bde8a57

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
agents Minor
@cloudflare/think Patch
@cloudflare/agent-think Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2773

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2773

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2773

hono-agents

npm i https://pkg.pr.new/hono-agents@2773

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2773

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2773

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2773

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2773

commit: bde8a57

devin-ai-integration[bot]

This comment was marked as resolved.

@mattzcarey mattzcarey changed the title feat(sessions): declare context blocks on the session handle feat(context): bind context blocks to a session, recorded in the transcript Oct 29, 2026
@agent-think

agent-think Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

🟢 agents import sizes: 1 entry point changed, no growth

Entry point Exports Largest gzip change Size now
🆕 agents/context 4 new — 95.4 KiB
Changed exports (4)
Import Gzip change Size now
🆕 agents/context#Context — 95.4 KiB
🆕 agents/context#CONTEXT_METADATA_KEY — 95 B
🆕 agents/context#SET_CONTEXT_TOOL — 89 B
🆕 agents/context#SEARCH_CONTEXT_TOOL — 88 B
How this works

Each runtime export is bundled on its own, minified, and gzipped. Changes smaller than 100 B, or smaller than 1% and 1 KiB, are ignored. Growth over 10% or 5 KiB is marked 🔴. This report is informational and does not fail CI. The workflow artifact contains every measurement.

Compared 54ae3e86 → bde8a574 · workflow run · reported by agent-think[bot]

devin-ai-integration[bot]

This comment was marked as resolved.

Bring back withContext()/withCachedPrompt() on the Sessions handle. The
session stores the frozen prompt by default, backs provider-less blocks
with SQLite, supports searchable: true, and refreshes the prompt after
compact() and clearMessages(). Think declares its blocks on the default
session and its context methods are no longer deprecated.

Refs #2772
…script

Context records the system prompt snapshot and each delivered remind
change on the user message of the turn, and each request is a projection
of that record, so earlier turns are sent unchanged and the prefix cache
holds. Sessions is unchanged. Think's context runs on Context, ThinkHarness
takes a context option, and the harness-backed Think plugs configureContext
into it.
…uations, and forged records

- keep a pending refresh and a pointer to the latest snapshot in the block
  storage, so a refresh survives eviction and a bounded history window still
  opens with the recorded prompt
- never record on a user message that already has an answer (regeneration);
  the change rides on that request only
- ThinkHarness prepares context on every model call, placing changes it could
  not record, and reserves the context metadata key so clients cannot forge it
- update the workspace skills test to assert the delivered request
@mattzcarey
mattzcarey force-pushed the feat/session-context branch from eed362f to f10d0cb Compare October 29, 2026 17:44
devin-ai-integration[bot]

This comment was marked as resolved.

… state

- ContextBlocks keeps blocks and tools only: freezeSystemPrompt,
  refreshSystemPrompt, reminder and the prompt store are gone; Context
  records the prompt in the session, and renderSystemPrompt() renders the
  blocks without storing anything
- replace the in-memory unit tests for the old frozen prompt, provider
  init, and search tool with integration tests on a real Durable Object
  and session
- mark the context exports and the ThinkHarness context option experimental
  and update the changeset
devin-ai-integration[bot]

This comment was marked as resolved.

…st it adversarially

- each change record names its snapshot and carries everything delivered
  since, and lookups take the newest record on the stored path to the
  history's last message, so a bounded window sees exactly what the full
  history does: never a prompt from a later turn or another branch, and
  not a changed answer once compaction hides a record
- reserve the _context_state label for Context's own state
- add an invariant test that drives random sessions on a real Durable
  Object (turns, block changes, refreshes, regenerations, compactions) and
  checks every window against the full path after every step
devin-ai-integration[bot]

This comment was marked as resolved.

…at requests extend each other

- a snapshot taken after a refresh used the values loaded when the refresh
  was asked for, so a block changed in between went out stale; always
  reload when recording a snapshot
- add an invariant test: in random sessions on a real Durable Object, each
  turn's request extends the previous one exactly (what the provider is
  sent), except after a refresh, a compaction, or a branch switch, and a
  change is delivered once per branch
…compacted leaf

- every turn's user message carries a record or a pointer to one, and the
  lookup walks the stored path to the nearest user message that has one,
  so there is no bounded index for an old branch to fall out of
- a history that ends in a compaction summary walks from the stored
  message the compaction ends at
- tests: regenerate an old turn after 300 later changes, compaction that
  reaches the last message, and invariants that compact to the end
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