Commit Graph

3 Commits

Author SHA1 Message Date
41926e770d fix(lua): 适配器永久落后 —— 修死代码 + 改为显式 opt-in 同步
## 缺陷:适配器会永久卡在旧版

writeBundledAdapters 用清单里的 prevHash 判断「盘上有没有被人动过」,
被动的那个就保留。但清单只存 hash ——「内置旧版本的 hash」与
「用户改过的 hash」在清单里长得一模一样。于是:

    记了盘上那份当新基线 ⇒ 下次 onDisk == prevHash
      ⇒ 被误判成「没跟上新版本」⇒ 覆盖掉用户改动
    而保留分支里的 cur[name] = onDisk 又会

## 被循环末尾无条件 `cur[name] = sum` 冲掉

⇒ 判「用户改过」后清单仍记内置 hash ⇒ 下次再判一遍「用户改过」
⇒ **永久**停在旧版。日志只有一句「保留用户版本」,
看起来像用户自己的选择,完全看不出是卡住。

本机实测 8 个适配器中招(openai/anthropic/github/groq/mistral/ollama/
kimicode/server),落后内置版 3000~6500 字节,缺的是用量归一化
(cache_read / reasoning_tokens)、流式 tool_calls 等真实修复。
症状最终表现为「模型不听话」「工具调不动」——指不到适配器身上。

## 为什么不能靠「自动覆盖」解决

「旧发行版残留」与「用户手工改过」在文件内容上无法区分(都只是
hash 不等于内置版)。擅自改用户盘上的文件,比落后一个版本更糟。

所以改成**显式 opt-in**:

    core.lua.adapter_sync = off(默认)  不碰文件,但告警必须可操作
    core.lua.adapter_sync = auto         差异即备份 .bak-<hash8> 后覆盖

零值 = off:忘记注入配置时宁可不动用户文件,也不能默默全量覆盖。

## 告警要可操作,不是「保留用户版本」一句话

教训(2026-10-26 + 2026-11-11 两次):这类静默落后最难查,因为症状
指向模型而不是适配器。现在会给出:数量、名单、影响、三条处理路径
(跟随内核 / 长期定制改 adapter_path / 删掉旧文件)。

长期定制的正道是 `core.llm.sources.<name>.adapter_path`(每源独立可配),
不需要改内置位置的同名文件 —— 这一点也写进告警里。

## 判据 7 条,均经变异验证

    TestAdapterSyncDefaultsToOff            零值/未注入必须是 off
      (变异:return "off" → "auto" ⇒ 红)
    TestOffModeLeavesDiskUntouched          off 不碰文件、不产生备份
      (变异:case mode=="auto" → case mode!="" ⇒ 红)
    TestAutoModeUpdatesWithBackup           auto 更新且备份可回滚
    TestAutoModeNoRedundantBackup           一致时不重复堆 .bak
    TestAdapterStateStableAcrossRestarts    两种模式启停 3 次不漂移
    TestManifestCoversAllBundledAdapters    清单不漏项
    TestFirstInstallHappensInBothModes      off 只「不动已存在文件」,≠不安装

    go build / vet / gofmt / go test -race  全绿

## 生产现状(已按用户确认保留更新)

    适配器 10/10 与内置一致,8 个 .bak-<hash8> 可回滚
    服务 active,NRestarts=0
2026-11-11 21:39:03 +08:00
f7dad243b6 fix(deepseek): reasoning_content 三层贯通 + thinking 关闭层级修正
## 生产故障

    all 2 providers failed, last error: api error 400:
    "The reasoning_content in the thinking mode must be passed back to the API."

## 两个根因,第一个更根本

### ① thinking 关闭一直没生效(真正的触发器)

deepseek.lua 写的是 `req.extra_body.thinking = {type="disabled"}`。
但官方文档里那个 `extra_body={"thinking":...}` 是 **OpenAI SDK 层参数** ——
SDK 会把它摊平进 HTTP 请求体顶层。而适配器是**裸发** JSON 给 api.deepseek.com,
多包一层 `extra_body` 服务端根本不认,直接忽略。

⇒ 即使内核 thinking_enabled=false(发出 disable_thinking=true),
   也从来没真正关掉过思考,每次都走 thinking 模式,每次都撞 reasoning_content 规则。

改为顶层 `req.thinking = {type="disabled"}`。

### ② reasoning 从未被保存和回传

DeepSeek 规则(官方 thinking_mode 指南):请求**带 tools** 时,
所有历史轮的 reasoning_content 都必须传回;内核每轮都带工具定义。

而内核三层全丢:

    L1  ContextEvent 没有该字段;renderL1Messages 渲染 assistant 时不带
    L2  coldcontext 只切 Input/Response 两块,推理内容不进块
    L3  蒸馏原文只读 Input/Response/工具输出

修法(按用户裁定:L1/L2/L3 都要包含 reasoning):

    L1  ContextEvent.ReasoningContent + renderL1Messages 回传
        顺带:textForVector 纳入 reasoning —— 关键线索常只在思考里
        ("用户要的是 A 不是 B,所以先查 C"),只看 Response 会误裁
    L2  RoleReasoning 独立块(不并进 Response:回灌时要放回不同字段,
        合并会让思维链被当成正式回复发给模型)
    L3  coldcontextOriginalText 按**时序** input → reasoning → response

