消息、Session Key 与入口
对应源码
| 主题 | 源码 |
|---|---|
| 通用消息对象 | nanobot/bus/events.py::InboundMessage、OutboundMessage |
| 异步消息队列 | nanobot/bus/queue.py::MessageBus |
| Session Key 规则 | nanobot/session/keys.py、nanobot/agent/loop.py::AgentLoop._effective_session_key |
| bus 入口与并发分派 | nanobot/agent/loop.py::AgentLoop.run、AgentLoop._dispatch |
| direct 入口 | nanobot/agent/loop.py::AgentLoop.process_direct |
学习目标
读完后,你应当能够:
- 解释
InboundMessage和OutboundMessage的职责; - 说明默认 session key 如何产生;
- 区分 MessageBus 路径和
process_direct()路径; - 找到 unified session 和 override 影响 Session 归属的位置。
前置知识
- 理解
dataclass; - 理解 Queue;
- 已阅读 五个入口与运行边界。
1. InboundMessage 是入口统一格式
定义位置:
nanobot/bus/events.py::InboundMessage
核心字段:
| 字段 | 含义 |
|---|---|
channel | 消息来源,如 cli、api、websocket |
sender_id | 发送者标识 |
chat_id | 对话或频道标识 |
content | 用户文本或系统注入文本 |
timestamp | 入站时间 |
media | 媒体路径或引用 |
metadata | Channel/运行时附加数据 |
session_key_override | 显式覆盖默认 Session 归属 |
默认属性:
session_key_override or f"{channel}:{chat_id}"这意味着 Session 默认按“入口类型 + 对话标识”隔离。
sender_id默认不参与 session keySession 归属默认使用
channel和chat_id,不是发送者 ID。群聊、线程和 Channel adapter 如何选择 chat_id,会直接影响上下文隔离。
2. 有效 Session Key 可能进一步变化
InboundMessage.session_key 是消息对象给出的默认值,但 AgentLoop 还会计算 effective session key,用于:
- unified session;
- thread-scoped override;
- Subagent 或内部 continuation;
- 某些运行表面的显式 session ID。
因此调试 Session 串线时,要同时观察:
msg.session_key
effective_session_key
SessionManager 实际读取的 key
磁盘文件中的 metadata.key相关源码:
nanobot/session/keys.pynanobot/agent/loop.py::_effective_session_keynanobot/agent/loop.py::_dispatch
3. OutboundMessage 是通用投递格式
定义位置:
nanobot/bus/events.py::OutboundMessage
核心字段:
channelchat_idcontentreply_tomediametadatabuttons
metadata 可能承载:
- 消息路由信息;
- stream/progress 标记;
- latency;
- Channel 无关的
_agent_ui结构化数据。
这些内部 metadata 不应默认视为稳定公共 API。
4. MessageBus 只是两个 Queue
nanobot/bus/queue.py::MessageBus 维护:
inbound: Queue[InboundMessage]
outbound: Queue[OutboundMessage]职责很小:
- 发布入站;
- 消费入站;
- 发布出站;
- 消费出站;
- 暴露队列长度。
它不负责:
- Session 持久化;
- 消息重放;
- Runtime Event;
- Provider 调用;
- Channel 权限;
- retry。
这种小职责使 Channel 和 Agent 核心解耦。
5. 两种进入 AgentLoop 的方式
方式 A:MessageBus
flowchart LR CH["Channel / 交互式 CLI"] --> IN["bus.publish_inbound"] IN --> RUN["AgentLoop.run"] RUN --> DIS["_dispatch"] DIS --> PM["_process_message"] PM --> OUT["bus.publish_outbound"] OUT --> CM["ChannelManager / CLI consumer"]
AgentLoop.run():
- 持续消费 inbound;
- 处理 priority command 和 runtime control;
- 将同一活跃 session 的新消息放入 pending queue;
- 为其他消息创建 Task;
- 最终由
_dispatch()取得 session lock。
方式 B:process_direct()
flowchart LR E["CLI -m / API / SDK"] --> PD["process_direct"] PD --> LOCK["session lock"] LOCK --> PM["_process_message"] PM --> RET["直接返回结果"]
它仍然复用同一个 _process_message() 和状态机,只是入口不依赖 Channel 的 inbound/outbound dispatcher。
6. 同 Session 串行
AgentLoop._dispatch() 为 effective session key 取得独立 asyncio.Lock:
同一 key → 共用一把 lock → Turn 串行
不同 key → 不同 lock → 可以并发此外还有全局并发 gate,限制同时处理的请求数量。
同一 session 在 Turn 进行中收到新消息时,可能进入该 session 的 pending queue,而不是启动竞争 Turn。后续在 AgentRunner 的安全边界处注入。
7. 各入口的 Session 习惯
| 入口 | 典型 key |
|---|---|
| CLI 单次 | 显式 --session,默认 cli:direct |
| Channel | 通常为 <channel>:<chat_id> |
| API | api:<session_id>,默认 api:default |
| SDK | 由 SDK 参数或默认值决定 |
| Subagent 回传 | 使用 override 对齐主 Session |
不要假设 CLI、API 和 WebUI 会天然共享历史。只有相同 effective key 或启用 unified session 时才可能共享。
8. 观察任务
在 AgentLoop.process_direct() 或 _dispatch() 设置断点,观察:
msg.channelmsg.chat_idmsg.session_keysession_keyself._session_locks.keys()self._pending_queues.keys()
分别运行:
nanobot agent -c $nanobotConfig -w $nanobotWorkspace -s "cli:lesson-a" -m "记住 A"
nanobot agent -c $nanobotConfig -w $nanobotWorkspace -s "cli:lesson-b" -m "我让你记住了什么?"预期:不同 session 不应依赖彼此的对话历史。
源码与测试
| 主题 | 位置 |
|---|---|
| 消息 dataclass | nanobot/bus/events.py |
| Queue | nanobot/bus/queue.py |
| Session key helper | nanobot/session/keys.py |
| 主消费循环 | AgentLoop.run |
| session 串行 | AgentLoop._dispatch |
| direct 入口 | AgentLoop.process_direct |
| unified session 测试 | tests/agent/test_unified_session.py |
| workspace/session scope | tests/agent/test_workspace_scope.py |
自检
- MessageBus 为什么不保存历史?
- 为什么
sender_id和chat_id不能混为一谈? process_direct()为什么仍能复用 AgentLoop 状态机?- 同一 session 的新消息为什么不应直接启动另一个竞争 Turn?
下一步
阅读 AgentLoop 状态机。