Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
- [Server-Driven Messages](./server-driven-messages.md) - Autonomous agent workflows: scheduled follow-ups, queue processing, webhooks, chained reasoning
- TODO: [Using AI Models](./using-ai-models.md) - OpenAI, Anthropic, Workers AI, and other providers
- TODO: [RAG (Retrieval Augmented Generation)](./rag.md) - Vector search with Vectorize
- [Sessions (Experimental)](./sessions.md) - Persistent conversation storage with tree-structured messages, context blocks, compaction, and search
- [Workspace (Experimental)](./workspace.md) - Durable virtual filesystem backed by SQLite + R2
- [Codemode (Experimental)](./codemode.md) - LLM-generated executable code for tool orchestration
- [Client Tools Continuation](./client-tools-continuation.md) - Handling tool calls across client/server
Expand Down
794 changes: 794 additions & 0 deletions docs/sessions.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions experimental/session-memory/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export class ChatAgent extends Agent<Env> {
const result = streamText({
model: this.getAI(),
system: await this.session.freezeSystemPrompt(),
messages: await convertToModelMessages(truncated),
messages: await convertToModelMessages(truncated as UIMessage[]),
tools: await this.session.tools(),
stopWhen: stepCountIs(5)
});
Expand Down Expand Up @@ -140,7 +140,7 @@ export class ChatAgent extends Agent<Env> {

@callable()
getMessages(): UIMessage[] {
return this.session.getHistory();
return this.session.getHistory() as UIMessage[];
}

@callable()
Expand Down
1 change: 0 additions & 1 deletion experimental/session-multichat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ SessionManager.create(agent)
.onCompaction(fn) // register compaction function
.compactAfter(tokenThreshold) // auto-compact threshold
.withCachedPrompt(provider?) // cache frozen system prompt
.maxContextMessages(count) // limit context messages per session
```

## Session Lifecycle
Expand Down
4 changes: 2 additions & 2 deletions experimental/session-multichat/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class MultiSessionAgent extends Agent<Env> {
const result = streamText({
model: this.getAI(),
system: await session.freezeSystemPrompt(),
messages: await convertToModelMessages(truncated),
messages: await convertToModelMessages(truncated as UIMessage[]),
tools: { ...(await session.tools()), ...this.manager.tools() },
stopWhen: stepCountIs(5)
});
Expand Down Expand Up @@ -149,7 +149,7 @@ export class MultiSessionAgent extends Agent<Env> {

@callable()
getHistory(chatId: string): UIMessage[] {
return this.manager.getSession(chatId).getHistory();
return this.manager.getSession(chatId).getHistory() as UIMessage[];
}

@callable()
Expand Down
4 changes: 2 additions & 2 deletions experimental/session-search/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class SearchAgent extends Agent<Env> {
const result = streamText({
model: this.getAI(),
system: await this.session.freezeSystemPrompt(),
messages: await convertToModelMessages(truncated),
messages: await convertToModelMessages(truncated as UIMessage[]),
tools: await this.session.tools(),
stopWhen: stepCountIs(5)
});
Expand Down Expand Up @@ -135,7 +135,7 @@ export class SearchAgent extends Agent<Env> {

@callable()
getMessages(): UIMessage[] {
return this.session.getHistory();
return this.session.getHistory() as UIMessage[];
}

@callable()
Expand Down
1 change: 0 additions & 1 deletion packages/agents/src/experimental/memory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export {
Session,
AgentSessionProvider,
AgentContextProvider,
type MessageQueryOptions,
type SessionProvider,
type SearchResult,
type StoredCompaction,
Expand Down
13 changes: 7 additions & 6 deletions packages/agents/src/experimental/memory/session/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
* - SearchProvider (get+search+set?) → searchable via search_context tool
*/

import { jsonSchema, type ToolSet } from "ai";
import type { ToolSet } from "ai";
import { z } from "zod";
import { estimateStringTokens } from "../utils/tokens";
import { isSearchProvider, type SearchProvider } from "./search";
import { isSkillProvider, type SkillProvider } from "./skills";
Expand Down Expand Up @@ -651,9 +652,9 @@ export class ContextBlocks {

toolSet.set_context = {
description: `Write to a context block. Available blocks:\n${blockDescriptions.join("\n")}\n\nWrites are durable and persist across sessions.`,
inputSchema: jsonSchema({
inputSchema: z.fromJSONSchema({
type: "object" as const,
properties: properties as Record<string, object>,
properties: properties as Record<string, Record<string, unknown>>,
required
}),
execute: async ({
Expand Down Expand Up @@ -713,7 +714,7 @@ export class ContextBlocks {
"Available skill blocks: " +
skillLabels.map((l) => `"${l}"`).join(", ") +
". Check the system prompt for available keys.",
inputSchema: jsonSchema({
inputSchema: z.fromJSONSchema({
type: "object" as const,
properties: {
label: {
Expand Down Expand Up @@ -746,7 +747,7 @@ export class ContextBlocks {
(loadedList.length > 0
? " Currently loaded: " + loadedList.join(", ") + "."
: " No skills currently loaded."),
inputSchema: jsonSchema({
inputSchema: z.fromJSONSchema({
type: "object" as const,
properties: {
label: {
Expand Down Expand Up @@ -782,7 +783,7 @@ export class ContextBlocks {
"Available searchable blocks: " +
searchLabels.map((l) => `"${l}"`).join(", ") +
".",
inputSchema: jsonSchema({
inputSchema: z.fromJSONSchema({
type: "object" as const,
properties: {
label: {
Expand Down
6 changes: 5 additions & 1 deletion packages/agents/src/experimental/memory/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,8 @@ export type { SearchProvider } from "./search";
export { AgentSearchProvider, isSearchProvider } from "./search";
export type { SkillProvider } from "./skills";
export { isSkillProvider, R2SkillProvider } from "./skills";
export type { MessageQueryOptions, SessionOptions } from "./types";
export type {
SessionMessage,
SessionMessagePart,
SessionOptions
} from "./types";
45 changes: 15 additions & 30 deletions packages/agents/src/experimental/memory/session/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
* Cross-session search and tools.
*/

import type { UIMessage } from "ai";
import { jsonSchema, type ToolSet } from "ai";
import type { ToolSet } from "ai";
import { z } from "zod";
import type { CompactResult } from "../utils/compaction-helpers";
import type { WritableContextProvider } from "./context";
import type { StoredCompaction } from "./provider";
import type { SqlProvider } from "./providers/agent";
import type { SearchProvider } from "./search";
import { Session, type SessionContextOptions } from "./session";
import type { SessionMessage } from "./types";

export interface SessionInfo {
id: string;
Expand All @@ -35,27 +36,23 @@ interface PendingManagerContext {
options: SessionContextOptions;
}

export interface SessionManagerOptions {
maxContextMessages?: number;
}
export interface SessionManagerOptions {}

export class SessionManager {
private agent!: SqlProvider;
private _maxContextMessages = 100;
private _pending: PendingManagerContext[] = [];
private _cachedPrompt?: WritableContextProvider | true;
private _compactionFn?:
| ((messages: UIMessage[]) => Promise<CompactResult | null>)
| ((messages: SessionMessage[]) => Promise<CompactResult | null>)
| null;
private _tokenThreshold?: number;
private _sessions = new Map<string, Session>();
private _historyLabel?: string;
private _tableReady = false;
private _ready = false;

constructor(agent: SqlProvider, options: SessionManagerOptions = {}) {
constructor(agent: SqlProvider, _options: SessionManagerOptions = {}) {
this.agent = agent;
this._maxContextMessages = options.maxContextMessages ?? 100;
this._ready = true;
this._ensureTable();
}
Expand All @@ -69,7 +66,7 @@ export class SessionManager {
* .withContext("soul", { provider: { get: async () => "You are helpful." } })
* .withContext("memory", { description: "Learned facts", maxTokens: 1100 })
* .withCachedPrompt()
* .maxContextMessages(50);
* .compactAfter(100_000);
*
* // Each getSession(id) auto-creates namespaced providers:
* // memory key: "memory_<sessionId>"
Expand All @@ -80,7 +77,6 @@ export class SessionManager {
static create(agent: SqlProvider): SessionManager {
const mgr: SessionManager = Object.create(SessionManager.prototype);
mgr.agent = agent;
mgr._maxContextMessages = 100;
mgr._pending = [];
mgr._compactionFn = null;
mgr._tokenThreshold = undefined;
Expand All @@ -102,17 +98,12 @@ export class SessionManager {
return this;
}

maxContextMessages(count: number): this {
this._maxContextMessages = count;
return this;
}

/**
* Register a compaction function propagated to all sessions.
* Called by `Session.compact()` to compress message history.
*/
onCompaction(
fn: (messages: UIMessage[]) => Promise<CompactResult | null>
fn: (messages: SessionMessage[]) => Promise<CompactResult | null>
): this {
this._compactionFn = fn;
return this;
Expand Down Expand Up @@ -278,7 +269,7 @@ export class SessionManager {

async append(
sessionId: string,
message: UIMessage,
message: SessionMessage,
parentId?: string
): Promise<string> {
await this.getSession(sessionId).appendMessage(message, parentId);
Expand All @@ -288,7 +279,7 @@ export class SessionManager {

async upsert(
sessionId: string,
message: UIMessage,
message: SessionMessage,
parentId?: string
): Promise<string> {
const session = this.getSession(sessionId);
Expand All @@ -304,7 +295,7 @@ export class SessionManager {

async appendAll(
sessionId: string,
messages: UIMessage[],
messages: SessionMessage[],
parentId?: string
): Promise<string | null> {
const session = this.getSession(sessionId);
Expand All @@ -317,7 +308,7 @@ export class SessionManager {
return lastParent;
}

getHistory(sessionId: string, leafId?: string): UIMessage[] {
getHistory(sessionId: string, leafId?: string): SessionMessage[] {
return this.getSession(sessionId).getHistory(leafId);
}

Expand All @@ -337,7 +328,7 @@ export class SessionManager {

// ── Branching ──────────────────────────────────────────────────

getBranches(sessionId: string, messageId: string): UIMessage[] {
getBranches(sessionId: string, messageId: string): SessionMessage[] {
return this.getSession(sessionId).getBranches(messageId);
}

Expand All @@ -357,7 +348,7 @@ export class SessionManager {
let parentId: string | null = null;
for (const msg of history) {
const newId = crypto.randomUUID();
const copy: UIMessage = { ...msg, id: newId };
const copy: SessionMessage = { ...msg, id: newId };
await newSession.appendMessage(copy, parentId);
parentId = newId;
}
Expand All @@ -368,12 +359,6 @@ export class SessionManager {

// ── Compaction ────────────────────────────────────────────────

needsCompaction(sessionId: string): boolean {
return (
this.getSession(sessionId).getPathLength() > this._maxContextMessages
);
}

addCompaction(
sessionId: string,
summary: string,
Expand Down Expand Up @@ -470,7 +455,7 @@ export class SessionManager {
session_search: {
description:
"Search past conversations for relevant context. Searches across all sessions.",
inputSchema: jsonSchema({
inputSchema: z.fromJSONSchema({
type: "object" as const,
properties: {
query: { type: "string" as const, description: "Search query" }
Expand Down
14 changes: 7 additions & 7 deletions packages/agents/src/experimental/memory/session/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Pure storage for tree-structured messages with compaction overlays and search.
*/

import type { UIMessage } from "ai";
import type { SessionMessage } from "./types";

export interface SearchResult {
id: string;
Expand All @@ -29,17 +29,17 @@ export interface StoredCompaction {
export interface SessionProvider {
// ── Read ────────────────────────────────────────────────────────

getMessage(id: string): UIMessage | null;
getMessage(id: string): SessionMessage | null;

/**
* Get conversation as a path from root to leaf.
* Applies compaction overlays. If leafId is null, uses the latest leaf.
*/
getHistory(leafId?: string | null): UIMessage[];
getHistory(leafId?: string | null): SessionMessage[];

getLatestLeaf(): UIMessage | null;
getLatestLeaf(): SessionMessage | null;

getBranches(messageId: string): UIMessage[];
getBranches(messageId: string): SessionMessage[];

getPathLength(leafId?: string | null): number;

Expand All @@ -49,9 +49,9 @@ export interface SessionProvider {
* Append a message. Parented to the latest leaf unless parentId is provided.
* Idempotent — same message.id twice is a no-op.
*/
appendMessage(message: UIMessage, parentId?: string | null): void;
appendMessage(message: SessionMessage, parentId?: string | null): void;

updateMessage(message: UIMessage): void;
updateMessage(message: SessionMessage): void;

deleteMessages(messageIds: string[]): void;

Expand Down
Loading
Loading