Files
HomeAgent/docs/en/design-summaries.md
JianFeeeee 06f3161e1d docs(en): 建立英文文档目录 —— 索引 + 教训全文 + 设计摘要
## 问题

仓库是公开的,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
2026-11-04 20:05:10 +08:00

200 lines
9.7 KiB
Markdown
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.

# 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.