qoderclicn 默认把会话历史保存在运行它的机器上。如果服务运行在多台机器、容器或 Serverless 环境中,下一次请求可能由另一台机器处理,导致之前的会话无法继续。
外置会话存储会在你的应用所控制的存储中保留每个会话的一份镜像。镜像是一份额外的副本:qoderclicn 仍然把会话写在本地,任何机器之后都可以通过 session ID 继续同一个会话。
适合使用外置存储的场景包括:
这种设计带来两个特性:
所有机器必须使用相同的
每次需要镜像或恢复会话的 query,都在 options 里传入 store(TypeScript 为
会话管理函数在 store 而不是本地文件上操作。TypeScript 中通过
列出会话的函数要求 store 实现列出会话的方法;列出子代理的函数要求
SDK 不提供可直接用于生产的存储实现;应用需要针对自己选择的共享存储实现
一个
Java 的接口按批次追加,和上面的清单有以下不同:
失败处理。 外置写入失败不会中断当前对话。最后一次重试失败后,SDK 会发出镜像错误消息(TypeScript 为
- 服务有多个实例,请求可能在实例之间切换
- 容器或 Serverless 环境的本地磁盘不可靠
- 需要自行管理会话数据的权限、加密、备份或保留周期
工作方式
- 写入是尽力而为的。 SDK 在后台镜像 entry。写入失败会被上报,但绝不会中断正在进行的对话;参见下文的运维。
- entry 对存储不透明。 你的存储原样保存并返回 SDK 定义的 transcript entry。应用不需要理解 qoderclicn 的本地文件格式。
快速开始
InMemorySessionStore 只把数据保存在当前进程中。在接入共享后端之前,用它来验证接线是否正确——即 query 确实会写入 store 并能从中恢复。它无法演示跨机器恢复,因为进程退出后数据就没了;跨机器场景需要实现真正的存储,见实现存储。
cwd(Java 为 workingDirectory),SDK 才能把请求识别为同一个项目。
Java 的镜像写入是异步的,会话关闭时最多等待 closeDrainTimeoutMs(默认 5 秒)写完剩余记录。像上例这样在同一进程里紧接着恢复时,先等待上一次查询的 closeAsync() 完成。
在应用中使用
每次需要镜像或恢复会话的 query,都在 options 里传入 store(TypeScript 为 sessionStore,Python 为 session_store),然后选择如何指定会话:
- 已知 session ID —— 传入
resume。 - 该项目最近的会话 —— 传入
continue: true(TypeScript)/continue_conversation=True(Python)/continueSession(true)(Java)。这要求 store 实现列出会话的方法。
QoderClient.Builder.sessionStore(store) 设置客户端的默认 store,查询、会话和 client.sessions() 都会使用它。单次会话可以用 SessionOptions.Builder.sessionStorage(...) 覆盖:SessionStorage.external(otherStore) 改用另一个 store,SessionStorage.cliLocal() 只用 CLI 本地历史。
Java 按 store 中根 transcript 条目的最新 timestamp 选择最近的会话。条目没有时间戳、最新时间并列或没有可用会话时,启动以 InvalidOptionsException 失败,需要改传明确的 session ID。
管理外置会话
会话管理函数在 store 而不是本地文件上操作。TypeScript 中通过 sessionStore 选项传入 store;Python 中每个函数都以 store 作为第一个参数;Java 用 client.sessions(options) 取得绑定了 store 和项目目录的 SessionRepository。options 应与创建会话时一致,否则查不到按其 workingDirectory 保存的会话;client.sessions() 使用客户端的 projectDirectory,适合在 QoderClient.Builder 上统一配置项目目录的场景:
listSubkeys / list_subkeys;删除会话的函数要求 delete。如果实现了 listSubkeys / list_subkeys,获取子代理消息的函数也会使用它。
Java 的 list() 要求 store 实现 ListableSessionStore,delete() 要求实现 SessionDeletionStore,删除会话的根 transcript 和全部子代理 transcript;没有配置 store 或缺少对应接口时,方法以 UnsupportedCliCapabilityException 失败。内置的 InMemorySessionStore 两个接口都实现了。Java 不提供重命名、打标签和导入会话的函数。
要把已有的本地会话复制进 store——例如迁移一台此前未启用外置存储的机器——使用导入函数,注意 session ID 在前、store 在后:
实现存储
SDK 不提供可直接用于生产的存储实现;应用需要针对自己选择的共享存储实现 SessionStore 接口(TypeScript)/ 协议(Python)/ 接口(Java)。Redis 和 PostgreSQL 的可运行参考实现展示了具体用法,见 TypeScript 示例或 Python 示例。它们是起点,而非可直接用于生产的实现。
SessionKey 标识一份 transcript。主会话没有 subpath;每份子代理 transcript 复用相同的 project key 和 session ID,但带有不同的 subpath。把 key 和 entry 当作不透明数据——原样保存并返回,不要解析其中的消息内容。
两个必需方法提供保存与恢复。每个可选方法解锁一项能力:
| 方法 | 解锁的能力 |
|---|---|
append、load | 镜像会话并按 ID 恢复 —— 必需 |
listSessions / list_sessions | 恢复最近会话;列出已存会话 |
delete | 从 store 删除会话 |
listSubkeys / list_subkeys | 完整恢复并查看子代理 transcript |
实现检查清单
- 同一个 key 保持追加顺序,
load返回完整历史。回放依赖顺序。 - 隔离各 key。 绝不要把一个 key 的 entry 返回到另一个 key 下。
- 让
append幂等。 SDK 可能用相同的 entry 重试失败的写入,因此重试不能重复写入历史。 - 列出会话时
mtime返回 Unix 毫秒时间戳,并且只返回主会话,即不带subpath的会话。 - 级联删除。 删除主会话时,必须同时删除它的子代理 transcript。
listSubkeys/list_subkeys只返回相对标识——不要返回绝对路径,也不要返回包含.或..的路径。这些会成为存储键,路径穿越片段会让 transcript 逃出它所在的命名空间。- 串行化并发写入。 如果同一会话可能被多个进程写入,在存储层串行化这些写入。
SessionStore conformance 测试;Python 在 qodercn_agent_sdk.testing 中提供 run_session_store_conformance),并为具体后端补充并发和重试测试。连接管理、权限、加密、备份、迁移和数据保留仍由应用负责。
Java 实现差异
Java 的接口按批次追加,和上面的清单有以下不同:
- 幂等和 CAS。
append按AppendBatch.operationId()幂等,重放时返回原操作的版本和条数。expectedVersion()不为null时做 CAS,版本不匹配时以SessionStoreConflictException失败。 - 列出会话。
listSessions返回项目下的全部键,包括子代理的 subpath 键,顺序不保证。最近会话按条目里的timestamp选择,不需要mtime。 - 删除。
deleteSessionTree在同一事务内删除根 transcript 和全部 subpath,并永久标记会话已删除;之后对该会话的写入以SessionStoreDeletedException拒绝。deleteSession只删除单个键。 - 异步。 两个必需方法可以同步完成,但失败必须通过异常完成的 stage 报告,不要直接抛出,也不要在调用线程上做阻塞 I/O。
运维
失败处理。 外置写入失败不会中断当前对话。最后一次重试失败后,SDK 会发出镜像错误消息(TypeScript 为 system/mirror_error,Python 为 SDKMirrorErrorMessage,Java 为 MirrorErrorMessage)。对镜像完整性有要求时应监控它——即使某次写入没成功,对话仍可能成功完成。
Java 中某个键写入失败或超时后,该键停止镜像,其他键不受影响。MirrorErrorMessage 的错误文本在 rawJson() 的 error 字段,格式不稳定,不要按字符串匹配。
调优。
- 外置读取默认最多等待 60 秒。用
loadTimeoutMs/load_timeout_ms调整。 - flush 策略(
sessionStoreFlush/session_store_flush)默认为batched。设为eager会更早镜像 entry,代价是更多的存储请求。 - Java 没有
loadTimeoutMs和 flush 策略选项。镜像的待写批次、字节和等待上限用sessionMirrorOptions(...)设置,见SessionMirrorOptions。
- 显式指定的会话在 store 中不存在时,SDK 仍可回退到本机上的同 ID 会话。Java 不回退:显式指定的记录在 store 中缺失或为空时,启动以
InvalidOptionsException失败。 - store 不能与文件 checkpoint 或自定义 transport 同时使用(TypeScript 中也不能与
persistSession: false同时使用)。TypeScript 中 store 在内置的 Process 和 Worker transport 上受支持;Python 中需要内置的 subprocess transport。 - store 只保存会话历史,不保存鉴权状态、应用配置、文件 checkpoint 或数据保留策略。
- Java 中生效的 store 与
fileCheckpointing(true)同时设置时,以InvalidOptionsException失败。 - Java 在外部 store 上分叉会话,需要 CLI 支持对应能力(见能力要求)。SDK 等分叉的初始记录写入 store 后才完成初始化并发送输入;能力缺失或写入失败时启动失败。
CLI_START_FAILED 失败,错误消息说明原因。