feat(experimental): Postgres session providers with Hyperdrive support#1196
feat(experimental): Postgres session providers with Hyperdrive support#1196mattzcarey wants to merge 2 commits intomainfrom
Conversation
Core session primitives for the agents package: - Session class with tree-structured messages, compaction overlays, context blocks, FTS5 search - Chainable builder: Session.create(agent).withContext(...).withCachedPrompt() - AgentSessionProvider: SQLite-backed with session_id scoping, content column (Think-compatible) - AgentContextProvider: key-value block storage - ContextProvider interface for custom backends (R2, KV, etc.) - Compaction utilities with head/tail protection - Iterative compaction: newer overlays supersede older ones at same fromId - session-memory example with builder API
|
| async searchMessages(query: string, limit = 20): Promise<SearchResult[]> { | ||
| await this.ensureTable(); | ||
| const { rows } = await this.conn.execute( | ||
| `SELECT id, role, content FROM assistant_messages | ||
| WHERE session_id = ? AND MATCH(content) AGAINST(? IN NATURAL LANGUAGE MODE) | ||
| LIMIT ?`, | ||
| [this.sessionId, query, limit] | ||
| ); | ||
| return rows.map((r) => ({ | ||
| id: r.id as string, | ||
| role: r.role as string, | ||
| content: r.content as string, | ||
| createdAt: "" | ||
| })); | ||
| } |
There was a problem hiding this comment.
🔴 PlanetScale searchMessages returns raw JSON blobs instead of extracted text content
The PlanetScaleSessionProvider.searchMessages() selects content from the assistant_messages table, which stores the full JSON.stringify(message) blob (set at planetscale.ts:166-170). This means both the FULLTEXT search and the returned SearchResult.content operate on raw JSON like {"id":"m1","role":"user","parts":[{"type":"text","text":"hello"}]} rather than the actual message text. In contrast, the reference AgentSessionProvider maintains a separate FTS5 table with extracted text parts (packages/agents/src/experimental/memory/session/providers/agent.ts:285-297) and queries that table for search (agent.ts:261-264). This causes two problems: (1) FULLTEXT search matches against JSON structure/keys (e.g., searching for "type" or "parts" would match every message), producing false positives, and (2) SearchResult.content returns an unparseable JSON string where callers expect readable text.
Prompt for agents
In packages/agents/src/experimental/memory/session/providers/planetscale.ts, the searchMessages method (lines 243-257) queries the assistant_messages table directly, which stores full JSON-serialized UIMessage objects. This causes both incorrect search matching (FULLTEXT on JSON blobs) and incorrect output (SearchResult.content is a JSON string instead of readable text).
To fix this properly, mirror the approach used by AgentSessionProvider (packages/agents/src/experimental/memory/session/providers/agent.ts:285-297):
1. Parse the JSON content in the search results to extract actual text parts, similar to how AgentSessionProvider.indexFTS extracts text:
message.parts.filter(p => p.type === 'text').map(p => p.text).join(' ')
2. For the SearchResult.content field, parse each row's content JSON and extract the text parts instead of returning raw JSON.
3. Ideally, consider creating a separate search table (like the agent provider's assistant_fts) that stores extracted text for proper FULLTEXT indexing. Without this, FULLTEXT search will still match against JSON structure, but at minimum the output content should be parsed text.
At minimum, change lines 251-256 to parse the JSON content and extract text parts before returning.
Was this helpful? React with 👍 or 👎 to provide feedback.
f8849ca to
616da1c
Compare
|
Superseded by #1297 (clean branch, rebased on main) |
Summary
Adds Postgres-backed session providers for storing conversation history, context blocks, and searchable knowledge in an external database via Hyperdrive. This enables cross-DO queries, analytics, and shared state without relying on DO SQLite.
What's new
Providers (
packages/agents/src/experimental/memory/session/providers/)PostgresSessionProvider— tree-structured messages, compaction overlays, message FTS via tsvectorPostgresContextProvider— writable context block storage (memory, cached prompt)PostgresSearchProvider— searchable knowledge base with tsvector + GIN indexFramework improvements (
packages/agents/src/experimental/memory/session/)Session.create()acceptsSessionProviderfor external storage (in addition toSqlProviderfor DO SQLite)set_contextAPI: removedkeyparam, auto-generates keys fromtitleor content slugfreezeSystemPrompt()returns cached,refreshSystemPrompt()force-reloads from providersclearMessages()callsrefreshSystemPrompt()to invalidate the cached promptappendToBlock()adds newline separator between entries[readonly],[searchable],[loadable],[not searchable]get()returns entry count only (no key listing)Example (
experimental/session-planetscale/)pgdriverwrapPgClienthelper converts?placeholders to$1, $2, ...for pg compatibilityTests (
packages/agents/src/tests/experimental/memory/session/postgres-providers.test.ts)convertToModelMessagescompatibility, prompt lifecycle (freeze/refresh/invalidation/concurrent)Docs (
docs/sessions.md)Migration SQL
Customers run this once — providers never create tables:
Test plan