Add R4 escalation reminders and the structured source-id evidence ledger (E4)
ci / vet-and-test (pull_request) Successful in 43s
ci / vet-and-test (pull_request) Successful in 43s
Why: The plan's E4 remainder under the MiniMax-only constraint: the harness must detect the model going in circles and inject a change-of-strategy instruction (R4 - the model cannot be trusted to notice its own repeat-fail loop), and number sourcing needs addressable evidence: tool results carry [src_N|tool] tags so "found in materials" becomes "found in source src_N" for both the model and the fact-check notice. Why: Failure counting lives in one place (onEvent end, keyed by the slot status) because tool execution is parallel; the ledger and tag append are serialized by the same mutex, and escalation reminders fire once per condition streak - following the dual-speed nudge finding that MiniMax ignores vague reminders but follows concrete instructions. Evidence: go test ./... green; new anchors TestEscalationOnConsecutive ToolFailures, TestEscalationOnZeroHitSearches, TestEvidenceLedger TagsSource, TestEvidenceLedgerSkipsNonEvidenceTools. Live before/ after comparison runs on the Dev side per ruling 6. Impact: knowledge/web/tavily searches, fs_read_file and the workspace read_file/grep results now end with a source tag and enter the run ledger (2k-char head); two consecutive tool failures or two empty search results inject a concrete change-of-strategy reminder; base.deliverables v7 and agent-sub-research v3 explain the tags; fact-check notice points at src results. Non-evidence tools and failures are unchanged. Refs: proposal 2026-10-10-agent-delegation-routing-sandbox.md (E4, R4); Dev task feature/agent-e4-escalation-ledger.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: agent.sub.research
|
id: agent.sub.research
|
||||||
layer: agent
|
layer: agent
|
||||||
version: 2
|
version: 3
|
||||||
title: 调研子代理
|
title: 调研子代理
|
||||||
description: 围绕问题多源调研,输出带依据的结论
|
description: 围绕问题多源调研,输出带依据的结论
|
||||||
tools: knowledge_search, web_search, tavily_search, time_now, list_conversations, read_conversation, fs_read_file, fs_list_dir
|
tools: knowledge_search, web_search, tavily_search, time_now, list_conversations, read_conversation, fs_read_file, fs_list_dir
|
||||||
@@ -9,7 +9,7 @@ model: lightweight
|
|||||||
---
|
---
|
||||||
你是调研子代理,围绕给定问题收集信息并给出结论。
|
你是调研子代理,围绕给定问题收集信息并给出结论。
|
||||||
|
|
||||||
- 结论里的关键数字与事实必须附原文引用(材料/文件与页/段落定位,重要处引原句);没有出处的数字不要写,无法回溯时明确说明「来源缺失」而不是估一个。
|
- 结论里的关键数字与事实必须附原文引用(材料/文件与页/段落定位,重要处引原句);没有出处的数字不要写,无法回溯时明确说明「来源缺失」而不是估一个。检索/读取结果带【src_N|工具】标记时,引用同时给出该标记编号。
|
||||||
- 先把问题拆成可回答的子问题,再逐一检索;优先项目内知识库与已给材料,其次按任务指定的外部来源。
|
- 先把问题拆成可回答的子问题,再逐一检索;优先项目内知识库与已给材料,其次按任务指定的外部来源。
|
||||||
- 每条结论标注依据来源;多来源冲突时不和稀泥,列出各方说法与差异点,说明你采信哪个及理由。
|
- 每条结论标注依据来源;多来源冲突时不和稀泥,列出各方说法与差异点,说明你采信哪个及理由。
|
||||||
- 覆盖正反两面:支持与不支持该结论的证据都要呈现。
|
- 覆盖正反两面:支持与不支持该结论的证据都要呈现。
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: base.deliverables
|
id: base.deliverables
|
||||||
layer: base
|
layer: base
|
||||||
version: 6
|
version: 7
|
||||||
title: 交付物工作流
|
title: 交付物工作流
|
||||||
description: 生成文档、表格、演示稿的工作流、样式决策与技能入口(常驻基座)
|
description: 生成文档、表格、演示稿的工作流、样式决策与技能入口(常驻基座)
|
||||||
---
|
---
|
||||||
@@ -16,7 +16,7 @@ description: 生成文档、表格、演示稿的工作流、样式决策与技
|
|||||||
5. **登记**:`register_artifact`(path 传相对路径)。登记后对话里出现可预览的卡片。
|
5. **登记**:`register_artifact`(path 传相对路径)。登记后对话里出现可预览的卡片。
|
||||||
6. **收尾**:回复只写结论摘要,并说明这次用的样式(主题、是否模板、是否仿制)以及用户还能怎么调。不要在回复里复述全文,也不要只口头说文件名。
|
6. **收尾**:回复只写结论摘要,并说明这次用的样式(主题、是否模板、是否仿制)以及用户还能怎么调。不要在回复里复述全文,也不要只口头说文件名。
|
||||||
|
|
||||||
数字溯源(硬性):交付物里的每一个百分比、日期和大额数字,都必须能在本轮上传材料或你自己的工具/计算输出里找到出处;找不到出处的,要么删除,要么用「目标 / 预计 / 假设」等词在文中显式标注为规划量(如「目标 85%」「预计三个月」)。终轮会对无出处且未标注的数字拦截两轮;仍无法回溯又拒绝标注的,会被系统如实记为未核实。
|
检索与读取的工具结果末尾带【src_N|工具名】来源标记:成文时数字与关键事实的出处就是这些带标记的结果,逐项核对;交付物里的每一个百分比、日期和大额数字,都必须能在本轮上传材料或你自己的工具/计算输出里找到出处;找不到出处的,要么删除,要么用「目标 / 预计 / 假设」等词在文中显式标注为规划量(如「目标 85%」「预计三个月」)。终轮会对无出处且未标注的数字拦截两轮;仍无法回溯又拒绝标注的,会被系统如实记为未核实。
|
||||||
|
|
||||||
没有调用工具、没有登记工件时,禁止说「已生成 / 已保存 / 已导出」。回复里不要写工件 ID(art_…)。工具只能通过函数调用触发,不要在回复里书写 `[调用工具 …]`、`[工具结果 …]`、`[产出工件 …]` 这类文字。
|
没有调用工具、没有登记工件时,禁止说「已生成 / 已保存 / 已导出」。回复里不要写工件 ID(art_…)。工具只能通过函数调用触发,不要在回复里书写 `[调用工具 …]`、`[工具结果 …]`、`[产出工件 …]` 这类文字。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
core "agent-system-core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- E4:R4 升级提醒 ----
|
||||||
|
|
||||||
|
// 两连败后 reminders 注入换路指令;成功一次即重置、可再次触发。
|
||||||
|
func TestEscalationOnConsecutiveToolFailures(t *testing.T) {
|
||||||
|
f := &fakeRunCtx{ctx: context.Background(), prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}, execStatus: "error", execOutput: "boom"}
|
||||||
|
r := &LLMRunner{Stream: scriptStream(padTurns(
|
||||||
|
textDeltaTurn("", toolCall("run_python", `{"code":"1"}`), toolCall("run_python", `{"code":"2"}`)),
|
||||||
|
textDeltaTurn("换路了。"),
|
||||||
|
), nil)}
|
||||||
|
if err := r.Execute(f); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// 完整 Execute 冒烟(真实事件路径跑通)+ 状态机断言(onEvent 终点计数
|
||||||
|
// → escalationReminders 文案)。
|
||||||
|
st2 := &directRun{l: &LLMRunner{}, inflight: map[string]*toolSlot{}}
|
||||||
|
st2.noteFailureOutcome("error")
|
||||||
|
if got := st2.escalationReminders(); len(got) != 0 {
|
||||||
|
t.Fatalf("single failure must not escalate: %v", got)
|
||||||
|
}
|
||||||
|
st2.noteFailureOutcome("error")
|
||||||
|
got := st2.escalationReminders()
|
||||||
|
if len(got) != 1 || !strings.Contains(got[0], "停止原样重试") {
|
||||||
|
t.Fatalf("two consecutive failures must escalate: %v", got)
|
||||||
|
}
|
||||||
|
if again := st2.escalationReminders(); len(again) != 0 {
|
||||||
|
t.Fatalf("escalation must be once per streak: %v", again)
|
||||||
|
}
|
||||||
|
st2.noteFailureOutcome("ok")
|
||||||
|
if st2.consecToolFailures != 0 || st2.failStreakNoted {
|
||||||
|
t.Fatal("success must reset the failure streak")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检索连续空结果(成功但无实质内容)触发「换关键词或如实写缺失」。
|
||||||
|
func TestEscalationOnZeroHitSearches(t *testing.T) {
|
||||||
|
f := &fakeRunCtx{ctx: context.Background(), prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}, execOutput: "未找到相关内容"}
|
||||||
|
r := &LLMRunner{Stream: scriptStream(padTurns(
|
||||||
|
textDeltaTurn("", toolCall("knowledge_search", `{"query":"a"}`), toolCall("knowledge_search", `{"query":"b"}`)),
|
||||||
|
textDeltaTurn("好了。"),
|
||||||
|
), nil)}
|
||||||
|
if err := r.Execute(f); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
st := &directRun{l: &LLMRunner{}, inflight: map[string]*toolSlot{}}
|
||||||
|
st.noteEvidenceCall("knowledge_search", "ok", "未找到相关内容")
|
||||||
|
if got := st.escalationReminders(); len(got) != 0 {
|
||||||
|
t.Fatalf("one zero hit must not escalate: %v", got)
|
||||||
|
}
|
||||||
|
st.noteEvidenceCall("knowledge_search", "ok", "无结果")
|
||||||
|
got := st.escalationReminders()
|
||||||
|
if len(got) != 1 || !strings.Contains(got[0], "材料中无此信息") {
|
||||||
|
t.Fatalf("two zero hits must escalate: %v", got)
|
||||||
|
}
|
||||||
|
// 有实质结果后计数重置。
|
||||||
|
st.noteEvidenceCall("knowledge_search", "ok", "营收 3,200 万元")
|
||||||
|
if st.zeroHitSearches != 0 {
|
||||||
|
t.Fatal("substantive hit must reset zero-hit count")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- E4:结构化 source_id 账本 ----
|
||||||
|
|
||||||
|
func TestEvidenceLedgerTagsSource(t *testing.T) {
|
||||||
|
f := &fakeRunCtx{ctx: context.Background(), prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}, execOutput: "上半年营收 3,200 万元。"}
|
||||||
|
r := &LLMRunner{Stream: scriptStream(padTurns(
|
||||||
|
textDeltaTurn("", toolCall("knowledge_search", `{"query":"营收"}`)),
|
||||||
|
textDeltaTurn("总结完成。"),
|
||||||
|
), nil)}
|
||||||
|
if err := r.Execute(f); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// 结果文本必须带来源标记,且账本登记了对应条目(直接经 runTool 断言)。
|
||||||
|
st := &directRun{l: &LLMRunner{}, rc: f, eff: f.Settings(), inflight: map[string]*toolSlot{}}
|
||||||
|
res := st.runTool("knowledge_search", `{}`)
|
||||||
|
if !strings.Contains(res.Content, "【src_1|knowledge_search】") {
|
||||||
|
t.Fatalf("tool result must carry the source tag: %.120s", res.Content)
|
||||||
|
}
|
||||||
|
if len(st.ledger) != 1 || st.ledger[0].ID != "src_1" || !strings.Contains(st.ledger[0].Text, "3,200") {
|
||||||
|
t.Fatalf("ledger = %+v", st.ledger)
|
||||||
|
}
|
||||||
|
// 第二次登记递增编号。
|
||||||
|
res2 := st.runTool("fs_read_file", `{}`)
|
||||||
|
if !strings.Contains(res2.Content, "【src_2|fs_read_file】") {
|
||||||
|
t.Fatalf("second tag must be src_2: %.120s", res2.Content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非证据工具不登记、不加标记。
|
||||||
|
func TestEvidenceLedgerSkipsNonEvidenceTools(t *testing.T) {
|
||||||
|
f := &fakeRunCtx{}
|
||||||
|
st := &directRun{l: &LLMRunner{}, rc: f, eff: f.Settings(), inflight: map[string]*toolSlot{}}
|
||||||
|
res := st.runTool("time_now", `{}`)
|
||||||
|
if strings.Contains(res.Content, "src_") || len(st.ledger) != 0 {
|
||||||
|
t.Fatalf("non-evidence tool must not enter the ledger: %.80s %+v", res.Content, st.ledger)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package runtime
|
package runtime
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -121,7 +122,7 @@ func factCheckNotice(tokens []string) string {
|
|||||||
}
|
}
|
||||||
b.WriteString(tok)
|
b.WriteString(tok)
|
||||||
}
|
}
|
||||||
b.WriteString("。逐条二选一:改成材料里能找到的数值,或删除/明确标注为推测或目标值(如“目标 85%”)。不要保留无法回溯的裸数字。")
|
b.WriteString("。逐条二选一:改成材料或来源结果(检索/读取输出带【src_N|…】标记)里能找到的数值,或删除/明确标注为推测或目标值(如“目标 85%”)。不要保留无法回溯的裸数字。")
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,3 +232,106 @@ func (st *directRun) factCheckGate(tokens []string) (notice string, cont bool) {
|
|||||||
}
|
}
|
||||||
return notice, true
|
return notice, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- E4:证据账本与 R4 升级提醒 ----
|
||||||
|
|
||||||
|
// evidenceEntry 是账本的一条来源记录(正文截头保存;全文在转录与工具证据里)。
|
||||||
|
type evidenceEntry struct {
|
||||||
|
ID string
|
||||||
|
Tool string
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
// evidenceTools 登记进 source_id 账本的证据类工具(读检索面)。
|
||||||
|
var evidenceTools = map[string]bool{
|
||||||
|
"knowledge_search": true,
|
||||||
|
"web_search": true,
|
||||||
|
"tavily_search": true,
|
||||||
|
"fs_read_file": true,
|
||||||
|
"read_file": true, // 内核工作区读(直连路径实际使用的名字)
|
||||||
|
"grep": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// escalationFailureThreshold 是连续工具失败的换路提醒阈值(R4)。
|
||||||
|
const escalationFailureThreshold = 2
|
||||||
|
|
||||||
|
// tagSource 把一次成功的证据工具输出登记进账本,返回给模型看的来源标记
|
||||||
|
// (如 src_3|knowledge_search)。并行执行下由 toolMu 串行化计数与追加。
|
||||||
|
func (st *directRun) tagSource(tool, text string) (string, bool) {
|
||||||
|
st.toolMu.Lock()
|
||||||
|
defer st.toolMu.Unlock()
|
||||||
|
st.ledgerCount++
|
||||||
|
id := fmt.Sprintf("src_%d", st.ledgerCount)
|
||||||
|
head := []rune(text)
|
||||||
|
if len(head) > 2000 { // 账本只存索引性头部,全文在转录/证据里
|
||||||
|
head = head[:2000]
|
||||||
|
}
|
||||||
|
st.ledger = append(st.ledger, evidenceEntry{ID: id, Tool: tool, Text: string(head)})
|
||||||
|
return id + "|" + tool, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// noteEvidenceCall 维护 R4 的检索空命中计数(成功但无实质内容的证据类
|
||||||
|
// 工具调用)。工具成败计数不在这里——并行下以 onEvent 终点的槽位状态为
|
||||||
|
// 单一事实源(noteFailureOutcome),避免两处重复计数。
|
||||||
|
func (st *directRun) noteEvidenceCall(name, status, output string) {
|
||||||
|
if status != "ok" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
st.toolMu.Lock()
|
||||||
|
defer st.toolMu.Unlock()
|
||||||
|
trimmed := strings.TrimSpace(output)
|
||||||
|
if evidenceTools[name] && (trimmed == "" || containsAny(trimmed, zeroHitMarkers)) {
|
||||||
|
st.zeroHitSearches++
|
||||||
|
} else if evidenceTools[name] {
|
||||||
|
st.zeroHitSearches = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// zeroHitMarkers 是检索结果「无命中」的常见表述(按工具输出原文判别)。
|
||||||
|
var zeroHitMarkers = []string{"未找到", "没有找到", "无结果", "无相关", "暂无匹配", "No results"}
|
||||||
|
|
||||||
|
func containsAny(s string, subs []string) bool {
|
||||||
|
for _, sub := range subs {
|
||||||
|
if strings.Contains(s, sub) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// escalationReminders 返回待注入的换路提醒并清标记(每类条件一次,
|
||||||
|
// 连续失败再次触发时会重新提醒——失败计数在成功后重置,streak 标记随重置)。
|
||||||
|
// 文案直接给动作:MiniMax 对泛泛提醒不敏感(实测),对具体指令照办。
|
||||||
|
func (st *directRun) escalationReminders() []string {
|
||||||
|
var out []string
|
||||||
|
st.toolMu.Lock()
|
||||||
|
fails, failNoted := st.consecToolFailures, st.failStreakNoted
|
||||||
|
zero, zeroNoted := st.zeroHitSearches, st.zeroHitNoted
|
||||||
|
if fails >= escalationFailureThreshold && !failNoted {
|
||||||
|
st.failStreakNoted = true
|
||||||
|
}
|
||||||
|
if zero >= escalationFailureThreshold && !zeroNoted {
|
||||||
|
st.zeroHitNoted = true
|
||||||
|
}
|
||||||
|
st.toolMu.Unlock()
|
||||||
|
if fails >= escalationFailureThreshold && !failNoted {
|
||||||
|
out = append(out, fmt.Sprintf("工具已连续失败 %d 次:停止原样重试。逐条重读最近一次错误信息,改参数、换工具或换路径再试;确认无法绕过时,如实在答复里说明卡点与已尝试的手段,不要编造结果。", fails))
|
||||||
|
}
|
||||||
|
if zero >= escalationFailureThreshold && !zeroNoted {
|
||||||
|
out = append(out, "检索已连续多次无结果:换关键词/换检索维度再试一次;仍无结果就如实写「材料中无此信息」,不要凭印象补数字。")
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// noteFailureOutcome 供 onEvent 在工具结束时同步成败计数(并行下事件乱序,
|
||||||
|
// 以 runToolCtx 已写入槽位的 status 为准)。
|
||||||
|
func (st *directRun) noteFailureOutcome(status string) {
|
||||||
|
st.toolMu.Lock()
|
||||||
|
defer st.toolMu.Unlock()
|
||||||
|
if status == "error" {
|
||||||
|
st.consecToolFailures++
|
||||||
|
} else {
|
||||||
|
st.consecToolFailures = 0
|
||||||
|
st.failStreakNoted = false // 新失败链可再触发提醒
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestUnverifiedFactsAndNotice(t *testing.T) {
|
|||||||
t.Fatalf("sourced figure must not be reported: %v", got)
|
t.Fatalf("sourced figure must not be reported: %v", got)
|
||||||
}
|
}
|
||||||
notice := factCheckNotice(got)
|
notice := factCheckNotice(got)
|
||||||
if !strings.Contains(notice, "95%") || !strings.Contains(notice, "改成材料里") {
|
if !strings.Contains(notice, "95%") || !strings.Contains(notice, "改成材料或来源结果") {
|
||||||
t.Fatalf("notice malformed: %q", notice)
|
t.Fatalf("notice malformed: %q", notice)
|
||||||
}
|
}
|
||||||
if UnverifiedFacts("", material, nil) != nil || UnverifiedFacts(artifact, "", nil) != nil {
|
if UnverifiedFacts("", material, nil) != nil || UnverifiedFacts(artifact, "", nil) != nil {
|
||||||
|
|||||||
@@ -62,6 +62,18 @@ type directRun struct {
|
|||||||
nudgedPlanless bool
|
nudgedPlanless bool
|
||||||
factCheckRounds int // E3:事实核查修正轮计数(上限 factCheckMaxRounds)
|
factCheckRounds int // E3:事实核查修正轮计数(上限 factCheckMaxRounds)
|
||||||
factCheckPrev map[string]bool // E3:上一轮的未核实 token 集合(未变化则不再拦)
|
factCheckPrev map[string]bool // E3:上一轮的未核实 token 集合(未变化则不再拦)
|
||||||
|
// E4(R4 升级规则):harness 侧触发的换路提醒——不依赖模型自觉发现
|
||||||
|
// 自己在原地打转。成败计数在 onEvent 终点统一维护(noteFailureOutcome),
|
||||||
|
// 达阈值注入一次换路指令;检索连续空结果同理(noteEvidenceCall)。
|
||||||
|
consecToolFailures int
|
||||||
|
failStreakNoted bool
|
||||||
|
zeroHitSearches int
|
||||||
|
zeroHitNoted bool
|
||||||
|
// E4(结构化 source_id 账本):证据类工具的成功结果登记为带 ID 的
|
||||||
|
// 条目,工具输出尾注【src_N|工具】标记——数字溯源的「材料/来源」
|
||||||
|
// 从此有可指地址。
|
||||||
|
ledger []evidenceEntry
|
||||||
|
ledgerCount int
|
||||||
progress *gates.ProgressReminder
|
progress *gates.ProgressReminder
|
||||||
checks []core.CheckPart
|
checks []core.CheckPart
|
||||||
servedModel string
|
servedModel string
|
||||||
@@ -162,6 +174,12 @@ func (l *LLMRunner) runDirect(rc RunCtx, stream agentllm.StreamFunc, model strin
|
|||||||
reg.Register(directTool{name: name, run: func(ctx context.Context, args json.RawMessage) agenttools.Result {
|
reg.Register(directTool{name: name, run: func(ctx context.Context, args json.RawMessage) agenttools.Result {
|
||||||
res := wsReg.Execute(ctx, name, args)
|
res := wsReg.Execute(ctx, name, args)
|
||||||
st.noteKernelResult(ctx, res)
|
st.noteKernelResult(ctx, res)
|
||||||
|
// E4:证据类工作区工具(read_file/grep)同样登记账本并尾注来源标记。
|
||||||
|
if !res.IsError && evidenceTools[name] && strings.TrimSpace(res.Content) != "" {
|
||||||
|
if tag, ok := st.tagSource(name, res.Content); ok {
|
||||||
|
res.Content += "\n【" + tag + "】"
|
||||||
|
}
|
||||||
|
}
|
||||||
return res
|
return res
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
@@ -553,6 +571,7 @@ func (st *directRun) onEvent(ev nruntime.Event) {
|
|||||||
}
|
}
|
||||||
st.evidence = append(st.evidence, toolEvidence{Name: slot.call.Name, Status: slot.status, Summary: slot.summary, VerifyText: slot.verifyText})
|
st.evidence = append(st.evidence, toolEvidence{Name: slot.call.Name, Status: slot.status, Summary: slot.summary, VerifyText: slot.verifyText})
|
||||||
st.toolMu.Unlock()
|
st.toolMu.Unlock()
|
||||||
|
st.noteFailureOutcome(slot.status)
|
||||||
_ = st.rc.Emit(core.EvToolResult, core.ToolResultPayload{CallID: slot.call.ID, Status: slot.status, Summary: slot.summary, Truncated: slot.truncated, DurationMs: durationMs(slot.start, end)})
|
_ = st.rc.Emit(core.EvToolResult, core.ToolResultPayload{CallID: slot.call.ID, Status: slot.status, Summary: slot.summary, Truncated: slot.truncated, DurationMs: durationMs(slot.start, end)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -816,6 +835,14 @@ func (st *directRun) runToolCtx(ctx context.Context, name, args string) agenttoo
|
|||||||
} else {
|
} else {
|
||||||
out = res.Output
|
out = res.Output
|
||||||
}
|
}
|
||||||
|
// E4:R4 空命中计数 + 证据账本登记(成功且非空的证据类工具输出
|
||||||
|
// 尾注【src_N|工具】来源标记——数字溯源从此有可指地址)。
|
||||||
|
st.noteEvidenceCall(call.Name, res.Status, out)
|
||||||
|
if res.Status == "ok" && evidenceTools[call.Name] && strings.TrimSpace(out) != "" {
|
||||||
|
if tag, ok := st.tagSource(call.Name, out); ok {
|
||||||
|
out += "\n【" + tag + "】"
|
||||||
|
}
|
||||||
|
}
|
||||||
if res.Artifact != nil && slot != nil {
|
if res.Artifact != nil && slot != nil {
|
||||||
slot.artifact = &core.ArtifactRef{ArtifactID: res.Artifact.ID, Name: res.Artifact.Name, Kind: res.Artifact.Kind}
|
slot.artifact = &core.ArtifactRef{ArtifactID: res.Artifact.ID, Name: res.Artifact.Name, Kind: res.Artifact.Kind}
|
||||||
if st.cardflow && res.Truncated && !st.explicitCard {
|
if st.cardflow && res.Truncated && !st.explicitCard {
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ type fakeRunCtx struct {
|
|||||||
runtimeExtra map[string]any // 覆盖 runtime 组设置(双速提醒阈值等测试用)
|
runtimeExtra map[string]any // 覆盖 runtime 组设置(双速提醒阈值等测试用)
|
||||||
attachments []core.ParsedDocument // Attachments 返回值(双速入口前置提示测试用)
|
attachments []core.ParsedDocument // Attachments 返回值(双速入口前置提示测试用)
|
||||||
spawnAgentFn func(agentID, name, task, hint string) (core.AgentResultPayload, error)
|
spawnAgentFn func(agentID, name, task, hint string) (core.AgentResultPayload, error)
|
||||||
|
execStatus string // 非空时覆盖 ExecuteTool 状态(R4 失败链测试)
|
||||||
|
execOutput string // 非空时覆盖 ExecuteTool 输出(空命中/账本测试)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeRunCtx) Context() context.Context { return f.ctx }
|
func (f *fakeRunCtx) Context() context.Context { return f.ctx }
|
||||||
@@ -103,6 +105,12 @@ func (f *fakeRunCtx) Retrieve(q core.RetrievalQuery) ([]core.RetrievalHit, error
|
|||||||
func (f *fakeRunCtx) ExecuteTool(call core.ToolCallRequest) core.ToolCallResult {
|
func (f *fakeRunCtx) ExecuteTool(call core.ToolCallRequest) core.ToolCallResult {
|
||||||
f.toolRuns++
|
f.toolRuns++
|
||||||
res := core.ToolCallResult{Status: "ok", Output: "done", Truncated: f.execTruncated}
|
res := core.ToolCallResult{Status: "ok", Output: "done", Truncated: f.execTruncated}
|
||||||
|
if f.execStatus != "" {
|
||||||
|
res.Status = f.execStatus
|
||||||
|
}
|
||||||
|
if f.execOutput != "" {
|
||||||
|
res.Output = f.execOutput
|
||||||
|
}
|
||||||
if f.execArtifact != nil {
|
if f.execArtifact != nil {
|
||||||
res.Artifact = f.execArtifact
|
res.Artifact = f.execArtifact
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,11 @@ func (st *directRun) reminders(turn int) []string {
|
|||||||
if note := st.dualSpeedNudge(); note != "" {
|
if note := st.dualSpeedNudge(); note != "" {
|
||||||
return []string{note}
|
return []string{note}
|
||||||
}
|
}
|
||||||
|
// E4(R4):harness 检测到的换路条件优先于计划提醒——原地打转时
|
||||||
|
// 计划提醒没有意义。
|
||||||
|
if notes := st.escalationReminders(); len(notes) > 0 {
|
||||||
|
return notes
|
||||||
|
}
|
||||||
if st.toolTurnsSince < gates.ReminderToolGap {
|
if st.toolTurnsSince < gates.ReminderToolGap {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user