Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

记忆

要让 agent 长期可用,就必须有记忆(Memory)。没有记忆,每一段对话都从零开始。agent 会忘记用户是谁、它学到了什么、它正在做什么。记忆正是把无状态的 LLM 调用变成持久、有上下文感知能力的 agent 的关键。

Session API 为基于 Cloudflare Agents SDK 构建的 agent 提供记忆层。它管理两类记忆:对话历史(构成一次会话的消息和工具调用)和上下文记忆(注入到系统提示中的持久化块,agent 可以读、写、搜索和加载)。

对话历史

最基础的一种记忆就是对话本身:用户与 agent 之间的消息、agent 发起的工具调用以及它收到的结果。Session 把这些信息都存放在以树状结构组织的消息历史中,后端默认是 SQLite 形式的 Session Provider。

JavaScript


import { Session } from "agents/experimental/memory/session";


// Append messages as the conversation progresses

await session.appendMessage({

  id: `user-${crypto.randomUUID()}`,

  role: "user",

  parts: [{ type: "text", text: "What's the status of the deployment?" }],

});


// Read the full conversation history

const history = session.getHistory();


Explain Code

TypeScript


import { Session } from "agents/experimental/memory/session";


// Append messages as the conversation progresses

await session.appendMessage({

  id: `user-${crypto.randomUUID()}`,

  role: "user",

  parts: [{ type: "text", text: "What's the status of the deployment?" }]

});


// Read the full conversation history

const history = session.getHistory();


Explain Code

对话历史在 Durable Object 的 hibernation 与 eviction 之间持久存在。当 agent 唤醒时,完整历史依然可在 SQLite 中找到,无需重放或重建。

消息通过 parent_id 以树状结构存储,从而支持分支对话。当你以一个已经有子节点的 parentIdappendMessage 时,就会形成一个新分支,这对回复重新生成等功能很有用。getHistory(leafId) 可以沿着树中任意一条路径行走。

Session 还提供对对话历史的全文搜索:

JavaScript


const results = session.search("deployment Friday", { limit: 10 });


TypeScript


const results = session.search("deployment Friday", { limit: 10 });


随着对话变长,compaction 会汇总较早的消息,在不丢失底层数据的前提下,把上下文窗口控制在可管理范围内。

上下文记忆

上下文记忆(Context memory)是注入到系统提示中的持久化信息,与对话历史相互独立。它让 agent 在每一轮对话中都能访问到身份信息、指令、已学到的事实、知识库和参考材料。

Session API 支持四种上下文记忆,各自适合不同类型的信息。其类型由支持该上下文块的 provider 决定。Session 会自动检测 provider 的能力。

只读上下文

这就是传统意义上的系统提示:agent 的身份、性格和指令。你可以直接写在代码里,从 R2 中的 SOUL.md 文件加载,或从某个 API 拉取。内容会注入系统提示,agent 无法修改它。

一个编程助手可能拥有一段定义其性格与约束的 soul:

JavaScript


import { Session } from "agents/experimental/memory/session";


const session = Session.create(this).withContext("soul", {

  provider: {

    get: async () =>

      "You are a senior TypeScript engineer. You write concise, " +

      "well-tested code. You prefer composition over inheritance. " +

      "When you are unsure, you say so rather than guessing.",

  },

});


Explain Code

TypeScript


import { Session } from "agents/experimental/memory/session";


const session = Session.create(this)

  .withContext("soul", {

    provider: {

      get: async () =>

        "You are a senior TypeScript engineer. You write concise, " +

        "well-tested code. You prefer composition over inheritance. " +

        "When you are unsure, you say so rather than guessing."

    }

  });


Explain Code

也可以从 R2 加载,这样无需重新部署就能更新 agent 的性格:

JavaScript


const session = Session.create(this).withContext("soul", {

  provider: {

    get: async () => {

      const obj = await env.CONFIG_BUCKET.get("soul.md");

      return obj ? obj.text() : "You are a helpful assistant.";

    },

  },

});


TypeScript


const session = Session.create(this)

  .withContext("soul", {

    provider: {

      get: async () => {

        const obj = await env.CONFIG_BUCKET.get("soul.md");

        return obj ? obj.text() : "You are a helpful assistant.";

      }

    }

  });


