353 lines
24 KiB
Markdown
353 lines
24 KiB
Markdown
# 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、工具、斜杠命令和 WebUI 共用的只读运行依赖检查 |
|
||
| `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/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 + Vite,Bits 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` 的原始尾部”。Model `token_limit` 是上下文硬上限,缺失时默认 128K;可选 Agent `token_limit` 只能收紧它,有效窗口取二者最小值。自动压缩使用 `context_tokens > context_window - effective_reserve`,默认 reserve 16,384、近期原样保留 20,000 tokens;小窗口会自适应缩小两者。摘要输入预算由有效窗口扣除摘要输出、提示词和安全余量得到;超大历史生成 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>>>` — 所有已加载的 session,key 为完整 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.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
|
||
|
||
自动压缩在完整请求占用满足 `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` key,Definition 绑定 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 permit;run 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 信号经两阶段 admission(claim → 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` | 停止当前任务并清空消息队列 |
|