Files
HomeAgent/cmd/homed/main.go
JianFeeeee 5aaae93367 feat(healthcheck): 内核状态快照报出内核版本号与 ONNX 模型启用状态
healthcheck_kernel 此前没有任何「ONNX 模型是否在用」的信息,只报「向量可用/不可用」,
分不清「统一多模态空间已加载」与「退回到词嵌入/TF-IDF 路径」;人格卡要求
「版本以运行时快照为准」,也缺一个可查字段(build.version 早就在,但没人知道)。

- KernelStatus 新增 onnx 段:enabled / provider / dim / fingerprint / modalities / reason。
  判据取 Loaded()(provider 真正打开且元数据合法),**不是**「配置里写了 provider」
  —— 后者在模型缺失 / 运行时缺失时也为真,报出去就是假绿。
- 未启用时 reason 给**具体原因**:未配置(说明会走回退路径)/ 打开失败的具体错误。
  homed 把「配置的 provider 名」与「打开失败原因」透传给 Agent,仅供状态报告。
- ProviderAdapter 新增 Modalities()(可选能力,按接口断言取用,不改公开契约)。
- healthcheck_kernel 的工具描述同步说明它回答这两件事。

**顺带修一个真实 panic**:collectKernelStatus 的 knowledge 是**接口**参数,
(*knowledge.Store)(nil) 塞进接口后 `ks != nil` 仍为真 → 调 List() 直接 panic,
而 healthcheck_kernel 正是走这条路径(panic 发生在工具 goroutine 里)。
GetKernelStatus 改为先按具体指针判空、再赋给接口;并加刻画测试钉住这个成因
(一旦不再 panic 说明参数形状已变,守卫与该测试应同步删除)。

验证:单测 4 例(已启用 / 打开失败 / 未配置 / 未加载)+ 刻画测试;
隔离实例 E2E 7/7:正例 provider=chineseclip → enabled=true、dim=512、模态 2;
反例 provider=nonexistent → enabled=false 且 reason 含具体错误与 provider 名,
真实对话仍通。
2026-09-12 14:56:25 +08:00

