Skip to content

feat(experimental): Session API — messages, context blocks, compaction, search#1166

Open
mattzcarey wants to merge 6 commits intomainfrom
feat/session-api-core
Open

feat(experimental): Session API — messages, context blocks, compaction, search#1166
mattzcarey wants to merge 6 commits intomainfrom
feat/session-api-core

Conversation

@mattzcarey
Copy link
Contributor

@mattzcarey mattzcarey commented Mar 23, 2026

Summary

  • Session class: tree-structured messages, non-destructive compaction overlays, context blocks with frozen system prompt, FTS5 search
  • AgentSessionProvider: SQLite-backed with session_id scoping for multi-session
  • AgentContextProvider: key-value block storage in SQLite
  • session.freezeSystemPrompt() — render once, persist, survives DO eviction
  • session.tools()update_context tool for AI block writes
  • createCompactFunction — reference compaction with head/tail protection
  • Chainable builder API: Session.create(agent).withContext(...).withCachedPrompt() — auto-wires SQLite providers, no manual provider construction needed
  • .forSession(id) namespaces all provider keys automatically
  • Lazy provider resolution — chain order doesn't matter
// Before
const session = new Session(new AgentSessionProvider(this), {
  context: [
    { label: "memory", maxTokens: 1100, provider: new AgentContextProvider(this, "memory") },
    { label: "soul", defaultContent: "You are helpful.", readonly: true },
  ],
  promptStore: new AgentContextProvider(this, "_system_prompt"),
});

// After
const session = Session.create(this)
  .withContext("memory", { maxTokens: 1100 })
  .withContext("soul", { defaultContent: "You are helpful.", readonly: true })
  .withCachedPrompt();

Stack

1/4 — this PR
2/4 — #1167 SessionManager (depends on this)
3/4 — #1168 Examples (depends on 2)
4/4 — #1169 Think integration (depends on 3)

Test plan

  • 20 unit tests: context blocks, frozen prompt, tools, builder API with namespacing
  • 12 provider tests: tree structure, branching, compaction overlays, FTS search

…n, search

Session API for persistent agent memory:

- Session: tree-structured messages (branching via parent_id), non-destructive
  compaction overlays, context blocks with frozen system prompt, FTS5 search
- AgentSessionProvider: SQLite-backed provider with session_id scoping for
  multi-session isolation within shared tables
- AgentContextProvider: key-value block storage in SQLite (memory, todos, etc)
- ContextBlocks: frozen snapshot system prompt (hermes-style plain text format),
  lazy loading, update_context tool with live usage feedback
- session.freezeSystemPrompt() — render once, persist to SQLite, survives DO eviction
- session.tools() — returns update_context tool for AI block writes
- session.replaceContextBlock/appendContextBlock — direct block access
- truncateOlderMessages — read-time tool output truncation
- createCompactFunction — reference compaction (head/tail protection, structured summary)
- 28 tests: frozen prompt, block writes, tool execute, provider ops, branching,
  compaction overlays, FTS search, persistence
@changeset-bot
Copy link

changeset-bot bot commented Mar 23, 2026

⚠️ No Changeset found

Latest commit: b04e2ec

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment on lines +161 to +165
this.agent.sql`
INSERT OR IGNORE INTO assistant_messages (id, session_id, parent_id, role, message)
VALUES (${message.id}, ${this.sessionId}, ${parent}, ${message.role}, ${json})
`;
this.indexFTS(message);
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 FTS5 indexing creates duplicate entries on idempotent append, violating documented contract

appendMessage uses INSERT OR IGNORE for the main table (line 170), correctly skipping duplicates. However, indexFTS is called unconditionally on line 173, regardless of whether the INSERT actually inserted a row. Inside indexFTS (line 277-280), INSERT OR REPLACE INTO assistant_fts is used — but FTS5 virtual tables don't enforce uniqueness on UNINDEXED columns like id. The only conflict trigger would be an explicit rowid collision, but no rowid is specified, so each call always creates a new FTS row. This means calling appendMessage twice with the same message ID creates duplicate FTS entries, causing searchMessages to return duplicate results. This violates the documented interface contract at provider.ts:50-51: "Idempotent — same message.id twice is a no-op."

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Session.create(agent) returns a Session directly — no .build() needed.
Builder methods (withContext, withCachedPrompt, forSession) configure
the session before first use. Providers are resolved lazily in
_ensureReady() so chain order doesn't matter.

