Files
HomeAgent/docs/en/ARCHITECTURE.md
root c9e67d3d55 docs: 修正全部文档使其与源码实现一致
主仓库:
- 修复 4 份英文文档语言切换链接指向错误 (../zh/ → ../en/)
- ARCHITECTURE.md 标题 "三种加载方式" → "四种加载方式" (实际表格4行)
- PLUGIN_DEV.md 示例表: 添加 webfetch, 移除不存在的 luaplugintest/testlua
- PLUGIN_DEV.md 代码示例: InjectInput/InjectInterrupt → InjectText/InjectInterruptText
- PLUGIN_DEV.md 代码示例: Memory/Knowledge/LLM/Events 接口签名修正
- PLUGIN_DEV.md .hmap 内容统一, plugindev 编译去除 .exe 后缀

SDK 仓库:
- Plugin.Start(sdk *PluginSDK) 接口签名改为指针
- 方法表重写: 移除 CallLLM/QueryKnowledge/SetMemory 等不存在方法
- IOInjector 参数顺序修正为 (source, channel, text)
- 删除虚构 SDKConfig, 替换为实际 New() 构造函数签名
- .hmap 内容描述一致化

修正前一次会话中的 QQ/Bili 插件问题:
- qq napcat() 超时, fetchBotInfo 竞态, handleWebhook 同步阻塞
- bili CDN 直连失败, 添加 HTTP_PROXY 代理
2026-07-18 20:46:58 +08:00

19 KiB
Raw Blame History

中文 | English

HomeAgent Architecture

The kernel performs zero IO; all external interaction comes from plugins.

:

Message Processing Flow

Full Pipeline

External input (via plugin InjectInput)
        │
        ▼
eventLoop() → processTextInput()
        │
        ├── on_input stage          Plugins can intercept/rewrite/short-circuit
        ├── Context.Append          Record to context window
        ├── Context.Prune           Low-relevance events archived to Document
        ├── buildMemoryContext()    Indexer recall → GraphDB BFS traversal
        │
        ├── pre_action stage        Plugins can inject system messages
        │
        ├── [Tool Loop] process()
        │   ├── buildSystemPrompt   Persona + Memory + Knowledge + Context
        │   ├── buildToolDefs       Built-in tools + Plugin tools
        │   ├── provider.Chat()     LLM call
        │   ├── post_action stage   Plugins see LLM output + tool list
        │   ├── Has tools?
        │   │   ├── before_toolcall Plugins can reject/modify params
        │   │   ├── executeToolCall Route to plugin/built-in
        │   │   ├── after_toolcall  Plugins can modify results
        │   │   └── → back to post_action
        │   └── No tools → exit loop
        │
        ├── Context.Append(response)
        ├── before_output stage     Plugins can modify final text
        ├── emitResponse()          Send via output_send
        └── after_output stage      Read-only, cleanup

Code: internal/agent/core/agent.goprocess() is the main tool loop

7 Stage Hooks

Stage Trigger Plugin Capabilities
on_input Message arrives at Agent, zero processing Blacklist/rate-limit/short-circuit reply
pre_action Context ready, before LLM call Inject external data into context
post_action LLM returns text + tool list Sensitive word filter/forced redirect
before_toolcall Before single tool execution Audit/reject/modify params
after_toolcall After single tool execution Desensitize/sort results
before_output Final text ready, before sending Format adaptation
after_output Already sent Statistics/logging

Code: internal/agent/core/stages.goStageHost orchestration

Loop Rules

post_action → [before_toolcall → execute → after_toolcall] → post_action forms the inner loop. Exit conditions: LLM has no tool calls / all rejected / exceeded limit.

Short-Circuit Rules

Setting ctx.Response at any stage jumps to after_output.

:

Three-Layer Memory

Memory Flow

① Context (Working Window)
   RelevanceContext — In-memory events[] + JSON persistence
   Append: Each input, CleanTemplateText → three-branch vector(textForVector)
           agent→Response, user→Input, cold_storage→Input+Response
           StaticEmbedder pretrained word embedding / TF-IDF fallback
   Prune:  StaticEmbedder CosineSimilarity, keep topK + last 10
       ├── Keep → timeline → chronologically sorted → system prompt
       └── Low score → Document layer archive (original timestamp)
   Save: 5s debounce write to disk

         ↓ Prune archive                       ↑ LLM active recall

