- AgentCatalog/definitions with strict Markdown frontmatter, delegation graph, fail-closed tool scoping, and signal contracts - structured cancellation (AgentError::Cancelled/TimedOut) across provider streams, tool batches, and sleep; /stop drives the same terminal state - schema v6 run/group/inbox persistence with execution-ID conditional transitions and completion-slot reservations - ExecutionGate separating run quota from provider/tool step permits - background completion inbox with hidden-trigger continuation turns, fairness scheduling, lease release, dead-lettering, and activation recovery - typed TurnMailbox with two-phase steer admission and atomic consumption at turn commit; /stop releases admitted steer events back to pending - emit_signal tool with contract-enforced rate/dedupe/severity/size limits - WS run/event projection (GetAgentRuns, AgentRunUpdated, AgentEventUpdated), /api/agent-runs* management endpoints, /api/tasks union, WebUI run tree and signal cards - ChannelContext.durable_private persisted for continuation delivery reuse Version 1.7.0
18 KiB
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 |
MCP(Model 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 状态的唯一 owner;Session 在消息原子提交成功后才发布 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 profile、工具/Skill allowlist、委托边与限制;新工具默认 RootOnly。Phase 1 支持单个/批量 foreground(批量并发、按请求顺序返回)和显式父子授权;具名 background 在 durable run/inbox 完成前拒绝。禁用编排时旧 general background 仍通过 MessageBus 直接通知原会话 - 复杂任务可使用
todo创建 session 级计划;多个子项可通过delegate.plan_item_id并行委托,子 Agent 不能修改计划 - WebUI 聊天复用
/ws与cli_chat;同源管理 API 只读取受限的日志、任务、记忆,并对白名单配置文件做原子写入 - WebUI 使用 Svelte 5 + Vite,Bits UI 提供无样式可访问组件;
cargo build增量生成前端到 CargoOUT_DIR,再嵌入单二进制,仓库不保存生成产物 - WebUI 通过
get_session_stats/session_stats显示当前会话累计输入输出 Token 和上下文窗口占用;/info [--json]读取同一份 SessionStats
关键约束
- Gateway 启动时切换到 workspace 目录
- SQLite 数据在
{workspace}/picobot.db - 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。同一 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
上下文压缩
当上下文接近 token 限制时触发:
- 快速裁剪:合并连续同角色消息,截断工具输出
- 硬截断:移除过老消息
- 压缩后保留用户消息确保结构完整
Skill 系统
三个优先级(高覆盖低):
{workspace}/skills/— 最高优先级~/.picobot/skills/— 中等优先级~/.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>>>— 所有已加载的 session,key 为完整 session IDcurrent_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 的处理原则:
- 短暂持 Session 锁抓取快照并记录
worker_generation/state_version。 - 释放锁后执行消息持久化、记忆召回、上下文压缩、LLM 和工具等慢操作。
- 提交由旧快照产生的结果前重新验证 generation/version,防止
/stop、/clear或/delete后写回陈旧状态。 - Session 持久化由独立
persistence_lock串行化;批量消息使用原子写入,失败时精确回滚内存后缀。 - 上下文溢出时按 Provider 返回的真实限制重新压缩并重试。
- 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 时:
- 若
last_compressed_message_at存在:先加载近 3 条 Timeline 记忆作为[Previous Context],再加载压缩标记后的原始消息 - 若无压缩记录:正常加载全部消息
- 自动修复断链的工具调用(gateway 崩溃中途重启导致)
记忆系统
记忆类别
| 类别 | 用途 | 生命周期 | 检索方式 |
|---|---|---|---|
| Knowledge | 事实、偏好、模式、洞察 | 长期保留,手动删除 | 每轮注入系统提示,关键词匹配 |
| Timeline | 历史会话摘要 | 配置预期保留 90 天;当前尚无自动清理循环 | timeline_recall 工具按需检索 |
MemoryEntry 字段
| 字段 | 说明 |
|---|---|
id |
UUID |
key |
唯一键,同 key 写入覆盖旧值 |
content |
记忆内容 |
category |
knowledge 或 timeline |
importance |
权重 (0.0–1.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
LLM 对话上下文接近 token 限制 (默认 128K × 70%) 时自动触发压缩:
- 快速裁剪:工具输出 ≥ 2000 字符时截断
- LLM 摘要:最多 3 轮,每轮找连续用户消息对,将中间的 assistant/tool 消息压缩为摘要 → 摘要作为 Timeline 记忆 持久化(importance 0.3)
- 硬截断:若仍超 90%,只保留前 N + 后 N 条消息
压缩后 last_compressed_message_at 标记边界,后续恢复时从标记点加载原始消息,以 Timeline 提供更早的上下文。
关键集成点
| 时机 | 操作 |
|---|---|
| 每次消息处理 | memory_manager.recall() 提取 Knowledge 上下文 |
| 系统提示构建 | MemorySection 渲染记忆工具指南;匹配的 Knowledge 附加到本轮 user message |
| 有压缩历史时 | HistorySection 提示 LLM 使用 timeline_recall |
| 压缩完成后 | 摘要自动存储为 Timeline 记忆 |
| 会话恢复 | 加载最近 Timeline 和压缩边界后的原始消息 |
memory.recall_limit、idle_consolidation_minutes、timeline_retention_days 和 max_failures_before_degrade 当前会被配置解析;其中每轮 Knowledge 召回在 worker 中仍固定为 5,其余自动维护策略尚未接入运行循环。不要把“配置可解析”误认为“行为已生效”。
MCP 工具集成
Gateway 初始化时读取 config.mcp.servers:
- 按服务器配置连接
stdio、sse或streamable-http传输 - 调用 MCP
list_tools - 将每个 MCP tool 包装为
McpToolWrapper - 注册到当前 session 的
ToolRegistry
/mcp 斜杠命令会显示 MCP 服务器连接状态和工具列表。
子 Agent / delegate
delegate 工具用于把独立任务交给子 Agent:
| 模式 | 行为 |
|---|---|
foreground |
当前轮等待一个或多个子 Agent;批量任务并发执行并按请求顺序聚合 |
background |
异步执行并返回 run ID;当前只有旧 general 兼容路径可用 |
启用 agent_orchestration 后,具名 Definition 固定角色、Provider profile、工具/Skill allowlist、委托边和限制。allowed_tools 只能收窄 Definition,不能扩权;新工具默认 RootOnly,当前明确可委托的工具包括 file_read、file_search、content_search、web_fetch、calculator、普通 browser 动作和 sleep。具名 Agent可按委托图继续 foreground 委托,但 ancestry 重复、越深度或不在白名单的目标会拒绝。具名 background 需要后续 durable run/inbox,当前明确拒绝。
未启用编排或省略 target 时使用旧 general 兼容路径。其工具也只能取旧默认集合与 Delegatable 策略的交集;旧后台任务写入 background_tasks 表,完成后通过原 channel/chat 直接通知,默认 24 小时后清理,不具备 durable inbox 语义。
后台子 Agent 通过 TaskSupervisor::spawn_graceful 注册,受 gateway.max_concurrent_background_tasks 限制;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 关停顺序:
- Ctrl-C 停止 Axum 接入并取消 WebSocket 连接。
ChannelManager::stop_all停止外部渠道并注销它们。- 取消 TaskSupervisor,并在共享的 10 秒宽限期内等待后台任务。
- 超时后 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 |
停止当前任务并清空消息队列 |