Skip to content

Commit 9bb1773

Browse files
committed
refactor: enhance orchestrator and configuration management
- Introduced a layered approach in the orchestrator's `RunTurn` process, separating concerns into distinct pipeline steps for better maintainability. - Added a new `approval_builder.go` to handle approval logic, improving the clarity and reusability of approval-related code. - Refactored configuration management to utilize default values from a dedicated `default_values.go` file, ensuring consistency and reducing hardcoded values. - Updated the initialization of project configuration to respect existing user settings and streamline the setup process. - Enhanced error handling and logging for tool execution and approval processes, improving user feedback and debugging capabilities. - Added tests for new approval logic and configuration handling to ensure robustness and reliability.
1 parent ce6513a commit 9bb1773

14 files changed

Lines changed: 767 additions & 551 deletions

docs/technical/02-Orchestrator执行循环.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@
2222
7. 若有工具调用:逐个执行,写入 tool 消息,再进入下一步。
2323
8. 达到步数上限返回 `step limit reached`
2424

25+
### 3.1 推荐实现分层(重构目标)
26+
27+
`RunTurn` 应只保留流程编排职责,具体逻辑下沉为 pipeline step:
28+
29+
1. `prepareTurnState`:输入入栈、上下文刷新、回合状态初始化。
30+
2. `chatStep`:调用 provider(含流式输出控制)。
31+
3. `toolLoopStep`:工具调用循环(策略、审批、执行、回写)。
32+
4. `autoVerifyStep`:自动验证与重试注入。
33+
5. `persistStep`:session 落盘与收尾同步。
34+
35+
要求:
36+
37+
- 每个 step 尽量可单测。
38+
- step 之间通过显式状态结构传递,不直接读写过多 orchestrator 字段。
39+
2540
## 4. 工具调用执行顺序
2641
1. Agent 工具开关检查。
2742
2. Policy 决策(`allow/ask/deny`)。
@@ -30,6 +45,11 @@
3045
5. 执行工具。
3146
6. 结果写入 `tool` 消息并更新运行状态。
3247

48+
补充约束:
49+
50+
- 审批链路应支持“策略层 ask + 工具层 approval request”聚合为一次交互。
51+
- tool 执行失败要标准化写回(`{"ok":false,"error":"..."}`)并继续后续流程判定。
52+
3353
## 5. 模式行为矩阵
3454
- `build`
3555
- 目标:交付改动。
@@ -111,6 +131,11 @@
111131
- 若无可执行白名单命令:显式提示“未执行自动验证”。
112132
- 可重试失败时注入修复提示继续回合;最多 `max_verify_attempts` 次。
113133

134+
工程约束:
135+
136+
- 自动验证开关与重试默认值应来源于配置层,orchestrator 不维护重复硬编码默认值。
137+
- 是否触发验证的判定逻辑与命令选择逻辑应解耦,分别可测试。
138+
114139
## 9. 复杂任务判定
115140
按需求固定规则执行:
116141
1. 显式多目标枚举命中即复杂。
@@ -122,8 +147,20 @@
122147
- `/new``/resume` 会切换当前 session 上下文。
123148
- 维护回合级 undo 快照栈(仅保留最近若干回合),供 `/undo` 精确回滚使用。
124149

150+
## 10.1 错误与取消语义
151+
152+
- `context canceled/deadline exceeded`:优先返回上下文错误,不包装为业务错误。
153+
- provider/tool/approval 等非上下文错误:按链路分别包装,保留根因信息。
154+
- Esc 取消后不自动回滚副作用,保持“已执行即生效”。
155+
125156
## 11. 运行态取消(Esc)
126157
- REPL 在执行 `RunInput` 时提供可取消的 `context`;Esc 触发 `context cancel`
127158
- `RunTurn` 在模型调用、审批、tool 执行、自动验证链路中检测 `context canceled` 并立即终止,不继续后续自动化步骤。
128159
- 审批等待场景中 Esc 语义为全局 Cancel(不是 `N`)。
129160
- 取消不做回滚;已完成副作用保持,todo 状态维持最后一次持久化结果。
161+
162+
## 12. 兼容策略(重构期间)
163+
164+
- 对外行为保持稳定:`RunInput``RunTurn``/``!` 命令契约不变。
165+
- 允许的小行为修复(例如错误文案更准确、边界值处理修复)需在变更说明中列出。
166+
- 任何行为变化必须配套回归测试(优先 orchestrator 层单测)。
Lines changed: 71 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,95 @@
11
# 10. 配置加载与覆盖(目标态)
22

