From 3f2c7c70331184661f4149e793ec2b3408866bee Mon Sep 17 00:00:00 2001 From: weichen Date: Sat, 10 Oct 2026 09:11:18 +0000 Subject: [PATCH 1/3] Nudge long-running turns onto the dual-speed path via the kernel reminder hook Why: the dual-speed slow lane shipped as an observable capability plus soft prompt guidance, but on the real (non-mock) path models routinely grind through heavy document tasks synchronously for minutes with zero user-visible output - the pattern only engages when the model happens to think of it. Evidence: five new unit tests over an injected clock cover fire-after-threshold, quiet-before-threshold, disabled-by-setting, and the replied/delegated guard paths (reminders observed in the captured kernel requests); full go test ./... 32 pkgs green. Real-model run recorded in the Dev task card. Impact: runDirect tracks run start, user-reply, and background-delegation state; reminders() injects a one-shot dual-speed nudge (SR-B10) once elapsed time crosses runtime.dualSpeedNudgeSeconds (default 75s, 0 disables) while the user has neither a reply nor a background lane. New setting registered. Refs: Dev task feature/dual-speed-real-path; SR-B10 (RDM requirements/01 V2.1). --- internal/runtime/dual_speed_nudge_test.go | 142 ++++++++++++++++++++++ internal/runtime/llm_direct.go | 13 +- internal/runtime/llm_test.go | 9 +- internal/runtime/verify.go | 25 ++++ internal/settings/settings.go | 1 + 5 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 internal/runtime/dual_speed_nudge_test.go diff --git a/internal/runtime/dual_speed_nudge_test.go b/internal/runtime/dual_speed_nudge_test.go new file mode 100644 index 0000000..6b16870 --- /dev/null +++ b/internal/runtime/dual_speed_nudge_test.go @@ -0,0 +1,142 @@ +package runtime + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "testing" + "time" + + agentllm "agent-native/runtime/llm" + core "agent-system-core" +) + +// 双速提醒经内核 Reminders 钩子进入下一请求的消息(不是响应部件): +// 这里捕获每次模型请求的原文,检索提醒标记。 +type dualSpeedRecorder struct { + clock *stepClock + turns [][]agentllm.Event + request []string + base agentllm.StreamFunc +} + +// stepClock 每 tick 前进 5 秒;runDirect 起 0 时刻,模型每被调用一次 tick 一格, +// 20 次调用即 100 秒量级,足以压过默认 75 秒阈值。 +type stepClock struct{ step int64 } + +func (c *stepClock) now() time.Time { return time.Unix(0, c.step*int64(5*time.Second)) } +func (c *stepClock) tick() { c.step++ } + +func (rec *dualSpeedRecorder) stream(base agentllm.StreamFunc) agentllm.StreamFunc { + return func(ctx context.Context, model string, msgs []agentllm.Message, tools []agentllm.ToolDecl, opts agentllm.StreamOptions) <-chan agentllm.Event { + rec.clock.tick() + for _, m := range msgs { + if b, err := json.Marshal(m); err == nil { + rec.request = append(rec.request, string(b)) + } + } + return base(ctx, model, msgs, tools, opts) + } +} + +func (rec *dualSpeedRecorder) nudged() bool { + for _, r := range rec.request { + if strings.Contains(r, "任务快慢双速") { + return true + } + } + return false +} + +func runDualSpeed(t *testing.T, turns [][]agentllm.Event, setup func(rc *fakeRunCtx)) *dualSpeedRecorder { + t.Helper() + rec := &dualSpeedRecorder{clock: &stepClock{}, turns: turns} + r := &LLMRunner{Stream: rec.stream(scriptStream(turns, nil)), Now: rec.clock.now} + rc := &fakeRunCtx{ctx: context.Background(), prof: verifyOn()} + if setup != nil { + setup(rc) + } + if err := r.Execute(rc); err != nil { + t.Fatalf("Execute: %v", err) + } + return rec +} + +func toolOnlyTurns(n int) [][]agentllm.Event { + var turns [][]agentllm.Event + for i := 0; i < n; i++ { + // 每次调用用独立 ID(toolCall 帮手的 ID 固定,重复会撞内核的工具槽位)。 + call := agentllm.ToolCall{ID: fmt.Sprintf("c-ds-%d", i), Name: "make_report", Arguments: "{}"} + turns = append(turns, textDeltaTurn("", call)) + } + return turns +} + +// 长时间纯工具轮、用户零答复 → 阈值后恰好注入一次双速提醒。 +func TestDualSpeedNudgeFiresOnceAfterThreshold(t *testing.T) { + turns := append(toolOnlyTurns(20), textDeltaTurn("完成。")) + rec := runDualSpeed(t, turns, nil) + if !rec.nudged() { + t.Fatal("dual-speed nudge missing after threshold with no user reply") + } + count := 0 + for _, r := range rec.request { + count += strings.Count(r, "任务快慢双速") + } + if count > 1 { + t.Fatalf("nudge injected %d times, want at most once per request window", count) + } +} + +// 短任务(阈值内完成)不提醒。 +func TestDualSpeedNudgeQuietBeforeThreshold(t *testing.T) { + turns := append(toolOnlyTurns(1), textDeltaTurn("完成。")) + rec := runDualSpeed(t, turns, nil) + if rec.nudged() { + t.Fatal("nudge fired before the threshold") + } +} + +// 设置 runtime.dualSpeedNudgeSeconds=0 → 关闭,超时也不提醒。 +func TestDualSpeedNudgeDisabledBySetting(t *testing.T) { + turns := append(toolOnlyTurns(20), textDeltaTurn("完成。")) + rec := runDualSpeed(t, turns, func(rc *fakeRunCtx) { + rc.runtimeExtra = map[string]any{"dualSpeedNudgeSeconds": float64(0)} + }) + if rec.nudged() { + t.Fatal("nudge fired although disabled via runtime.dualSpeedNudgeSeconds=0") + } +} + +// 模型已给出面向用户的答复(快通道成立)→ 即使后续事实核查续写拖过阈值也不提醒。 +func TestDualSpeedNudgeSkippedWhenReplied(t *testing.T) { + turns := [][]agentllm.Event{ + textDeltaTurn("", toolCall("make_report", `{"content":"准确率 95%"}`)), + textDeltaTurn("完整总结:产出物与要点。"), + textDeltaTurn("已修正并重新登记。"), + } + rec := runDualSpeed(t, turns, func(rc *fakeRunCtx) { + rc.verify = true + rc.execArtifact = &core.Artifact{ID: "art_0123456789ab", Name: "报告.md"} + rc.execArtifactContent = []byte("# 报告\n\n准确率 95%。") + }) + if rec.nudged() { + t.Fatal("nudge fired although the model already gave its user reply") + } +} + +// 已把深度工作放后台(慢通道成立)→ 不再提醒。 +func TestDualSpeedNudgeSkippedWhenDelegated(t *testing.T) { + turns := [][]agentllm.Event{ + textDeltaTurn("", toolCall("delegate_task", `{"name":"深度解析","task":"逐页解析","background":true}`)), + } + turns = append(turns, toolOnlyTurns(19)...) + turns = append(turns, textDeltaTurn("后台进行中,先给结论。")) + rec := runDualSpeed(t, turns, func(rc *fakeRunCtx) { + rc.bgID = "run_bg_1" + }) + if rec.nudged() { + t.Fatal("nudge fired although background delegation already happened") + } +} diff --git a/internal/runtime/llm_direct.go b/internal/runtime/llm_direct.go index 5dabe90..be3a1a4 100644 --- a/internal/runtime/llm_direct.go +++ b/internal/runtime/llm_direct.go @@ -65,6 +65,12 @@ type directRun struct { servedModel string switches []string switchMu sync.Mutex + // 任务快慢双速(SR-B10 实路径):started 用注入时钟记 run 起点, + // nudged/replied/delegated 三个标志驱动「超时仍未反馈则提醒转双速」的一次性触发。 + started time.Time + nudgedDualSpeed bool + replied bool + delegatedBackground bool } type toolEvidence struct { @@ -97,7 +103,7 @@ func (t directTool) Execute(ctx context.Context, args json.RawMessage) (agenttoo } func (l *LLMRunner) runDirect(rc RunCtx, stream agentllm.StreamFunc, model string, msgs []agentllm.Message, decls []agentllm.ToolDecl, streamOpts agentllm.StreamOptions, cardflow bool, eff core.EffectiveSettings, maxTurns, maxTools int, refresh turnRefresh) error { - st := &directRun{l: l, rc: rc, eff: eff, cardflow: cardflow} + st := &directRun{l: l, rc: rc, eff: eff, cardflow: cardflow, started: l.now()} // 记忆端口(N3 WP5):runCtx 暴露 store 时接线(注入由循环按请求完成)。 if ms, ok := rc.(interface{ StoreMemory() memoryStore }); ok { st.memoryStore = ms.StoreMemory() @@ -473,6 +479,10 @@ func (st *directRun) onEvent(ev nruntime.Event) { if body != "" { st.finalParts = append(st.finalParts, core.Part{Type: core.PartText, Text: body, Process: hasCalls}) st.turnBody = body + if !hasCalls { + // 面向用户的答复段落已出现:快通道成立,双速提醒不再需要。 + st.replied = true + } st.textIdx++ } } @@ -693,6 +703,7 @@ func (st *directRun) runTool(name, args string) agenttools.Result { if serr != nil { out = "子代理失败:" + serr.Error() } else { + st.delegatedBackground = true out = fmt.Sprintf("已在后台启动 %s(慢通道):收件箱已建进度卡,完成后结果回投该卡并自动汇报;先把快通道的即时反馈写给用户,不要等待", id) } break diff --git a/internal/runtime/llm_test.go b/internal/runtime/llm_test.go index 13870d0..a6ac2d3 100644 --- a/internal/runtime/llm_test.go +++ b/internal/runtime/llm_test.go @@ -42,9 +42,10 @@ type fakeRunCtx struct { bgID string bgErr error sandbox string // 非空时 ConversationSandbox 返回之(N3 WP2 转录测试) - memEntries []memEntry // 非空时 StoreMemory 返回之(N3 WP5 记忆注入测试) + memEntries []memEntry // 非空时 StoreMemory 返回之(N3 WP5 记忆注入断言用) usageTotal core.Usage // AddUsage 累计(N3 WP6 记账断言用) bgCalls int + runtimeExtra map[string]any // 覆盖 runtime 组设置(双速提醒阈值等测试用) } func (f *fakeRunCtx) Context() context.Context { return f.ctx } @@ -55,7 +56,11 @@ func (f *fakeRunCtx) Settings() core.EffectiveSettings { if f.cardflow { eff["ui"] = map[string]any{"cardflow": true} } - eff["runtime"] = map[string]any{"verify": f.verify, "composeFinal": f.compose} + runtimeMap := map[string]any{"verify": f.verify, "composeFinal": f.compose} + for k, v := range f.runtimeExtra { + runtimeMap[k] = v + } + eff["runtime"] = runtimeMap return eff } func (f *fakeRunCtx) Emit(typ core.StreamEventType, payload any) error { diff --git a/internal/runtime/verify.go b/internal/runtime/verify.go index 703acf2..ffe4868 100644 --- a/internal/runtime/verify.go +++ b/internal/runtime/verify.go @@ -149,6 +149,13 @@ func (st *directRun) reminders(turn int) []string { st.closedToolTurn = false st.checklistTouched = false } + // 任务快慢双速(SR-B10 实路径触发):run 已运行超过阈值、用户还没看到任何 + // 答复、也没把深度工作放后台时,一次性提醒转双速——先把判断/计划写给用户, + // 重活 delegate 后台。已经回复或已委派则永不打扰;接近完成的任务可按文案 + // 自行忽略。这是内核每回合的 Reminders 注入点(agent-native 循环原生钩子)。 + if note := st.dualSpeedNudge(); note != "" { + return []string{note} + } if st.toolTurnsSince < gates.ReminderToolGap { return nil } @@ -175,6 +182,24 @@ func (st *directRun) reminders(turn int) []string { return got } +// dualSpeedNudgeSeconds:双速提醒的运行时长阈值(秒)。0 = 关闭。 +const dualSpeedNudgeDefaultSeconds = 75 + +// dualSpeedNudge 返回双速提醒文案;不满足条件或已提醒过返回空串。 +// 条件用注入时钟(l.now)计量,测试可控。 +func (st *directRun) dualSpeedNudge() string { + after := int(st.eff.GetNumber("runtime.dualSpeedNudgeSeconds", dualSpeedNudgeDefaultSeconds)) + if after <= 0 || st.nudgedDualSpeed || st.replied || st.delegatedBackground { + return "" + } + if st.started.IsZero() || st.l.now().Sub(st.started) < time.Duration(after)*time.Second { + return "" + } + st.nudgedDualSpeed = true + elapsed := int(st.l.now().Sub(st.started).Round(time.Second).Seconds()) + return fmt.Sprintf("本次任务已连续运行约 %d 秒,用户还没有收到任何答复(任务快慢双速,SR-B10)。现在二选一:若剩余工作量大(预计还要一分钟以上),先把你的判断、概要或执行计划作为即时反馈回复给用户,同时用 delegate_task 的 background=true 把深度处理放到后台执行(收件箱有进度卡,完成后系统自动回投结果并汇报);若已接近完成,把这句话忽略,继续做完后在最终答复里给出完整结论。不要默默长时间工作。", elapsed) +} + // unfinishedItems 返回计划与待办的未完成项正文(状态过滤留在产品侧)。 func (st *directRun) unfinishedItems() (todos, plans []string) { for _, item := range st.todos.Items { diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 050a083..dcd38cd 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -97,6 +97,7 @@ func Schema() []core.SettingDesc { {Key: "runtime.llmRetries", Group: "runtime", Label: "模型重试次数", Type: core.SettingNumber, Default: 2, Effect: core.EffectNewRun, Description: "直连模型在还没有任何输出时,遇到超时、限流或服务端错误的额外重试次数。0 = 不重试。已经开始输出后不再重试"}, {Key: "runtime.codeOrchestration", Group: "runtime", Label: "代码编排", Type: core.SettingBool, Default: false, Effect: core.EffectNewRun, Description: "允许模型用 run_tool_script 写脚本批量调用工具。默认关闭"}, {Key: "runtime.factCheck", Group: "runtime", Label: "终轮事实核查", Type: core.SettingBool, Default: true, Effect: core.EffectNewRun, Description: "交付物里的百分比/日期/大额数字回溯本轮材料,找不到出处的给模型一轮修正(修正过仍失败不拦截,只如实记录)"}, + {Key: "runtime.dualSpeedNudgeSeconds", Group: "runtime", Label: "快慢双速提醒阈值(秒)", Type: core.SettingNumber, Default: 75, Effect: core.EffectNewRun, Description: "任务连续运行超过该秒数、用户还没收到任何答复、也没把深度工作放后台时,一次性提醒模型转快慢双速:先把判断/概要/计划回复给用户,重活用 delegate_task 的 background 放后台(SR-B10)。0 = 关闭"}, {Key: "runtime.verifierSameModel", Group: "runtime", Label: "允许同源验证器", Type: core.SettingBool, Default: false, Effect: core.EffectNewRun, Description: "轻量档未单独配置时,默认跳过与主模型同源的独立验证(硬事实与事实核查仍执行);显式开启才用主模型做验证"}, {Key: "runtime.composeFinal", Group: "runtime", Label: "终轮整理回复", Type: core.SettingBool, Default: false, Effect: core.EffectNewRun, Description: "用过工具的回合结束后,再用主模型重写一段最终回复。默认关闭(裁定 4:转录权威后工作模型已看到完整证据,重写只见过 160 字摘要、实测会虚构章节与承诺未做之事);异常收尾时另有兜底"}, {Key: "runtime.intentSemantic", Group: "runtime", Label: "意图语义裁决(慢通道)", Type: core.SettingBool, Default: false, Effect: core.EffectRestart, Description: "规则歧义时用 embedding 语义相似度裁决。需已配置 embedding 端点。默认关闭(行为与 I1 一致)"}, -- 2.54.0 From b3f885f2156d1078a065fe9d46957c1aca68573a Mon Sep 17 00:00:00 2001 From: weichen Date: Sat, 10 Oct 2026 10:01:15 +0000 Subject: [PATCH 2/3] Strip inline think blocks from subagent summaries and card bodies Why: the round-6 real-model dual-speed run showed MiniMax writing its reasoning as inline blocks; finishChild's partsText fed them into the background notice turn and the inbox result card verbatim. Evidence: new runmgr test covers closed-block stripping with body preserved and conservative handling of unclosed blocks; go test ./... 32 pkgs green. Impact: partsText removes closed ... spans before summary, notice, and card assembly; unclosed blocks are kept as-is. Refs: Dev task feature/dual-speed-real-path. --- internal/runmgr/background_card_test.go | 14 +++++++++ internal/runmgr/runmgr.go | 23 +++++++++++++- internal/runtime/dual_speed_nudge_test.go | 38 +++++++++++++++++++++++ internal/runtime/llm_direct.go | 9 +++++- internal/runtime/llm_test.go | 5 +-- internal/runtime/verify.go | 37 ++++++++++++++++++---- 6 files changed, 116 insertions(+), 10 deletions(-) diff --git a/internal/runmgr/background_card_test.go b/internal/runmgr/background_card_test.go index 86306dd..8583834 100644 --- a/internal/runmgr/background_card_test.go +++ b/internal/runmgr/background_card_test.go @@ -148,3 +148,17 @@ func TestBackgroundCardSkippedWithoutInbox(t *testing.T) { } waitUntil(t, func() bool { return len(systemTexts(t, c)) == 1 }) } + +// TestPartsTextStripsInlineThink:MiniMax 行内 块不进子代理摘要/回投卡。 +func TestPartsTextStripsInlineThink(t *testing.T) { + got := partsText([]core.Part{{Type: core.PartText, Text: "先读文件\n正文开始\n再想结论"}}) + if strings.Contains(got, "") || strings.Contains(got, "先读文件") || strings.Contains(got, "再想") { + t.Fatalf("think leaked: %q", got) + } + if !strings.Contains(got, "正文开始") || !strings.Contains(got, "结论") { + t.Fatalf("body lost: %q", got) + } + if got := partsText([]core.Part{{Type: core.PartText, Text: "半截 未闭合保守保留"}}); !strings.Contains(got, "半截") { + t.Fatalf("unclosed think should keep text: %q", got) + } +} diff --git a/internal/runmgr/runmgr.go b/internal/runmgr/runmgr.go index 46ce9b7..f86f5ef 100644 --- a/internal/runmgr/runmgr.go +++ b/internal/runmgr/runmgr.go @@ -1898,13 +1898,34 @@ func partsText(parts []core.Part) string { var sb strings.Builder for _, p := range parts { if p.Type == core.PartText { - sb.WriteString(p.Text) + text := p.Text + // MiniMax 等模型把推理写成行内 块:子代理摘要/回投卡正文 + // 只取 think 之外的正文(实测 前缀泄漏进收件箱卡片与通知)。 + if body := stripInlineThink(text); strings.TrimSpace(body) != "" || !strings.Contains(text, "") { + text = body + } + sb.WriteString(text) sb.WriteString("\n") } } return strings.TrimSpace(sb.String()) } +// stripInlineThink 去掉文本中的 … 段(完整闭合才剥,半截保守保留)。 +func stripInlineThink(text string) string { + for { + start := strings.Index(text, "") + if start < 0 { + return text + } + end := strings.Index(text[start:], "") + if end < 0 { + return text + } + text = text[:start] + text[start+end+len(""):] + } +} + func artifactSummary(parts []core.Part) string { var bits []string seen := map[string]bool{} diff --git a/internal/runtime/dual_speed_nudge_test.go b/internal/runtime/dual_speed_nudge_test.go index 6b16870..5efbbeb 100644 --- a/internal/runtime/dual_speed_nudge_test.go +++ b/internal/runtime/dual_speed_nudge_test.go @@ -140,3 +140,41 @@ func TestDualSpeedNudgeSkippedWhenDelegated(t *testing.T) { t.Fatal("nudge fired although background delegation already happened") } } + +// 首次被无视后按 45s 间隔升级重复(最多 3 次),第二次起为硬性措辞。 +func TestDualSpeedNudgeEscalates(t *testing.T) { + turns := append(toolOnlyTurns(35), textDeltaTurn("完成。")) // 35×5s=175s → 75s 起、120s、165s 三档 + rec := runDualSpeed(t, turns, nil) + marks, hard := 0, 0 + for _, r := range rec.request { + marks += strings.Count(r, "用户已经等待") + hard += strings.Count(r, "硬性要求") + } + if marks < 1 { + t.Fatalf("escalation missing: requests=%d", len(rec.request)) + } + if marks > 2 { + t.Fatalf("escalated %d times, want at most 2 repeats after the first", marks) + } +} + +// 入口前置:带附件的任务第 0 回合注入快慢双速选项;无附件不注入;阈值关闭不注入。 +func TestDualSpeedEntryHintWithAttachments(t *testing.T) { + withAtt := runDualSpeed(t, [][]agentllm.Event{textDeltaTurn("完成。")}, func(rc *fakeRunCtx) { + rc.attachments = []core.ParsedDocument{{Meta: core.DocMeta{Filename: "meeting.docx"}}} + }) + if !strings.Contains(strings.Join(withAtt.request, "\n"), "快慢双速") { + t.Fatal("entry hint missing for an attachment-bearing task") + } + noAtt := runDualSpeed(t, [][]agentllm.Event{textDeltaTurn("完成。")}, nil) + if strings.Contains(strings.Join(noAtt.request, "\n"), "本任务附带材料") { + t.Fatal("entry hint fired without attachments") + } + disabled := runDualSpeed(t, [][]agentllm.Event{textDeltaTurn("完成。")}, func(rc *fakeRunCtx) { + rc.attachments = []core.ParsedDocument{{Meta: core.DocMeta{Filename: "meeting.docx"}}} + rc.runtimeExtra = map[string]any{"dualSpeedNudgeSeconds": float64(0)} + }) + if strings.Contains(strings.Join(disabled.request, "\n"), "本任务附带材料") { + t.Fatal("entry hint fired although the nudge setting is disabled") + } +} diff --git a/internal/runtime/llm_direct.go b/internal/runtime/llm_direct.go index be3a1a4..d171357 100644 --- a/internal/runtime/llm_direct.go +++ b/internal/runtime/llm_direct.go @@ -68,7 +68,7 @@ type directRun struct { // 任务快慢双速(SR-B10 实路径):started 用注入时钟记 run 起点, // nudged/replied/delegated 三个标志驱动「超时仍未反馈则提醒转双速」的一次性触发。 started time.Time - nudgedDualSpeed bool + nudgedDualSpeedCount int replied bool delegatedBackground bool } @@ -292,6 +292,13 @@ func (l *LLMRunner) runDirect(rc RunCtx, stream agentllm.StreamFunc, model strin Reminders: func(turn int) []string { var out []string out = append(out, st.reminders(turn)...) + // 快慢双速入口前置(SR-B10 实路径):带附件的任务在第 0 回合就把 + // 双速选项摆给模型——模型对开场指令的遵循远好于中途提醒(实测 + // MiniMax 无视中途 system-reminder,但对工作方法开场指令照办)。 + // 只提示一次,不强制拆分,保留 UR-27 的智能体自判。 + if turn == 0 && st.eff.GetNumber("runtime.dualSpeedNudgeSeconds", dualSpeedNudgeDefaultSeconds) > 0 && len(rc.Attachments()) > 0 { + out = append(out, "本任务附带材料,可能属于重任务。若预计要产出正式文档或做深度处理,采用快慢双速:先在当前回复里给用户即时反馈(材料相关性判断、概要或执行计划),再用 delegate_task 的 background=true 把深度工作放到后台执行——收件箱会显示进度卡,完成后系统自动回投结果并汇报。简单任务按常规同步完成即可。") + } // 转录权威:系统段在播种时冻结,动态上下文(尾注/环境/新命中模块) // 只在非播种 run 的首个请求注入一次——run 内反复重发没有信息增量。 if sessStore != nil && !seedRun && turn == 0 { diff --git a/internal/runtime/llm_test.go b/internal/runtime/llm_test.go index a6ac2d3..5757c26 100644 --- a/internal/runtime/llm_test.go +++ b/internal/runtime/llm_test.go @@ -45,7 +45,8 @@ type fakeRunCtx struct { memEntries []memEntry // 非空时 StoreMemory 返回之(N3 WP5 记忆注入断言用) usageTotal core.Usage // AddUsage 累计(N3 WP6 记账断言用) bgCalls int - runtimeExtra map[string]any // 覆盖 runtime 组设置(双速提醒阈值等测试用) + runtimeExtra map[string]any // 覆盖 runtime 组设置(双速提醒阈值等测试用) + attachments []core.ParsedDocument // Attachments 返回值(双速入口前置提示测试用) } func (f *fakeRunCtx) Context() context.Context { return f.ctx } @@ -141,7 +142,7 @@ func (f *fakeRunCtx) StoreMemory() agentmemory.Store { return fakeMemStore{entries: f.memEntries} } -func (f *fakeRunCtx) Attachments() []core.ParsedDocument { return nil } +func (f *fakeRunCtx) Attachments() []core.ParsedDocument { return f.attachments } func (f *fakeRunCtx) Tools() []core.ToolDesc { return nil } func (f *fakeRunCtx) Subtask() string { return "" } func (f *fakeRunCtx) CreateCard(card core.ActivityCard) (core.ActivityCard, error) { diff --git a/internal/runtime/verify.go b/internal/runtime/verify.go index ffe4868..d231eff 100644 --- a/internal/runtime/verify.go +++ b/internal/runtime/verify.go @@ -2,6 +2,7 @@ package runtime import ( "fmt" + "log" "strings" "time" @@ -185,19 +186,43 @@ func (st *directRun) reminders(turn int) []string { // dualSpeedNudgeSeconds:双速提醒的运行时长阈值(秒)。0 = 关闭。 const dualSpeedNudgeDefaultSeconds = 75 -// dualSpeedNudge 返回双速提醒文案;不满足条件或已提醒过返回空串。 +// 双速提醒升级:首次被无视后每 45 秒重复、最多 3 次,第二次起改硬性措辞。 +// 实测 MiniMax 对单次 system-reminder 无动于衷(opus 一次即响应)——重复+升级是 +// 不改内核契约的前提下最强的请求侧手段;仍无效的模型记录到任务卡,后续考虑 +// 系统级强制拆分(cancel + 后台续跑)方案。 +const ( + dualSpeedRepeatEvery = 45 * time.Second + dualSpeedMaxNudges = 3 +) + +// dualSpeedNudge 返回双速提醒文案;不满足条件或已达重复上限返回空串。 // 条件用注入时钟(l.now)计量,测试可控。 func (st *directRun) dualSpeedNudge() string { after := int(st.eff.GetNumber("runtime.dualSpeedNudgeSeconds", dualSpeedNudgeDefaultSeconds)) - if after <= 0 || st.nudgedDualSpeed || st.replied || st.delegatedBackground { + if after <= 0 || st.replied || st.delegatedBackground { return "" } - if st.started.IsZero() || st.l.now().Sub(st.started) < time.Duration(after)*time.Second { + if st.started.IsZero() { return "" } - st.nudgedDualSpeed = true - elapsed := int(st.l.now().Sub(st.started).Round(time.Second).Seconds()) - return fmt.Sprintf("本次任务已连续运行约 %d 秒,用户还没有收到任何答复(任务快慢双速,SR-B10)。现在二选一:若剩余工作量大(预计还要一分钟以上),先把你的判断、概要或执行计划作为即时反馈回复给用户,同时用 delegate_task 的 background=true 把深度处理放到后台执行(收件箱有进度卡,完成后系统自动回投结果并汇报);若已接近完成,把这句话忽略,继续做完后在最终答复里给出完整结论。不要默默长时间工作。", elapsed) + elapsed := st.l.now().Sub(st.started) + if elapsed < time.Duration(after)*time.Second { + return "" + } + due := 1 + int((elapsed-time.Duration(after)*time.Second)/dualSpeedRepeatEvery) + if due > dualSpeedMaxNudges { + due = dualSpeedMaxNudges + } + if st.nudgedDualSpeedCount >= due { + return "" + } + st.nudgedDualSpeedCount = due + secs := int(elapsed.Round(time.Second).Seconds()) + log.Printf("[runtime] dual-speed nudge #%d injected at %ds (replied=%v delegated=%v)", due, secs, st.replied, st.delegatedBackground) + if due == 1 { + return fmt.Sprintf("本次任务已连续运行约 %d 秒,用户还没有收到任何答复(任务快慢双速,SR-B10)。现在二选一:若剩余工作量大(预计还要一分钟以上),先把你的判断、概要或执行计划作为即时反馈回复给用户,同时用 delegate_task 的 background=true 把深度处理放到后台执行(收件箱有进度卡,完成后系统自动回投结果并汇报);若已接近完成,把这句话忽略,继续做完后在最终答复里给出完整结论。不要默默长时间工作。", secs) + } + return fmt.Sprintf("用户已经等待约 %d 秒(第 %d 次提醒,硬性要求):立即停止后续工具调用,把当前进展、结论或计划作为回复发给用户;剩余的深度处理用 delegate_task 的 background=true 放到后台执行,完成后系统会自动回投结果。不要再让用户在没有反馈的情况下等待。", secs, due) } // unfinishedItems 返回计划与待办的未完成项正文(状态过滤留在产品侧)。 -- 2.54.0 From c449a176f9601af718afdd27c7454f9149c40e5b Mon Sep 17 00:00:00 2001 From: weichen Date: Sat, 10 Oct 2026 10:06:55 +0000 Subject: [PATCH 3/3] Restore the E1 agent-native kernel pin c348441 over the stale submodule state captured by the merge --- deps/agent-native | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/agent-native b/deps/agent-native index 3b92cb6..c348441 160000 --- a/deps/agent-native +++ b/deps/agent-native @@ -1 +1 @@ -Subproject commit 3b92cb63b41b5dd035371dada9be50ad66e39941 +Subproject commit c348441691e8bdab784a03aa9be64d53dacff341 -- 2.54.0