747 lines
29 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"context"
"flag"
"fmt"
"io"
"log"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
agentPkg "gitcode.com/JianFeeeee/HomeAgent/internal/agent"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
"gitcode.com/JianFeeeee/HomeAgent/internal/ipc"
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
logpkg "gitcode.com/JianFeeeee/HomeAgent/internal/log"
luapkg "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/media"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/pipeline"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/social"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
"gitcode.com/JianFeeeee/HomeAgent/internal/nlp"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins"
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/clawhubadapter"
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/healthcheck"
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr"
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
"gitcode.com/JianFeeeee/HomeAgent/internal/recovery"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
"gitcode.com/JianFeeeee/HomeAgent/internal/supervisor"
"gitcode.com/JianFeeeee/HomeAgent/internal/tracker"
"gitcode.com/JianFeeeee/HomeAgent/pkg/embedding"
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
// 空白导入内置 provider它们各自在 init 里注册到 pkg/embedding。
// 想把核心换成自己的模型,只需替换这一行(或另建一个发行版 main
_ "gitcode.com/JianFeeeee/HomeAgent/providers/chineseclip"
_ "gitcode.com/JianFeeeee/HomeAgent/providers/qwen3vl"
)
func main() {
// 平台门放在最前面:比 flag 解析还早,因为原生 Windows 上根本不应进入任何
// 初始化路径(会去建共享段、拉插件进程)。理由与 WSL 指引见
// platform_windows.go。
requireSupportedPlatform()
dataDir := flag.String("data", "", "data directory (default: auto-detect next to binary)")
httpAddr := flag.String("webui", "", "webui listen address (default: webui.listen_addr from config)")
cliSocket := flag.String("socket", "", "cli unix socket path (default: <data>/cli.sock)")
role := flag.String("role", "agent", "process role: guard (父守护) | agent (工作进程)")
boot := flag.String("boot", "normal", "agent boot mode: normal | failback (受限启动,仅 failback 插件集)")
flag.Parse()
// 父守护模式:只负责拉起/守护 worker不初始化 agent 内核
if *role == "guard" {
runGuard(resolveDataDir(*dataDir))
return
}
log.Printf("[homed] role=agent boot=%s", *boot)
if *dataDir == "" {
*dataDir = resolveDataDir(*dataDir)
}
if *cliSocket == "" {
*cliSocket = filepath.Join(*dataDir, "cli.sock")
}
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
// 文件日志:同时输出到控制台和 data/log/ 目录
logDir := filepath.Join(*dataDir, "log")
if err := os.MkdirAll(logDir, 0755); err != nil {
log.Printf("[homed] warning: cannot create log dir: %v", err)
} else {
logPath := filepath.Join(logDir, fmt.Sprintf("homed_%s.log", time.Now().Format("2006-01-02_15-04-05")))
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Printf("[homed] warning: cannot open log file: %v", err)
} else {
log.SetOutput(io.MultiWriter(os.Stderr, logFile))
log.Printf("[homed] logging to %s", logPath)
}
}
log.Printf("[homed] starting %s", meta.FullVersion())
agentWorkDir := filepath.Join(*dataDir, "agentfs")
dirs := []string{
*dataDir,
filepath.Join(*dataDir, "snapshots"),
filepath.Join(*dataDir, "plugins"),
filepath.Join(*dataDir, "changesets"),
filepath.Join(*dataDir, "memory"),
filepath.Join(*dataDir, "memory", "raw"),
filepath.Join(*dataDir, "adapters"),
agentWorkDir,
}
for _, d := range dirs {
if err := os.MkdirAll(d, 0755); err != nil {
log.Fatalf("create dir %s: %v", d, err)
}
}
// ========================================================================
// 基础设施层:记忆、技能
// ========================================================================
memDB, err := memory.NewGraphDB(filepath.Join(*dataDir, "memory", "graph.db"))
if err != nil {
log.Printf("[homed] warning: memory init failed: %v", err)
memDB = nil
} else {
log.Printf("[homed] graph memory initialized")
}
if memDB != nil {
defer memDB.Close()
}
memIdx := memory.NewIndexer(memDB)
memIdx.Sync() // 启动时立即同步避免前30分钟空窗
socialStore := social.New(memDB)
distiller := pipeline.NewDistiller(memDB, *dataDir, pipeline.DistillerConfig{
Interval: 10 * time.Minute,
RetentionDays: 7,
BatchSize: 50,
})
if memDB != nil {
distiller.Start()
defer distiller.Stop()
}
// ========================================================================
// 配置中心SQLite 持久化,唯一配置源)
// ========================================================================
cfgReg := internalConfig.NewConfigRegistry(filepath.Join(*dataDir, "config.db"))
defer cfgReg.Close()
cfgReg.SeedDefaults(*dataDir)
// LLM 配置写前留档config_set 写 core.llm.* 前自动快照guard 恢复用基线
cfgReg.SetLLMSnapshotFile(filepath.Join(*dataDir, "llm_snapshot.json"))
cfg := cfgReg.ToConfig()
// 共享词嵌入蒸馏提取Phase 3 TransE 验证)与 Agent 上下文复用同一实例,
// 避免同一模型被二次加载(约 200k×300 维 ≈ 数百 MB 内存)。
embedder := memory.NewStaticEmbedder(strings.Split(cfgReg.GetString("core.agent.embedding_model_path", ""), ",")...)
distiller.SetEmbedder(embedder)
// ========================================================================
// Lua VMLLM 协议适配)
// ========================================================================
luaVM := luapkg.NewVM(filepath.Join(cfg.Daemon.DataDir, "adapters"))
if err := luaVM.Start(); err != nil {
log.Printf("[homed] warning: lua vm init failed: %v", err)
} else {
defer luaVM.Stop()
}
// ========================================================================
// 守护管理(代理生命周期管理)
// ========================================================================
sup := supervisor.New(cfg)
if err := sup.Start(); err != nil {
log.Fatalf("start supervisor: %v", err)
}
// ========================================================================
// 变更追踪overlayfs
// ========================================================================
trk := tracker.NewTracker(cfg.Daemon.DataDir, agentWorkDir,
tracker.WithKeepChangesets(100),
tracker.WithMaxChangesetAge(30*24*time.Hour),
)
if err := trk.Init(); err != nil {
log.Printf("[homed] warning: tracker init: %v", err)
} else {
if err := trk.Start(); err != nil {
log.Printf("[homed] warning: tracker mount overlay: %v (non-fatal: no overlayfs support?)", err)
} else {
log.Printf("[homed] change tracker active at %s", trk.MergeDir())
}
}
// ========================================================================
// 内核 APIIOManagerIO 抽象层) + EventBus事件总线
// 所有插件通过这两个通道与核心交互
// ========================================================================
iom := agentIO.NewIOManager()
evBus := events.NewBus()
log.Printf("[homed] kernel API ready: IOManager + EventBus")
// ========================================================================
// 文本记忆 + 记忆蒸馏管线
// ========================================================================
textMem := text.New(filepath.Join(cfg.Daemon.DataDir, "memory", "text"))
if err := textMem.Start(); err != nil {
log.Printf("[homed] warning: text memory start: %v", err)
} else {
defer textMem.Stop()
log.Printf("[homed] text memory active at %s", filepath.Join(cfg.Daemon.DataDir, "memory", "text"))
}
ctx, stop := context.WithCancel(context.Background())
defer stop()
go func() {
for {
select {
case <-ctx.Done():
return
case evt, ok := <-iom.OutputChan():
if !ok {
return
}
if evt.Target == "memory" && evt.Type == "memory_candidate" {
source, _ := evt.Payload["source"].(string)
input, _ := evt.Payload["input"].(string)
response, _ := evt.Payload["response"].(string)
toolsUsed, _ := evt.Payload["tools_used"].([]string)
toolResults, _ := evt.Payload["tool_results"].([]interface{})
agentID, _ := evt.Payload["agent_id"].(string)
if input != "" && textMem != nil {
te := text.Event{
Timestamp: time.Now().Unix(),
Source: source,
Input: input,
Response: response,
ToolsUsed: toolsUsed,
AgentID: agentID,
}
if err := textMem.Append(te); err != nil {
log.Printf("[homed] text memory append: %v", err)
}
}
if input != "" && memDB != nil {
distiller.Append("agent", "user", input)
}
if response != "" && memDB != nil {
distiller.Append("agent", "assistant", response)
}
// 工具输出接入蒸馏管线
for _, tr := range toolResults {
if trMap, ok := tr.(map[string]interface{}); ok {
if text, ok := trMap["output"].(string); ok && text != "" && memDB != nil {
distiller.Append("agent", "tool", text)
}
}
}
}
}
}
}()
// ========================================================================
// LLM Provider 管理(多源,通过 Lua 适配器协议转换)
// ========================================================================
apiKey := cfg.LLM.APIKey
if apiKey == "" {
apiKey = os.Getenv("LLM_API_KEY")
}
if apiKey == "" {
apiKey = os.Getenv("DEEPSEEK_API_KEY")
}
baseAPIKey := apiKey
providerMgr := agentAPI.NewProviderManager()
adapterConcurrency := map[string]int{}
for _, src := range cfg.LLM.Sources {
if !agentAPI.IsValidSourceConfig(src.Name, src.BaseURL, src.Model, src.Adapter) {
log.Printf("[homed] skip invalid llm source %q (base_url=%q model=%q adapter=%q)", src.Name, src.BaseURL, src.Model, src.Adapter)
continue
}
key := src.APIKey
if key == "" {
key = baseAPIKey
}
luaProvider := agentAPI.NewLuaAdaptedProvider(agentAPI.BaseConfig{
Model: src.Model,
BaseURL: src.BaseURL,
APIKey: key,
Temperature: cfg.LLM.Temperature,
MaxTokens: cfg.LLM.MaxTokens,
ContextWindow: src.ContextWindow,
MaxConcurrent: src.MaxConcurrent,
Priority: src.Priority,
Vision: src.Vision,
Audio: src.Audio,
}, luaVM, src.Name, src.Adapter)
providerMgr.Register(src.Name, luaProvider)
if src.Adapter != "" {
adapterConcurrency[src.Adapter] += src.MaxConcurrent
}
}
luaVM.ConfigureConcurrency(adapterConcurrency)
if cfg.LLM.Provider != "" {
providerMgr.SetDefault(cfg.LLM.Provider)
}
provider := providerMgr.Default()
// L1 failback受限 worker 启动即跑恢复梯子probe→还原DNS/proxy→还原config+ReloadFromConfig→probe
// 结果以退出码 exitRecovered=43 / exitRecoveryFailed=44 交回 guard不进入主 agent 循环。
if *boot == "failback" {
runFailbackRecovery(*dataDir, cfgReg, luaVM, providerMgr, baseAPIKey)
}
// ========================================================================
// 文档记忆 + 知识库
// ========================================================================
docStore := document.NewStore(filepath.Join(cfg.Daemon.DataDir, "memory", "documents"), memory.TokenizeWords)
if err := docStore.Start(); err != nil {
log.Printf("[homed] warning: document store: %v", err)
}
// 关停时落盘。文档记忆的内存态变更(迁移结果、访问计数等)只在 flush
// 里写盘,而 flush 的唯一入口是 Stop()——此前全仓无人调用它,
// 于是迁移结果永不落盘、每次启动白算一遍。
defer docStore.Stop()
// 媒体存储(内容寻址):记忆块的内容后端。
// 开关默认开;关闭后全部媒体接线静默跳过,对话行为与本特性上线前一致。
var mediaStore *media.Store
if cfgReg.GetBool("core.memory.media.enabled", true) {
mediaDir := cfgReg.GetString("core.memory.media.dir",
filepath.Join(cfg.Daemon.DataDir, "memory", "media"))
ms, err := media.New(mediaDir)
if err != nil {
// 媒体存储开不起来不该阻止启动——它是记忆增强,不是对话必需品
log.Printf("[homed] warning: media store: %v媒体记忆已禁用", err)
} else {
mediaStore = ms
defer mediaStore.Close()
st := mediaStore.Stats()
log.Printf("[homed] media store active: %v 条 / %v 字节",
st["count"], st["total_bytes"])
}
}
// 统一多模态向量空间。
//
// 核心**不**知道任何具体模型:它只按配置里的 provider 名从公共注册表
// pkg/embedding打开一个 provider并把 options.* 原样交给它。模型文件
// 布局、预处理、解码、运行时全部属于 provider 内部实现。
// provider 名为空时禁用多模态向量检索,退回纯 fastText 文本路径。
var multimodalSpace vector.MultimodalEmbedder
// 这两个值只用于状态报告healthcheck_kernel 的 onnx 段):
// 「配了哪个 provider」与「为什么没启用」避免只能看到 false 却不知原因。
var mmProviderName, mmErr string
if mmProvider := cfgReg.GetString("core.memory.multimodal_space.provider", ""); mmProvider != "" {
mmProviderName = mmProvider
opts := map[string]string{}
const optPrefix = "core.memory.multimodal_space.options."
for _, key := range cfgReg.List("core.memory.multimodal_space.options.") {
opts[strings.TrimPrefix(key, optPrefix)] = cfgReg.GetString(key, "")
}
provider, err := embedding.Open(mmProvider, embedding.Config{Options: opts})
if err != nil {
mmErr = err.Error()
log.Printf("[homed] warning: 多模态向量 provider %q 打开失败: %v多模态向量检索已禁用已注册: %s",
mmProvider, err, strings.Join(embedding.Names(), ", "))
} else if adapted, err := vector.AdaptProvider(provider); err != nil {
provider.Close()
mmErr = err.Error()
log.Printf("[homed] warning: 多模态向量 provider %q 元数据不合法: %v多模态向量检索已禁用", mmProvider, err)
} else {
multimodalSpace = adapted
defer adapted.Close()
info := provider.Info()
// 指纹可能很长(模型文件哈希),日志里只取前 12 个字符便于对照。
shortFP := info.Fingerprint
if len(shortFP) > 12 {
shortFP = shortFP[:12]
}
log.Printf("[homed] multimodal space active: provider=%s dim=%d fp=%s modalities=%v",
mmProvider, info.Dimension, shortFP, info.Modalities)
}
}
ks := knowledge.NewStore(filepath.Join(cfg.Daemon.DataDir, "knowledge"))
if err := ks.Start(); err != nil {
log.Printf("[homed] warning: knowledge store: %v", err)
} else {
log.Printf("[homed] knowledge store active with %d items", len(ks.List()))
}
// ========================================================================
// 人格设定
// ========================================================================
// 人格来源优先级personal/personal.md高级覆盖存在且非空才生效
// > 配置项 core.agent.personal_prompt默认模板 = config.DefaultPersonaPrompt
//
// 曾经只有「文件」一个来源且无人维护,导致人格卡写死旧版本号与已删除的 C ABI、
// 反过来让实例自称旧版本v1.2.0 压测发现)。故:
// - 配置项化 + 内置默认模板(不含版本号字面量)
// - 文件仍在时生效,但扫到腐坏内容就在启动日志里明确告警
personalPath := filepath.Join(cfg.Daemon.DataDir, "personal", "personal.md")
personality, err := agentPkg.LoadPersonality(personalPath)
if err != nil {
log.Printf("[homed] warning: load personality: %v", err)
}
if personality != nil && personality.Content != "" {
log.Printf("[homed] 人格来源=文件 %s优先于配置项%d 字节", personalPath, len(personality.Content))
if hints := agentPkg.PersonaStaleHints(personality.Content); len(hints) > 0 {
log.Printf("[homed] warning: 人格文件含会腐坏的内容 %v — 建议迁到配置项 core.agent.personal_prompt"+
"(默认模板不含版本号,被问版本时以运行时快照为准)", hints)
}
} else if pv := cfgReg.GetString("core.agent.personal_prompt", internalConfig.DefaultPersonaPrompt); strings.TrimSpace(pv) != "" {
personality = &agentPkg.Personality{Content: pv, Path: "(core.agent.personal_prompt)"}
log.Printf("[homed] 人格来源=配置项 core.agent.personal_prompt%d 字节", len(pv))
} else {
log.Printf("[homed] 人格来源=无(配置项为空且无人格文件)")
}
// ========================================================================
// 阶段管道StageHost+ 插件系统Registry
// ========================================================================
stageHost := agentCore.NewStageHost()
pluginReg := plugin.NewRegistry()
pluginReg.SetIOManager(iom)
pluginReg.SetEventBus(evBus)
pluginReg.SetMemory(memDB)
pluginReg.SetTextMemory(textMem)
pluginReg.SetDocStore(docStore)
pluginReg.SetMediaStore(mediaStore) // 插件写入的记忆也走媒体链路nil 时静默降级
pluginReg.SetKnowledge(ks)
pluginReg.SetProviderManager(providerMgr)
pluginReg.SetConfigRegistry(cfgReg)
pluginReg.SetPluginDir(cfg.Plugin.Dir)
pluginReg.SetDataDir(*dataDir) // 插件 SettingsAPI.DataDir() 的数据根目录
// Wire registration callbacks: plugins' RegisterTool/RegisterStage → StageHost
pluginReg.SetToolRegistrar(func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
log.Printf("[homed] SetToolRegistrar registering tool: %s (plugin=%s)", name, def.Plugin)
return stageHost.RegisterTool(name, def, handler)
})
pluginReg.SetStageRegistrar(func(stage sdk.Stage, handler sdk.StageHandler) {
stageHost.RegisterStage(stage, handler)
})
pluginReg.SetAPIRegistrar(func(name string) error {
return nil
})
pluginReg.SetToolCleaner(stageHost)
// ========================================================================
// Agent Core (需在插件加载前创建,因为插件 Configure 需要 StatusProvider)
// ========================================================================
defaultPrompt := `你是 HomeAgent一个持续运行的个人管家。
你的每次回复会自动发送到当前输出通道(默认=输入源),无需额外工具。
如需切换回复通道,使用 output_set_channel。
如需异步发送消息或通知,使用 output_send 指定通道和内容。
使用 output_list_channels 查看可用通道及其能力。
可用工具列表会由系统自动传入,按需使用即可。以下是你尤其需要关注的几类工具:
- memory_* — 图记忆(长期记忆,记录和查询个人信息/事实)
- knowledge_* — 知识库(查阅预设知识文档)
- doc_* — 文档记忆(近期对话的存档,查询后自动清除)
- person_* — 人物特质与社交关系网
- llm_* — LLM 源管理(列出/切换模型提供商)
- output_* — 输出通道管理(切换/发送消息)
- timer_set — 设置定时提醒
- plgreload — 热重载插件
- spawn_child — 生成子 Agent 异步执行独立任务(可传 max_turns 控制工具轮数,默认 5
并行策略:遇到多个互不依赖的子任务时,优先并行 spawn 多个子 Agent 而非自己串行逐个执行;
长耗时任务(批量处理、多轮搜索汇总)也应交给子 Agent避免阻塞当前对话。
- describe_image — 描述用户上传的图片
- transcribe_audio — 转写用户上传的音频
- ocr_image — 识别图片中的文字
命令与文件操作策略:
- cmd_run 经完整 shellbash执行支持管道、分号、&&、命令替换、heredoc、重定向。
- 多步交互式程序vim/top/ssh 会话、需要持续输入的进程)用 terminal_create 创建终端,
terminal_write 发送输入、terminal_read 读输出——不要用 cmd_run 硬等交互程序退出。
- 写文件优先 files_write原子+留档),生成多行内容时可用 heredoc 或 files_write
不要用 echo 拼接长文本。
- 读用户发来的文件用 files_read向 webui 回传图片/文件用 output_send__webui(type=image/file)。
当用户上传图片或音频时,系统会自动附着媒体内容。如果模型不支持直接处理多媒体,请使用上述工具。
回复你的真实想法,用自然语言与用户交流。不要在回复中使用 emoji 表情。`
sysPrompt := cfgReg.GetString("core.agent.system_prompt", defaultPrompt)
if sysPrompt == "" {
sysPrompt = defaultPrompt
}
agent := agentCore.New(agentCore.AgentConfig{
ID: "main",
SystemPrompt: sysPrompt,
Provider: provider,
ProviderManager: providerMgr,
IO: iom,
Memory: memDB,
Indexer: memIdx,
Tracker: trk,
DocStore: docStore,
Knowledge: ks,
SocialStore: socialStore,
TextMemory: textMem,
MediaStore: mediaStore,
Personality: personality,
// 人格落库面:首启门禁(任何通道都问一次)与 persona_set 工具用。
// 与 WebUI 向导共用 internal/config 的同一份落库逻辑。
PersonaStore: internalConfig.RegistryPersonaStore{Reg: cfgReg},
PluginReg: pluginReg,
PluginDir: cfg.Plugin.Dir,
DistillInterval: cfgReg.GetDuration("core.agent.distill_interval", 30*time.Minute),
ArchiveInterval: cfgReg.GetDuration("core.agent.archive_interval", 60*time.Minute),
ReviewInterval: cfgReg.GetDuration("core.agent.review_interval", 120*time.Minute),
MergeInterval: cfgReg.GetDuration("core.agent.merge_interval", 120*time.Minute),
ContextSavePath: filepath.Join(cfg.Daemon.DataDir, "memory", "context.json"),
EmbeddingModelPath: cfgReg.GetString("core.agent.embedding_model_path", ""),
Embedder: embedder,
MultimodalSpace: multimodalSpace,
EmbeddingProvider: mmProviderName,
EmbeddingError: mmErr,
StageHost: stageHost,
EventBus: evBus,
ThinkingEnabled: cfg.LLM.ThinkingEnabled,
InputProcessing: cfg.InputProcessing,
})
// 通过 Registry 将内核依赖注入每个插件的 PluginSDK阶段6 将替换遗留的 util.Configure
pluginReg.SetLuaVM(luaVM)
pluginReg.SetBaseAPIKey(baseAPIKey)
pluginReg.SetSupervisor(supervisor.NewSDKAdapter(sup))
pluginReg.SetTracker(trk)
pluginReg.SetConfig(cfg)
pluginReg.SetStageHost(stageHost)
pluginReg.SetIndexer(memIdx)
pluginReg.SetStatusProvider(agent)
// 为内置插件注入内核依赖(各插件通过 init() 自注册工厂)
cli.DefaultSocket = *cliSocket
// webui 插件作为内置插件经 Registry 启动,读取自身 settings["addr"](默认 :8080
// 保留 CLI --webui 与 webui.listen_addr 配置对监听地址的覆盖。
webuiListenAddr := *httpAddr
if webuiListenAddr == "" {
webuiListenAddr = cfgReg.GetString("webui.listen_addr", ":8080")
}
if ps := cfgReg.PluginConfig("webui"); ps != nil {
if v, _ := ps.Get("addr"); v == nil {
_ = ps.Set("addr", webuiListenAddr)
}
}
// ========================================================================
// 依存句法分析器(内嵌 ONNX 模型 / 规则引擎)
// ========================================================================
modelPath := cfgReg.GetString("core.agent.onnx_model_path", "")
onnxParser, err := nlp.NewONNXParser(nlp.ONNXConfig{
ModelPath: modelPath,
DataDir: filepath.Join(cfg.Daemon.DataDir, "nlp"),
})
if err != nil {
log.Printf("[homed] warn: ONNX parser init: %v, using fallback", err)
} else {
nlp.SetDefaultParser(onnxParser)
log.Printf("[homed] dep parser initialized (model: %s)", modelPath)
}
// Auto-create plugins directory (without hardcoding plugin names)
os.MkdirAll(cfg.Plugin.Dir, 0755)
// failback 受限启动:仅装载 failback 插件集webfetch/files/cmd 为内核内置,
// 此处仅控制外部插件,默认含 recoverydiag 以便直接在受限态产出恢复结论)
if *boot == "failback" {
list := cfgReg.GetString("core.agent.failback_plugins", "webui,pluginmgr,recoverydiag")
// 优先使用 guard.yaml 经过 recovery 任务下发的插件集guard 是 failback 权威)
if task, terr := recovery.LoadTask(recovery.TaskPath(*dataDir)); terr == nil && len(task.Plugins()) > 0 {
list = strings.Join(task.Plugins(), ",")
}
var names []string
for _, s := range strings.Split(list, ",") {
if s = strings.TrimSpace(s); s != "" {
names = append(names, s)
}
}
pluginReg.SetLoadAllowlist(names)
log.Printf("[homed] failback boot: plugin allowlist = %v", names)
}
// Load all plugins — each scans its own dir and is loaded via factory or .so
if err := pluginReg.Load(cfg.Plugin.Dir); err != nil {
log.Printf("[homed] warning: load plugins: %v", err)
}
log.Printf("[homed] stage host ready with %d registered tools", stageHost.ToolCount())
// 技能索引接线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)
go logManager.Start(ctx)
defer logManager.Stop()
agent.Start()
defer agent.Stop()
// PING/ACK 心跳服务worker 监听 unix socketguard 发 PING、worker 回 ACK
// (含自诊断 kernel 状态快照),替换纯文件心跳。文件心跳保留作回退。
ipcServer := ipc.NewServer(*dataDir, func() *ipc.Status {
st := agent.GetKernelStatus()
llmOK := st != nil && st.LLM.Available
tools := 0
if st != nil {
tools = len(st.Tools)
}
uptime := int64(0)
if st != nil {
if d, err := time.ParseDuration(st.Uptime); err == nil {
uptime = int64(d.Seconds())
}
}
return &ipc.Status{
PID: os.Getpid(),
Boot: *boot,
UptimeSec: uptime,
LLMOK: &llmOK,
Tools: tools,
LastDiag: lastDiagSummary(*dataDir),
}
})
if err := ipcServer.Start(); err != nil {
log.Printf("[homed] warning: ipc heartbeat server: %v", err)
} else {
defer ipcServer.Stop()
}
sup.SetTracker(trk)
sup.RegisterAgent("main")
// 真实存活源 + 重启通道daemon 心跳语义由此修正lastHB 只在确认存活时更新),
// 重启动作不再空转——清理后以特殊退出码交给 guard/systemd 重建。
restartCh := make(chan struct{}, 1)
sup.SetHeartbeatSource(func(id types.AgentID) (time.Time, types.HealthStatus, error) {
st := agent.GetKernelStatus()
if st == nil {
return time.Time{}, types.HealthDown, fmt.Errorf("no kernel status")
}
h := types.HealthHealthy
if !st.LLM.Available {
h = types.HealthDegraded
}
return time.Now(), h, nil
})
sup.SetRestartHandler(func(id types.AgentID) {
select {
case restartCh <- struct{}{}:
default:
}
})
log.Printf("[homed] main agent started, model=%s base=%s sources=%d adapters=%d",
cfg.LLM.Model, cfg.LLM.BaseURL, len(cfg.LLM.Sources), len(luaVM.ListAdapters()))
log.Printf("[homed] kernel ready, waiting for plugin IO...")
// ========================================================================
// 等待退出信号
// ========================================================================
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
// 心跳:每 5s 触碰 <data>/heartbeatguard 据此判定工作进程是否存活/卡死
hbPath := filepath.Join(*dataDir, "heartbeat")
hbStop := make(chan struct{})
go func() {
t := time.NewTicker(5 * time.Second)
defer t.Stop()
writeHB := func() {
if f, err := os.OpenFile(hbPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil {
fmt.Fprintf(f, "t=%d\n", time.Now().Unix())
f.Close()
}
}
writeHB()
for {
select {
case <-t.C:
writeHB()
case <-hbStop:
return
case <-ctx.Done():
return
}
}
}()
restartRequested := false
select {
case <-sigCh:
log.Printf("[homed] shutting down...")
case <-restartCh:
restartRequested = true
log.Printf("[homed] restart requested, shutting down cleanly then exiting with code %d", exitRestartRequested)
}
close(hbStop)
pluginReg.StopAll()
if trk != nil {
trk.Stop()
}
if err := cfgReg.Flush(); err != nil {
log.Printf("[homed] flush config: %v", err)
}
sup.Shutdown()
log.Printf("[homed] stopped")
if restartRequested {
os.Exit(exitRestartRequested)
}
}