3-
## 1. 首次启动初始化项目配置
3+
## 1. 设计目标
44

5-
当在某个工作目录下首次运行 agent 二进制时,如果当前目录下:
5+
-`config.Default()` 作为**默认值单一来源(SSOT)**
6+
- `Load()` 仅负责“读取 + 合并 + 归一化 + 环境变量覆盖”。
7+
- 业务层(如 orchestrator/bootstrap)不再复制配置默认值常量。
68

7-
- 不存在 `./.coder/config.json`,且
8-
- 不存在 `./agent.config.json`
9+
## 2. 首次启动初始化项目配置
910

10-
程序会自动
11+
当首次在项目目录运行时,若不存在 `./.coder/config.json`,程序会
1112

12-
1. 创建 `./.coder/` 目录(如有需要)。
13-
2. 将内置默认配置(`config.Default()`)序列化为 JSON,写入 `./.coder/config.json`(缩进 2 空格)
13+
1. 创建 `./.coder/`(若不存在)。
14+
2. `config.Default()` 序列化输出模板到 `./.coder/config.json`
1415

15-
若目录无写权限或写入失败,仅在 stderr 打印警告,不会中断后续配置加载流程
16+
若目录无写权限或写入失败,仅输出 warning,不阻断后续加载流程
1617

17-
## 2. 加载顺序
18+
## 3. 加载顺序
1819

19-
`config.Load()` 实际流程
20+
`config.Load()` 的确定性顺序
2021

21-
1. 内置默认配置(`config.Default()`)。
22-
2. 全局配置:`~/.coder/config.json`(若存在则合并)。
23-
3. 项目配置(按顺序自动发现并合并):
24-
- 当前路径下的 `agent.config.json`
25-
- 当前路径下的 `.coder/config.json`
26-
4. 归一化:
27-
- 补默认值;
28-
- 路径展开(如 `~`);
29-
- 列表去重与去空等。
30-
5. 环境变量覆盖(如 `AGENT_BASE_URL``AGENT_MODEL` 等)。
31-
6. 二次归一化(保证覆盖后仍满足约束)。
22+
1. 内置默认配置:`config.Default()`
23+
2. 全局配置:`~/.coder/config.json`(存在则 merge)。
24+
3. 项目配置:`./.coder/config.json`(存在则 merge)。
25+
4. `normalize`(补缺省、路径展开、去重与约束校验)。
26+
5. 环境变量覆盖(如 `AGENT_BASE_URL``AGENT_MODEL`)。
27+
6. 二次 `normalize`(确保 env 覆盖后仍满足约束)。
3228

33-
## 3. JSONC 支持
34-
- 解析前去除行注释与块注释。
35-
- 使用标准 JSON 反序列化。
29+
## 4. JSONC 支持
3630

37-
## 4. merge 规则
38-
- 标量字段:非空/有效值覆盖。
39-
- 列表字段:新列表整体覆盖旧列表。
40-
- map 字段:按配置块整体替换。
31+
- 读取后先移除行注释 `//` 与块注释 `/* ... */`
32+
- 再使用标准 JSON 反序列化。
4133

42-
## 5. normalize 规则
43-
- 补默认值:`max_steps``timeout``token_limit` 等。
44-
- 路径展开:`~` -> home,统一绝对路径。
45-
- 去重与去空:模型列表、技能路径、命令列表。
46-
- 空白字段清理。
34+
## 5. merge 责任边界
35+
36+
- `types`:声明配置结构,不包含 merge 行为。
37+
- `loader`:文件读取、JSONC 清洗、反序列化。
38+
- `merge`:字段级覆盖策略(标量、list、map 规则)。
39+
- `normalize`:约束收敛与路径归一化。
40+
- `env`:环境变量读取与覆盖。
41+
42+
merge 规则:
43+
44+
- 标量:override 为非空/有效值时覆盖 base。
45+
- list:采用“整体替换”或“显式追加”策略,禁止隐式拼接。
46+
- map:按配置块语义处理(例如 `permission.bash` 为整体替换)。
47+
48+
## 6. normalize 规则
49+
50+
- 补默认值:来自 `config.Default()`
51+
- 路径展开:`~` 展开为 home,统一绝对路径。
52+
- 去重与去空:模型列表、命令列表、路径列表。
53+
- 约束修正:如阈值上下界、最小重试次数等。
54+
55+
## 7. 环境变量覆盖
4756

