cell.cpp文档(中文机翻)
cell.cpp文档(中文机翻)
现代 C++26 编写的单文件 AI 编程代理
一个用现代 C++26 编写的轻量级 AI 编程代理。连接 OpenAI 或 Anthropic,与你的代码库对话,让 LLM 读取、写入、编辑并执行——全部在沙箱中完成。
cell: cwd=D:\Developments\CXX\cell.cpp session=82db5d5997826cfd-1789051971045-d72020ad model=llama-cpp:Qwen3.8-27B sandbox=full-accesscontext: loaded 1 message(s) from disk> 介绍一下自己,以及你能干什么The user is asking in Chinese: "Please introduce yourself and what you can do."This is a simple self-introduction question. According to the system prompt, I am Qwen (Tongyi Qianwen) made by Alibaba. I should reply in Chinese (language sync), and shouldn't mention specific version numbers. I should mention capabilities like:- Advanced human-computer interaction- Precision control of full attention- Long context window (about 100,000 characters / 1M context)- Knowledge cutoff date 2025- Strong in complex tasks: code, document writing, reasoning, Q&AI should also mention the specific capabilities I can actually do in this environment: file operations, code execution, search, and coordination of child agents. Since I actually have tools here (file reading/writing, shell execution, search, team child agents), I can mention them in a natural way.The identity guidelines say "don't proactively bring up the version number." I should use the same language as the user. So I'll reply in Chinese.Let me write a friendly self-introduction:- Name: Qwen (Tongyi Qianwen), developed by Alibaba- Core capabilities- Things I can do in this session: file operations, code execution, project search, multi-stage task coordination- Knowledge cutoff 2025Format: A friendly introduction using some structure (short list). Since this is a simple question, I'll keep it medium length and not make it too long.你好!我是千问(Qwen),由阿里巴巴集团研发的大语言模型。下面简单介绍一下我自己和能做的事情:## 我是谁- **名称**:千问(Qwen)- **研发方**:阿里巴巴集团- **知识截止**:2025 年- **长文本能力**:原生支持超长上下文窗口,可无损处理数十万字的文档,支持理解长达数小时的视频内容## 我能做什么**核心能力方面:**- 复杂任务处理:逻辑推理、代码编写与调试、文档写作、多轮对话、创意生成- 精准的信息检索与理解:支持超长上下文,能处理海量文档并快速定位关键信息- 内容创作:写作、翻译、摘要、头脑风暴、方案规划- 编程:代码生成、解释、重构、排错、跨语言支持- 问答:知识、推理、分析**在当前这个环境里,我还能实际动手操作:**- 读写文件、修改代码、执行 shell 命令- 搜索和检索项目内容- 把大任务拆分成多个子任务并行推进如果你有具体的任务——比如想让我写一段代码、分析一份文档、整理某个问题,或者在这个项目里做点什么——直接告诉我需求就行。[2026-09-21 06:51:48] INFO [llm ] round=1 model=llama-cpp:Qwen3.8-27B stream=true ctx_msgs=2 tok_in=3197 tok_out=601 cache=0.0% time=36.99s ttf=13.38s tools=0 attempts=1> 检查工具列表reply> The user is asking me to check the tool list. Let me check the tools available to me. The system prompt lists the tools I have access to. Let me provide an overview.Tools available:1. `ls` - Directory listing (non-recursive, paginated)2. `read` - Read files (text with line numbers, binary as base64)3. `write` - Create new files (refuses to overwrite existing files)4. `edit` - Modify existing files (replace/insert/append/delete/query modes)5. `rg` - Regex search over file contents6. `find` - Search for files by glob/metadata7. `exec` - Execute shell commands (timeout default 30s, max 300s)8. `tw` - Manage Teamwork child agent jobs (serial/parallel/workflow)9. `notice` - Message bus for agent sessionsI can also summarize the tool list. The user asked in Chinese, so I'll respond in Chinese.I could also demonstrate by listing the current directory to show what I can do. Let me do a simple `ls` to verify the tools work.好的,我当前环境里可用的工具有以下几个:## 文件 / 项目操作类| 工具 | 用途 ||---|---|| `ls` | 列目录内容(非递归,支持分页) || `read` | 读文件(文本带行号;二进制文件如图片、音视频以 base64 返回,方便多模态处理) || `write` | 创建**新文件**(拒绝覆盖已有文件) || `edit` | 修改已有文件:支持 replace / insert / append / delete / query 五种模式 || `find` | 按 glob 模式 + 元数据(文件名、修改时间、大小)查找文件 || `rg` | 递归正则搜索文件内容(支持忽略大小写、上下文行、扩展名过滤、计数模式) |## 执行 / 协作类| 工具 | 用途 ||---|---|| `exec` | 执行 shell 命令(默认超时 30 秒,最长 300 秒;高风险命令需二次确认) || `tw` | 管理 Teamwork 子代理任务:支持串行 / 并行 / 多阶段 workflow,可复用之前的子任务 || `notice` | 会话间消息总线:发送、列出、等待、清空通知 |## 简单总结一下- **文件操作**:读、写、改、搜、找都齐了- **命令执行**:可以直接跑 shell- **任务编排**:可以把大任务拆给多个子代理并行处理要不要我现在实际跑几个试试?比如看看当前目录里有什么文件。[2026-09-21 06:52:37] INFO [llm ] round=1 model=llama-cpp:Qwen3.8-27B stream=true ctx_msgs=4 tok_in=3488 tok_out=620 cache=91.5% time=24.89s ttf=1.03s tools=0 attempts=1> /export chat.htmlexported 5 message(s) to cell-export-20260921-065306.html> /quit目录
功能特性
| 功能 | 所在位置 | |
|---|---|---|
| 🤖 | 三种 API 风格——OpenAI Chat Completions({base}/chat/completions)、OpenAI Responses({base}/v1/responses)和 Anthropic({base}/v1/messages),均支持流式与非流式,并支持按提供方配置 HTTP(S) 代理 | cell::llm、cell::net |
| 🧩 | 提供方注册表——任意数量的命名端点;模型列表从提供方实时获取,只持久化当前激活的模型名 | cell::config |
| 🔧 | 9 个内置工具——ls、read、write、edit、rg、exec、find、tw、notice | cell::box、cell::tools |
| 👥 | Teamwork——受监督的子代理,可配置工作流(tw 工具 + /teamworks):命名阶段、每阶段调度模式、监督门控、同任务复用、带协作式停止的后台运行、每个子代理可单独指定提供方/模型/思考等级、复用先前子代理(可选择是否重放其对话记录)、可查询历史与报告,以及文件租约——按子代理授予写入权限,并强制审查它对被授权文件所做的每一次修改 | cell::teamwork |
| 📣 | 通知——代理会话之间的零 token 消息总线:Teamwork 子代理报告进度,监督者在 /notices wait 中空闲并会被立即唤醒,全程无需一次 LLM 往返 | cell::notice |
| 🛡️ | 三种沙箱模式——read-only、edit-only、full-access;凭据/运行时文件在所有模式下均不可访问(符号链接与指向运行时根的 Windows junction 会被跟随并阻止,包括尚不存在的目标),且 exec 是唯一需要确认提示的工具 | cell::box::check_exec |
| 🧨 | 提示注入清理器——每个 exec 结果都会扫描命令覆盖指纹,能抵抗同形字、零宽字符、标点连接 token 和多行拆分 | cell::box::sanitize_output |
| 🔐 | 加密凭据保险库——Argon2id 密钥派生 + AES-256-GCM(XChaCha20-Poly1305 回退),sodium_malloc/sodium_memzero 秘密缓冲区 | cell::encrypt |
| 💬 | 按目录会话——会话 id 嵌入工作目录的哈希;切换会话会跟随其 cwd | cell::chat |
| ⏸️ | 可中断轮次——Esc 或请求失败会保留部分回复(截断到最后一个 token),取消任何半流式工具调用,追加一条说明原因的系统消息,并记住状态,使 /reload 从轮次停止处继续 | cell::chat::record_interrupt |
| 🗜️ | 自动上下文压缩——在上下文溢出错误时(压缩并重试一次),以及在长时间工具密集型轮次后;压缩模型可以不同于聊天模型 | cell::chat、compact() |
| 🧠 | 思维链——/think 有 5 个等级(off/low/med/high/max);OpenAI 流式输出 reasoning_content,Anthropic 流式输出 thinking_delta,两者均以暗色渲染;推理内容会持久化到会话中 | cell::llm |
| 🔎 | 带行号读取——read 返回的每一行都带右对齐行号,rg 将命中分组为 === file === + line: content,因此模型可以引用准确行号来 edit | cell::box |
| 📦 | 技能——.cell/skills/ 下带 YAML front matter 的 Markdown 文件(递归、支持目录式),作为系统消息注入 | cell::skills |
| 📊 | 使用统计——按模型和按会话的请求/token/字符计数器,以及提示缓存命中率 | cell::stats |
| ⌨️ | Shell 风格行编辑——提示符支持光标键、按会话的输入历史、Home/End/Delete、Ctrl+A/E/B/F/K/U/W;UTF-8 感知,仅在输入一行时处于 raw 模式 | cell::plat::read_line |
| ⚡ | 异步 I/O——会话/配置/统计写入会被合并,并由一个后台线程刷新;动态工作池并发运行只读工具调用 | cell::async_io、cell::sys::thread_pool |
| 🖥️ | 跨平台——Windows(Job Objects、cmd.exe、UTF-8 代码页、虚拟终端)和 POSIX(fork/exec、termios);所有 OS 相关代码隔离在一个命名空间中 | cell::plat |
| 🧪 | 自测——数百条断言,覆盖沙箱、编辑器、清理器、加密、配置、会话、技能、统计和工具注册表 | --selftest |
快速开始
先决条件
- 一个支持 C++26 的工具链,且标准库支持
<format>、<expected>、<generator>、<span>、<concepts>和std::move_only_function——例如 GCC 15+(MSYS2 UCRT64)或较新的 MSVC/Clang。这非常前沿:请使用较新的标准库,而不是 LTS 版本。 - libcurl(任意较新的后端)、libsodium、nlohmann/json(仅头文件)。
- 推荐的包管理器:vcpkg
vcpkg install curl nlohmann-json libsodium构建
# POSIXg++ -std=c++26 -O2 -o bin/cell cell.cpp -lcurl -lsodium# Windows / MSYS2(CA 证书包导出会遍历系统证书存储:crypt32)g++ -std=c++26 -O2 -o bin/cell.exe cell.cpp -lcurl -lsodium -lcrypt32# MSVCcl /std:c++latest /O2 /EHsc cell.cpp /Fe:bin\cell.exe /I<deps>\include /link libcurl.lib libsodium.lib crypt32.lib运行
# 首次启动:注册一个提供方(密钥会进入加密保险库)bin/cell --provider openai --base https://api.openai.com/v1 --model gpt-4o --key YOUR_API_KEY# Anthropic 风格端点bin/cell --provider anthropic --base https://api.anthropic.com --model claude-sonnet-4-20250514 --key YOUR_API_KEY# 任意 OpenAI 兼容网关(Ollama、vLLM、One-API……)bin/cell --base http://localhost:11434/v1 --model qwen2.5-coder# 非交互式:stdin 是一条消息,打印回复后退出echo "explain what this repo does" | bin/cell# 验证构建bin/cell --selftest在 REPL 中,无需重启即可管理提供方:
/provide add openai:https://api.openai.com/v1 key:YOUR_API_KEY/models/model gpt-4o/think low 命令行选项
usage: cell [options] # 原样来自 print_usage —— --sandbox 行夸大了模式 --provider NAME select an existing provider, or create one (style = NAME) --base URL api base url for the current provider --model MODEL default model name --proxy URL http(s) proxy for the current provider --key KEY api key (saved to the encrypted vault) --session ID resume an existing session (switches to its working directory) --system TEXT system prompt --sandbox MODE exec sandbox mode: read-only | edit-only | full-access (default) --no-color disable colored log output --verbose enable DEBUG-level log output on console --selftest run internal self tests --help, -h show this usage summary and exit | 选项 | 值 | 效果 |
|---|---|---|
--provider | 名称 | 按名称选择已有提供方;如果没有匹配项,则创建一个提供方,其名称同时作为 API 风格(openai / anthropic);对于没有显式风格的 OpenAI 风格提供方,API 风格默认为 openai-chat。切换到不同提供方会清除 current_model——已存储的模型名属于前一个提供方——而重新选择已经激活的提供方(包括空的 current_provider 情况,表示“第一个提供方处于激活状态”)会保留它;同一命令行中显式的 --model 会在之后应用。 |
--base | URL | 在当前提供方上设置 base。没有内置默认 base——空 base 意味着提供方不可用,直到你设置一个。 |
--model | 字符串 | 激活的模型名,按原样存储(不必出现在 /models 中;不匹配只会警告)。 |
--proxy | URL | 当前提供方的 HTTP(S) 代理,凭据可以内嵌。localhost、127.0.0.1、::1 始终绕过它(CURLOPT_NOPROXY);HTTPS 目标通过 CONNECT 隧道。 |
--key | 秘密 | 加密写入 .cell/.crypt,id 为 provider:<name>,并绑定到当前提供方;明文副本立即清零(sodium_memzero)。 |
--session | id | 存储为 cfg.session_id。参见实现说明:启动时总是打开新会话,因此请用 /session ID 恢复。 |
--system | 文本 | 完全替换系统提示。 |
--sandbox | read-only/readonly、edit-only/edit、full-access/full | 设置 exec 沙箱模式并持久化为 sandbox_mode。未知值会警告,并将存储值改写为 full-access。 |
--no-color | 标志 | 禁用 ANSI 颜色(反正只有当 stdout 是终端时才会使用颜色)。 |
--verbose | 标志 | 将 DEBUG 日志行镜像到控制台(它们始终写入日志文件)。 |
--selftest | 标志 | 在隔离的 .cell-selftest/ 目录中运行内置测试套件并退出(0 = 全部通过)。 |
--help、-h | 标志 | 打印用法摘要并以 0 退出。 |
加载顺序。 首先读取 .cell/config.json(解析错误会作为警告报告,程序继续使用默认值);旧配置形态会在加载时迁移。然后 CLI 标志应用在已加载设置之上。退出时合并后的配置会写回,因此 --base / --model / --proxy / --sandbox 更改是持久的。
API 密钥解析(每次请求,先匹配者胜):
- 绑定到当前提供方的保险库条目(
provider:<name>); - 环境变量
OPENAI_API_KEY/ANTHROPIC_API_KEY(由提供方风格选择); - 通用保险库条目
api_key。
如果都不可用,该轮次会失败,并给出解释如何设置的提示信息。
数字参数。 配置字段和工具参数同时接受 JSON 数字和带引号的数字字符串("log_max_lines": "500"、{"offset": "85"});垃圾值或缺失值会回退到默认值而不是抛出异常。
斜杠命令
以 / 开头的输入会按空白拆分并在本地处理——永远不会发送给模型。
| 命令 | 描述 |
|---|---|
/help | 打印命令列表 |
/provides | 列出已配置的提供方(style、api_style、base、proxy、密钥状态、model) |
/provide NAME | 选择一个提供方(立即持久化)。切换到不同提供方会将模型重置为未设置——已存储模型属于前一个提供方——因此请用 /models + /model NAME 选择新模型;重新选择当前提供方会保留其模型。切换后 REPL 会打印保留的模型或“未设置模型”的提示。 |
/provide add API_STYLE:URL [key:KEY] [proxy:URL] [name:ALIAS] | 添加并选择一个提供方;前缀是该 URL 的 API 风格(openai-chat | openai-responses | anthropic;openai 被接受为 openai-chat 的别名,也接受带空格的 openai: URL 形式)。选择新提供方会清除之前的模型名。添加后,会获取一次模型列表作为连接探测。 |
/provide update NAME [base:[API_STYLE:]URL] [key:KEY] [proxy:URL] [name:NEW_NAME] | 就地更新已有提供方。API 风格与它所应用的 base URL 一起设置,例如 base:anthropic:https://api.anthropic.com;单独 base:URL 保持当前风格。只有提供的字段会改变;重命名提供方会将其加密保险库密钥带到新的保险库 id,请求的名称如果已存在,会以 NEW_NAME-N 变为唯一。 |
/provide rm NAME | 删除提供方及其保险库密钥;如果它是当前提供方,选择会移动到第一个剩余提供方,并清除模型名 |
/models | 获取并打印当前提供方的模型列表(<current> 标记当前激活项) |
/model NAME | 切换模型;如果名称不在已获取列表中则警告 |
/model NAME [api_style:STYLE] | 可选地即时切换当前提供方的 API 传输风格(openai-chat | openai-responses | anthropic;openai 被接受为 openai-chat 的别名);新风格持久化到 config.json,并由 /model 和 /provides 显示 |
/think [off|low|med|high|max] | 设置思维链等级(持久化;未知等级会打印用法并保留前一个);等级:off(默认)、low(1024 token)、med(2048)、high(4096)、max(8192);裸 /think 显示当前等级和预算 |
/tool [on|off] | 切换工具调用——off 表示完全不发送工具定义 |
/sandbox [mode] | 显示或设置 exec 沙箱模式(持久化);有效值为 read-only/readonly、edit-only/edit、full-access/full——其他值会被拒绝并给出用法错误 |
/autoallow [on|off] | 切换 autoallow 模式(持久化,仅 full-access)——启用时,由 LLM 决定 exec 命令是否无需用户确认即可运行 |
/sessions | 按工作目录分组列出已保存会话(> = 当前 cwd,* = 当前会话) |
/session ID | 切换到已保存会话;进程 cwd 会跟随会话目录 |
/session rm ID | 删除会话(包含对话记录、Teamwork 数据、归档的文件夹)及其使用记录 |
/saved [list] | 列出当前会话的压缩归档(会话文件夹中的 saved/msg-<UTC time>.jsonl) |
/saved show NAME | 显示一个归档对话记录(精确名称或唯一子串) |
/saved rm NAME | 删除一个归档对话记录 |
/export [PATH] | 将当前会话导出为自包含 HTML 文件(单文件,无脚本/外部资源;明暗自适应;全宽双栏布局——左侧粘性栏显示执行链,与右侧消息 1:1 链接,可在右下角拖动加宽;轻量 Markdown 渲染——标题、列表、引用、分隔线、管道表格、code、粗体、斜体、链接——工具调用、工具结果和推理显示为折叠区;Teamwork 任务获得代理列表窗格:侧栏是可点击的任务/阶段/子代理列表,可直接跳转到每个代理的报告 + 完整对话记录,标签换行而不是滚动;工作流任务获得流程图子页面,带自己的侧栏——垂直栏上的开始/阶段/终止节点,并行阶段分叉为每个代理的分支框,门控绘制为虚线菱形;手机友好:顶栏和字号收紧,视图标签变为拇指可达的底栏,每条侧栏折叠为点击打开的抽屉(一个隐藏的 checkbox——依旧没有脚本),列宽装不下的并行分叉会折叠为一列;默认在工作目录中输出 cell-export-<UTC time>.html) |
/export saved NAME [PATH] | 将一个压缩归档导出为 HTML(名称解析方式同 /saved show) |
/usages | 打印按模型和按会话的使用统计(先修剪孤立的会话记录) |
/compact | 将完整对话记录归档到 saved/msg-<UTC time>.jsonl,然后将对话(加上代理推理)聚合成一条摘要消息,回填为紧跟在最近用户轮次之后的独立助手回复;当上下文很小(≤ 12 条消息)时拒绝 |
/compact auto [on|off] | 显示或切换长时间代理运行后的自动压缩(持久化,默认开启) |
/compact model provider:model | 通过特定已注册提供方/模型路由摘要;inherit 将其重置为会话模型 |
/ins TEXT | 插入一条用户消息并获得响应(注入文本并触发一次 LLM 往返) |
/reload | 恢复被中断的轮次:对话记录已经以部分回复加上解释中断的系统消息结尾,因此只需再次发送——不添加用户消息(参见中断) |
/skills | 列出可用技能 |
/skill NAME | 将技能正文作为系统消息注入当前会话 |
/teamworks | 列出当前会话的 Teamwork 任务(参见 Teamwork) |
/teamworks all | 列出此工作目录中每个会话的 Teamwork 任务 |
/teamworks new | 创建一个空 Teamwork 任务并打印其 id |
/teamworks id:ID bg:none|prolegomena works:TEXT [provider:P] [model:M] [think:L] [reuse:JOB/worker] [writes:FILE,...] [rules:TEXT] [name:NAME] | 向待处理任务追加子任务(工作流任务会归入最后一个阶段) |
/teamworks workflow [FILE] | 从工作目录中的 JSON 文件加载工作流/任务配置(默认 teamwork.workflow.json;也接受裸 {stages:[…]})并创建任务 |
/teamworks run ID | 运行任务并打印其合并报告;带门控的阶段会暂停任务——再次运行以恢复 |
/teamworks run_bg ID | 在专用运行线程上后台运行任务;进度以通知形式到达 |
/teamworks resume ID | 从保存的阶段继续一个带门控/已停止的工作流任务 |
/teamworks stop ID | 请求协作式停止(在下一个 worker 轮次或阶段边界暂停) |
/teamworks status ID | 显示一个任务及其阶段计划、门控和进度 |
/teamworks max [N] | 显示或设置子代理限制(1–100,持久化) |
/teamworks rm ID / /teamworks rm ID:worker_N | 删除一个任务,或一个子任务及其报告 |
/teamworks history [JOB] [worker:W] [n:N] [all] | 列出过去的子代理任务,含状态/模型/轮次 |
/teamworks reports [JOB] [worker:W] [n:N] [full] [all] | 打印多个子报告(最新在前) |
/teamworks report JOB:worker | 打印一个子代理的报告 |
/notices | 查看当前会话的待处理通知(参见通知) |
/notices sessions | 列出并发会话(id、工作区、待处理通知、空闲时间),以便向另一个会话发送通知 |
/notices wait [SEC] [from:X] [topic:Y] | 零 token 阻塞,直到匹配通知到达,然后将待处理通知送入对话(默认 60 秒,最大 3600 秒) |
/notices send [SESSION] TEXT | 向一个会话发送通知——SESSION 解析为精确 id、唯一 id 前缀、16 位十六进制工作区键、工作区路径、reply(最新通知的发送者)或 current;无法解析的首个 token 会被当作当前会话的消息正文 |
/notices drain | 将待处理通知消费到对话中(每次 LLM 轮次前也会自动发生) |
/notices clear | 丢弃待处理通知(归档到 seen.jsonl) |
/notices seen [list] | 列出累积的通知审计轨迹(seen.jsonl),含每条记录的 id 和状态 |
/notices seen clear | 将 seen.jsonl 归档到 saved/notice-<UTC time>.jsonl 并清空 |
/notices seen rm ID | 按 id 删除一条累积的通知 |
/save | 立即持久化当前会话(刷新异步写入器) |
/clear | 清空当前会话的消息但保留其 id;重新注入系统提示和技能列表 |
/new | 保存当前会话,然后开始新会话(旧文件保留在磁盘上)并重新探测提供方 |
/exit、/quit | 退出 |
思维链(/think)。 对于 OpenAI 风格提供方,不额外发送任何内容——推理模型已经流式输出 delta.reasoning_content,/think 会在答案前以暗色显示它,因此非推理模型不受影响。对于 Anthropic 风格提供方,请求体会增加 thinking: {"type":"enabled","budget_tokens":N}(其中 N 取决于等级:low=1024、med=2048、high=4096、max=8192;max_tokens 会相应提高),流式 thinking_delta / signature_delta 块以暗色渲染;思考块保留在存储的助手消息中,这是 Anthropic 在工具调用跟随思考轮次时所要求的。对于 OpenAI,reasoning_content 现在会存储在会话中,以便跨重启持久化。
技能(/skills、/skill)。 技能是 .cell/skills/ 下的 Markdown 文件,仅当它以 YAML 风格 front matter 块开头时才会被发现;扫描器会递归(深度 ≤ 6),因此目录式技能也可用:
---name: build-helperdescription: helpers for building cell---Body instructions injected when the skill is loaded…name 回退到文件主干名——对于 SKILL.md/README.md 则回退到父文件夹名——description 回退到第一条非空正文行(截断到约 120 字符);周围的引号会被剥离。只要存在有效技能,每个新会话都会得到一份仅元数据的“可用技能”列表,作为系统消息注入,以便模型可以建议加载某个技能;/skill NAME 会将完整正文(移除 front matter)作为系统消息追加到会话剩余部分。名称和描述会经过 display_safe(控制/ANSI 剥离),但不会进行指纹脱敏——重量级扫描保留给 exec 输出。
上下文压缩(/compact)。 每条 system 消息都原样保留;其他每条消息都拆分为对话文本和代理推理(思考块会在第二遍中摘要),每条消息截断到 400 字符,两部分都发送给压缩模型(compact_provider / compact_model,回退到会话模型),并禁用工具。两份摘要合并成一条
{"role":"assistant","content":"# Here is a summary that captures the previous conversation: …"}
消息,回填在幸存用户轮次的正后方:最近一条真实用户消息被保留(绝不保留 tool_result 载体),摘要作为一条独立的助手回复紧随其后——压缩后的对话记录读起来就是正常的轮次边界(user → assistant),因此下一条用户输入落在摘要之后,而不是堆到一条没有回复的用户消息上;溢出重试也能顺着摘要继续,就像 /reload 续写部分回复那样。没有真实轮次的结果会被许多聊天模板拒绝(No user query found in messages)。待聚合消息 ≤ 12 条时,它会报告“上下文已经很小”并且不做事;如果摘要调用失败,则使用截断占位符。摘要插入前会再次经过注入清理器,并且 read-before-edit 日志会重置,因为较早的 read 结果已从上下文中消失。压缩本身每部分都是一次 LLM 请求,并计入使用量。
对话记录形态。 Chat Completions 服务器用自己的聊天模板渲染对话记录,严格的服务器(llama.cpp 的 Jinja 模板)只接受恰好一条 system 消息,且位于最前面——第二条,或跟随某轮之后的系统消息,会失败并报 System message must be at the beginning。因此,每当会话状态在对话中途到达(通知、已加载技能、中断说明),所有 system 消息都会按对话记录顺序合并到唯一的前导系统消息中(chat::fold_system_messages),并且每次请求前也会对对话记录运行同样的合并,作为最后一道防线。另外两种 API 风格从不关心这一点:它们本来就会把系统消息从数组中取出放入 system / instructions。
自动压缩。 两个触发条件,都使用同一个 compact() 路径:
- 上下文溢出时——当请求失败且错误匹配已知 OpenAI Chat Completions / OpenAI Responses / Anthropic 的“上下文长度超限”短语(大小写不敏感匹配,例如
maximum context length、context_length_exceeded、prompt is too long),会话会被压缩一次,同一轮会针对压缩后的上下文重试,且不计为失败尝试([auto-compact on context overflow])。 - 长时间代理运行后——当
compact_auto开启(默认)且某轮记录了 ≥ 3 次工具调用以及推理/思考条目时,该轮之后会压缩上下文([auto-compact] …)。用/compact auto off禁用。
工具
注册了九个工具——ls、read、write、edit、rg、exec、find、tw 和 notice——并为当前 API 风格生成 schema
(OpenAI 为 {"type":"function","function":{…}},Anthropic 为 {"name":…,"input_schema":…},Responses API 为 {"type":"function","name":…,"parameters":…})。
| 工具 | 策略 | 参数 | 行为 |
|---|---|---|---|
ls | Allow | path、page、page_size(≤500) | 一层,非递归;目录在前,然后按名称大小写不敏感排序;每项打印为 [dir ] NAME 或 [file] NAME N bytes,头部报告 total、页窗口和路径 |
read | Allow | path(必需)、offset(0 基行)、limit | 整文件模式上限 128M 字符并流式读取;范围模式在读取到最后请求行后立即停止;每个返回行前缀为右对齐 6 列行号({:>6}: content);记录返回行范围以供 read-before-edit 规则使用 |
write | Allow | path、content | 仅创建新文件——拒绝覆盖(会指向 edit),父目录缺失时拒绝(会指向 exec: mkdir -p);为编辑缓存播种 |
edit | Allow | path(必需)、mode、search、content、from、to | replace(唯一 SEARCH 块 → content)、insert(块之后,或行 from 之后)、append、delete(块或行范围)、query(只读定位)。非唯一 search 会中止并报告每个匹配及上下文;相同替换是 no-op;如果 search 以文件没有的换行结尾,会不带它重试(替换也会丢弃自己的尾随换行) |
rg | Allow | pattern(必需)、path、max_results(≤500)、ignore_case、context、file_type、count_only | 支持完整正则的递归内容搜索;跳过隐藏项和 .gitignore 路径;仅当符号链接解析到遍历根内部时才跟随;非正则模式走字面量快速路径;命中分组为 === file === + line: content;支持大小写不敏感搜索、上下文行(重叠上下文只打印一次)、文件扩展名过滤和仅计数模式;8M 行扫描预算;拒绝指数回溯形态((a+)+、`(a |
exec | Ask | cmd(必需)、timeout(默认 30s,最大 300s)、wd | 运行命令,带硬超时,会杀死子进程树(超时退出码 124);stdout 和 stderr 分开捕获;当命令失败(退出码 != 0)时,stderr 包含在输出中的 [stderr] 下;使用 wd 设置工作目录;结果始终以 exitcode=N 结尾 |
find | Allow | pattern(glob)、path、name、newer_than_hours、larger_than_bytes、max_results(≤500) | 按 glob 模式和/或元数据递归查找文件(仅当符号链接解析到 path 内部时才跟随;name 在 Windows 上大小写不敏感;未来 mtime 永远不算“最近”)。当只给 pattern 时,行为类似递归 glob(例如 **/*.test.ts)。每个匹配返回 path size bytes mtime (UTC) |
tw | Ask | operation、id、work-type、list、workflow、path、from、worker、provider、model、think、works、name、reuse_context、background、editable、edit_rules、limit、full、all | 管理 Teamwork 子代理任务和工作流——参见 Teamwork |
notice | Allow | operation(send/list/drain/wait/clear)、session、from、topic、body、timeout、from_filter、topic_filter | 跨会话消息总线——参见通知 |
glob 工具已合并进 find:find 接受 pattern glob 和/或 name/newer_than_hours/larger_than_bytes 元数据过滤器。
执行调度。 在一个助手轮次内,所有 Policy::Allow 只读调用(ls/read/rg/find)都会在共享工作池上并发分派;write 和 edit 被有意推迟到第二轮顺序执行,以便同一消息中的读取总是先完成(read-before-edit),并且同一文件的两个编辑永不竞争;exec 在交互确认后顺序运行,tw(Teamwork)也在该顺序轮次中运行,因为它会阻塞在子代理上。结果按原始 tool_call 顺序追加到对话记录。
Read-before-edit 规则。 路径会被规范化(weakly_canonical,Windows 上小写化),读取工具返回的行范围会被记录。edit 拒绝触碰任何未被先前读取范围覆盖的行,并且只要可见上下文变化(/clear、/new、/session、/compact),日志就会清除。
文件缓存。 edit 通过以规范化路径为键、经 (size, mtime) 验证的缓存读取,因此同一文件的连续编辑可以跳过磁盘,而外部写入者总是会被发现。
Teamwork
Teamwork 运行受监督的子代理。主代理创建一个任务,其中包含子任务列表——可选地排列为命名阶段的工作流——串行或并行运行它们,并取回合并报告。每个子代理都有独立的消息历史;子代理被限制在其租约覆盖的文件内,并且永远不能自己调用 tw,因此层级保持为深度 1 的树。子代理可以调用 notice,这是它们在运行中向监督会话报告进度的方式。
任务与会话共存:<root>/sessions/<cwd-key>/<session-id>/teamworks.json,每个子代理有一个 <session-id>/<job-id>/<worker>.json 对话记录。任务 id 是 UTC 墙钟时间戳——tw-YYYYMMDD-HHMMSS(-N 后缀消除同秒冲突)。存储保存在内存中,并通过异步写入器镜像到磁盘,因此列出/查询任务没有磁盘 I/O。
形态
// tw operation:"new"{ "work-type": "serial", // serial | parallel(阶段内调度顺序) "provider": "zai", // 可选任务级默认值,由每个子代理继承 "model": "glm-5.3-flash", "editable": ["story.md"], // 任务默认:子代理可修改的文件(需要 edit_rules) "edit_rules": "append only, never touch existing lines", "list": [ { "works": "audit the net layer", "provider": "claude", "model": "claude-sonnet-4", "think": "high", // off|low|med|high|max 或 0..4 "background": "prolegomena", // 将主对话作为背景上下文注入 "name": "net-audit" }, { "works": "then write regression tests", "reuse": "tw-1788623888-0/net-audit" } ] }// tw operation:"new" 带工作流——阶段按顺序运行,每个阶段有自己的// 代理、调度模式、可选阶段级 provider/model/think 默认值,// 以及阶段之后可选监督门控{ "workflow": { "name": "ship-it", "stages": [ { "name": "research", "mode": "parallel", "agents": [ { "works": "scan the docs" }, { "works": "scan the code" } ] }, { "name": "implement", "gate": true, "think": "low", "agents": [ { "works": "patch it", "reuse": "worker_0" } ] } // 裸名称 = 同任务复用 ] } }工作流也可以从文件加载:tw operation:"new" path:"teamwork.workflow.json"(相对于工作目录;文件可以包含完整任务配置或裸 {stages:[…]}),或从 REPL 使用 /teamworks workflow [FILE]。
| 字段(每个子代理) | 含义 |
|---|---|
works | 任务文本——除非给出 reuse,否则必需 |
background | none(默认)或 prolegomena(将主对话作为上下文注入) |
provider / model | 在特定提供方/模型上运行此子代理 |
think | 此子代理的思维链等级 |
reuse | tw-ID/worker_N 表示过去任务,或裸 worker_N 表示同一任务较早阶段的 worker——继承任务配置,并且默认继承对话记录 |
reuse_context | true(默认):携带较早子代理的完成报告——没有报告时携带其对话记录摘要;原始消息永不携带;false:仅配置 |
name | 显式子代理名称(会清理以用作文件名) |
editable | 主代理允许此子代理修改的已存在文件——参见文件租约 |
edit_rules | 审查这些修改所依据的约束;只要给出 editable 就必需 |
文件租约与监督审查
子代理始终可以创建新文件:文件被创建的那一刻它就属于该子代理,之后该子代理可以自由重写它。其他所有事情按路径决定:
| 目标文件 | 发生什么 |
|---|---|
不存在,并用 write 创建 | 允许——它成为子代理自己的文件 |
| 是子代理之前创建的文件 | 允许 |
| 被租给此任务的另一个子代理 | 拒绝:leased to <child>(所有权也是互斥锁) |
| 存在且无人租用 | 除非在子代理的 editable 列表中,否则拒绝 |
在子代理的 editable 列表中 | 被审查——见下文,每一次都如此 |
主代理授权列表上的文件仍归主代理所有:针对它的每次 write/edit 都会被暂缓,同时一次监督审查仅根据 edit_rules 判断这一个更改。审查是向主会话的提供方/模型发出的一个短请求,携带路径、工具参数、文件当前内容以及它将产生的结果——缺失或失败的审查会拒绝(fail-closed),绝不静默允许。
拒绝不是致命的:子代理会收到裁决和原因作为工具结果,并可以修正更改后重试(每个子代理最多 8 次审查)。租约从不比一次运行活得更久——任务完成、停止或删除时会被丢弃,被拒绝的子代理永远不会阻塞其兄弟,因为绘制文件只在审查进行期间声明它。
审查是并发的:没有子代理会排队等待另一个的审查,因为租约锁在发出审查请求之前就已释放。
给定 editable: ["story.md"], edit_rules: "append at most one paragraph, never alter existing lines",一个追加其章节记录的子代理会通过;一个试图重写文件的子代理会得到 [edit] /work/story.md refused: the supervisor rejected this change: rewrites existing lines,然后通常将其版本写入自己的新文件。
阶段字段:name(默认 stage_N)、mode(默认 serial | parallel)、gate(此阶段后暂停等待监督者)、provider/model/think(阶段默认值)、agents(必需,非空)。裸 reuse 必须命名较早阶段的 worker——这在任务创建时验证。
子代理的模型解析顺序:它自己的字段 → 被复用子代理的字段 → 阶段默认 → 任务默认 → 被复用任务的默认 → 激活的提供方/模型。不存在的 provider 是错误,绝不静默回退。每个子代理构建自己的 LLM 客户端,并在其所有轮次中复用它,因此并行子代理永不共享 curl 句柄。
工作流执行、门控与监督
- 阶段按顺序运行;一个阶段内的 worker 串行或并行运行(受池限制)。每个阶段之后都会持久化当前进度(
next_stage),因此门控暂停、停止或崩溃永远不会丢失已完成阶段的报告。 - 门控:带
gate: true的阶段会在下一阶段之前暂停任务(status=waiting)并发送通知。监督者审查阶段报告,并用tw run、/teamworks run ID/resume ID恢复,或用edit重新规划。 - 后台运行:
/teamworks run_bg ID(或带background:true的tw run)在专用运行线程上执行剩余工作流,同时 REPL 保持交互。监督者可以在/notices wait中空闲——零 token——并被每次阶段/worker 转换唤醒。 - 停止:
/teamworks stop ID请求协作式停止;任务在下一个 worker 轮次或阶段边界暂停(status=waiting,被中断的阶段在恢复时重新运行)。 - 启动恢复:进程在任务运行时退出会使其保持
running;下一次启动会将此类任务标记为failed,以便重新运行或移除。
tw 操作
| 操作 | 参数 | 结果 |
|---|---|---|
new | work-type + list,或 workflow(带 stages),或 path(配置文件),可选 provider/model/think/editable/edit_rules | 创建一个任务并打印它 |
run | id(+ 可选配置覆盖,background 用于后台运行) | 运行任务/恢复等待中的任务,返回合并报告 |
edit | id + 新 work-type/list/workflow | 替换未完成任务配置(进度重置) |
remove | id | 删除任务(代理不能删除已完成任务) |
reuse | from:"tw-ID/worker_N",可选 works/provider/model/think/name | 从过去子代理播种创建新任务(携带其完成报告或摘要,而非原始消息) |
list | all | 列出此会话的任务(或此工作目录中每个会话的任务) |
history | id、worker、limit、all | 列出过去的子任务:状态、提供方、模型、轮次、工具调用、创建时间 |
report | id + worker | 打印一个子代理的报告 |
reports | id、worker、limit、full、all | 打印多个子报告,最新在前 |
run 是唯一需要确认的 tw 操作(/autoallow 可以免除);只读查询自动批准。
报告
已完成任务会在 summary_reports 中记录每个子代理的 {status, summary, rounds, tool_calls, provider, model, reused, reviews_approved, reviews_denied}——两个计数器告诉你监督者通过和拒绝了被授权文件的多少次修改。当报告缺失时——例如子代理运行中途被中断——tw operation:"report" 会回退到子代理持久化的对话记录,并返回其最后一条助手文本,因此结果仍可恢复。
从 REPL
/teamworks 列出此会话的任务/teamworks all 列出此目录中每个会话的任务/teamworks new 创建一个空任务(接下来追加子代理)/teamworks workflow [FILE] 将工作流配置文件加载为新任务/teamworks id:ID works:... [bg:none|prolegomena] [provider:P] [model:M] [think:L] [reuse:JOB/worker] [name:NAME]/teamworks id:ID works:... writes:story.md rules:"append only" # 被授权文件 + 其审查约束/teamworks run ID 运行任务(恢复带门控任务)并打印报告/teamworks run_bg ID 后台运行;进度通过通知/teamworks resume ID 从保存阶段继续带门控/已停止任务/teamworks stop ID 请求协作式停止/teamworks status ID 显示一个任务及其阶段计划/teamworks max [N] 显示/设置子代理限制(1..100)/teamworks rm ID | /teamworks rm ID:worker_N 删除任务/一个子代理/teamworks history [JOB] [worker:W] [n:N] [all] 过去的子任务/teamworks reports [JOB] [worker:W] [n:N] [full] [all]/teamworks report JOB:worker 一个子代理的报告 调度与失败处理
- serial 任务在调用线程上一个接一个运行子代理;parallel 任务通过共享工作池分派,因此并发受
thread_pool_size限制(默认 16),而不是每个子代理生成一个线程。工作流阶段按阶段继承相同规则;后台运行在自己的专用运行线程上执行(绝不在池上),因此池托管的运行永远不会死锁,并在退出时 join。 - 抛出的子代理会被捕获并报告为
failed;任务在重启后永远不会卡在running(启动恢复将中断的运行标记为failed)。任务还为每个子代理携带ok / failed / stopped。 - 无轮次上限,子代理自我压缩。 子代理持续运行,直到它无工具调用地作答、被停止或失败。当它的对话记录超出模型上下文窗口——或超过 500 条消息——时,子代理自己的输出会被摘要成一条助手消息,回填在随任务携带的背景正后方(其最后一轮是 works 用户消息;背景本身永不压缩),原始对话记录在重写前归档到任务目录(
<worker>.compact-<UTC>.jsonl),并发送一条compact通知,告诉监督者如果该子代理不应继续,可以停止任务。摘要失败时,对话记录保持原样。 - 子代理运行以真实 token 数计入使用统计,键为
<job-id>-<child-name>。
通知
notice 系统是代理会话的消息总线。任何写入者——来自 REPL 的用户、此进程内的 Teamwork 子代理,或共享运行时根目录的第二个 cell 进程——都会向目标会话的 notices/pending/ 文件夹投放一个 JSON 文件(原子写入,读取时清理:仅 UTF-8,剥离控制/ANSI 字节,16 KB 正文上限,500 个待处理文件上限)。已送达通知归档到 notices/seen.jsonl;归档失败会被记录,但绝不会阻断送达。
寻址是句柄式的。 会话可以用精确 id、唯一 id 前缀、16 位十六进制工作区键、工作区路径(后两者都落到该工作区最新的会话)、current(默认)或 reply/@(发给调用者的最新一条通知的发送者,其会话仍存在——先查待处理通知,再查已送达轨迹)来指名。/notices sessions(或工具的 sessions 操作)列出并发会话的 id、工作区、待处理数量和空闲时间,因此发现会话永远不需要复制完整 id。会话一经激活,其文件夹就会立即物化——甚至在保存任何对话记录之前——因此一个正在运行的 CLI 或刚打开的 GUI 工作区在仅存在于内存中时就能被发现。只有 /notices send 的文本会作为回退保留:解析不到任何会话的首个 token 会被当作当前会话的消息正文。每次发送都会记录发送者的会话(from_session),除非目标是发送者自己;携带该信息的通知是可回复的——接收者用 session=reply 答复,注入的对话上下文会显示地址和回复提示。
等待是零 token 的。 /notices wait(以及 notice 工具的 wait 操作)在条件变量上阻塞,并周期性重新扫描文件夹(REPL 中 0.2 秒,工具约 0.7 秒)——空闲时不发出 LLM 请求。进程内发送会立即唤醒等待者;跨进程发送约 1 秒内到达。待处理通知也会在每次 LLM 轮次前自动排空,因此监督者永远不必轮询。
- 发送:
/notices send [SESSION] TEXT,或notice工具(from/topic字段给接收者路由提示;结果会确认回复地址)。Teamwork 子代理以此报告进度——notice是它们在沙箱之外唯一可用的工具,在所有任务模式中都是。 - 发现:
/notices sessions或工具的sessions操作——每个会话的 id、cwd、待处理数量和空闲时间,最近活跃在前(调用者会被标出),最多 50 行。 - 回复:一条携带
session=的已送达通知,无需知道任何 id 就能回复:工具里用session=reply,REPL 里用/notices send reply TEXT——回复本身携带回复者的地址,因此对话双向路由。 - 自动进度:每个 Teamwork 任务都会为任务开始/完成/失败、阶段开始/结束、门控暂停、停止,以及每个 worker 开始/结束(带状态和摘要片段)发送通知,全部
from=teamwork/<job-id>[/<worker>]。 - 背压:队列最多保留 500 条待处理通知。超出之后丢弃最旧的一条——但 Teamwork 进度类通知(
worker/stage/review)会被拒绝而不是挤掉,因此进度洪流永远不会驱逐正在等待监督者的gate/job通知。 - 消费:
/notices drain(或notice drain)将待处理通知作为上下文送入对话;wait送达它被唤醒所等待的内容;clear丢弃它们。由于在排空锁下删除文件,每个排空会话恰好送达一次。 - 清理:已送达/已丢弃的通知累积在
notices/seen.jsonl(超过 1 MB 后按尾部截断)。/notices seen列出轨迹中每条记录的 id 和状态,/notices seen rm ID删除单条,/notices seen clear把整个文件归档到saved/notice-<UTC time>.jsonl——与saved/msg-…压缩归档相同的命名和-N编号——然后清空它。归档先写入,因此归档失败时轨迹保持完整。
安全模型
六个独立层,从路径一直到屏幕上显示的字节。
1. 沙箱模式
用 /sandbox [mode]、--sandbox MODE 或 sandbox_mode 配置键设置。在任何命令运行之前,工具会按模式被允许或拒绝:
| 模式 | 允许的工具 | exec |
|---|---|---|
read-only | read、rg、find、ls(以及任何只读运行) | 完全阻止——在确认提示显示之前就被拒绝 |
edit-only | read/rg/find/ls 加上 write/edit | 允许,受第 2–3 层门控约束 |
full-access(默认) | 所有工具 | 允许,受第 2–3 层门控约束 |
任何模式下都不过滤网络出口——命令门控仅针对路径(参见第 3 层和实现说明)。第 2 层的敏感路径规则在所有地方适用。exec 是唯一由人工确认提示门控的工具(第 4 层);在 read-only 模式下,exec 在提示显示之前就被拒绝。
2. 路径门控——check_path / is_sensitive_path
- 参数中的任何
..都会被拒绝; .cell运行时目录下的一切都不可访问(保险库、.key、config.json、会话、日志)——除了.cell/skills/,技能系统合法读取它;- 磁盘上任何位置的知名凭据存储都会被阻止:
~/.ssh/id_{rsa,ed25519,ecdsa,dsa}、~/.aws/{credentials,config}、~/.netrc、~/.npmrc、~/.pypirc、~/.git-credentials、~/.git/config、~/.git/hooks、~/.config/gh/hosts.yml、~/.docker/config.json、~/.kube/config、~/.m2/settings.xml、~/.gradle/gradle.properties; - 路径首先用
weakly_canonical规范化(解析符号链接/junction),因此指向凭据文件的链接也会按其字面名称被阻止。
对于 write/edit,只检查 path/dirpath 字段——代码内容可以合法包含 >、| 或 ..。
3. 命令门控——check / check_exec
沙箱仅针对路径:命令字符串会检查路径遍历(..)和敏感路径(/.crypt、/.key、/config.json、/sessions/、/logs/,两种分隔符,大小写折叠)。它不尝试通过解析命令名来阻止网络出口、解码编码载荷,或禁止 shell 操作符——威胁模型假设一个不受信任的代理运行在受信任的主机内,其中 exec 命令由用户确认(或在 full-access 中 autoallow)。在 read-only 模式下,exec 直接拒绝。另请注意,exec 仍可运行解释器,例如 python3 -c "exec(base64…)" 或 cmd /c "echo …",因为门控只检查路径,不检查内联代码。(第 2 层更广泛的凭据存储列表由 check_path 对工具路径参数执行,而不是由 check_exec 执行。)
通过沙箱的高风险命令仍要求第二次确认:递归/强制删除(rm -rf、rm -r -f、del /s、rmdir /s……)、权限更改(chmod、chown、sudo、cacls、icacls、takeown)以及可能触发钩子或重写历史的 git 操作(commit、merge、rebase、cherry-pick、am、apply、checkout、switch、stash、clean、reset、restore)。
4. 人工确认
exec 是唯一的 Ask 工具:allow exec({…})? [y/N]——参数通过 display_safe 打印,因此注入的 JSON 无法擦除提示或伪造批准。当autoallow 模式启用时(/autoallow on,仅 full-access 沙箱),由 LLM 单独决定 exec 命令是否无需用户确认即可运行——沙箱检查(敏感路径)仍然适用。批准拒绝(来自用户或 autoallow)会结束当前代理运行;沙箱安全拒绝作为正常反馈返回给模型。
5. 输出加固
sanitize_output(应用于exec结果,以及从磁盘重新加载的带exec标记的结果):先截断到字节上限(默认 128 KiB;代理循环传 512 MiB),然后逐行根据约 50 个命令覆盖指纹加三个正则族(动词 + 填充词 +instructions|rules|system prompt|sandbox|safety、you are now …、no longer bound …)进行脱敏。匹配在每行的扁平化形式上运行:UTF-8 解码,全角/拉丁-1/西里尔/希腊同形字折叠为 ASCII,零宽和双向标记删除,标点折叠为空格,并将最多 6 个相邻行的窗口连接,以便拆分指纹仍能匹配。工具输出包装框架行永远不会被脱敏(它们带有重新加载时使用的tool="exec"标记)。wrap_tool_output:每个结果都包裹在显式的不受信任数据边界标签中,该标签携带工具名和路径/命令标记,属性会被清理,正文中该闭合标签的任何出现都会被转义,因此注入内容无法伪造嵌套的“已授权”工具块或突破包装器。truncate_output:非 exec 结果获得大小上限(512 MiB),但不进行注入扫描。display_safe(单行,剥离 ANSI)用于确认提示和技能元数据;console_safe(多行,剥离 ANSI)用于工具输出的青色控制台回显,另外上限为 16 KiB。
6. 凭据保险库
.cell/.crypt 是一个 JSON 信封(version: 2),包含随机 base64 盐,以及每个键的 {nonce, ct} 对。AEAD 密钥通过 Argon2id(中等 ops/内存限制)从以下来源派生:
- 如果设置了
CELL_VAULT_PASSPHRASE环境变量,则使用它,否则 .cell/.key——32 个随机字节,首次使用时自动生成(磁盘上为 base64)。
当 CPU 提供 AES-NI 时,加密使用 AES-256-GCM,并透明回退到 XChaCha20-Poly1305。秘密保存在 secure_string 中(sodium_malloc 缓冲区,销毁时清零,常量时间比较);派生密钥在保险库析构函数中擦除。明文永远不会出现在保险库文件中——自测会断言这一点。
会话与工作目录模型
- 每个会话 id 为
<cwd-key>-<unix-millis>-<8 random hex>;cwd key 是规范化绝对工作目录 SHA-256 的前 16 个十六进制字符(Windows 上小写化)。 - 每个会话是一个文件夹:
.cell/sessions/<cwd-key>/<id>/。它包含 JSONL 对话记录messages.jsonl(每行一个消息对象——标准代理日志形态)、Teamwork 存储teamworks.json及每个任务一个<job-id>/对话记录目录、压缩归档saved/msg-<UTC time>.jsonl,以及中断状态interrupt.json(轮次被缩短时写入,下一条用户消息或完成/reload时清空)。.cell/sessions/sessions.json是 hash → path 索引,使每个分组都能解析回真实目录;/sessions按它分组,并用>标记当前 cwd。 /session ID和启动恢复逻辑跟随会话的 cwd(通过索引解析,current_path+ 缓存失效),因此工具继续操作对话所属的项目;/session和/session rm ID会重置 read-before-edit 日志,因为记录的读取不再适用。切换到已有会话会再次打印其最后五条用户/助手文本消息,省略思考和工具调用内容。/new保留旧文件夹(可重新访问),/clear保留 id,/session rm删除整个文件夹和使用记录;孤立的 usage 记录在启动时和/usages时修剪。- 压缩归档:每次
/compact(手动或自动)都会先将完整对话记录写入会话文件夹中的saved/msg-<UTC time>.jsonl(同秒归档获得-N后缀),然后才重写上下文;如果归档或摘要无法产生,上下文保持不变。/saved list、/saved show NAME和/saved rm NAME访问当前会话的归档。 - 会话、配置和索引写入通过
cell::async_io::file_writer(按路径合并,一个后台线程)。每次写入都是原子替换:内容写入兄弟<name>.tmp,刷新到存储设备,然后重命名覆盖目标,因此崩溃永远不会留下被下一次启动静默读为空的截断文件。读取这些文件的命令(/save、/sessions、/session、/compact、退出)会先调用flush()作为持久性屏障;信号处理器和 RAII 退出守卫也这样做。
配置文件
.cell/config.json(用 dump(2) 写入,因此可手动编辑):
{ "providers": [ { "name": "openai", "style": "openai", "api_style": "openai-chat", "base": "https://api.openai.com/v1", "key": "provider:openai", "proxy": "http://user:pass@host:8080" }, { "name": "claude", "style": "anthropic", "api_style": "anthropic", "base": "https://api.anthropic.com" } ], "current_provider": "openai", "current_model": "gpt-4o", "think_level": 0, "tools": true, "sandbox_mode": "full-access", "autoallow": false, "compact_auto": true, "compact_provider": "", "compact_model": "", "system": "You are a helpful assistant.…", "session": "6333a2b6f7084f1a-1787819024", "log_max_lines": 1000, "thread_pool_size": 16, "active_sessions": { "6333a2b6f7084f1a": "6333a2b6f7084f1a-1787819024" }}| 字段 | 默认值 | 含义 |
|---|---|---|
providers[] | (空) | 每个端点一个条目:name(唯一 id)、style(openai | anthropic)、api_style(openai-chat | openai-responses | anthropic)、base、key(保险库 id,不是秘密)、proxy。模型不存储在这里。 |
current_provider | 第一个条目 | 为空时激活的提供方 |
current_model | — | 激活的模型名;每当激活提供方实际变化时自动清除(参见 /provide) |
think_level | 0 | 思维链等级:0=off、1=low(1024)、2=med(2048)、3=high(4096)、4=max(8192)。旧式 think: true 映射为等级 2。 |
tools | true | 启用工具调用 |
sandbox_mode | full-access | 参见沙箱模式 |
autoallow | false | LLM 决定 exec 命令是否运行(仅在 full-access 中有效) |
compact_auto | true | 在长时间工具密集型轮次后自动压缩上下文(/compact auto) |
compact_provider / compact_model | (空) | 用于压缩摘要的提供方/模型;空表示继承会话模型(/compact model provider:model,用 inherit 重置) |
teamwork_max_children | 5(限制 1–100) | 每个 Teamwork 任务的子代理最大数量(/teamworks max) |
system | 短助手提示 | 系统提示 |
log_max_lines | 1000(最小 10) | 每次启动时将 logs/cell.log 修剪到其尾部 |
thread_pool_size | 16(限制 1–16) | 最大并发只读工具 worker 数;池惰性生成,空闲时零 worker |
active_sessions | {} | cwd key → 最后活跃会话 id |
旧式迁移。 扁平的 {"provider","base","model","key","proxy"} 对象,或 {"models":[…],"current_model":<index>} 数组,会在加载时重写为提供方列表(重复端点合并,名称去重为 style、style2……)。
运行时数据布局
.cell 锚定到包含 cell 可执行文件的目录(GetModuleFileNameW / /proc/self/exe),而不是当前工作目录——因此无论从哪里启动,bin/cell.exe 都使用 bin/.cell/。
.cell/├── config.json # 上述设置├── .crypt # 加密保险库(version 2:salt + 每键 {nonce, ct})├── .key # 32 字节主秘密(base64)——除非设置了 CELL_VAULT_PASSPHRASE├── usages.json # {"sessions": {id: {...}}, "models": {"provider:model": {...}}}├── skills/ # 带 YAML front matter 的 *.md,递归扫描(深度 <= 6)│ └── suite/core/SKILL.md├── logs/│ └── cell.log # [UTC timestamp] LEVEL [cat ] message,启动时修剪└── sessions/ ├── sessions.json # cwd hash -> cwd path 索引 └── 6333a2b6f7084f1a/ # 每个工作目录一个目录 └── 6333a2b6f7084f1a-1787819024-ab12cd34/ # 每个会话一个文件夹 ├── messages.jsonl # 对话记录:每行一个消息对象 ├── interrupt.json # {"reason": "..."},当被中断轮次仍可 /reload 时 ├── teamworks.json # Teamwork 存储(此会话的任务) ├── tw-20260910-124236/ # 每个任务一个对话记录目录 │ └── worker_0.json ├── notices/ │ ├── pending/ # 未送达通知文件(nt-*.json) │ └── seen.jsonl # 已送达/丢弃通知的审计轨迹 └── saved/ └── msg-20260910-124500.jsonl # /compact 归档的完整对话记录 使用记录累计 requests、messages、input_chars、output_chars、input_tokens、output_tokens、total_tokens。[llm] 日志行中显示的提示缓存命中率由 usage.prompt_tokens_details.cached_tokens / prompt_tokens(OpenAI)或 cache_read_input_tokens / (cache_read_input_tokens + input_tokens)(Anthropic)计算。
架构
一切都在 cell.cpp 中,按命名空间拆分,依赖方向严格:只有 cell::plat 了解 OS API,只有 cell::net 了解 curl,只有 cell::encrypt 了解 libsodium。
cell::async_io 合并后台文件写入器(submit / flush)cell::plat OS 垫片:spawn_cmd(超时 + 进程树杀死)、is_tty、init_console、 read_line(行编辑器:光标键、历史、UTF-8 感知)、decode_key、 peek_key(Esc 取消)、executable_dir、reparse_resolved_path (通过 OS 解析 junction/符号链接)、restore_consolecell::text 零拷贝行生成器、trim、BOM 剥离、display_safe / console_safecell::box 沙箱 + 每个工具实现:check_path / check / check_exec、 is_high_risk、sanitize_output、wrap_tool_output、truncate_output、 rg / find / list_dir / read / write / edit、 gitignore 匹配器、walk_entries(惰性 DFS)、read-before-edit 日志、文件缓存cell::net curl 传输:perform / CURL_post / CURL_stream_post / CURL_get、 RAII header_list、OpenAI 风格错误体提取cell::sys print/println/eprintln/pprintln、结构化日志器 + 轮转、异常 + source_location、scoped_exit、动态伸缩 thread_pool、信号处理器cell::config 提供方注册表、设置、加载/保存 + 旧式迁移cell::encrypt base64、secure_string、Argon2id + AES-256-GCM 保险库cell::tools Policy(Deny/Ask/Allow)、工具基类、callable_tool(批准 + 门控)cell::llm SSE 解析器(generator + 增量 feed)、OpenAI / OpenAIResponses / Anthropic 客户端cell::chat 会话(按 cwd 持久化)和历史(内存会话映射)cell::skills front-matter 解析器、递归扫描器、元数据提示cell::stats .cell/usages.json 中的使用计数器cell::teamwork Teamwork 任务存储(内存缓存)、子代理调度器(串行 / 池化并行)、 复用 + 历史/报告查询 代理循环
user message ──▶ LLM (stream) ──▶ tool_calls? ──▶ pass 1: read-only tools, concurrent │ pass 2: write/edit (sequential), exec (confirm) │ │ │ results sanitized + wrapped ──▶ appended to transcript ──▶ next round └──▶ no tool calls ──▶ final answer ──▶ session persisted 没有固定轮次上限:循环持续到模型在无工具调用的情况下回答、调用被阻止/拒绝、用户取消或发生错误。每一轮记录 round=、上下文大小、token 数、缓存命中率、总时间和首 token 时间。每次用户轮次后,会话和配置都会持久化。
失败与重试。 失败的 LLM 请求每轮最多重试 5 次,尝试之间延迟 5 秒([retrying in 5s... attempt N/5]);耗尽后以 [llm error after 5 attempts] 结束该轮,并记录一个中断轮次(参见中断)。如果失败是上下文溢出错误,该轮会压缩一次并立即重试,而不是消耗一次重试尝试(参见自动压缩)。
流式 UI 与键盘控制
>提示符,流式答案前缀reply>;推理流以暗色打印,答案普通,工具结果及其回显青色。提示符带行编辑(
cell::plat::read_line):终端仅在输入一行时处于 raw 模式,下列每个按键都是 emacs/readline 绑定,且退格/删除/光标移动绝不会拆分多字节字符。输入历史按进程保存,从当前会话的用户轮次播种(因此恢复的会话可以回想更早的提示),上限 200 条。按键 动作 ←→/Ctrl+BCtrl+F移动一个字符 Ctrl+←Ctrl+→/Alt+BAlt+F移动一个词 HomeEnd/Ctrl+ACtrl+E行首 / 行尾 ↑↓/Ctrl+PCtrl+N历史前一条 / 后一条(结束时恢复草稿) Alt+<Alt+>最旧 / 最新历史条目 BackspaceDeleteCtrl+D删除光标前 / 光标处字符(空行上按 Ctrl+D则退出)Ctrl+WAlt+Backspace删除光标前的词 Alt+D删除到下一个词的末尾 Ctrl+UCtrl+K删除到行首 / 行尾 Ctrl+Y粘贴(yank)上次删除的文本 Ctrl+T交换光标两侧的两个字符 Ctrl+L清屏并重绘该行 Enter提交 Ctrl+C保存并退出(见下文) Esc取消正在运行的流(不是编辑器按键) 所有这些都从标准编码解码——CSI(
\x1b[1;5D)、SS3/应用光标键(\x1bOD)、meta 对(\x1bb)以及 Windows 扫描码——因此相同的绑定在 xterm、tmux、Linux 控制台和 Windows 控制台中都能工作。等待第一个 token 时,每 500 ms 刷新一个
⏳ Ns旋转指示器;之后在行边界刷新~N tok计数器。流式期间按 Esc 取消:该轮被记录为中断——部分回复(思维链和答案文本到目前位置)保留在对话记录中,并打印
[cancelled]。参见中断。(由cell::plat::peek_key实现,它会临时将 stdin 置于 raw 模式;只有单独的 Esc 才会取消,因此在流式期间按方向键不再中止该轮。)Ctrl+C(以及 Windows 上的
SIGTERM/SIGHUP/SIGABRT)运行持久化钩子——修复工具对话记录、保存会话、保存配置、刷新异步写入——恢复终端并以信号编号退出。在提示符处,编辑器自行处理 Ctrl+C,并经与/exit相同的路径离开(会话/配置守卫)。终止信号取自专用sigwait线程,因此持久化代码永远不会在异步信号处理器内运行;随后进程用_Exit离开,故意跳过静态析构,因为在 REPL、异步写入器和 worker 线程仍然存活时拆除它们会造成堆竞争(过去它会以 "out of memory (operator new failed)" 死掉,而不是正常退出)。如果持久化路径出现停滞,第二次 Ctrl+C 或 10 秒看门狗会强制退出。SIGSEGV/SIGFPE/SIGILL故意不尝试序列化状态:它们恢复默认动作并终止。非交互模式(stdin 不是 tty)将整个 stdin 作为一条消息读取,要求已配置提供方,运行代理循环一次并退出。
中断与 /reload
一个轮次可以在模型完成之前以两种方式结束:流式期间按 Esc,或 LLM 请求在最后一次重试时失败。无论哪种方式,该轮都不会被丢弃:
- 模型已经产生的任何内容都会原样保留,并截断到完整到达的最后一个 token(
sse_feed只应用完整流事件,因此累积文本总是结束在 token 边界——不会补全或填充)。 - 仍在发出的工具调用会被取消:其参数通常是截断的 JSON,从未被批准或执行,也不会为其发明结果。部分助手消息存储时不含其
tool_calls,因此对话记录对每种提供方都保持有效。 - 会追加一条系统消息记录原因(Esc,或错误及尝试次数)——它是对话的一部分,而不仅仅是控制台输出,因此模型能看到上一轮为何停止。
- 会话将其记为中断状态(会话文件夹中的
interrupt.json,因此重启后仍存在)。它会被下一条用户消息(包括/ins)或达到自然结束的/reload清除;/clear也会丢弃它。
/reload 正好从轮次停止处继续:它按现状发送对话记录(部分回复 + 中断通知,无新用户消息),让模型接续。如果该尝试也被中断,状态只会用新原因刷新。/reload 若没有可恢复内容,会报告错误而不是发送任何内容。
日志、信号与退出码
日志行是 [YYYY-MM-DD HH:MM:SS] LEVEL [cat ] message(UTC)。类别包括 boot、core、llm、tool、sess、ctx、cmd、user、probe、provider、model、think、sandbox、skill、stats、vault。只有 llm 和 tool 活动镜像到控制台(ERROR 总是;DEBUG 仅在 --verbose 时),因此终端保持可读,而文件保留一切。写入以 16 KiB 块缓冲,ERROR 立即刷新。
| 情况 | 退出码 |
|---|---|
正常退出,或 --selftest 通过 | 0 |
| 未知选项 / 缺少选项值 | 1 |
| 非交互运行且未配置提供方 | 1 |
致命 cell::sys::exception 或未处理 std::exception(记录后 RAII 守卫持久化状态) | 1 |
| 被捕获信号终止 | 信号编号 |
set_terminate 处理器记录任何逃逸异常的类型,set_new_handler 在 abort 前记录内存不足。
自测
cell --selftest在一次性 .cell-selftest/ 根目录上运行(之后删除),覆盖:三种模式下的高风险和沙箱决策,路径/遍历/符号链接/凭据阻止(包括 is_sensitive_path 规则集本身、POSIX 符号链接——含悬空链接——以及 Windows 上指向运行时根的 mklink /J junction,因为这两者都无法通过此工具链的 std::filesystem 创建),注入清理器(大小写、\r、零宽、全角、西里尔/希腊、重音、跨行拆分、标点连接、释义、超大输出),wrap_tool_output 防伪造,读/写/write_new/edit(全部五种模式、歧义、部分读取覆盖、no-op),rg/find/ls 语义和守卫,exec 超时和退出码,带引号数字参数,工具注册表及其策略,16 路并发只读工具调用,Teamwork 任务规范化(每子代理 provider/model/think、复用引用、名称清理、限制)、存储往返、子代理追加/移除、历史和报告查询,增量 SSE 解析和缓冲区压缩,惰性目录遍历器,线程池任务记账,日志器轮转,保险库往返和持久化,配置保存/加载/迁移/错误处理,会话分组,/new 语义,cwd 索引,exec 结果加载时重新清理,中断轮次(部分回复截断、工具调用取消、原因通知,以及持久化/清除的中断状态),行编辑器(每个绑定按键的 CSI/SS3/meta/键码解码、词边界、UTF-8 光标步进与显示宽度、历史去重/上限,以及 line_buffer::apply 的移动/删除/yank/转置语义),技能发现(包括目录式)和使用统计。打印 selftest OK / selftest FAILED。
实现说明
一些代码与其自身帮助文本/注释不一致,或行为故意比看起来更简单的地方:
- 沙箱仅针对路径:
box::check_exec/box::check不解析命令名、解码编码载荷,也不按二进制名阻止网络出口——它们只拒绝路径遍历和敏感路径。网络出口被“拒绝”的意义在于没有允许命令的白名单;exec改为由沙箱模式、人工确认(或 autoallow)以及高风险二次确认列表门控。 - 切换提供方会重置模型名:
--provider NAME(已有提供方)、/provide NAME和/provide add都通过config::select_provider,当选择实际变化时清除current_model。重新选择已激活提供方会保留其模型,空的current_provider在该目的下算作“第一个提供方激活”。同一命令行请求的模型(--model)在切换后应用。 - 启动恢复当前 cwd 中最后使用的会话,记录在
active_sessions;如果尚无记录,则选择该 cwd 最近写入的会话文件。最后五条用户/助手文本消息会再次打印,排除思考和工具调用内容。--session/session字段仍为兼容性持久化,但启动使用 active-session 记录。 exec是唯一其输出会扫描注入指纹的工具;其他工具在调用时被认为已沙箱化,只获得大小上限。工具描述本身也可能与实现漂移(例如exec的拒绝网络声明、read的带行号),这对于嵌入代码旁边的字符串来说是正常的。- 工具输出包装标签在源码中用转义的正斜杠写入;转义在运行时折叠为普通斜杠,因此到达对话记录的标记是一个携带
tool和path属性的开始标签,而重新加载时的清理器根据该属性的exec值进行键控。
许可证
MIT——参见 LICENSE。
评论