只读块通过提供仅含 get() 方法的对象来定义。系统不会生成任何工具,内容直接出现在系统提示里,agent 无法修改它。

可写的短期上下文

可以把它当作 agent 给自己使用的草稿本,用来记录需要记住的内容。就像 Claude Code 维护一份待办事项,或者客服 agent 跟踪在对话中了解到的用户信息一样。

JavaScript


const session = Session.create(this)

  .withContext("memory", {

    description: "Important facts learned during conversation",

    maxTokens: 1100,

  })

  .withContext("todos", {

    description: "Task list, track what needs to be done and what is complete",

    maxTokens: 2000,

  });


TypeScript


const session = Session.create(this)

  .withContext("memory", {

    description: "Important facts learned during conversation",

    maxTokens: 1100

  })

  .withContext("todos", {

    description: "Task list, track what needs to be done and what is complete",

    maxTokens: 2000

  });


当你在 builder 中省略 provider 选项时,Session 会自动接入由 SQLite 支持的可写 provider。agent 会获得一个 set_context 工具,可用于替换或追加这些块的内容。token 上限会被强制执行,所以 agent 写入的内容不能超过 maxTokens 的预算。

系统提示在渲染可写块时会带上 token 用量指示,让 agent 知道还有多少空间可用:


══════════════════════════════════════════════

MEMORY (Important facts learned during conversation) [45% — 495/1100 tokens] [writable]

══════════════════════════════════════════════

User prefers dark mode.

User's project uses React and TypeScript.

Deployment target is Cloudflare Workers.


══════════════════════════════════════════════

TODOS (Task list) [12% — 240/2000 tokens] [writable]

══════════════════════════════════════════════

- [x] Set up project scaffolding

- [ ] Add authentication middleware

- [ ] Write integration tests


Explain Code

内容会在多条消息之间持久化,并在 hibernation 后依然存在。它始终出现在系统提示里,因此 agent 在每一轮都能看到,无需额外拉取。

可搜索上下文

当你拥有大量信息(知识库、文档、日志、累积笔记)时,你不会想把它们全部塞进系统提示里。可搜索上下文(Searchable context)只在系统提示里保留一段摘要(例如“42 entries indexed“),并允许 agent 在需要时检索具体条目。

你需要提供一个带 search() 方法的 provider。具体怎么搜索完全由你决定:全文搜索、通过 Vectorize 做向量搜索、调用外部 API,等等。Session 不关心具体实现,只要 provider 有 search() 方法即可。

内置的 AgentSearchProvider 默认使用 Durable Object SQLite + FTS5:

JavaScript


import { AgentSearchProvider } from "agents/experimental/memory/session";


const session = Session.create(this).withContext("knowledge", {

  description:

    "Searchable knowledge base, search for relevant information before answering",

  provider: new AgentSearchProvider(this),

});


TypeScript


import { AgentSearchProvider } from "agents/experimental/memory/session";


const session = Session.create(this)

  .withContext("knowledge", {

    description: "Searchable knowledge base, search for relevant information before answering",

    provider: new AgentSearchProvider(this)

  });


但你也可以基于任何搜索机制实现自己的 provider:

JavaScript


const session = Session.create(this).withContext("knowledge", {

  description: "Searchable knowledge base",

  provider: {

    get: async () => "Product documentation and FAQs",

    search: async (query) => {

      // Use Vectorize, an external API, whatever you need

      const results = await env.VECTORIZE_INDEX.query(

        await generateEmbedding(query),

        { topK: 5 },

      );

      return results.matches.map((m) => m.metadata.text).join("\n\n");

    },

    set: async (key, content) => {

      // Index new content

    },

  },

});


Explain Code

TypeScript


const session = Session.create(this)

  .withContext("knowledge", {

    description: "Searchable knowledge base",

    provider: {

      get: async () => "Product documentation and FAQs",

      search: async (query) => {

        // Use Vectorize, an external API, whatever you need

        const results = await env.VECTORIZE_INDEX.query(

          await generateEmbedding(query), { topK: 5 }

        );

        return results.matches.map(m => m.metadata.text).join("\n\n");

      },

      set: async (key, content) => {

        // Index new content

      }

    }

  });


