fix(cabi): applyStageResult 支持 diff 回传(plan 11.3 配套)

外部插件 bridge 模板改为只回传变更字段后(SDK 仓 5648519),内核侧配套:
- applyStageResult 的 tool_calls/tool_results 去掉 len(v)>0 拦截——改为键存在即应用,
  使插件「清空全部工具调用」的显式回传 [] 能被表达(旧插件仅 len>0 才带键,不误清空)
- 逐字段应用,未回传的键保持原值(diff 语义:只改变更字段,不覆盖他人改写)
- output_test.go 新增 TestApplyStageResult_ClearedSlicesAreApplied / _OnlyPresentKeysApplied

验证: go build exit 0; go test ./internal/plugin/... ./internal/agent/... 全绿
This commit is contained in:
dev
2026-08-31 12:30:04 +08:00
parent b74ee15321
commit 9bb9cb3b1a
2 changed files with 49 additions and 3 deletions

View File

@ -360,7 +360,10 @@ func applyStageResult(sc *sdk.StageContext, resultJSON string) {
vv := v
sc.Response = &vv
}
if v, ok := m["tool_calls"].([]interface{}); ok && len(v) > 0 {
if v, ok := m["tool_calls"].([]interface{}); ok {
// 注意不要加 len(v)>0 条件ABI v2 diff 回传plan.md 11.3)下,插件拒绝全部
// 工具调用时会显式回传 `[]`,必须能表达「清空」。旧插件(全量回传)仅在
// len>0 时才带该键,因此不会因此变更而被误清空。
if b, err := json.Marshal(v); err == nil {
var tcs []sdk.ToolCall
if json.Unmarshal(b, &tcs) == nil {
@ -368,7 +371,7 @@ func applyStageResult(sc *sdk.StageContext, resultJSON string) {
}
}
}
if v, ok := m["tool_results"].([]interface{}); ok && len(v) > 0 {
if v, ok := m["tool_results"].([]interface{}); ok {
if b, err := json.Marshal(v); err == nil {
var trs []sdk.ToolResult
if json.Unmarshal(b, &trs) == nil {

View File

@ -5,9 +5,11 @@ import (
"strings"
"testing"
"time"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
// awaitOutputResult 的 decision 核心:
// output_send 不再假成功plan.md 11.1sent / error / unconfirmed 三态。
func TestAwaitOutputResult_Success(t *testing.T) {
res, err := awaitOutputResultWith(0, "qq", `{"x":1}`, func(pid int32, ch, args string) error {
@ -47,3 +49,44 @@ func TestAwaitOutputResult_Timeout(t *testing.T) {
t.Fatalf("expected status=unconfirmed, got %v", m["status"])
}
}
// applyStageResult 必须能表达「插件清空了 tool_calls/tool_results」——
// ABI v2 diff 回传plan.md 11.3)下插件拒绝全部工具调用时会显式回传 []。
func TestApplyStageResult_ClearedSlicesAreApplied(t *testing.T) {
sc := &sdk.StageContext{
ToolCalls: []sdk.ToolCall{{ID: "t1", Name: "cmd_run"}},
ToolResults: []sdk.ToolResult{{CallID: "t1", Name: "cmd_run", Result: "x"}},
}
applyStageResult(sc, `{"tool_calls":[],"tool_results":[]}`)
if len(sc.ToolCalls) != 0 {
t.Fatalf("tool_calls 应被清空,实际 %v", sc.ToolCalls)
}
if len(sc.ToolResults) != 0 {
t.Fatalf("tool_results 应被清空,实际 %v", sc.ToolResults)
}
}
// diff 回传只带变更字段:未出现的键不得被改动(避免旧快照覆盖)。
func TestApplyStageResult_OnlyPresentKeysApplied(t *testing.T) {
sc := &sdk.StageContext{
RawMessage: "原始输入",
LLMText: "原始LLM",
FinalText: "原始最终",
ToolResults: []sdk.ToolResult{{CallID: "c1", Result: "已清洗"}},
}
// 只回传 final_text 的变更
applyStageResult(sc, `{"final_text":"新最终"}`)
if sc.FinalText != "新最终" {
t.Fatalf("final_text 应被应用,实际 %q", sc.FinalText)
}
if sc.RawMessage != "原始输入" {
t.Errorf("raw_message 未回传却被改动: %q", sc.RawMessage)
}
if sc.LLMText != "原始LLM" {
t.Errorf("llm_text 未回传却被改动: %q", sc.LLMText)
}
if len(sc.ToolResults) != 1 || sc.ToolResults[0].Result != "已清洗" {
t.Errorf("tool_results 未回传却被改动: %v", sc.ToolResults)
}
}