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:
1
.gitignore
vendored
1
.gitignore
vendored
@ -134,3 +134,4 @@ dist/
|
||||
|
||||
# 内核升级 CLI 的本地构建产物(与 /homed 同理,不入库)
|
||||
/homed-upgrade
|
||||
/kernelplan.md
|
||||
|
||||
@ -145,19 +145,26 @@ func loadPlugins(cfg *types.Config, cfgReg *internalConfig.ConfigRegistry, plugi
|
||||
log.Printf("[homed] stage host ready with %d registered tools", stageHost.ToolCount())
|
||||
}
|
||||
|
||||
// startAgentRuntime 接线技能索引、起日志管理、启动 agent,返回逆序关停的 cleanup。
|
||||
// startAgentRuntime 起日志管理、启动 agent,返回逆序关停的 cleanup。
|
||||
//
|
||||
// 本函数体是 main() 里对应启动阶段的整块平移:语句、日志文本、错误语义不变,
|
||||
// 只把「*dataDir」变成参数、把 defer 变成由调用点注册的 cleanup。
|
||||
//
|
||||
// ★ 2026-11-04 删掉了这里的"技能索引接线"(原 5 行)。
|
||||
//
|
||||
// 那 5 行是「内核能力靠启动代码手工接给根 agent」的典型:skillmgr 实现了
|
||||
// agentCore.SkillIndexProvider,main 在插件加载后 type-assert 它、再塞进
|
||||
// 根 agent 的字段。于是 rtchild、驻留子各自构造时都得记得再传一遍 ——
|
||||
// 漏传不报错、不崩,只是那个 agent 的提示词里一条 skill 都没有。
|
||||
//
|
||||
// 生产症状:A2A 分诊助手(驻留子)不知道 cmd 插件要先 plugin_start,
|
||||
// 拿 terminal_create 当一次性命令执行器,一条命令开一个终端。
|
||||
//
|
||||
// 现在系统提示词段是**插件声明式**的(sdk.SystemPromptAPI):插件声明进
|
||||
// 内核级注册表(KernelState.PromptSections),任何 agent 每轮重建提示词时
|
||||
// 读同一份 —— 启动代码不需要知道有哪些插件、也不需要 type-assert 任何东西。
|
||||
func startAgentRuntime(cfgReg *internalConfig.ConfigRegistry, pluginReg *plugin.Registry,
|
||||
agent *agentCore.Agent, logDir string, ctx context.Context) func() {
|
||||
// 技能索引接线:skillmgr 插件实现 SkillIndexProvider 时注入 agent(方案B prompt 注入)
|
||||
if sp := pluginReg.Get("skillmgr"); sp != nil {
|
||||
if prov, ok := sp.(agentCore.SkillIndexProvider); ok {
|
||||
agent.SetSkillIndexProvider(prov)
|
||||
log.Printf("[homed] skill index wired from skillmgr plugin")
|
||||
}
|
||||
}
|
||||
|
||||
// 日志管理:层级压缩 + 保留策略
|
||||
logManager := logpkg.NewManager(logDir, cfgReg)
|
||||
|
||||
@ -4,6 +4,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -46,7 +47,7 @@ type kernelAssembleHooks struct {
|
||||
//
|
||||
// 有了本回调,调度器在 **Run 之前**订阅 syncbus ⇒ 内核每完成一层
|
||||
// 都会触发它重新评估 ⇒ 声明了 config 的插件不必等 agent 层。
|
||||
BeforeRun func(bus *kernelboot.SyncBus) error
|
||||
BeforeRun func(bus *kstate.SyncBus) error
|
||||
|
||||
// OnLayerDone 在每层完成后调用(供 main 在 **stage-host 就绪** 的那一刻
|
||||
// 接上插件拉起动作)。
|
||||
@ -66,7 +67,7 @@ type kernelAssembleHooks struct {
|
||||
OnLayerDone func(idx int, layer []kernelboot.Module)
|
||||
}
|
||||
|
||||
func RunKernelModules(ks *kernelboot.KernelState, hooks kernelAssembleHooks) (*kernelboot.SyncBus, error) {
|
||||
func RunKernelModules(ks *kstate.State, hooks kernelAssembleHooks) (*kstate.SyncBus, error) {
|
||||
mods, err := buildModuleList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -88,7 +89,7 @@ func RunKernelModules(ks *kernelboot.KernelState, hooks kernelAssembleHooks) (*k
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bus := kernelboot.NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
|
||||
// ★★ 先把**全部模块声明的设施**登记为"内核注册过",**再**调用 BeforeRun。
|
||||
//
|
||||
|
||||
@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
agentCore "github.com/JianFeeeee/HologramAgent/internal/agent/core"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -164,10 +166,10 @@ func main() {
|
||||
// 与 bootstrap_order_test.go 的结构性判据。
|
||||
//
|
||||
// 从 cfg 起全部改为**模块自声明依赖 + 注册表 + BFS 分层**:
|
||||
// · 公共数据统一在 kernelboot.KernelState(不再散落各包包级变量)
|
||||
// · 公共数据统一在 kstate.State(不再散落各包包级变量)
|
||||
// · 每个模块在 internal/kernelmod/<name>/ 里**自己声明** Requires/Provides
|
||||
// · 注册表是乱序的(顺序无意义,仅供查询"内核有哪些模块")
|
||||
ks := kernelboot.NewKernelState()
|
||||
ks := kstate.New()
|
||||
ks.CfgReg = cfgReg
|
||||
ks.DataDir = opt.dataDir
|
||||
ks.AgentWorkDir = agentWorkDir
|
||||
@ -260,7 +262,9 @@ func main() {
|
||||
trk := ks.Tracker
|
||||
ctx := ks.Ctx
|
||||
pluginReg := ks.PluginRegistry
|
||||
agent := ks.Agent
|
||||
// Agent 在挂载点里存 any(kstate 不能依赖 agent/core,否则成环)。
|
||||
// cmd/homed 本来就 import core,这里断言回具体类型即可。
|
||||
agent, _ := ks.Agent.(*agentCore.Agent)
|
||||
|
||||
// 候选消费管线:常驻 goroutine,把 eventbus 的 memory_candidate 写进
|
||||
// 文本记忆并喂给蒸馏管线。原顺序里它在 textmem 之后、provider 之前。
|
||||
|
||||
@ -71,3 +71,25 @@ type BuiltinToolHandlers interface {
|
||||
ExecutePluginReload() string
|
||||
ExecuteGetPluginTools(pluginName string) string
|
||||
}
|
||||
|
||||
// DeviceMounter 是"能把数据面挂到自己身上"的最小面。
|
||||
//
|
||||
// ## 为什么需要它(2026-11-04)
|
||||
//
|
||||
// `kstate.State.Agent` 存 `any`(挂载点不能依赖 agent/core,否则成环)。
|
||||
// 消费方要调 `MountDevice` 就必须断言回具体类型 —— 但 device 模块断言成
|
||||
// `*agentCore.Agent` 会让 device 依赖 core 的全部实现细节,而它真正需要
|
||||
// 的只有这一个方法。
|
||||
//
|
||||
// 用一个单方法接口把需求显式化:
|
||||
//
|
||||
// · device 只依赖 api.DeviceMounter(叶子包,零环)
|
||||
// · core 的 *Agent 天然实现它(方法名与签名一致)
|
||||
// · 谁想提供"可挂载数据面的宿主",实现这一个方法即可
|
||||
//
|
||||
// ★ 与 ToolHost 的分工:ToolHost 是**执行面**(工具定义与执行),
|
||||
// DeviceMounter 是**装配面**(挂上数据面)。两者用途不同,不合并。
|
||||
type DeviceMounter interface {
|
||||
// MountDevice 挂载工具/通道数据面(装配期调用一次)。
|
||||
MountDevice(d DeviceSurface)
|
||||
}
|
||||
|
||||
@ -286,9 +286,6 @@ type Agent struct {
|
||||
// fusionCfg 控制文本路与视觉路的跨模态融合权重,可按模型实测结果配置。
|
||||
fusionCfg CrossModalFusionConfig
|
||||
|
||||
// 技能索引提供者:由 skillmgr 插件实现,向 system prompt 注入轻量技能索引
|
||||
skillIndex SkillIndexProvider
|
||||
|
||||
// device 是挂载上来的工具/通道数据面(kernelmod/device 产出)。
|
||||
// 类型是低层接口:core 不认识具体模块,避免 core ↔ kernelboot 成环。
|
||||
device agentAPI.DeviceSurface
|
||||
@ -323,14 +320,6 @@ type Agent struct {
|
||||
usageLedger usageLedger
|
||||
}
|
||||
|
||||
// SkillIndexProvider 提供已加载技能的精炼索引,供 buildSystemPrompt 注入。
|
||||
// 实现方(skillmgr)需线程安全并快速返回(每次 LLM 调用都会调用)。
|
||||
type SkillIndexProvider interface {
|
||||
// SkillIndex 返回多行文本的技能索引,每行形如 "name vX.Y - description";
|
||||
// 无技能时返回空串。
|
||||
SkillIndex() string
|
||||
}
|
||||
|
||||
type AgentConfig struct {
|
||||
ID types.AgentID
|
||||
SystemPrompt string
|
||||
@ -449,8 +438,6 @@ type AgentConfig struct {
|
||||
EventBus *events.Bus
|
||||
ThinkingEnabled bool
|
||||
|
||||
SkillIndexProvider SkillIndexProvider
|
||||
|
||||
InputProcessing types.InputProcessingConfig // 非文本输入处理配置
|
||||
|
||||
// MaxToolTurns 是单个任务允许的工具轮次上限(0 = 不限)。
|
||||
@ -576,7 +563,6 @@ func New(cfg AgentConfig) *Agent {
|
||||
maxContextSize: cfg.MaxContextSize,
|
||||
ctxTuning: cfg.CtxTuning,
|
||||
stageHost: cfg.StageHost,
|
||||
skillIndex: cfg.SkillIndexProvider,
|
||||
eventBus: cfg.EventBus,
|
||||
selfInputCh: make(chan selfInputMsg, 64),
|
||||
childTasks: make(map[string]*childTaskState),
|
||||
@ -607,9 +593,6 @@ func New(cfg AgentConfig) *Agent {
|
||||
return a
|
||||
}
|
||||
|
||||
// SetSkillIndexProvider 注入技能索引提供者(skillmgr 插件加载后由 main 接线)。
|
||||
func (a *Agent) SetSkillIndexProvider(p SkillIndexProvider) { a.skillIndex = p }
|
||||
|
||||
func (a *Agent) Start() {
|
||||
go a.schedulerLoop()
|
||||
go a.interceptLoop()
|
||||
|
||||
286
internal/agent/core/promptsections.go
Normal file
286
internal/agent/core/promptsections.go
Normal file
@ -0,0 +1,286 @@
|
||||
package core
|
||||
|
||||
// 内核自身声明的系统提示词段(2026-11-04 统一到 promptsec)。
|
||||
//
|
||||
// ## 用户裁定
|
||||
//
|
||||
// > 同时把现在内核所有 systemprompt 的相关注入都接入到这个统一的
|
||||
// > systemprompt 管理器上。
|
||||
//
|
||||
// 在此之前的形态是两套并存:
|
||||
//
|
||||
// ① 内核在 buildSystemPrompt 里用 appendPromptBlock 硬拼十几块
|
||||
// ② 插件(skillmgr)走 promptSections 声明自己的段
|
||||
//
|
||||
// 两套并存的问题不是"不好看",而是**顺序不可控**:插件段只能整体插在
|
||||
// 内核硬拼流程的某个固定位置,没法说"我要排在某两块之间";而内核那十几块
|
||||
// 的相对顺序又完全由代码书写顺序决定,改一处顺序得动函数体。
|
||||
//
|
||||
// 现在统一走一个注册表:内核把自己那十几块也**声明**进去,由 Order 决定
|
||||
// 全部顺序。内核只是"另一个声明者",插件看到的渲染规则与内核完全一致。
|
||||
//
|
||||
// ## 与插件段的区别(只有一点)
|
||||
//
|
||||
// 插件段:插件声明 → 常驻注册表(跨轮存活,可运行时更新)
|
||||
// 内核段:每轮根据当前 agent 实例状态实时计算 → 当轮渲染
|
||||
//
|
||||
// 内核块大多依赖实例状态(人格卡、记忆上下文、任务提示词、索引器、首启门禁),
|
||||
// 不能预先存进共享注册表。所以内核走"当轮临时渲染":
|
||||
//
|
||||
// kernelSections(a, memContext) → 当轮快照 → 与注册表合并 → 渲染
|
||||
//
|
||||
// ★ 合并时**不写回**共享注册表:注册表是插件声明的家,内核的每轮快照
|
||||
// 写进去会让它无限增长(每个 agent 一份),且下一轮读到上一轮的陈旧块。
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/promptsec"
|
||||
)
|
||||
|
||||
// 内核段的 Order 区段。
|
||||
//
|
||||
// 区段划分与 SDK 的 PromptSection.Order 约定一致(见 sdk/systemprompt.go):
|
||||
//
|
||||
// 0–99 内核框架(基础人格、人格卡、任务提示词)
|
||||
// 100–199 工具与技能使用说明(插件段:skillmgr 等)
|
||||
// 200–299 记忆与上下文说明
|
||||
// 300–399 输出与通道规则
|
||||
// 400–499 工具目录
|
||||
//
|
||||
// ★ 为什么把"记忆"排在"工具"之后:"什么时候用哪个工具"要在模型已经知道
|
||||
// 有哪些工具之后读才有效;反过来(先讲记忆工具、后列工具)会让前面那段
|
||||
// 引用还不存在的工具名。这是既有顺序,此处只是把它显式化。
|
||||
const (
|
||||
orderKernelBase = 0 // 基础人格(systemPrompt 兜底句)
|
||||
orderKernelTask = 10 // 驻留子的任务提示词
|
||||
orderKernelPersona = 20 // 人格卡
|
||||
orderKernelMemoryCtx = 200 // 记忆上下文(由 memoryPass 产出)
|
||||
orderKernelMemory = 210 // 【Memory】三层记忆说明
|
||||
orderKernelMemMaint = 220 // 【Memory maintenance】
|
||||
orderKernelInterrupt = 230 // 【Interrupt】
|
||||
orderKernelToolOrder = 240 // 【Tool order】
|
||||
orderKernelOutput = 300 // 【Output rules】
|
||||
orderKernelIndexer = 320 // 索引器(工具提示)
|
||||
orderKernelFirstRun = 400 // 首启人格门禁
|
||||
orderKernelToolCata = 500 // 工具目录
|
||||
)
|
||||
|
||||
// kernelSection 是内核当轮计算出来的一段。
|
||||
type kernelSection struct {
|
||||
order int
|
||||
section string // 仅用于同 Order 的稳定排序(按名字典序)
|
||||
content string
|
||||
}
|
||||
|
||||
// kernelSections 按**当前实例状态**算出内核本轮要注入的段。
|
||||
//
|
||||
// 顺序由 order 决定,与函数体里的书写顺序无关 —— 这是本次统一的要点:
|
||||
// 调整提示词结构变成调常量,而不是挪代码块。
|
||||
func (a *Agent) kernelSections(memContext string) []kernelSection {
|
||||
base := expandPromptVars(a.systemPrompt)
|
||||
if base == "" {
|
||||
base = "You are HologramAgent, a household-manager AI assistant. Never use Unicode emoji; express affect with kaomoji only, and end sentences with a soft particle. The WebUI overview page shows your portrait."
|
||||
}
|
||||
out := []kernelSection{
|
||||
{order: orderKernelBase, section: "kernel.base", content: base},
|
||||
}
|
||||
|
||||
// 驻留子:在固定提示词之上注入任务提示词(设计 §7「创建」)。
|
||||
if a.taskPrompt != "" {
|
||||
out = append(out, kernelSection{
|
||||
order: orderKernelTask, section: "kernel.task",
|
||||
content: "[Task] " + a.taskPrompt,
|
||||
})
|
||||
}
|
||||
|
||||
// 人格卡:同样展开版本占位符(人格里写死版本号会随发版说谎 ——
|
||||
// 实测线上人格卡写 v1.0.3,内核早已 1.3.x)。
|
||||
if a.personality != nil {
|
||||
if pp := a.personality.InjectPrompt(); pp != "" {
|
||||
out = append(out, kernelSection{
|
||||
order: orderKernelPersona, section: "kernel.persona",
|
||||
content: expandPromptVars(pp),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if memContext != "" {
|
||||
out = append(out, kernelSection{
|
||||
order: orderKernelMemoryCtx, section: "kernel.memory_context",
|
||||
content: memContext,
|
||||
})
|
||||
}
|
||||
|
||||
// 【三层记忆】只留"模型据此选择工具"的语义。
|
||||
//
|
||||
// 它讲的是内核真实执行语义(哪些内容在哪一层、何时进上下文、哪个工具
|
||||
// 会做什么),不是工具用法 —— 工具用法归各 ToolDef.Description。
|
||||
//
|
||||
// 为什么必须有:此前系统提示词从未解释 L1/L2/L3,agent 只能从工具名猜。
|
||||
// 生产日志实测:agent 几乎从不调 coldcontext_query,即使调了也只看返回值、
|
||||
// 不知道原文已被回填到对话时序。
|
||||
//
|
||||
// ★ 但有一件事必须说:coldcontext_query 会把历史事件**插回时序**,
|
||||
// 而不是返回摘要 —— 这与模型对"检索工具"的默认预期相反,不说就误用。
|
||||
out = append(out, kernelSection{
|
||||
order: orderKernelMemory, section: "kernel.memory",
|
||||
content: "[Memory] Recent turns are above. For older conversations use `coldcontext_query` - it **re-inserts them into your timeline at their original timestamps**, so read them there, not in the return value. " +
|
||||
"Facts and relations: `stablemem_recall`. Written knowledge: `knowledge_search`.",
|
||||
})
|
||||
|
||||
// 记忆维护:具体选哪个工具、各有什么副作用都写在 memory_* 的工具描述里
|
||||
//(【记忆清理】前缀)。这里只保留那条**不看描述也会犯**的错:只描述计划不执行。
|
||||
out = append(out, kernelSection{
|
||||
order: orderKernelMemMaint, section: "kernel.memory_maintenance",
|
||||
content: "[Memory maintenance] To tidy or clear memory, call the memory_* tools - a described plan is not the work. Report the result in one sentence.",
|
||||
})
|
||||
|
||||
// L2(冷上下文)预填充已并入 buildMemoryContext(输入路/工具路共用一次),
|
||||
// 这里不再重复注入,否则同一批摘要会出现两遍。
|
||||
|
||||
out = append(out, kernelSection{
|
||||
order: orderKernelInterrupt, section: "kernel.interrupt",
|
||||
content: "[Interrupt] During long tasks a system message prefixed [Interrupt] may arrive (new message, terminal output, timer). It is not user speech, but handle it: pause, respond, or consciously continue.",
|
||||
})
|
||||
|
||||
// 【工具执行顺序】只留**模型必须据此作决定**的三条。
|
||||
//
|
||||
// 必须在并行执行落地之后才加:反序会让本段对模型说谎 —— 说"并发"而
|
||||
// 内核仍串行,模型据此推断安全性并写出真正依赖顺序的调用。
|
||||
// ⚠️ 措辞必须与 batchRunnable 的**真实**判据一致。
|
||||
out = append(out, kernelSection{
|
||||
order: orderKernelToolOrder, section: "kernel.tool_order",
|
||||
content: "[Tool order] Calls in one reply run **in parallel** by default. **Never depend on order**: if one needs another's result, use two turns. " +
|
||||
"Outputs to the same channel **keep their order**, so \"acknowledge, then conclude\" fits in one turn.",
|
||||
})
|
||||
|
||||
out = append(out, kernelSection{
|
||||
order: orderKernelOutput, section: "kernel.output_rules",
|
||||
content: kernelOutputRules(),
|
||||
})
|
||||
|
||||
if a.indexer != nil {
|
||||
out = append(out, kernelSection{
|
||||
order: orderKernelIndexer, section: "kernel.indexer",
|
||||
content: a.indexer.BuildToolPrompt(),
|
||||
})
|
||||
}
|
||||
|
||||
// seq 的触发条件**刻意不写在这里**(用户裁定:seq 知识应在 skill 中)。
|
||||
//
|
||||
// 一度加过【加速序列】块,理由是"skill 索引只给一行摘要,模型不知道何时该用"。
|
||||
// 那个理由站不住:插件声明的技能索引已明确要求"准备调用某个插件的工具时
|
||||
// 先看同名技能",seq 的"什么时候用"写在 skill 正文里会被这条规则正确带到
|
||||
// 模型面前。再加一块提示词 = 同一件事记两处,且必然漂移。
|
||||
//
|
||||
// 副作用证据:加块后 TestSystemPromptStaysCompact 报 2022 > 2000。
|
||||
// 那个超限**本身就是信号** —— 该去掉的是内容,不是调高上限。
|
||||
|
||||
// 首启人格门禁(跨通道唯一闸口):人格未确认时要求模型主动询问用户。
|
||||
// 系统提示词每轮重建,因此 WebUI / QQ / CLI / ACP / 邮件等所有通道都会
|
||||
// 带上它;模型调用 persona_set(或用户在向导里选)落地后,标记置位,本段消失。
|
||||
if a.personaStore != nil && !a.personaStore.PersonaInitialized() {
|
||||
out = append(out, kernelSection{
|
||||
order: orderKernelFirstRun, section: "kernel.first_run",
|
||||
content: "[First-run persona setup] Your **persona is not confirmed**. Ask once this turn: default persona, or a custom one? Then **call persona_set**: " +
|
||||
"default -> mode=default; custom -> mode=custom with the text in content; later -> mode=later. Until answered, don't assume it is set and don't repeat the question.",
|
||||
})
|
||||
}
|
||||
|
||||
out = append(out, kernelSection{
|
||||
order: orderKernelToolCata, section: "kernel.tool_catalog",
|
||||
content: a.buildToolCatalog(),
|
||||
})
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// kernelOutputRules 是【Output rules】的正文。
|
||||
//
|
||||
// 单独成函数只为了让 kernelSections 保持可读;渲染结果与原先逐字一致。
|
||||
func kernelOutputRules() string {
|
||||
var b strings.Builder
|
||||
// ★ 这一句是本段最重要的:用户明确要求「必须显式调用 outputch 才会输出给用户」。
|
||||
// 模型最常见的错误不是不会调工具,而是**以为返回文本就等于回复了** ——
|
||||
// 异步通道下那等于自言自语(用户永远收不到)。
|
||||
_, _ = b.WriteString("[Output rules]\n")
|
||||
_, _ = b.WriteString("**Content reaches the user only when you explicitly call `output_send__{channel}`.** " +
|
||||
"Returning plain text only speaks inside this reasoning step; the user cannot see it - except on the synchronous channels described below.\n")
|
||||
_, _ = b.WriteString("- Don't assume a fixed channel: one conversation may span devices and channels. If unsure, call output_list_channels.\n")
|
||||
_, _ = b.WriteString("- Sync channels (webui / cli / terminal): return plain text. **The only exception.**\n")
|
||||
_, _ = b.WriteString("- Async channels (qq / wechat / groups): call output_send__{channel} with the right user_id or group_id.\n")
|
||||
_, _ = b.WriteString("- One call may send several messages: pass an array as payload. Unclear how to send? output_send__{channel}_help.\n")
|
||||
// ❗这里**不得**限制"一轮只能发一次"。设计上输出是 agent 的**主动调用**:
|
||||
// 收到一次输入后,可以往**任意(已授权的)通道**发**任意多次**(分段播报、
|
||||
// 先回执后结论、同时通知多个通道都合法)。此前这里写着"每轮对话通常只需
|
||||
// 调用一次 output_send" —— 那是一条**凭空的限制**,会让模型自己收起合理
|
||||
// 的多次输出。真正需要提醒的只有两件事:单条长度上限(超长拆成完整段落)
|
||||
// 与"别反复重发完全相同的内容"(自律,不是判据)。
|
||||
_, _ = b.WriteString("- **You choose how many outputs and which channels.** **No \"one output per turn\" limit.** Keep messages whole; don't resend identical content.\n")
|
||||
_, _ = b.WriteString("- Multi-step work: acknowledge on the current channel **first**, then run the real tools. An acknowledgement is not completion - finish and report.")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderSystemPrompt 把内核当轮段与插件声明段合并渲染成最终 system prompt。
|
||||
//
|
||||
// ★ 这是**唯一**的渲染入口:buildSystemPrompt 只负责算出 memContext 递进来,
|
||||
// 不再自己拼块。这样"提示词长什么样"只有一处可看。
|
||||
//
|
||||
// 合并语义:
|
||||
// - 内核段与插件段按 order 统一排序(同 order 按段名字典序,保证稳定)
|
||||
// - 段间用空行分隔(与 appendPromptBlock 的语义一致,保证输出逐字不变)
|
||||
// - 空内容段跳过(插件可能声明了段但暂时没内容)
|
||||
func (a *Agent) renderSystemPrompt(memContext string) string {
|
||||
all := make([]renderableSection, 0, 16)
|
||||
for _, k := range a.kernelSections(memContext) {
|
||||
if strings.TrimSpace(k.content) == "" {
|
||||
continue
|
||||
}
|
||||
all = append(all, renderableSection{order: k.order, name: k.section, content: k.content})
|
||||
}
|
||||
// 插件声明的段从**统一挂载点**读(2026-11-04)。
|
||||
//
|
||||
// ★ 为何不是 Agent 字段:字段方案要求每种 agent(rtchild / 驻留子 /
|
||||
// 工作式子)在构造时都记得传一遍,漏传不报错也不崩,只是那个 agent
|
||||
// 的提示词里一条插件说明都没有 —— 生产上就是这样让分诊助手不知道
|
||||
// "cmd 插件要先 plugin_start",于是拿 terminal_create 当命令执行器。
|
||||
//
|
||||
// 挂在 kstate 上后,任何 agent(含未来新类型)读同一份,**零接线**。
|
||||
if ks := kstate.Get(); ks != nil && ks.PromptSections != nil {
|
||||
for _, s := range ks.PromptSections.Snapshot() {
|
||||
if strings.TrimSpace(s.Content) == "" {
|
||||
continue
|
||||
}
|
||||
all = append(all, renderableSection{order: s.Order, name: s.Section, content: s.Content})
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(all, func(i, j int) bool {
|
||||
if all[i].order != all[j].order {
|
||||
return all[i].order < all[j].order
|
||||
}
|
||||
return all[i].name < all[j].name
|
||||
})
|
||||
|
||||
var b strings.Builder
|
||||
for i, s := range all {
|
||||
if i > 0 {
|
||||
_, _ = b.WriteString("\n\n")
|
||||
}
|
||||
_, _ = b.WriteString(strings.TrimSpace(s.content))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderableSection 是排序用的临时结构(内核段与插件段合并后)。
|
||||
type renderableSection struct {
|
||||
order int
|
||||
name string
|
||||
content string
|
||||
}
|
||||
|
||||
// 编译期确认:内核用的段类型与 registry 的段类型同源,避免两处各自演化。
|
||||
var _ = promptsec.Section{}
|
||||
270
internal/agent/core/promptsections_test.go
Normal file
270
internal/agent/core/promptsections_test.go
Normal file
@ -0,0 +1,270 @@
|
||||
package core
|
||||
|
||||
// 判据:**插件声明的系统提示词段对所有 agent 可见**(2026-11-04)。
|
||||
//
|
||||
// ## 这组判据守的是哪类缺陷
|
||||
//
|
||||
// 生产故障:A2A 分诊助手(驻留子)拿 `terminal_create` 当一次性命令执行器,
|
||||
// 每条 `cd … && echo … && grep …` 开一个新终端,命令结束终端就退出,
|
||||
// `terminal_read` 立刻撞 `read /dev/ptmx: input/output error`。
|
||||
//
|
||||
// 链路拆开是四层:
|
||||
//
|
||||
// ① cmd 插件 auto_start=false ⇒ 工具表里没有 cmd_run
|
||||
// ② 需要 cmd-tools skill 告诉它"先 plugin_start cmd"
|
||||
// ③ 但 skill 索引**没进那个 agent 的提示词**
|
||||
// ④ 因为 skillIndex 只是根 agent 的字段,由 bootstrap.go 手工接线
|
||||
//
|
||||
// 本文件钉 ③ 与 ④:**声明表挂在统一挂载点上,任何 agent 读同一份**。
|
||||
//
|
||||
// ## 为什么判据必须测"子 agent"而不是只测根
|
||||
//
|
||||
// 只测根 agent 会全绿 —— 那正是它当初上线时的状态。缺陷恰好在"子 agent
|
||||
// 拿不到"这一层,所以判据必须**逐个类型**验证:rtchild / 驻留子 / 工作式子。
|
||||
//
|
||||
// ## 为什么测生成的 prompt 而不是测字段
|
||||
//
|
||||
// 断言 `carrier.promptSections != nil` 是结构判据:字段接上了,但
|
||||
// renderSystemPrompt 里读取那段被删掉照样绿。所以直接看渲染结果。
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/promptsec"
|
||||
)
|
||||
|
||||
// mountWithSection 装一个带指定段的挂载点,返回清理函数。
|
||||
//
|
||||
// ★ 用 MountForTest 而不是 Mount:并行测试会互相覆盖挂载点,
|
||||
// 表现为随机红(本仓已有教训:判据在 -race 下才暴露)。
|
||||
func mountWithSection(t *testing.T, section, content string) {
|
||||
t.Helper()
|
||||
reg := promptsec.NewRegistry()
|
||||
if err := reg.Declare(promptsec.Section{
|
||||
Section: section,
|
||||
Owner: "skillmgr",
|
||||
Order: 100,
|
||||
Content: content,
|
||||
}); err != nil {
|
||||
t.Fatalf("声明段失败: %v", err)
|
||||
}
|
||||
restore := kstate.MountForTest(&kstate.State{PromptSections: reg})
|
||||
t.Cleanup(restore)
|
||||
}
|
||||
|
||||
// rtHostWithIO 造一个父 agent + 成对 rt 通道(不带 promptSections 字段)。
|
||||
func rtHostWithIO(t *testing.T) *Agent {
|
||||
t.Helper()
|
||||
pio := agentIO.NewIOManager()
|
||||
host := New(AgentConfig{ID: "ps-host", IO: pio, Provider: &rtBlockingProvider{}})
|
||||
if err := pio.RegisterDevice(&rtRecordingDevice{name: "psout"}); err != nil {
|
||||
t.Fatalf("注册输出设备: %v", err)
|
||||
}
|
||||
for _, n := range []string{"psout", "psin"} {
|
||||
if err := pio.ChannelRegistry().Register(agentIO.InputChannel{
|
||||
Name: n, Plugin: "pstest",
|
||||
Capabilities: agentIO.CapText | agentIO.CapRT,
|
||||
}); err != nil {
|
||||
t.Fatalf("注册通道 %s: %v", n, err)
|
||||
}
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// TestRTChildSeesPromptSections 钉住 rtchild 能看到插件声明的段。
|
||||
//
|
||||
// 变红条件:renderSystemPrompt 改回读 `a.promptSections` 字段(子没有)⇒ 红。
|
||||
func TestRTChildSeesPromptSections(t *testing.T) {
|
||||
const marker = "TERMINAL-SKILL-MARKER: always read the terminal skill first"
|
||||
mountWithSection(t, "skillmgr.skills", marker)
|
||||
|
||||
host := rtHostWithIO(t)
|
||||
defer host.StopRTChildren()
|
||||
|
||||
id, err := host.SpawnRTChild(RTChildOptions{ID: "ps-rt", Input: "psin", Output: "psout"})
|
||||
if err != nil {
|
||||
t.Fatalf("SpawnRTChild: %v", err)
|
||||
}
|
||||
prompt := host.rtChildOf(id).agent.buildSystemPrompt("", "hi")
|
||||
if !strings.Contains(prompt, "TERMINAL-SKILL-MARKER") {
|
||||
t.Fatalf("★ rtchild 的提示词里没有插件声明的段 ——\n"+
|
||||
"它看不到任何 skill 说明,只能凭工具名猜用法(%d 字符)", len(prompt))
|
||||
}
|
||||
}
|
||||
|
||||
// TestResidentSeesPromptSections 钉住驻留子能看到(**生产实际中招的那一类**)。
|
||||
//
|
||||
// A2A 的分诊助手就是驻留子。这条变红 = 故障复现。
|
||||
func TestResidentSeesPromptSections(t *testing.T) {
|
||||
const marker = "CMD-SKILL-MARKER: run plugin_start cmd before cmd_run"
|
||||
mountWithSection(t, "skillmgr.skills", marker)
|
||||
|
||||
parent, _, dir := newRootWith(t, &countingProvider{})
|
||||
spawnTestResident(t, parent, dir, "ps-res")
|
||||
|
||||
rc := parent.residentOf("ps-res")
|
||||
if rc == nil {
|
||||
t.Fatal("驻留子没创建成功")
|
||||
}
|
||||
prompt := rc.agent.buildSystemPrompt("", "hi")
|
||||
if !strings.Contains(prompt, "CMD-SKILL-MARKER") {
|
||||
t.Fatalf("★ 驻留子的提示词里没有插件声明的段 ——\n"+
|
||||
"这正是生产上'拿 terminal_create 当 cmd_run 用'的根因(%d 字符)", len(prompt))
|
||||
}
|
||||
}
|
||||
|
||||
// TestRootAgentSeesPromptSections 确认根 agent 路径没被改坏(保守侧)。
|
||||
func TestRootAgentSeesPromptSections(t *testing.T) {
|
||||
const marker = "ROOT-MARKER: skills are declared, not pulled"
|
||||
mountWithSection(t, "skillmgr.skills", marker)
|
||||
|
||||
a := New(AgentConfig{ID: "ps-root"})
|
||||
if got := a.buildSystemPrompt("", "hi"); !strings.Contains(got, "ROOT-MARKER") {
|
||||
t.Fatal("根 agent 看不到插件声明的段 —— 本次改造改坏了原有路径")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNoPromptSectionsMount 钉住"没装挂载点时不能 panic、也不能凭空出现段"。
|
||||
//
|
||||
// 单测与独立工具(memgc 等)不装挂载点,这是常态而非异常。
|
||||
func TestNoPromptSectionsMount(t *testing.T) {
|
||||
restore := kstate.MountForTest(nil)
|
||||
defer restore()
|
||||
|
||||
a := New(AgentConfig{ID: "ps-none"})
|
||||
prompt := a.buildSystemPrompt("", "hi")
|
||||
if prompt == "" {
|
||||
t.Fatal("无挂载点时提示词为空 —— 基础人格丢了")
|
||||
}
|
||||
if strings.Contains(prompt, "[Skills]") {
|
||||
t.Fatal("没装注册表,提示词里却出现了 [Skills] 段")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeclareTwiceRendersOnce 钉住**幂等**(回应用户"会不会多份"的担心)。
|
||||
//
|
||||
// 这条不是理论风险:system prompt 每轮重建,而一次任务里 stepPrepare 可能
|
||||
// 被重入(上下文裁剪后回到该步)。若声明是"追加"而不是"覆盖",
|
||||
// 索引会越堆越多,最终挤掉真正的对话历史。
|
||||
//
|
||||
// 变红条件:promptsec.Registry 用 slice 存段(追加语义)⇒ 出现两份。
|
||||
func TestDeclareTwiceRendersOnce(t *testing.T) {
|
||||
reg := promptsec.NewRegistry()
|
||||
for i := 0; i < 5; i++ { // 反复声明同一段 5 次
|
||||
if err := reg.Declare(promptsec.Section{
|
||||
Section: "skillmgr.skills",
|
||||
Owner: "skillmgr",
|
||||
Order: 100,
|
||||
Content: "IDEMPOTENT-MARKER",
|
||||
}); err != nil {
|
||||
t.Fatalf("declare: %v", err)
|
||||
}
|
||||
}
|
||||
restore := kstate.MountForTest(&kstate.State{PromptSections: reg})
|
||||
defer restore()
|
||||
|
||||
prompt := New(AgentConfig{ID: "ps-idem"}).buildSystemPrompt("", "hi")
|
||||
if n := strings.Count(prompt, "IDEMPOTENT-MARKER"); n != 1 {
|
||||
t.Fatalf("★ 同一段声明 5 次后渲染出 %d 份(应为 1)——\n"+
|
||||
"注册表必须用**覆盖**语义,否则每轮重建都会累积", n)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSectionRemovalTakesEffect 钉住"撤销后段消失"(插件 Stop 的路径)。
|
||||
//
|
||||
// 不撤销的后果:插件已停用,提示词里还留着"用 skill_info 读某 skill",
|
||||
// 模型于是去调不存在的工具 —— 比没有说明更糟。
|
||||
func TestSectionRemovalTakesEffect(t *testing.T) {
|
||||
reg := promptsec.NewRegistry()
|
||||
if err := reg.Declare(promptsec.Section{
|
||||
Section: "skillmgr.skills", Owner: "skillmgr",
|
||||
Order: 100, Content: "REMOVABLE-MARKER",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
restore := kstate.MountForTest(&kstate.State{PromptSections: reg})
|
||||
defer restore()
|
||||
|
||||
a := New(AgentConfig{ID: "ps-rm"})
|
||||
if !strings.Contains(a.buildSystemPrompt("", "hi"), "REMOVABLE-MARKER") {
|
||||
t.Fatal("声明后应能看到")
|
||||
}
|
||||
|
||||
reg.Remove("skillmgr.skills")
|
||||
if strings.Contains(a.buildSystemPrompt("", "hi"), "REMOVABLE-MARKER") {
|
||||
t.Fatal("撤销后段仍在 —— 插件 Stop 后模型会去调已不存在的工具")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRuntimeUpdateVisibleToExistingAgent 钉住"可运行时更新"(用户裁定)。
|
||||
//
|
||||
// 关键:**同一个 agent 实例**在注册表更新后,下一轮重建就应看到新内容。
|
||||
// 若 Agent 在构造期拷贝了注册表或其内容,这条会红。
|
||||
func TestRuntimeUpdateVisibleToExistingAgent(t *testing.T) {
|
||||
reg := promptsec.NewRegistry()
|
||||
restore := kstate.MountForTest(&kstate.State{PromptSections: reg})
|
||||
defer restore()
|
||||
|
||||
a := New(AgentConfig{ID: "ps-live"})
|
||||
|
||||
// 初始无内容
|
||||
if strings.Contains(a.buildSystemPrompt("", "hi"), "v1-of-skill") {
|
||||
t.Fatal("初始不该有内容")
|
||||
}
|
||||
|
||||
// 运行时声明
|
||||
if err := reg.Declare(promptsec.Section{
|
||||
Section: "skillmgr.skills", Owner: "skillmgr",
|
||||
Order: 100, Content: "v1-of-skill",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(a.buildSystemPrompt("", "hi"), "v1-of-skill") {
|
||||
t.Fatal("运行时声明后,已存在的 agent 应立刻看到")
|
||||
}
|
||||
|
||||
// 运行时更新(覆盖)
|
||||
if err := reg.Declare(promptsec.Section{
|
||||
Section: "skillmgr.skills", Owner: "skillmgr",
|
||||
Order: 100, Content: "v2-of-skill",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prompt := a.buildSystemPrompt("", "hi")
|
||||
if !strings.Contains(prompt, "v2-of-skill") {
|
||||
t.Fatal("更新后应看到新内容")
|
||||
}
|
||||
if strings.Contains(prompt, "v1-of-skill") {
|
||||
t.Fatal("旧内容仍在 —— 声明必须是覆盖语义")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSectionOrderIsRespected 钉住排序(插件的 Order 生效,且稳定)。
|
||||
func TestSectionOrderIsRespected(t *testing.T) {
|
||||
reg := promptsec.NewRegistry()
|
||||
if err := reg.Declare(promptsec.Section{
|
||||
Section: "skillmgr.late", Owner: "skillmgr", Order: 450, Content: "LATE-BLOCK",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := reg.Declare(promptsec.Section{
|
||||
Section: "skillmgr.early", Owner: "skillmgr", Order: 100, Content: "EARLY-BLOCK",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
restore := kstate.MountForTest(&kstate.State{PromptSections: reg})
|
||||
defer restore()
|
||||
|
||||
prompt := New(AgentConfig{ID: "ps-order"}).buildSystemPrompt("", "hi")
|
||||
early := strings.Index(prompt, "EARLY-BLOCK")
|
||||
late := strings.Index(prompt, "LATE-BLOCK")
|
||||
if early < 0 || late < 0 {
|
||||
t.Fatalf("两段都应出现(early=%d late=%d)", early, late)
|
||||
}
|
||||
if early > late {
|
||||
t.Fatalf("★ Order 小的必须在前(early 在 %d,late 在 %d)", early, late)
|
||||
}
|
||||
}
|
||||
@ -266,14 +266,12 @@ func (a *Agent) SpawnResident(opts ResidentOptions) (ResidentInfo, error) {
|
||||
PreferredProvider: firstNonEmpty(opts.PreferredProvider, a.preferredProvider),
|
||||
IO: childIO,
|
||||
StageHost: a.stageHost,
|
||||
// ★ 技能索引必须继承父的:不继承时子的 system prompt 里没有索引,
|
||||
// 模型看不到"按需插件要先 plugin_start"这类说明(同 rtchild)。
|
||||
SkillIndexProvider: a.skillIndex,
|
||||
LightMemory: light, // 轻量内核:只有图记忆共同面
|
||||
AllowedOutputs: opts.AllowedOutputs,
|
||||
KernelSource: parentID, // 子的 L4 只属于父
|
||||
ParentID: parentID,
|
||||
TaskPrompt: opts.TaskPrompt,
|
||||
// ★ 同一个注册表指针(不是拷贝):插件运行时更新的段立刻对子 agent 生效。
|
||||
LightMemory: light, // 轻量内核:只有图记忆共同面
|
||||
AllowedOutputs: opts.AllowedOutputs,
|
||||
KernelSource: parentID, // 子的 L4 只属于父
|
||||
ParentID: parentID,
|
||||
TaskPrompt: opts.TaskPrompt,
|
||||
// ★ 安全运行时必须继承(2026-10-30 补)。
|
||||
//
|
||||
// 驻留子是长期挂着、无人盯的执行体:它自己跑调度器、接自己的输入、
|
||||
|
||||
@ -627,17 +627,10 @@ func (a *Agent) SpawnRTChild(opts RTChildOptions) (string, error) {
|
||||
PreferredProvider: firstNonEmpty(opts.PreferredProvider, a.preferredProvider),
|
||||
IO: childIO,
|
||||
StageHost: a.stageHost,
|
||||
// ★ 技能索引必须**继承父的**(2026-11-04 修)。
|
||||
//
|
||||
// 不继承的后果与 Security 那次同型:子 agent 的 system prompt 里
|
||||
// **完全没有 skill 索引**,于是它不知道该先读哪个 skill 就直接调工具。
|
||||
// 实测症状:A2A 分诊助手手上没有 cmd_run(cmd 插件 auto_start=false),
|
||||
// 又看不到 cmd-tools skill 里"先 plugin_start cmd"的说明,
|
||||
// 就把 terminal_create 当成命令执行器用,一条命令开一个终端。
|
||||
SkillIndexProvider: a.skillIndex,
|
||||
AllowedOutputs: []string{opts.Output},
|
||||
KernelSource: string(a.id),
|
||||
ParentID: string(a.id),
|
||||
// ★ 同一个注册表指针(不是拷贝):插件运行时更新的段立刻对子 agent 生效。
|
||||
AllowedOutputs: []string{opts.Output},
|
||||
KernelSource: string(a.id),
|
||||
ParentID: string(a.id),
|
||||
// ★ 安全运行时**必须继承父的**(2026-10-30 补)。
|
||||
//
|
||||
// rtchild 是高频、低延迟的执行体,且它照样能调工具(R3:rt 流不禁工具)。
|
||||
|
||||
@ -1,147 +0,0 @@
|
||||
package core
|
||||
|
||||
// 判据:**子 agent 必须继承技能索引**(2026-11-04 生产缺陷)。
|
||||
//
|
||||
// ## 缺陷原貌
|
||||
//
|
||||
// `SetSkillIndexProvider` 只在 `cmd/homed/bootstrap.go` 里对**根 agent** 调了一次:
|
||||
//
|
||||
// if sp := pluginReg.Get("skillmgr"); sp != nil {
|
||||
// if prov, ok := sp.(agentCore.SkillIndexProvider); ok {
|
||||
// agent.SetSkillIndexProvider(prov)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 而 rtchild / 驻留子 / 转投驻留子 各自 `New(AgentConfig{...})` 构造载体,
|
||||
// 都**没有**传 `SkillIndexProvider`。于是子的 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 那次同型
|
||||
//
|
||||
// 父有的能力,子没继承。`Security` 那次也是"只修了主 agent",
|
||||
// 后来补成"构造期落位 + 各子构造点显式传"。
|
||||
//
|
||||
// ## 判据为什么测 prompt 而不是测字段
|
||||
//
|
||||
// 断言 `carrier.skillIndex != nil` 是结构判据:字段接上了,但
|
||||
// `buildSystemPrompt` 里注入那段被删了照样绿。所以直接看**生成的提示词**。
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
|
||||
)
|
||||
|
||||
// fakeSkillIndex 是最小 SkillIndexProvider 替身。
|
||||
type fakeSkillIndex struct{ content string }
|
||||
|
||||
func (f *fakeSkillIndex) SkillIndex() string { return f.content }
|
||||
|
||||
// rtHostWithSkills 造一个带技能索引的父 agent + 成对 rt 通道。
|
||||
func rtHostWithSkills(t *testing.T, idx SkillIndexProvider) *Agent {
|
||||
t.Helper()
|
||||
pio := agentIO.NewIOManager()
|
||||
host := New(AgentConfig{
|
||||
ID: "si-host", IO: pio, Provider: &rtBlockingProvider{},
|
||||
SkillIndexProvider: idx,
|
||||
})
|
||||
if err := pio.RegisterDevice(&rtRecordingDevice{name: "siout"}); err != nil {
|
||||
t.Fatalf("注册输出设备: %v", err)
|
||||
}
|
||||
for _, n := range []string{"siout", "siin"} {
|
||||
if err := pio.ChannelRegistry().Register(agentIO.InputChannel{
|
||||
Name: n, Plugin: "sitest",
|
||||
Capabilities: agentIO.CapText | agentIO.CapRT,
|
||||
}); err != nil {
|
||||
t.Fatalf("注册通道 %s: %v", n, err)
|
||||
}
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// TestRTChild_InheritsSkillIndex 钉住 rtchild 继承技能索引。
|
||||
//
|
||||
// 变红条件:从 SpawnRTChild 的 AgentConfig 删掉 SkillIndexProvider ⇒ 红。
|
||||
func TestRTChild_InheritsSkillIndex(t *testing.T) {
|
||||
const marker = "kernel-terminal v1.0.0 - Terminal waits for input"
|
||||
host := rtHostWithSkills(t, &fakeSkillIndex{content: marker})
|
||||
defer host.StopRTChildren()
|
||||
|
||||
id, err := host.SpawnRTChild(RTChildOptions{ID: "si-rt", Input: "siin", Output: "siout"})
|
||||
if err != nil {
|
||||
t.Fatalf("SpawnRTChild: %v", err)
|
||||
}
|
||||
carrier := host.rtChildOf(id).agent
|
||||
|
||||
prompt := carrier.buildSystemPrompt("", "hi")
|
||||
if !strings.Contains(prompt, "kernel-terminal") {
|
||||
t.Fatalf("★ rtchild 的 system prompt 里没有技能索引 —— "+
|
||||
"它看不到任何 skill,只能凭工具名猜用法(%d 字符的 prompt)", len(prompt))
|
||||
}
|
||||
if !strings.Contains(prompt, "/dev/ptmx") && !strings.Contains(prompt, "Terminal waits for input") {
|
||||
t.Fatalf("★ 索引内容没进 prompt(只匹配到了 skill 名?)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResident_InheritsSkillIndex 钉住驻留子继承技能索引。
|
||||
//
|
||||
// 这是**生产实际中招的那一类**:A2A 的临时分诊助手就是驻留子。
|
||||
//
|
||||
// 变红条件:从 SpawnResident 的 AgentConfig 删掉 SkillIndexProvider ⇒ 红。
|
||||
func TestResident_InheritsSkillIndex(t *testing.T) {
|
||||
const marker = "cmd-tools v1.0.0 - on-demand: plugin_start cmd first"
|
||||
parent, _, dir := newRootWith(t, &countingProvider{})
|
||||
parent.skillIndex = &fakeSkillIndex{content: marker}
|
||||
|
||||
spawnTestResident(t, parent, dir, "si-res")
|
||||
rc := parent.residentOf("si-res")
|
||||
if rc == nil {
|
||||
t.Fatal("驻留子没创建成功")
|
||||
}
|
||||
prompt := rc.agent.buildSystemPrompt("", "hi")
|
||||
if !strings.Contains(prompt, "cmd-tools") {
|
||||
t.Fatalf("★ 驻留子的 system prompt 里没有技能索引 —— "+
|
||||
"这正是生产上'拿 terminal_create 当 cmd_run 用'的根因"+
|
||||
"(%d 字符的 prompt)", len(prompt))
|
||||
}
|
||||
}
|
||||
|
||||
// TestRootAgent_SkillIndexStillInjected 钉住根 agent 不受本次改动影响。
|
||||
//
|
||||
// 保守侧:确认修复没有把根 agent 的路径改坏。
|
||||
func TestRootAgent_SkillIndexStillInjected(t *testing.T) {
|
||||
const marker = "kernel-memory v1.0.0 - read before memory tools"
|
||||
a := New(AgentConfig{ID: "root-si", SkillIndexProvider: &fakeSkillIndex{content: marker}})
|
||||
prompt := a.buildSystemPrompt("", "hi")
|
||||
if !strings.Contains(prompt, "kernel-memory") {
|
||||
t.Fatal("根 agent 的技能索引丢了 —— 本次改动改坏了原有路径")
|
||||
}
|
||||
}
|
||||
|
||||
// TestChildWithoutSkillIndexIsNotBroken 钉住"父没有索引时子也不炸"。
|
||||
//
|
||||
// 父可能没接 skillmgr(单测、独立工具)。那种情况下子照常工作,
|
||||
// 只是 prompt 里没有索引段 —— 不能因为 nil 就 panic。
|
||||
func TestChildWithoutSkillIndexIsNotBroken(t *testing.T) {
|
||||
host := rtHostWithSkills(t, nil) // 显式不接
|
||||
defer host.StopRTChildren()
|
||||
|
||||
id, err := host.SpawnRTChild(RTChildOptions{ID: "si-none", Input: "siin", Output: "siout"})
|
||||
if err != nil {
|
||||
t.Fatalf("父无技能索引时 SpawnRTChild 不该失败: %v", err)
|
||||
}
|
||||
prompt := host.rtChildOf(id).agent.buildSystemPrompt("", "hi")
|
||||
if strings.Contains(prompt, "[Skills]") {
|
||||
t.Fatal("父没有技能索引,子的 prompt 却出现了 [Skills] 段")
|
||||
}
|
||||
}
|
||||
@ -173,132 +173,26 @@ func appendPromptBlock(prompt, block string) string {
|
||||
return strings.TrimRight(prompt, "\n") + "\n\n" + block
|
||||
}
|
||||
|
||||
// buildSystemPrompt 生成发给 LLM 的 system prompt。
|
||||
//
|
||||
// ★ 2026-11-04 起**不再自己拼块**:所有内容(内核的十几块 + 插件声明的段)
|
||||
// 统一由 renderSystemPrompt 按 Order 排序渲染。本函数只负责把当轮上下文
|
||||
// (memContext)递进去。
|
||||
//
|
||||
// 为什么要统一(用户裁定):
|
||||
//
|
||||
// 「把现在内核所有 systemprompt 的相关注入都接入到这个统一的
|
||||
// systemprompt 管理器上」
|
||||
//
|
||||
// 此前两套并存(内核硬拼 + 插件声明),后果是**顺序不可控**:插件段只能
|
||||
// 整块插在某个固定位置;内核那十几块的相对顺序由函数体书写顺序决定,
|
||||
// 改顺序得挪代码。现在顺序由 Order 常量决定,内核只是"另一个声明者"。
|
||||
//
|
||||
// userInput 参数保留:它是部分内核块(如记忆上下文的相关性)的输入,
|
||||
// 由调用方在 buildMemoryContext 里使用后传入,渲染层不需要它。
|
||||
func (a *Agent) buildSystemPrompt(memContext string, userInput string) string {
|
||||
prompt := expandPromptVars(a.systemPrompt)
|
||||
if prompt == "" {
|
||||
prompt = "You are HologramAgent, a household-manager AI assistant. Never use Unicode emoji; express affect with kaomoji only, and end sentences with a soft particle. The WebUI overview page shows your portrait."
|
||||
}
|
||||
|
||||
// 驻留子:在**固定提示词之上**注入任务提示词(设计 §7「创建」)。
|
||||
if a.taskPrompt != "" {
|
||||
prompt = appendPromptBlock(prompt, "[Task] "+a.taskPrompt)
|
||||
}
|
||||
|
||||
if a.personality != nil {
|
||||
if pp := a.personality.InjectPrompt(); pp != "" {
|
||||
// 人格卡同样展开版本占位符:人格里写死版本号会随发版说谎
|
||||
// (实测线上人格卡写 v1.0.3,内核早已 1.3.x)。
|
||||
prompt = appendPromptBlock(prompt, expandPromptVars(pp))
|
||||
}
|
||||
}
|
||||
|
||||
if memContext != "" {
|
||||
prompt = appendPromptBlock(prompt, memContext)
|
||||
}
|
||||
|
||||
// 【三层记忆】是**内核运行时块**(提示词分层规范:内核行为由内核生成)。
|
||||
//
|
||||
// 它讲的是内核的真实执行语义(哪些内容在哪一层、何时会进上下文、
|
||||
// 哪个工具会做什么),不是工具用法 —— 工具用法归各 ToolDef.Description。
|
||||
//
|
||||
// 为什么必须有这一段:此前系统提示词从未解释 L1/L2/L3 是什么,
|
||||
// agent 只能从工具名猜。生产日志实测:agent 几乎从不调用 coldcontext_query,
|
||||
// 即使调用了也只看返回值、不知道原文已被回填到对话时序。
|
||||
// 【三层记忆】只留**模型据此选择工具**的语义。
|
||||
//
|
||||
// 技术性内容(L1/L2/L3 编号、向量空间、消费式删除、缩略语)全部下沉到
|
||||
// 对应工具的 description 与 skill:模型只需要知道"哪里有原文、哪里有
|
||||
// 关系、用什么去取",不需要知道内核怎么存。
|
||||
//
|
||||
// ★ 但有一件事必须说:coldcontext_query 会把历史事件**插回时序**,而不是返回
|
||||
// 一段摘要 —— 这与模型对"检索工具"的默认预期相反,不说就会误用。
|
||||
prompt = appendPromptBlock(prompt, "[Memory] Recent turns are above. For older conversations use `coldcontext_query` - it **re-inserts them into your timeline at their original timestamps**, so read them there, not in the return value. "+
|
||||
"Facts and relations: `stablemem_recall`. Written knowledge: `knowledge_search`.")
|
||||
|
||||
// 记忆维护:具体选哪个工具、各有什么副作用,都写在 memory_* 的工具描述里
|
||||
//(【记忆清理】前缀)。这里只保留那条**不看描述也会犯**的错:只描述计划不执行。
|
||||
prompt = appendPromptBlock(prompt, "[Memory maintenance] To tidy or clear memory, call the memory_* tools - a described plan is not the work. Report the result in one sentence.")
|
||||
|
||||
// L2(冷上下文)预填充已并入 buildMemoryContext(输入路/工具路共用一次),
|
||||
// 这里不再重复注入,否则同一批摘要会出现两遍。
|
||||
|
||||
prompt = appendPromptBlock(prompt, "[Interrupt] During long tasks a system message prefixed [Interrupt] may arrive (new message, terminal output, timer). It is not user speech, but handle it: pause, respond, or consciously continue.")
|
||||
|
||||
// 工具执行顺序(阶段 2.5)。必须在**并行执行落地之后**才加:
|
||||
// 反序会让本段对模型说谎——说"并发"而内核仍串行,模型据此推断
|
||||
// 安全性并写出真正依赖顺序的调用。宁可晚改,不可错改。
|
||||
//
|
||||
// ⚠️ 措辞必须与 batchRunnable 的**真实**判据一致(全批 ParallelSafe
|
||||
// 才并发 + 同通道保序),否则只是把"没说"换成"说错"。
|
||||
// 【工具执行顺序】只留**模型必须据此作决定**的三条。
|
||||
//
|
||||
// 技术性内容(ParallelSafe 声明、批内降级、内核如何判据)全部下沉:
|
||||
// 那是实现细节,模型既不需要也改不了 —— 讲它只会让回复带上说明书腔。
|
||||
// 判断依据仍是真实的 batchRunnable 语义(勿写反)。
|
||||
prompt = appendPromptBlock(prompt, "[Tool order] Calls in one reply run **in parallel** by default. **Never depend on order**: if one needs another's result, use two turns. "+
|
||||
"Outputs to the same channel **keep their order**, so \"acknowledge, then conclude\" fits in one turn.")
|
||||
|
||||
prompt = appendPromptBlock(prompt, "[Output rules]\n")
|
||||
// ★ 这一句是本段最重要的:用户明确要求「必须显式调用 outputch 才会输出给用户」。
|
||||
// 模型最常见的错误不是不会调工具,而是**以为返回文本就等于回复了**——
|
||||
// 异步通道下那等于自言自语(用户永远收不到)。
|
||||
prompt += "**Content reaches the user only when you explicitly call `output_send__{channel}`.** " +
|
||||
"Returning plain text only speaks inside this reasoning step; the user cannot see it - except on the synchronous channels described below.\n"
|
||||
prompt += "- Don't assume a fixed channel: one conversation may span devices and channels. If unsure, call output_list_channels.\n"
|
||||
prompt += "- Sync channels (webui / cli / terminal): return plain text. **The only exception.**\n"
|
||||
prompt += "- Async channels (qq / wechat / groups): call output_send__{channel} with the right user_id or group_id.\n"
|
||||
prompt += "- One call may send several messages: pass an array as payload. Unclear how to send? output_send__{channel}_help.\n"
|
||||
// ❗这里**不得**限制"一轮只能发一次"。设计上输出是 agent 的**主动调用**:
|
||||
// 收到一次输入后,可以往**任意(已授权的)通道**发**任意多次**(分段播报、
|
||||
// 先回执后结论、同时通知多个通道都合法)。此前这里写着"每轮对话通常只需调用
|
||||
// 一次 output_send"——那是一条**凭空的限制**,会让模型自己收起合理的多次输出。
|
||||
// 真正需要提醒的只有两件事:单条长度上限(超长拆成完整段落)与"别反复重发
|
||||
// 完全相同的内容"(自律,不是判据)。
|
||||
prompt += "- **You choose how many outputs and which channels.** **No \"one output per turn\" limit.** Keep messages whole; don't resend identical content.\n"
|
||||
prompt += "- Multi-step work: acknowledge on the current channel **first**, then run the real tools. An acknowledgement is not completion - finish and report.\n"
|
||||
|
||||
if a.indexer != nil {
|
||||
prompt += "\n\n" + a.indexer.BuildToolPrompt()
|
||||
}
|
||||
|
||||
// seq 的触发条件**刻意不写在这里**(用户裁定:seq 知识应在 skill 中)。
|
||||
//
|
||||
// 一度加过【加速序列】块,理由是"skill 索引只给一行摘要,模型不知道何时该用"。
|
||||
// 那个理由站不住:紧邻下方的【可用技能】已明确要求"准备调用某个插件的工具时
|
||||
// 先看同名技能",seq 的"什么时候用"写在 skill 正文里会被这条规则正确带到
|
||||
// 模型面前。再加一块提示词 = 同一件事记两处,且必然漂移。
|
||||
//
|
||||
// 副作用证据:加块后 TestSystemPromptStaysCompact 报 2022 > 2000。
|
||||
// 那个超限**本身就是信号**——该去掉的是内容,不是调高上限。
|
||||
|
||||
// 技能索引(方案B):轻量注入已加载技能列表,LLM 匹配到场景时
|
||||
// 主动 skill_info 拉取全文按文档执行
|
||||
if a.skillIndex != nil {
|
||||
if idx := a.skillIndex.SkillIndex(); idx != "" {
|
||||
// ★ 措辞从"用户请求匹配某技能时"改成"**调用插件工具之前**"。
|
||||
//
|
||||
// 旧措辞把人带偏成"只有用户点名要某个技能才去读",而 skill 的真实
|
||||
// 用途是**工具的使用说明**:里面写着参数怎么填、有哪些已知坑
|
||||
// (例如 QQ 的"拉私聊历史只传 user_id,不要传 group_id")、
|
||||
// 以及按需插件要先 plugin_start 拉起。
|
||||
//
|
||||
// 不看 skill 就直接调工具,是这个仓库反复吃过亏的一类错误。
|
||||
prompt = appendPromptBlock(prompt, "[Skills] Usage manuals for plugins. Before calling a plugin's tool (or when it fails), read the same-named skill with skill_info(\"skill-name\") and follow it. "+
|
||||
"Never guess from the name.\n\n"+idx)
|
||||
}
|
||||
}
|
||||
|
||||
// 首启人格门禁(跨通道唯一闸口):人格未确认时,要求模型主动询问用户。
|
||||
// 系统提示词每轮重建,因此 WebUI / QQ / CLI / ACP / 邮件等所有通道都会带上它;
|
||||
// 模型调用 persona_set(或用户在 WebUI 向导里选)落地后,标记置位,本段消失。
|
||||
if a.personaStore != nil && !a.personaStore.PersonaInitialized() {
|
||||
prompt = appendPromptBlock(prompt, "[First-run persona setup] Your **persona is not confirmed**. Ask once this turn: default persona, or a custom one? Then **call persona_set**: "+
|
||||
"default -> mode=default; custom -> mode=custom with the text in content; later -> mode=later. Until answered, don't assume it is set and don't repeat the question.")
|
||||
}
|
||||
|
||||
prompt += a.buildToolCatalog()
|
||||
|
||||
return prompt
|
||||
_ = userInput
|
||||
return a.renderSystemPrompt(memContext)
|
||||
}
|
||||
|
||||
func cleanParams(params map[string]interface{}) map[string]interface{} {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package kernelboot
|
||||
|
||||
import "github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
|
||||
// 把既有的内核 `init*` 函数**适配**成 Module,而不必重写它们的签名。
|
||||
//
|
||||
// ## 为什么不直接改 init* 的签名
|
||||
@ -20,7 +22,7 @@ package kernelboot
|
||||
// Name: "lua",
|
||||
// Requires: []string{"config"},
|
||||
// Provides: []string{"lua"},
|
||||
// Init: func(ks *kernelboot.KernelState) error {
|
||||
// Init: func(ks *kstate.State) error {
|
||||
// vm, closeFn := initLuaVM(ks.Cfg) // 依赖的产物:直接读字段
|
||||
// ks.LuaVM = vm // 自己的产物:写回字段
|
||||
// ks.AddCloser(closeFn) // 关闭函数统一登记
|
||||
@ -45,7 +47,7 @@ type FuncSpec struct {
|
||||
Requires []string
|
||||
Provides []string
|
||||
// Init 执行装配:从 ctx 取依赖、调既有函数、把产物 Put 回 ctx。
|
||||
Init func(ks *KernelState) error
|
||||
Init func(ks *kstate.State) error
|
||||
}
|
||||
|
||||
// funcModule 是 FuncSpec 的 Module 实现。
|
||||
@ -56,7 +58,7 @@ type funcModule struct {
|
||||
func (m *funcModule) Name() string { return m.spec.Name }
|
||||
func (m *funcModule) Provides() []string { return m.spec.Provides }
|
||||
func (m *funcModule) Requires() []string { return m.spec.Requires }
|
||||
func (m *funcModule) Init(ks *KernelState) error {
|
||||
func (m *funcModule) Init(ks *kstate.State) error {
|
||||
if m.spec.Init == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ package kernelboot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -60,7 +61,7 @@ type Module interface {
|
||||
// 模块从中**读**已就绪的依赖,把自己产出的公共数据**写**回它的字段。
|
||||
//
|
||||
// 返回错误表示装配失败(**必须传播,不得吞掉**)。
|
||||
Init(ks *KernelState) error
|
||||
Init(ks *kstate.State) error
|
||||
}
|
||||
|
||||
// Plan 是一次依赖解析的结果:分好层的调度计划。
|
||||
@ -278,7 +279,7 @@ type RunOptions struct {
|
||||
//
|
||||
// 为何由调用方传入而不是 Run 内部自建:产物在 Run 返回后**仍然要用**
|
||||
// (如 LuaVM、Memory 要一直活着),所以它必须比 Run 活得久。
|
||||
State *KernelState
|
||||
State *kstate.State
|
||||
|
||||
// Bus 是状态同步总线。非 nil 时,Run 会在**每个层边界**把该层模块贡献的
|
||||
// 设施标记为就绪(设计 §2.6.2)。
|
||||
@ -287,7 +288,7 @@ type RunOptions struct {
|
||||
// 若模块自己 MarkReady,顺序取决于**层内并发的完成顺序** ⇒ 不可复现;
|
||||
// 而且模块可能"忘记标记"(静默漏掉一个设施 ⇒ 插件永远等不到它就绪)。
|
||||
// 由 Run 在层边界统一写,既确定又不可能漏。
|
||||
Bus *SyncBus
|
||||
Bus *kstate.SyncBus
|
||||
}
|
||||
|
||||
// RunResult 是执行结果。
|
||||
@ -381,7 +382,7 @@ func Run(plan *Plan, opts RunOptions) (*RunResult, error) {
|
||||
|
||||
ks := opts.State
|
||||
if ks == nil {
|
||||
ks = NewKernelState()
|
||||
ks = kstate.New()
|
||||
}
|
||||
|
||||
// ★ 执行前把**全部**模块声明的设施登记为"内核注册过"。
|
||||
|
||||
@ -15,6 +15,7 @@ package kernelboot
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -40,7 +41,7 @@ type fakeModule struct {
|
||||
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(_ *KernelState) error {
|
||||
func (m *fakeModule) Init(_ *kstate.State) error {
|
||||
if m.initCount != nil {
|
||||
atomic.AddInt32(m.initCount, 1)
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ package kernelboot
|
||||
// (判据的"数量"不是指标,"每条原判据都有对应"才是)。
|
||||
|
||||
import (
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@ -25,7 +26,7 @@ import (
|
||||
// 为何逆序:先初始化的一般是底层(config → memory → llm),
|
||||
// 停机应**反向拆**,否则可能拆掉别人还在用的东西。
|
||||
func TestKernelStateCloseAllIsReverseOrder(t *testing.T) {
|
||||
ks := NewKernelState()
|
||||
ks := kstate.New()
|
||||
var mu sync.Mutex
|
||||
var order []string
|
||||
|
||||
@ -48,7 +49,7 @@ func TestKernelStateCloseAllIsReverseOrder(t *testing.T) {
|
||||
|
||||
// ★★★ 判据:**CloseAll 不因单个关闭函数 panic 而中断**。
|
||||
func TestKernelStateCloseAllSurvivesPanic(t *testing.T) {
|
||||
ks := NewKernelState()
|
||||
ks := kstate.New()
|
||||
closed := false
|
||||
ks.AddCloser(func() { panic("关闭时炸了") })
|
||||
ks.AddCloser(func() { closed = true })
|
||||
@ -61,7 +62,7 @@ func TestKernelStateCloseAllSurvivesPanic(t *testing.T) {
|
||||
|
||||
// ★★★ 判据:**CloseAll 幂等**(清空后再次调用不重复执行)。
|
||||
func TestKernelStateCloseAllIsIdempotent(t *testing.T) {
|
||||
ks := NewKernelState()
|
||||
ks := kstate.New()
|
||||
var n int32
|
||||
ks.AddCloser(func() { atomic.AddInt32(&n, 1) })
|
||||
|
||||
@ -78,7 +79,7 @@ func TestKernelStateCloseAllIsIdempotent(t *testing.T) {
|
||||
// 第 1 层有 8 个模块并行,其中 5 个注册关闭函数 ⇒ 并发 append
|
||||
// ⇒ 数据竞争 + 可能丢失元素(某个模块的关闭函数永不调用 ⇒ 资源泄漏)。
|
||||
func TestKernelStateAddCloserConcurrent(t *testing.T) {
|
||||
ks := NewKernelState()
|
||||
ks := kstate.New()
|
||||
const n = 500
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@ -99,7 +100,7 @@ func TestKernelStateAddCloserConcurrent(t *testing.T) {
|
||||
|
||||
// ★★★ 判据:**nil 关闭函数被忽略**(不 panic、不占位)。
|
||||
func TestKernelStateAddCloserIgnoresNil(t *testing.T) {
|
||||
ks := NewKernelState()
|
||||
ks := kstate.New()
|
||||
ks.AddCloser(nil)
|
||||
if len(ks.Closers) != 0 {
|
||||
t.Fatalf("nil 关闭函数应被忽略,实际记录 %d 个", len(ks.Closers))
|
||||
|
||||
@ -25,6 +25,7 @@ package kernelboot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"log"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@ -87,7 +88,7 @@ type PluginQuiescer interface {
|
||||
// defer sess.End() // 无论成功失败都唤醒
|
||||
// // ... 外部脚本迁移 ...
|
||||
type MigrateSession struct {
|
||||
ks *KernelState
|
||||
ks *kstate.State
|
||||
plugins PluginQuiescer
|
||||
paused []string
|
||||
stopped []string // 已运行态停掉的插件(按停止顺序,唤醒时逆序)
|
||||
@ -97,7 +98,7 @@ type MigrateSession struct {
|
||||
// BeginMigration 进入伪关机:停插件(运行态)→ 逆序暂停内核写者 → 等在途归零。
|
||||
//
|
||||
// 返回的 session 必须由调用方 `defer sess.End()`。
|
||||
func (s *KernelState) BeginMigration(pq PluginQuiescer) (*MigrateSession, error) {
|
||||
func BeginMigration(s *kstate.State, pq PluginQuiescer) (*MigrateSession, error) {
|
||||
sess := &MigrateSession{ks: s, plugins: pq}
|
||||
|
||||
// ① 先停**插件**(运行态停启,可逆)。
|
||||
|
||||
@ -21,9 +21,11 @@ import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
)
|
||||
|
||||
func newTestKS() *KernelState { return NewKernelState() }
|
||||
func newTestKS() *kstate.State { return kstate.New() }
|
||||
|
||||
// ★★★ 判据:Quiesce 逆序暂停、Resume 逆序恢复。
|
||||
func TestQuiescePausesInReverseOrder(t *testing.T) {
|
||||
@ -121,7 +123,7 @@ func TestMigrateSessionEndIsIdempotent(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
sess, err := ks.BeginMigration(nil)
|
||||
sess, err := BeginMigration(ks, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -185,7 +187,7 @@ func TestMigrationStopsAndRestoresAllPlugins(t *testing.T) {
|
||||
ks := newTestKS()
|
||||
fp := &fakePlugins{running: []string{"webui", "rss", "calendar", "commondev"}}
|
||||
|
||||
sess, err := ks.BeginMigration(fp)
|
||||
sess, err := BeginMigration(ks, fp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -209,7 +211,7 @@ func TestMigrationRollsBackPluginsOnFailure(t *testing.T) {
|
||||
// 让 calendar 失败 ⇒ 已停的 commondev 必须被拉回
|
||||
fp := &fakePlugins{running: []string{"webui", "rss", "calendar", "commondev"}, failOn: "calendar"}
|
||||
|
||||
if _, err := ks.BeginMigration(fp); err == nil {
|
||||
if _, err := BeginMigration(ks, fp); err == nil {
|
||||
t.Fatal("停插件失败时 BeginMigration 必须返回错误")
|
||||
}
|
||||
got := fp.RunningPlugins()
|
||||
|
||||
@ -29,6 +29,7 @@ package kernelboot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -56,7 +57,7 @@ type PluginSpec struct {
|
||||
// Scheduler 按 syncbus 状态调度插件。
|
||||
type Scheduler struct {
|
||||
mu sync.Mutex
|
||||
bus SyncState
|
||||
bus kstate.SyncState
|
||||
pending []PluginSpec
|
||||
launched map[string]bool
|
||||
launch func(PluginSpec) error
|
||||
@ -67,9 +68,9 @@ type Scheduler struct {
|
||||
|
||||
// NewScheduler 创建调度器。launch 是实际拉起动作(由插件管理器提供)。
|
||||
//
|
||||
// ★ bus 参数类型是 **SyncState**(只读接口),不是 *SyncBus。
|
||||
// ★ bus 参数类型是 **kstate.SyncState**(只读接口),不是 *kstate.SyncBus。
|
||||
// 这就是设计 §2.6.3 的权限隔离:调度器**在编译期就没有**写 syncbus 的能力。
|
||||
func NewScheduler(bus SyncState, launch func(PluginSpec) error) *Scheduler {
|
||||
func NewScheduler(bus kstate.SyncState, launch func(PluginSpec) error) *Scheduler {
|
||||
s := &Scheduler{
|
||||
bus: bus,
|
||||
launch: launch,
|
||||
@ -82,8 +83,8 @@ func NewScheduler(bus SyncState, launch func(PluginSpec) error) *Scheduler {
|
||||
//
|
||||
// 若 bus 不支持订阅(只传了个只读快照实现),退化为"只在 Submit 时评估"——
|
||||
// 那种情况下调度器仍可用,只是不会自动跟进后续状态变化。
|
||||
if sub, ok := bus.(interface{ OnUpdate(func(SyncState)) }); ok {
|
||||
sub.OnUpdate(func(SyncState) { s.evaluate() })
|
||||
if sub, ok := bus.(interface{ OnUpdate(func(kstate.SyncState)) }); ok {
|
||||
sub.OnUpdate(func(kstate.SyncState) { s.evaluate() })
|
||||
}
|
||||
return s
|
||||
}
|
||||
@ -293,7 +294,7 @@ func (s *Scheduler) WaitString(p PluginSpec) string {
|
||||
if len(p.RequiresFacilities) == 0 {
|
||||
return "等内核完全初始化"
|
||||
}
|
||||
// AllReady 需要 *SyncBus;这里只做只读渲染,用 Snapshot 判断
|
||||
// AllReady 需要 *kstate.SyncBus;这里只做只读渲染,用 Snapshot 判断
|
||||
snap := s.bus.Snapshot()
|
||||
var missing []string
|
||||
for _, f := range p.RequiresFacilities {
|
||||
|
||||
@ -25,17 +25,18 @@ package kernelboot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ★★★ 压力:**SyncBus 读写并发**(内核写 + N 个插件读 + 订阅回调)。
|
||||
// ★★★ 压力:**kstate.SyncBus 读写并发**(内核写 + N 个插件读 + 订阅回调)。
|
||||
//
|
||||
// 用 -race 跑时,任何未保护的状态都会报数据竞争。
|
||||
func TestStressSyncBusConcurrentReadWrite(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
const readers = 64
|
||||
const facilities = 200
|
||||
|
||||
@ -43,7 +44,7 @@ func TestStressSyncBusConcurrentReadWrite(t *testing.T) {
|
||||
// 订阅者**读状态**(这正是死锁的触发条件:若 fire 持锁调用回调,
|
||||
// 回调里 Snapshot() 会等读锁 ⇒ 死锁。压力下必现)
|
||||
for i := 0; i < 8; i++ {
|
||||
bus.OnUpdate(func(s SyncState) {
|
||||
bus.OnUpdate(func(s kstate.SyncState) {
|
||||
_ = s.Snapshot()
|
||||
_ = s.AllReady([]string{"llm"})
|
||||
atomic.AddInt64(&callbacks, 1)
|
||||
@ -109,7 +110,7 @@ func TestStressSyncBusConcurrentReadWrite(t *testing.T) {
|
||||
// · 不丢插件(全部最终被拉起)
|
||||
// · 无数据竞争
|
||||
func TestStressSchedulerConcurrentSubmit(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
bus.MarkDeclared([]string{"config", "llm", "memory"})
|
||||
// ★ 必须 MarkLayerReady:MarkDeclared 只登记"名字存在",
|
||||
// 不代表就绪。插件要求 config **就绪**才拉起。
|
||||
@ -164,7 +165,7 @@ func TestStressSchedulerConcurrentSubmit(t *testing.T) {
|
||||
|
||||
// ★★★ 压力:**状态变化 + 提交并发**(真实时序:内核在标记就绪的同时插件在提交)。
|
||||
func TestStressSchedulerConcurrentStateChange(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
bus.MarkDeclared([]string{"config", "llm", "memory", "stagehost"})
|
||||
|
||||
var launched int64
|
||||
@ -291,9 +292,9 @@ func TestStressRunRespectsMaxParallelAtScale(t *testing.T) {
|
||||
t.Logf("%d 个模块同层,上限 %d,观测峰值 %d", n, maxP, observed)
|
||||
}
|
||||
|
||||
// ★★★ 压力:**KernelState 同层并行写**(各写自己字段,无竞争)。
|
||||
// ★★★ 压力:**kstate.State 同层并行写**(各写自己字段,无竞争)。
|
||||
func TestStressKernelStateParallelWrites(t *testing.T) {
|
||||
ks := NewKernelState()
|
||||
ks := kstate.New()
|
||||
const n = 256
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
@ -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"
|
||||
})
|
||||
|
||||
@ -12,13 +12,14 @@ package kernelboot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ★★★ 判据:**拼错的设施名在 Submit 时被拒绝**(不再静默挂起)。
|
||||
func TestSchedulerRejectsUnknownFacility(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
bus.MarkDeclared([]string{"config", "llm", "memory"}) // 内核注册过这些
|
||||
|
||||
var launched []string
|
||||
@ -57,7 +58,7 @@ func TestSchedulerRejectsUnknownFacility(t *testing.T) {
|
||||
|
||||
// ★★★ 判据:**正确的设施名不受影响**(不能把正常路径也拦了)。
|
||||
func TestSchedulerAcceptsKnownFacility(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
bus.MarkDeclared([]string{"config", "llm"})
|
||||
// ★ 必须同时 MarkLayerReady —— 真实路径里 Run 两者都会做:
|
||||
// MarkDeclared(执行前,登记名字)与 MarkLayerReady(每层完成,标记就绪)。
|
||||
@ -86,7 +87,7 @@ func TestSchedulerAcceptsKnownFacility(t *testing.T) {
|
||||
// ★ 这条很重要:llm 声明了但还在第 1 层时,Known 为 true、Ready 为 false。
|
||||
// 若把"未就绪"当"拼错",所有多依赖插件都会在提交时被误拒。
|
||||
func TestSchedulerAcceptsDeclaredButNotYetReady(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
bus.MarkDeclared([]string{"config", "llm"}) // 两者都已声明
|
||||
bus.MarkLayerReady([]string{"config"}) // 只有 config 就绪
|
||||
|
||||
@ -111,7 +112,7 @@ func TestSchedulerAcceptsDeclaredButNotYetReady(t *testing.T) {
|
||||
// 那些设施名就从未被 MarkLayerReady 登记 ⇒ 会**误报**"内核从未注册过"。
|
||||
// 所以 Run 必须**执行前**就 MarkDeclared。
|
||||
func TestRunDeclaresAllFacilitiesUpfront(t *testing.T) {
|
||||
bus := NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
mods := []Module{
|
||||
&fakeModule{name: "a", provides: []string{"fa"}},
|
||||
&fakeModule{name: "b", provides: []string{"fb"}, requires: []string{"fa"},
|
||||
|
||||
@ -13,6 +13,7 @@ package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -21,7 +22,6 @@ import (
|
||||
agentCore "github.com/JianFeeeee/HologramAgent/internal/agent/core"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/config"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory"
|
||||
"github.com/JianFeeeee/HologramAgent/pkg/types"
|
||||
@ -58,7 +58,7 @@ func (Module) Requires() []string {
|
||||
}
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacAgent} }
|
||||
|
||||
func (Module) Init(k *kernelboot.KernelState) error {
|
||||
func (Module) Init(k *kstate.State) error {
|
||||
if k.Memory == nil {
|
||||
return errMissing("agent", kernelmod.FacMemory)
|
||||
}
|
||||
@ -99,7 +99,7 @@ Answer only from what the tools actually return. If something could not be retri
|
||||
|
||||
Reply in natural language, like a person talking: do not enumerate which tools you called, their parameters, or timings unless the user explicitly asks. For how to address the user and what tone to use, see [Persona].`
|
||||
|
||||
func buildAgent(k *kernelboot.KernelState) *agentCore.Agent {
|
||||
func buildAgent(k *kstate.State) *agentCore.Agent {
|
||||
cfg := k.Cfg
|
||||
cfgReg := k.CfgReg
|
||||
provider := k.LLMProvider
|
||||
@ -117,7 +117,9 @@ func buildAgent(k *kernelboot.KernelState) *agentCore.Agent {
|
||||
multimodalSpace := k.MultimodalSpace
|
||||
mmProviderName := k.MultimodalProviderName
|
||||
mmErr := k.MultimodalErr
|
||||
stageHost := k.StageHost
|
||||
// StageHost 在挂载点里存 any(kstate 不能依赖 agent/core,否则成环),
|
||||
// 取用处断言回具体类型。装配顺序由依赖图保证(FacStageHost)。
|
||||
stageHost, _ := k.StageHost.(*agentCore.StageHost)
|
||||
evBus := k.EventBus
|
||||
secRT := k.Security
|
||||
|
||||
@ -308,8 +310,12 @@ func buildAgent(k *kernelboot.KernelState) *agentCore.Agent {
|
||||
// pluginhost 同时可能正在加载插件(buildSDK 读 status)⇒ 写读依然并发。
|
||||
// 所以 registry 侧 Set*/读侧的锁是**真正的修复**,去重只是消除了一处
|
||||
// 无意义的重复写。两件事都做,缺一不可。
|
||||
func wirePluginSDK(k *kernelboot.KernelState) {
|
||||
k.PluginRegistry.SetStatusProvider(k.Agent)
|
||||
func wirePluginSDK(k *kstate.State) {
|
||||
// Agent 是本包自己的类型,构造期已知;不走挂载点断言。
|
||||
a, _ := k.Agent.(*agentCore.Agent)
|
||||
if a != nil {
|
||||
k.PluginRegistry.SetStatusProvider(a)
|
||||
}
|
||||
}
|
||||
|
||||
// loadPersonality 载入人格(原 cmd/homed 的整块平移)。
|
||||
|
||||
@ -131,6 +131,7 @@ package all
|
||||
// 源码怎么组织都无所谓 —— 只认"装配起来之后发生了什么"。
|
||||
|
||||
import (
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
@ -158,8 +159,8 @@ import (
|
||||
// machine 是一台**真实装配起来**的内核(全部模块都是 kernelmod 里的真货)。
|
||||
type machine struct {
|
||||
t *testing.T
|
||||
ks *kernelboot.KernelState
|
||||
bus *kernelboot.SyncBus
|
||||
ks *kstate.State
|
||||
bus *kstate.SyncBus
|
||||
// plan 是真实登记表算出的分层计划(启动图)。
|
||||
plan *kernelboot.Plan
|
||||
// res 是执行结果(层数 / 峰值并发 / 完成顺序)。
|
||||
@ -203,7 +204,7 @@ func bootMachine(t *testing.T) *machine {
|
||||
// (实测:3 轮后 goroutine 数 2→5,逐轮 +1,全部来自 database/sql)。
|
||||
// 关闭点放到 shutdown()(与生产的停机序列同处),并由 machine 保证只关一次。
|
||||
|
||||
ks := kernelboot.NewKernelState()
|
||||
ks := kstate.New()
|
||||
ks.CfgReg = reg
|
||||
ks.DataDir = dir
|
||||
ks.AgentWorkDir = filepath.Join(dir, "agentfs")
|
||||
@ -225,7 +226,7 @@ func bootMachine(t *testing.T) *machine {
|
||||
t.Fatalf("★ 真实登记表算不出启动图(漏挂模块 / 缺依赖 / 环): %v", err)
|
||||
}
|
||||
|
||||
bus := kernelboot.NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
// 与生产 RunKernelModules 同序:先 MarkDeclared(让拼错校验有真相源),
|
||||
// 再 Run(Run 内部还会 MarkDeclared,幂等)。
|
||||
var declared []string
|
||||
@ -869,7 +870,7 @@ func removeAllWithRetry(dir string, tries int) error {
|
||||
// 只读反射(不 Set、不 Interface),用于把"关闭时挂订阅"这种泄漏
|
||||
// 变成可断言的数字。字段名/类型变化时本函数会 fatalf —— 那是判据
|
||||
// 失去观测点,必须显式修,不能静默变成 0。
|
||||
func countBusSubscribers(t *testing.T, bus *kernelboot.SyncBus) int {
|
||||
func countBusSubscribers(t *testing.T, bus *kstate.SyncBus) int {
|
||||
t.Helper()
|
||||
if bus == nil {
|
||||
return 0
|
||||
|
||||
@ -26,6 +26,7 @@ package all
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -68,10 +69,10 @@ func layerNames(layer []kernelboot.Module) []string {
|
||||
// 变异,模块会真的跑到写盘那一行。不填的话 text-memory 会往**仓内**
|
||||
// 当前目录下的 memory/text 写(实测产生过 internal/kernelmod/all/memory/)。
|
||||
// 夹具不能往仓库里漏文件。
|
||||
func emptyState(t *testing.T) *kernelboot.KernelState {
|
||||
func emptyState(t *testing.T) *kstate.State {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
ks := kernelboot.NewKernelState()
|
||||
ks := kstate.New()
|
||||
ks.Cfg = &types.Config{}
|
||||
ks.Cfg.Daemon.DataDir = dir
|
||||
ks.Cfg.Plugin.Dir = filepath.Join(dir, "plugins")
|
||||
|
||||
@ -27,6 +27,9 @@ package all
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
agentCore "github.com/JianFeeeee/HologramAgent/internal/agent/core"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"testing"
|
||||
|
||||
agentcore "github.com/JianFeeeee/HologramAgent/internal/agent/core"
|
||||
@ -118,7 +121,13 @@ func TestAgentAssemblyWiresSecurity(t *testing.T) {
|
||||
const deniedTool = "boot_probe_denied_tool"
|
||||
|
||||
// ① 先证明这个工具名**没被注册**(否则下面"not permitted"可能来自别处)。
|
||||
out, _ := ks.Agent.ExecuteTool(deniedTool, nil)
|
||||
// ks.Agent 在挂载点里存 any(kstate 不能依赖 agent/core,否则成环)。
|
||||
// 本判据要真实执行工具,断言回具体类型。
|
||||
agentImpl, _ := ks.Agent.(*agentCore.Agent)
|
||||
if agentImpl == nil {
|
||||
t.Fatal("ks.Agent 不是 *agentCore.Agent")
|
||||
}
|
||||
out, _ := agentImpl.ExecuteTool(deniedTool, nil)
|
||||
if strings.Contains(out, "not permitted") {
|
||||
t.Fatalf("前置不成立:未配黑名单时就已被拒(%q)—— 检查环境里的实际策略", out)
|
||||
}
|
||||
@ -127,7 +136,7 @@ func TestAgentAssemblyWiresSecurity(t *testing.T) {
|
||||
// 如果 Agent 里拿的是另一个实例(或 nil),它看不到这条 ⇒ 判据红。
|
||||
ks.Security.SetPolicy(security.Policy{ToolDeny: []string{deniedTool}})
|
||||
|
||||
out, _ = ks.Agent.ExecuteTool(deniedTool, nil)
|
||||
out, _ = agentImpl.ExecuteTool(deniedTool, nil)
|
||||
if !strings.Contains(out, "not permitted") {
|
||||
t.Fatalf("★ 已给 ks.Security 配 %q 黑名单,Agent 的执行链却没拦住(实际返回 %q)。\n"+
|
||||
" 可能原因(按概率):\n"+
|
||||
@ -140,7 +149,7 @@ func TestAgentAssemblyWiresSecurity(t *testing.T) {
|
||||
|
||||
// ③ 反向:未命中的工具必须放行。
|
||||
// 否则是"全拒"型假绿(闸门写死了),而不是接线正确。
|
||||
if out, _ := ks.Agent.ExecuteTool("boot_probe_unlisted_tool", nil); strings.Contains(out, "not permitted") {
|
||||
if out, _ := agentImpl.ExecuteTool("boot_probe_unlisted_tool", nil); strings.Contains(out, "not permitted") {
|
||||
t.Fatalf("未命中黑名单的工具被误拒(%q)—— 判定逻辑坏了,不是接线问题", out)
|
||||
}
|
||||
}
|
||||
@ -182,7 +191,7 @@ func TestAgentAssemblyRefusesNilSecurity(t *testing.T) {
|
||||
//
|
||||
// 为什么这样做而不是直接用 base:Init 成功时会覆盖 base.Agent,
|
||||
// 破坏后续判据的上下文。新建一份是干净的。
|
||||
var probe kernelboot.KernelState
|
||||
var probe kstate.State
|
||||
probe.Cfg = base.Cfg
|
||||
probe.CfgReg = base.CfgReg
|
||||
probe.DataDir = base.DataDir
|
||||
|
||||
@ -18,8 +18,8 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
)
|
||||
|
||||
// Module 提供配置快照。
|
||||
@ -29,7 +29,7 @@ func (Module) Name() string { return "config" }
|
||||
func (Module) Requires() []string { return nil } // 前提由 Group 0 保证
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacConfig} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
// ★ 检查而不是假设(用户说的"nil 加读取前检查"):
|
||||
// ks.Cfg 为 nil 说明 Group 0 段没跑或顺序错了。响亮报错远好于让后续
|
||||
// 模块拿到 nil —— 那会在很远的地方变成"模型没有配置"之类的模糊症状。
|
||||
|
||||
@ -30,8 +30,8 @@ import (
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/agent/api"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
sec "github.com/JianFeeeee/HologramAgent/internal/security"
|
||||
)
|
||||
|
||||
@ -46,9 +46,12 @@ func (Module) Requires() []string {
|
||||
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacDevice} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
if ks.Agent == nil {
|
||||
return fmt.Errorf("device module: ks.Agent is nil (requires %s)", kernelmod.FacAgent)
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
// Agent 在挂载点里存 any(kstate 不能依赖 agent/core,否则成环)。
|
||||
// 这里断言成**最小接口**而不是具体类型:device 只需要"能挂数据面"。
|
||||
host, ok := ks.Agent.(api.DeviceMounter)
|
||||
if !ok || host == nil {
|
||||
return fmt.Errorf("device module: ks.Agent is not a DeviceMounter (requires %s)", kernelmod.FacAgent)
|
||||
}
|
||||
if ks.Security == nil {
|
||||
// 安全模块必须已装配(配置关闭时是 Disabled Runtime,**不是** nil)。
|
||||
@ -57,9 +60,15 @@ func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
"assembled first (when disabled it installs a Disabled Runtime, not nil)")
|
||||
}
|
||||
|
||||
rt := New(ks.Agent, ks.Security)
|
||||
// ExecuteTool 面:ToolHost 与 DeviceMounter 是两个不同用途的接口
|
||||
// (执行 vs 装配),此处两个断言都要成功。
|
||||
toolHost, ok := ks.Agent.(api.ToolHost)
|
||||
if !ok || toolHost == nil {
|
||||
return fmt.Errorf("device module: ks.Agent is not a ToolHost (requires %s)", kernelmod.FacAgent)
|
||||
}
|
||||
rt := New(toolHost, ks.Security)
|
||||
ks.Device = rt
|
||||
ks.Agent.MountDevice(rt)
|
||||
host.MountDevice(rt)
|
||||
|
||||
if ks.EventBus != nil {
|
||||
ks.EventBus.Publish(&events.Event{
|
||||
|
||||
@ -7,8 +7,8 @@ import (
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory/coldcontext"
|
||||
)
|
||||
@ -20,7 +20,7 @@ func (Module) Name() string { return "doc-store" }
|
||||
func (Module) Requires() []string { return []string{kernelmod.FacConfig} }
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacDocStore} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
coldcontextStore := coldcontext.NewStore(
|
||||
filepath.Join(ks.Cfg.Daemon.DataDir, "memory", "documents"), memory.TokenizeWords)
|
||||
if err := coldcontextStore.Start(); err != nil {
|
||||
|
||||
@ -12,8 +12,8 @@ import (
|
||||
|
||||
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
)
|
||||
|
||||
// Module 提供 IO 抽象层与事件总线。
|
||||
@ -23,7 +23,7 @@ func (Module) Name() string { return "kernel-api" }
|
||||
func (Module) Requires() []string { return nil }
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacKernelIO} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
iom := agentIO.NewIOManager()
|
||||
evBus := events.NewBus()
|
||||
log.Printf("[homed] kernel API ready: IOManager + EventBus")
|
||||
|
||||
@ -16,8 +16,8 @@ package knowledge
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
)
|
||||
|
||||
// MigrationModule 做知识库目录改名迁移。
|
||||
@ -27,7 +27,7 @@ func (MigrationModule) Name() string { return "knowledge-migration" }
|
||||
func (MigrationModule) Requires() []string { return []string{kernelmod.FacConfig} }
|
||||
func (MigrationModule) Provides() []string { return []string{kernelmod.FacMigrated} }
|
||||
|
||||
func (MigrationModule) Init(ks *kernelboot.KernelState) error {
|
||||
func (MigrationModule) Init(ks *kstate.State) error {
|
||||
root := filepath.Join(ks.Cfg.Daemon.DataDir, "knowledge")
|
||||
runKnowledgeMigration(root)
|
||||
return nil
|
||||
|
||||
@ -5,9 +5,9 @@ import (
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/knowledge"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
)
|
||||
|
||||
// StoreModule 提供知识库。
|
||||
@ -20,7 +20,7 @@ func (StoreModule) Requires() []string {
|
||||
}
|
||||
func (StoreModule) Provides() []string { return []string{kernelmod.FacKnowledge} }
|
||||
|
||||
func (StoreModule) Init(ks *kernelboot.KernelState) error {
|
||||
func (StoreModule) Init(ks *kstate.State) error {
|
||||
ks.Knowledge = knowledge.NewStore(filepath.Join(ks.Cfg.Daemon.DataDir, "knowledge"))
|
||||
if err := ks.Knowledge.Start(); err != nil {
|
||||
log.Printf("[homed] warning: knowledge store: %v", err)
|
||||
|
||||
@ -8,8 +8,8 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/agent/api"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
luapkg "github.com/JianFeeeee/HologramAgent/internal/lua"
|
||||
"github.com/JianFeeeee/HologramAgent/pkg/types"
|
||||
)
|
||||
@ -21,7 +21,7 @@ func (Module) Name() string { return "llm" }
|
||||
func (Module) Requires() []string { return []string{kernelmod.FacConfig, kernelmod.FacLua} }
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacLLM} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
// ★ 敏感信息防火模块的装配点。
|
||||
//
|
||||
// 原 main 注释:"必须在任何 provider 注册之前装配"—— provider 的出向
|
||||
|
||||
@ -7,8 +7,8 @@ import (
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
luapkg "github.com/JianFeeeee/HologramAgent/internal/lua"
|
||||
)
|
||||
|
||||
@ -19,7 +19,7 @@ func (Module) Name() string { return "lua" }
|
||||
func (Module) Requires() []string { return []string{kernelmod.FacConfig} }
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacLua} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
luaVM := luapkg.NewVM(filepath.Join(ks.Cfg.Daemon.DataDir, "adapters"))
|
||||
if err := luaVM.Start(); err != nil {
|
||||
log.Printf("[homed] warning: lua vm init failed: %v", err)
|
||||
|
||||
@ -20,12 +20,12 @@ package memory
|
||||
// 判据改为直接跑真实模块(经 kernelboot.Run),确保测的是**真实入口**。
|
||||
|
||||
import (
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
internalConfig "github.com/JianFeeeee/HologramAgent/internal/config"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
)
|
||||
|
||||
// ★★★ 判据:模块 Init 返回后蒸馏器**仍在运行**(无 defer Stop 残留)。
|
||||
@ -38,7 +38,7 @@ func TestMemoryModuleKeepsDistillerRunning(t *testing.T) {
|
||||
// 而它对 nil 会现在**明确报错**(而不是 panic 在 strings.Split)。
|
||||
reg := internalConfig.NewConfigRegistry(filepath.Join(dir, "config.db"))
|
||||
defer reg.Close()
|
||||
ks := &kernelboot.KernelState{DataDir: dir, CfgReg: reg}
|
||||
ks := &kstate.State{DataDir: dir, CfgReg: reg}
|
||||
|
||||
if err := (Module{}).Init(ks); err != nil {
|
||||
t.Fatalf("memory 模块 Init: %v", err)
|
||||
@ -73,7 +73,7 @@ func TestMemoryModuleRegistersCloser(t *testing.T) {
|
||||
os.MkdirAll(filepath.Join(dir, "memory"), 0o755)
|
||||
reg := internalConfig.NewConfigRegistry(filepath.Join(dir, "config.db"))
|
||||
defer reg.Close()
|
||||
ks := &kernelboot.KernelState{DataDir: dir, CfgReg: reg}
|
||||
ks := &kstate.State{DataDir: dir, CfgReg: reg}
|
||||
if err := (Module{}).Init(ks); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@ -18,8 +18,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/config"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory/distill"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory/pipeline"
|
||||
@ -34,7 +34,7 @@ func (Module) Name() string { return "memory" }
|
||||
func (Module) Requires() []string { return []string{kernelmod.FacKernelIO} }
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacMemory} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
// ★ 读取前检查(用户指定的做法:"结构体成员初始置 nil 加读取前检查")。
|
||||
//
|
||||
// 为何要检查 CfgReg:它是 **Group 0** 的产物(不由任何调度模块提供),
|
||||
@ -78,7 +78,7 @@ func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
distiller.Start()
|
||||
}
|
||||
|
||||
ks.Memory = &kernelboot.MemoryBundle{
|
||||
ks.Memory = &kstate.MemoryBundle{
|
||||
DB: memDB,
|
||||
Indexer: memIdx,
|
||||
Social: socialStore,
|
||||
|
||||
@ -21,13 +21,13 @@ import (
|
||||
"testing"
|
||||
|
||||
internalConfig "github.com/JianFeeeee/HologramAgent/internal/config"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
)
|
||||
|
||||
// ★★★ 判据:**CfgReg 为 nil 时给可操作错误,而不是 panic**。
|
||||
func TestMemoryModuleRejectsNilCfgReg(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ks := &kernelboot.KernelState{DataDir: dir} // CfgReg 故意为 nil
|
||||
ks := &kstate.State{DataDir: dir} // CfgReg 故意为 nil
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -54,7 +54,7 @@ func TestMemoryModuleRejectsNilCfgReg(t *testing.T) {
|
||||
func TestMemoryModuleRejectsEmptyDataDir(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
reg := newTestReg(t, dir)
|
||||
ks := &kernelboot.KernelState{CfgReg: reg} // DataDir 空
|
||||
ks := &kstate.State{CfgReg: reg} // DataDir 空
|
||||
|
||||
err := (Module{}).Init(ks)
|
||||
if err == nil {
|
||||
|
||||
@ -31,8 +31,8 @@ import (
|
||||
"sort"
|
||||
"syscall"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/systemd"
|
||||
)
|
||||
|
||||
@ -68,7 +68,7 @@ type DirInfo struct {
|
||||
Kind string `json:"kind"` // data | runtime | optional | mount
|
||||
}
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
// 体检结果放在 KernelState 上,供工具/WebUI 读(可选能力,nil 安全)
|
||||
ks.MigrationPreflight = func(target string) (interface{}, error) {
|
||||
return Preflight(ks.DataDir, target)
|
||||
|
||||
@ -7,8 +7,8 @@ import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory/vector"
|
||||
"github.com/JianFeeeee/HologramAgent/pkg/embedding"
|
||||
)
|
||||
@ -20,7 +20,7 @@ func (Module) Name() string { return "multimodal" }
|
||||
func (Module) Requires() []string { return []string{kernelmod.FacConfig} }
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacMultimodal} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
cfgReg := ks.CfgReg
|
||||
var multimodalSpace vector.MultimodalEmbedder
|
||||
// 这两个值只用于状态报告(healthcheck_kernel 的 onnx 段):
|
||||
|
||||
@ -17,8 +17,8 @@ import (
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/nlp"
|
||||
)
|
||||
|
||||
@ -29,7 +29,7 @@ func (Module) Name() string { return "onnx-parser" }
|
||||
func (Module) Requires() []string { return []string{kernelmod.FacConfig} }
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacONNX} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
modelPath := ks.CfgReg.GetString("core.agent.onnx_model_path", "")
|
||||
onnxParser, err := nlp.NewONNXParser(nlp.ONNXConfig{
|
||||
ModelPath: modelPath,
|
||||
|
||||
@ -12,6 +12,7 @@ package pluginhost
|
||||
// ④ 关键顺序:failback 白名单必须在插件拉起前生效
|
||||
|
||||
import (
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
@ -311,8 +312,8 @@ func TestCatalogConcurrentRead(t *testing.T) {
|
||||
// ⇒ 判据:把 typed-nil 传给 Scheduler 前必须已被拦截。
|
||||
// 这里直接验证**拦截逻辑本身**(不依赖 produciton 装配)。
|
||||
func TestNilSyncBusDoesNotPanic(t *testing.T) {
|
||||
var nb *kernelboot.SyncBus // typed-nil
|
||||
var iface kernelboot.SyncState = nb
|
||||
var nb *kstate.SyncBus // typed-nil
|
||||
var iface kstate.SyncState = nb
|
||||
if iface == nil {
|
||||
t.Skip("接口就是 nil(不会有陷阱)—— 与本判据的前提不符")
|
||||
}
|
||||
@ -378,7 +379,7 @@ func TestUndeclaredPluginsWaitForKernelComplete(t *testing.T) {
|
||||
t.Fatalf("两个自动启动插件都应进调度器(含**未声明**的),实际 %d 个", len(specs))
|
||||
}
|
||||
|
||||
bus := kernelboot.NewSyncBus()
|
||||
bus := kstate.NewSyncBus()
|
||||
bus.MarkDeclared([]string{"config", "agent"})
|
||||
bus.MarkReady("config")
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@ package pluginhost
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -44,7 +46,7 @@ func (Module) Requires() []string {
|
||||
|
||||
func (Module) Provides() []string { return []string{FacPlugins} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
// ── 前置检查(用户要求:"nil 加读取前检查")──
|
||||
if ks.PluginRegistry == nil || ks.StageHost == nil {
|
||||
return fmt.Errorf("plugin-host:ks.PluginRegistry/StageHost 为 nil —— " +
|
||||
@ -111,8 +113,8 @@ func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
//
|
||||
// 这样声明的插件仍在依赖就绪时尽早启动(本步的核心收益不变),
|
||||
// 而未声明的**恢复原行为**(等内核完全初始化)。
|
||||
var bus kernelboot.SyncState
|
||||
var busConcrete *kernelboot.SyncBus // 保留具体类型以便订阅 OnUpdate
|
||||
var bus kstate.SyncState
|
||||
var busConcrete *kstate.SyncBus // 保留具体类型以便订阅 OnUpdate
|
||||
if ks.SyncBus != nil {
|
||||
bus = ks.SyncBus
|
||||
busConcrete = ks.SyncBus
|
||||
@ -122,7 +124,7 @@ func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
// 所以这里必须显式判 `!= nil` 再赋给接口变量。
|
||||
log.Printf("[pluginhost] warning: SyncBus is nil -- non-production assembly path, " +
|
||||
"退化为立即加载(依赖未就绪的插件在 Start 时自行处理)")
|
||||
fallback := kernelboot.NewSyncBus()
|
||||
fallback := kstate.NewSyncBus()
|
||||
fallback.MarkKernelComplete()
|
||||
bus = fallback
|
||||
busConcrete = fallback
|
||||
@ -172,7 +174,7 @@ func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
// 不在 catalog 里,只有 LoadSkipping 的工厂分支能加载它们)。
|
||||
skip := buildSkipSet(catalog, groups)
|
||||
var loadOnce sync.Once
|
||||
loadRest := func(st kernelboot.SyncState) {
|
||||
loadRest := func(st kstate.SyncState) {
|
||||
if !st.KernelComplete() {
|
||||
return
|
||||
}
|
||||
@ -296,7 +298,7 @@ func buildSkipSet(c *PluginCatalog, g formGroups) map[string]bool {
|
||||
// ★ 这里**刻意不做** SetStatusProvider —— 它依赖 agent,
|
||||
// 由 agent 模块**后补注入**(见其注释)。这不是遗漏,是有意的分层:
|
||||
// 让插件加载不必等 agent 那 35 秒。
|
||||
func injectBaseDeps(ks *kernelboot.KernelState) {
|
||||
func injectBaseDeps(ks *kstate.State) {
|
||||
reg := ks.PluginRegistry
|
||||
reg.SetLuaVM(ks.LuaVM)
|
||||
reg.SetBaseAPIKey(ks.BaseAPIKey)
|
||||
@ -305,7 +307,17 @@ func injectBaseDeps(ks *kernelboot.KernelState) {
|
||||
}
|
||||
reg.SetTracker(ks.Tracker)
|
||||
reg.SetConfig(ks.Cfg)
|
||||
reg.SetStageHost(ks.StageHost)
|
||||
// StageHost 在挂载点里存 any(kstate 不依赖 agent/core,否则成环)。
|
||||
// 断言成 sdk.ToolSource(plugin 真正需要的只读工具视图)。
|
||||
//
|
||||
// 断言失败时**不静默跳过**:SetStageHost 不设的后果是插件加载后
|
||||
// 工具表为空,而那与"插件没注册工具"无法区分。所以这里响亮记录。
|
||||
// (Init 里已检查过 StageHost != nil,此处失败是类型不合约,属装配缺陷。)
|
||||
if ts, ok := ks.StageHost.(sdk.ToolSource); ok && ts != nil {
|
||||
reg.SetStageHost(ts)
|
||||
} else {
|
||||
log.Printf("[plugin-host] WARNING: ks.StageHost 未实现 sdk.ToolSource —— 插件工具将不可见")
|
||||
}
|
||||
|
||||
if ks.Memory != nil {
|
||||
reg.SetIndexer(ks.Memory.Indexer)
|
||||
@ -350,7 +362,7 @@ func classify(c *PluginCatalog) formGroups {
|
||||
}
|
||||
|
||||
// loadOne 加载单个插件(走既有 LoadOneDir 路径,不绕过任何机制)。
|
||||
func loadOne(ks *kernelboot.KernelState, dir, name string) (bool, error) {
|
||||
func loadOne(ks *kstate.State, dir, name string) (bool, error) {
|
||||
if err := ks.PluginRegistry.LoadOneDir(filepath.Join(dir, name), name); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -358,7 +370,7 @@ func loadOne(ks *kernelboot.KernelState, dir, name string) (bool, error) {
|
||||
}
|
||||
|
||||
// planCatalog 取元数据缓存(类型断言集中一处,便于排查"缓存没建起来")。
|
||||
func planCatalog(ks *kernelboot.KernelState) *PluginCatalog {
|
||||
func planCatalog(ks *kstate.State) *PluginCatalog {
|
||||
c, _ := ks.PluginCatalog.(*PluginCatalog)
|
||||
return c
|
||||
}
|
||||
@ -368,7 +380,7 @@ func planCatalog(ks *kernelboot.KernelState) *PluginCatalog {
|
||||
// 失败只记日志、不阻断:系统本身仍可正常接受输入,只是依赖本事件的
|
||||
// 订阅者(如 seq 的开机序列)不会自动执行。**必须可见** ——
|
||||
// 静默失败会让"为什么开机序列没跑"完全无从查起。
|
||||
func publishToolsReady(ks *kernelboot.KernelState) {
|
||||
func publishToolsReady(ks *kstate.State) {
|
||||
if ks == nil || ks.EventBus == nil {
|
||||
log.Printf("[pluginhost] tools_ready NOT published: EventBus is nil " +
|
||||
"(subscribers such as seq boot sequences will not run)")
|
||||
|
||||
@ -14,7 +14,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
)
|
||||
|
||||
// 没有 EventBus 时必须**响亮报错而不是静默返回** ——
|
||||
@ -26,7 +26,7 @@ func TestPublishToolsReadyWithoutBusDoesNotPanic(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
publishToolsReady(nil)
|
||||
publishToolsReady(&kernelboot.KernelState{})
|
||||
publishToolsReady(&kstate.State{})
|
||||
}
|
||||
|
||||
// 有 EventBus 时必须真的发布一条 tools_ready,且带 plugin_count。
|
||||
@ -35,7 +35,7 @@ func TestPublishToolsReadyPublishes(t *testing.T) {
|
||||
var got []*events.Event
|
||||
bus.Subscribe(events.EventToolsReady, func(e *events.Event) { got = append(got, e) })
|
||||
|
||||
ks := &kernelboot.KernelState{EventBus: bus}
|
||||
ks := &kstate.State{EventBus: bus}
|
||||
publishToolsReady(ks)
|
||||
|
||||
if len(got) != 1 {
|
||||
|
||||
@ -39,8 +39,8 @@ import (
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/config"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
sec "github.com/JianFeeeee/HologramAgent/internal/security"
|
||||
)
|
||||
|
||||
@ -51,7 +51,7 @@ func (Module) Name() string { return "security" }
|
||||
func (Module) Requires() []string { return []string{kernelmod.FacConfig} }
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacSecurity} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
if ks.DataDir == "" {
|
||||
return fmt.Errorf("security module: ks.DataDir is empty - Group 0 must set the data dir first")
|
||||
}
|
||||
|
||||
@ -16,11 +16,12 @@ import (
|
||||
"github.com/JianFeeeee/HologramAgent/internal/config"
|
||||
|
||||
agentCore "github.com/JianFeeeee/HologramAgent/internal/agent/core"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/plugin"
|
||||
cli "github.com/JianFeeeee/HologramAgent/internal/plugins/cli"
|
||||
webui "github.com/JianFeeeee/HologramAgent/internal/plugins/webui"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/promptsec"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/sdk"
|
||||
)
|
||||
|
||||
@ -34,9 +35,16 @@ func (Module) Requires() []string {
|
||||
}
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacStageHost} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
stageHost := agentCore.NewStageHost()
|
||||
|
||||
// 插件声明式系统提示词段的注册表:本模块产出,挂到内核挂载点。
|
||||
//
|
||||
// ★ 不往下传参:读方(agent 模块、SDK)自己去 ks 取 ——
|
||||
// 与 StageHost / PluginRegistry 完全同型。往构造点传指针会让
|
||||
// "记得接这一根线"成为每种 agent 类型的义务(那正是之前的老毛病)。
|
||||
ks.PromptSections = promptsec.NewRegistry()
|
||||
|
||||
pluginReg := plugin.NewRegistry()
|
||||
pluginReg.SetIOManager(ks.IO)
|
||||
pluginReg.SetEventBus(ks.EventBus)
|
||||
|
||||
@ -8,8 +8,8 @@ package supervisor
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/supervisor"
|
||||
)
|
||||
|
||||
@ -20,7 +20,7 @@ func (Module) Name() string { return "supervisor" }
|
||||
func (Module) Requires() []string { return []string{kernelmod.FacConfig} }
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacSupervisor} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
sup := supervisor.New(ks.Cfg)
|
||||
if err := sup.Start(); err != nil {
|
||||
log.Fatalf("start supervisor: %v", err)
|
||||
|
||||
@ -24,8 +24,8 @@ import (
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory/text"
|
||||
)
|
||||
|
||||
@ -40,7 +40,7 @@ func (Module) Requires() []string {
|
||||
}
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacTextMem} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
textMem := text.New(filepath.Join(ks.Cfg.Daemon.DataDir, "memory", "text"))
|
||||
if err := textMem.Start(); err != nil {
|
||||
log.Printf("[homed] warning: text memory start: %v", err)
|
||||
|
||||
@ -8,8 +8,8 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/tracker"
|
||||
)
|
||||
|
||||
@ -20,7 +20,7 @@ func (Module) Name() string { return "tracker" }
|
||||
func (Module) Requires() []string { return []string{kernelmod.FacConfig} }
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacTracker} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
trk := tracker.NewTracker(ks.Cfg.Daemon.DataDir, ks.AgentWorkDir,
|
||||
tracker.WithKeepChangesets(100),
|
||||
tracker.WithMaxChangesetAge(30*24*time.Hour),
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
@ -41,7 +42,7 @@ func (Module) Requires() []string { return nil } // 第 0 层:不依赖任何
|
||||
func (Module) Provides() []string { return []string{FacilityVersion} }
|
||||
|
||||
// Init 执行版本自检,必要时回滚上一次升级。
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
func (Module) Init(ks *kstate.State) error {
|
||||
// ⓪ 把内核**真实**的 dataDir 注入 update 包。
|
||||
//
|
||||
// 必须最先做:pending 标记/备份/回滚验全都靠它。进程内调用方
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package kernelboot
|
||||
package kstate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -50,7 +50,7 @@ type Quiescer struct {
|
||||
}
|
||||
|
||||
// AddQuiescer 登记一个可暂停/恢复的写者(在模块 Init 里调用)。
|
||||
func (s *KernelState) AddQuiescer(name string, pause, resume func() error) {
|
||||
func (s *State) AddQuiescer(name string, pause, resume func() error) {
|
||||
if pause == nil {
|
||||
return
|
||||
}
|
||||
@ -64,7 +64,7 @@ func (s *KernelState) AddQuiescer(name string, pause, resume func() error) {
|
||||
// 返回成功暂停的清单 —— Resume 只唤醒这些(没有的不要"凭空唤醒")。
|
||||
// 任何一个 Pause 失败都会**停止继续暂停**并返回错误:宁可只关一半
|
||||
// 让调用方放弃迁移,也不要"看起来关了但仍有写者在跑"。
|
||||
func (s *KernelState) Quiesce() ([]string, error) {
|
||||
func (s *State) Quiesce() ([]string, error) {
|
||||
s.quiesceMu.Lock()
|
||||
qs := make([]Quiescer, len(s.Quiescers))
|
||||
copy(qs, s.Quiescers)
|
||||
@ -90,7 +90,7 @@ func (s *KernelState) Quiesce() ([]string, error) {
|
||||
}
|
||||
|
||||
// Resume 唤醒:按**暂停的逆序**(即原启动顺序)恢复。
|
||||
func (s *KernelState) Resume(paused []string) error {
|
||||
func (s *State) Resume(paused []string) error {
|
||||
s.quiesceMu.Lock()
|
||||
qs := make([]Quiescer, len(s.Quiescers))
|
||||
copy(qs, s.Quiescers)
|
||||
@ -110,7 +110,7 @@ func (s *KernelState) Resume(paused []string) error {
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (s *KernelState) resumeOne(qs []Quiescer, name string) error {
|
||||
func (s *State) resumeOne(qs []Quiescer, name string) error {
|
||||
for _, q := range qs {
|
||||
if q.Name == name {
|
||||
if q.Resume == nil {
|
||||
101
internal/kstate/mount.go
Normal file
101
internal/kstate/mount.go
Normal file
@ -0,0 +1,101 @@
|
||||
package kstate
|
||||
|
||||
// 内核统一挂载点:本进程唯一的公共数据持有者 + 它的存取入口。
|
||||
//
|
||||
// ## 用户设计(本包存在的理由)
|
||||
//
|
||||
// > 内核通过一个结构体收集全部包级共享变量,其他模块通过这个结构体访问数据,
|
||||
// > 各个模块的公共数据也统一注册到内核这个公共结构体中。
|
||||
//
|
||||
// > 结构体成员初始置 nil 加读取前检查不就好了
|
||||
//
|
||||
// > 所有共享数据统一挂载一个 kernel 提供的挂载点上暴露。
|
||||
//
|
||||
// ## 为什么本包必须与装配引擎分开(2026-11-04 拆分)
|
||||
//
|
||||
// 拆分前 `KernelState` 与装配引擎同居 `internal/kernelboot`。数据结构要认识
|
||||
// 各业务类型(`*security.Runtime`、`*plugin.Registry`、`*agentCore.StageHost`…),
|
||||
// 于是 `kernelboot` 被迫依赖 **19 个业务包**:
|
||||
//
|
||||
// kernelboot ──▶ agent/core
|
||||
// ──▶ plugin
|
||||
// ──▶ security
|
||||
// ──▶ memory / knowledge / text / coldcontext / …
|
||||
//
|
||||
// 而装配引擎本身(kernelboot.go / scheduler.go / funcmodule.go)
|
||||
// **一行业务代码都不需要** —— 依赖全在数据结构这一个文件里。
|
||||
//
|
||||
// 后果是所有被依赖的包都无法反向引用 kernelboot,于是它们各自造通道:
|
||||
//
|
||||
// core → Agent 字段(靠每种 agent 构造点记得传)
|
||||
// plugin → 12 个 SetXxx
|
||||
// sdk → SDKConfig 传参
|
||||
// security → 包级单例 Global() / RuntimeGlobal() / ConfigCodec()
|
||||
//
|
||||
// 四条通道都指向同一个根因。把数据结构搬出来(本包),环就断了:
|
||||
//
|
||||
// kstate ← 纯数据,谁都能 import(它就是挂载点本身)
|
||||
// kernelboot ← 装配引擎,零业务包
|
||||
//
|
||||
// ## 并发约定
|
||||
//
|
||||
// 状态结构体上的字段**不加锁**(用户设计):同层模块并行 Init 时各自只写
|
||||
// **自己那一个字段**,字段互不相同 ⇒ 无数据竞争。读方直接读字段 + 判 nil。
|
||||
//
|
||||
// 但**挂载点本身**要加锁:它是全部调用方共同读的单一变量,而装配期写入
|
||||
// 与运行期读取必然跨 goroutine(`Mount` 在启动时一次,`Get` 在热路径)。
|
||||
|
||||
import "sync"
|
||||
|
||||
var (
|
||||
mountMu sync.RWMutex
|
||||
mount *State
|
||||
)
|
||||
|
||||
// Mount 装入本进程的挂载点。**装配期调用一次**(由 kernelboot 的 Run 完成)。
|
||||
//
|
||||
// 重复调用会覆盖:便于测试与热重启路径(后装的生效)。
|
||||
func Mount(s *State) {
|
||||
mountMu.Lock()
|
||||
mount = s
|
||||
mountMu.Unlock()
|
||||
}
|
||||
|
||||
// Get 取挂载点。未装配时返回 **nil**(不 panic)。
|
||||
//
|
||||
// ★ 为什么返回 nil 而不是空结构体:调用点散布在内核热路径上,未装配
|
||||
// (单元测试、独立工具如 memgc)时必须**零成本跳过**。所有调用方写成:
|
||||
//
|
||||
// if ks := kstate.Get(); ks != nil && ks.Xxx != nil { … }
|
||||
//
|
||||
// 返回空结构体看似更"安全",实则让"未装配"与"装配了但字段为 nil"两种
|
||||
// 完全不同的情况混为一谈 —— 前者是环境问题,后者是装配缺陷。
|
||||
func Get() *State {
|
||||
mountMu.RLock()
|
||||
defer mountMu.RUnlock()
|
||||
return mount
|
||||
}
|
||||
|
||||
// MountForTest 装入一个测试用挂载点,返回**恢复函数**。
|
||||
//
|
||||
// 测试里必须这样写:
|
||||
//
|
||||
// restore := kstate.MountForTest(fake)
|
||||
// t.Cleanup(restore)
|
||||
//
|
||||
// 为什么不直接用 Mount:并行测试(`t.Parallel()`)会互相覆盖全局挂载点,
|
||||
// 表现为随机红——而随机红是判据可信度的头号杀手(本仓已有教训:
|
||||
// 判据在 -race 下才暴露竞态)。恢复函数让每个测试回到"进入前的状态"
|
||||
// 而不是"nil",嵌套/串行测试也正确。
|
||||
func MountForTest(s *State) (restore func()) {
|
||||
mountMu.Lock()
|
||||
prev := mount
|
||||
mount = s
|
||||
mountMu.Unlock()
|
||||
|
||||
return func() {
|
||||
mountMu.Lock()
|
||||
mount = prev
|
||||
mountMu.Unlock()
|
||||
}
|
||||
}
|
||||
217
internal/kstate/mount_test.go
Normal file
217
internal/kstate/mount_test.go
Normal file
@ -0,0 +1,217 @@
|
||||
package kstate_test
|
||||
|
||||
// 判据:`kernelboot`(装配引擎)不得依赖业务包。
|
||||
//
|
||||
// ## 为什么这条是本计划的总判据
|
||||
//
|
||||
// 拆分前 `KernelState` 与装配引擎同居 `internal/kernelboot`。数据结构要认识
|
||||
// 各业务类型,于是 kernelboot 被迫依赖 19 个业务包:
|
||||
//
|
||||
// kernelboot ──▶ agent/core ──▶ …
|
||||
// ──▶ plugin
|
||||
// ──▶ security
|
||||
//
|
||||
// 后果是所有底层包都无法反向引用 kernelboot,只能各自造通道:
|
||||
//
|
||||
// core → Agent 字段(每种 agent 构造点都要记得传)
|
||||
// plugin → 12 个 SetXxx
|
||||
// sdk → SDKConfig 传参
|
||||
// security → 包级单例 Global() / RuntimeGlobal() / ConfigCodec()
|
||||
//
|
||||
// 这四条通道是同一个根因的四个表现。**只要 kernelboot 还依赖业务包,
|
||||
// 任何"把共享数据收敛到统一挂载点"的努力都会退回去**——因为底层包读不到它。
|
||||
//
|
||||
// ## 判据的形态
|
||||
//
|
||||
// 1. **结构判据**:`go list -deps ./internal/kernelboot` 不得含业务包。
|
||||
// 2. **`kstate` 叶子性**:`kstate` 不得 import `kernelboot` / `kernelmod` / `cmd`。
|
||||
// 3. **挂载点行为**:`Mount` / `Get` / `MountForTest` 的语义。
|
||||
//
|
||||
// ★ 第 1 条在 T1(别名过渡期)会**红**,这是**故意的**:
|
||||
// 别名文件要 import kstate,而 kstate 认识业务类型 ⇒ 依赖仍在。
|
||||
// T2 删掉别名、把 54 处引用改为 kstate.State 后才会转绿。
|
||||
//
|
||||
// 也就是说这条判据现在就在保护 T2:如果 T2 做了一半(漏改几处又把别名
|
||||
// 加回去),它会立刻红。
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
)
|
||||
|
||||
// TestCoreCanImportKstate 是**本计划的总判据**(2026-11-04)。
|
||||
//
|
||||
// ## 它守的到底是什么
|
||||
//
|
||||
// 不是"kernelboot 零业务依赖"——那不可能:`kstate.State` 必须认识
|
||||
// `*plugin.Registry`、`*memory.GraphDB` 等类型,否则字段就只能是 any
|
||||
// (而 any 会把"拼错/取错类型"从编译错误降级成运行期断言,违背本结构体
|
||||
// 的设计原则:类型化字段让错误编译期暴露)。
|
||||
//
|
||||
// 真正的不变量是:**`internal/agent/core` 能 import `kstate`**。
|
||||
//
|
||||
// 因为 `core` 是环的一极:只要 `kstate` 反过来依赖 `core`,
|
||||
// `core` 就永远读不到统一挂载点,只能自建通道:
|
||||
//
|
||||
// core → Agent 字段(每种 agent 构造点都要记得传)
|
||||
// core → security.Global() 包级单例
|
||||
//
|
||||
// 实测这两条都真实发生过(skill 索引没进子 agent 提示词、16 处 Global())。
|
||||
//
|
||||
// ## 判据怎么做
|
||||
//
|
||||
// 不能只检查 import 列表(`core` 目前确实没 import `kstate`)——
|
||||
// 那样即使环存在也不会红。必须**真的让 core 引 kstate 并编译**。
|
||||
func TestCoreCanImportKstate(t *testing.T) {
|
||||
probe := filepath.Join("..", "agent", "core", "zz_kstate_probe_test.go")
|
||||
src := `package core
|
||||
|
||||
// 临时探针:证明 core 能 import kstate(无 import 环)。
|
||||
import _ "github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||||
`
|
||||
if err := os.WriteFile(probe, []byte(src), 0o644); err != nil {
|
||||
t.Fatalf("写探针失败: %v", err)
|
||||
}
|
||||
defer func() { _ = os.Remove(probe) }()
|
||||
|
||||
out, err := exec.Command("go", "build", "github.com/JianFeeeee/HologramAgent/internal/agent/core").CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("★ core 无法 import kstate —— 环又出现了:\n%s\n\n"+
|
||||
"原因通常是 kstate.State 里加了 *agentCore.Xxx 类型的字段。\n"+
|
||||
"正确做法:其他包把内容挂到 kstate(存最小接口或 any + 断言),\n"+
|
||||
"而不是让 kstate 反向认识 core。", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKstateDoesNotDependOnCore 是上面那条的**静态版**(无需起编译)。
|
||||
//
|
||||
// 两条都要有:静态版能立刻指出"谁把 core 拖进来的",
|
||||
// 编译版能抓到静态检查漏掉的间接环。
|
||||
func TestKstateDoesNotDependOnCore(t *testing.T) {
|
||||
deps := goListDeps(t, "github.com/JianFeeeee/HologramAgent/internal/kstate")
|
||||
for _, dep := range deps {
|
||||
if dep == "github.com/JianFeeeee/HologramAgent/internal/agent/core" {
|
||||
t.Fatalf("★ kstate 依赖了 internal/agent/core ⇒ core 再也无法 import kstate\n" +
|
||||
"(具体是哪个字段见 internal/kstate/state.go 里 `agentCore.` 的引用)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestKstateIsLeaf 钉住 kstate 不得依赖**上层**(装配器 / 模块 / 入口)。
|
||||
//
|
||||
// 允许它依赖业务**类型**包(plugin / memory / security …)——那是 State
|
||||
// 的类型化字段需要的。不允许的是反向依赖"使用它的上层"。
|
||||
func TestKstateIsLeaf(t *testing.T) {
|
||||
deps := goListDeps(t, "github.com/JianFeeeee/HologramAgent/internal/kstate")
|
||||
|
||||
forbidden := map[string]bool{
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot": true,
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod": true,
|
||||
"github.com/JianFeeeee/HologramAgent/cmd/homed": true,
|
||||
}
|
||||
for _, dep := range deps {
|
||||
if forbidden[dep] {
|
||||
t.Fatalf("★ kstate 依赖了 %s —— 挂载点必须是叶子", dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMountGetRoundTrip 钉住挂载点基本语义。
|
||||
func TestMountGetRoundTrip(t *testing.T) {
|
||||
prev := kstate.Get()
|
||||
restore := kstate.MountForTest(&kstate.State{DataDir: "/tmp/rt"})
|
||||
defer restore()
|
||||
|
||||
got := kstate.Get()
|
||||
if got == nil {
|
||||
t.Fatal("Mount 之后 Get 返回 nil")
|
||||
}
|
||||
if got.DataDir != "/tmp/rt" {
|
||||
t.Fatalf("DataDir = %q,期望 /tmp/rt", got.DataDir)
|
||||
}
|
||||
if prev == got {
|
||||
t.Fatal("MountForTest 没有替换挂载点")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetNilWhenUnmounted 钉住"未装配返回 nil、不 panic"。
|
||||
//
|
||||
// 这是热路径的前提:单测与独立工具(memgc 等)不装挂载点,
|
||||
// 调用方靠 `if ks := kstate.Get(); ks != nil` 零成本跳过。
|
||||
func TestGetNilWhenUnmounted(t *testing.T) {
|
||||
restore := kstate.MountForTest(nil)
|
||||
defer restore()
|
||||
|
||||
if got := kstate.Get(); got != nil {
|
||||
t.Fatalf("未装配时 Get 应返回 nil,实际 %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMountForTestRestores 钉住恢复语义(并行测试隔离的基础)。
|
||||
//
|
||||
// 变红条件:restore 把挂载点恢复成 nil 而不是"进入前的值" ⇒ 嵌套测试互相破坏。
|
||||
func TestMountForTestRestores(t *testing.T) {
|
||||
outer := &kstate.State{DataDir: "/outer"}
|
||||
restoreOuter := kstate.MountForTest(outer)
|
||||
defer restoreOuter()
|
||||
|
||||
inner := &kstate.State{DataDir: "/inner"}
|
||||
restoreInner := kstate.MountForTest(inner)
|
||||
if kstate.Get().DataDir != "/inner" {
|
||||
t.Fatal("内层 MountForTest 未生效")
|
||||
}
|
||||
restoreInner()
|
||||
|
||||
if got := kstate.Get(); got == nil || got.DataDir != "/outer" {
|
||||
t.Fatalf("restore 后应回到外层挂载点 /outer,实际 %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMountIsConcurrencySafe 钉住挂载点的跨 goroutine 安全(-race 下有效)。
|
||||
func TestMountIsConcurrencySafe(t *testing.T) {
|
||||
restore := kstate.MountForTest(&kstate.State{DataDir: "/base"})
|
||||
defer restore()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 200; j++ {
|
||||
_ = kstate.Get()
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 50; j++ {
|
||||
kstate.Mount(&kstate.State{DataDir: "/hot"})
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// goListDeps 返回某包的完整依赖列表。
|
||||
//
|
||||
// ★ 必须传**完整导入路径**而不是 `./internal/xxx`:`go test` 的进程 cwd
|
||||
// 是**包目录**(internal/kstate),相对路径会解析成
|
||||
// internal/kstate/internal/kernelboot 这种不存在的路径。
|
||||
func goListDeps(t *testing.T, pkg string) []string {
|
||||
t.Helper()
|
||||
out, err := exec.Command("go", "list", "-deps", pkg).Output()
|
||||
if err != nil {
|
||||
var stderr string
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
stderr = string(ee.Stderr)
|
||||
}
|
||||
t.Fatalf("go list -deps %s 失败: %v\n%s", pkg, err, stderr)
|
||||
}
|
||||
return strings.Fields(string(out))
|
||||
}
|
||||
@ -1,13 +1,14 @@
|
||||
package kernelboot
|
||||
package kstate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/plugin"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/security"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/agent/api"
|
||||
agentAPI "github.com/JianFeeeee/HologramAgent/internal/agent/api"
|
||||
agentCore "github.com/JianFeeeee/HologramAgent/internal/agent/core"
|
||||
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/config"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||||
@ -19,8 +20,7 @@ import (
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory/social"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory/text"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory/vector"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/plugin"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/security"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/promptsec"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/supervisor"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/tracker"
|
||||
"github.com/JianFeeeee/HologramAgent/pkg/types"
|
||||
@ -33,7 +33,7 @@ import (
|
||||
// 它们的持有者都假设彼此一致)。拆开会让"只填了 db 忘了 indexer"变成可能。
|
||||
//
|
||||
// ★ 为何放在 kernelboot 而不是 cmd/homed:
|
||||
// KernelState 要引用它的类型 ⇒ 类型必须与 KernelState 同包
|
||||
// State 要引用它的类型 ⇒ 类型必须与 State 同包
|
||||
// (否则 kernelboot 得 import cmd/homed,那是不可能的)。
|
||||
type MemoryBundle struct {
|
||||
DB *memory.GraphDB
|
||||
@ -42,7 +42,7 @@ type MemoryBundle struct {
|
||||
Distiller *pipeline.Distiller
|
||||
}
|
||||
|
||||
// KernelState 是内核**唯一的公共数据持有者**(用户设计)。
|
||||
// State 是内核**唯一的公共数据持有者**(用户设计)。
|
||||
//
|
||||
// ## 设计意图(用户原话)
|
||||
//
|
||||
@ -94,7 +94,7 @@ type MemoryBundle struct {
|
||||
//
|
||||
// 内核**不**在字段上加锁:加锁会给调用方"可以乱写"的错觉,
|
||||
// 正确约束是"各模块只写自己的字段"(由代码审查 + 判据守)。
|
||||
type KernelState struct {
|
||||
type State struct {
|
||||
// ── Group 0 产出(core 自己立起来的东西,所有模块只读) ──
|
||||
|
||||
// Cfg 是配置快照(config 模块产出)。
|
||||
@ -213,11 +213,72 @@ type KernelState struct {
|
||||
UpdateCapabilities any
|
||||
|
||||
// StageHost 是阶段管道(stage-host 模块产出)。
|
||||
StageHost *agentCore.StageHost
|
||||
//
|
||||
// ## 为什么是 any 而不是 *agentCore.StageHost(2026-11-04)
|
||||
//
|
||||
// 本包是**统一挂载点**,必须能被任何人 import。而 `*agentCore.StageHost`
|
||||
// 让本包依赖 `internal/agent/core` ⇒ 反过来 `core` 就不能 import 本包:
|
||||
//
|
||||
// kstate ──▶ agent/core ──▶ … (core 想读共享数据就只能自建通道)
|
||||
//
|
||||
// 实测确认过这是真的环(`go build` 报 import cycle not allowed)。
|
||||
// 而"底层包自己把产物挂上来"才是本包的设计意图(用户裁定):
|
||||
//
|
||||
// > 是其他包在kstate挂共享内容啊
|
||||
//
|
||||
// 所以:存 any,**取用处断言**回 `*agentCore.StageHost`。
|
||||
// 与本包已有的 PluginCatalog / PluginScheduler / UpdateCapabilities 同型,
|
||||
// 代价是一次断言,收益是本包零业务依赖。
|
||||
StageHost any
|
||||
|
||||
// PluginRegistry 是插件注册表(stage-host 模块产出)。
|
||||
//
|
||||
// ★ 为什么可以是具体类型(2026-11-04 实测,不是推测):
|
||||
//
|
||||
// 判据是"它会不会反过来依赖 internal/agent/core"。实测:
|
||||
//
|
||||
// go list -deps internal/plugin | grep agent/core → 空
|
||||
//
|
||||
// 所以 plugin 不依赖 core ⇒ 本包依赖 plugin **不构成环**,
|
||||
// core 仍能 import 本包。类型化字段让"拼错/取错类型"是编译错误
|
||||
// (本结构体的设计原则,见包注释)。
|
||||
//
|
||||
// 对照:StageHost / Agent 曾被改成 any 是因为它们**就是** core 的类型
|
||||
// (core 是链的一极,那才是真环)。
|
||||
PluginRegistry *plugin.Registry
|
||||
|
||||
// PromptSections 是**插件声明式系统提示词段**的注册表(stage-host 模块产出)。
|
||||
//
|
||||
// ## 为什么挂在这里(用户指出的设计)
|
||||
//
|
||||
// > 所有共享数据统一挂载一个 kernel 提供的挂载点上暴露
|
||||
//
|
||||
// 与 StageHost / PluginRegistry / EventBus 同层同型:模块产出、挂上 State、
|
||||
// 任何读方直接 ks.PromptSections 取,**没有 Set* 传参、没有构造点传递**。
|
||||
//
|
||||
// ## 它解决什么(真实生产故障)
|
||||
//
|
||||
// 此前技能索引是 Agent 上的一个字段(Agent.skillIndex),由
|
||||
// cmd/homed/bootstrap.go 用 type-assert 手工接给**根 agent**:
|
||||
//
|
||||
// if prov, ok := sp.(agentCore.SkillIndexProvider); ok {
|
||||
// agent.SetSkillIndexProvider(prov)
|
||||
// }
|
||||
//
|
||||
// 于是 rtchild、驻留子、工作式子各自构造时都得记得再传一遍 ——
|
||||
// 漏传不报错、不崩,只是那个 agent 的提示词里一条 skill 都没有。
|
||||
//
|
||||
// 生产症状(日志实录):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 每轮重建提示词时读同一份。
|
||||
// 加一种 agent 类型是**零接线**。
|
||||
PromptSections *promptsec.Registry
|
||||
|
||||
// PluginCatalog 是**插件元数据缓存**(plugin-host 模块产出)。
|
||||
//
|
||||
// ## 为什么需要它(用户指出的设计)
|
||||
@ -242,13 +303,13 @@ type KernelState struct {
|
||||
//
|
||||
// ## ★ 为什么它必须在这里(而不只是 Run 的局部变量)
|
||||
//
|
||||
// 模块的 `Init(ks)` 只能看到 KernelState —— 若 bus 只在 Run 里,
|
||||
// 模块的 `Init(ks)` 只能看到 State —— 若 bus 只在 Run 里,
|
||||
// **模块拿不到它**,于是无法:
|
||||
// · 订阅状态变化(plugin-host 要据此拉起"等更晚设施"的插件)
|
||||
// · 标记就绪(虽然 Run 会自动做,但模块可能需要主动补标)
|
||||
//
|
||||
// 实测撞到:plugin-host 想在 Init 里挂"等 agent 就绪再拉起某些插件"的
|
||||
// 回调,却找不到 bus。把它放进 KernelState(内核公共数据,用户设计)
|
||||
// 回调,却找不到 bus。把它放进 State(内核公共数据,用户设计)
|
||||
// 是最自然的落点 —— 它本来就是**内核级共享状态**。
|
||||
SyncBus *SyncBus
|
||||
|
||||
@ -268,7 +329,16 @@ type KernelState struct {
|
||||
PluginsReady bool
|
||||
|
||||
// Agent 是主 agent(agent 模块产出)。
|
||||
Agent *agentCore.Agent
|
||||
//
|
||||
// 存 any 的理由与 StageHost 完全相同(见上):本包不能依赖 agent/core。
|
||||
// 取用处断言回 `*agentCore.Agent`。
|
||||
//
|
||||
// 实际使用面极小,只有两处:
|
||||
// · device 模块:`ks.Agent.MountDevice(rt)` —— 挂上数据面
|
||||
// · agent 模块:`k.PluginRegistry.SetStatusProvider(k.Agent)` —— 状态面
|
||||
// 若日后使用面扩大,应抽一个最小接口到叶子包(如 internal/agent/api)
|
||||
// 而不是把 core 拉回本包。
|
||||
Agent any
|
||||
|
||||
// Security 是**安全防火墙运行时**(security 模块产出)。
|
||||
//
|
||||
@ -279,6 +349,10 @@ type KernelState struct {
|
||||
// 而类型化字段能让"取错类型/拼错字段"变成编译错误 —— 这正是本结构体
|
||||
// 存在的理由(见包注释)。未装配时为 nil;配置显式关闭时是
|
||||
// Disabled Runtime(所有审查零成本透传),不是 nil。
|
||||
// Security 是**安全防火墙运行时**(security 模块产出)。
|
||||
//
|
||||
// ★ 具体类型而非接口/any(实测 plugin 也不依赖 core,同样的判据):
|
||||
// 类型化字段让取错类型成为编译错误。
|
||||
Security *security.Runtime
|
||||
|
||||
// Device 是**工具/通道数据面**(device 模块产出)。
|
||||
@ -315,7 +389,7 @@ type KernelState struct {
|
||||
// (两个 goroutine 同时读到旧 len、各自写同一槽)⇒ 某个模块的
|
||||
// 关闭函数永远不被调用 ⇒ **资源泄漏**(且无任何报错)。
|
||||
//
|
||||
// 为什么不用 KernelState 上"模块只写自己字段"的约定来避免:
|
||||
// 为什么不用 State 上"模块只写自己字段"的约定来避免:
|
||||
// Closers 不属于任一模块 —— 它是**共享的回收站**。
|
||||
// 共享可变状态就必须有锁(这与"各写各的字段"是两条不同的保证)。
|
||||
Closers []func()
|
||||
@ -338,16 +412,16 @@ type KernelState struct {
|
||||
quiesceMu sync.Mutex
|
||||
}
|
||||
|
||||
// NewKernelState 创建公共结构体(所有字段零值 = nil,由各模块填充)。
|
||||
func NewKernelState() *KernelState {
|
||||
return &KernelState{}
|
||||
// NewState 创建公共结构体(所有字段零值 = nil,由各模块填充)。
|
||||
func New() *State {
|
||||
return &State{}
|
||||
}
|
||||
|
||||
// AddCloser 注册一个关闭函数(模块在 Init 里调用)。
|
||||
//
|
||||
// 关闭时按**注册顺序的逆序**执行(LIFO,与 `defer` 语义一致):
|
||||
// 先初始化的一般是底层(config → memory → llm),停机应反向拆。
|
||||
func (s *KernelState) AddCloser(fn func()) {
|
||||
func (s *State) AddCloser(fn func()) {
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
@ -362,10 +436,10 @@ func (s *KernelState) AddCloser(fn func()) {
|
||||
// ★ 失败策略:**继续执行剩余关闭**(返回首个错误)。
|
||||
// 一个模块关不掉不该阻止其他模块关 —— 那会造成更多泄漏。
|
||||
// 关闭函数 panic 也被隔离(否则一个模块的问题会连带其他模块都关不掉)。
|
||||
func (s *KernelState) CloseAll() error {
|
||||
func (s *State) CloseAll() error {
|
||||
// ★ 先取快照再执行(不持锁执行回调):关闭函数可能很慢
|
||||
// (停进程/flush 数据库),持锁会让并发调用 AddCloser 的模块卡住;
|
||||
// 且回调可能反查 KernelState ⇒ 死锁。
|
||||
// 且回调可能反查 State ⇒ 死锁。
|
||||
s.closerMu.Lock()
|
||||
fns := make([]func(), len(s.Closers))
|
||||
copy(fns, s.Closers)
|
||||
@ -1,4 +1,4 @@
|
||||
package kernelboot
|
||||
package kstate
|
||||
|
||||
// syncbus:内核就绪状态的同步快照,驱动插件调度(设计 §2.6)。
|
||||
//
|
||||
@ -224,6 +224,7 @@ func (r *Registry) SetIOManager(iom *agentIO.IOManager) {
|
||||
defer r.mu.Unlock()
|
||||
r.iom = iom
|
||||
}
|
||||
|
||||
func (r *Registry) SetEventBus(evBus *events.Bus) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
@ -104,6 +104,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
log.Printf("[skillmgr] detected skill %s: %v", path, err)
|
||||
return
|
||||
}
|
||||
p.publishIndex()
|
||||
log.Printf("[skillmgr] registered via %s event: %s", detectEvent, filepath.Base(path))
|
||||
})
|
||||
|
||||
@ -112,11 +113,18 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
// 后续事件重复到达时 loadOne 幂等跳过)
|
||||
n := p.scanExisting()
|
||||
p.registerTools()
|
||||
// 首次声明:把当前索引推给内核的统一注册表。
|
||||
// 不调它的后果:模型看不到任何 skill 索引,而 skill_list 仍能列出
|
||||
// —— "工具能列但模型不知道",是最难查的一类降级。
|
||||
p.publishIndex()
|
||||
log.Printf("[skillmgr] started, dir=%s, loaded %d native skills", p.skillsDir, n)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Stop() error {
|
||||
// 先撤声明再退订:顺序反过来的话,退订期间仍有事件在加载 skill,
|
||||
// 那部分会来不及进索引(虽然进程即将退出,但热重载路径下不退出)。
|
||||
p.unpublishIndex()
|
||||
if p.unsub != nil {
|
||||
p.unsub()
|
||||
p.unsub = nil
|
||||
@ -250,9 +258,26 @@ func (p *Plugin) get(name string) (*SkillEntry, bool) {
|
||||
return e, ok
|
||||
}
|
||||
|
||||
// SkillIndex 实现 agentCore.SkillIndexProvider:返回已启用技能的精炼索引。
|
||||
// 每次 LLM 调用都会执行,必须轻量(只读锁 + 字符串拼接,无 IO)。
|
||||
func (p *Plugin) SkillIndex() string {
|
||||
// skillSectionName 是本插件声明的系统提示词段名。
|
||||
//
|
||||
// 命名规则由 SDK 强制:`<owner>.<名字>`,owner 必须等于插件名。
|
||||
// 因此别的插件无法覆盖本段(attempting 会被内核拒绝)。
|
||||
const skillSectionName = "skillmgr.skills"
|
||||
|
||||
// skillSectionOrder 让技能索引排在**工具与技能使用说明**区段(100–199)。
|
||||
//
|
||||
// 为什么不是更靠前:索引里列的是工具名,模型要先知道"这轮有哪些工具"
|
||||
// 再读"怎么用"才有意义。顺序约定见 sdk/systemprompt.go 的 PromptSection.Order。
|
||||
const skillSectionOrder = 100
|
||||
|
||||
// buildIndex 生成已启用技能的精炼索引(名称 + 版本 + 一行描述)。
|
||||
//
|
||||
// 这是**声明内容**,由 publishIndex 推给内核;不再是让内核来 pull 的接口。
|
||||
func (p *Plugin) buildIndex() string {
|
||||
const header = "[Skills] Usage manuals for plugins. Before calling a plugin's tool " +
|
||||
"(or when it fails), read the same-named skill with skill_info(\"skill-name\") " +
|
||||
"and follow it. Never guess from the name."
|
||||
|
||||
entries := p.snapshot()
|
||||
var b strings.Builder
|
||||
for _, e := range entries {
|
||||
@ -266,7 +291,70 @@ func (p *Plugin) SkillIndex() string {
|
||||
desc = p.truncateDesc(desc)
|
||||
fmt.Fprintf(&b, "%s v%s - %s\n", e.sk.Name(), e.sk.Version(), desc)
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
list := strings.TrimRight(b.String(), "\n")
|
||||
if list == "" {
|
||||
// 没有技能时**不声明**(而不是声明一段只有标题的空段)。
|
||||
return ""
|
||||
}
|
||||
return header + "\n\n" + list
|
||||
}
|
||||
|
||||
// publishIndex 把当前技能索引声明到内核的**统一系统提示词注册表**。
|
||||
//
|
||||
// ## 为什么是"声明"而不是"让内核来问"
|
||||
//
|
||||
// 此前是内核定义 `SkillIndexProvider` 接口、skillmgr 实现它、bootstrap
|
||||
// 手工把它接给根 agent。那条路有三个问题:
|
||||
//
|
||||
// 1. **双向耦合**:内核硬编码插件的产出,插件又 import 内核接口
|
||||
// 2. **子 agent 漏接**:rtchild / 驻留子 / 工作式子各自构造,都要记得传;
|
||||
// 漏传不报错也不崩,只是那个 agent 的提示词里一条 skill 都没有
|
||||
// 3. **顺序不可控**:插件内容只能整块插在内核硬拼流程的某个固定位置
|
||||
//
|
||||
// 现在是声明式:插件声明自己的段,内核每轮重建时按 Order 渲染。
|
||||
// 任何 agent(含未来新类型)读同一份注册表 —— **零接线**。
|
||||
//
|
||||
// ## 为什么每个变更点都要调
|
||||
//
|
||||
// 段内容是**静态字符串快照**(不是回调),所以技能集合变了必须重新声明。
|
||||
// 漏调的症状是"新装的 skill 在提示词里看不到",而 skill_list 工具却能列出
|
||||
// —— 模型于是不知道有这个 skill 可用。这是本仓踩过的同类问题
|
||||
// (built-in skill 不更新:机制在,但没推给读到的地方)。
|
||||
//
|
||||
// 内核侧同段重复声明是**覆盖**(幂等),所以无条件调用是安全的。
|
||||
func (p *Plugin) publishIndex() {
|
||||
if p.sdk == nil {
|
||||
return
|
||||
}
|
||||
api := p.sdk.SystemPrompt()
|
||||
if api == nil {
|
||||
return
|
||||
}
|
||||
content := p.buildIndex()
|
||||
if err := api.DeclarePromptSection(sdk.PromptSection{
|
||||
Section: skillSectionName,
|
||||
Owner: p.name,
|
||||
Order: skillSectionOrder,
|
||||
Content: content,
|
||||
Volatile: true, // 内容随 skill 增删/启停变化(诊断用)
|
||||
}); err != nil {
|
||||
// 不静默:声明失败意味着模型看不到任何 skill 索引,
|
||||
// 而 skill_list 仍能列出 —— 这种"工具能列但模型不知道"最难查。
|
||||
log.Printf("[skillmgr] WARN: 系统提示词段声明失败(模型将看不到技能索引): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// unpublishIndex 撤销本插件的声明(Stop / 停用时)。
|
||||
//
|
||||
// 不撤销的后果:插件已停用,但提示词里还留着"用 skill_info 读某 skill",
|
||||
// 模型于是去调一个不存在的工具。那比"没有说明"更糟。
|
||||
func (p *Plugin) unpublishIndex() {
|
||||
if p.sdk == nil {
|
||||
return
|
||||
}
|
||||
if api := p.sdk.SystemPrompt(); api != nil {
|
||||
_ = api.RemovePromptSection(skillSectionName)
|
||||
}
|
||||
}
|
||||
|
||||
// truncateDesc truncates a skill description to 80 runes and appends an ellipsis.
|
||||
|
||||
@ -357,7 +357,7 @@ func TestSkillIndex(t *testing.T) {
|
||||
|
||||
// The plugin now materializes the four kernel skills before scanning. They are
|
||||
// part of the built-in skill index, so an empty user registry is not empty.
|
||||
idx := p.SkillIndex()
|
||||
idx := p.buildIndex()
|
||||
for _, want := range []string{"kernel-memory", "kernel-output", "kernel-plugins", "kernel-tool-order"} {
|
||||
if !strings.Contains(idx, want) {
|
||||
t.Fatalf("built-in skill missing from index: %q", idx)
|
||||
@ -370,14 +370,14 @@ func TestSkillIndex(t *testing.T) {
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
idx = p.SkillIndex()
|
||||
idx = p.buildIndex()
|
||||
if !strings.Contains(idx, "idx-test v0.2.0") || !strings.Contains(idx, "用于测试的演示技能。") {
|
||||
t.Fatalf("index missing name/desc: %q", idx)
|
||||
}
|
||||
|
||||
// 禁用后不出现在索引里
|
||||
spy.call(t, "skill_disable", map[string]interface{}{"name": "idx-test"})
|
||||
if idx := p.SkillIndex(); strings.Contains(idx, "idx-test") {
|
||||
if idx := p.buildIndex(); strings.Contains(idx, "idx-test") {
|
||||
t.Fatalf("disabled skill should be excluded from index: %q", idx)
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,6 +111,7 @@ func (p *Plugin) registerLoad() {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.publishIndex() // 索引变了:重新声明,否则新 skill 模型看不到
|
||||
return fmt.Sprintf("已加载技能 %s v%s", e.sk.Name(), e.sk.Version()), nil
|
||||
})
|
||||
}
|
||||
@ -139,6 +140,7 @@ func (p *Plugin) registerUnload() {
|
||||
if !p.removeOne(name) {
|
||||
return nil, fmt.Errorf("skill %q not loaded", name)
|
||||
}
|
||||
p.publishIndex() // 索引变了
|
||||
msg := fmt.Sprintf("已卸载技能 %s(内存)", name)
|
||||
if delFiles {
|
||||
if err := os.RemoveAll(path); err != nil {
|
||||
@ -176,14 +178,20 @@ func (p *Plugin) registerSetEnabled(v bool) {
|
||||
},
|
||||
}, func(args map[string]interface{}) (interface{}, error) {
|
||||
skillName, _ := args["name"].(string)
|
||||
// ★ 不用 defer:下面要在**持锁之外**调 publishIndex,
|
||||
// 而 publishIndex → buildIndex → snapshot 会再取 RLock。
|
||||
// sync.RWMutex 不可重入 ⇒ defer 持写锁到函数结束会直接死锁。
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
e, ok := p.skills[skillName]
|
||||
if !ok {
|
||||
p.mu.Unlock()
|
||||
return nil, fmt.Errorf("skill %q not loaded", skillName)
|
||||
}
|
||||
e.enabled = v
|
||||
e.sk.SetEnabled(v)
|
||||
p.mu.Unlock()
|
||||
|
||||
p.publishIndex() // 启/停改变了索引内容
|
||||
return fmt.Sprintf("已%s技能 %s", word, skillName), nil
|
||||
})
|
||||
}
|
||||
@ -232,6 +240,7 @@ func (p *Plugin) registerCreate() {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.publishIndex() // 新建/覆盖技能:索引变了
|
||||
return fmt.Sprintf("技能 %s 已写入并加载生效(v%s)。用 skill_info 可查看全文。", e.sk.Name(), e.sk.Version()), nil
|
||||
}
|
||||
|
||||
@ -327,6 +336,7 @@ func (p *Plugin) registerInstall() {
|
||||
os.RemoveAll(dst)
|
||||
return nil, fmt.Errorf("installed but load failed: %w", err)
|
||||
}
|
||||
p.publishIndex() // 安装:索引变了
|
||||
return fmt.Sprintf("已从本地目录安装技能 %s v%s", e.sk.Name(), e.sk.Version()), nil
|
||||
|
||||
case strings.HasSuffix(strings.ToLower(source), PackExt):
|
||||
@ -351,6 +361,7 @@ func (p *Plugin) registerInstall() {
|
||||
os.RemoveAll(dst)
|
||||
return nil, fmt.Errorf("unpacked %d files but load failed: %w", n, err)
|
||||
}
|
||||
p.publishIndex() // 安装:索引变了
|
||||
return fmt.Sprintf("已安装 %d 个文件,技能 %s v%s 生效", n, e.sk.Name(), e.sk.Version()), nil
|
||||
|
||||
default:
|
||||
|
||||
238
internal/promptsec/registry.go
Normal file
238
internal/promptsec/registry.go
Normal file
@ -0,0 +1,238 @@
|
||||
package promptsec
|
||||
|
||||
// 插件**声明式系统提示词段**:插件只声明内容,内核每轮重建时渲染。
|
||||
//
|
||||
// ## 用户模型(2026-11-04)
|
||||
//
|
||||
// > 每个插件都可以声明 systemprompt,可运行时更新,内核每轮重建。
|
||||
//
|
||||
// 于是"某个插件想告诉模型怎么用自己的工具"变成**插件自己声明的一段文本**:
|
||||
//
|
||||
// skillmgr 声明 "skillmgr.skills" → 技能索引 + "调工具前先读同名 skill"
|
||||
// qq 声明 "qq.identity" → 身份绑定规则
|
||||
// 其他插件 声明 "<自己>.<段名>" → 各自的说明
|
||||
//
|
||||
// 而内核只做两件事:**存**(按 section 覆盖)与**渲染**(每轮按 Order 拼进
|
||||
// system prompt)。内核不认识 skill、不认识 qq,也不判内容。
|
||||
//
|
||||
// ## 为什么必须由内核持有(不能是 Agent 字段)
|
||||
//
|
||||
// 本仓已有一处栽在这上面:技能索引曾经是 `Agent.skillIndex` 字段,
|
||||
// 由 `cmd/homed/bootstrap.go` 手工接线到**根 agent**。于是 rtchild、驻留子
|
||||
// 各自 `New(AgentConfig{...})` 时都得记得再传一遍 —— 漏传不报错、不崩,
|
||||
// 只是"那个 agent 看不到任何 skill",然后模型凭工具名瞎猜。
|
||||
//
|
||||
// 生产症状:A2A 分诊助手(驻留子)手上没有 cmd_run(cmd 插件按需拉起),
|
||||
// 也看不到 cmd-tools skill 里"先 plugin_start cmd"的说明,于是把
|
||||
// terminal_create 当成一次性命令执行器,一条命令开一个终端。
|
||||
//
|
||||
// 教训:**能力归属内核,插件只声明**。放在 Registry 里,任何 agent
|
||||
// (含未来新类型)零改动自动可见;放在 Agent 字段里,每个构造点都是一根
|
||||
// 要记得接的线 —— 那正是"洋葱"退化成"莲藕"的形态。
|
||||
//
|
||||
// ## 幂等语义(用户明确要求"可运行时更新")
|
||||
//
|
||||
// 同一 Section 重复声明 ⇒ **覆盖**,不是追加。
|
||||
//
|
||||
// 这条不是风格问题,是正确性问题:`stepPrepare` 在一次任务里可能被重入
|
||||
// (上下文裁剪后 `f.Step = StepPrepare`),而 system prompt 每轮重建。
|
||||
// 若声明是追加,索引会越堆越多,最终挤掉真正的对话历史。
|
||||
// 用 map 而不是 slice,让"多份"在结构上不可能发生。
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Section 是插件声明的一段系统提示词内容。
|
||||
type Section struct {
|
||||
// Section 是**全局唯一**的段名,约定 `<插件名>.<名字>`(如 "skillmgr.skills")。
|
||||
//
|
||||
// ★ 强制前缀约定(与 security 的 Profile.Name 同型):拼错能被立刻发现,
|
||||
// 且插件之间不可能互相覆盖。
|
||||
Section string
|
||||
|
||||
// Owner 是声明它的插件名。必须与 Section 的前缀一致。
|
||||
Owner string
|
||||
|
||||
// Order 决定渲染顺序(小的在前)。同 Order 按 Section 字典序,保证稳定。
|
||||
Order int
|
||||
|
||||
// Content 是正文。**空串表示移除该段**(运行时撤销声明)。
|
||||
Content string
|
||||
|
||||
// Volatile 表示内容会频繁变化(如技能索引随 skill 增删变化)。
|
||||
//
|
||||
// 仅用于诊断输出(状态面能看到哪些段每轮都在变),不参与渲染决策。
|
||||
// 存在的理由:技能索引是本类声明里唯一"随时可能变"的段,
|
||||
// 出问题时需要能一眼区分"这段是静态说明"还是"这段每轮重算"。
|
||||
Volatile bool
|
||||
}
|
||||
|
||||
// Registry 是**内核级**的声明表:一个进程一份,所有 agent 共享。
|
||||
//
|
||||
// ★ 并发:pluginmgr 在插件 Start 期间声明,agent 每轮渲染读取。
|
||||
// 读多写少,用 RWMutex;渲染时先拷贝快照再排序,不在持锁期做字符串拼接。
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
sections map[string]Section
|
||||
}
|
||||
|
||||
// NewRegistry 建一个空的声明表。
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{sections: make(map[string]Section)}
|
||||
}
|
||||
|
||||
// Declare 声明/更新一段。同 Section 覆盖(幂等)。
|
||||
//
|
||||
// 返回 error 的情形:
|
||||
//
|
||||
// · 段名不符合 `<owner>.<名字>` 形式
|
||||
// · Owner 与段名前缀不一致(试图声明别人的段)
|
||||
//
|
||||
// ★ 不做"静默接受":拼错段名却悄悄生效,是这个仓库反复吃过亏的
|
||||
// 一类故障(参见 security 档位未声明即报错的理由)。
|
||||
func (r *Registry) Declare(s Section) error {
|
||||
if r == nil {
|
||||
return fmt.Errorf("promptsec: registry 未初始化")
|
||||
}
|
||||
owner := strings.TrimSpace(s.Owner)
|
||||
section := strings.TrimSpace(s.Section)
|
||||
if owner == "" {
|
||||
return fmt.Errorf("promptsec: Owner 不能为空")
|
||||
}
|
||||
if section == "" {
|
||||
return fmt.Errorf("promptsec: Section 不能为空")
|
||||
}
|
||||
prefix, _, ok := strings.Cut(section, ".")
|
||||
if !ok || prefix == "" || !strings.HasPrefix(section, owner+".") {
|
||||
return fmt.Errorf("promptsec: 段名 %q 必须形如 %q.<名字>(Owner=%q)",
|
||||
section, owner, owner)
|
||||
}
|
||||
if len(section) <= len(owner)+1 {
|
||||
return fmt.Errorf("promptsec: 段名 %q 缺少名字部分", section)
|
||||
}
|
||||
|
||||
s.Owner = owner
|
||||
s.Section = section
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if strings.TrimSpace(s.Content) == "" {
|
||||
// 空正文 = 撤销。保持"map 里没有该键"的语义。
|
||||
delete(r.sections, section)
|
||||
return nil
|
||||
}
|
||||
r.sections[section] = s
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove 撤销一段声明(等价于 Declare 传空 Content)。
|
||||
//
|
||||
// 单独提供是因为"撤销"在插件 Stop / 禁用时是常见动作,写成空串容易漏。
|
||||
func (r *Registry) Remove(section string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
delete(r.sections, section)
|
||||
}
|
||||
|
||||
// RemoveOwner 撤销某插件的**全部**声明(插件停用/卸载时调用)。
|
||||
//
|
||||
// ★ 为什么必须有:插件停用后它的说明还在提示词里,模型会去调一个
|
||||
// 已不存在的工具 —— 这比"没有说明"更糟。单个 Remove 需要插件自己
|
||||
// 记住声明过哪些段,插件崩了就没机会记住。
|
||||
func (r *Registry) RemoveOwner(owner string) {
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for k, v := range r.sections {
|
||||
if v.Owner == owner {
|
||||
delete(r.sections, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot 返回全部声明段的**副本**,按 Order 升序(同 Order 按 Section 字典序)。
|
||||
//
|
||||
// 与 Render 的分工:
|
||||
//
|
||||
// Render = 自己拼成最终文本(注册表独占的场景)
|
||||
// Snapshot = 交出各段,由调用方与**别的来源**合并(内核的当轮段)
|
||||
//
|
||||
// ★ 为什么必须能交出各段而不能只给拼好的字符串:内核自己也有十几块
|
||||
// 系统提示词(人格卡、记忆说明、输出规则…),它们要能与插件段按 Order
|
||||
// 交错排序。只给拼好的字符串,插件段就只能整块插在某个固定位置。
|
||||
func (r *Registry) Snapshot() []Section {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
r.mu.RLock()
|
||||
list := make([]Section, 0, len(r.sections))
|
||||
for _, s := range r.sections {
|
||||
list = append(list, s)
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
if list[i].Order != list[j].Order {
|
||||
return list[i].Order < list[j].Order
|
||||
}
|
||||
return list[i].Section < list[j].Section
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
||||
// Render 渲染全部声明段,按 Order 升序(同 Order 按 Section 字典序)。
|
||||
//
|
||||
// 无声明时返回空串——调用点用 `if s != ""` 判断,不要自己判 nil。
|
||||
func (r *Registry) Render() string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
r.mu.RLock()
|
||||
list := make([]Section, 0, len(r.sections))
|
||||
for _, s := range r.sections {
|
||||
list = append(list, s)
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
if len(list) == 0 {
|
||||
return ""
|
||||
}
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
if list[i].Order != list[j].Order {
|
||||
return list[i].Order < list[j].Order
|
||||
}
|
||||
return list[i].Section < list[j].Section
|
||||
})
|
||||
|
||||
var b strings.Builder
|
||||
for i, s := range list {
|
||||
if i > 0 {
|
||||
_, _ = b.WriteString("\n\n")
|
||||
}
|
||||
_, _ = b.WriteString(s.Content)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Names 返回全部段名(诊断/状态面用)。
|
||||
func (r *Registry) Names() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
out := make([]string, 0, len(r.sections))
|
||||
for k := range r.sections {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
@ -39,6 +39,12 @@ type ToolCall = pubsdk.ToolCall
|
||||
type ToolResult = pubsdk.ToolResult
|
||||
type ToolError = pubsdk.ToolError
|
||||
type ToolDef = pubsdk.ToolDef
|
||||
|
||||
// PromptSection 是插件声明式系统提示词段(见 pubsdk.SystemPromptAPI)。
|
||||
//
|
||||
// 与 ToolDef / Stage 同型:用别名 re-export,避免每个内置插件都去 import
|
||||
// 对外的 SDK 包(它们已经 import 本包了)。
|
||||
type PromptSection = pubsdk.PromptSection
|
||||
type IOInjector = pubsdk.IOInjector
|
||||
type ToolRegistrar = pubsdk.ToolRegistrar
|
||||
type StageRegistrar = pubsdk.StageRegistrar
|
||||
|
||||
@ -12,7 +12,7 @@ import (
|
||||
//
|
||||
// ★ 定义在 internal/update 而不是 kernelmod:三端(CLI/WebAPI/agent 工具)
|
||||
// 都要用,而 KernelState 只有内核启动一条路径。模块通过
|
||||
// kernelboot.UpdateStateProvider 接口把它暴露给内核,避免 import 循环。
|
||||
// kstate.UpdateStateProvider 接口把它暴露给内核,避免 import 循环。
|
||||
type State struct {
|
||||
BinaryPath string
|
||||
DepGraphPath string
|
||||
|
||||
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