Explain Code

agent 会获得一个 search_context 工具用于查询,以及一个 set_context 工具用于索引新条目。它决定要搜什么,你决定怎么搜。

当 agent 需要从一个大集合中检索具体片段、而不是加载整篇文档时,这种方式最合适。

可加载上下文(Skills)

Skills 是较大的上下文块(完整文档、参考指南、运行手册、模板),agent 可以按需发现并加载。可以把它们想象成书架上的参考资料:agent 看到一份带标题和描述的清单,挑出与当前任务相关的那本,加载进来,使用,用完后再卸下。

与 searchable 上下文从大集合中取小片段不同,skills 设计上是被整块加载的。当 agent 加载一个 skill 时,它会把整篇文档放进自己的上下文窗口。

Skills 的支持者是 SkillProvider 接口,该接口有三个方法:

  • get() 返回一个出现在系统提示中的元数据清单(标题与描述)
  • load(key) 拉取某个 skill 的完整内容
  • set(key, content, description?) 写入或更新一个 skill 条目(可选)

系统提示以清单形式展示可用的 skills。[loadable] 标记告诉 LLM 这些条目不是内联的,需要使用工具来访问完整内容:


══════════════════════════════════════════════

SKILLS [loadable]

══════════════════════════════════════════════

- api-ref: API Reference documentation

- style-guide: Company style guide

- deploy-checklist: Production deployment checklist


agent 看到这些标题,判断哪一项与当前任务相关,然后用 load_context 把完整内容拉进自己的工作上下文。完成后用 unload_context 释放空间。当 skill provider 实现了 set() 时,agent 还可以反向写入,更新已有 skill 或创建新条目。


Agent sees: "- deploy-checklist: Production deployment checklist"

User asks: "Walk me through a production deployment"

Agent calls: load_context({ block: "skills", key: "deploy-checklist" })

→ Full checklist content is loaded into the agent's working context


由 R2 支持的 skills

内置的 R2SkillProvider 把 skills 存储在 Cloudflare R2 bucket 中。每个 skill 都是一个 R2 对象,可选地带有用作描述的自定义 metadata。

JavaScript


import { Session, R2SkillProvider } from "agents/experimental/memory/session";


const session = Session.create(this)

  .withContext("soul", {

    provider: {

      get: async () =>

        [

          "You are a helpful assistant with access to skills.",

          "When a user asks you to do something, check the SKILLS section",

          "for a relevant skill and use load_context to load it.",

        ].join("\n"),

    },

  })

  .withContext("memory", {

    description: "Learned facts",

    maxTokens: 1100,

  })

  .withContext("skills", {

    provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" }),

  })

  .withCachedPrompt();


Explain Code

TypeScript


import { Session, R2SkillProvider } from "agents/experimental/memory/session";


const session = Session.create(this)

  .withContext("soul", {

    provider: {

      get: async () => [

        "You are a helpful assistant with access to skills.",

        "When a user asks you to do something, check the SKILLS section",

        "for a relevant skill and use load_context to load it.",

      ].join("\n")

    }

  })

  .withContext("memory", {

    description: "Learned facts",

    maxTokens: 1100

  })

  .withContext("skills", {

    provider: new R2SkillProvider(env.SKILLS_BUCKET, { prefix: "skills/" })

  })

  .withCachedPrompt();


Explain Code

prefix 选项把 provider 限定到 bucket 的某个子目录。元数据清单中的 skill key 不会带前缀,因此 skills/api-ref 在系统提示中会显示为 api-ref

在 Wrangler 配置里加上 R2 bucket 绑定:

JSONC


{

  "r2_buckets": [

    {

      "binding": "SKILLS_BUCKET",

      "bucket_name": "my-agent-skills"

    }

  ]

}


TOML


[[r2_buckets]]

binding = "SKILLS_BUCKET"

bucket_name = "my-agent-skills"


Skills 就是普通的 R2 对象。可以通过任意 R2 接口上传(Wrangler CLI、Dashboard 或 Workers API):

