mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 09:17:31 +00:00
refactor(kernel): 统一挂载点 kstate + 插件声明式系统提示词段
## 起因(生产故障)
A2A 分诊助手(驻留子)把 terminal_create 当一次性命令执行器:
terminal_create(command='cd /usr/lib/node_modules/@deepseek-ai && ls -d */ | head -60 && …')
→ read /dev/ptmx: input/output error
→ 再开 term_7、term_8、term_9 …
链路拆开四层:
① cmd 插件 auto_start=false ⇒ 工具表里没有 cmd_run
② 需要 cmd-tools skill 说明"先 plugin_start cmd"
③ 但 skill 索引没进那个 agent 的提示词
④ 因为 skillIndex 只是根 agent 的字段、由 bootstrap 手工接线
④ 才是总根。而它之所以必须手工接线:agent/core 读不到内核的共享数据。
## 环的精确形状(实测,非推测)
kernelboot 因为持有 KernelState 而依赖 19 个业务包:
kernelboot ──▶ agent/core ──▶ …
──▶ plugin
──▶ security
──▶ memory / knowledge / text / coldcontext / …
而 kernelboot.go / scheduler.go / funcmodule.go / syncbus.go / lifecycle.go
**一行业务代码都不需要** —— 依赖全在 kernelstate.go 这一个数据文件里。
后果是四个底层包各自造通道:
core → Agent 字段(每种 agent 构造点都要记得传)
plugin → 12 个 SetXxx
sdk → SDKConfig 传参
security → 包级单例 Global() / RuntimeGlobal() / ConfigCodec()
四条通道是同一个根因的四个表现。
## 本次做的事
### 1. 抽出 internal/kstate(断环)
kernelstate.go / syncbus.go / lifecycle.go 搬出 kernelboot:
internal/kstate 纯数据 + 唯一挂载点(叶子,谁都能 import)
internal/kernelboot 装配引擎,零业务包
挂载点 API:Mount / Get(nil 安全)/ MountForTest(返回 restore,并行测试隔离)。
验证:core 现在能 import kstate(此前 import cycle not allowed)。
### 2. StageHost / Agent 字段存 any
它们是 core 自己的类型(core 是环的一极,这是真环)。按用户裁定的方向:
> 是其他包在kstate挂共享内容啊
存 any,消费方断言回具体类型 —— 与本仓既有的 PluginCatalog /
PluginScheduler / UpdateCapabilities 同型。
★ 其余字段保持**具体类型**,没有一律改 any。判据是"它会不会反向依赖
agent/core":实测 plugin / security / sdk 都不依赖 core,所以它们作为
具体类型不构成环,而类型化字段让"拼错/取错类型"是编译错误(本结构体的
设计原则)。
### 3. 插件声明式系统提示词段
SDK 新增 SystemPromptAPI(见 submodule 提交)。内核侧:
kstate.State.PromptSections *promptsec.Registry ← 唯一事实来源
Agent.renderSystemPrompt() 每轮重建时读它
skillmgr 从"实现 SkillIndexProvider 让内核来问"改为"声明自己的段":
DeclarePromptSection{Section:"skillmgr.skills", Order:100, Volatile:true}
并挂到全部变更点(启动全扫后、移交事件后、load/unload/create/install/
enable/disable 后、Stop 时撤销)。
### 4. 内核自身的提示词段也走同一管理器
按用户裁定「把现在内核所有 systemprompt 的相关注入都接入到这个统一的
systemprompt 管理器上」,把 buildSystemPrompt 里硬拼的十几块
(人格卡/任务提示词/记忆说明/输出规则/工具目录…)改为声明式:
kernelSections() 当轮计算 → 与插件段按 Order 合并渲染。
顺序由 Order 常量决定,改结构变成调常量而不是挪代码块。
## 判据(均经变异验证)
TestCoreCanImportKstate 真编译探针;环重现则红(实测)
TestKstateDoesNotDependOnCore 静态版,指向具体字段
TestKstateIsLeaf kstate 不得反依赖上层
TestRTChildSeesPromptSections 子 agent 能看到插件段
TestResidentSeesPromptSections 生产实际中招的那一类;变异后红(实测)
TestRootAgentSeesPromptSections 确认没改坏根路径
TestNoPromptSectionsMount 未装配时不 panic、不凭空出现段
TestDeclareTwiceRendersOnce ★ 回应"会不会多份":声明 5 次渲染 1 份
TestSectionRemovalTakesEffect Stop 后段消失
TestRuntimeUpdateVisibleToExistingAgent 同一实例下一轮看到新内容
TestSectionOrderIsRespected Order 生效且稳定
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l internal/ cmd/ pkg/ 干净
go test ./... -count=1 63 包全过,0 FAIL
干净检出(git worktree) build + 4 包测试全绿
## 未做(见 kernelplan.md)
T3 security 三单例收敛(含 Store 复用 Codec 的惰性迁移)
T5 plugin Setter 收敛
T6 防腐判据(部分已随本次落地)
This commit is contained in:
19
third_party/hologramagentsdk/sdk/plugin.go
vendored
19
third_party/hologramagentsdk/sdk/plugin.go
vendored
@ -627,6 +627,7 @@ type PluginSDK struct {
|
||||
social SocialAPI
|
||||
secrets SecretsAPI
|
||||
security SecurityAPI
|
||||
sysPrompt SystemPromptAPI
|
||||
rawFiles RawFilesAPI
|
||||
events EventSubscriber
|
||||
plgMgr PluginMgrAPI
|
||||
@ -740,6 +741,24 @@ func (s *PluginSDK) Security() SecurityAPI {
|
||||
return s.security
|
||||
}
|
||||
|
||||
// SystemPrompt 返回**系统提示词声明面**(插件声明自己的段;渲染由内核做)。
|
||||
//
|
||||
// ★ 只声明、不渲染:本接口没有"设置整个 system prompt"的方法。
|
||||
// 插件能改的只有自己那段(段名以插件名开头,内核强制),
|
||||
// 否则任一插件都能改写别人的人格/安全说明。
|
||||
func (s *PluginSDK) SystemPrompt() SystemPromptAPI {
|
||||
s.apiMu.RLock()
|
||||
defer s.apiMu.RUnlock()
|
||||
return s.sysPrompt
|
||||
}
|
||||
|
||||
// SetSystemPromptAPI 由内核注入声明面的实现(插件不调用)。
|
||||
func (s *PluginSDK) SetSystemPromptAPI(v SystemPromptAPI) {
|
||||
s.apiMu.Lock()
|
||||
defer s.apiMu.Unlock()
|
||||
s.sysPrompt = v
|
||||
}
|
||||
|
||||
func (s *PluginSDK) Social() SocialAPI {
|
||||
s.apiMu.RLock()
|
||||
defer s.apiMu.RUnlock()
|
||||
|
||||
113
third_party/hologramagentsdk/sdk/systemprompt.go
vendored
Normal file
113
third_party/hologramagentsdk/sdk/systemprompt.go
vendored
Normal file
@ -0,0 +1,113 @@
|
||||
package sdk
|
||||
|
||||
// SystemPromptAPI 是插件**声明**自身系统提示词段的接口。
|
||||
//
|
||||
// ## 用户模型(2026-11-04)
|
||||
//
|
||||
// > 每个插件都可以声明 systemprompt,可运行时更新,内核每轮重建。
|
||||
//
|
||||
// 于是"某个插件想告诉模型怎么用自己的工具"变成**插件自己声明的一段文本**:
|
||||
//
|
||||
// skillmgr 声明 "skillmgr.skills" → 技能索引 + "调工具前先读同名 skill"
|
||||
// qq 声明 "qq.identity" → 身份绑定规则(哪些操作以谁的身份做)
|
||||
// 任意插件 声明 "<自己>.<段名>" → 各自的说明
|
||||
//
|
||||
// 内核只做两件事:**存**(按段名覆盖)与**渲染**(每轮重建时按 Order 拼进
|
||||
// system prompt)。内核不认识 skill、不认识 qq,也不判断内容。
|
||||
//
|
||||
// ## 分工:插件只声明,内核只渲染
|
||||
//
|
||||
// 插件:DeclarePromptSection / RemovePromptSection (只声明)
|
||||
// 内核:持有注册表 + 每轮重建 system prompt 时渲染 (只渲染)
|
||||
//
|
||||
// ★ 为什么声明必须由**内核**持有,而不是各 Agent 自己存一份
|
||||
//
|
||||
// 本仓栽过这个坑:技能索引曾经是内核 Agent 上的一个字段,由启动代码
|
||||
// 手工接线到**根 agent**。于是 rtchild、驻留子各自构造时都要记得再传一遍 ——
|
||||
// 漏传不报错、不崩,只是那个 agent 的提示词里一条技能说明都没有,
|
||||
// 模型只能凭工具名猜用法。
|
||||
//
|
||||
// 生产症状(真实日志):A2A 分诊助手(驻留子)手上没有 cmd_run
|
||||
// (cmd 插件 auto_start=false,需先 plugin_start),而 cmd-tools skill
|
||||
// 正文里写着"先 plugin_start cmd"。它看不到这条 skill,于是把
|
||||
// terminal_create 当成一次性命令执行器 —— 每条 `cd … && echo … && grep …`
|
||||
// 开一个新终端,命令结束终端就退出,terminal_read 立刻撞
|
||||
// `read /dev/ptmx: input/output error`,然后为下一条命令再开一个。
|
||||
//
|
||||
// 结论:**能力归属内核(共享注册表),插件只声明**。这样任何 agent
|
||||
// (含未来新增类型)读同一份注册表,零接线。
|
||||
//
|
||||
// ## 幂等语义("可运行时更新"的正确形态)
|
||||
//
|
||||
// 同一 Section 重复声明 ⇒ **覆盖**,不是追加。
|
||||
//
|
||||
// 这不是风格问题:system prompt 每轮重建,一次任务里 stepPrepare 可能
|
||||
// 被重入(上下文裁剪后回到该步)。若声明是追加,索引会越堆越多,
|
||||
// 最终挤掉真正的对话历史。注册表用 map 存段,让"多份"在结构上不可能发生。
|
||||
//
|
||||
// ## 典型用法(skillmgr)
|
||||
//
|
||||
// // Start:声明一段静态说明 + 当前索引
|
||||
// s.SystemPrompt().DeclarePromptSection(sdk.PromptSection{
|
||||
// Section: "skillmgr.skills",
|
||||
// Owner: "skillmgr",
|
||||
// Order: 100,
|
||||
// Content: skillIndexText(),
|
||||
// })
|
||||
//
|
||||
// // 技能增删/启停后:重新声明同一段即可(自动覆盖)
|
||||
// s.SystemPrompt().DeclarePromptSection(...)
|
||||
//
|
||||
// // Stop / 停用:移除,避免模型去调已不存在的 skill
|
||||
// s.SystemPrompt().RemovePromptSection("skillmgr.skills")
|
||||
//
|
||||
// ## 名字约束
|
||||
//
|
||||
// 段名必须是 `<owner>.<名字>`,其中 owner 等于插件名。这条在内核侧强制
|
||||
// (否则插件 A 能声明 `skillmgr.skills` 并夺走插件 B 的段)。
|
||||
type SystemPromptAPI interface {
|
||||
// DeclarePromptSection 声明或更新一段系统提示词内容。
|
||||
//
|
||||
// 同 Section 重复声明会覆盖(同一 owner 只能覆盖自己的段,
|
||||
// 不能覆盖别人的)。Content 为空串等价于移除该段。
|
||||
//
|
||||
// 返回 error 的情形:
|
||||
// · Section 不符合 `<owner>.<名字>` 形式
|
||||
// · Owner 与 Section 前缀不一致(试图声明别人的段)
|
||||
// · 内核未提供注册表(系统提示词声明面未装配)
|
||||
DeclarePromptSection(s PromptSection) error
|
||||
|
||||
// RemovePromptSection 移除一段声明。
|
||||
//
|
||||
// 单独提供而不是"传空 Content":插件 Stop / 被停用时这是常见动作,
|
||||
// 写成空串容易漏 —— 漏了就会出现"插件已停用,但它的说明还留在
|
||||
// 提示词里,模型去调一个不存在的工具",比没有说明更糟。
|
||||
//
|
||||
// 移除不存在或不属于自己的段是静默成功(幂等,便于无条件收尾)。
|
||||
RemovePromptSection(section string) error
|
||||
}
|
||||
|
||||
// PromptSection 是一段声明的系统提示词内容。
|
||||
type PromptSection struct {
|
||||
// Section 是段名,必须形如 `<owner>.<名字>`(如 "skillmgr.skills")。
|
||||
Section string
|
||||
// Owner 是声明者(插件名),必须与 Section 前缀一致。
|
||||
Owner string
|
||||
|
||||
// Order 决定渲染顺序(小的在前)。同 Order 按段名字典序,保证稳定。
|
||||
//
|
||||
// 约定给个宽松的区段,便于后续插入:
|
||||
// 0–99 内核/框架性质(极少用)
|
||||
// 100–199 工具与技能使用说明(skillmgr 用 100)
|
||||
// 200–299 身份与权限说明
|
||||
// 900+ 其他
|
||||
Order int
|
||||
|
||||
// Content 是正文(空串 = 移除该段)。
|
||||
Content string
|
||||
|
||||
// Volatile 表示内容会随运行时频繁变化(如技能索引)。
|
||||
//
|
||||
// 只用于诊断(状态面能区分"静态说明"与"每轮重算"),不参与渲染决策。
|
||||
Volatile bool
|
||||
}
|
||||
Reference in New Issue
Block a user