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 防腐判据(部分已随本次落地)
291 lines
9.3 KiB
Go
291 lines
9.3 KiB
Go
package kstate
|
||
|
||
// syncbus:内核就绪状态的同步快照,驱动插件调度(设计 §2.6)。
|
||
//
|
||
// ## 一句话
|
||
//
|
||
// 内核每完成一层装配就把该层贡献的设施标记为"就绪";
|
||
// 插件管理器**只读**这份状态,据此拉起"依赖已齐"的插件。
|
||
//
|
||
// ## 为什么要有它(解决什么)
|
||
//
|
||
// 现状(`cmd/homed/main.go:291 loadPlugins`)是**一刀切**:
|
||
// 12 个 init* 全部做完才加载全部插件。
|
||
// 于是"只读本地文件"的插件也要等多模态空间(ONNX 加载)+ 知识库建索引做完。
|
||
//
|
||
// 而内核那 12 步本身有明确先后(§2.3 算出 4 层),"阶段"是天然存在的,
|
||
// 只是此前没有对外发布。syncbus 就是那个发布物。
|
||
//
|
||
// ## ★ 权限:插件管理器只读(这不是习惯,是设计)
|
||
//
|
||
// 若插件管理器能写 syncbus,它就能**谎报内核状态**,让插件在自己依赖的东西
|
||
// 还不存在时启动 —— 那正是病 A(静默顺序错)。
|
||
//
|
||
// 所以类型层面分离:内核拿 `*SyncBus`(可写),
|
||
// 插件管理器只拿 `SyncState` 接口(**上面没有写方法**)。
|
||
// 想写也写不了 —— 编译期保证,而非"约定不要写"。
|
||
// 同 `Get[T]` 泛型取用(类型错=编译不过)是一个思路。
|
||
|
||
import (
|
||
"sort"
|
||
"sync"
|
||
)
|
||
|
||
// SyncState 是 syncbus 的**只读**视图。
|
||
//
|
||
// ★ 刻意不含 MarkReady/Reset:插件管理器拿到的就是这个接口,
|
||
// 因此它**在编译期就不可能**篡改内核状态。
|
||
type SyncState interface {
|
||
// IsReady 报告某设施是否已就绪。
|
||
IsReady(facility string) bool
|
||
// Snapshot 返回当前全部就绪状态的副本(调用方改不动内部)。
|
||
Snapshot() map[string]bool
|
||
// ReadyInOrder 按**就绪顺序**返回设施名(调试/判据用)。
|
||
ReadyInOrder() []string
|
||
// KernelComplete 报告内核是否已完全初始化(最后一层完成)。
|
||
KernelComplete() bool
|
||
// AllReady 报告给定设施是否**全部**就绪(插件调度器的核心判断)。
|
||
//
|
||
// ★ 空列表返回 false(不是 true):见实现处的说明 ——
|
||
// "没有依赖"与"依赖全就绪"是不同语义,混淆会让未声明依赖的插件提前拉起。
|
||
AllReady(facilities []string) bool
|
||
|
||
// Known 报告某设施名**是否被内核注册过**(不论是否已就绪)。
|
||
//
|
||
// ## ★ 为什么必须有它(修一个"静默永不启动"的缺口)
|
||
//
|
||
// 插件声明 `requires_facilities` 是**字符串**。若拼错(如写 "llm " 或
|
||
// "llmservice"),原本的行为是:它永远等不到 → **永不拉起**,
|
||
// 且**没有任何报错**、没有超时兜底(我第一版设计文档写了"超时兜底"
|
||
// 但从未实现)。
|
||
//
|
||
// 这正是本仓反复吃亏的"静默降级"形态:系统看着正常,某插件就是不工作。
|
||
//
|
||
// ⇒ 有了 Known,调度器能在**提交时**立刻报错:
|
||
// "插件 X 声明了内核从未注册过的设施 Y(可能是拼写错误)"
|
||
// 并列出全部已知设施名供对照。
|
||
Known(facility string) bool
|
||
|
||
// KnownFacilities 返回内核**注册过的全部设施名**(含尚未就绪的)。
|
||
//
|
||
// 用途:错误信息里列出可选值(让"拼错了"当场可修)。
|
||
KnownFacilities() []string
|
||
}
|
||
|
||
// SyncBus 是可写的同步总线(内核持有)。
|
||
type SyncBus struct {
|
||
mu sync.RWMutex
|
||
// known 是内核**注册过的全部设施名**(不论是否已就绪)。
|
||
//
|
||
// ★ 它存在的唯一目的是让"插件拼错设施名"从**静默永不启动**
|
||
// 变成**提交时立刻报错**(见 SyncState.Known 的说明)。
|
||
known map[string]bool
|
||
ready map[string]bool
|
||
order []string
|
||
complete bool
|
||
onUpdate []func(SyncState)
|
||
notifyMu sync.Mutex
|
||
}
|
||
|
||
// NewSyncBus 创建空总线。
|
||
func NewSyncBus() *SyncBus {
|
||
return &SyncBus{ready: map[string]bool{}, known: map[string]bool{}}
|
||
}
|
||
|
||
// MarkReady 标记一个设施就绪。**只有内核该调它。**
|
||
//
|
||
// 幂等:重复标记同一设施不报错(便于层边界回调重入)。
|
||
// 但**不重复记录**顺序,保证 ReadyInOrder 可复现。
|
||
func (b *SyncBus) MarkReady(facility string) {
|
||
if facility == "" {
|
||
return
|
||
}
|
||
b.mu.Lock()
|
||
if b.ready[facility] {
|
||
b.mu.Unlock()
|
||
return
|
||
}
|
||
b.ready[facility] = true
|
||
b.order = append(b.order, facility)
|
||
b.mu.Unlock()
|
||
b.fire()
|
||
}
|
||
|
||
// MarkLayerReady 在**层边界**标记该层所有模块贡献的设施(设计 §2.6.2)。
|
||
//
|
||
// ★ 为何以"层"而不是"单个模块"为同步点:
|
||
// 层内是**并发**执行的,若层内逐个模块标记就绪,就绪顺序会**不确定**,
|
||
// 而判据要求可复现。层边界天然是一个确定的同步点。
|
||
func (b *SyncBus) MarkLayerReady(provides []string) {
|
||
// 先在锁内一次性写入,再统一通知(避免 N 次通知、N 次唤醒)
|
||
b.mu.Lock()
|
||
changed := false
|
||
for _, f := range provides {
|
||
if f == "" {
|
||
continue
|
||
}
|
||
b.known[f] = true // 注册过(不论是否首次就绪)
|
||
if b.ready[f] {
|
||
continue
|
||
}
|
||
b.ready[f] = true
|
||
b.order = append(b.order, f)
|
||
changed = true
|
||
}
|
||
b.mu.Unlock()
|
||
if changed {
|
||
b.fire()
|
||
}
|
||
}
|
||
|
||
// MarkDeclared 在**执行前**登记一批设施名(视为"内核注册过")。
|
||
//
|
||
// ## ★ 为什么需要它(时序)
|
||
//
|
||
// 插件的 `requires_facilities` 校验要在**插件提交时**做(那是错误最该被发现
|
||
// 的时刻,紧挨着写错的插件作者)。但插件的提交发生在内核装配**完成之后**
|
||
// —— 那时所有层都跑完了,Known 自然齐了。
|
||
//
|
||
// 但仍然需要本方法,因为:
|
||
//
|
||
// · 装配**中途失败**时,后面的层没跑 ⇒ 那些设施名从未登记 ⇒
|
||
// 若此时还有插件提交,会报"内核从未注册过 X"(**误报**:其实是没跑到)
|
||
// · 判据要能在**不执行**的情况下校验"插件声明 ⊆ 内核声明"
|
||
// (只看声明,不看执行结果)
|
||
//
|
||
// ⇒ 装配开始时把**全部模块的 Provides** 一次性登记为 known。
|
||
// 这样 Known 的语义精确为"**模块声明过**这个设施",与"是否已就绪"解耦。
|
||
func (b *SyncBus) MarkDeclared(facilities []string) {
|
||
b.mu.Lock()
|
||
for _, f := range facilities {
|
||
if f != "" {
|
||
b.known[f] = true
|
||
}
|
||
}
|
||
b.mu.Unlock()
|
||
}
|
||
|
||
// MarkKernelComplete 标记内核已完全初始化。
|
||
//
|
||
// 这是**未声明依赖的插件**的启动信号(默认路径 = 现行为,§2.6.5)。
|
||
func (b *SyncBus) MarkKernelComplete() {
|
||
b.mu.Lock()
|
||
if b.complete {
|
||
b.mu.Unlock()
|
||
return
|
||
}
|
||
b.complete = true
|
||
b.mu.Unlock()
|
||
b.fire()
|
||
}
|
||
|
||
// OnUpdate 注册状态变化回调(插件调度器用它重新检查挂起的插件)。
|
||
//
|
||
// 回调在**不持锁**的情况下调用 —— 否则回调里再读 syncbus 会死锁。
|
||
// (这个坑很容易踩:回调显然要读状态,持锁调用必然死锁。)
|
||
func (b *SyncBus) OnUpdate(fn func(SyncState)) {
|
||
b.notifyMu.Lock()
|
||
b.onUpdate = append(b.onUpdate, fn)
|
||
b.notifyMu.Unlock()
|
||
}
|
||
|
||
func (b *SyncBus) fire() {
|
||
b.notifyMu.Lock()
|
||
cbs := make([]func(SyncState), len(b.onUpdate))
|
||
copy(cbs, b.onUpdate)
|
||
b.notifyMu.Unlock()
|
||
for _, fn := range cbs {
|
||
fn(b) // b 实现了 SyncState(只读方法)
|
||
}
|
||
}
|
||
|
||
// --- SyncState 实现(只读) ---
|
||
|
||
func (b *SyncBus) IsReady(facility string) bool {
|
||
b.mu.RLock()
|
||
defer b.mu.RUnlock()
|
||
return b.ready[facility]
|
||
}
|
||
|
||
func (b *SyncBus) Snapshot() map[string]bool {
|
||
b.mu.RLock()
|
||
defer b.mu.RUnlock()
|
||
out := make(map[string]bool, len(b.ready))
|
||
for k, v := range b.ready {
|
||
out[k] = v
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (b *SyncBus) ReadyInOrder() []string {
|
||
b.mu.RLock()
|
||
defer b.mu.RUnlock()
|
||
out := make([]string, len(b.order))
|
||
copy(out, b.order)
|
||
return out
|
||
}
|
||
|
||
func (b *SyncBus) KernelComplete() bool {
|
||
b.mu.RLock()
|
||
defer b.mu.RUnlock()
|
||
return b.complete
|
||
}
|
||
|
||
// Known 报告某设施名是否被内核注册过(不论是否已就绪)。
|
||
//
|
||
// ★ 为何"注册过但未就绪"也算 Known:那是**正常的中间状态**
|
||
// (如 llm 声明了但还在第 1 层)。真正的错误是"内核从未注册过这个名字"。
|
||
func (b *SyncBus) Known(facility string) bool {
|
||
b.mu.RLock()
|
||
defer b.mu.RUnlock()
|
||
return b.known[facility]
|
||
}
|
||
|
||
// KnownFacilities 返回内核注册过的全部设施名(升序,便于对照阅读)。
|
||
func (b *SyncBus) KnownFacilities() []string {
|
||
b.mu.RLock()
|
||
defer b.mu.RUnlock()
|
||
out := make([]string, 0, len(b.known))
|
||
for k := range b.known {
|
||
out = append(out, k)
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
// AllReady 报告给定的设施**全部**就绪(插件调度器的核心判断)。
|
||
//
|
||
// 空列表 = 真的全就绪 → 返回 false。
|
||
//
|
||
// ★ 为何空列表返回 false 而不是 true:
|
||
// "没有依赖" 与 "依赖全就绪" 是**不同语义**,混淆会让"未声明依赖的插件"
|
||
// 被当成"依赖已满足"而**提前拉起** —— 那就破坏了 §2.6.5 的存量兼容。
|
||
// 未声明走的是 KernelComplete 那条路径,与这条刻意分开。
|
||
func (b *SyncBus) AllReady(facilities []string) bool {
|
||
if len(facilities) == 0 {
|
||
return false
|
||
}
|
||
b.mu.RLock()
|
||
defer b.mu.RUnlock()
|
||
for _, f := range facilities {
|
||
if !b.ready[f] {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// Missing 返回尚未就绪的设施名(诊断用:告诉用户"还差什么")。
|
||
func (b *SyncBus) Missing(facilities []string) []string {
|
||
b.mu.RLock()
|
||
defer b.mu.RUnlock()
|
||
var out []string
|
||
for _, f := range facilities {
|
||
if !b.ready[f] {
|
||
out = append(out, f)
|
||
}
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|