Terminal window


# Upload a skill from a file

wrangler r2 object put my-agent-skills/skills/style-guide --file ./docs/style-guide.md --content-type text/markdown


要添加描述(显示在元数据清单中),在 R2 对象上设置自定义 metadata:

JavaScript


await env.SKILLS_BUCKET.put("skills/api-ref", content, {

  customMetadata: { description: "API Reference documentation" },

});


TypeScript


await env.SKILLS_BUCKET.put("skills/api-ref", content, {

  customMetadata: { description: "API Reference documentation" }

});


自定义 skill provider

通过实现 SkillProvider 接口,你可以使用任意存储来支持 skills:

JavaScript


class DatabaseSkillProvider {

  db;


  constructor(db) {

    this.db = db;

  }


  async get() {

    const rows = await this.db

      .prepare("SELECT key, description FROM skills ORDER BY key")

      .all();

    if (rows.results.length === 0) return null;

    return rows.results

      .map((r) => `- ${r.key}${r.description ? `: ${r.description}` : ""}`)

      .join("\n");

  }


  async load(key) {

    const row = await this.db

      .prepare("SELECT content FROM skills WHERE key = ?")

      .bind(key)

      .first();

    return row ? row.content : null;

  }


  async set(key, content, description) {

    await this.db

      .prepare(

        "INSERT INTO skills (key, content, description) VALUES (?, ?, ?) " +

          "ON CONFLICT(key) DO UPDATE SET content = ?, description = ?",

      )

      .bind(key, content, description ?? null, content, description ?? null)

      .run();

  }

}


Explain Code

TypeScript


import type { SkillProvider } from "agents/experimental/memory/session";


class DatabaseSkillProvider implements SkillProvider {

  private db: D1Database;


  constructor(db: D1Database) {

    this.db = db;

  }


  async get(): Promise<string | null> {

    const rows = await this.db

      .prepare("SELECT key, description FROM skills ORDER BY key")

      .all();

    if (rows.results.length === 0) return null;

    return rows.results

      .map(r => `- ${r.key}${r.description ? `: ${r.description}` : ""}`)

      .join("\n");

  }


  async load(key: string): Promise<string | null> {

    const row = await this.db

      .prepare("SELECT content FROM skills WHERE key = ?")

      .bind(key)

      .first();

    return row ? (row.content as string) : null;

  }


  async set(key: string, content: string, description?: string): Promise<void> {

    await this.db

      .prepare(

        "INSERT INTO skills (key, content, description) VALUES (?, ?, ?) " +

        "ON CONFLICT(key) DO UPDATE SET content = ?, description = ?"

      )

      .bind(key, content, description ?? null, content, description ?? null)

      .run();

  }

}


Explain Code

Session 通过 duck-typing 检测 load() 方法,并自动生成对应的工具。

Skills 与其他记忆类型对比

维度Skills可写上下文可搜索上下文
在系统提示中仅元数据清单完整内容摘要数量
访问方式按 key 加载整篇文档始终可见按查询搜索
最适合大型文档、参考资料短笔记、偏好由许多小条目组成的大集合
上下文成本低(加载前)与内容长度成正比低(搜索前)
agent 是否可写?可选(若实现了 set)是(通过 set_context)是(通过 set_context)

最关键的区别是:skills 是惰性的。在 agent 决定要使用之前,它在系统提示中的开销几乎为零。这非常适合大型参考资料 —— 任何一次对话通常只用得到其中一小部分。

agent 如何与记忆交互

Session 会基于上下文块的 provider 类型自动生成工具。把这些工具与你自己的应用工具一起传给 LLM:

JavaScript


const sessionTools = await session.tools();

const allTools = { ...sessionTools, ...myApplicationTools };


const result = streamText({

  model: myModel,

  system: await session.freezeSystemPrompt(),

  messages: await convertToModelMessages(session.getHistory()),

  tools: allTools,

});


TypeScript


const sessionTools = await session.tools();

const allTools = { ...sessionTools, ...myApplicationTools };


const result = streamText({

  model: myModel,

  system: await session.freezeSystemPrompt(),

  messages: await convertToModelMessages(session.getHistory()),

  tools: allTools

});


