diff --git a/internal/prompts/library/base-method.md b/internal/prompts/library/base-method.md new file mode 100644 index 0000000..9fe9358 --- /dev/null +++ b/internal/prompts/library/base-method.md @@ -0,0 +1,16 @@ +--- +id: base.method +layer: base +version: 1 +title: 工作方法 +description: 把任务做完的方法论:计划、换路、查 API、坚持到完成、回到材料(常驻基座) +--- +按以下方法工作: + +- **先计划再动手**:预计三步以上、或要产出文件的任务,开始前用 update_plan 列出步骤;每完成一步就更新状态,同一时间只有一步进行中。计划是给自己看的进度账,不是给用户看的排场。 +- **一个方法失败两次就换路**:同样的调用连续失败两次,停下来想第三条路(换工具、换参数结构、换实现方式),不要原样重试第三次。 +- **写脚本前先查 API**:调用不熟悉的库/模块前,先用 grep 查它的函数签名(`inspect.signature`/`dir`)或读 SKILL.md 的示例,确认参数形态后再写完整脚本。猜 API 是失败的最大来源。 +- **编辑粒度**:修改文件时先想清楚要改哪几处,用 multi_edit 或大块 edit 一次改完,不要一行一个 edit_file 连发十几次;改完立即验证(build/运行),验证通过再继续下一处。 +- **坚持到完成**:任务没做完不要停。收尾前检查最后一段:如果还是计划、承诺或"接下来我将",就先把承诺的事做掉。只有用户输入才能解决的阻塞才允许停下来问。 +- **结论回到材料**:写进交付物的数字、百分比、日期、人名必须能在本轮材料(附件/工具输出/会议原文)里找到出处;找不到的改成不写,或明确标注"推测"。材料里的日期基准与你理解的不一致时,以材料为准或向用户确认。 +- **终稿如实**:最后的回复先给结论,再列交付物与要点;没做完、没验证的部分如实说明,不用模糊措辞掩盖。 diff --git a/internal/prompts/library/tool-edit-file.md b/internal/prompts/library/tool-edit-file.md new file mode 100644 index 0000000..15380bf --- /dev/null +++ b/internal/prompts/library/tool-edit-file.md @@ -0,0 +1,8 @@ +--- +id: tool.edit_file +layer: tool +version: 1 +title: edit_file 工具描述 +description: 工作区文件精确编辑的模型侧说明文案 +--- +对工作区文件做唯一匹配替换:old 必须与文件内容逐字一致(含缩进)且在文件中唯一,不唯一时带上更多上下文行。改前必须先 read 过该文件;文件在读取后被外部修改会拒绝,重读即可。多处相关修改用 multi_edit 一次提交,不要一行一个调用连发。 diff --git a/internal/prompts/library/tool-glob.md b/internal/prompts/library/tool-glob.md new file mode 100644 index 0000000..689ec69 --- /dev/null +++ b/internal/prompts/library/tool-glob.md @@ -0,0 +1,8 @@ +--- +id: tool.glob +layer: tool +version: 1 +title: glob 工具描述 +description: 文件名模式匹配的模型侧说明文案 +--- +按通配模式找文件路径(** 跨目录),用于"这类文件有哪些/在哪个目录"。找内容用 grep,不要用 glob 后逐个 cat。 diff --git a/internal/prompts/library/tool-grep.md b/internal/prompts/library/tool-grep.md new file mode 100644 index 0000000..a90b34b --- /dev/null +++ b/internal/prompts/library/tool-grep.md @@ -0,0 +1,8 @@ +--- +id: tool.grep +layer: tool +version: 1 +title: grep 工具描述 +description: 工作区内容检索的模型侧说明文案 +--- +在工作区文件内容里做正则检索(支持 include 按文件名过滤、ignore_case)。用于定位章节、函数、数据点;配合 read_file 的 offset 直达相关段落,替代整文件通读。 diff --git a/internal/prompts/library/tool-read-file.md b/internal/prompts/library/tool-read-file.md new file mode 100644 index 0000000..37e8b5e --- /dev/null +++ b/internal/prompts/library/tool-read-file.md @@ -0,0 +1,8 @@ +--- +id: tool.read_file +layer: tool +version: 1 +title: read_file 工具描述 +description: 工作区文件读取的模型侧说明文案 +--- +读取会话工作区内的文本文件,输出带行号(1-based)。用法:先不带 offset/limit 读前 2000 行看全貌;文件大时用 offset/limit 分页,不要反复从头读。编辑前必须先读过目标文件。长输出会被截断并提示分页方式。 diff --git a/internal/prompts/library/tool-run-python.md b/internal/prompts/library/tool-run-python.md new file mode 100644 index 0000000..4e24b92 --- /dev/null +++ b/internal/prompts/library/tool-run-python.md @@ -0,0 +1,8 @@ +--- +id: tool.run_python +layer: tool +version: 1 +title: run_python 工具描述 +description: Python 执行原语的模型侧说明文案 +--- +执行完整 Python 脚本(python3,工作目录=会话工作区)。适用:数据处理、生成 Excel/PPT/图片(housestyle kit)、批量操作。Word 交付物优先写 .md 源后用 run_shell 跑 `python3 -m housestyle.build`,不要在脚本里手工拼 docx。报错先读退出码与 traceback 定位行号再改,同一错误不要原样重跑。生成带排式的文件(xlsx/pptx)先读 `$AGENT_SKILLS_DIR/house-style/SKILL.md` 的构件表,不确定签名先 `inspect.signature`。 diff --git a/internal/prompts/library/tool-run-shell.md b/internal/prompts/library/tool-run-shell.md new file mode 100644 index 0000000..ad989d8 --- /dev/null +++ b/internal/prompts/library/tool-run-shell.md @@ -0,0 +1,8 @@ +--- +id: tool.run_shell +layer: tool +version: 1 +title: run_shell 工具描述 +description: bash 执行原语的模型侧说明文案 +--- +在会话工作区内执行 bash 脚本(环境最小化:PATH/HOME/LANG;AGENT_SKILLS_DIR 与 PYTHONPATH 已注入)。适用:housestyle build/lint、pip install --user、文件批操作。读文件用 read_file、找内容用 grep——不要用 cat/sed/grep 命令行代替专用工具。同一命令失败两次换方法。 diff --git a/internal/prompts/prompts_test.go b/internal/prompts/prompts_test.go index 3733b14..973696b 100644 --- a/internal/prompts/prompts_test.go +++ b/internal/prompts/prompts_test.go @@ -73,9 +73,9 @@ func TestResolveSystemDefaultBase(t *testing.T) { if err != nil { t.Fatal(err) } - // 默认:base 七块(identity/behavior/caution/style/tools/deliverables/quality)+ env 一块,无技能注入。 - if len(blocks) != 8 { - t.Errorf("blocks = %d, want 8 (7 base + env)", len(blocks)) + // 默认:base 八块(identity/behavior/method/caution/style/tools/deliverables/quality)+ env 一块,无技能注入。 + if len(blocks) != 9 { + t.Errorf("blocks = %d, want 9 (8 base + env)", len(blocks)) } var envBody string for _, b := range blocks { diff --git a/internal/runtime/llm_direct.go b/internal/runtime/llm_direct.go index 6262157..fe9204a 100644 --- a/internal/runtime/llm_direct.go +++ b/internal/runtime/llm_direct.go @@ -58,6 +58,7 @@ type directRun struct { closedToolTurn bool checklistTouched bool toolTurnsSince int + nudgedPlanless bool progress *gates.ProgressReminder checks []core.CheckPart servedModel string @@ -318,9 +319,16 @@ func (l *LLMRunner) runDirect(rc RunCtx, stream agentllm.StreamFunc, model strin }, }, user) composed := false - if err == nil && res.Err == nil && len(st.evidence) > 0 && st.eff.GetBool("runtime.composeFinal", true) { + // composeFinal 默认关闭(裁定 4):转录权威后工作模型自带完整证据,重写层 + // 只见 160 字摘要——实测会虚构章节、承诺未做之事。开启或异常兜底时才重写。 + composeWanted := st.eff.GetBool("runtime.composeFinal", false) + if err == nil && res.Err == nil && len(st.evidence) > 0 && composeWanted { composed = st.composeFinal(stream, model, res.Notice, streamOpts) } + if err == nil && res.Err == nil && !composed && res.Notice == "" && strings.TrimSpace(st.turnBody) == "" && len(st.evidence) > 0 { + // 兜底:工作轮结束但没有任何给用户的正文(罕见),才用重写层补一段。 + composed = st.composeFinal(stream, model, "", streamOpts) + } if res.Notice != "" && !composed { st.finalParts = append(st.finalParts, core.Part{Type: core.PartText, Text: res.Notice}) } diff --git a/internal/runtime/planless_nudge_test.go b/internal/runtime/planless_nudge_test.go new file mode 100644 index 0000000..87e3edb --- /dev/null +++ b/internal/runtime/planless_nudge_test.go @@ -0,0 +1,42 @@ +package runtime + +import ( + "context" + "strings" + "testing" + + core "agent-system-core" +) + +// P4:计划提醒常开——没有任何计划/待办而任务变长时,给一次可忽略的轻提醒; +// 已有计划但停滞的旧提醒行为不变。 +func TestPlanlessNudgeFiresOnce(t *testing.T) { + rc := &fakeRunCtx{ + ctx: context.Background(), + prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}}, + } + quiet := textDeltaTurn("", toolCall("make_report", "{}")) + turns := [][]interface{}{} + _ = turns + st := &directRun{l: &LLMRunner{}, rc: rc, eff: core.EffectiveSettings{}} + var seen []string + emitReminder := func(turn int) []string { + // 模拟连续工具轮推进。 + st.closedToolTurn = true + st.toolTurnsSince++ + return st.reminders(turn) + } + for i := 0; i < 3; i++ { + seen = append(seen, emitReminder(i)...) + } + if len(seen) != 1 || !strings.Contains(seen[0], "update_plan") { + t.Fatalf("planless nudge should fire exactly once, got %v", seen) + } + for i := 0; i < 6; i++ { + seen = append(seen, emitReminder(i)...) + } + if len(seen) != 1 { + t.Fatalf("planless nudge must not repeat, got %v", seen) + } + _ = quiet +} diff --git a/internal/runtime/verify.go b/internal/runtime/verify.go index 157669d..195cb26 100644 --- a/internal/runtime/verify.go +++ b/internal/runtime/verify.go @@ -143,12 +143,22 @@ func (st *directRun) reminders(turn int) []string { if st.toolTurnsSince < gates.ReminderToolGap { return nil } + todos, plans := st.unfinishedItems() + if len(todos) == 0 && len(plans) == 0 { + // 轻提醒(每 run 一次):有计划却没更新是失职;没有任何计划而任务变长 + // 同样值得提醒——可忽略,但把选项摆出来(P4:计划提醒常开)。 + if !st.nudgedPlanless { + st.nudgedPlanless = true + st.toolTurnsSince = 0 + return []string{"本次任务已连续多个工具轮没有任何计划。如果剩余工作不止一步,用 update_plan 列出步骤再继续;确实只剩简单收尾就忽略本条。"} + } + return nil + } // N1 WP5:节流、封顶与文案走下沉的 ProgressReminder; // 工具轮计数留在这里(它与 directRun 的事件流耦合)。 if st.progress == nil { st.progress = &gates.ProgressReminder{} } - todos, plans := st.unfinishedItems() got := st.progress.Remind(todos, plans, time.Now()) if got != nil { st.toolTurnsSince = 0 diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 2be39ec..a28b995 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -96,7 +96,7 @@ func Schema() []core.SettingDesc { {Key: "runtime.verify", Group: "runtime", Label: "终轮结果检查", Type: core.SettingBool, Default: true, Effect: core.EffectNewRun, Description: "终轮确定性检查与独立验证"}, {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.composeFinal", Group: "runtime", Label: "终轮整理回复", Type: core.SettingBool, Default: true, 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 一致)"}, {Key: "runtime.intentGenerator", Group: "runtime", Label: "意图命令生成(慢通道)", Type: core.SettingBool, Default: false, Effect: core.EffectRestart, Description: "规则与语义都未命中时用轻量模型生成意图命令(幻觉 fail-closed 回退)。默认关闭(行为与 I1 一致)"}, {Key: "telemetry.otlpEndpoint", Group: "telemetry", Label: "OTLP 端点", Type: core.SettingString, Default: "", Effect: core.EffectRestart, Description: "OTel HTTP JSON 上报端点(如 http://localhost:4318)。空 = 不导出(默认);环境变量 OTEL_EXPORTER_OTLP_ENDPOINT 作为后备"},