mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 09:17:31 +00:00
## 问题 仓库是公开的,README / README_EN 已经双语,但 `docs/` 只有 `docs/zh/`: 英文读者读完首页就撞墙。 ## 做法:摘要而非全译,并写明为什么 `docs/zh/` 有 27 份文档、约 7000 行设计。**全译会烂得比审得还快**, 而一份陈旧的译文比没有更糟 —— 读者不知道它落后于原文多少。 所以按"内容变化速度"分级: | 类型 | 译法 | 理由 | | --- | --- | --- | | 教训 | ✅ 全文 | 复用价值最高,失败形态会反复出现 | | 索引 | ✅ 全文 | 没有它其余都不可发现 | | 设计文档 | ⚠️ 一页摘要 + 状态行 | 7000 行全译跟不上演进 | | 历史快照/证据 | ❌ 不译 | 时点产物,翻译无增益 | | 未实施计划 | ❌ 不译 | 会变;且按 §4.3 本就不该进仓 | 判据写在 `docs/en/README.md` 里: > 如果一份文档的内容变化速度快过译文能被审阅的速度,就译它的**摘要**而不是正文。 ## 三份文件 **docs/en/README.md** —— 索引。含分类表(哪些已译、哪些只有中文摘要、 哪些不译)、状态行约定、以及"本索引必须在同一提交里更新"这条硬规则 (滞后的索引比没有索引更糟:它教读者信任错的地图)。 **docs/en/engineering-lessons.md** —— §1(判据会骗人)+ §5(判据形态) 全文翻译,§2–§4 摘要。§5 是本轮新增的核心内容: 5.1 判据必须把被测模块当黑箱(含三个正当例外) 5.2 禁止读源码做断言(附"想保证什么 → 错误做法 → 正确做法"表) 5.2.1 判据不得以文档为依据 5.3 测试与业务代码分离 5.3.1 三种测试位置的实测边界 5.3.2 覆盖率必须用 -coverpkg=./... 5.4 CI 必须覆盖每个独立 module 5.5 CI 与本地必须跑同一套门 每个小节都带实测数据与变异验证结果,不是原则罗列。 **docs/en/design-summaries.md** —— 6 份核心设计的一页摘要: kernel-modularization 6 层 20 模块;依赖图在构建期算好并嵌进二进制; depgraph.bin 必须与二进制同目录,放数据目录会崩溃循环 secret-firewall security 是外围到核心之间的防火墙,自持数据结构、 只接受事实不接受策略;出入向都要洗 input-scheduler L1–L4 + 现场保存;工具执行是临界区不可抢占 resident-subagent 与 rtchild 语义相反;单数/复数共用 Cleanup toolcall-contract assistant(tool_calls) 后必须紧跟每个 id 的 tool 消息;批内不得插入任何东西 context-overflow-l4 下沉到冷上下文而非截断;InactivePages 是本次渲染 的临时标记(json:"-") 另有 13 份(插件相关 / 数据报告型)列为 "Chinese only" 并说明理由。 ## 中文索引加了英文入口 `docs/zh/README.md` 顶部增加指向 `../en/README.md` 的一行,并明确: > 本目录是**中文原文,也是唯一权威**;英文侧只做摘要,与本文冲突时以本文为准。 这一条很重要 —— 否则两份文档会逐渐分叉,而读者不知道该信哪个。 ## 验证 go build ./... PASS go vet ./... PASS gofmt -l 干净 go test ./... 65 包全过,0 FAIL
200 lines
9.7 KiB
Markdown
200 lines
9.7 KiB
Markdown
# Design summaries (English)
|
||
|
||
> **Status: summaries only.** Each design document exists in full only in Chinese:
|
||
> `docs/zh/*-design.md`. Those are the authority. This file gives an English reader
|
||
> enough to know *what problem each design solves* and *whether it is in effect*,
|
||
> plus a pointer for the details.
|
||
>
|
||
> **Why summaries instead of translations**: the design docs total ~7,000 lines. A
|
||
> full translation would rot faster than it could be reviewed, and a stale translation
|
||
> is worse than none. For content that changes quickly, translate the summary.
|
||
|
||
---
|
||
|
||
## kernel-modularization
|
||
|
||
**Source**: `docs/zh/kernel-modularization-design.md` (1,502 lines)
|
||
**Status**: implemented and deployed (designed 2026-10-16, landed 2026-10-24, fully
|
||
revised 2026-10-31). This document is the single authority on assembly architecture.
|
||
|
||
**Problem it solves**: the kernel had 17 serial startup steps. Any module that needed
|
||
another module's result had to be started later than it, which produced a hand-maintained
|
||
ordering that silently broke whenever a dependency was added.
|
||
|
||
**Shape**: every kernel module declares `Requires()` (facilities) and `Provides()`
|
||
(facilities). A dependency graph is computed **at build time** and embedded in the binary
|
||
as `depgraph.bin`. At startup the kernel assembles in layers — independent modules in the
|
||
same layer start concurrently. Current production shape: **6 layers / 20 modules, peak
|
||
concurrency 4**.
|
||
|
||
**Two invariants worth knowing**:
|
||
|
||
1. The dependency graph and the binary must match. The kernel compares the embedded
|
||
graph digest against `depgraph.bin` on disk and **refuses to start** on mismatch
|
||
(`★ 依赖图与本二进制不配套`). This is deliberate: a mismatched graph means the
|
||
assembly order is wrong, and a wrong order produces silent `nil` reads.
|
||
2. **`depgraph.bin` lives next to the binary, not in the data directory.** Deploying it
|
||
into the data dir produces a crash loop.
|
||
|
||
**Key concepts** for reading the code:
|
||
|
||
- *facility* — a named capability one module provides and others require
|
||
- *layer* — computed from the graph; modules in a layer have no dependencies between them
|
||
- *`kstate`* — the single mount point for shared data. Other packages mount onto it;
|
||
it never knows about them (leaf package).
|
||
|
||
---
|
||
|
||
## secret-firewall
|
||
|
||
**Source**: `docs/zh/kernel-secret-firewall-design.md` (952 lines)
|
||
**Status**: implemented and deployed; architecture converged 2026-10-31.
|
||
|
||
**Problem it solves**: secrets (API keys, platform credentials) must not reach the model's
|
||
context, logs, or disk in plaintext — but plugins legitimately need the real values to make
|
||
API calls. So there must be a translation layer at the boundary.
|
||
|
||
**Shape**: `security` is a **firewall between the peripheral kernel and the core kernel**.
|
||
It owns its own data structures and makes its own decisions. Callers supply **facts** only
|
||
(`CheckTool(name)`, `ScanContent(text)`) — never a policy, never a "trust me" flag.
|
||
|
||
**Three properties that matter**:
|
||
|
||
1. **Aliases, not values.** The model sees `sec_xxx`; the firewall substitutes the real
|
||
value inside the executor, after the argument has left the model's context.
|
||
2. **Direction matters.** Inbound (model → plugin) fills aliases. Outbound (plugin → model)
|
||
scrubs any secret accidentally echoed back — e.g. `enc_shell` running `curl` whose
|
||
response body contains the key. Both directions have separate tests because only
|
||
inbound was implemented at first, and the outbound leak was found in production logs.
|
||
3. **`Store` delegates encryption to `Codec`, and domains must differ.** Store ciphertext
|
||
uses domain `security/variables`; config-at-rest uses `config-at-rest`. Sharing a domain
|
||
means one leak informs the other.
|
||
|
||
**Judgement criterion** the design insists on: `CheckPath` / `CheckNetwork` / `CheckTool`
|
||
are only real if something in the *execution chain* calls them. A policy that exists but
|
||
has no caller is indistinguishable from no policy — and this happened here twice.
|
||
|
||
---
|
||
|
||
## input-scheduler
|
||
|
||
**Source**: `docs/zh/input-scheduler-design.md` (642 lines)
|
||
**Status**: implemented and deployed (`internal/agent/core/scheduler.go`).
|
||
|
||
**Problem it solves**: a single agent receives messages from many channels (chat, CLI,
|
||
device bridge) with wildly different urgency. A naive FIFO queue either delays urgent
|
||
interrupts behind long tasks, or lets interrupts destroy work in progress.
|
||
|
||
**Shape**: four interrupt levels (L1–L4) plus a queue for non-interrupting work;
|
||
preemption with **state snapshots**, so a suspended task resumes exactly where it stopped
|
||
rather than restarting. Interrupts form a **strict LIFO stack** bounded by the number of
|
||
levels.
|
||
|
||
**Invariants** (each has a test):
|
||
|
||
- L4 does not preempt L4 (same-level interrupts queue rather than nest)
|
||
- Stack depth is bounded; exceeding it demotes rather than unbounded recursion
|
||
- A suspended task and its resumed counterpart see the same state (`SuspendedEqualsResumed`)
|
||
- Tool execution is a **critical section**: interrupts do not cancel mid-tool (side effects
|
||
are not rollback-able). The interrupt is recorded and applied after the tool completes.
|
||
- The notify hook fires on push and pop, **while holding the lock** (a hook that takes the
|
||
lock itself would self-deadlock)
|
||
|
||
---
|
||
|
||
## resident-subagent
|
||
|
||
**Source**: `docs/zh/resident-subagent-design.md` (712 lines)
|
||
**Status**: implemented and deployed.
|
||
|
||
**Problem it solves**: some work spans many messages over hours (a project conversation, a
|
||
phone call). Stateless sub-agents lose everything between turns; giving every sub-agent a
|
||
full agent loop is too heavy.
|
||
|
||
**Shape**: a resident child has its own input queue, its own two-level memory, and a
|
||
parent-child interrupt relationship. Residual tasks left at teardown have four states
|
||
(keep / drop / park / return) rather than being silently lost.
|
||
|
||
**Why it is separate from `rtchild`**: the rt (realtime) child has the *opposite* lifetime
|
||
semantics — no task queue, no residuals, a fresh context window per call. They share the
|
||
registry skeleton but not the semantics.
|
||
|
||
**Known trap** (this is why `child.Registry` exists): the singular stop path used to
|
||
unregister the inbound channel while the plural path forgot it, so residue accumulated
|
||
monotonically with each create/destroy until restart. Both paths now call the same
|
||
`Cleanup`.
|
||
|
||
---
|
||
|
||
## toolcall-contract
|
||
|
||
**Source**: `docs/zh/toolcall-contract-and-sequence-design.md` (839 lines)
|
||
**Status**: implemented and deployed.
|
||
|
||
**Problem it solves**: with parallel tool execution, the wire format has hard constraints
|
||
that are *not* optional — an `assistant` message declaring `tool_calls` must be followed
|
||
**immediately** by one `tool` message per `tool_call_id`. Anything inserted in between
|
||
(multimodal attachment, memory recall, a system note) makes providers reject the whole
|
||
request with HTTP 400.
|
||
|
||
**Shape**: the batch executor collects all tool results first, then writes messages
|
||
sequentially, then flushes deferred attachments (`flushBatchAttachments`). Nothing may be
|
||
inserted inside the batch.
|
||
|
||
**Measured failure modes** (all three happened):
|
||
|
||
1. Parallel path appended `ToolResults` but never `ToolCalls` ⇒ results outnumbered calls
|
||
2. Budget truncation in L1 rendering cut between `assistant(tool_calls)` and its `tool`
|
||
messages ⇒ dangling calls
|
||
3. A rejected/skipped tool recorded a call but not a result ⇒ same mismatch, persisted into
|
||
`context.json` and replayed every turn
|
||
|
||
The third is the instructive one: **"skipped" ≠ "no result"**. The rejection reason *is* the
|
||
result, and the model already saw it. The context copy must match what the model saw.
|
||
|
||
---
|
||
|
||
## context-overflow-l4
|
||
|
||
**Source**: `docs/zh/context-overflow-l4-design.md` (163 lines)
|
||
**Status**: implementation record; **terminology in the document is outdated** (it still
|
||
says `doc_query` / `doc_commit`, renamed to `coldcontext_*`).
|
||
|
||
**Problem it solves**: when the context window is exhausted, the two conventional options
|
||
are both bad — truncate (lose information) or ask the model to summarize (costly, lossy,
|
||
and the model may summarize wrongly).
|
||
|
||
**Shape**: instead of discarding, low-relevance events are **sunk** into a cold-context
|
||
layer, and from there distilled into a graph. Nothing is deleted; it changes storage tier.
|
||
Overflow handling recurses to L4 when trimming alone cannot help.
|
||
|
||
**Why it matters for reading the code**: `InactivePages` in `ContextEvent` is the "this page
|
||
is hidden for *this* render" marker. It is deliberately `json:"-"` — hidden pages are a
|
||
per-render decision, not persisted state. The constraint is that a *whole* tool call and its
|
||
results are hidden or shown together; hiding one half produces the 400 in
|
||
`toolcall-contract` above.
|
||
|
||
---
|
||
|
||
## Other design docs (Chinese only, not summarized here)
|
||
|
||
| Document | Scope |
|
||
| --- | --- |
|
||
| `commondev-design.md` | The `commondev` plugin (multi-repo supervisor) |
|
||
| `commondev-agent-runtime-design.md` | Runtime wiring for that plugin |
|
||
| `triple-distill-llm-design.md` | Triple extraction/distillation pipeline |
|
||
| `triple-extract-attention.md` | Attention weighting for extraction |
|
||
| `recall-as-association.md` | Recall as spreading activation rather than lookup |
|
||
| `recall-capability-tiers.md` | What each recall tier can and cannot do |
|
||
| `multimodal-space.md` | Unified multimodal embedding space |
|
||
| `vector-provider-decision.md` | Choice of embedding provider and why |
|
||
| `abstention-criterion.md` | When the agent should decline to answer |
|
||
| `legacy-issues.md` | Known legacy problems |
|
||
| `legacy-table-retirement.md` | Old-table retirement plan (cited by production code) |
|
||
| `kernel-performance.md` | Performance measurements and budgets |
|
||
| `production-recall-validation.md` | Recall validation on production data |
|
||
|
||
These are not translated because they are either plugin-specific or data-report shaped —
|
||
their value is in the numbers and the local context, both of which the Chinese original
|
||
carries adequately.
|