48-
## 6. 环境变量覆盖
4957
- `AGENT_BASE_URL`
5058
- `AGENT_MODEL`
5159
- `AGENT_API_KEY`(可回退 `DASHSCOPE_API_KEY`
5260
- `AGENT_WORKSPACE_ROOT`
5361
- `AGENT_MAX_STEPS`
5462
- `AGENT_CACHE_PATH`
5563

56-
非法值处理:
57-
- 例如 `AGENT_MAX_STEPS<=0` 直接报错。
64+
错误处理:
65+
66+
- 非法值立即返回错误(如 `AGENT_MAX_STEPS<=0`)。
67+
68+
## 8. 关键配置块
5869

59-
## 7. 关键配置块
60-
- `provider`:私有模型地址、默认模型、超时、模型列表。
70+
- `provider`:模型地址/默认模型/超时/模型列表。
6171
- `runtime`:workspace、最大步数、上下文上限。
62-
- `safety`:命令超时、输出截断。
63-
- `compaction`:压缩阈值与保留条数。
64-
- `workflow`:复杂任务 todo、自动验证开关与命令、模式轮转。
65-
- `workflow`:复杂任务 todo、自动验证开关与命令。
66-
- `permission`:工具权限与 bash pattern。
67-
- `agents`:agent 定义与默认 agent。
68-
- `skills`:skill 扫描路径(默认开启)。
69-
- `storage`:SQLite 路径。
70-
71-
## 8. 预设与运行规则
72-
- `/permissions` 预设:`build``plan`(与 `/mode` 联动)。
73-
- `/model <name>` 持久化仅写 `./.coder/config.json`
74-
- 主题/字体不提供配置项:固定 `OpenCode Dark` + 终端默认等宽字体。
72+
- `safety`:命令超时、输出上限。
73+
- `compaction`:压缩开关、阈值、保留消息数。
74+
- `workflow`:todo 约束、自动验证、重试次数、验证命令。
75+
- `approval`:交互审批与自动放行策略。
76+
- `permission`:工具权限、bash 策略、allowlist。
77+
- `agent/agents`:模式与 agent profile 定义。
78+
- `skills`:skill 搜索路径。
79+
- `storage`:持久化目录与缓存策略。
80+
- `lsp`:语言服务配置。
81+
- `fetch`:抓取超时、大小限制、默认请求头。
82+
83+
## 9. 兼容与行为变更记录(重构要求)
84+
85+
- 配置重构应保持外部字段兼容,不改 JSON key。
86+
- 若存在行为变化(如默认值修正),必须:
87+
- 在 PR 描述列出“Before/After”;
88+
- 在本文件追加兼容说明与迁移建议;
89+
- 为变化点补充回归测试。
90+
91+
## 10. 运行规则
7592

76-
## 9. 离线默认建议
77-
- `workflow.verify_commands` 配置白名单命令。
78-
- token 估算默认启发式,精确计数仅作可选增强。
79-
- 不配置 MCP 相关项。
93+
- `/permissions` 预设:`build``plan`(与 `/mode` 联动)。
94+
- `/model <name>` 持久化仅写入 `./.coder/config.json`
95+
- 默认离线模式不要求 MCP 配置。
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package bootstrap
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"os"
9+
"strings"
10+
11+
"golang.org/x/term"
12+
13+
"coder/internal/config"
14+
"coder/internal/permission"
15+
"coder/internal/tools"
16+
)
17+
18+
func buildApprovalFunc(cfg config.Config, policy *permission.Policy, workspaceRoot string) func(context.Context, tools.ApprovalRequest) (bool, error) {
19+
return func(ctx context.Context, req tools.ApprovalRequest) (bool, error) {
20+
isTTY := term.IsTerminal(int(os.Stdin.Fd()))
21+
isBash := strings.EqualFold(strings.TrimSpace(req.Tool), "bash")
22+
reason := strings.TrimSpace(req.Reason)
23+
24+
// 非交互环境:为安全起见,继续拒绝执行,避免静默放行破坏性操作。
25+
if !isTTY {
26+
return false, nil
27+
}
28+
29+
// 解析 bash 命令文本(仅在需要展示或加入 allowlist 时使用)。
30+
reader := bufio.NewReader(os.Stdin)
31+
bashCommand := ""
32+
if isBash && strings.TrimSpace(req.RawArgs) != "" {
33+
var in struct {
34+
Command string `json:"command"`
35+
}
36+
if err := json.Unmarshal([]byte(req.RawArgs), &in); err == nil {
37+
bashCommand = strings.TrimSpace(in.Command)
38+
}
39+
}
40+
41+
// 区分策略层 ask 与工具层危险命令风险审批。
42+
isPolicyAsk := strings.Contains(reason, "policy requires approval")
43+
isDangerous := strings.Contains(reason, "dangerous") ||
44+
strings.Contains(reason, "overwrite") ||
45+
strings.Contains(reason, "substitution") ||
46+
strings.Contains(reason, "parse failed") ||
47+
strings.Contains(reason, "matches dangerous command policy")
48+
49+
// 非交互模式配置:策略层 ask 可自动放行;危险命令仍需显式 y/n。
50+
if !cfg.Approval.Interactive && !isDangerous {
51+
// 仅策略层 ask 走 auto_approve_ask;危险命令一律不在此路径放行。
52+
if cfg.Approval.AutoApproveAsk || isPolicyAsk {
53+
return true, nil
54+
}
55+
}
56+
57+
if prompter, ok := approvalPrompterFromContext(ctx); ok {
58+
decision, err := prompter.PromptApproval(ctx, req, ApprovalPromptOptions{
59+
AllowAlways: !isDangerous,
60+
BashCommand: bashCommand,
61+
})
62+
if err != nil {
63+
return false, err
64+
}
65+
switch decision {
66+
case ApprovalDecisionAllowOnce:
67+
return true, nil
68+
case ApprovalDecisionAllowAlways:
69+
if !isDangerous && isBash && bashCommand != "" {
70+
name := config.NormalizeCommandName(bashCommand)
71+
if name != "" && policy.AddToCommandAllowlist(name) {
72+
_ = config.WriteCommandAllowlist(workspaceRoot, name)
73+
}
74+
}
75+
return true, nil
76+
default:
77+
return false, nil
78+
}
79+
}
80+
81+
// 交互式审批:策略层 ask 支持 y/n/always;危险命令仅 y/n。
82+
_, _ = fmt.Fprintln(os.Stdout)
83+
_, _ = fmt.Fprintf(os.Stdout, "[approval required] tool=%s reason=%s\n", req.Tool, req.Reason)
84+
if isBash && bashCommand != "" {
85+
_, _ = fmt.Fprintf(os.Stdout, "[command] %s\n", bashCommand)
86+
}
87+
88+
if isDangerous {
89+
// 危险命令风险审批:始终仅 y/n。
90+
_, _ = fmt.Fprint(os.Stdout, "允许执行?(y/N): ")
91+
line, _ := reader.ReadString('\n')
92+
ans := strings.ToLower(strings.TrimSpace(line))
93+
if ans != "y" && ans != "yes" {
94+
return false, nil
95+
}
96+
return true, nil
97+
}
98+
99+
// 策略层 ask:支持 y/n/always。
100+
_, _ = fmt.Fprint(os.Stdout, "允许执行?(y/N/always): ")
101+
line, _ := reader.ReadString('\n')
102+
ans := strings.ToLower(strings.TrimSpace(line))
103+
switch ans {
104+
case "y", "yes":
105+
return true, nil
106+
case "always", "a":
107+
// 仅针对 bash 记录 allowlist;按命令名归一化。
108+
if isBash && bashCommand != "" {
109+
name := config.NormalizeCommandName(bashCommand)
110+
if name != "" && policy.AddToCommandAllowlist(name) {
111+
// best-effort 持久化到项目配置;失败不影响本次放行。
112+
_ = config.WriteCommandAllowlist(workspaceRoot, name)
113+
}
114+
}
115+
return true, nil
116+
default:
117+
return false, nil
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)