自动生成的工具

Session 会根据存在哪些 provider 类型动态生成工具:

工具何时生成作用
set_context存在任何 writable、skill 或 search 块时向命名块写入内容。对 writable 块,替换或追加;对 skill/search 块,写入按 key 标识的条目。
load_context存在任何 skill 块时按 key 把某个文档的完整内容加载到 agent 的上下文。
unload_context存在任何 skill 块时通过移除一个之前加载的文档来释放上下文空间。该文档仍可被重新加载。
search_context存在任何 search 块时在某个可搜索块内做全文搜索。返回按相关性排序的前若干条结果。
session_search使用 SessionManager 时跨所有 session 搜索(跨对话搜索)。

工具会附带描述和参数 schema,告诉 LLM 哪些块可用以及它们的用途。agent 自行决定何时及如何使用这些工具。

完整的工具签名和所有 Session 方法,请参阅 Session API 参考

系统提示

各上下文块会被组装成一个结构化的系统提示,带有清晰的标题和元信息。每个块都有一段带标签的小节,标签指明它的类型与容量:


══════════════════════════════════════════════

SOUL (Identity) [readonly]

══════════════════════════════════════════════

You are a helpful coding assistant who speaks concisely.


══════════════════════════════════════════════

MEMORY (Important facts) [45% — 495/1100 tokens] [writable]

══════════════════════════════════════════════

User prefers dark mode.

User's project uses React and TypeScript.


══════════════════════════════════════════════

KNOWLEDGE (Searchable knowledge base) [searchable]

══════════════════════════════════════════════

12 entries indexed.


══════════════════════════════════════════════

SKILLS [loadable]

══════════════════════════════════════════════

- api-ref: API Reference documentation

- style-guide: Company style guide


Explain Code

标签([readonly][writable][searchable][loadable])告诉 LLM 它对每个块可以进行哪种交互。token 预算让 agent 看到可写块还有多少空间,从而管理自己的记忆。

注意事项

Prompt 缓存

LLM 服务商(Anthropic、OpenAI 等)会缓存系统提示的前缀。当连续请求共享同一段系统提示时,服务商可以跳过对该前缀的重复处理,从而降低延迟和成本。打破缓存(改动系统提示)就会损失这一收益。

Session API 在设计上与 prompt 缓存兼容:

  • freezeSystemPrompt() 在首次调用时根据所有上下文块渲染系统提示,后续调用直接返回缓存值。即使 agent 通过 set_context 写入了记忆,提示在不同轮之间也保持不变。
  • withCachedPrompt() 把冻结后的提示持久化到存储中,以便在 Durable Object hibernation 与 eviction 之后依然存在。agent 唤醒时无需再向所有 provider 拉取,即可加载到相同的提示。

当 agent 通过 set_context 更新一个可写块时,底层 provider 会立即更新(数据已经保存),但被冻结的系统提示不会被重新渲染。LLM 只有在你显式调用 refreshSystemPrompt() 后,才会在下一轮看到这一更新 —— 通常是在对话轮之间执行,而不是中途。

这意味着系统提示在多步工具调用的整轮中保持稳定,从而在每一步都保留 provider 的前缀缓存命中。

JavaScript


const session = Session.create(this)

  .withContext("soul", {

    provider: { get: async () => "You are a helpful assistant." },

  })

  .withContext("memory", { description: "Learned facts", maxTokens: 1100 })

  .withCachedPrompt(); // Persist the frozen prompt across hibernation


// During a conversation turn:

const system = await session.freezeSystemPrompt(); // Same value every call

const tools = await session.tools();


// ... agent calls set_context to update memory ...

// The frozen prompt is NOT changed, prefix cache stays warm


// Between turns (optional, if you want the agent to see its own updates):

await session.refreshSystemPrompt();


Explain Code

TypeScript


const session = Session.create(this)

  .withContext("soul", {

    provider: { get: async () => "You are a helpful assistant." }

  })

  .withContext("memory", { description: "Learned facts", maxTokens: 1100 })

  .withCachedPrompt(); // Persist the frozen prompt across hibernation


// During a conversation turn:

