返回 DeepSeek-Reasonix
SPEC.zh-CN.md
根目录 / docs / SPEC.zh-CN.md
1 # Reasonix 工程规格
2
3 <a href="./SPEC.md">English</a>
4
5 > Reasonix 是一个 coding agent:由极薄的 harness 驱动多个模型,所有能力都由配置和插件提供。本文是工程契约,代码应遵循它;需要改变行为时,应先更新契约,再修改代码。
6
7 英文原文是规范性版本;本文按相同章节提供中文说明,代码标识符、配置键和协议名保持原样。
8
9 ## 1. 设计原则
10
11 1. **配置与插件驱动。** 核心只依赖接口;具体模型和工具通过 registry 按名称解析、在配置中声明,或由插件注入,不硬编码 `switch model`。
12 2. **单一静态二进制。** 使用 `CGO_ENABLED=0`,一条命令完成跨平台编译,CLI 开箱即用。
13 3. **精简依赖。** 默认使用标准库。第三方依赖必须是纯 Go、足够轻量,且不能破坏单二进制、跨平台和分发体验;TOML parser 是当前唯一接受的基础依赖。
14 4. **两级扩展。** 编译期 built-in 通过 `init()` 自注册;运行时外部插件以 stdio JSON-RPC 子进程或 MCP 兼容传输接入。
15 5. **接口优先、registry 驱动。** `Provider` 与 `Tool` 都是接口。
16 6. **持续演进,不过度设计。**
17
18 所有代码、注释、面向用户的字符串、工具描述、system prompt 和英文规范以英语为主;README 同时维护英文版 `README.md` 与中文版 `README.zh-CN.md`。
19
20 ## 2. 目录与依赖方向
21
22 ```text
23 reasonix/
24 ├── go.mod / go.sum
25 ├── Makefile
26 ├── README.md / README.zh-CN.md
27 ├── reasonix.example.toml
28 ├── docs/SPEC.md / docs/SPEC.zh-CN.md
29 ├── cmd/reasonix/main.go
30 ├── cmd/reasonix-plugin-example/
31 └── internal/
32 ├── cli/
33 ├── config/
34 ├── provider/
35 │ └── openai/
36 ├── tool/
37 │ └── builtin/
38 ├── permission/
39 ├── command/
40 ├── plugin/
41 ├── remote/
42 │ ├── forward/
43 │ ├── sftpfs/
44 │ └── bootstrap/
45 └── agent/
46 ```
47
48 核心依赖方向保持无环:
49
50 ```text
51 cli → {agent, plugin, config} → {tool, provider}
52 ```
53
54 `provider/openai`、`tool/builtin` 等 built-in 子包导入父包完成自注册,父包不反向导入子包。Remote-SSH 采用 `cli → remote/bootstrap → remote` 的分层,`remote` 及其子包不依赖 `cli`、`agent` 或 `serve`;host key 和 secret prompt 等交互都通过 callback 暴露,供桌面端复用。
55
56 ## 3. 核心抽象
57
58 ### 3.1 Provider 与 registry(`internal/provider`)
59
60 ```go
61 type Provider interface {
62 Name() string
63 Stream(ctx context.Context, req Request) (<-chan Chunk, error)
64 }
65
66 type Factory func(cfg Config) (Provider, error)
67
68 func Register(kind string, f Factory)
69 func New(kind string, cfg Config) (Provider, error)
70 ```
71
72 - `openai` kind 实现 OpenAI-compatible `/chat/completions`。
73 - OpenAI-compatible vendor 只是 `kind = "openai"` 的不同配置实例,通过 `base_url`、`model`、`api_key_env` 区分;新增兼容模型通常只需改配置。
74 - 一个 provider 表示一个 vendor endpoint,可通过 `models` 暴露多个模型,并以 `default` 指定默认项。`default_model`、`--model` 和桌面端模型选择器都经 `Config.ResolveModel` 解析,可接受 provider 名、裸模型名或 `provider/model`。
75 - `context_window` 是 provider 级默认值;`model_overrides.<model>.context_window` 可覆盖单个模型。
76 - `max_output_tokens` 是独立的总输出预算,不由客户端 reasoning 字节上限换算。0 表示使用 provider 安全默认值,正数表示显式上限,负数表示在协议允许时省略;混合网关可用 `model_overrides.<model>.max_output_tokens` 覆盖单个模型。Anthropic 因协议要求仍会提供 `max_tokens` 默认值。
77 - streaming tool-call delta 在 provider 内按 index 聚合,只向上层发出完整 `ToolCall`。
78
79 ### 3.2 Tool 与 registry(`internal/tool`)
80
81 ```go
82 type Tool interface {
83 Name() string
84 Description() string
85 Schema() json.RawMessage
86 Execute(ctx context.Context, args json.RawMessage) (string, error)
87 }
88 ```
89
90 - built-in tool 通过 `tool.RegisterBuiltin` 注册到进程级集合。
91 - 每次运行创建独立 `*Registry`,由启用的 built-in 与插件工具组成;agent 只看到该 registry。
92 - tool schema 在插入 registry 时 canonicalize;内置契约见[工具合约](./TOOL_CONTRACT.zh-CN.md),测试会校验文档与 canonical schema 不漂移。
93 - `Execute` 自行解析原始 JSON 参数。错误作为结果返回给模型,让模型有机会自我修正,而不是直接终止进程。
94
95 ### 3.3 插件与 MCP(`internal/plugin`)
96
97 外部插件是配置中声明的 MCP server。协议统一为 JSON-RPC 2.0,传输由 `transport` 接口抽象:
98
99 - `stdio`:本地持久子进程,每行一条 JSON 消息。
100 - `http` / `streamable-http`:向远程 `url` POST,支持 `application/json` 和 SSE 响应,并复用 `Mcp-Session-Id`。
101 - `sse`:兼容旧版 2024-11-05 HTTP+SSE;持久 GET 接收 server 公布的相对 POST endpoint、JSON-RPC 响应与 server 消息。为避免静态 header 泄漏,会拒绝跨域 endpoint。
102
103 `${VAR}` 与 `${VAR:-default}` 可用于 `command`、`args`、`env`、`url` 和 `headers`,使 secret 留在环境中。生命周期为 `initialize` → `notifications/initialized` → `tools/list`,调用使用 `tools/call`。
104
105 存在工作区根目录时,初始化会声明 `roots` 能力,并用文件 URI 响应 `roots/list`。`tools/call` 会附带逐调用 `_meta.progressToken`;匹配的 `notifications/progress` 会进入现有工具进度事件链路。
106
107 远程工具适配为 `Tool`,命名为 `mcp__<server>__<tool>`。`annotations.readOnlyHint` 映射为 `Tool.ReadOnly()`,默认 false;只有显式声明为只读的工具才进入并行读取与默认只读权限路径。MCP prompt 暴露为 slash command,resource 可通过 `@<server>:<uri>` 引用。
108
109 ### 3.4 Agent loop(`internal/agent`)
110
111 `Session` 保存 `[]Message`。`Run(ctx, input)` 的主循环为:
112
113 1. 构建包含历史消息和 tool schema 的 `Request`。
114 2. 调用 `provider.Stream` 并实时输出 text delta。
115 3. 收集完整 tool call;若没有 tool call,则本回合结束。
116 4. 执行 built-in 或 plugin tool,把结果加入会话后继续,直到完成或达到安全边界。
117
118 `ctx` 贯穿调用链,Ctrl-C 可以取消进行中的请求。`Agent` 与 `Coordinator` 都实现 `Runner`,因此 CLI 不需要区分单模型或双模型执行。
119
120 ### 3.5 双模型协作(`Coordinator`)
121
122 当 `agent.planner_model` 与 executor 不同时,planner 与 executor 使用独立 session:
123
124 - 宿主使用原始用户文本和可信回合元数据做确定性路由,不调用 classifier 模型,也不从
125 controller 注入的 prompt block 猜测宿主状态;路由结果为 executor-only、Light、Full、
126 plan-for-approval 或显式 plan-only,并用不含用户原文的 route/depth/reason 写入阶段详情;
127 - 显式 Plan Mode、synthetic turn、上下文短回复、明确单点小改和边界清楚的纯只读动作
128 不再调用第二个 Planner;跨面、结构化、模糊或高风险工作使用 Full;活跃 Goal 与
129 Delivery 中的非原子修改工作同样升级为 Full,纯只读动作仍直达 Executor;
130 - Light 使用较小的单轮调研预算,输出紧凑目标、1–4 个有序步骤、候选触点和主要验证;
131 Full 使用较大的有界预算,区分已验证与候选触点,并补充风险、验收标准、命令级验证及
132 必要回滚;深度合约保持在同一个稳定 system prompt 中,单轮只追加很小的
133 `<planner-turn>`;若 Planner 在有界调研和最终总结轮后仍未收敛,普通
134 plan-and-execute 用原始任务降级到 Executor,plan-only 与 plan-for-approval 仍保持
135 fail-closed;不完整的 Planner 回合会被回滚,不暴露成无法继续的手动续跑;
136 - 普通“先规划”在计划完成后直接交接 Executor;plan-for-approval 只用于明确要求等待
137 确认的请求,由宿主强制审批边界,批准后交接 Executor;headless 场景会保存计划供后续
138 回合继续;明确 plan-only 会保存计划并结束当前回合;上述两种执行边界下 Planner 失败
139 都不能降级执行;这些边界可位于任务子句之后,引号内的示例不改变路由;
140 - executor 在另一 session 中验证候选假设,并使用完整工具执行计划;
141 - 两条会话互不混合,prompt prefix 都只追加增长,避免切换模型破坏 prefix cache。
142
143 ### 3.6 上下文管理
144
145 Reasonix 通过低频 compaction 保持 cache-first:
146
147 - 低于 `agent.tool_result_snip_ratio` 时不改写历史;
148 - 达到 snip ratio 后,归档并缩短较旧 tool result;
149 - 达到 `agent.compact_ratio` 后,先把旧 tool result 修剪为占位符,仍超阈值才调用摘要;
150 - 达到 `agent.compact_force_ratio` 后,可执行强制折叠;
151 - `context_window = 0` 会关闭该实例的 compaction。
152
153 用户可用 `reasonix config compact-ratio [--local] [VALUE]` 查看或修改 65–85% 的自动
154 压缩阈值,内置默认值为 80%。项目级设置优先于桌面端与新 CLI 会话共用的用户全局配置。
155
156 tool result 的 snip/prune 不删除消息,确保 assistant `tool_calls` 与 tool result 配对。摘要只折叠 assistant/tool 工作;正常大小的用户回合和既有 digest 原样保留。被移除的原文归档到 `reasonix/archive/<timestamp>.jsonl`。
157
158 `history` tool 支持对 session 与归档进行 BM25 搜索;`memory` tool 用于检索自动记忆,
159 `remember` 与 `forget` 负责写入和归档。每个真实用户回合前,Reasonix 会用原始用户消息执行
160 有预算的 BM25 自动召回,把命中作为低权限 user-turn 后缀追加;泛化请求会被抑制,等价事实优先
161 项目级版本,stale 内容会降权。这不会修改稳定 system prompt 或工具 schema。
162
163 拥有当前项目 store 的父 controller(包括顶层 headless)只有在新事实有界、非敏感、纯创建,且明确属于 project/reference 时才能
164 免确认保存。全局事实、偏好、feedback、更新、重复项、敏感/超长内容和所有 `forget` 仍需
165 新鲜人工确认,Auto、YOLO、Guardian、permission hook 或子智能体都不能代为批准;子智能体和
166 不拥有该作用域 controller 的 headless surface 会 fail closed。事实带有不变 ID、单调 revision、时间、type 与 scope;更新先快照旧版本,
167 restore 与 archive recovery 会创建更高 revision,并拒绝路径逃逸、符号链接、冲突和覆盖。
168 详细约定见 [`SESSION_MEMORY_RETRIEVAL.zh-CN.md`](SESSION_MEMORY_RETRIEVAL.zh-CN.md)。
169
170 ### 3.7 权限
171
172 权限层按单次 tool call 返回 `Allow`、`Ask` 或 `Deny`:
173
174 ```go
175 type Decision int
176 const (Allow Decision = iota; Ask; Deny)
177
178 type Policy struct { Mode Decision; Allow, Ask, Deny []Rule }
179 func (p Policy) Decide(toolName string, readOnly bool, args json.RawMessage) Decision
180 ```
181
182 - rule 可以是 `Tool` 或 `Tool(specifier)`,例如 `Bash(go test:*)`、`Edit(docs/**)`;`Bash=<literal>` 是整条 Bash 命令的精确授权格式,其中 glob 与 Shell 元字符都按普通字符匹配。
183 - 优先级为 `deny > ask > allow > fallback`;只读工具 fallback 为 Allow,写工具 fallback 使用 `Mode`。
184 - 交互模式中的 Ask 由用户选择单次允许、session scope 允许、持久允许或拒绝;显式 Deny 在所有模式下都不可绕过。
185 - 非交互 `reasonix run` 与无头子智能体没有审批界面:默认 Ask/manual 对普通 writer fallback 与显式 ask 规则失败关闭;Auto 只放行普通 writer fallback,显式 ask 仍拒绝;YOLO 可越过普通 Ask,但不能越过 deny、Sandbox 或强制新鲜人工审批。无人值守自动化需要普通 writer 自主执行时,使用现有的 `--auto` / `-y`。
186 - 动态 Bash 分两级:参数/算术展开、赋值、不含嵌套执行的 heredoc、普通文件重定向与 Shell glob 不能复用裸 `Bash`、前缀或 glob Allow,保存时只生成 `Bash=<literal>`,但仍遵循普通 fallback,因此 Auto 与获批计划窗口可无提示执行。命令/进程替换、动态命令名、无法解析结构,以及 `eval`、`source`、Shell `-c`、PowerShell/cmd 命令字符串、运行时内联代码参数属于嵌套/间接执行;默认情况下交互 Ask/Auto 必须人工批准,Guardian 与 hook allow 不能代替,无头 Ask/Auto/DontAsk 直接拒绝,只有完全相同的 literal 或 YOLO 可以绕过。高级用户可设置 `[permissions] allow_dynamic_bash = true`,让 Allow fallback(包括 Auto)覆盖这类动态命令;显式 `ask` 与 `deny` 规则仍然优先。
187 - 安装 MCP server 即授权其全部工具,不再有 server、raw tool、writer 或 destructive 的第二套审批策略;项目 `reasonix.toml` 与 `.mcp.json` 声明同样默认可信,不需要额外启动确认,显式全局 `deny` 仍然优先。全局安装写入用户 `config.toml`,项目声明保留在原项目文件;同名时项目覆盖全局,项目内部 `reasonix.toml` 高于 `.mcp.json`。编辑写回当前生效来源,删除高优先级声明后露出下一层。`readOnlyHint` 与 `destructiveHint` 仅用于调度、Plan/严格只读边界及缓存到实时安全分类复核,不会新增逐调用审批。严格只读子智能体 registry 仍仅暴露已授权且 `readOnlyHint: true`、无 `destructiveHint` 的 MCP;双模型 Planner 通过固定 `use_capability` 代理(从不暴露直接 `mcp__*` schema)调用已授权、非 destructive 的 MCP,不再要求 `readOnlyHint`,destructive 工具留给 Executor。Balanced 双模型的 Executor 使用独立 frontend 复用同一稳定代理,因此 Planner 发现的 capability ID 可在 handoff 后直接执行,同时保持两侧 ledger/audit 隔离。分发前代理会再次复核当前 controller 的 enable、授权和完整运行时连接身份;共享 Host 中仅 server 同名不构成复用权限。
188 - Plan 是协作流程,不等于全工具只读。普通 built-in 与 Bash 仍走 Ask/Auto/YOLO 和 Sandbox;独立双模型 Planner 允许已授权、非 destructive 的 MCP(即使没有 `readOnlyHint`),但在规划阶段持续阻止 destructive 与未授权目标;没有独立 Planner 的单模型 Plan 仍阻止 MCP writer/destructive。
189 - Plan 只能由用户显式选择进入,与当前工具审批姿态相互独立;普通聊天不会自动切换到 Plan。Auto/YOLO 不会回答 `ask`,也不会替用户批准 `exit_plan_mode`,获批计划的短期自动执行窗口也不会自动批准后续计划或嵌套/间接 Bash。
190 - 桌面端协作模式分为 `normal`、`plan` 和 `goal`。Goal 会持续推进目标,直到完成、同一阻塞状态重复三次、用户停止或达到安全续跑边界。只有用户在输入框中选择 Goal 或运行 `/goal` 显式启动后,长周期研究、调试、优化或实现目标才可启用 AutoResearch;普通聊天不会隐式切换协作模式,也不会创建持久化 AutoResearch 状态。动态状态保存在 `.reasonix/autoresearch/.../`。
191
192 ### 3.8 Slash command
193
194 Slash command 分为三类:
195
196 - built-in action:`/compact`、`/new`、`/clear`、`/effort`、`/mcp`、`/help`;
197 - `.reasonix/commands/*.md` 与用户配置目录中的自定义命令;
198 - MCP prompt:`/mcp__<server>__<prompt>`。
199
200 自定义命令支持简单 frontmatter、`$ARGUMENTS`、`$1…$N` 和 `$$`。加载失败的单个命令会被跳过,不应使应用整体退出。
201
202 Bubble Tea TUI 的 modal overlay 必须隐藏 composer;slash/`@` autocomplete 等 input-owned overlay 保留 composer。新增 overlay 时必须更新 `chat_tui.hideComposer()` 与 layout test。
203
204 ### 3.9 `@` 引用
205
206 - `@<server>:<uri>` 读取 MCP resource;
207 - `@<path>` 仅在本地路径真实存在时读取文件或目录,普通 `@mention` 与邮箱保持原文本;
208 - 文件内容有大小限制,binary 只标记不展开;目录按深度优先列出并跳过 `.git`、`node_modules` 等噪音;
209 - 解析异步进行,失败显示 notice 但不阻止本回合;
210 - autocomplete 每次只读取一层目录,避免在大型目录中递归遍历。
211
212 ### 3.10 子智能体 Profile
213
214 子智能体 Profile 是带 `runAs: subagent` 的 Skill。桌面端和 CLI 只允许修改简单、手动调用的 project/global profile;包含 `references/`、`scripts/` 或非托管 frontmatter 的丰富 Skill 不会被编辑器扁平化覆盖。
215
216 `reasonix subagent try` 使用只读 Skill runner;`reasonix subagent run` 使用常规权限与 Sandbox。`task` 支持 `profile`、`model`、`effort` 和 `write_paths`;`fleet` 在 session scheduler 上并发调度多个任务。详见[子智能体 Profile](./SUBAGENT_PROFILES.zh-CN.md)。
217
218 ## 4. 数据类型
219
220 provider 层的核心类型包括 `Role`、`Message`、`ToolCall`、`ToolSchema`、`Request` 和 streaming `Chunk`。`Message` 保留 `tool_calls`、`tool_call_id` 与 `name`;`Chunk` 区分 text、tool call、done 和 error。字段定义以英文规范及 `internal/provider` 源码为准。
221
222 ## 5. 配置
223
224 配置优先级:
225
226 ```text
227 flag > ./reasonix.toml > 用户 config.toml > 内置默认值
228 ```
229
230 从 v1.8.1 起,用户配置位于 macOS/Linux 的 `~/.reasonix/config.toml` 或 Windows 的 `%AppData%\reasonix\config.toml`。provider key 保存在 Reasonix home 的 `.env`;项目 `.env` 只用于 workspace 范围的非 provider 变量展开。完整路径见[配置路径](./CONFIG_PATHS.zh-CN.md)。
231
232 ```toml
233 default_model = "deepseek"
234
235 [agent]
236 temperature = 0.0
237 reasoning_language = "auto"
238
239 [[providers]]
240 name = "deepseek"
241 kind = "openai"
242 base_url = "https://api.deepseek.com"
243 models = ["deepseek-v4-flash", "deepseek-v4-pro"]
244 default = "deepseek-v4-flash"
245 api_key_env = "DEEPSEEK_API_KEY"
246 context_window = 1000000
247 max_output_tokens = 32768 # 正文、reasoning 与工具调用共用的总输出预算;0 使用 provider 默认值
248
249 [tools]
250 enabled = []
251 bash_timeout_seconds = 120
252 mcp_startup_timeout_seconds = 30
253 mcp_call_timeout_seconds = 300
254
255 [permissions]
256 mode = "ask"
257 deny = ["Bash(rm -rf*)", "Bash(git push*)"]
258 allow = ["Bash(go test:*)", "Bash(git status:*)"]
259
260 [sandbox]
261 # workspace_root = ""
262 # allow_write = ["/tmp"]
263 # forbid_read = ["${HOME}/.ssh"]
264
265 [serve]
266 auth_mode = "none"
267 ```
268
269 原生 CLI 更新器始终安装最新的严格 `vX.Y.Z` 正式版。1.x 期间仍解析旧渠道配置与
270 参数,但统一指向正式版,并在后续保存配置时省略这些字段。
271
272 `[sandbox]` 是权限策略之下的强制执行层。file writer 默认限制在 workspace root、Reasonix 用户配置目录和 `allow_write`;`forbid_read` 可阻止读取敏感路径。macOS 使用 Seatbelt,Linux 使用 bubblewrap;若声明 enforce 但平台 backend 不可用,Bash 应拒绝执行而不是静默降级。Windows 当前没有 OS 级 Bash sandbox,file tool 的路径限制仍然生效。
273
274 `[serve]` 控制 `reasonix serve` 的 browser frontend。默认 `auth_mode = "none"` 仅适合 loopback;暴露到其他机器时必须使用 token 或 password。只有位于可信 reverse proxy 后方时才能启用 `behind_proxy`。
275
276 项目根目录的 `.mcp.json` 可使用 Claude Code 的 `mcpServers` schema;与 `reasonix.toml` 同名时,以后者为准。
277
278 MCP 启动与单次工具调用使用不同生命周期。调用方只短暂等待冷启动,而共享的进程启动、授权、
279 `initialize`、`tools/list` 可在后台继续,最长由 `mcp_startup_timeout_seconds`(默认 `30`)
280 限制;单个服务器可用 `startup_timeout_seconds` 覆盖。MCP 调用超时只在连接就绪后开始计算。
281
282 ## 6. 错误处理
283
284 - library code 使用 `fmt.Errorf("...: %w", err)` 包装并返回错误,不打印也不调用 `os.Exit`;
285 - 只有 `cli` / `main` 决定 exit code 和面向用户的信息;
286 - tool error 返回给模型,不直接终止 agent loop;
287 - network layer 应对 429 / 5xx 使用有界指数退避。
288
289 ## 7. 代码风格
290
291 - `gofmt`、`go vet` 必须通过;
292 - package name 使用小写,exported identifier 必须有文档;
293 - 注释解释“为什么”,而不只是复述“做了什么”;
294 - 避免过早抽象,优先清晰直接的实现。
295
296 ## 8. 分发
297
298 - 构建:`CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$(VERSION)" -o reasonix ./cmd/reasonix`
299 - 目标矩阵:`darwin|linux|windows × amd64|arm64`
300 - 版本通过 ldflags 注入,来源为 `git describe --tags --always`
301 - 支持预编译二进制、`go install` 与 Homebrew。
302
303 ## 9. 路线图(当前范围之外)
304
305 - 完成 Sandbox Phase 1 的 escape prompt:检测 sandbox 不可用或拒绝时,提供一次明确、受权限控制的非 sandbox 重试。
306 - MCP long tail:OAuth 2.0、`headersHelper`、更多 `.mcp.json` scope、tool-search 延迟加载、`list_changed`、channel、elicitation、root,以及可提供 provider 的插件。
307 - 增加 Anthropic-native provider kind,用于验证 registry 不依赖单一 wire format,并支持原生 prompt cache control。
308 - 把“始终允许”规则持久化到项目配置,以及为 `reasonix run` 提供 session 级权限覆盖。
309
309 lines MARKDOWN