mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
## 缺陷(生产实测)
`SetSkillIndexProvider` 只在 cmd/homed/bootstrap.go 里对**根 agent** 调了一次。
rtchild / 驻留子各自 `New(AgentConfig{...})` 构造载体,都没传这个字段 ⇒
子的 system prompt 里**完全没有 skill 索引**。
症状(日志实录):A2A 会话由「临时分诊助手」(驻留子)接手,它手上没有
`cmd_run`(cmd 插件 `auto_start=false`),而 `cmd-tools` skill 正文里写着
「先 plugin_start cmd」。它看不到这条 skill,于是把 `terminal_create` 当成
命令执行器:每条 `cd … && echo … && grep …` 开一个新终端,终端随命令结束退出,
`terminal_read` 立刻撞 `read /dev/ptmx: input/output error`,然后为下一条命令
再开一个 —— 终端列表被填满,每一条都死在半路。
与本仓 Security 那次同型:父有的能力,子没继承。
## 修法
rtchild 与驻留子的 AgentConfig 各补一行 `SkillIndexProvider: a.skillIndex`,
与 `SystemPrompt` / `StageHost` / `Security` 同一组「必须继承父」的字段。
## 顺带两件事
### 1. 内置 skill 的更新永远到不了已部署机器
原落盘规则是「目标目录已存在就跳过」,本意是保住用户改动。但它把两件事混成一件:
用户改过的版本 ⇒ 应该保留
内核自己写过的旧版 ⇒ 应该随内核更新
两者都表现为「目录已存在」,于是 b850fea 把 `memory_recall` 改成
`stablemem_recall` 后,生产机的 kernel-memory 一直停在旧名,模型照着
旧名调工具必然失败,而日志里一个字都没有。
改为用 `.builtin-sha256` 摘要区分:
当前内容 == stamp ⇒ 没人动过 ⇒ 可安全覆盖成新版
当前内容 != stamp ⇒ 用户改过 ⇒ 保留
没有 stamp(老目录)⇒ 保守视为用户版本,不覆盖
### 2. 新增 kernel-terminal skill
工具描述只说「适用于交互式程序」,没说何时**不该**用;`command` 参数又鼓励
传整条命令。补一条内置 skill 讲清 cmd_run 与 terminal_create 的边界、
以及终端的 input/key/notify 用法。描述控制在 80 字符内(进索引不截断)。
## 判据(均经变异验证)
TestRTChild_InheritsSkillIndex 删掉接线 ⇒ 红(实测确认)
TestResident_InheritsSkillIndex 同上(生产实际中招的那一类)
TestRootAgent_SkillIndexStillInjected 确认没改坏根 agent 路径
TestChildWithoutSkillIndexIsNotBroken 父无索引时子不 panic
TestMaterializeBuiltinSkills_* 5 条:覆盖语义四条成组断言
(改回旧行为 ⇒ OverwritesUnmodified 红,实测确认)
判据测的是**生成的 prompt**,不是 `skillIndex != nil`:
后者接上了但注入那段被删照样绿。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 62 包全过,0 FAIL
干净检出(git worktree)单独验证 PASS
162 lines
6.0 KiB
Go
162 lines
6.0 KiB
Go
package skillmgr
|
||
|
||
// 内核自带的 skill:把**技术机制**写进 skill,由 skillmgr 注入,而不是塞进
|
||
// 系统提示词。
|
||
//
|
||
// ## 为什么(用户 2026-10-26 的两次裁定)
|
||
//
|
||
// 1. 「系统提示词把机制讲得太深太技术化,导致 agent 说话没人味」——
|
||
// 机制留在提示词里,模型会连**语气**一起模仿:回复变成说明书腔。
|
||
// 2. 「技术相关下沉到 skill,由 skillmgr 自行管理注入」——
|
||
// 提示词只留"必须做什么","怎么实现"由 skill 讲。
|
||
//
|
||
// 所以提示词里只剩一两句行为约束,机制在下面这四个 skill 正文里:
|
||
//
|
||
// kernel-memory 记忆分层与 coldcontext_query 的回填语义
|
||
// kernel-output 同步/异步通道怎么真正送达用户
|
||
// kernel-tool-order 一次回复里多工具调用的调度与例外
|
||
// kernel-plugins 按需插件怎么发现与拉起
|
||
//
|
||
// ## 为什么内嵌在二进制里,而不是让用户自己装
|
||
//
|
||
// 它们是**内核行为**的一部分:提示词引用了它们,就必须保证它们一定存在。
|
||
// 若依赖外部安装,用户删掉/改名后提示词会指向不存在的 skill —— 那比不写更糟。
|
||
//
|
||
// ## 落盘纪律:不覆盖用户版本
|
||
//
|
||
// 只在目标目录**不存在**时写入(与 adapters 的 `.bundled` 语义一致)。
|
||
// 用户改过的同名 skill 一律保留:内核不该覆盖人的修改。
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"embed"
|
||
"encoding/hex"
|
||
"io/fs"
|
||
"log"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
)
|
||
|
||
//go:embed builtin
|
||
var builtinSkills embed.FS
|
||
|
||
// builtinSkillDir 是内嵌 skill 的根目录名。
|
||
const builtinSkillDir = "builtin"
|
||
|
||
// skillFileName 与 plugin.LoadSKILL 认的文件名一致。
|
||
const skillFileName = "SKILL.md"
|
||
|
||
// builtinStampName 记录"上次由内核写入的版本"的摘要。
|
||
//
|
||
// ★ 为什么需要它(2026-11-04 实测缺陷):
|
||
//
|
||
// 原规则是"目标目录已存在就跳过",本意是保住用户改动。但它把两件事混成一件:
|
||
//
|
||
// 用户改过的版本 ⇒ 应该保留
|
||
// 内核自己写过的旧版 ⇒ 应该随内核更新
|
||
//
|
||
// 两者都表现为"目录已存在",于是 **内核改名/改文档后,内置 skill 永远
|
||
// 更新不到已部署机器**。实测:b850fea 把 `memory_recall` 改成
|
||
// `stablemem_recall`,生产机的 kernel-memory 一直停在旧名,
|
||
// 模型照着旧名调工具必然失败。
|
||
//
|
||
// 现在用摘要区分:把写入时的内容 sha256 记进 `.builtin-sha256`。
|
||
//
|
||
// 当前内容 == 记录的 sha256 ⇒ 没人改过 ⇒ 可以安全覆盖成新版
|
||
// 当前内容 != 记录的 sha256 ⇒ 用户改过 ⇒ 保留,只在日志里说明
|
||
// 没有 stamp 文件(历史遗留)⇒ 保守处理,视为"用户版本",不覆盖
|
||
//
|
||
// 保守默认很重要:老机器上没有 stamp,宁可不更新也不要覆盖掉人的修改。
|
||
const builtinStampName = ".builtin-sha256"
|
||
|
||
// materializeBuiltinSkills 把内嵌 skill 落到 skillsDir,返回新写入的数量。
|
||
//
|
||
// 覆盖规则见 builtinStampName 的说明:只有"内核写的且未被改动"才覆盖。
|
||
func materializeBuiltinSkills(skillsDir string) int {
|
||
if skillsDir == "" {
|
||
return 0
|
||
}
|
||
entries, err := builtinSkills.ReadDir(builtinSkillDir)
|
||
if err != nil {
|
||
log.Printf("[skillmgr] read embedded skills: %v", err)
|
||
return 0
|
||
}
|
||
written := 0
|
||
for _, e := range entries {
|
||
if !e.IsDir() {
|
||
continue
|
||
}
|
||
name := e.Name()
|
||
dstDir := filepath.Join(skillsDir, name)
|
||
data, readErr := fs.ReadFile(builtinSkills, builtinSkillDir+"/"+name+"/"+skillFileName)
|
||
if readErr != nil {
|
||
log.Printf("[skillmgr] embedded skill %s: %v", name, readErr)
|
||
continue
|
||
}
|
||
// 已存在:判断是不是"内核写的且没人改过",是才覆盖。
|
||
if _, statErr := os.Stat(dstDir); statErr == nil {
|
||
if !builtinSkillUnmodified(dstDir) {
|
||
continue // 用户改过(或无法确认):保留人的版本
|
||
}
|
||
if cur, err := os.ReadFile(filepath.Join(dstDir, skillFileName)); err == nil && string(cur) == string(data) {
|
||
continue // 内容已是最新,不用重写(保留 mtime)
|
||
}
|
||
if wErr := os.WriteFile(filepath.Join(dstDir, skillFileName), data, 0644); wErr != nil {
|
||
log.Printf("[skillmgr] update %s: %v", dstDir, wErr)
|
||
continue
|
||
}
|
||
writeBuiltinStamp(dstDir, data)
|
||
log.Printf("[skillmgr] updated builtin skill %s (kernel-owned, unmodified by user)", name)
|
||
written++
|
||
continue
|
||
}
|
||
if mkErr := os.MkdirAll(dstDir, 0755); mkErr != nil {
|
||
log.Printf("[skillmgr] create %s: %v", dstDir, mkErr)
|
||
continue
|
||
}
|
||
if wErr := os.WriteFile(filepath.Join(dstDir, skillFileName), data, 0644); wErr != nil {
|
||
log.Printf("[skillmgr] write %s: %v", dstDir, wErr)
|
||
continue
|
||
}
|
||
writeBuiltinStamp(dstDir, data)
|
||
written++
|
||
}
|
||
if written > 0 {
|
||
log.Printf("[skillmgr] materialized %d kernel skills (mechanics live here, not in the system prompt)", written)
|
||
}
|
||
return written
|
||
}
|
||
|
||
// builtinSkillUnmodified 报告 skillsDir 下的 skill 是否"内核写的且未被改动"。
|
||
//
|
||
// 判定依据是 .builtin-sha256 与**当前** SKILL.md 的实际摘要是否一致。
|
||
// 没有 stamp(历史遗留或用户手动放置)⇒ 返回 false(保守:不覆盖)。
|
||
func builtinSkillUnmodified(dir string) bool {
|
||
stamp, err := os.ReadFile(filepath.Join(dir, builtinStampName))
|
||
if err != nil {
|
||
return false
|
||
}
|
||
cur, err := os.ReadFile(filepath.Join(dir, skillFileName))
|
||
if err != nil {
|
||
return false
|
||
}
|
||
return strings.TrimSpace(string(stamp)) == sha256Hex(cur)
|
||
}
|
||
|
||
// writeBuiltinStamp 记录本次写入内容的摘要,供下次判断"有没有人改过"。
|
||
//
|
||
// 写失败只记日志、不影响 skill 本身可用:没有 stamp 的后果是
|
||
// "下次不会被自动更新"(保守侧),而不是 skill 坏了。
|
||
func writeBuiltinStamp(dir string, data []byte) {
|
||
if err := os.WriteFile(filepath.Join(dir, builtinStampName), []byte(sha256Hex(data)), 0644); err != nil {
|
||
log.Printf("[skillmgr] write stamp %s: %v", dir, err)
|
||
}
|
||
}
|
||
|
||
// sha256Hex 返回 b 的十六进制 sha256。
|
||
func sha256Hex(b []byte) string {
|
||
sum := sha256.Sum256(b)
|
||
return hex.EncodeToString(sum[:])
|
||
}
|