- withContext: auto-creates AgentContextProvider for writable blocks
- withCachedPrompt: auto-creates prompt store provider
- forSession: namespaces all auto-created provider keys by session ID
- Fix empty DO binding in wrangler.jsonc that broke test runner
devin-ai-integration[bot]

This comment was marked as resolved.

…s export

- Add ensureTable() call in addCompaction (could fail if called before any read)
- Delete FTS entries in deleteMessages (was leaving stale search results)
- Add package.json export for agents/experimental/memory/utils
devin-ai-integration[bot]

This comment was marked as resolved.

…ompat

- Remove unused imports (MessageQueryOptions, estimateStringTokens, etc.)
- Fix AI SDK v6: parameters → inputSchema, maxSteps → stopWhen
- Cast tool-result part via unknown for v6 UIMessage types
- Add ToolExecuteFn type alias for cleaner test casts
- Move session-memory example update to this PR (fixes typecheck)
- Remove invalid Button props (shape, loading)
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 23, 2026

Open in StackBlitz

agents

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

@cloudflare/ai-chat

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

@cloudflare/codemode

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

hono-agents

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

@cloudflare/shell

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

@cloudflare/think

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

@cloudflare/voice

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

@cloudflare/worker-bundler

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

commit: b04e2ec

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 15 additional findings in Devin Review.

Open in Devin Review

Comment on lines +154 to +158
"./experimental/memory/utils": {
"types": "./dist/experimental/memory/utils/index.d.ts",
"import": "./dist/experimental/memory/utils/index.js",
"require": "./dist/experimental/memory/utils/index.js"
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Missing changeset for new public API exports in packages/agents

The repository's AGENTS.md states: "Changes to packages/ that affect the public API or fix bugs need a changeset" and "Every new public export needs: an entry in package.json exports, a build entry in scripts/build.ts, and a changeset." This PR adds a new ./experimental/memory/utils export path to packages/agents/package.json (line 154-158), adds a corresponding build entry in scripts/build.ts (line 21), and significantly modifies the ./experimental/memory/session exports. No changeset file was created in .changeset/.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

- getPathLength: validate leafId against session_id
- latestLeafRow: scope LEFT JOIN children to same session
- needsCompaction: use getHistory().length (post-compaction count)
- compact example: skip synthetic compaction_ IDs in removed set
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 15 additional findings in Devin Review.

Open in Devin Review

Comment on lines +55 to 60
async chat(message: string): Promise<UIMessage> {
this.session.appendMessage({
id: `user-${crypto.randomUUID()}`,
role: "user",
parts: [{ type: "text", text: message }]
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Client and server create user messages with different IDs, causing ID instability on reload

The client creates an optimistic user message with id: \user-${crypto.randomUUID()}`atclient.tsx:134and adds it to local state. The server'schat()method atserver.ts:57 creates its own user message with a **different** UUID. The old code passed the client's message ID to the server (agent.call("chat", [text, userMsg.id])), but the new code only sends the text (agent.call("chat", [text])). This means after any reload — reconnect (client.tsx:119), compact (client.tsx:171`), or page refresh — the user message IDs change because they're loaded from the server's versions. React re-mounts all user message elements due to key changes, and any code relying on stable message IDs across client and server will break.

Prompt for agents
In experimental/session-memory/src/server.ts, the chat() method should accept an optional message ID from the client to keep user message IDs in sync, restoring the old behavior.

Change the chat method signature at line 55 to accept a second parameter:
  async chat(message: string, messageId?: string): Promise<UIMessage>

Then on line 57, use the client-provided ID if available:
  id: messageId ?? `user-${crypto.randomUUID()}`

In experimental/session-memory/src/client.tsx at line 140, pass the user message ID to the server:
  const msg = await agent.call<UIMessage>("chat", [text, userMsg.id]);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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