记忆与沙箱:四层记忆与 CLI-Agent 执行器
30 秒导读: 前面几章讲的是"一次请求怎么在 Go 进程里跑完"。这一章讲运行时在这条主线之外,给 agent 挂的两类外部资源:一是能跨会话/跨用户留下来的记忆(不是聊天历史,是可编程的 KV/数组/集合),二是真的起一个容器、在里面跑一个 CLI coding agent(Claude CLI 那一类)的执行器。前者是 agent 的"状态持久化面",后者是一条完全不同的"执行路径"。
本章属于 Yao Agent 运行时 系列。相邻章节:主管道见 02-pipeline.md;记忆如何暴露给脚本见 03-hooks-jsapi.md;常规(非沙箱)的工具循环见 04-toolloop-mcp.md。本章只讲记忆子系统和 Sandbox V2 这条 CLI-Agent 执行路径,不讲通用二进制里 sandbox/ 的容器底座实现(那不是本 area)。
1. 这是什么(零基础也能懂)
先分清两样东西,它们凑在一章是因为都属于"运行时给 agent 的外部资源",但彼此独立。
第一样:记忆(memory)。 不是聊天记录。聊天记录是"消息数组",记忆是一块可编程的键值存储——agent 的 hook 脚本或工具可以往里 Set("用户偏好", ...)、Incr("调用次数", 1)、Push("待办", ...),下次再读出来。关键在它能活多久、给谁看:
- 记在用户名下 → 这个用户所有对话都读得到,永不过期。
- 记在团队名下 → 团队里所有人共享。
- 记在这次会话名下 → 只在这个 chat 里有效,24 小时后过期。
- 记在这一次请求名下 → 只在这一轮请求里当草稿纸,30 分钟就没。
第二样:沙箱(Sandbox V2)。 有些 agent 不满足于"调模型 + 调几个工具",它要跑一个完整的命令行编码 agent——比如在一个隔离容器里起 claude CLI,让它自己读文件、改代码、跑命令。Yao 把这条路叫 Sandbox V2:请求进来后不直连大模型,而是把活儿整个交给容器里的 runner(yaocode / claude / opencode / tai 四选一)。
一句话直觉。 记忆是"给 agent 一块可编程的、分作用域的便签本";沙箱是"给 agent 一台装好了 coding agent 的一次性电脑,让它自己去干,干完销毁"。
2. 顶层全景(它大概怎么转)
这两块都挂在主管道(见 02-pipeline.md)的 context.Context 上。看一张图理解它们各自的位置:
一次请求 (agent.Execute 主管道)
│
┌───────────────────────────┼───────────────────────────┐
│ │ │
┌────▼─────┐ ┌───────▼────────┐ ┌──────▼───────┐
│ ctx.Memory│ │ Create Hook │ │ 执行 LLM 段 │
│ (四层记忆) │◄──JSAPI 读写─┤ / 工具 / JS 脚本 │ │ │
└────┬─────┘ └────────────────┘ │ 分叉两条路: │
│ │ │
│ 只快照 Context 层 │ ①无沙箱→直连LLM│
│ 进 Buffer(供 resume) │ +Go侧工具循环 │
▼ │ │
┌──────────┐ │ ②有沙箱→交给 │
│ 持久 store│ User/Team 永久 · Chat 24h · Context 30m │ 容器runner跑 │
│ (xun/mem) │ │ (跳过Go工具循环)│
└──────────┘ └──────────────┘
两块各自的部件和职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
Space(四个常量) | 定义记忆的四种作用域 | agent/memory/types.go:15-27 |
Memory | 一次请求的记忆总入口,持有四个 Namespace | agent/memory/types.go:72-83 |
Manager | 全局管家,按 id 缓存/复用 Memory 实例 | agent/memory/manager.go:37-85 |
Namespace | 单个作用域,带前缀隔离 + 默认 TTL 的 KV/数组/集合 API | agent/memory/namespace.go |
UpdateSpaceSnapshot | 把 Context 层记忆快照进 Buffer,供 resume | agent/assistant/chat.go:163-170 |
HasSandboxV2 / initSandboxV2 | 判定 + 起容器、拿 Computer 和 runner | agent/assistant/sandbox_v2.go:27-293 |
executeSandboxV2Stream | 走 runner 流式执行,不直连 LLM | agent/assistant/sandbox_v2.go:339-397 |
initStandaloneWorkspace | 无沙箱但选了 workspace 时,单独挂载 FS | agent/assistant/sandbox_v2.go:420-436 |
下面分两大部分讲:§3-§5 记忆,§6-§8 沙箱。
3. 记忆第一层:四种作用域(Space)
这节讲什么: 记忆的"作用域"到底是怎么定义的,以及每一层默认存哪、默认活多久。
3.1 四个 Space 常量
作用域就是四个字符串常量,定义在 agent/memory/types.go:12-28(Space 类型):
| Space 常量 | 值 | 覆盖范围 | 典型用途 |
|---|---|---|---|
SpaceUser | "user" | 一个用户的所有会话 | 用户偏好、长期知识、个人设置 |
SpaceTeam | "team" | 一个团队里所有用户 | 团队知识、共享设置、协作数据 |
SpaceChat | "chat" | 单个会话内 | 对话上下文、本会话累积的知识 |
SpaceContext | "context" | 单次请求内临时 | 中间结果、临时变量、请求级缓存 |
作用域从上到下越来越窄、越来越短命。这是整个记忆系统最核心的设计:同一套 API,靠"记在哪一层"决定数据的可见范围和寿命。
3.2 每层的默认 store 与默认 TTL
每一层背后都有一个默认 store(存储后端 ID)和一个默认 TTL(存活时长)。默认 store ID 是四个固定常量(agent/memory/types.go:41-46):
| 层 | 默认 store 常量 | 默认 store ID 字符串 | 默认 TTL |
|---|---|---|---|
| User | DefaultUserStore | __yao.agent.memory.user | 0(永不过期) |
| Team | DefaultTeamStore | __yao.agent.memory.team | 0(永不过期) |
| Chat | DefaultChatStore | __yao.agent.memory.chat | 24 * time.Hour |
| Context | DefaultContextStore | __yao.agent.memory.context | 30 * time.Minute |
TTL 常量见 agent/memory/memory.go:11-16(DefaultUserTTL / DefaultChatTTL 等)。store ID 可以在 Config 里被覆盖(agent/memory/types.go:33-38,四个字段 User/Team/Chat/Context),留空就用上面的默认。注释里点明所有层默认都用 xun-based(Yao 的数据库存储层)持久化。
关键细节: User / Team 的 TTL 是
0= 永不过期,这就是它们"持久"的来源;Chat 24 小时、Context 30 分钟,决定了它们是"半持久"和"临时草稿"。TTL 的落点在Namespace.Set—— 传ttl == 0时会回退成该层的默认 TTL(agent/memory/namespace.go:31-36)。
4. 记忆第二层:Manager 与 Namespace 原语
这节讲什么: 谁来创建和缓存这些记忆实例(Manager),以及单个作用域到底提供哪些操作(Namespace API)、怎么做隔离。
4.1 Manager:按四元组缓存 Memory
一个全局 Manager(agent/memory/manager.go:37 的 DefaultManager)是记忆的入口。它做两件事:
- 按
userID:teamID:chatID:contextID四元组做 key,缓存已创建的Memory实例(memoryKey,agent/memory/manager.go:63-65),用sync.Map+LoadOrStore保证并发安全(Memory方法,manager.go:68-85)。 agent.Load()时调Init(config)装配(manager.go:12-14);没配置就NewManagerWithDefaults()用上面四个默认 store 兜底(manager.go:53-60)。
对外主入口是 GetMemory(userID, teamID, chatID, contextID)(manager.go:18-24)——这就是主管道拿到 ctx.Memory 的来源。
一个记忆实例装四个 namespace。 New() 按传入的 id 哪个非空就建哪个 namespace(agent/memory/memory.go:19-69):userID != "" 才建 User 层,依此类推。所以一次内部调用如果没有 teamID,就压根没有 Team 层。
4.2 前缀隔离:一个 store 装下所有人的记忆
四个默认 store 是共享的物理存储,那"用户 A 的偏好"和"用户 B 的偏好"怎么不打架?靠 key 前缀。
每个 Namespace 建立时算出一个 Prefix,格式是 "{space}:{id}:"(agent/memory/memory.go:98,newNamespace):
user 层, id=123 → 前缀 "user:123:"
team 层, id=456 → 前缀 "team:456:"
chat 层, id=abc → 前缀 "chat:abc:"
之后所有读写都先过 prefixKey(agent/memory/namespace.go:21-23)把用户给的 key 加上前缀,再落到底层 store。于是同一个物理 store 里,user:123:偏好 和 user:456:偏好 天然隔离。反向操作(Keys / GetMulti / Snapshot)会把前缀再剥掉,对调用方透明(namespace.go:60-69、117-127)。
调用方视角: ns.Set("theme", "dark")
│ prefixKey()
▼
底层 store 真实键: store.Set("user:123:theme", "dark", ttl)
4.3 Namespace 提供的原语:不止 KV
Namespace 嵌入了 store.Store 接口(agent/memory/interfaces.go:38-49,NamespaceAccessor),所以它远不止 get/set。按类别看(全部在 agent/memory/namespace.go,每个方法都先 prefixKey 再委托底层 store):
| 类别 | 方法 | 作用 |
|---|---|---|
| KV 基本 | Get / Set / Has / Del / Keys / Len | 单键读写、存在性、按 pattern 列 键 |
| KV 原子 | GetSet / GetDel | 取旧值+按需初始化 / 取值即删(原子) |
| 计数器 | Incr / Decr | 数值原子增减(namespace.go:176-183) |
| 批量 | GetMulti / SetMulti / DelMulti / GetSetMulti | 多键一次操作 |
| 数组(列表) | Push / Pop / Pull / ArrayGet / ArraySet / ArraySlice / ArrayPage / ArrayAll / ArrayLen | 有序列表增删、按下标/分页读 |
| 集合 | AddToSet | 加入集合(自动去重,namespace.go:206-208) |
| 统计/快照 | Stats / Snapshot / Clear / Flush | 键数统计、导出全部、清空 |
这些原语最终暴露给 JS 脚本用:hook 里的 ctx.memory.user.Set(...)、ctx.memory.context.Incr(...) 就是走 createNamespaceObject 把这些方法逐个绑到 V8 对象上(agent/context/jsapi.go:785 起,createMemoryObject 在 jsapi.go:751)。这条 V8 桥的细节见 03-hooks-jsapi.md,本章只需知道:JS 里操作的 ctx.memory.<层> 就是这里的 Namespace。
GetSet的用法演示(帮助建立直觉;下面是示意,非源码):// 示意,非源码:第一次调用会执行 getValue 初始化,之后直接命中缓存const profile = ctx.memory.user.GetSet("profile", 0, (key) => {return loadProfileFromDB(ctx.user.id) // 只在缺失时跑一次})真实实现:
Namespace.GetSet(agent/memory/namespace.go:97-102)——ttl==0回退默认 TTL,再委托store.GetSet。
5. 记忆与 resume:只快照 Context 层
这节讲什么: 一次请求中断后要能 resume(见 02-pipeline.md 的 Buffer 机制),但记忆不是全部都存进去——只有 Context 层被快照。为什么?
5.1 为什么只快照 Context
User / Team / Chat 三层本来就在持久 store 里,resume 时直接从 store 读得回来,不必塞进 Buffer 重复存。只有 Context 层是"这一次请求的临时草稿"——它 30 分钟就过期,而且是请求级的中间状态,如果请求断在半路,重放时这些中间变量必须能恢复。所以快照只针对它。
UpdateSpaceSnapshot 把这条规则写死了(agent/assistant/chat.go:163-170):
- 先判空:
ctx.Buffer == nil || ctx.Memory == nil || ctx.Memory.Context == nil任一为空就直接返回; - 否则
snapshot := ctx.Memory.Context.Snapshot()—— 只取 Context 层; ctx.Buffer.SetSpaceSnapshot(snapshot)存进 Buffer。
函数注释一句话点破:"Only captures Context-level memory (request-scoped temporary data) for recovery"。
5.2 什么时候快照
快照在每次 BeginStep 前刷新一次(agent/assistant/chat.go:180,BeginStep 里先 ast.UpdateSpaceSnapshot(ctx);context.go:493-504 的 BeginStep 也在开步前 SetSpaceSnapshot(ctx.Memory.Context.Snapshot()))。也就是说,每推进一个执行步,就把当前 Context 记忆的全貌拍一张照,连同这一步记进 Buffer。
Snapshot() 本身很朴素(agent/memory/namespace.go:252-261):遍历本 namespace 所有 key,逐个 Get 出来组成 map。存进 Buffer 时 SetSpaceSnapshot 做了一次 copyMap 深拷贝(agent/context/buffer.go:412-416),避免后续改动污染快照。
每个 step 落盘时的记忆快照流:
ctx.Memory.Context ──Snapshot()──► map{k:v...} ──copyMap──► Buffer.spaceSnapshot
│
随 BufferedStep 一起持久化
(step.SpaceSnapshot, buffer.go:328)
│
resume 时从 xun 记录读回 (store/xun/resume.go:360)
resume 侧:xun 存储层把 SpaceSnapshot 序列化进 chat 记录(agent/store/xun/resume.go:97-98),恢复时再读回 record.SpaceSnapshot(resume.go:360-363)。这样一条中断的请求重放时,Context 层记忆能精确还原到中断前那一步的样子。这与 02-pipeline.md 讲的 Buffer/resume 机制是同一套。
边界:
Fork(agent/memory/memory.go:194-239)给并行子 agent(ctx.agent.All/Any/Race)克隆一个独立的 Context 层,而 User/Team/Chat 三层是共享的。这样并发子 agent 的临时状态互不干扰,但共享记忆仍然可见。多智能体编排见 05-multiagent-stack.md。
6. 沙箱是什么:另一条执行路径
这节讲什么: 从这里开始换主题讲沙箱。先讲它和普通请求的根本区别——它不直连大模型。
普通 agent 的一次请求(见 02-pipeline.md)大致是:建 prompt → 调 LLM → 模型说要调工具 → Go 侧执行工具(工具循环,见 04-toolloop-mcp.md)→ 回填结果 → 再调 LLM。
Sandbox V2 agent 完全是另一条路:请求进来,起一个容器,把消息、系统提示、连接器、MCP 配置整个打包丢给容器里的一个 runner,runner 内部自己跑一个 CLI coding agent(比如 claude)。这个 CLI agent 在容器里自理读文件、调工具、跑命令的整个循环,Go 侧只负责把它的输出流式转出来。
判定入口极简(agent/assistant/sandbox_v2.go:27-29,HasSandboxV2):assistant 的 DSL 里有没有 SandboxV2 配置。有,就走沙箱路;没有,走普通路。主管道里这个分叉点在 agent/assistant/agent.go:317。
7. 起沙箱:initSandboxV2 干了什么
这节讲什么: 从判定到容器就绪,initSandboxV2 的关键几步(agent/assistant/sandbox_v2.go:97-293)。这一步在主管道里先于 hooks 执行,好让 hooks 能访问 sandbox 上下文(agent/assistant/agent.go:162-195)。
流程(挑关键步,完整看源码):
initSandboxV2 (sandbox_v2.go:97)
│
1. 浅拷贝 ast.SandboxV2 → cfg (并发请求各拿一份可变配置, :98-99)
│
2. 加载并应用用户级设置 (loadUserAgentSetting / applyUserSetting, :113-114)
│ └ 用户的 secrets / 镜像覆盖 DSL 默认
│
3. 解析 runner 集合 (ResolveRunnerSet: 用户偏好 > DSL > 全局, :125)
│
4. 选节点 (BuildNodeSnapshot + SelectNode, :128-151)
│
├─ 4a. 若 sel.Mode == "local": 就地执行短路(yaocode),不需要 Computer, :154-190
│
5. 拿连接器 + 解析角色矩阵 (GetConnector + resolveRoles, :193-198)
6. 镜像预检 + 拉取 (box 模式:ImageExists/PullImage, :202-228)
7. 拿 Computer(容器/机器句柄) (GetComputer, :232)
8. 拿 Runner (sandboxv2.Get(sel.Runner), :245)
9. 解析 assistant 目录 / skills / MCP (:253-254)
10. runner.Prepare(...) (把配置灌进容器, :257-268)
│
└► 返回 {Runner, Computer, Config, Cleanup, Roles}
几个值得单独讲的点:
7.1 四种 runner 与"本地短路"
runner 有四种,注册在 agent/sandbox/v2/init.go:12-17:
| runner | 说明 |
|---|---|
claude | 容器里跑 Claude CLI |
opencode | 容器里跑 opencode |
tai | 容器里跑 tai runner |
yaocode | Yao 自带,可就地执行(local 模式) |
当 SelectNode 判出 sel.Mode == "local"(通常是 yaocode),initSandboxV2 走短路分支(sandbox_v2.go:154-190):不起容器、不要 Computer,PrepareRequest.Computer 传 nil,直接在进程内 Prepare 后返回。这是"沙箱"里最轻的一档——省掉了容器开销。
runner 的选取优先级:用户偏好 > sandbox.yao 里 runner.name > 全局默认 > 兜底 yaocode(ResolveRunnerSet,agent/sandbox/v2/runners.go:59;优先级注释在 lifecycle.go:35)。
7.2 灌给容器的三样东西:目录、skills、MCP
Prepare 之前,Go 侧把三样东西准备好塞进 PrepareRequest:
- assistant 目录 + skills 目录(
resolveAssistantDirs,sandbox_v2.go:296-306):以config.Conf.AppSource + ast.Path为根;若根下存在skills/子目录,就把它作为skillsDir一并挂进去,让容器里的 CLI agent 能加载技能。 - MCP 服务器列表(
buildMCPServers,sandbox_v2.go:309-322):把 assistant 的ast.MCP.Servers转成沙箱类型MCPServer{ServerID, Resources, Tools}。注意:这是把 MCP 配置交给容器里的 CLI agent 去连,而不是 Go 侧自己连——呼应 §8"沙箱跳过 Go 工具循环"。MCP 本身见 04-toolloop-mcp.md。
7.3 角色矩阵:多连接器
一个沙箱 agent 可能要用多个模型扮不同角色。resolveRoles(sandbox_v2.go:402-416)构建 map[string]connector.Connector:
- 主连接器(用户选的或系统默认)→ 键
"default"; - 再从
llmprovider.Global按 identity 取三个角色模型:"heavy"(重活)、"light"(轻活)、"vision"(视觉),取到就加进 map。
这个 roles map 一路传到 Prepare 和 Stream,让容器里的 runner 能按角色分派到不同模型。
8. 跑沙箱:executeSandboxV2Stream 与"跳过 Go 工具循环"
这节讲什么: 容器就绪后怎么执行,以及本章最关键的一个设计——为什么沙箱模式下 Go 侧不跑工具循环。
8.1 执行:打包丢给 runner.Stream
主管道到了 LLM 执行段,若 HasSandboxV2() && v2Init.Runner != nil,就走 executeSandboxV2Stream 而不是 executeLLMStream(分叉在 agent/assistant/agent.go:317-333)。
executeSandboxV2Stream(sandbox_v2.go:339-397)做的事:
- 自己拼系统提示:解析
ast.Prompts里的$CTX变量,取出 system 那条(sandbox_v2.go:347-358)——注意它没走普通路的buildSystemPrompts,而是自己解析,因为提示要以字符串形式交给容器。 - 签发沙箱令牌
IssueSandboxToken(:363-370),让容器有受限凭证回调。 - 打包成
StreamRequest(消息、系统提示、连接器、roles、ChatID、token 等,:372-385),再包一层ExecuteRequest,交给sandboxv2.ExecuteSandboxStream(:387-396)流式执行。
Go 侧从此只是转发容器的输出流,不参与"模型说要调工具→执行→回填"这个循环。
8.2 为什么跳过 Go 侧 工具调用
这是全章的题眼。看主管道里三处 !ast.HasSandboxV2() 守卫:
| 位置 | 代码 | 含义 |
|---|---|---|
agent/assistant/agent.go:363 | completionResponse.ToolCalls != nil && !ast.HasSandboxV2() | 沙箱模式不进 Go 工具执行块 |
agent/assistant/agent.go:549 | len(toolCallResponses) > 0 && !ast.HasSandboxV2() && ... | 沙箱模式不进工具循环回灌 |
agent.go:361 的注释把原因写死了:"Skip MCP tool calls execution for sandbox mode - Claude CLI handles them internally"。
用一句话解释这条设计的合理性:
普通模式: Go 是"大脑",模型只出主意,工具由 Go 执行
模型 → ToolCalls → [Go 执行工具] → 回填 → 模型 ...
沙箱模式: 容器里的 CLI agent 才是"大脑",它自带完整循环
Go → 把活丢进容器 → [CLI agent 自己读文件/调MCP/跑命令/回灌] → 只把输出流出来
↑ 如果 Go 这边也跑一遍工具循环,就会和容器里的循环打架、重复执行
所以 §7.2 里 MCP 配置是交给容器、而不是 Go 侧连接——因为工具的实际调用发生在容器内那个 CLI agent 手里。Go 侧若再插一脚就是双重执行。这就是 !HasSandboxV2() 这些守卫存在的根本原因。工具循环的普通实现见 04-toolloop-mcp.md,两者正好是对照。
8.3 无沙箱但要 workspace:initStandaloneWorkspace
还有个中间态:agent 没配沙箱,但用户选了一个 workspace(想让 hook 能读写工作区文件)。这时走 initStandaloneWorkspace(sandbox_v2.go:420-436):
- 从
ctx.Metadata["workspace_id"]取 workspace id; - 用
workspace.M().FS(...)加载该 workspace 的文件系统; ctx.SetWorkspace(wsFS)挂到 context 上,供 hook 访问ctx.workspace。
调用点在主管道 agent/assistant/agent.go:208-210,条件是 !ctx.HasWorkspace()(沙箱路已经挂过 workspace 的话就不重复挂)。它只加载 FS,不起任何容器——这是"轻量工作区加载",和 §7 的容器化沙箱 是两回事。
9. 巧妙之处(可带走的技术)
- 同一套 API,靠"记在哪层"决定寿命与可见性。 不为四种作用域各写一套接口,而是共用
Namespace,差异只在前缀和默认 TTL(agent/memory/memory.go:98、memory.go:11-16)。加一个作用域=加一个常量+一档 TTL。 - 前缀隔离而非分库。 一个物理 store 装下所有用户/团队的记忆,靠
"{space}:{id}:"前缀在 key 空间里切租户(prefixKey,namespace.go:21-23),读出时透明剥前缀。省掉了每租户建库的开销。 - resume 只快照最短命的一层。 User/Team/Chat 本就在持久 store,不必重复存;只有请求级的 Context 需要随 step 拍照(
chat.go:163-170)。这让 resume 的快照体积最小。 - 沙箱模式用守卫位彻底让出控制权。 不是"沙箱里也跑一遍工具循环再合并",而是用
!HasSandboxV2()在三处直接跳过 Go 侧工具执行(agent.go:363、549),把大脑完全交给容器里的 CLI agent,避免双重执行。 - local 短路省容器。
yaocode在sel.Mode == "local"时就地执行、Computer 传 nil(sandbox_v2.go:154-190),让"沙箱"能有一档零容器开销的轻量实现。
10. 边界与局限(诚实)
- 记忆不是聊天历史。 它是可编程 KV,不自动记消息;要持久化对话得自己
Set/Push。历史消息走的是另一套(Buffer + chat 存储,见 02-pipeline.md)。 - 快照只覆盖 Context 层。 若把关键中间态误写进 User/Team/Chat 层,resume 快照里不会有它们(它们靠持久 store 而非 Buffer 恢复)——这是设计,不是 bug,但用错层会困惑。
- 沙箱下 Go 侧工具/自动搜索等能力基本让位。 工具调用、Go 侧工具循环都被
!HasSandboxV2()关掉;这些能力得由容器里的 CLI agent 自己具备。 - 容器有生命周期成本。 非 local 模式要选节点、可能拉镜像、起 Computer、
Prepare,失败点多(initSandboxV2每步都有closeLoadingV2(..., "sandbox.failed")兜底);Cleanup走 5 秒超时的runner.Cleanup + LifecycleAction(sandbox_v2.go:278-283)。 - 本章不覆盖容器底座。
GetComputer/LifecycleAction背后的通用二进制sandbox/实现不在本 area,这里只讲 agent 侧如何"用"它。
11. 代码地图(导航索引)
用符号名 grep 定位比行号更抗漂移(上游更新后行号会变,符号名通常还在)。
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 四个作用域常量 | agent/memory/types.go | SpaceUser / SpaceTeam / SpaceChat / SpaceContext |
| 默认 store ID | agent/memory/types.go | DefaultUserStore … DefaultContextStore |
| Memory 结构(四 namespace) | agent/memory/types.go | Memory / Namespace |
| 默认 TTL | agent/memory/memory.go | DefaultUserTTL … DefaultContextTTL |
| 创建记忆 + 前缀计算 | agent/memory/memory.go | New / newNamespace |
| 并行子 agent 克隆 Context | agent/memory/memory.go | Fork |
| 全局管家 + 缓存 | agent/memory/manager.go | DefaultManager / GetMemory / NewManagerWithDefaults |
| KV/数组/集合原语 | agent/memory/namespace.go | Get / Set / GetSet / Incr / Push / AddToSet / prefixKey |
| 导出快照 | agent/memory/namespace.go | Snapshot |
| 只快照 Context 层 | agent/assistant/chat.go | UpdateSpaceSnapshot |
| Buffer 存快照 | agent/context/buffer.go | SetSpaceSnapshot / GetSpaceSnapshot |
| 记忆暴露给 JS | agent/context/jsapi.go | createMemoryObject / createNamespaceObject |
| 沙箱判定 | agent/assistant/sandbox_v2.go | HasSandboxV2 |
| 起沙箱全过程 | agent/assistant/sandbox_v2.go | initSandboxV2 |
| 目录 / skills / MCP / 角色 | agent/assistant/sandbox_v2.go | resolveAssistantDirs / buildMCPServers / resolveRoles |
| 走 runner 执行 | agent/assistant/sandbox_v2.go | executeSandboxV2Stream |
| 无沙箱挂 workspace | agent/assistant/sandbox_v2.go | initStandaloneWorkspace |
| runner 注册 | agent/sandbox/v2/init.go | Register(claude/opencode/yaocode/tai) |
| runner 选取 | agent/sandbox/v2/runners.go | ResolveRunnerSet |
| 主管道分叉点 | agent/assistant/agent.go | HasSandboxV2 守卫(:317/:363/:549) |