const system = await session.freezeSystemPrompt(); // Same value every call

const tools = await session.tools();


// ... agent calls set_context to update memory ...

// The frozen prompt is NOT changed, prefix cache stays warm


// Between turns (optional, if you want the agent to see its own updates):

await session.refreshSystemPrompt();


Explain Code

Compaction(压缩)

长对话最终会超出 LLM 的上下文窗口。Compaction 在两个层面应对这个问题:macro-compaction 汇总成段的较早消息,micro-compaction 截断单条过大的消息。

Macro-compaction

Macro-compaction 汇总较旧的消息,但永远不会删除原始消息。

它使用 overlay:汇总结果保存在一张独立的表里,以它覆盖的消息范围为 key。当调用 getHistory() 时,overlay 会在读取时被透明地应用。被压缩的范围会被一条合成的摘要消息替换。底层消息仍保留在 SQLite 中,完整对话依然可用,可用于审计、搜索以及分支。


Messages:  [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]

                    ↓ compaction ↓

Overlay:   [1] [2] [SUMMARY of 3-7]           [8] [9] [10]

                                                ↑ tail protected


要点:

  • 非破坏性,原始消息永远不会被删除。完整对话始终在数据库中可查。

  • 迭代式,当对话再次变长并触发新一轮 compaction 时,会把已有摘要交给 LLM 更新,而不是从零开始。

  • 边界感知,压缩边界会被调整,避免拆开 tool call 与 tool result 的配对。

  • 可配置,protectHead 保留前 N 条消息(通常是系统上下文),tailTokenBudget 保留最近的消息不被压缩。

  • JavaScript

  • TypeScript

JavaScript


import { createCompactFunction } from "agents/experimental/memory/utils/compaction-helpers";


const session = Session.create(this)

  .withContext("memory", { maxTokens: 1100 })

  .onCompaction(

    createCompactFunction({

      summarize: (prompt) =>

        generateText({ model: myModel, prompt }).then((r) => r.text),

      protectHead: 3,

      tailTokenBudget: 20000,

      minTailMessages: 2,

    }),

  )

  .compactAfter(100_000); // Auto-compact when token estimate exceeds threshold


Explain Code

TypeScript


import { createCompactFunction } from "agents/experimental/memory/utils/compaction-helpers";


const session = Session.create(this)

  .withContext("memory", { maxTokens: 1100 })

  .onCompaction(createCompactFunction({

    summarize: (prompt) =>

      generateText({ model: myModel, prompt }).then(r => r.text),

    protectHead: 3,

    tailTokenBudget: 20000,

    minTailMessages: 2

  }))

  .compactAfter(100_000); // Auto-compact when token estimate exceeds threshold


Explain Code

自动 compaction 会在 appendMessage() 之后,当估算 token 数超过阈值时被触发。Compaction 失败不会导致致命错误,因为消息已经保存。

Micro-compaction

Micro-compaction 工作在单条消息的层面,而不是跨范围。它处理两个问题:

读取时截断:truncateOlderMessages() 在把较早的消息发给 LLM 之前,会缩短其中的工具输出和长文本。最近的消息(默认是最近 4 条)会保留完整。该操作作用于副本,存储中的消息不会被改动。

JavaScript


import { truncateOlderMessages } from "agents/experimental/memory/utils";


const history = session.getHistory();

const truncated = truncateOlderMessages(history);

// Pass truncated history to the LLM


TypeScript


import { truncateOlderMessages } from "agents/experimental/memory/utils";


const history = session.getHistory();

const truncated = truncateOlderMessages(history);

// Pass truncated history to the LLM


行大小限制:当一条消息被持久化时(通常是带有大型工具输出的 assistant 消息),会按 SQLite 行大小上限做检查。过大的工具输出会被替换为预览以及一段建议重跑工具的备注。这避免单条消息超出存储上限,同时保持对话流的完整性。

相关资源

Session API 参考 Session 的完整 API 参考,涵盖消息、上下文块、compaction、搜索、工具和自定义 provider。

存储与同步 state 用于更简单的键值持久化与实时同步的 setState()。

Think 通过 configureSession() 内置 Session 集成的、有自身风格的聊天 agent。