② Document (File Memory)
   DocStore — JSON files + TF-IDF InvertedIndex
   Write: Prune archive / doc_commit / Graph snapshot (syncGraphToDocs)
   Read:
       ├── Auto-inject: Query(input, top3) → similarity summary → [Related Memory Docs] → system prompt (read-only)
       └── LLM active:  doc_query → Consume(read and delete)
                         → context.Append{Timestamp: d.CreatedAt, Source: "cold_storage"} per doc
                         → Docs written to context timeline with original timestamps, deleted from docStore
   Cold: FindColdDocs(72h, ≤2 accesses) → docToTriples → Graph

         ↓ Cold doc distillation               ↑ Auto recall

③ Graph (Graph Database)
   SQLite — entities + relations tables
   Write: memory_commit / cold doc distillation / Pipeline rule distillation / memory_merge
   Read:
       ├── Auto recall: Indexer.BuildContext(input)
       │     → CleanTemplateText → vector entity search + jieba keywords → SQLite LIKE + BFS depth=2
       │     → [Memory Index] → system prompt
       └── LLM active: memory_recall / memory_merge / memory_purge / memory_edit / memory_delete_entity
   Social: person_query / set_trait / relate (wraps GraphDB)

④ Distillation Pipeline (30min heartbeat)
   distillContext → window > 2×maxSize → force Prune
   syncGraphToDocs → Graph snapshot to Document (cross-layer searchable)
   reorgGraph:
       Step1: indexer.Sync — rebuild entity vector index
       Step2: docStore.Reindex — rebuild document vector index
       Step3: Cold docs → docToTriples → GraphDB.Commit
       Step4: Entity similarity (Bigram Jaccard > 0.75) → consolidation → LLM decides merge
       Step5: evaluateGraphQuality → LLM decides keep/delete

⑤ Pipeline Rule Distiller (every heartbeat)
   distillOnce → regex match personal info:
     我叫X / 我住在X / 我喜欢X / 我X岁 / 我的工作是X
     → triples → GraphDB.Commit

Vectorization: Pretrained Word Embedding + TF-IDF Fallback

All vectorization unified under StaticEmbedder (internal/memory/static_embedder.go):

Primary Strategy — Pretrained Word Embedding (aligned 300d)

  • Model sources: ConceptNet Numberbatch (77-language aligned) / fastText Chinese / fastText English
  • Configured via core.agent.embedding_model_path (comma-separated multi-model)
  • Path containing numberbatch → auto-download ConceptNet; cc.zh. → fastText Chinese; cc.en. → fastText English
  • Falls back to ConceptNet by default if no match
  • Pre-processing: CleanTemplateText strips QQ tool-call templates and timestamp noise
  • Three-branch vector source: agent→Response, user→Input, cold_storage→Input+Response
  • TF-IDF fallback: auto-fallback to bag-of-words TF-IDF if model download fails or not configured
Location File Purpose Algorithm
Context Prune context.go:155 Trim low-relevance context events VectorizeClean → CosineSimilarity(queryVec, evt.Vector)
DocStore Query document.go:206 Recall from document memory TF-IDF Vectorize → vec.Search
Indexer Entity Search indexer.go:96+111 Recall from Graph vector entity search + jieba keywords → SQLite LIKE + BFS
Entity Similarity Detection agent.go Detect similar entities in Graph Bigram Jaccard (>0.75 → consolidation)

Context Layer

internal/agent/core/context.goRelevanceContext

  • Maintains recent event list, writes JSON on each Append/Prune to prevent data loss
  • Pre-vectorization pipeline runs through CleanTemplateText to remove template noise
  • Three-branch textForVector: agent events → Response, user events → Input, cold_storage → Input+Response
  • Pretrained word embedding StaticEmbedder → CosineSimilarity, auto-fallback to TF-IDF if unavailable
  • Protects last 10 events from eviction; excess candidates are sorted by relevance and archived to document memory
  • Archived events retain original timestamps; on doc_query recall they re-insert into the context timeline at their original position

Document Layer

internal/memory/document/document.goStore

  • Consume-on-read mode: deleted after doc_query retrieval
  • Dual recall: char-bigram TF-IDF vector search + jieba keyword extraction

Graph Layer

internal/memory/graph.goGraphDB

  • SQLite WAL mode, two tables (driver: mattn/go-sqlite3, CGo)
  • Commit(triples) — UPSERT entities + INSERT relations
  • Recall(keywords, depth) — Keyword LIKE search + BFS traversal

Memory Tools (LLM-callable)

