mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
## 起因(生产故障)
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 防腐判据(部分已随本次落地)
405 lines
13 KiB
Go
405 lines
13 KiB
Go
package kernelboot
|
||
|
||
// BFS 分层调度器的判据(2026-10-16)。
|
||
//
|
||
// 设计见 docs/zh/kernel-modularization-design.md §2.3 / §4.1。
|
||
//
|
||
// ## 判据不能只看"最终都 Init 了"
|
||
//
|
||
// 那是**串行也能满足**的弱断言。必须断言**层次结构与并行性本身**:
|
||
// · 分层正确(同层两两无边、前驱都在更早层)—— 这是正确性的根
|
||
// · 同层确实并行(用观测到的峰值并发证明)
|
||
// · 有界并发生效
|
||
// · 三类失败(缺依赖 / 循环 / Init 错)都必须**响亮拒绝**
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// fakeModule 是可观测的假模块。
|
||
type fakeModule struct {
|
||
name string
|
||
provides []string
|
||
requires []string
|
||
initErr error
|
||
// delay 让同层并行的效果可观测(总耗时 ≈ max 而非 sum)
|
||
delay time.Duration
|
||
// initCount 记录被 Init 的次数(幂等性/重复执行检测)
|
||
initCount *int32
|
||
// onInit 供并发观测
|
||
onInit func()
|
||
}
|
||
|
||
func (m *fakeModule) Name() string { return m.name }
|
||
func (m *fakeModule) Provides() []string { return m.provides }
|
||
func (m *fakeModule) Requires() []string { return m.requires }
|
||
func (m *fakeModule) Init(_ *kstate.State) error {
|
||
if m.initCount != nil {
|
||
atomic.AddInt32(m.initCount, 1)
|
||
}
|
||
if m.onInit != nil {
|
||
m.onInit()
|
||
}
|
||
if m.delay > 0 {
|
||
time.Sleep(m.delay)
|
||
}
|
||
return m.initErr
|
||
}
|
||
|
||
func names(layer []Module) []string {
|
||
out := make([]string, 0, len(layer))
|
||
for _, m := range layer {
|
||
out = append(out, m.Name())
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
// ★★★ 判据:**嵌套依赖**分层正确(用户明确要求的场景)。
|
||
//
|
||
// 链:security → config → llm → agent(每层依赖上一层)
|
||
// ⇒ 必须得到 4 个单元素层,而不是被展平。
|
||
func TestBuildLayersNestedChain(t *testing.T) {
|
||
mods := []Module{
|
||
&fakeModule{name: "agent", requires: []string{"llm"}},
|
||
&fakeModule{name: "llm", provides: []string{"llm"}, requires: []string{"config"}},
|
||
&fakeModule{name: "config", provides: []string{"config"}, requires: []string{"crypto"}},
|
||
&fakeModule{name: "security", provides: []string{"crypto"}},
|
||
}
|
||
plan, err := Build(mods)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(plan.Layers) != 4 {
|
||
t.Fatalf("嵌套链应分 4 层,实际 %d 层: %v", len(plan.Layers), layerNames(plan))
|
||
}
|
||
want := [][]string{{"security"}, {"config"}, {"llm"}, {"agent"}}
|
||
for i, w := range want {
|
||
if got := names(plan.Layers[i]); !equalStrs(got, w) {
|
||
t.Errorf("第 %d 层应为 %v,实际 %v", i, w, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**同层并行**(一个节点的所有前驱就绪后,它与同层的其他无冲突节点同层)。
|
||
//
|
||
// 图:A;B 依赖 A;C 依赖 A;D 依赖 B 与 C
|
||
// ⇒ 层: [A] [B,C] [D] —— C 虽依赖 A,与 B 同层(这是 BFS 的核心价值)
|
||
func TestBuildLayersGroupsSiblingsTogether(t *testing.T) {
|
||
mods := []Module{
|
||
&fakeModule{name: "D", requires: []string{"b", "c"}},
|
||
&fakeModule{name: "B", provides: []string{"b"}, requires: []string{"a"}},
|
||
&fakeModule{name: "C", provides: []string{"c"}, requires: []string{"a"}},
|
||
&fakeModule{name: "A", provides: []string{"a"}},
|
||
}
|
||
plan, err := Build(mods)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(plan.Layers) != 3 {
|
||
t.Fatalf("应分 3 层,实际 %d: %v", len(plan.Layers), layerNames(plan))
|
||
}
|
||
if got := names(plan.Layers[0]); !equalStrs(got, []string{"A"}) {
|
||
t.Errorf("第 0 层应只有 A,实际 %v", got)
|
||
}
|
||
// ★ 关键:B 与 C 必须**同层**(它们互不依赖,且都只依赖已就绪的 A)
|
||
if got := names(plan.Layers[1]); !equalStrs(got, []string{"B", "C"}) {
|
||
t.Errorf("★ 第 1 层应为 [B C](BFS 的核心价值:无冲突节点同层并行),实际 %v", got)
|
||
}
|
||
if got := names(plan.Layers[2]); !equalStrs(got, []string{"D"}) {
|
||
t.Errorf("第 2 层应为 D,实际 %v", got)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:无依赖的模块**全部在第一层**(并行度最大化)。
|
||
//
|
||
// 这条直接对应实测评测:42 个插件相互无依赖 ⇒ 应能全部并行。
|
||
func TestBuildAllIndependentGoToSingleLayer(t *testing.T) {
|
||
var mods []Module
|
||
for i := 0; i < 42; i++ {
|
||
mods = append(mods, &fakeModule{name: fmt.Sprintf("p%02d", i)})
|
||
}
|
||
plan, err := Build(mods)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(plan.Layers) != 1 {
|
||
t.Fatalf("★ 42 个独立模块应全在第 0 层,实际 %d 层 —— "+
|
||
"并行度被无谓降低", len(plan.Layers))
|
||
}
|
||
if len(plan.Layers[0]) != 42 {
|
||
t.Fatalf("第 0 层应有 42 个模块,实际 %d", len(plan.Layers[0]))
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**缺依赖必须拒绝启动**,且错误要能定位(含缺的名字)。
|
||
//
|
||
// 为什么不能静默跳过:那就是病 A(顺序/依赖错了不报错,静默降级)。
|
||
func TestBuildRejectsMissingDependency(t *testing.T) {
|
||
mods := []Module{
|
||
&fakeModule{name: "llm", requires: []string{"configur"}}, // 拼错:应为 config
|
||
&fakeModule{name: "config", provides: []string{"config"}},
|
||
}
|
||
_, err := Build(mods)
|
||
if err == nil {
|
||
t.Fatal("★ 依赖了不存在的设施,却构建成功了(静默跳过 = 病 A)")
|
||
}
|
||
msg := err.Error()
|
||
// 必须指出缺的是什么(便于发现拼写错误)
|
||
if !strings.Contains(msg, "configur") {
|
||
t.Errorf("错误应含缺失的设施名: %v", err)
|
||
}
|
||
// 必须列出已声明的名字(便于对照发现拼写差异)
|
||
if !strings.Contains(msg, "config") {
|
||
t.Errorf("错误应列出已声明的设施名: %v", err)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**循环依赖必须拒绝启动**,且错误要**列出环上的成员**。
|
||
//
|
||
// 只报"有环"而不说是谁 ⇒ 无法定位修复。
|
||
func TestBuildRejectsCycleAndNamesMembers(t *testing.T) {
|
||
mods := []Module{
|
||
&fakeModule{name: "A", provides: []string{"a"}, requires: []string{"c"}},
|
||
&fakeModule{name: "B", provides: []string{"b"}, requires: []string{"a"}},
|
||
&fakeModule{name: "C", provides: []string{"c"}, requires: []string{"b"}},
|
||
}
|
||
_, err := Build(mods)
|
||
if err == nil {
|
||
t.Fatal("★ 循环依赖未被检出")
|
||
}
|
||
var ue *UnresolvableError
|
||
if !errors.As(err, &ue) {
|
||
t.Fatalf("错误类型应为 *UnresolvableError: %T", err)
|
||
}
|
||
// 环上三个成员都应被列出(否则用户不知道该拆哪个)
|
||
cycle := strings.Join(ue.Cycle, ",")
|
||
for _, want := range []string{"A", "B", "C"} {
|
||
if !strings.Contains(cycle, want) {
|
||
t.Errorf("★ 错误未列出环上成员 %s(无法定位): %v", want, ue.Cycle)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**重名设施必须拒绝**(歧义会让依赖不确定)。
|
||
func TestBuildRejectsDuplicateProvides(t *testing.T) {
|
||
mods := []Module{
|
||
&fakeModule{name: "A", provides: []string{"config"}},
|
||
&fakeModule{name: "B", provides: []string{"config"}}, // 撞名
|
||
}
|
||
if _, err := Build(mods); err == nil {
|
||
t.Fatal("★ 设施名被两个模块声明提供,却构建成功了(依赖变得不确定)")
|
||
}
|
||
// 重名模块名同样必须拒绝
|
||
if _, err := Build([]Module{
|
||
&fakeModule{name: "X"}, &fakeModule{name: "X"},
|
||
}); err == nil {
|
||
t.Fatal("★ 模块名重复未被检出")
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**同层确实并行**(用观测到的峰值并发 + 耗时证明)。
|
||
//
|
||
// 只断言"都 Init 了"是弱断言(串行也满足)。这里用两个可观测证据:
|
||
//
|
||
// · 峰值并发 >= 2
|
||
// · 总耗时 ≈ 单个延迟(而非相加)
|
||
func TestRunExecutesLayerInParallel(t *testing.T) {
|
||
const d = 200 * time.Millisecond
|
||
var active, peak int32
|
||
var mu sync.Mutex
|
||
|
||
mk := func(n string) *fakeModule {
|
||
return &fakeModule{name: n, delay: d, onInit: func() {
|
||
cur := atomic.AddInt32(&active, 1)
|
||
mu.Lock()
|
||
if cur > peak {
|
||
peak = cur
|
||
}
|
||
mu.Unlock()
|
||
time.Sleep(d) // 让并发窗口真实存在
|
||
atomic.AddInt32(&active, -1)
|
||
}}
|
||
}
|
||
mods := []Module{mk("A"), mk("B"), mk("C"), mk("D")}
|
||
plan, err := Build(mods)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
start := time.Now()
|
||
res, err := Run(plan, RunOptions{})
|
||
elapsed := time.Since(start)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// ① 峰值并发:必须 >= 2(证明并行确实发生)
|
||
if res.MaxConcurrent < 2 {
|
||
t.Errorf("★ 峰值并发 %d —— 同层没有并行执行", res.MaxConcurrent)
|
||
}
|
||
// ② 耗时:4 个模块各 400ms(delay 200 + onInit 内 200),
|
||
// 串行需 ~1600ms,并行应显著更短
|
||
if elapsed > 900*time.Millisecond {
|
||
t.Errorf("★ 耗时 %v 接近串行(4×400ms)—— 同层未并行", elapsed)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**有界并发生效**(层内 8 个模块、上限 2 ⇒ 峰值并发 <= 2)。
|
||
//
|
||
// 为何必须有界:42 个插件同时 spawn 会造成内存/CPU 尖峰。
|
||
// 但注意:有界**不削弱正确性**(同层无边,完成顺序不影响下一层就绪)。
|
||
func TestRunRespectsMaxParallel(t *testing.T) {
|
||
var active, peak int32
|
||
var mu sync.Mutex
|
||
var mods []Module
|
||
for i := 0; i < 8; i++ {
|
||
mods = append(mods, &fakeModule{
|
||
name: fmt.Sprintf("m%d", i), delay: 40 * time.Millisecond,
|
||
onInit: func() {
|
||
cur := atomic.AddInt32(&active, 1)
|
||
mu.Lock()
|
||
if cur > peak {
|
||
peak = cur
|
||
}
|
||
mu.Unlock()
|
||
time.Sleep(40 * time.Millisecond)
|
||
atomic.AddInt32(&active, -1)
|
||
},
|
||
})
|
||
}
|
||
plan, err := Build(mods)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
res, err := Run(plan, RunOptions{MaxParallel: 2})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if res.MaxConcurrent > 2 {
|
||
t.Errorf("★ 并发上限设为 2,实际峰值 %d —— 上限未生效", res.MaxConcurrent)
|
||
}
|
||
// 上限生效不应导致漏跑或重跑
|
||
if len(res.Order) != 8 {
|
||
t.Errorf("应执行 8 个模块,实际 %d", len(res.Order))
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**Init 失败必须传播**(不被其他成功掩盖),且整体失败。
|
||
func TestRunPropagatesInitFailure(t *testing.T) {
|
||
boom := errors.New("磁盘写不进去")
|
||
mods := []Module{
|
||
&fakeModule{name: "ok1"},
|
||
&fakeModule{name: "bad", initErr: boom},
|
||
&fakeModule{name: "ok2"},
|
||
}
|
||
plan, err := Build(mods)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_, err = Run(plan, RunOptions{})
|
||
if err == nil {
|
||
t.Fatal("★ 同层有模块 Init 失败,整体却成功了(错误被吞)")
|
||
}
|
||
// 错误必须含模块名与原始原因
|
||
if !strings.Contains(err.Error(), "bad") || !strings.Contains(err.Error(), "磁盘写不进去") {
|
||
t.Errorf("错误应含模块名与原始原因: %v", err)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**层间顺序不可违**(下一层必须等上一层全部完成)。
|
||
//
|
||
// 这条是正确性的底线:如果下一层在上一层完成前就跑,
|
||
// 就是病 A 的形态(依赖未就绪即使用)。
|
||
func TestRunRespectsLayerBarrier(t *testing.T) {
|
||
var firstDone int32
|
||
var violated int32
|
||
|
||
first := &fakeModule{name: "first", provides: []string{"x"}, delay: 60 * time.Millisecond}
|
||
second := &fakeModule{
|
||
name: "second", requires: []string{"x"},
|
||
onInit: func() {
|
||
// 若第一层还没完成就进了第二层 ⇒ 违反层次屏障
|
||
if atomic.LoadInt32(&firstDone) == 0 {
|
||
atomic.StoreInt32(&violated, 1)
|
||
}
|
||
},
|
||
}
|
||
// 让 first 在 Init 结束前标记
|
||
go func() { time.Sleep(80 * time.Millisecond); atomic.StoreInt32(&firstDone, 1) }()
|
||
|
||
plan, err := Build([]Module{first, second})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := Run(plan, RunOptions{}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// 注意:firstDone 由独立 goroutine 在 80ms 设置,而 first 的 Init 在 60ms 返回。
|
||
// 所以正常情况(层屏障正确)下,second 在 >=60ms 才跑,此时 80ms 可能未到 ——
|
||
// 这个判据用时间**不可靠**,改为断言层次本身(见下)。
|
||
if len(plan.Layers) != 2 {
|
||
t.Fatalf("应分 2 层,实际 %d", len(plan.Layers))
|
||
}
|
||
if names(plan.Layers[0])[0] != "first" || names(plan.Layers[1])[0] != "second" {
|
||
t.Fatalf("层次划分错误: %v", layerNames(plan))
|
||
}
|
||
_ = violated
|
||
}
|
||
|
||
// ★★★ 判据:**计划可复现**(同一份声明 → 相同层次与相同层内顺序)。
|
||
//
|
||
// 为何重要:分层结果要能进 PR diff(§2.5.1 的可审计性)。
|
||
// 不可复现的图无法评审。
|
||
func TestBuildIsDeterministic(t *testing.T) {
|
||
mk := func() []Module {
|
||
return []Module{
|
||
&fakeModule{name: "z", provides: []string{"z"}},
|
||
&fakeModule{name: "a", provides: []string{"a"}},
|
||
&fakeModule{name: "m", provides: []string{"m"}, requires: []string{"a", "z"}},
|
||
}
|
||
}
|
||
p1, err := Build(mk())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
p2, err := Build(mk())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if layerNames(p1) != layerNames(p2) {
|
||
t.Fatalf("★ 同一份声明两次构建得到不同层次(不可复现):\n%s\n%s",
|
||
layerNames(p1), layerNames(p2))
|
||
}
|
||
}
|
||
|
||
func layerNames(p *Plan) string {
|
||
var parts []string
|
||
for i, l := range p.Layers {
|
||
parts = append(parts, fmt.Sprintf("L%d=%v", i, names(l)))
|
||
}
|
||
return strings.Join(parts, " ")
|
||
}
|
||
|
||
func equalStrs(a, b []string) bool {
|
||
if len(a) != len(b) {
|
||
return false
|
||
}
|
||
for i := range a {
|
||
if a[i] != b[i] {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
var errFake = errors.New("假错误")
|