mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 01:07:35 +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:
@ -9,6 +9,7 @@ package kernelboot
|
||||
// 静默改变启动时机 = 静默引入竞态。
|
||||
|
||||
import (
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -24,7 +25,7 @@ import (
|
||||
// 反例(若实现错误):把"未声明"当成"依赖集为空",而空集在数学上已满足
|
||||
// ⇒ 第一层完成后就拉起 ⇒ 比现状早得多。
|
||||
func TestSchedulerUndeclaredWaitsForKernelComplete(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
var launched []string
|
||||
var mu sync.Mutex
|
||||
sch := NewScheduler(bus, func(p PluginSpec) error {
|
||||
@ -63,7 +64,7 @@ func TestSchedulerUndeclaredWaitsForKernelComplete(t *testing.T) {
|
||||
//
|
||||
// 断言"早于内核完成" —— 否则这条判据在"等内核完成"的实现下也会通过(假绿)。
|
||||
func TestSchedulerDeclaredLaunchesEarly(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
var launched []string
|
||||
var mu sync.Mutex
|
||||
sch := NewScheduler(bus, func(p PluginSpec) error {
|
||||
@ -98,7 +99,7 @@ func TestSchedulerDeclaredLaunchesEarly(t *testing.T) {
|
||||
|
||||
// ★★★ 判据:**依赖未齐则挂起**,不会误拉起。
|
||||
func TestSchedulerWaitsForMissingDependency(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
var launched []string
|
||||
sch := NewScheduler(bus, func(p PluginSpec) error {
|
||||
launched = append(launched, p.Name)
|
||||
@ -128,21 +129,21 @@ func TestSchedulerWaitsForMissingDependency(t *testing.T) {
|
||||
|
||||
// ★★★ 判据:**只读隔离**(编译期保证,不是约定)。
|
||||
//
|
||||
// 断言 Scheduler 拿到的接口**没有**写方法 —— 如果给它 *SyncBus,
|
||||
// 断言 Scheduler 拿到的接口**没有**写方法 —— 如果给它 *kstate.SyncBus,
|
||||
// 它就能谎报内核状态,让插件在依赖不存在时启动(病 A)。
|
||||
func TestSchedulerCannotWriteSyncState(t *testing.T) {
|
||||
// ★ 检查必须是**接口类型的方法集**,不是某个具体值。
|
||||
//
|
||||
// 我第一次写错了:用 `var st SyncState = NewSyncBus()` 再断言 `st.(writer)` ——
|
||||
// 那检查的是**动态类型**(*SyncBus 当然有 MarkReady),判据当场报假红。
|
||||
// 要约束的是"拿到 SyncState 的人**能不能**调写方法" ⇒ 必须查接口的方法集。
|
||||
rt := reflect.TypeOf((*SyncState)(nil)).Elem()
|
||||
// 我第一次写错了:用 `var st kstate.SyncState = kstate.NewSyncBus()` 再断言 `st.(writer)` ——
|
||||
// 那检查的是**动态类型**(*kstate.SyncBus 当然有 MarkReady),判据当场报假红。
|
||||
// 要约束的是"拿到 kstate.SyncState 的人**能不能**调写方法" ⇒ 必须查接口的方法集。
|
||||
rt := reflect.TypeOf((*kstate.SyncState)(nil)).Elem()
|
||||
forbidden := []string{"MarkReady", "MarkKernelComplete", "MarkLayerReady"}
|
||||
for i := 0; i < rt.NumMethod(); i++ {
|
||||
m := rt.Method(i).Name
|
||||
for _, bad := range forbidden {
|
||||
if m == bad {
|
||||
t.Fatalf("★ SyncState(插件管理器拿到的接口)暴露了写方法 %s —— "+
|
||||
t.Fatalf("★ kstate.SyncState(插件管理器拿到的接口)暴露了写方法 %s —— "+
|
||||
"它能谎报内核状态,让插件在依赖不存在时启动(病 A)", m)
|
||||
}
|
||||
}
|
||||
@ -150,23 +151,23 @@ func TestSchedulerCannotWriteSyncState(t *testing.T) {
|
||||
// 反向确认:读方法必须在(否则接口退化到没用了)
|
||||
for _, want := range []string{"IsReady", "Snapshot", "AllReady", "KernelComplete"} {
|
||||
if _, ok := rt.MethodByName(want); !ok {
|
||||
t.Errorf("SyncState 缺少读方法 %s", want)
|
||||
t.Errorf("kstate.SyncState 缺少读方法 %s", want)
|
||||
}
|
||||
}
|
||||
// Scheduler 持有的字段类型也必须是 SyncState(不能是 *SyncBus)
|
||||
// Scheduler 持有的字段类型也必须是 kstate.SyncState(不能是 *kstate.SyncBus)
|
||||
st := reflect.TypeOf(Scheduler{})
|
||||
f, ok := st.FieldByName("bus")
|
||||
if !ok {
|
||||
t.Fatal("Scheduler 应有 bus 字段")
|
||||
}
|
||||
if f.Type.String() != "kernelboot.SyncState" {
|
||||
t.Fatalf("★ Scheduler.bus 的类型是 %s —— 必须是只读的 SyncState", f.Type)
|
||||
if f.Type.String() != "kstate.SyncState" {
|
||||
t.Fatalf("★ Scheduler.bus 的类型是 %s —— 必须是只读的 kstate.SyncState", f.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// ★★★ 判据:**不伴随启动的插件即使依赖齐了也不自动拉起**(§2.6.6)。
|
||||
func TestSchedulerAutoStartFalseIsNotLaunched(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
var launched []string
|
||||
sch := NewScheduler(bus, func(p PluginSpec) error {
|
||||
launched = append(launched, p.Name)
|
||||
@ -196,7 +197,7 @@ func TestSyncBusReadyOrderIsDeterministic(t *testing.T) {
|
||||
// 模拟"层内并发完成":同一层 8 个设施,用 goroutine 乱序 MarkReady
|
||||
var first []string
|
||||
for round := 0; round < 5; round++ {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
var wg sync.WaitGroup
|
||||
fac := []string{"f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8"}
|
||||
for i, f := range fac {
|
||||
@ -225,7 +226,7 @@ func TestSyncBusReadyOrderIsDeterministic(t *testing.T) {
|
||||
// ★ 真正的可复现性断言:MarkLayerReady 一次性写入
|
||||
var order []string
|
||||
for round := 0; round < 3; round++ {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
bus.MarkLayerReady([]string{"a", "b", "c"})
|
||||
bus.MarkLayerReady([]string{"d", "e"})
|
||||
got := bus.ReadyInOrder()
|
||||
@ -250,7 +251,7 @@ func TestSyncBusReadyOrderIsDeterministic(t *testing.T) {
|
||||
//
|
||||
// 这条是补的:变异"去掉幂等检查"第一次没被咬住 —— 因为原判据
|
||||
// 只跑了一份声明、没重复标记,测不到幂等性。
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
bus.MarkLayerReady([]string{"a", "b"})
|
||||
bus.MarkLayerReady([]string{"a", "b"}) // 重复
|
||||
bus.MarkLayerReady([]string{"c"})
|
||||
@ -263,7 +264,7 @@ func TestSyncBusReadyOrderIsDeterministic(t *testing.T) {
|
||||
//
|
||||
// 这条守的是:装配中途失败 ⇒ 未声明依赖的插件**不**被拉起。
|
||||
func TestRunDoesNotMarkCompleteOnFailure(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
mods := []Module{
|
||||
&fakeModule{name: "A", provides: []string{"a"}},
|
||||
&fakeModule{name: "B", provides: []string{"b"}, initErr: errFake},
|
||||
@ -285,7 +286,7 @@ func TestRunDoesNotMarkCompleteOnFailure(t *testing.T) {
|
||||
// 为何由 Run 做:模块自己标记会漏(静默漏一个设施 ⇒ 插件永远等不到),
|
||||
// 且顺序取决于层内并发完成序。
|
||||
func TestRunPublishesFacilitiesAtLayerBoundary(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
mods := []Module{
|
||||
&fakeModule{name: "config", provides: []string{"config", "config-write"}},
|
||||
&fakeModule{name: "llm", provides: []string{"llm"}, requires: []string{"config"}},
|
||||
@ -311,7 +312,7 @@ func TestRunPublishesFacilitiesAtLayerBoundary(t *testing.T) {
|
||||
//
|
||||
// 若这条反了,未声明依赖的插件会被提前拉起(见第一条判据)。
|
||||
func TestAllReadyEmptyIsFalse(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
bus.MarkKernelComplete()
|
||||
if bus.AllReady(nil) {
|
||||
t.Fatal("★ AllReady(空) 返回 true —— \"没有依赖\"被当成\"依赖全就绪\",未声明插件会提前拉起")
|
||||
@ -328,7 +329,7 @@ func TestAllReadyEmptyIsFalse(t *testing.T) {
|
||||
|
||||
// ★★★ 判据:**拉起失败不阻塞其他插件**(一个坏插件不该拖垮全局)。
|
||||
func TestSchedulerLaunchFailureDoesNotBlockOthers(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
var okLaunched []string
|
||||
sch := NewScheduler(bus, func(p PluginSpec) error {
|
||||
if p.Name == "bad" {
|
||||
@ -357,7 +358,7 @@ func TestSchedulerLaunchFailureDoesNotBlockOthers(t *testing.T) {
|
||||
|
||||
// ★★★ 判据:**重复 Submit 不重复拉起**(幂等)。
|
||||
func TestSchedulerIsIdempotent(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
n := 0
|
||||
sch := NewScheduler(bus, func(PluginSpec) error { n++; return nil })
|
||||
bus.MarkKernelComplete()
|
||||
@ -378,11 +379,11 @@ func TestSchedulerIsIdempotent(t *testing.T) {
|
||||
// (单个设施的标记),若它持锁回调,只有"既调 MarkReady 又订阅"的场景才会卡住。
|
||||
// 补这条判据把两条路径都盖住。
|
||||
func TestSyncBusMarkReadyNotifiesWithoutDeadlock(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
got := make(chan string, 4)
|
||||
|
||||
// 订阅者**读状态**(真实回调必然会读,这正是死锁触发条件)
|
||||
bus.OnUpdate(func(s SyncState) {
|
||||
bus.OnUpdate(func(s kstate.SyncState) {
|
||||
_ = s.Snapshot() // 持锁调用的话,这里会等 RLock ⇒ 死锁
|
||||
got <- "ok"
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user