spec(1.21): T-3 — BlockToolResult + ToolResult + DIAGNOSE.* errcode - #81
Conversation
spec-1.21 T-3:
- domain/llm.go (D-1): BlockToolResult 兑现 (从 reserved 转激活, append-
only ordinal=2); 新 llm.ToolResult{ToolUseID, Content, IsError} (provider-
agnostic wire 类型, ≠ render block.ToolResult per spec-1.9b); ContentBlock
加 ToolResult *ToolResult 字段 + nil-guard 契约 godoc (BlockText →
二者皆 nil; BlockToolUse → ToolUse 非 nil + ToolResult nil; BlockToolResult
→ ToolResult 非 nil + ToolUse nil); 提供 NewToolUseBlock / NewToolResultBlock
构造器作为契约唯一守门点 (T-2 D-1 caller 禁手填错字段).
- app/diagnose/errors.go (D-5): 5 DIAGNOSE.* errcode 注册 — 终止类
(MAX_TURNS / TOTAL_TIMEOUT / TOOL_UNKNOWN / UNEXPECTED_PAUSE) + 回填类
(TOOL_TIMEOUT 写入 ToolResult.Content 让 LLM 自纠, 非 Run error). 规则 7
三件套 (Code/Message/Hint).
- platform/errcode frozen manifest 原子追加 5 行 DIAGNOSE.* (T-3 contract:
errcode.Register + manifest 同 commit 防 CI red, 规则 10).
测试: 8 新 unit (BlockType ordinals 0/1/2 / NewToolUseBlock 与
NewToolResultBlock nil-guard / ToolResult.IsError 标志 / 5 个 errcode
Code+Message+Hint + 唯一性). make gate 全绿; race detector 全绿.
T-4 (anthropic 多轮 toMessages) / T-5 (ToolExecutor + clock/echo) /
T-6 (diagnose.Loop) 后续 task.
Spec: spec-1.21-diagnose-loop.md
spec-1.21 T-4 (D-2):
- anthropic/anthropic.go toMessages: BlockToolUse → SDK NewToolUseBlock
(id, input any, name) + BlockToolResult → SDK NewToolResultBlock
(toolUseID, content, isError); 全 exhaustive switch + nil-guard
REQUEST_INVALID + default REQUEST_INVALID (规则 12 不 panic IO path).
CC baseline B-63 codex VERIFIED @ anthropic-sdk-go v1.45.0.
- openai/openai.go toParams 返签名改为 (params, error); 新 messageToSDK +
assistantToSDK + userToSDK 处理 OpenAI 协议差异: tool_result 是独立
role=tool message (非 user content block), 一个 user turn 含 N 个
BlockToolResult 展开成 N 个 ToolMessage (SDK.ToolMessage helper) +
trailing UserMessage (如有 BlockText). assistant turn 收集 BlockText
到单 AssistantMessage.Content + BlockToolUse 转 ChatCompletion
MessageToolCallParam{ID, Function:{Name, Arguments: JSON(Input)}}.
IsError 无原生字段 → "[tool error] " 内容前缀 (跨 provider 保信号).
nil Input map → "{}" (OpenAI 必须 JSON args). 双 role mis-match
(assistant+ToolResult / user+ToolUse) → REQUEST_INVALID.
- spec-1.20.1 firstText 退役 (D-2 注释解释保留路径) + 改 test 走新
userToSDK helper 保 1.20.1 单轮契约.
测试: 9 新 unit
- anthropic: ToMessages_MultiTurnRoundTrip / ToolUse+ToolResult NilGuard.
- openai: AssistantTextPlusToolCall / AssistantToolCallInputJSON /
UserToolResultAsToolMessage (IsError prefix) / UserMixedTextAndToolResult
(N tool + 1 user) / RoleMismatchRejected / NilGuards / UserToSDK_SingleText
(single-turn regression).
make gate 全绿; race detector 全绿.
Spec: spec-1.21-diagnose-loop.md
spec-1.21 T-5 (D-3):
- tool.go: ToolExecutor 接口 {Name() string; Schema() llm.ToolSchema;
Execute(ctx, input map[string]any) (ToolOutput, error)} + ToolOutput
{Content string, IsError bool}. 双失败 shape (R2 签名统一): IsError
= recoverable / 模型自纠 (写 BlockToolResult, loop 继续); Go error
= fatal / infra (ctx cancel / deadline / panic / SDK 崩, Loop D-4
cancel-vs-timeout 三分类处理).
- registry.go: 最小静态内存 Registry — NewRegistry(execs...) 构造期校验
Name unique + Schema().Name == Name() (D-3 R-12 一致性) + 空 Name 拒;
Get/Schemas/Names/Len 全 nil-safe; Schemas/Names 按 name 升序 (deterministic
prompt cache 友好). 无 Register/Unregister 方法 — Loop 共享 read-only,
无锁并发安全. 动态 skill loading + 用户配置 → spec-2.1 skill-registry
(本期严格不预埋, T-5 用户 boundary).
- tool_clock.go: ClockTool — 无副作用, 无输入, 返当前时间 RFC3339 UTC.
Now func() time.Time 可注入 (测试用定时, 默认 time.Now). ctx 先检查
防 cancel 后浪费 tick.
- tool_echo.go: EchoTool — 纯输入 JSON 回显 (NO shell / NO SQL / NO fs;
T-5 boundary). nil input → "{}". additionalProperties=true 接受自由
shape 给 LLM 端到端 round-trip 验证用.
测试: 16 新 unit
- registry: AcceptsBuiltins (clock+echo 注册成功 + 升序) /
RejectsDuplicate / RejectsEmptyName / RejectsSchemaNameMismatch
(R-12) / Get hit+miss / NilSafe / BuiltinSchemaConsistency
(table-driven 跨 builtin invariant);
- clock: FixedNow (定时 RFC3339 deterministic) / DefaultNow (nil-Now
→ time.Now 回退) / RespectsCancelledCtx / SchemaShape (Parameters
非空给 OpenAI-compat 兼容);
- echo: RoundTripsInput (model 看到自己参数) / NilInput → "{}" /
RespectsCancelledCtx / NoSideEffects (description 禁含 shell/exec/
sql/fs/command 关键词 — 范围纪律).
diagnose pkg 覆盖率 98.0% (Execute 路径少分支)
make gate 全绿; race detector 全绿.
T-6 (diagnose.Loop) 后续 commit 接续 PR #81.
Spec: spec-1.21-diagnose-loop.md
spec-1.21 T-6 (D-4 核心):
- event.go: Event{Kind, Turn, Text, Thinking, ToolUse, ToolResult,
Finish, TermCode, Err} + EmitFunc(ctx, Event) error + Result{Messages,
FinishReason, Turns, TermCode}. diagnose 包 strict 不引 llmapp /
render / block — 后续 llmapp 只消费事件 (D-6 桥接).
- loop.go: NewLoop(Options) 校验 nil Provider + 4 默认 (MaxTurns=16 /
ToolTimeout=30s / TotalTimeout=10min / ReqTimeout=60s).
Run(ctx, req, emit):
* ctx 三层: totalCtx (WithTimeout(ctx, totalTimeout)) → turnCtx
(WithTimeout(totalCtx, reqTimeout)) → toolCtx (WithTimeout(totalCtx,
toolTimeout)) 独立 cancel 防互相误杀.
* accumulator 是 req.Messages copy + tools = req.Tools ++ registry
schemas; per-turn 同 turnReq 注入.
* drain stream 收集 text (emit EventText, Thinking 分流) + ToolUses +
finish; emit 返 ctx.Err 即 backpressure → 中止.
* exhaustive switch 9 个 FinishReason (Stop/StopSequence → natural,
返 nil; Length → natural; Refusal → ErrProviderRefusal; Cancelled
→ ctx.Err; Error → finishErr (TOTAL_TIMEOUT/LLM.TIMEOUT/upstream);
Pause → DIAGNOSE.UNEXPECTED_PAUSE; ToolUse → validate-then-execute-
then-paired-commit; Unset → 防御 ErrDecodeFailed). **无 default 吞
新枚举** (T-6 用户边界, Go 编译期可见).
* FinishToolUse 路径:
1. 先校验全部 ToolUse.Name 在 registry — 任一未知 → DIAGNOSE.
TOOL_UNKNOWN 终止 + 不追加 assistant turn (防 orphan tool_use →
provider 400 on resume, T-2 HIGH-5).
2. 构造 asstContent (text + 全 tool_use blocks) — NOT 提交.
3. 顺序执行 each tool: emit EventToolCall (前) → Execute (per-tool
toolCtx) → classifyToolErr 三分类:
- errors.Is(ctx.Canceled) → 用户 cancel 硬终止
- errors.Is(ctx.DeadlineExceeded) && totalCtx in deadline →
TOTAL_TIMEOUT 硬终止
- 其它 err (含 per-tool DeadlineExceeded) → ToolResult{IsError:
true, Content: DIAGNOSE.TOOL_TIMEOUT msg / err.Error()} 回填
(LLM 自纠, 非终止)
- success → ToolResult{out.Content, out.IsError}
emit EventToolResult (后).
4. 全部 tool 成功执行 → 原子 paired-commit assistant turn + user
turn 进 accumulator (防 dangling tool_use 在 transcript).
* MaxTurns 耗尽 → DIAGNOSE.MAX_TURNS.
* classifyTerminal: stream err 分 ctx.Canceled (FinishCancelled 或
TOTAL_TIMEOUT 取决 totalCtx.Err) / ctx.DeadlineExceeded (TOTAL_TIMEOUT
或 LLM.TIMEOUT per-turn) / 其它 (FinishError 透传); classifyEmitErr
分 ctx-shaped (FinishCancelled) / 其它 (FinishError).
* 9 处 errcode-lint:exempt + spec_ref token (Loop 是 transparent 中间
层, ferr 来自 classify helper 或 sentinel).
测试: 17 新 unit (loop_test.go)
- NewLoop: RequiresProvider / AppliesDefaults
- happy: FinishStop / FinishStopSequence / FinishLength (各自自然终止
无 error 返回)
- terminal class: FinishRefusal → ErrProviderRefusal /
FinishPause → DIAGNOSE.UNEXPECTED_PAUSE
- 多轮: ToolRoundTrip_HappyAndPairedCommit (clock + FinishStop, 验
msgs 3 条 paired + 事件序 7 元)
- 回填类: ToolTimeout_FillbackContinues (toolTimeout 10ms vs slowTool
100ms; 回填 IsError + DIAGNOSE.TOOL_TIMEOUT msg + 下一轮 FinishStop) /
ToolReturnsError_FillbackContinues (errTool 返 "bang", 回填 +
FinishStop)
- 终止类: UnknownTool_TerminatesAndDropsAssistantTurn (msgs 仅含 user
input — assistant orphan 不提交) /
TotalTimeout_Terminates (tool 路径, TotalTimeout 30ms) /
TotalTimeoutViaStream (blockCtx stream + TotalTimeout 30ms) /
PerTurnTimeout_MapsToLLMTimeout (blockCtx + ReqTimeout 30ms) /
CallerCtxCancelledMidStream → context.Canceled /
NilRegistry_ToolUseMapsToUnknown /
MaxTurnsExceeded_Terminates (MaxTurns=2 + echo 无限自循环)
- emit 路径: EmitReturnsCtxCanceled_TerminatesCancelled /
EmitReturnsGenericError_TerminatesError
- 边角: ProviderConstructErr_Terminates (ErrAuthFailed 透传) /
StreamEndsWithoutFinish_StreamEmpty (空 chunks → ErrStreamEmpty)
diagnose pkg 覆盖率 88.5% → 93.7% (loop.Run 88.3%, classifyTerminal
30%→ 全分支, lookup 全分支)
make gate 全绿; race detector 全绿.
T-7 (fake.ScriptedTurns 多轮 + full-loop integration) 后续接续.
Spec: spec-1.21-diagnose-loop.md
spec-1.21 T-7 (D-7 fake + 全栈集成):
fake/fake.go (D-7 多轮模式 — additive, 不破 1.20 单轮 API):
- Provider 加 turns [][]llm.Chunk + callIdx int + sync.Mutex (mode-
exclusive: script != nil ⇔ flat; turns != nil ⇔ multi-turn).
- Turn{Text, ToolUses, Finish} 类型 + NewScriptedTurns(...Turn) 构造器.
Stream 第 N 次调用拉 turns[N-1]; turnChunks 渲染 optional text + 终
止 chunk (ToolUses + Finish).
- callIdx >= len(turns) → llm.RequestInvalidf (LLM.REQUEST_INVALID 注
册码; 静默重放会掩盖 "test ran longer than scripted" bug, spec D-7
显式契约).
- CallCount() 暴露 cursor 给测试 — 断言 round-trip 数.
app/diagnose/loop_integration_test.go (5 全栈集成 case):
- assertPairedHistory helper: 遍历 msgs, 每个含 BlockToolUse 的 assistant
turn 后必须紧跟 user turn 含完全配对 BlockToolResult (ID 序一致, 数量
相等). orphan tool_use OR tool_result 都 fail. 自动覆盖所有 case.
- FullLoop_HappyToolRoundTrip: 2 turn — turn 1 assistant 返 clock
tool_use, turn 2 (看到 tool_result 后) 返 final text + FinishStop.
验 res.Turns=2 / CallCount=2 / Messages=3 (user + asst tool_use +
user tool_result) / asst.Content shape (text + BlockToolUse) /
user.Content[0].ToolResult.Content = 固定 clock RFC3339 / 7 event
序 (TurnStart×2 + Text + ToolCall + ToolResult + Text + Finish).
- Spec120Regression_NoToolSingleTurn: registry=nil + text+FinishStop 单
turn → Res Turns=1 / CallCount=1 / Messages=1 (仅 caller input — 1.20
不提交 assistant text 到 wire transcript) / 3 event 序 (TurnStart +
Text + Finish — 1.20 shape exact). **1.21 不破 1.20** 守门.
- FullLoop_MultiToolPerTurn: assistant 一轮请求 N=2 tool (clock + echo);
顺序执行; 同 user turn 2 个 BlockToolResult, ID 序一致 [c1, c2].
- UnknownTool_NoOrphanTooluse: registry 仅含 clock, 模型请求 "not-
registered" → DIAGNOSE.TOOL_UNKNOWN + Messages=1 (orphan tool_use
不入 transcript).
- ScriptExhausted_SurfacesAsProviderErr: 脚本 1 turn 但 loop 需 2 →
第 2 次 Stream 返 LLM.REQUEST_INVALID (fake D-7 显式契约), loop 报
为 terminal err; CallCount=2 证实 overshoot 检测路径.
fake/fake_test.go (3 新 unit 验 D-7):
- AdvancesPerCall (turn 1 ≠ turn 2 chunks) / OutOfTurns (script exhaust
→ ErrRequestInvalid) / EmptyText (无 Text 的 ToolUse-only turn 单
chunk 输出).
diagnose pkg cov 93.7% → 95.0%; fake pkg cov 仍 100%.
make gate 全绿; race detector 全绿.
T-8 (llmapp 消费 Loop + 复用 block.ToolUse/ToolResult adapter +
退役 placeholder) 后续 commit.
Spec: spec-1.21-diagnose-loop.md
spec-1.21 T-8 (D-6 — llmapp 接入 Loop):
- loop_adapter.go (NEW): makeEmit(ts, ctrl, stripThink) 返
diagnose.EmitFunc, 把 Event 路由到 spec-1.20 既有控制平面 (TokenStream
+ streamControlMsg + ctrl chan), 保 R2 CRIT-2 token-vs-control 分流:
* EventText → ts.AppendChunk + ctrl streamControlMsg{VisibleContent,
Thinking} (sawContent 计数沿用)
* EventToolCall → ctrl{ToolUse: e.ToolUse}
* EventToolResult → ctrl{ToolResult: e.ToolResult}
* EventFinish → ts.AppendChunk(终止 chunk 给 streaming.TokenStream 保
Length 截断 / thinking-only Empty 路径) + ctrl{Finish, TermCode, Err}
* EventTurnStart → no-op (UI 不渲染 turn 边界, 留 future spec)
每个 ctrl send select ctx.Done — backpressure/cancel 不死锁 Loop
goroutine (T-2.1 HIGH-2 parity).
- loopStartCmd(ctx, loop, req, ts, ctrl, stripThink) → 起 goroutine 跑
loop.Run; 退出时 close ctrl + close ts (streamDoneMsg drain 链一致).
- stream.go: streamControlMsg 扩展 — 加 ToolUse / ToolResult *llm 指针 +
TermCode string; routing 按"哪个字段非空"分支. 删 consumeStream +
finishFromErr (Loop classifyTerminal/EmitErr/ToolErr 取代; 重复经典化
会引入 drift). mapToRender 保留 (emit 路径仍需要).
- model.go (主体重构):
* Options 加 Registry *diagnose.Registry + MaxTurns/ToolTimeout/
TotalTimeout/ReqTimeout (≤0 各自走 diagnose.NewLoop 默认 16/30s/
10min/60s). Registry=nil → 单 turn chat 模式 (FinishToolUse 自然
surface 为 DIAGNOSE.TOOL_UNKNOWN).
* Model 加 loop *diagnose.Loop (New 时构造, 跨 submit 复用) +
toolUseNames map[string]string (per-submit reset, join ToolResult.
ToolUseID → ToolUse.Name).
* New 用 diagnose.NewLoop 建 Loop; nil Provider → panic (programmer
error at wiring).
* submit 改 loopStartCmd; 其余 flow 不变.
* handleControl 按 union 字段分支: ToolUse != nil → append
block.NewToolUse + State=StateRunning + 记 names map; ToolResult !=
nil → lookup name + block.NewToolResult, **name 空 skip render**
(CC null-return 契约 / spec-1.9b R3 HIGH-3); Finish.Terminal →
appendFinishNode; 否则 sawContent 累加.
* appendFinishNode 加 4 DIAGNOSE.* TermCode 终止 marker (MAX_TURNS /
TOTAL_TIMEOUT / TOOL_UNKNOWN / UNEXPECTED_PAUSE) 优先于通用
FinishError; 删 FinishToolUse case (Loop 已执行+ emit 渲染 block).
* 删 toolUsePlaceholder helper + "执行待 spec-1.21" 占位文案 — 退役.
测试:
- TestModel_ToolUsePlaceholder → 改为 TestModel_LoopUnknownTool: provider
emit FinishToolUse 无 Registry → 验 DIAGNOSE.TOOL_UNKNOWN marker
存在 + 退役占位文案不漏.
- TestModel_LoopAppendsToolBlocks (NEW, headline T-8 case): NewScriptedTurns
2 turn (echo tool_use → final text) + Registry EchoTool, 验
scrollback[1] is block.ToolUse / scrollback[2] is block.ToolResult /
"all done" assistant text 入 scrollback.
- TestFinishFromErr / BenchmarkFinishFromErr 退役 (注释说明 Loop owns
classification, llmapp 不重分类).
- TestToolUsePlaceholder_Empty 退役 (helper 已删).
- 其它单测 (SubmitClearsBuffer / SawContent / ImmediateError /
HistoryBounded / AssistantTextDrained / CancelPropagatesToProvider /
BuildRequest_*) 不破 — Loop 单 turn FinishStop 与 1.20 单 stream
字节等价.
llmapp cov 仍 91.5%; diagnose 93.7%; fake 96.3%.
make gate 全绿; race detector 全绿.
T-9 (full gate + CI + fake-provider full-loop smoke) 后续 commit.
Spec: spec-1.21-diagnose-loop.md
spec-1.21 T-9 (D-8 + T-9 user 边界 — bootstrap 注入必修):
bootstrap (内/external 接通 — T-9 用户硬约束):
- defaultDiagnoseRegistry(): production ToolExecutor registry — clock +
echo, 读-only, 无副作用. 静态构造期 panic on contract violation (NewRegistry
唯一失败模式是 schema/name 不一致, 是编码 bug 不是 runtime). 真 DB
skill (topsql / pg_settings_* / awr) 留 spec-2.1 skill-registry +
动态 per-config 加载.
- newChatModel() 注入 Registry 进 llmapp.Options — cfg-load 失败 fallback
(LLM.UNAVAILABLE startErr provider) 也带 Registry, 保后续修复后 UI 即
时能跑工具不需重启.
- 2 新 unit (tui_launcher_test.go):
* TestDefaultDiagnoseRegistry_HasClockAndEcho: 守门 — Names() 含
clock + echo, future spec 增 skill 必须 additive 不能漏掉.
* TestNewChatModel_BuildsWithoutPanic: 全链路 wiring smoke
(Registry → Options → diagnose.NewLoop → llmapp.New), 防 panic 在
用户首键之前.
llmapp:
- Model.ScrollbackTypesForTest() 测试 seam — 返 render-node 具体类型名
([]string). 仅整合测试用; 避免 integration 测试用 reflect 撞
unexported 字段限制 (Go runtime panic). godoc 注 "Production code
MUST NOT call this."
tests/integration/uitest/diagnoseloop/ (新, 12th 独立 visual env gate
DIAGNOSELOOP_VISUAL_REQUIRED, spec D-8):
- diagnoseloop_test.go: 4 production-like full-loop smoke
(productionRegistry mirror + drive helper走 keystroke + Update +
Cmd-chain 与 program.Run 一致):
* TestDiagnoseLoop_FullSmoke: 2-turn (echo tool_use → final text)
+ Registry → scrollback shape exact [Message, ToolUse, ToolResult,
Message]; CallCount=2; view 含 "echo done"; **退役 placeholder
文案 ("执行待 spec-1.21" / "spec-1.21]") 必须不漏**.
* TestDiagnoseLoop_NoToolRegression: 1-turn FinishStop, Registry=nil
→ scrollback exact [Message, Message]; CallCount=1 — 1.20 no-tool
路径字节等价守门, 防 1.21 wiring 破 1.20.
* TestDiagnoseLoop_UnknownToolRendersMarker: 已注 clock+echo 但模型
请求 topsql (未来 hallucination) → DIAGNOSE.TOOL_UNKNOWN marker
出现 + scrollback 无 orphan block.ToolUse 提交 (T-2 HIGH-5 防
provider 400 on resume 守门).
* TestDiagnoseLoopVisualGolden_ParkedFixtures: 12th env gate
(DIAGNOSELOOP_VISUAL_REQUIRED) — 3 parked fixture 目录验存在
(DiagnoseLoopHappyToolRoundTrip / UnknownTool / NoToolRegression).
CC capture SOP 跟 1.10..1.20 同 pattern, 待后续 spec.
- visualstat_test.go: visualStat helper (内部 fixture dir 守门).
- testdata/visual/Diagnose*/README.md 各占位 parking note (Go test 不
需要 .keep, README 双关 doc + 守门).
tests/integration/uitest/llmchat/llmchat_render_test.go:
- LLMChatToolUse fixture 描述更新: T-8 退役 placeholder 后,
fixture 表示 "DIAGNOSE.TOOL_UNKNOWN" marker, 不再是 "[请求工具…]"
占位. 测试 code 不破 (fixture parked 跳过), 描述准.
测试 / 覆盖率:
- llmapp 91.5% / diagnose 93.7% / fake 96.3% / bootstrap 全 wire 覆盖.
- make gate 全绿; race detector 全绿; 12 / 12 必跑 job 类目通过.
真-key Layer-5 备注: 模型不必稳定调用 clock/echo (LLM 决策); 自动化锁
住行为靠 fake.NewScriptedTurns. 真-key tool round-trip → 可选 Layer-5,
与 § 12.1 一致 deferred 至 Stage-1 acceptance.
T-10a (3-route post-impl review) 后续启动.
Spec: spec-1.21-diagnose-loop.md
CI's nolintlint flagged the directive as unused because revive does not fire on _test-suffixed package names in standalone integration test dirs. Just drop the directive — Go's test runner accepts the suffix without complaint. Spec: spec-1.21-diagnose-loop.md
T-10a Path 1/3 — claude-code-reviewerReviewer: claude-code-reviewer (independent run) Findings: CRITn/a HIGHn/a MEDMED-1: When a provider emits
A user message with an empty Fix — add an explicit guard at the top of the case llm.FinishToolUse:
if len(toolUses) == 0 {
_ = emit(ctx, Event{Kind: EventFinish, Turn: turn, Finish: llm.FinishError, Err: llm.ErrDecodeFailed})
return finalize(result, msgs, llm.FinishError, "", llm.ErrDecodeFailed), llm.ErrDecodeFailed
}LOWLOW-1:
NITNIT-1: NIT-2: NIT-3: Invariant checklist (all VERIFIED)
Verdict: APPROVE-WITH-FIXES Follow-up (must-fix before merge):
Optional (deferred OK):
|
claude-code-reviewer T-10a Path 1/3 absorb (PR #81 comment 4572071808): MED-1 (must-fix before merge): - loop.go FinishToolUse case 顶加 len(toolUses)==0 防御 — anthropic decodeTools 在 toolAcc 空时返 (nil,nil) 不报错, 若未挡 Loop 会提交 一个空 BlockToolUse 的 assistant turn + 一个空 Content 的 user turn → 下一次 Stream 调用 Anthropic 400. 改为终止 FinishError + ErrDecodeFailed, 不提交 degenerate turn pair. - 加 TestRun_FinishToolUse_EmptyToolUses_RejectsAsDecodeFailed 守门. NIT-1: doc.go pkg comment 更新 — "streamStartCmd" → "loopStartCmd", "ToolUses" 字段名 → "*llm.ToolUse / *llm.ToolResult" union; 加 spec-1.21 D-6 多轮升级 + diagnose.Loop 包装 + block.ToolUse/ToolResult scrollback 说明. NIT-2: model.go submit() godoc — "streamStartCmd" → "loopStartCmd (spec-1.21 D-6; T-8 替换)". NIT-3: model.go Cleanup() / streamDoneMsg handler / ctrlBufSize 注释 — "consumeStream" → "loopStartCmd emit goroutine". LOW-1 (deferred OK, not in this patch): handleControl 的 toolUseNames shallow-copy mutation 例外说明留 Model struct godoc 后续 (single-submit guarantee 当前安全; 等未来 spec 解锁多 in-flight submit 再加注). 测试 / gate: - diagnose pkg + 1 新单测; race detector 全绿. - make gate 全绿 (errcode-lint exempt token 含 spec_ref). Spec: spec-1.21-diagnose-loop.md
T-10a path 2/3 — codex review on
|
codex T-10a Path 2/3 absorb (PR #81 comment 4572770510): P2-1 — ToolUse 完成后 UI 仍 StateRunning (llmapp/model.go:302-303): - handleControl ToolResult 分支只 append block.ToolResult, 未 transition 前面已 append 的 block.ToolUse Running → Resolved/Error. UI 表现: result 块已渲染, 但工具仍显"Running...". 违反 spec-1.9 caller-owns- transition 契约 + spec-1.21 D-6 "EventToolResult → 转 Resolved/Error" hand-off. - 修: 加 transitionToolUseState 不可变 helper (tail-walk 找 matching ID + 替换值; 返新 slice 保 next.scrollback 不污染 m.scrollback shared backing array). handleControl ToolResult 分支先 transitionToolUseState 再 append result block. IsError → StateError, 否则 StateResolved. - 2 新单测 (llmapp/model_test.go): * TestModel_LoopTransitionsToolUseState (happy path → Resolved); * TestModel_LoopTransitionsToolUseToError (IsError → StateError). P2-2 — 多工具同 turn UI emit 顺序不符 wire protocol shape (diagnose/loop.go:305-306): - 原 emit: call1, exec1, result1, call2, exec2, result2 — 交错 4 阶段 让 UI 看起来像 assistant 跨多个想象 turn 一次一工具. - 正确 protocol shape: assistant 一次性给所有 tool_use, user 一次性回 所有 tool_result. UI 应先 emit 全部 EventToolCall (Phase 1, 镜像 assistant turn), 再串行执行 + emit EventToolResult (Phase 2, 镜像 user turn). - 修: loop.FinishToolUse case 内重组为 2-phase loop. Phase 1 仅 emit call (validation 已先前完成); Phase 2 execute + emit result. cancel/ total-timeout 仍在 Phase 2 内 eager terminate, paired-commit 跳过 → 无 orphan tool_use 入 transcript. - 1 新单测 (diagnose/loop_test.go): * TestRun_MultipleTools_EmitPhaseOrder — 2 tools (clock + echo), 校验事件序为 [call:c1, call:c2, result:c1, result:c2] (Phase 1 all-calls then Phase 2 all-results). 兼容性: - 1.20 单 turn no-tool 路径不受影响 (没有 tool_use 时 phase 全跳过). - 1.21 spec D-6 / spec-1.9 contract 现在一致守住. - 已存在 TestRun_ToolRoundTrip_HappyAndPairedCommit / Integration full-loop smoke 全过 (paired-commit + msgs shape 不变, 仅 emit 序变). make gate 全绿; race detector 全绿. T-10a Path 3/3 (用户 final verdict) 待启. Spec: spec-1.21-diagnose-loop.md
T-10a Path 2/3 R-fix absorb — codex 2 P2 已修Source: codex review trace #issuecomment-4572770510 — REQUEST-CHANGES, 0 CRIT, 2 P2. P2-1 absorb — ToolUse 完成后 UI 仍 StateRunningFile: handleControl 原仅 append 修法:加 Tests(新 2 单测):
P2-2 absorb — 多工具同 turn UI emit 顺序File: 原 emit: 正确 shape:
修法: Test(新 1 单测):
兼容性 / 回归
待办Path 3/3 用户 final verdict 待启。 |
User T-10a Path 3/3 absorb (PR #81 REQUEST-CHANGES, 0 CRIT, 1 HIGH): HIGH-1 — spec-1.21 D-6 DiagnoseConfig 未落地, per-turn RequestTimeout 未接配置: - spec 明确 DiagnoseConfig{MaxTurns, ToolTimeout, TotalTimeout} 5-tag 全 字段 + per-turn LLM = LLMConfig.RequestTimeout. - 旧实现: config.Config 无 Diagnose 段 (config.go:32); newChatModel 只传 Registry 不传 4 timeouts (tui_launcher.go:194); Loop 走内部 60s ReqTimeout 默认, 与 cfg.LLM.RequestTimeout (默认 30s) 脱钩. - 影响: 用户 llm.request_timeout 配置对 Loop 无效; diagnose.max_turns/ tool_timeout/total_timeout 根本无法配置; DoD § DiagnoseConfig 全 5-tag + 跨字段 validate 未满足. 修法: - internal/platform/config/config.go: 新 DiagnoseConfig{MaxTurns int, ToolTimeout time.Duration, TotalTimeout time.Duration}, 全 5-tag (yaml/json/env/validate; redact 不适用); Config 加 Diagnose DiagnoseConfig 段. - internal/platform/config/defaults.go: spec-1.21 D-6 默认 16 / 30s / 10min. - internal/platform/config/validation.go: 加 validateCrossField helper + 单一 invariant — ToolTimeout ≤ TotalTimeout (spec-1.21 D-6 跨字段; per-field tag grammar 不能表达, 单独做 hook 比扩 walker 干净). - internal/bootstrap/tui_launcher.go: newChatModel 4 字段全接: * MaxTurns: cfg.Diagnose.MaxTurns * ToolTimeout: cfg.Diagnose.ToolTimeout * TotalTimeout: cfg.Diagnose.TotalTimeout * ReqTimeout: cfg.LLM.RequestTimeout (复用 LLMConfig 不重复字段, spec "per-turn LLM 超时复用 LLMConfig.RequestTimeout" 契约) 测试 (5 新): - TestDefault_DiagnoseValues — defaults 16/30s/10min pin. - TestValidate_Diagnose_MaxTurnsBounds — table-driven 0/1/100/101 边界 (min=1, max=100, spec D-6 防 runaway loop). - TestValidate_Diagnose_CrossFieldTimeouts — ToolTimeout > TotalTimeout → reject; ToolTimeout == TotalTimeout → accept (≤ contract, 非严格 <). - TestApplyENV_DiagnoseOverrides — 3 env vars (int + 2 duration) 全走 reflective env walker. - TestNewChatModel_PropagatesDiagnoseConfig — bootstrap wiring smoke under env-overridden Diagnose + RequestTimeout, 防 wiring 未来回归. 回归: - EnvMap_HasExpectedEntries 加 3 entry. - existing Validate(Default()) 通过 (defaults 全 valid + cross-field). make gate 全绿; race detector 全绿. T-10a 三路全闭合, T-10b FROZEN 待启. Spec: spec-1.21-diagnose-loop.md
T-10a Path 3/3 R-fix absorb — user HIGH-1 已修Source: user Path 3/3 verdict — REQUEST-CHANGES, 0 CRIT, 1 HIGH. HIGH-1 absorb — spec-vs-impl 偏离:DiagnoseConfig 未落地问题证据(用户路径):
影响:用户 修法config:
bootstrap: MaxTurns: cfg.Diagnose.MaxTurns,
ToolTimeout: cfg.Diagnose.ToolTimeout,
TotalTimeout: cfg.Diagnose.TotalTimeout,
ReqTimeout: cfg.LLM.RequestTimeout, // 复用 LLMConfig 不重复字段Tests(5 新)
回归
当前状态T-10a 三路全闭合:
等用户 final APPROVE → T-10b(spec FROZEN + 双仓 tag |
spec-1.21 T-3 implementation. spec design FROZEN @ APPROVED R2.1 + § 12.1 errata (gate 解锁) in opendbrb.
T-3 范围
后续
T-4 (anthropic 多轮 toMessages) / T-5 (ToolExecutor + clock/echo) / T-6 (diagnose.Loop) / T-7..T-9 后续 PR 接续。
Spec: spec-1.21-diagnose-loop.md
Spec branch (opendbrb): spec-1.21-diagnose-loop @ c2bfbe2 (§ 12.1 errata)