第7章:会话与 Gateway —— 把一套 Agent 变成长期服务
一、Session 的本质:不是聊天窗口,而是恢复单元
gateway/session.py:149 的 SessionSource、:314 的 SessionContext、:760 的 SessionEntry 和 :1192 的 SessionStore 共同说明:session 不只是 message list。它还包含来源平台、用户/频道/线程、profile、cwd、时间、显示元数据、API sidecar、压缩信息和持久化游标。
一个可恢复 session 至少要能回答:
- 哪个外部会话对应它?
- 当前消息历史到哪里?
- 当前 turn 是否还在执行?
- 发送到模型的内容是否与显示 transcript 不同?
- Gateway 重启后能否避免重复投递?
- 同一个用户从 CLI 和 Telegram 进入时,应该共享还是隔离?
二、从平台消息到 Agent Session
三、Gateway 为什么不能只是一个 while loop
gateway/run.py:5432 的 GatewayRunner 组合了 authorization、kanban watcher、slash command 等 mixin,并在约 :23100、:23253 处承载 Agent 执行路径。它必须管理:
- 多平台 adapter 的启动、停止和重连;
- inbound 消息去重、配对、权限和 profile 路由;
- session lock / turn lease,避免同一 conversation 并发写历史;
- 流式事件的消费、合并、背压和超时;
- delivery ledger,避免重启后重复发送;
- graceful shutdown、watchdog、health 和 forensics。
这就是为什么 Gateway 目录接近一个独立运行时,而不是 cli.py 的一个子函数。
四、流式事件:模型事件到平台消息的翻译
gateway/stream_events.py 定义 ToolCallChunk、ToolCallFinished、LongToolHint、GatewayNotice 等事件;gateway/stream_dispatch.py:40 的 GatewayEventDispatcher 再把这些事件分发给平台或 TUI。
模型流是高频、细粒度、可能半截的;平台消息是低频、有大小限制、需要编辑或分块的。适配层因此要有自己的聚合和节流策略:
token delta → stream consumer → platform buffer
tool start → progress/status
tool delta → long-tool hint / partial update
tool finish → final tool result event
turn end → final message / delivery ledger
不要把 provider 的 SSE event 直接交给 Telegram/Discord。连接层的职责就是把模型时间轴翻译成用户平台可承受的时间轴。
五、Session Store 的持久化取舍
SessionStore 负责 append、load、prune、import/export、search、rename 和上下文辅助数据。它会保留可恢复的结构,而不是把每次发送前临时拼出的全部内容写进去。
典型的数据分层是:
- transcript:用户、assistant、tool 的事实事件;
- display metadata:平台、UI、reaction、展示 kind;
- sidecar:实际发给 API 的临时内容;
- runtime state:当前 turn lease、压缩 cursor、delivery 状态;
- search index:跨 session 回忆和 insights 使用的索引。
这种分层让“可搜索”和“可复现 API 请求”可以同时成立。
六、Profile Routing:一台 Gateway,多套人格/记忆
gateway/profile_routing.py 和 docs/profile-routing.md 描述 profile routing:同一个 Gateway 可以按 community、channel、thread 或 source 映射不同 profile。每个 profile 可以拥有自己的 persona、memory、toolset、model 和 session namespace。
inbound source
→ platform/chat/thread 匹配
→ profile route
→ profile-specific session + memory + tools
这是一种“共享进程、隔离状态”的折中:减少进程数和连接数,同时避免不同社区的上下文混在一起。
七、并发控制:Turn Lease 和生命周期 ledger
同一个 session 同时收到两个消息时,最危险的不是 CPU 争用,而是历史顺序和 tool pair 被交叉写入。gateway/turn_lease.py 的 SessionTurnLeaseRegistry 提供 session 级租约;lifecycle_ledger.py、delivery_ledger.py 则记录运行和投递状态。
这类 ledger 的价值在失败时才显现:进程崩溃后,重启逻辑可以区分“已开始未完成”“已生成未投递”“已投递但未确认”等状态,而不是凭时间猜测。
八、平台适配器的最小契约
gateway/platforms/base.py:2626 的 BasePlatformAdapter 让平台差异集中在:
- 认证与连接生命周期;
- 入站事件归一化;
- chat/user/thread identity;
- 文本、媒体、reaction 和 typing/status 投递;
- 平台大小限制、重试、限速和错误。
PlatformRegistry 负责选择启用的 adapter;新增平台时不应复制 session 或 Agent Loop。
九、Gateway 的三条设计结论
- Session 是“恢复单元”,包含事实、元数据和运行账本。
- Stream 是内部事件协议;平台适配器消费协议,而不是理解模型 SDK。
- 长期进程的正确性依靠 lease、ledger、watchdog 和 shutdown flush,而不只是异常处理。