Tool Purpose
memory_recall Recall from Graph
memory_commit Write triples to Graph
memory_merge Merge two entity nodes
memory_purge Delete entity node
memory_edit Edit existing entity/relation
memory_delete_entity Delete entity and all its relations
memory_introspect View memory statistics
doc_query Search from Document
doc_commit Write to Document

Other Memory Layers

  • Social (internal/memory/social/social.go) — Persona traits and relationship network, wraps GraphDB entity types
  • Text Memory (internal/memory/text/text.go) — Raw conversation JSONL logs, rotation strategy
  • Memory Indexer (internal/memory/indexer.go) — Entity vectorization + jieba keyword extraction, auto-inject into system prompt

Distillation Pipeline

internal/memory/pipeline/pipeline.go

  • 10-minute tick, 7-day retention
  • Rule-based triple extraction (name / location / likes / age / job patterns)
  • Writes to GraphDB

Context Pruning

Heartbeat 30min:
  ├── distillContext()        — Distill current context
  ├── syncGraphToDocs()       — Graph → Document sync
  └── reorgGraph()
      ├── Indexer.Sync()
      ├── DocStore.Reindex()
      ├── Cold docs → Graph
      └── Entity conflicts → enqueueConsolidationTask()
                                │
                          selfInputCh → LLM decides merge/skip

Entity conflict detection heuristic (bigram Jaccard > 0.75), routed through selfInputCh internal channel, LLM makes the final merge decision.

:

Knowledge Base

internal/knowledge/knowledge.go

  • File directory knowledge/<name>/content.md
  • Independent TF-IDF index, separate from memory system
  • knowledge_search / knowledge_create / knowledge_list

:

Provider & Lua Adapter Layer

Agent
  │
  ▼
Provider Interface (Name / Chat / ChatStream)
  │
  ├── OpenAIProvider   — Standard OpenAI API
  ├── OllamaProvider   — Local Ollama
  └── LuaAdaptedProvider (primary)
      ├── Serialize CompletionRequest → JSON
      ├── adapter.transform_request() → API format
      ├── HTTP request + adapter.headers
      ├── adapter.transform_response() → unified format
      └── Deserialize

Code: internal/agent/api/provider.go

ProviderManager manages multiple sources, fallback in registration order. Lua adapters at internal/lua/adapters/, each .lua script defines transform_request / transform_response / transform_stream_chunk.

VM built-ins: json.encode / json.decode / log / http_get / http_post.

:

Plugin System

Four Loading Methods

Method Registration Mechanism Compilation Usage
Built-in init()RegisterFactory internal/plugins/ compiled into kernel webui/cli/timer/mcp etc.
External .so C ABI dynamic loading -buildmode=c-shared + bridge qq/files/web/memo etc.
Lua script plugin Parse main.lua to register tools No compilation, hot-reload luaplugintest/testlua etc.
SKILL plugin Parse SKILL.md Markdown definition OpenClaw compatible

Built-in plugin registration: internal/plugins/all.go blank imports → each plugin init()Registry.Load() scans directory to match factory. External plugin loading: internal/plugin/dynamic.go → copy to SHA256 temp path (bypass plugin.Open path cache) → Open + Lookup("NewPlugin"). Lua script plugin loading: internal/lua/ → parse main.lua via Lua VM, call start() to register tools.

PluginSDK Four Channels

Plugin ──→ Kernel

RegisterTool(name, fn)   ──→  buildToolDefs() / executeToolCall()
RegisterStage(stage, fn, scope...)  ──→  runStage() called at corresponding phase (scope: global / own-tools-only)
Subscribe(event, fn)      ──→  Publish() notify all subscribers
RegisterOutputChannel(name, caps, desc, handler) ──→ output_send__{name} tool generation

internal/sdk/ bridges external SDK interface to kernel, defines complete PluginSDK:

sdk.RegisterTool(name, def, handler)
sdk.RegisterStage(stage, handler, scope...)
sdk.Publish(event)
sdk.InjectInput(source, channel, payload)
sdk.InjectInterrupt(source, channel, payload)
sdk.Memory().Recall/Commit
sdk.Knowledge().Search/Create
sdk.Settings().Get/Set/List
sdk.RegisterOutputChannel("qq", sdk.CapText, "QQ消息通道content为JSON: {text, group_id, user_id}", handler)

Plugin Interface

type Plugin interface {
    Name() string
    Start(sdk *PluginSDK) error
    Stop() error
}

:

Output Channel System

Each output channel generates two tools:

Tool Type Purpose
output_send__{name} function Accepts a content JSON string parameter, transparently routed to the plugin's registered handler
output_send__{name}_help function Returns the channel's JSON format documentation (desc field)

Capability flags:

Flag Value Meaning
CapText 1 Plain text
CapFile 2 File
CapImage 4 Image
CapAudio 8 Audio
CapStructured 16 Structured data

System prompt injection: output gate rules, multi-call support, long message splitting. Child agent permission: output_send__ prefix tools are allowed.

:

EventAgentLLMChain Event

  • Event type agent_llm_chain emitted after each LLM turn
  • Contains the full LLM response (text + tool calls + reasoning)
  • WebUI subscribes to this event via SSE for real-time display
  • Plugins can subscribe via EventSubscriber (read-only for external plugins)

:

Restricted External Plugin API

Layered architecture: internal plugins get full PluginSDK, external plugins get restricted SDK.

API Internal Plugin External Plugin
SocialAPI Full read/write Read-only (GetPerson / GetTrait / GetRelations / GetNetwork / ListPersons)
EventSubscriber Subscribe + Publish Subscribe-only (no Publish capability)

Extended fields:

  • Triple extensions: Confidence, SubjectType, ObjectType
  • Relation extension: Confidence

:

Interrupt Mechanism

interceptLoop (goroutine)
  ├── InputInterruptChan() ← Timer/message notifications
  ├── (a) cancelLLM() → Cancel Provider HTTP request
  ├── (b) interceptCh → process() pre-loop read [interrupt message]
  └── (c) InjectInput() → Trigger new processing when idle

Three delivery paths:

Path Effect Timing
cancelLLM Cancel current HTTP request On context.Canceled
interceptCh Insert [interrupt message] in process() Before each LLM call
InjectInput Trigger new processing when eventLoop is idle No ongoing request

Code: internal/agent/core/agent.gointerceptLoop / drainInterrupt

:

Configuration System

internal/config/registry.go — ConfigRegistry

  • SQLite storage, config table + config_<plugin> independent tables
  • Namespaces: core.* / plugin.<name>.*
  • RegisterDefault inserts ~80 default keys (seeds for 8 LLM sources)
  • WebUI settings page /api/v1/settings for read/write

:

Code Structure

cmd/homed/main.go          — Entry: assembles all subsystems
cmd/waiter/main.go         — CLI client (Unix socket)
internal/
├── agent/
│   ├── core/              — Agent core (eventLoop/process/stages/context)
│   │   └── plugin_health.go — Plugin health monitoring and auto-restart
│   ├── api/               — Provider interface + LuaAdaptedProvider
│   ├── io/                — IOManager (queue/interrupt/output)
│   └── personal.go        — Persona loading
├── plugin/
│   ├── registry.go        — Registry + lifecycle
│   ├── dynamic.go         — .so dynamic loader
│   └── manifest.go        — plugin.json metadata
├── plugins/               — Built-in plugin implementations
│   ├── all.go             — Blank imports
│   ├── webui/             — HTTP server + embedded SPA
│   ├── cli/               — Unix socket CLI
│   ├── timer/             — Timer
│   ├── cmd/               — Command execution
│   ├── mcp/               — MCP protocol
│   ├── openclaw/          — OpenClaw compatible
│   ├── agentcli/          — PTY terminal
│   ├── healthcheck/       — Health check
│   └── pluginmgr/         — Plugin manager
├── sdk/                   — PluginSDK definitions
│   ├── plugin.go          — Plugin interface + PluginSDK
│   ├── memory.go          — MemoryAPI
│   ├── knowledge.go       — KnowledgeAPI
│   ├── settings.go        — SettingsAPI
│   └── llm.go             — LLMAPI
├── memory/
│   ├── graph.go           — SQLite graph database
│   ├── indexer.go         — Graph → vector index
│   ├── vector/store.go    — TF-IDF vector engine
│   ├── document/document.go — Document memory
│   ├── text/text.go       — Text logs
│   └── pipeline/          — Distiller
├── knowledge/knowledge.go — Knowledge base
├── lua/
│   ├── vm.go              — Lua VM (json/log/http)
│   └── adapters/          — 8 LLM adapter scripts
├── config/registry.go     — SQLite config center
├── events/bus.go          — Event bus
├── tracker/               — OverlayFS change tracking
├── supervisor/            — Daemon management
├── skill/                 — Skill plugin management
│   └── manager.go         — Skill loading/matching
└── meta/                  — Meta information
    └── meta.go            — Agent metadata