## 一个易错点:不能用 f.Resp.ReasoningContent

f.Resp 只在「本批有工具调用」时赋值(stepLLM 里 len(ToolCalls)==0 就 return)。
纯文本收尾轮拿到的是**上一次工具轮**的旧响应 —— 用它写进 L1 会把上一批的
思维链冒名顶替成本轮的(比缺失更坏)。所以单独一个 TurnReasoning,
由 stepLLM 在分支**之前**无条件写入。

## 适配器兜底(治存量)

内核修了,但**存量会话**的 context.json 是修复前写的,那些事件没这个字段。
不兜底就是死结:历史缺字段 ⇒ 400 ⇒ 会话永久不可用(重启也救不回,
缺的字段在磁盘上的旧记录里)。

transform_request 里:带 tools 时给每条 assistant 补 reasoning_content="",
已存在则不覆盖(不抹掉真实思考内容)。

## 顺手修的既有缺陷

零时间戳时 docBlocksForEntry 用 time.Now() 算块 ID,而 ColdEvent.Timestamp
留零值 ⇒ 哈希永久失配 ⇒ 内容就在那里却一个字段都读不回,且不报错不记日志。
线上因为 L1 总带时间戳没暴露。docEventsFromEntries 里先归一化时间戳。

## 验证

    生产实机(A2A 多轮工具调用,日志确认落到 deepseek 源直连):
      LLM fallback: trying provider "deepseek"
      response (12186ms, tools=[cmd_run plugin_start cmd_run])
      reasoning_content 400 = 0 次

    单元判据 16 条,全部变异验证(改回错误形态即红):
      coldcontext/test/reasoning_contract_test.go   5 条(L2 往返/独立块/
        无思考不造空块/特殊字符/InOut 兼容)
      agent/api/test/reasoning_protocol_test.go     3 条(顶层字段/无思考不发/
        带 tool_calls 仍发)
      lua/test/deepseek_reasoning_test.go           8 条(补空串/不覆盖真实值/
        无 tools 不补/补全部轮/不破其他字段/顶层 thinking/默认不加 thinking)

    go build / vet / gofmt / go test -race  全绿
2026-11-11 21:38:54 +08:00
3420ceca60 test: 按模块导出接口重写黑盒判据(批次 10:agent / lua / ollama / log)
4 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 20 条判据,只调导出接口,输入 → 可观察输出。

## internal/agent —— 人格文件

    TestSaveThenLoadPersonality            Save → Load 内容/路径
    TestLoadMissingPersonalityGivesEmpty   文件不存在 → 空人格
    TestInjectPromptContainsContentAndRules 注入含原文 + 内核说话规则
    TestInjectPromptEmptyPersonalityIsEmpty
    TestPersonaStaleHints                  陈旧措辞给提示;干净文本不报
    TestSavePersonalityCreatesParentDirs   自动建父目录

## internal/lua —— VM 适配器

    TestNewVMExposesAdapterDir             构造参数 → AdapterDir
    TestListAdaptersEmptyThenLoaded        LoadAdapterSource → ListAdapters
    TestLoadAdapterSourceRejectsBadScript
    TestCallTransformRequestUnknownAdapter
    TestGetAdapterEndpointUnknownIsEmpty

## providers/ollama —— httptest 真实 HTTP

    TestGenerateReturnsText                200 → Response.Text
    TestGenerateMarksTruncated             done_reason=length → Truncated
    TestGenerateHTTPError                  5xx → 报错
    TestGenerateRejectsInvalidJSONSchema
    TestNewFallsBackToDefaultModel         空 model 走默认值

**判据写错过一次**:第一版断言"空 model 构造失败",但 `optString`
对空值回退默认,构造成功才是真实契约,按此修正。

## internal/log —— Manager 生命周期

    TestManagerStartStopIsSafe             Start/cancel/Stop 不挂住
    TestManagerStopIsIdempotent            连停两次不 panic
    TestRetentionPoliciesAreDistinct       四个策略常量互异

## 变异验证(改回旧实现必须红)

    agent  SavePersonality 不建父目录  → ★ 文件没落盘
    lua    ListAdapters 恒空           → ★ ListAdapters = [],期望 1 个
    ollama 丢 Truncated               → ★ done_reason=length 未标记 Truncated
    log    Stop 重复 close             → panic: close of closed channel

lua / log 第一版变异分别是文本不匹配与"import 变未使用导致编译失败",
都不算干净的断言红;重做成只改行为后确认有效。

## 验证

    go build ./...                            PASS
    go vet ./...                              PASS
    gofmt -l                                  干净
    python3 .github/scripts/check-ci-gates.py PASS
    go test ./... -count=1                    39 个测试包 / 100 包无测试 / 0 FAIL
    4 个新包 -race                            全绿
    源码目录 _test.go                         0
2026-11-09 14:41:43 +08:00