353 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PicoBot 架构机制
## 核心数据流
```
Channel → MessageBus.inbound → Gateway processor → SessionManager → per-session worker → AgentLoop
↑ │
└── Channel ← OutboundDispatcher ← per-conversation lane ← MessageBus.outbound
AgentLoop → TurnEvent → TurnController → latest TurnSnapshot → DeliveryCoordinator → TurnSink → Channel
WebSocket/Channel → MessageBus.control → Gateway processor → SessionManager (dialog 操作)
Scheduler → SessionManager.handle_cron_message → AgentLoop → send_message
```
## 模块职责
| 模块 | 职责 |
|------|------|
| `gateway` | HTTP/WebSocket 服务器与嵌入式 WebUI持有 GatewayState |
| `client` | TUI 聊天客户端 |
| `channels` | 外部集成飞书、CLI仅收发消息 |
| `bus` | 有界 inbound/outbound/control 队列;出站 dispatcher 与分目标 lane |
| `session` | 会话生命周期、dialog 操作、每 session 串行队列、Turn 状态、上下文与持久化协调 |
| `agent` | LLM 调用循环、工具执行、上下文压缩、媒体处理、子 Agent、Turn 语义事件 |
| `providers` | OpenAI/Anthropic 原生流解析统一正文、reasoning、工具、usage 与私有回放状态 |
| `delivery` | 完整 Turn 快照的展示过滤、latest-wins 节流、终态投递和 TurnSink 生命周期 |
| `tools` | Agent 工具bash、文件操作、搜索、HTTP、web、browser、memory、delegate 等) |
| `skills` | Skill 加载、管理和 prompt 构建 |
| `storage` | SQLite 持久化 |
| `scheduler` | Cron 作业调度 |
| `observability` | Observer 模式agent/工具遥测事件 |
| `protocol` | WebSocket 协议消息定义 |
| `config` | 配置加载、环境变量替换、路径解析 |
| `health` | CLI、工具和斜杠命令共用的只读运行依赖检查 |
| `memory` | 长期记忆存储与检索 |
| `mcp` | MCPModel Context Protocol工具集成 |
| `task_supervisor` | Gateway 后台任务注册、取消、限时等待和强制回收 |
| `work` | 每个 session 的单 active plan、并行子项状态、版本与变更事件 |
## 功能边界
- Channels 通过 MessageBus 发布入站消息,通过 OutboundDispatcher 或每 Turn 一个的 TurnSink 接收出站写入,不感知 session 或 LLM
- MessageBus 本体持有三条有界队列;出站路由、顺序和重试由 `OutboundDispatcher` 负责
- SessionManager 拥有 session 状态、dialog 路由、上下文构建、每 session worker 和活动 Turn 的 steering mailbox并通过 worker 创建 AgentLoop
- TurnController 是活动 Turn 状态的唯一 ownerSession 在消息原子提交成功后才发布 Completed
- AgentLoop 跨轮无状态,接收已准备的 history并在安全模型边界排空本 Turn steering 后调用 LLM、执行工具并返回一次结果
- Providers 是纯 HTTP 流客户端,无 bus/session/channel 感知;签名 reasoning 状态只回放给匹配 Provider不下发客户端或 Channel
- DeliveryCoordinator 只投影完整快照,不修改会话历史;慢消费者跳过中间 revision终态显式、有界投递
- 每个活动 Turn 独占一个 TurnSink平台 message ID 和 reaction 清理状态只存在于 sink 内
- Tools 接收原始参数,通常返回字符串结果;有状态适配器额外接收 session/turn `ToolExecutionContext`
- MCP 工具在 Gateway 初始化时连接服务器、发现工具,并包装成普通 Tool 注册到 ToolRegistry
- 具名子 Agent 从运行代不可变 `AgentCatalog` 加载Definition 固定 Provider/Model、工具/Skill allowlist、委托边与限制工具集完全由定义文件的 `tools` 列表决定(管理员显式授权),`delegate`/`emit_signal`/`get_skill`/`agent_task` 为运行时注入不可静态声明。单个定义校验失败(坏 YAML、未知 provider/profile/model/tool/skill、或显式委托到缺失目标仅停用该定义并记入 `load_errors``GET /api/agents` 返回),不会阻塞启动或热重载;配置与目录信任级错误仍然致命。支持单个/批量 foreground 和显式父子授权Root 对具名 Agent 的 background单任务或批量走 durable run/inbox + continuation 投递,每个 run 独立完成、空闲时完成即返回。内置 general-purpose 定义随二进制释放到 `~/.picobot/agents/`WebUI「子 Agent」页可增删改与启停定义
- 复杂任务可使用 `todo` 创建 session 级计划;多个子项可通过 `delegate.plan_item_id` 并行委托,子 Agent 不能修改计划
- WebUI 聊天复用 `/ws``cli_chat`;同源管理 API 只读取受限的日志、任务、记忆,并对白名单配置文件做原子写入
- WebUI 使用 Svelte 5 + ViteBits UI 提供无样式可访问组件;`cargo build` 增量生成前端到 Cargo `OUT_DIR`,再嵌入单二进制,仓库不保存生成产物
- WebUI 通过 `get_session_stats`/`session_stats` 显示当前会话累计输入输出 Token 和上下文窗口占用;`/info [--json]` 读取同一份 SessionStats
## 关键约束
- Gateway 启动时切换到 workspace 目录
- SQLite 数据在 `{config_dir}/data/picobot.db``config_dir` 默认 `~/.picobot`),与 workspace 相互独立
- ChannelManager 持有 MessageBus 和所有 channel
- OutboundDispatcher 通过 ChannelManager 路由出站消息
- 配置目录 `.env` 与 workspace `.env` 仅在单线程启动阶段分层加载,并使用 `unsafe { env::set_var(...) }` 写入进程环境;优先级为既有进程环境 > workspace > 配置目录
- `browser` 工具默认启用,只有 `browser.enabled=false` 时不注册;缺少 CLI/Chrome 不阻止 Gateway 启动,但 health 和实际调用会给出安装错误。每次调用按参数分流:不传 `persistent_id` 时按 dialog 使用普通临时浏览器,默认空闲一小时后自动关闭;长期工作时 Agent 可自主创建持久身份,并在后续相关 action 中持续传入同一个 ID持久 daemon 禁用空闲自动关闭。同一 ID 跨 dialog 共享 session/锁,不同 ID 相互独立并可并发。`browser_profiles` 只在受控根目录中创建、设置语义标签、列出或删除合法 ID没有全局持久化开关、默认 ID也不自动按 dialog 建立或选择持久 Profile不依赖 Fantoccini/ChromeDriver/WebDriver
- 所有工具调用统一包装为 `ToolOutput` 并经过公共处理器;产物按模型/用户受众分流。浏览器截图默认同时供模型查看并附到最终回复,`file_read` 图片默认仅供模型理解
- 同一 session 只运行一个 Turn活动 Turn 期间普通输入默认 steering`/queue` 明确等待下一 Turn不同 session 可并发
- steering mailbox 容量为 32 条/64 KiB满或关闭时可靠回退到容量 32 的 session 队列;两者都无法接收时明确拒绝
- 出站消息按 `(channel, chat_id)` 分 lane 保序lane 容量为 64慢目标不阻塞其他目标
- 活动 Turn 与普通出站消息共享 `(channel, chat_id)` 写锁;禁止把 token delta 放入 MessageBus
- `cli_chat` 向 TUI/WebUI 发送统一 `turn_updated` 完整快照;飞书默认 FinalOnly开启 `live_updates` 后编辑同一卡片
- 长生命周期后台任务由 TaskSupervisor 管理;连接局部任务由其 owner 限时 join 或 abort
- 外部建连、重试等待和关停 join 必须可取消且有硬超时
- 不得记录 API Key、Authorization header 或包含临时凭据的完整连接 URL
- WebUI 管理 API 与 `/ws` 默认要求设备配对;一次性代码由本机 CLI 签发,服务端只持久化令牌哈希。对外暴露时仍必须由外层提供 TLS
## 上下文压缩
`messages` 是 append-only 原始日志压缩不会改写消息、工具结果、ID 或 seq。每个 session 最多有一个活动 `ContextCheckpoint`Provider 历史确定为“一条累计摘要 + `seq >= first_retained_seq` 的原始尾部”。自动压缩使用 `context_tokens > context_window - effective_reserve`,默认 reserve 16,384、近期原样保留 20,000 tokens小窗口会自适应缩小两者。摘要输入预算由当前模型 `token_limit` 扣除摘要输出、提示词和安全余量得到;超大历史生成 checkpoint 加最新材料优先的有界 request-local 转录,不受固定 32K 上限约束。`/compact`、自动入口和首次 overflow 复用同一压缩编排和 checkpoint 原子提交路径,每次最多一次摘要调用。真实 Provider overflow 或换模后发送前已检测到的硬超限,能在摘要不可用时生成明确标记的确定性降级 checkpoint并且正式 Provider 请求只重试一次。工具已经执行后若发生 overflow只在同一个 AgentLoop 中保留本 Turn 工具链、裁掉旧完整 Turn 的请求副本并重试当前模型步骤一次,不从 durable history 重跑工具。
## Skill 系统
三个优先级(高覆盖低):
1. `{workspace}/skills/` — 最高优先级
2. `~/.picobot/skills/` — 中等优先级
3. `~/.agents/skills/` — 最低优先级
同名 skill 按优先级覆盖。每个 skill 是包含 `SKILL.md` 的目录。内置 skill 在 `~/.picobot/skills/` 下不存在时自动从二进制释放安装。
## 会话系统
### 会话 ID 格式
统一会话 ID 为三段式:**`<channel>:<chat_id>:<dialog_id>`**
| 部分 | 含义 | 示例 |
|------|------|------|
| `channel` | 消息渠道 | `cli_chat``feishu` |
| `chat_id` | 聊天/群组标识 | `sid_abc123` |
| `dialog_id` | 对话标识 | `default``d_xxxx`(短 ID |
同一 `channel:chat_id` 下可有多个 dialog。`chat_scope()` 返回 `"channel:chat_id"` 用于分组。
### Session 生命周期
```
create → 存入 Storage → 载入 memory → 设为当前 dialog
get_or_create
← 接收消息、LLM 响应 →
switch → rename → archive → delete(soft)
```
| 操作 | 效果 |
|------|------|
| `create` | 新建 dialog_id立即持久化到 SQLite设为当前 |
| `get_or_create` | 先在内存 HashMap 中找 → 再查 Storage → 都不存在则新建 |
| `switch_dialog` | 切换当前 dialog目标 session 自动从 Storage 恢复入内存 |
| `list_dialogs` | 列出 `channel:chat_id` 下最近 10 个 session |
| `rename` | 更新标题,内存 + Storage 同步 |
| `delete` | 软删除(设 deleted_at从内存移除 |
| `archive` | 设置 archived_at从内存和当前 dialog 追踪中移除;可通过 include_archived 查询 |
### SessionManager 数据结构
两层追踪:
- **`sessions`**`HashMap<String, Arc<Mutex<Session>>>` — 所有已加载的 sessionkey 为完整 session ID
- **`current_sessions`**`HashMap<String, String>` — 每个 `channel:chat_id` 当前的 session ID
消息到达时 `resolve_dialog_id()` 按顺序确定接收 session当前 session → Storage 最近活跃 session → 新建。
### 消息处理与并发
没有活动 Turn 时,普通消息先 `try_send` 到该 session 的有界 worker 队列Gateway 主 processor 随即返回 `AgentProcessing`。活动 Turn 期间普通消息默认进入有界 steering mailbox并在完整工具批次结束后或无工具最终回复边界作为真实 `role=user` 消息注入下一次模型调用;`/queue <message>` 绕过 mailbox明确进入下一 Turn。`/stop` 直接取消当前 Turn 并清空 mailbox 与普通队列,不会排在长模型调用后。
Worker 的处理原则:
1. 短暂持 Session 锁抓取快照并记录 `worker_generation`/`state_version`
2. 释放锁后执行消息持久化、记忆召回、上下文压缩、LLM 和工具等慢操作。
3. 提交由旧快照产生的结果前重新验证 generation/version防止 `/stop``/clear``/delete` 后写回陈旧状态。
4. Session 持久化由独立 `persistence_lock` 串行化;批量消息使用原子写入,失败时精确回滚内存后缀。
5. 首次请求上下文溢出且尚未执行工具时,按 Provider 返回的真实限制提交 checkpoint 并正式重试一次;工具执行后的溢出只能在原 AgentLoop 内保留当前工具链进行一次请求级恢复。
6. mailbox 的接收与关闭原子互斥;所有输入在 Session 锁内取得单调序号,未消费 steering 与普通队列按该序号恢复,不能丢失或互相超越。
WebUI/TUI 的 Active Turn 使用 `send_message(files=...)` 向自身 session 投递附件时,附件暂存到 task-local Turn delivery成功结束后并入最终 assistant 消息,因此工具链始终排在附件回复之前且不会出现自引用来源前缀。其他自投递要求 task-local Turn ID 与 session 的 active Turn 匹配;历史中的 assistant/system 附件只作为文本清单提供给模型,原生媒体块仅用于 user 输入和当前工具结果。
### 活动 Turn
每个主 Agent 请求会创建一个内存 Turn。Provider delta 经 AgentLoop 转换为 reasoning、正文、工具开始/完成等语义事件TurnController 归约为有序 block 和单调 revision 的完整快照。TUI/WebUI 使用 `history + active_turn` 渲染,不自行拼接 token中间帧可丢下一快照会自动收敛。
展示策略在 Gateway 核心出口应用:交互客户端可显示 reasoning 和详细工具状态;外部渠道隐藏 reasoning、工具仅显示紧凑状态无人值守投递只保留正文。运行态不逐 token 入库,完成、取消或中断时才原子保存消息及 completion status。
### 会话恢复
从 Storage 恢复 session 时:
- 加载全部原始消息及 Session 的活动 checkpoint
- 有 checkpoint 时确定性投影累计摘要和 `first_retained_seq` 之后的原始尾部;没有 checkpoint 时投影全部原始消息
- Timeline 和 `last_compressed_message_at` 不参与恢复边界判断,恢复过程不调用 Provider
- 自动修复断链的工具调用gateway 崩溃中途重启导致)
---
## 记忆系统
### 记忆类别
| 类别 | 用途 | 生命周期 | 检索方式 |
|------|------|----------|----------|
| **Knowledge** | 事实、偏好、模式、洞察 | 长期保留,手动删除 | 每轮注入系统提示,关键词匹配 |
| **Timeline** | 历史会话摘要 | 配置预期保留 90 天;当前尚无自动清理循环 | `timeline_recall` 工具按需检索 |
### MemoryEntry 字段
| 字段 | 说明 |
|------|------|
| `id` | UUID |
| `key` | 唯一键,同 key 写入覆盖旧值 |
| `content` | 记忆内容 |
| `category` | `knowledge``timeline` |
| `importance` | 权重 (0.01.0)Timeline 默认为 0.3 |
| `session_id` | 关联会话(可选) |
### 存储与检索
- 主表 `memories` + FTS5 虚拟表 `memory_fts(key, content)` 全文索引
- 中文分词使用 jieba-rs 逐词精确匹配,用 OR 连接
- FTS5 无结果时回退到 LIKE 模糊匹配
- 支持 category、session_id、时间范围过滤
### 工作流程
```
用户消息到达
→ MemoryManager::recall(content, 5, Knowledge)
返回最多 5 条匹配的知识记忆(按 importance DESC
→ 格式化为 "- key: content"
→ 作为运行时上下文附加到本轮 user message
→ LLM 可见,辅助回答
```
### 记忆工具
| 工具 | 写操作 | 说明 |
|------|--------|------|
| `memory_store` | 是 | 存储 Knowledge。必填: key, content。可选: importance |
| `memory_recall` | 否 | 搜索 Knowledge。必填: query(空格分隔关键词)。可选: since, until, limit |
| `timeline_recall` | 否 | 搜索 Timeline压缩后的会话摘要。必填: query。可选: session_id, since, until |
| `memory_forget` | 是 | 按 key 删除记忆 |
### 上下文压缩与 Timeline
自动压缩在完整请求占用满足 `context_tokens > context_window - effective_reserve` 时触发。压缩器从尾部按完整 Turn 尽量保留近期历史,用一次 LLM 调用生成累计摘要,并以 `first_retained_seq` 记录精确尾部边界。摘要请求按当前模型窗口动态限制输入;若待压缩源更大,只在请求副本中保留已有 checkpoint、最新消息和确定性 head/tail 摘录原始内容仍完整持久化。checkpoint 与 Session 活动指针在同一 SQLite 事务中提交;提交失败继续使用旧投影。原始消息与工具结果始终保留,旧内容只是不再进入 Provider context。
语义 checkpoint 提交后会 best-effort 写入一条 **Timeline**importance 0.3)供主动检索,但 Timeline 不是恢复权威。真实 context overflow或换模/改配置后在发送前已检测到的硬超限,能使用无语义 breadcrumb 降级,且只重试一次正式 Provider 请求;低于硬窗口的普通自动压缩和 `/compact` 失败时不会裁掉历史。
### 关键集成点
| 时机 | 操作 |
|------|------|
| 每次消息处理 | `memory_manager.recall()` 提取 Knowledge 上下文 |
| 系统提示构建 | `MemorySection` 渲染记忆工具指南;匹配的 Knowledge 附加到本轮 user message |
| 有活动 checkpoint 时 | 累计摘要和精确 raw tail 组成 Provider 历史 |
| 语义 checkpoint 提交后 | 摘要 best-effort 存储为 Timeline 记忆 |
| 会话恢复 | 从 checkpoint 与原始 seq 确定性重建,不读取 Timeline |
`memory.recall_limit``idle_consolidation_minutes``timeline_retention_days``max_failures_before_degrade` 当前会被配置解析;其中每轮 Knowledge 召回在 worker 中仍固定为 5其余自动维护策略尚未接入运行循环。不要把“配置可解析”误认为“行为已生效”。
---
## MCP 工具集成
Gateway 初始化时读取 `config.mcp.servers`
1. 按服务器配置连接 `stdio``sse``streamable-http` 传输
2. 调用 MCP `list_tools`
3. 将每个 MCP tool 包装为 `McpToolWrapper`
4. 注册到当前 session 的 `ToolRegistry`
`/mcp` 斜杠命令会显示 MCP 服务器连接状态和工具列表。
---
## 子 Agent / delegate
`delegate` 工具用于把独立任务交给子 Agent
| 模式 | 行为 |
|------|------|
| `foreground` | 当前轮等待一个或多个子 Agent批量任务并发执行并按请求顺序聚合全部持久化到 `agent_runs` |
| `background` | 异步执行并立即返回 run ID仅限 Root 对具名 Agent 的单任务,结果经 durable inbox 由主 Agent 的 continuation Turn 汇总 |
### 具名 Agent Definition身份设定
启用 `agent_orchestration` 后,每个具名子 Agent 是一个 Markdown 文件:`<配置目录>/agents/<id>.md`(默认 `~/.picobot/agents/`。frontmatter 只保存非秘密引用与限制API key/base URL 仍在 `config.json`/`.env`
```md
---
id: researcher
description: 搜索、阅读并整理技术资料
llm_profile: research-sonnet
tools:
- file_read
- file_search
- content_search
- web_fetch
delegates:
- reviewer
skills:
- technical-research
limits:
timeout_secs: 900
max_iterations: 24
max_children: 4
max_depth: 3
signal:
delivery: steer
severity_allowlist: [info, warning, critical]
---
# Role
你是一名严谨的研究 Agent。只返回与任务有关的结论、证据和不确定性。
```
- `id``[a-z][a-z0-9_-]{0,63}`,文件名必须与 id 一致;`root`/`main`/`default`/`general` 为保留名。重复 ID、大小写折叠冲突、越界 symlink 或引用错误(未知 Provider profile、未注册/不可委托工具、未知 skill 或 delegate 目标)会拒绝整个候选运行代,绝不静默裁剪。
- `llm_profile`:引用 `config.json``agents` keyDefinition 绑定 Provider 与模型,运行中不热切换。
- `tools`/`skills``tools` 直接指定该 Agent 可用的全部普通工具;`skills` 声明要求工具集含 `get_skill`,且只注入该 allowlist。运行时注入工具`delegate`/`emit_signal`/`agent_task`)不能写进 `tools`
- `delegates`:出边白名单,运行时还校验 ancestry 重复、`max_tree_depth` 与树级 `max_runs_per_tree` 预算。
- `signal`:可选信号契约。带该块的 run 才获得 `emit_signal` 工具fail-closed`delivery: steer` 使信号在活动 Turn 的安全边界注入主 Agent`queue` 走 continuation。
- 角色正文(`---` 之后)即 `role_prompt`,与 frontmatter 一起做 SHA-256 `definition_hash` 快照。
### 执行与投递
- 每次具名委托先持久化 run含 execution_id、budget、Definition 快照),`allowed_tools` 只能收窄、不能扩权;子 Agent 输出视为不可信数据。
- foreground 父 run 等待子 run 时进入 `waiting_children` 且不占 provider/tool step permitrun quota 只约束 background 接纳,嵌套 foreground 并发上限为 1 时不死锁。
- background 接纳时预留 completion slot容量条件更新runner 持有 run quota permit 与 activity guard 直到 terminal commit完成后 completion 事件落 `agent_inbox_events`Session worker 按 `max_user_turn_burst_before_inbox`/`max_inbox_wait_secs` 公平调度,以 hidden trigger + 只读工具集的 continuation Turn 让主 Agent 汇总结果,不再直接发 Channel 通知。失败按 lease token 释放重试,超 `max_inbox_delivery_attempts` 进 dead-letter重启经 activation recovery 收敛。
- `agent_task`get/list/get_result/cancel查询与控制 run`agent_task.cancel` 会把该 run 未消费的普通信号标记 superseded。`/stop` 取消活动 run 但保留已存在的 pending 事件archive/delete 取消 run 并将未消费事件 dead-letter。
- 后台 run 内可调用 `emit_signal`key/severity/summary/details/dedupe_key总数、速率、burst、severity allowlist、载荷大小/深度与冷却窗去重均由契约强制steer 信号经两阶段 admissionclaim → mailbox 预留 → admit(turn_id) → 激活)注入当前 Turn`/stop` 时按 token 条件释放回 pending绝不静默丢弃。
- 每个 run 的完成事件 payload 携带该 run 已发出的 signal IDs主 Agent 可识别重复报告。
旧匿名 general 兼容路径已移除:委托必须指定具名 `target`
后台子 Agent 通过 `TaskSupervisor::spawn_graceful` 注册Gateway 关停时先收到取消信号,再在总宽限期内清理。
## Session Todo 计划
每个 session 最多有一个 active plan普通闲聊没有计划上下文。计划和子项分别持久化到 `task_plans``task_items`历史压缩后仍从权威状态生成精简摘要。WebUI 聊天页通过 `session_plan``plan_updated` WebSocket 帧显示默认隐藏的侧栏,当前 session 变化时自动展开,其他 session 只标记未读。
---
## 出站投递与关停
`OutboundDispatcher` 对每个 `(channel, chat_id)` 创建独立 lane同一目标保持顺序单次发送超时 30 秒,最多尝试 3 次。只有 `ConnectionError``SendError` 被视为瞬态错误;永久错误不重试。`deliver_outbound` 等待真实渠道投递结果,`publish_outbound` 只表示成功入队。
Gateway 关停顺序:
1. Ctrl-C 停止 Axum 接入并取消 WebSocket 连接。
2. `ChannelManager::stop_all` 停止外部渠道并注销它们。
3. 取消 TaskSupervisor并在共享的 10 秒宽限期内等待后台任务。
4. 超时后 abort 剩余任务并回收 JoinHandle。
渠道 `stop()` 也必须有界。飞书端点请求、WebSocket 建连、重试 sleep 和连接循环共享 CancellationToken另有 5 秒强制终止兜底。
---
## 当前斜杠命令
| 命令 | 说明 |
|------|------|
| `/new` | 创建新对话 |
| `/sessions` | 列出最近对话 |
| `/switch <dialog_id>` | 切换到指定对话 |
| `/rename <title>` | 重命名当前对话 |
| `/delete` | 删除当前对话 |
| `/compact` | 手动触发上下文压缩 |
| `/info [--json]` | 显示当前对话、累计 Token 与上下文窗口信息;可选 JSON 输出 |
| `/dump` | 保存当前对话为 markdown |
| `/?`, `/help` | 显示帮助 |
| `/mcp` | 显示 MCP 状态 |
| `/health` | 检查 PicoBot 运行依赖 |
| `/queue <message>` | 等当前 Turn 完成后作为下一 Turn 处理 |
| `/stop` | 停止当前任务并清空消息队列 |