Skip to content

Commit 8a551f5

Browse files
authored
Merge pull request #483 from phantom5099/main
pref(provider): Runtime Final Acceptance 主链重构与领域模型收敛
2 parents 3079a24 + 2d4bec6 commit 8a551f5

82 files changed

Lines changed: 2144 additions & 3696 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/compatibility-fallback-lifecycle.md

Lines changed: 0 additions & 21 deletions
This file was deleted.

docs/guides/adding-providers.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ func DefaultProviders() []ProviderConfig {
4949
return []ProviderConfig{
5050
OpenAIProvider(),
5151
GeminiProvider(),
52-
OpenLLProvider(),
5352
QiniuProvider(),
5453
ModelScopeProvider(),
5554
DeepSeekProvider(),

docs/guides/configuration.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ generate_start_timeout_sec: 90
2828
runtime:
2929
max_no_progress_streak: 5
3030
max_repeat_cycle_streak: 3
31-
max_turns: 40
31+
max_turns: 90
3232
assets:
3333
max_session_asset_bytes: 20971520
3434
max_session_assets_total_bytes: 20971520
@@ -157,7 +157,6 @@ internal/config/provider.go
157157

158158
- `openai`
159159
- `gemini`
160-
- `openll`
161160
- `qiniu`
162161
- `modelscope`
163162

@@ -210,7 +209,6 @@ API Key 只从系统环境变量读取。
210209
|----------|----------|
211210
| `openai` | `OPENAI_API_KEY` |
212211
| `gemini` | `GEMINI_API_KEY` |
213-
| `openll` | `AI_API_KEY` |
214212
| `qiniu` | `QINIU_API_KEY` |
215213
| `modelscope` | `MODELSCOPE_API_KEY` |
216214

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# Stop Reason And Decision Priority
22

3-
## StopReason 全集
3+
## StopReason 集合
44
- `user_interrupt`
55
- `fatal_error`
66
- `budget_exceeded`
77
- `max_turn_exceeded`
8-
- `retry_exhausted`
98
- `verification_failed`
109
- `accepted`
1110
- `todo_not_converged`
@@ -16,16 +15,13 @@
1615
- `verification_config_missing`
1716
- `verification_execution_denied`
1817
- `verification_execution_error`
19-
- `compatibility_fallback`
2018

21-
## 优先级
22-
- `user_interrupt` > `fatal_error` > `budget_exceeded` > `max_turn_exceeded` > `retry_exhausted` > `verification_failed` > `accepted`
23-
24-
## 决议互斥关系
25-
- decider 返回单一 stop reason。
26-
- acceptance/verifier 只提供输入,不直接终裁。
19+
## 决策优先级
20+
- controlplane decider 仍负责输出唯一 stop reason。
21+
- 通用优先级保持为:`user_interrupt` > `fatal_error` > `budget_exceeded` > `max_turn_exceeded` > `verification_failed` > `accepted`
22+
- final acceptance 只根据 completion gate、verifier gate 与 terminal decision 规则产出结果,不再额外注入 todo retry 旁路。
2723

2824
## 与 ErrorClass 的关系
29-
- `ErrorClass` 只描述失败分类(compile/test/lint/type/timeout/permission 等)
30-
- stop reason 描述终止归因;error class 描述失败类型,二者不重复表达
31-
25+
- `StopReason` 表达“为什么这次 run 结束”
26+
- `ErrorClass` 只表达 verifier 失败的领域分类,例如 `env_missing``execution_denied``timeout`
27+
- `pass` 结果不得携带 `ErrorClass`

docs/task-acceptance-design.md

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,33 @@
11
# Task Acceptance Design
22

3-
## 背景问题
4-
- 旧流程中,模型输出 final 且无 tool call 时,runtime 可能直接完成。
5-
- 这会导致“文本 final”与“任务真实完成”混淆。
6-
7-
## 为什么模型 final 不能直接等于完成
8-
- final 仅代表模型主观结束意图,不代表 required todo、文件产物或验证命令已满足。
9-
- 真实完成必须由 runtime 验收层裁决。
10-
11-
## completion / verification / acceptance 区分
12-
- `completion_gate`:判断当前回合是否可尝试收尾(必要非充分)。
13-
- `verification_gate`:由 verifier engine 判断任务是否满足验收条件。
14-
- `acceptance_decision`:聚合两者输出 `accepted/continue/incomplete/failed`
15-
16-
## 双门控模型
17-
- `completed = completion_gate.passed && verification_gate.passed`
18-
- 任一门未通过都不能直接 `agent_done`
19-
20-
## 状态机
21-
- provider final -> `beforeAcceptFinal` -> verification -> acceptance_decided
22-
- `accepted` -> `agent_done`
23-
- `continue` -> 注入系统提醒继续推理
24-
- `incomplete/failed` -> 结束 run 并输出 stop reason
25-
26-
## StopReason 设计
27-
- stop reason 由 controlplane decider 统一输出。
28-
- 新增 `verification_failed``todo_not_converged``retry_exhausted` 等枚举。
29-
30-
## 与 todo / subagent / runtime 的关系
31-
- todo 是 verifier 输入,不直接决定终态。
32-
- subagent 完成不等于主任务完成,仍需通过 verifier gate。
33-
- runtime 只消费 decider 输出,不再平行判定终态。
34-
35-
## decider 单一裁决层
36-
- 终态只由 decider 输出。
37-
- events / TUI / persistence 统一消费 decider 决议。
38-
3+
## 主链目标
4+
final acceptance 只保留一条主链:
5+
6+
`session-owned verification profile -> completion gate -> verifier gate -> acceptance decision -> terminal decision`
7+
8+
这条链只负责回答一件事:现在是否可以稳定结束任务。
9+
10+
## 结构化输入
11+
- verifier 集合只由 `session.TaskState.VerificationProfile` 决定。
12+
- `session.TaskState``session.TodoItem` 是唯一结构化验收输入来源。
13+
- `Acceptance` 只用于人类阅读,不参与机器判定。
14+
- `Artifacts``ContentChecks``Supersedes` 通过 session 契约驱动 `file_exists``content_match` 与 required todo replacement 语义。
15+
16+
## 决策规则
17+
- completion gate 未通过:`continue`
18+
- verifier 首个非 `pass``soft_block``continue`
19+
- verifier 首个非 `pass``hard_block``incomplete`
20+
- verifier 首个非 `pass``fail``failed`
21+
- 全部 verifier `pass``accepted`
22+
23+
## Candidate Final
24+
- provider 返回 final 后,usage/provider/model 会先持久化。
25+
- assistant final 只作为 candidate final 暂存在内存。
26+
- 只有 `accepted``incomplete``failed` 才会把 candidate final 写入 `session.Messages`
27+
- `continue` 时不会落盘 candidate final,只会追加 reminder。
28+
29+
## 约束
30+
- 不再支持 compatibility fallback。
31+
- 不再支持通过 `runtime.verification.enabled=false``final_intercept=false` 跳过 verifier gate。
32+
- 不再支持 shell string verifier command。
33+
- 不再支持基于 task 文本的 verifier policy 推断。
Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
# Verifier Configuration And Policy
22

33
## 配置来源
4-
- 全局:`~/.neocode/config.yaml` `runtime.verification`
5-
- 仓库级扩展预留:`.neocode/verification.yaml`(本阶段先保留接口与策略位)
4+
- verifier 的执行参数只来自 `~/.neocode/config.yaml` 中的 `runtime.verification.verifiers.<name>`
5+
- final acceptance 要运行哪组 verifier,不再由配置里的 task policy 或任务文本推断决定,而是只由 session 持有的 `TaskState.VerificationProfile` 决定
66

7-
## 优先级
8-
- 仓库级 > 全局级 > 内建默认值(策略已按该优先级设计)。
7+
## VerificationProfile 映射
8+
- `task_only` -> `todo_convergence`
9+
- `create_file` / `docs` -> `todo_convergence`, `file_exists`, `content_match`
10+
- `config` -> `todo_convergence`, `file_exists`, `content_match`, `command_success`
11+
- `edit_code` -> `todo_convergence`, `git_diff`, `build`, `test`, `typecheck`
12+
- `fix_bug` -> `todo_convergence`, `git_diff`, `test`, `build`, `typecheck`
13+
- `refactor` -> `todo_convergence`, `git_diff`, `build`, `test`, `lint`, `typecheck`
914

10-
## 命令来源
11-
- 所有命令型 verifier 从 `runtime.verification.verifiers.<name>.command` 读取。
12-
- verifier 内禁止硬编码项目命令。
15+
## 命令模型
16+
- 命令型 verifier 只接受 `command: ["argv0", "argv1", ...]`
17+
- runtime 直接执行 argv,不再经过 `powershell -Command``sh -lc` 或其他 shell string 兼容层。
18+
- 旧配置里的 string command 只会在“简单空白分隔且不含 shell 语义”时自动迁移;带引号、管道、重定向、子命令替换等写法会被显式拒绝,并要求手工改成 argv。
1319

14-
## 启停规则
15-
- verifier 支持独立 `enabled/required`
16-
- 未配置 command 时:
17-
- required=true -> 返回显式 soft_block/fail
18-
- required=false -> skip(显式结果,不 silent pass)
19-
20-
## required / optional 行为
21-
- required verifier 失败会阻断 final。
22-
- optional verifier 缺省可跳过,但仍有事件与结果记录。
23-
24-
## non-interactive policy
25-
- verifier 命令走独立 `execution_policy`,不走普通 ask 权限链路。
26-
- 默认白名单命令(go/git/test/lint/typecheck 等)。
27-
- 明确拒绝高风险命令(如 `rm``sudo`)。
20+
## 结果语义
21+
- verifier 只允许返回 `pass``soft_block``hard_block``fail`
22+
- orchestrator 按顺序执行 verifier,并在首个非 `pass` 处短路。
23+
- `pass` 结果不得携带 `ErrorClass`
24+
- `fail_open``fail_closed``enabled``required` 等旧策略字段已移除,不再对结果做事后改写。
2825

26+
## Loader 与迁移
27+
- `Loader.Load()` 会在 strict decode 前对 verification schema 做内存态预处理,用于删除废弃字段并安全迁移 legacy command string。
28+
- loader 不会自动改写磁盘文件。
29+
- `go run ./cmd/neocode migrate context-budget` 仍是显式落盘升级入口,会保留 `.bak` 备份。

internal/app/bootstrap_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1979,7 +1979,6 @@ func disableBuiltinProviderAPIKeys(t *testing.T) {
19791979
t.Helper()
19801980
t.Setenv(config.OpenAIDefaultAPIKeyEnv, "")
19811981
t.Setenv(config.GeminiDefaultAPIKeyEnv, "")
1982-
t.Setenv(config.OpenLLDefaultAPIKeyEnv, "")
19831982
t.Setenv(config.QiniuDefaultAPIKeyEnv, "")
19841983
}
19851984

internal/config/context_budget_migration.go

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"gopkg.in/yaml.v3"
1212
)
1313

14-
// ContextBudgetMigrationResult 汇总 config.yaml 预算配置迁移的执行结果
14+
// ContextBudgetMigrationResult 汇总 config.yaml schema 升级的执行结果
1515
type ContextBudgetMigrationResult struct {
1616
Path string
1717
Changed bool
@@ -21,7 +21,7 @@ type ContextBudgetMigrationResult struct {
2121
}
2222

2323
const (
24-
// ContextBudgetMigrationNoteEnabledDeprecated 标记旧开关被废弃且预算门禁不可关闭
24+
// ContextBudgetMigrationNoteEnabledDeprecated 提示旧 enabled 开关已废弃
2525
ContextBudgetMigrationNoteEnabledDeprecated = "旧 context.auto_compact.enabled 已废弃,新预算门禁不可关闭"
2626
)
2727

@@ -35,7 +35,7 @@ func UpgradeConfigSchema(path string) (ContextBudgetMigrationResult, error) {
3535
return MigrateContextBudgetConfigFile(path, false)
3636
}
3737

38-
// MigrateContextBudgetConfigFile 将 config.yaml 中的 context.auto_compact 迁移到 context.budget
38+
// MigrateContextBudgetConfigFile 将 config.yaml 中的旧 schema 迁移到当前实现
3939
func MigrateContextBudgetConfigFile(path string, dryRun bool) (ContextBudgetMigrationResult, error) {
4040
if path == "" {
4141
path = DefaultConfigPath()
@@ -76,7 +76,7 @@ func MigrateContextBudgetConfigFile(path string, dryRun bool) (ContextBudgetMigr
7676
return result, nil
7777
}
7878

79-
// MigrateContextBudgetConfigContent 将旧预算 YAML 块替换为当前预算 YAML 块,并返回迁移说明。
79+
// MigrateContextBudgetConfigContent 将旧 YAML schema 迁移为当前 schema,并返回迁移说明。
8080
func MigrateContextBudgetConfigContent(raw []byte) ([]byte, bool, []string, error) {
8181
if len(bytes.TrimSpace(raw)) == 0 {
8282
return raw, false, nil, nil
@@ -127,6 +127,14 @@ func MigrateContextBudgetConfigContent(raw []byte) ([]byte, bool, []string, erro
127127
}
128128
}
129129

130+
verificationChanged, err := migrateVerificationConfig(doc)
131+
if err != nil {
132+
return nil, false, nil, err
133+
}
134+
if verificationChanged {
135+
changed = true
136+
}
137+
130138
if !changed {
131139
return raw, false, nil, nil
132140
}
@@ -138,6 +146,116 @@ func MigrateContextBudgetConfigContent(raw []byte) ([]byte, bool, []string, erro
138146
return out, true, notes, nil
139147
}
140148

149+
// migrateVerificationConfig 清理已废弃的 verification 字段,并将安全的旧 command string 收敛成 argv。
150+
func migrateVerificationConfig(doc map[string]any) (bool, error) {
151+
runtimeValue, ok := doc["runtime"]
152+
if !ok {
153+
return false, nil
154+
}
155+
runtimeMap, ok := migrationStringMap(runtimeValue)
156+
if !ok {
157+
return false, nil
158+
}
159+
verificationValue, ok := runtimeMap["verification"]
160+
if !ok {
161+
return false, nil
162+
}
163+
verificationMap, ok := migrationStringMap(verificationValue)
164+
if !ok {
165+
return false, nil
166+
}
167+
168+
changed := false
169+
for _, key := range []string{"enabled", "default_task_policy", "final_intercept", "max_retries", "hooks"} {
170+
if _, exists := verificationMap[key]; exists {
171+
delete(verificationMap, key)
172+
changed = true
173+
}
174+
}
175+
176+
verifiersValue, ok := verificationMap["verifiers"]
177+
if ok {
178+
verifiersMap, ok := migrationStringMap(verifiersValue)
179+
if ok {
180+
for name, rawVerifier := range verifiersMap {
181+
verifierMap, ok := migrationStringMap(rawVerifier)
182+
if !ok {
183+
continue
184+
}
185+
for _, key := range []string{"enabled", "required", "fail_open", "fail_closed"} {
186+
if _, exists := verifierMap[key]; exists {
187+
delete(verifierMap, key)
188+
changed = true
189+
}
190+
}
191+
commandChanged, err := migrateVerifierCommandField(verifierMap)
192+
if err != nil {
193+
return false, err
194+
}
195+
if commandChanged {
196+
changed = true
197+
}
198+
verifiersMap[name] = verifierMap
199+
}
200+
verificationMap["verifiers"] = verifiersMap
201+
}
202+
}
203+
204+
runtimeMap["verification"] = verificationMap
205+
doc["runtime"] = runtimeMap
206+
return changed, nil
207+
}
208+
209+
// migrateVerifierCommandField 将简单的旧 command string 迁移为 argv;含 shell 语义时直接报错。
210+
func migrateVerifierCommandField(verifierMap map[string]any) (bool, error) {
211+
value, ok := verifierMap["command"]
212+
if !ok {
213+
return false, nil
214+
}
215+
command, ok := value.(string)
216+
if !ok {
217+
return false, nil
218+
}
219+
fields, err := parseLegacyVerificationCommand(command)
220+
if err != nil {
221+
return false, err
222+
}
223+
if len(fields) == 0 {
224+
delete(verifierMap, "command")
225+
return true, nil
226+
}
227+
228+
args := make([]any, 0, len(fields))
229+
for _, field := range fields {
230+
args = append(args, field)
231+
}
232+
verifierMap["command"] = args
233+
return true, nil
234+
}
235+
236+
// parseLegacyVerificationCommand 仅接受不含 shell 语义的简单空白分隔命令。
237+
func parseLegacyVerificationCommand(command string) ([]string, error) {
238+
trimmed := strings.TrimSpace(command)
239+
if trimmed == "" {
240+
return nil, nil
241+
}
242+
if containsUnsafeLegacyVerifierCommandSyntax(trimmed) {
243+
return nil, errors.New("runtime.verification.verifiers.command uses unsupported shell syntax; rewrite it as argv")
244+
}
245+
return strings.Fields(trimmed), nil
246+
}
247+
248+
// containsUnsafeLegacyVerifierCommandSyntax 判断旧命令是否包含无法安全自动迁移的 shell 结构。
249+
func containsUnsafeLegacyVerifierCommandSyntax(command string) bool {
250+
unsafeTokens := []string{"'", "\"", "`", "|", "&&", "||", ";", ">", "<", "$(", "\n", "\r"}
251+
for _, token := range unsafeTokens {
252+
if strings.Contains(command, token) {
253+
return true
254+
}
255+
}
256+
return false
257+
}
258+
141259
// collectContextBudgetMigrationNotes 汇总迁移过程中需要提示给用户的行为变化说明。
142260
func collectContextBudgetMigrationNotes(autoCompact map[string]any) []string {
143261
if value, ok := autoCompact["enabled"]; ok && migrationExplicitFalse(value) {

0 commit comments

Comments
 (0)