# Reasonix 工程规格
English
> Reasonix 是一个 coding agent:由极薄的 harness 驱动多个模型,所有能力都由配置和插件提供。本文是工程契约,代码应遵循它;需要改变行为时,应先更新契约,再修改代码。
英文原文是规范性版本;本文按相同章节提供中文说明,代码标识符、配置键和协议名保持原样。
## 1. 设计原则
1. **配置与插件驱动。** 核心只依赖接口;具体模型和工具通过 registry 按名称解析、在配置中声明,或由插件注入,不硬编码 `switch model`。
2. **单一静态二进制。** 使用 `CGO_ENABLED=0`,一条命令完成跨平台编译,CLI 开箱即用。
3. **精简依赖。** 默认使用标准库。第三方依赖必须是纯 Go、足够轻量,且不能破坏单二进制、跨平台和分发体验;TOML parser 是当前唯一接受的基础依赖。
4. **两级扩展。** 编译期 built-in 通过 `init()` 自注册;运行时外部插件以 stdio JSON-RPC 子进程或 MCP 兼容传输接入。
5. **接口优先、registry 驱动。** `Provider` 与 `Tool` 都是接口。
6. **持续演进,不过度设计。**
所有代码、注释、面向用户的字符串、工具描述、system prompt 和英文规范以英语为主;README 同时维护英文版 `README.md` 与中文版 `README.zh-CN.md`。
## 2. 目录与依赖方向
```text
reasonix/
├── go.mod / go.sum
├── Makefile
├── README.md / README.zh-CN.md
├── reasonix.example.toml
├── docs/SPEC.md / docs/SPEC.zh-CN.md
├── cmd/reasonix/main.go
├── cmd/reasonix-plugin-example/
└── internal/
├── cli/
├── config/
├── provider/
│ └── openai/
├── tool/
│ └── builtin/
├── permission/
├── command/
├── plugin/
├── remote/
│ ├── forward/
│ ├── sftpfs/
│ └── bootstrap/
└── agent/
```
核心依赖方向保持无环:
```text
cli → {agent, plugin, config} → {tool, provider}
```
`provider/openai`、`tool/builtin` 等 built-in 子包导入父包完成自注册,父包不反向导入子包。Remote-SSH 采用 `cli → remote/bootstrap → remote` 的分层,`remote` 及其子包不依赖 `cli`、`agent` 或 `serve`;host key 和 secret prompt 等交互都通过 callback 暴露,供桌面端复用。
## 3. 核心抽象
### 3.1 Provider 与 registry(`internal/provider`)
```go
type Provider interface {
Name() string
Stream(ctx context.Context, req Request) (<-chan Chunk, error)
}
type Factory func(cfg Config) (Provider, error)
func Register(kind string, f Factory)
func New(kind string, cfg Config) (Provider, error)
```
- `openai` kind 实现 OpenAI-compatible `/chat/completions`。
- OpenAI-compatible vendor 只是 `kind = "openai"` 的不同配置实例,通过 `base_url`、`model`、`api_key_env` 区分;新增兼容模型通常只需改配置。
- 一个 provider 表示一个 vendor endpoint,可通过 `models` 暴露多个模型,并以 `default` 指定默认项。`default_model`、`--model` 和桌面端模型选择器都经 `Config.ResolveModel` 解析,可接受 provider 名、裸模型名或 `provider/model`。
- `context_window` 是 provider 级默认值;`model_overrides..context_window` 可覆盖单个模型。
- streaming tool-call delta 在 provider 内按 index 聚合,只向上层发出完整 `ToolCall`。
### 3.2 Tool 与 registry(`internal/tool`)
```go
type Tool interface {
Name() string
Description() string
Schema() json.RawMessage
Execute(ctx context.Context, args json.RawMessage) (string, error)
}
```
- built-in tool 通过 `tool.RegisterBuiltin` 注册到进程级集合。
- 每次运行创建独立 `*Registry`,由启用的 built-in 与插件工具组成;agent 只看到该 registry。
- tool schema 在插入 registry 时 canonicalize;内置契约见[工具合约](./TOOL_CONTRACT.zh-CN.md),测试会校验文档与 canonical schema 不漂移。
- `Execute` 自行解析原始 JSON 参数。错误作为结果返回给模型,让模型有机会自我修正,而不是直接终止进程。
### 3.3 插件与 MCP(`internal/plugin`)
外部插件是配置中声明的 MCP server。协议统一为 JSON-RPC 2.0,传输由 `transport` 接口抽象:
- `stdio`:本地持久子进程,每行一条 JSON 消息。
- `http` / `streamable-http`:向远程 `url` POST,支持 `application/json` 和 SSE 响应,并复用 `Mcp-Session-Id`。
- `sse`:兼容旧版 2024-11-05 HTTP+SSE;持久 GET 接收 server 公布的相对 POST endpoint、JSON-RPC 响应与 server 消息。为避免静态 header 泄漏,会拒绝跨域 endpoint。
`${VAR}` 与 `${VAR:-default}` 可用于 `command`、`args`、`env`、`url` 和 `headers`,使 secret 留在环境中。生命周期为 `initialize` → `notifications/initialized` → `tools/list`,调用使用 `tools/call`。
存在工作区根目录时,初始化会声明 `roots` 能力,并用文件 URI 响应 `roots/list`。`tools/call` 会附带逐调用 `_meta.progressToken`;匹配的 `notifications/progress` 会进入现有工具进度事件链路。
远程工具适配为 `Tool`,命名为 `mcp____`。`annotations.readOnlyHint` 映射为 `Tool.ReadOnly()`,默认 false;只有显式声明为只读的工具才进入并行读取与默认只读权限路径。MCP prompt 暴露为 slash command,resource 可通过 `@:` 引用。
### 3.4 Agent loop(`internal/agent`)
`Session` 保存 `[]Message`。`Run(ctx, input)` 的主循环为:
1. 构建包含历史消息和 tool schema 的 `Request`。
2. 调用 `provider.Stream` 并实时输出 text delta。
3. 收集完整 tool call;若没有 tool call,则本回合结束。
4. 执行 built-in 或 plugin tool,把结果加入会话后继续,直到完成或达到安全边界。
`ctx` 贯穿调用链,Ctrl-C 可以取消进行中的请求。`Agent` 与 `Coordinator` 都实现 `Runner`,因此 CLI 不需要区分单模型或双模型执行。
### 3.5 双模型协作(`Coordinator`)
当 `agent.planner_model` 与 executor 不同时,planner 与 executor 使用独立 session:
- 宿主使用原始用户文本和可信回合元数据做确定性路由,不调用 classifier 模型,也不从
controller 注入的 prompt block 猜测宿主状态;路由结果为 executor-only、Light、Full、
plan-for-approval 或显式 plan-only,并用不含用户原文的 route/depth/reason 写入阶段详情;
- 显式 Plan Mode、synthetic turn、上下文短回复、明确单点小改和边界清楚的纯只读动作
不再调用第二个 Planner;跨面、结构化、模糊或高风险工作使用 Full;活跃 Goal 与
Delivery 中的非原子修改工作同样升级为 Full,纯只读动作仍直达 Executor;
- Light 使用较小的单轮调研预算,输出紧凑目标、1–4 个有序步骤、候选触点和主要验证;
Full 使用较大的有界预算,区分已验证与候选触点,并补充风险、验收标准、命令级验证及
必要回滚;深度合约保持在同一个稳定 system prompt 中,单轮只追加很小的
``;若 Planner 在有界调研和最终总结轮后仍未收敛,普通
plan-and-execute 用原始任务降级到 Executor,plan-only 与 plan-for-approval 仍保持
fail-closed;不完整的 Planner 回合会被回滚,不暴露成无法继续的手动续跑;
- 普通“先规划”在计划完成后直接交接 Executor;plan-for-approval 只用于明确要求等待
确认的请求,由宿主强制审批边界,批准后交接 Executor;headless 场景会保存计划供后续
回合继续;明确 plan-only 会保存计划并结束当前回合;上述两种执行边界下 Planner 失败
都不能降级执行;这些边界可位于任务子句之后,引号内的示例不改变路由;
- executor 在另一 session 中验证候选假设,并使用完整工具执行计划;
- 两条会话互不混合,prompt prefix 都只追加增长,避免切换模型破坏 prefix cache。
### 3.6 上下文管理
Reasonix 通过低频 compaction 保持 cache-first:
- 低于 `agent.tool_result_snip_ratio` 时不改写历史;
- 达到 snip ratio 后,归档并缩短较旧 tool result;
- 达到 `agent.compact_ratio` 后,先把旧 tool result 修剪为占位符,仍超阈值才调用摘要;
- 达到 `agent.compact_force_ratio` 后,可执行强制折叠;
- `context_window = 0` 会关闭该实例的 compaction。
tool result 的 snip/prune 不删除消息,确保 assistant `tool_calls` 与 tool result 配对。摘要只折叠 assistant/tool 工作;正常大小的用户回合和既有 digest 原样保留。被移除的原文归档到 `reasonix/archive/.jsonl`。
`history` tool 支持对 session 与归档进行 BM25 搜索;`memory` tool 用于检索自动记忆,`remember` 与 `forget` 负责写入和归档。智能体发起的记忆写操作每次都需要人工确认,不能由 YOLO、自动审查或子智能体代为批准。详细约定见 `SESSION_MEMORY_RETRIEVAL.md`。
### 3.7 权限
权限层按单次 tool call 返回 `Allow`、`Ask` 或 `Deny`:
```go
type Decision int
const (Allow Decision = iota; Ask; Deny)
type Policy struct { Mode Decision; Allow, Ask, Deny []Rule }
func (p Policy) Decide(toolName string, readOnly bool, args json.RawMessage) Decision
```
- rule 可以是 `Tool` 或 `Tool(specifier)`,例如 `Bash(go test:*)`、`Edit(docs/**)`。
- 优先级为 `deny > ask > allow > fallback`;只读工具 fallback 为 Allow,写工具 fallback 使用 `Mode`。
- 交互模式中的 Ask 由用户选择单次允许、session scope 允许、持久允许或拒绝;显式 Deny 在所有模式下都不可绕过。
- 安装 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 同名不构成复用权限。
- Plan 是协作流程,不等于全工具只读。普通 built-in 与 Bash 仍走 Ask/Auto/YOLO 和 Sandbox;独立双模型 Planner 允许已授权、非 destructive 的 MCP(即使没有 `readOnlyHint`),但在规划阶段持续阻止 destructive 与未授权目标;没有独立 Planner 的单模型 Plan 仍阻止 MCP writer/destructive。
- Plan 只能由用户显式选择进入,与当前工具审批姿态相互独立;普通聊天不会自动切换到 Plan。Auto/YOLO 不会回答 `ask`,也不会替用户批准 `exit_plan_mode`,获批计划的短期自动执行窗口也不会自动批准后续计划。
- 桌面端协作模式分为 `normal`、`plan` 和 `goal`。Goal 会持续推进目标,直到完成、同一阻塞状态重复三次、用户停止或达到安全续跑边界。只有用户在输入框中选择 Goal 或运行 `/goal` 显式启动后,长周期研究、调试、优化或实现目标才可启用 AutoResearch;普通聊天不会隐式切换协作模式,也不会创建持久化 AutoResearch 状态。动态状态保存在 `.reasonix/autoresearch/.../`。
### 3.8 Slash command
Slash command 分为三类:
- built-in action:`/compact`、`/new`、`/clear`、`/effort`、`/mcp`、`/help`;
- `.reasonix/commands/*.md` 与用户配置目录中的自定义命令;
- MCP prompt:`/mcp____`。
自定义命令支持简单 frontmatter、`$ARGUMENTS`、`$1…$N` 和 `$$`。加载失败的单个命令会被跳过,不应使应用整体退出。
Bubble Tea TUI 的 modal overlay 必须隐藏 composer;slash/`@` autocomplete 等 input-owned overlay 保留 composer。新增 overlay 时必须更新 `chat_tui.hideComposer()` 与 layout test。
### 3.9 `@` 引用
- `@:` 读取 MCP resource;
- `@` 仅在本地路径真实存在时读取文件或目录,普通 `@mention` 与邮箱保持原文本;
- 文件内容有大小限制,binary 只标记不展开;目录按深度优先列出并跳过 `.git`、`node_modules` 等噪音;
- 解析异步进行,失败显示 notice 但不阻止本回合;
- autocomplete 每次只读取一层目录,避免在大型目录中递归遍历。
### 3.10 子智能体 Profile
子智能体 Profile 是带 `runAs: subagent` 的 Skill。桌面端和 CLI 只允许修改简单、手动调用的 project/global profile;包含 `references/`、`scripts/` 或非托管 frontmatter 的丰富 Skill 不会被编辑器扁平化覆盖。
`reasonix subagent try` 使用只读 Skill runner;`reasonix subagent run` 使用常规权限与 Sandbox。`task` 支持 `profile`、`model`、`effort` 和 `write_paths`;`fleet` 在 session scheduler 上并发调度多个任务。详见[子智能体 Profile](./SUBAGENT_PROFILES.zh-CN.md)。
## 4. 数据类型
provider 层的核心类型包括 `Role`、`Message`、`ToolCall`、`ToolSchema`、`Request` 和 streaming `Chunk`。`Message` 保留 `tool_calls`、`tool_call_id` 与 `name`;`Chunk` 区分 text、tool call、done 和 error。字段定义以英文规范及 `internal/provider` 源码为准。
## 5. 配置
配置优先级:
```text
flag > ./reasonix.toml > 用户 config.toml > 内置默认值
```
从 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)。
```toml
default_model = "deepseek"
[agent]
temperature = 0.0
reasoning_language = "auto"
[[providers]]
name = "deepseek"
kind = "openai"
base_url = "https://api.deepseek.com"
models = ["deepseek-v4-flash", "deepseek-v4-pro"]
default = "deepseek-v4-flash"
api_key_env = "DEEPSEEK_API_KEY"
context_window = 1000000
[tools]
enabled = []
bash_timeout_seconds = 120
mcp_call_timeout_seconds = 300
[permissions]
mode = "ask"
deny = ["Bash(rm -rf*)", "Bash(git push*)"]
allow = ["Bash(go test:*)", "Bash(git status:*)"]
[sandbox]
# workspace_root = ""
# allow_write = ["/tmp"]
# forbid_read = ["${HOME}/.ssh"]
[serve]
auth_mode = "none"
```
`[sandbox]` 是权限策略之下的强制执行层。file writer 默认限制在 workspace root、Reasonix 用户配置目录和 `allow_write`;`forbid_read` 可阻止读取敏感路径。macOS 使用 Seatbelt,Linux 使用 bubblewrap;若声明 enforce 但平台 backend 不可用,Bash 应拒绝执行而不是静默降级。Windows 当前没有 OS 级 Bash sandbox,file tool 的路径限制仍然生效。
`[serve]` 控制 `reasonix serve` 的 browser frontend。默认 `auth_mode = "none"` 仅适合 loopback;暴露到其他机器时必须使用 token 或 password。只有位于可信 reverse proxy 后方时才能启用 `behind_proxy`。
项目根目录的 `.mcp.json` 可使用 Claude Code 的 `mcpServers` schema;与 `reasonix.toml` 同名时,以后者为准。
## 6. 错误处理
- library code 使用 `fmt.Errorf("...: %w", err)` 包装并返回错误,不打印也不调用 `os.Exit`;
- 只有 `cli` / `main` 决定 exit code 和面向用户的信息;
- tool error 返回给模型,不直接终止 agent loop;
- network layer 应对 429 / 5xx 使用有界指数退避。
## 7. 代码风格
- `gofmt`、`go vet` 必须通过;
- package name 使用小写,exported identifier 必须有文档;
- 注释解释“为什么”,而不只是复述“做了什么”;
- 避免过早抽象,优先清晰直接的实现。
## 8. 分发
- 构建:`CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$(VERSION)" -o reasonix ./cmd/reasonix`
- 目标矩阵:`darwin|linux|windows × amd64|arm64`
- 版本通过 ldflags 注入,来源为 `git describe --tags --always`
- 支持预编译二进制、`go install` 与 Homebrew。
## 9. 路线图(当前范围之外)
- 完成 Sandbox Phase 1 的 escape prompt:检测 sandbox 不可用或拒绝时,提供一次明确、受权限控制的非 sandbox 重试。
- MCP long tail:OAuth 2.0、`headersHelper`、更多 `.mcp.json` scope、tool-search 延迟加载、`list_changed`、channel、elicitation、root,以及可提供 provider 的插件。
- 增加 Anthropic-native provider kind,用于验证 registry 不依赖单一 wire format,并支持原生 prompt cache control。
- 把“始终允许”规则持久化到项目配置,以及为 `reasonix run` 提供 session 级权限覆盖。