# DeepTutor source analysis: how “lifelong tutoring” grew into a “unified learning runtime”

> **Subject**: [HKUDS/DeepTutor](https://github.com/HKUDS/DeepTutor) (product name **DeepTutor**)
> **Baseline**: tag **`v1.5.11`** · commit **`456f9c2`** (`release: v1.5.11`) · single source of version truth `deeptutor/__version__.py:9`
> **License**: Apache-2.0 (`LICENSE`)
> **Code size (baseline)**: `deeptutor/` **151,534** lines of Python (excluding top-level `tests/`); `deeptutor_cli/` **5,147** lines; `web/` **108,133** lines of TS/TSX (464 files); whole repo **1,057** `.py` files
> **Product in one sentence**: HKUDS (University of Hong Kong) open-source **agent-native lifelong personalized tutoring workspace**—Chat / Quiz / Research / Visualize / Solve / Mastery share the same agent loop; knowledge bases, books, memory, Partners, and coding-CLI sub-agents all hang on the same context.
> **Audience**: product managers and engineers who have read at least one of this series’ Claude Code / MiroFish / OpenWorker / Reasonix analyses.
> **Narrative**: unfolded in a five-beat **motivation → constraint → rejected alternatives → choice → cost**. First answer “why build an education agent, and what the spine in their head was,” then go into the code; every key design tries to say “what would happen if we didn’t.”
> **Local baseline path**: `参考项目/DeepTutor @ 456f9c2`
> **Star snapshot**: **35,022** star · **4,444** fork (2026-08-12) · paper [arXiv:2604.26962](https://arxiv.org/abs/2604.26962)
> **This edition (second-round re-check)**: correct 3 factual errors (write semantics of `preferences.md`, LoopCapability count and dual identity, several line numbers); fill in 8 previously omitted load-bearing walls: deferred-tools progressive disclosure, 36-provider compatibility layer and DSML fallback, sandbox three-grade isolation, MCP connection model, 101 CLI Apps supply chain, Codex OAuth, replayable turn protocol, multi-user authorization matrix.

---

## Contents

**Part I · Origin: why the developers built this**
1. [Pain and insight: tutoring should not be a pile of isolated tools](#ch1)
2. [Product position and motive evidence: the original words in README / AGENTS / the paper](#ch2)
3. [Repo map and stack: numeric shape and dependency direction](#ch3)

**Part II · Mind map: how one constraint yields a dozen designs**
4. [Developer mind map: from “switch the objective, not the engine” to twelve designs](#ch4)
5. [Three-layer plugin constitution: Tools × Capabilities × LoopCapability](#ch5)
6. [UnifiedContext: the entire truth of one turn](#ch6)

**Part III · Implementation: from constraint to code**
7. [ChatOrchestrator: three entries share one face](#ch7)
8. [Dual-track main loop: Chat’s “no tools means done” × deep capabilities’ Label protocol](#ch8)
9. [Tool mount, layer 1: ToolMountFlags lets context decide the tool surface](#ch9)
10. [Tool mount, layer 2: deferred tools and `load_tools` progressive disclosure](#ch10)
11. [Provider compatibility layer: 36 capability bits · graceful degrade · DSML text-tool fallback](#ch11)
12. [Three-layer Memory: L1 traces → L2 surface → L3 synthesis (including four synthesis modes)](#ch12)
13. [Multi-engine Knowledge: five index engines + treating vault / CLI as a KB](#ch13)
14. [Deterministic skeleton vs model judgment: Mastery / Solve / Obsidian tool-division philosophy](#ch14)
15. [ExploreContext: structurally peel “understand the material” from “answer the question”](#ch15)
16. [Partners: an IM companion on the same brain](#ch16)
17. [Subagent: treat a coding CLI as a consultable knowledge base](#ch17)
18. [Four legs of the tool ecology: MCP · 101 CLI Apps · sandbox three grades · Codex OAuth](#ch18)
19. [Replayable turn protocol and multi-user authorization matrix](#ch19)
20. [Skills and BookEngine: progressively disclosed knowledge and a “living book” compiler](#ch20)

**Part IV · Taste and bounds**
21. [Twelve decisions, five-beat recap](#ch21)
22. [Cross-cut (education / general / coding-agent table)](#ch22)
23. [Honest bounds](#ch23)
24. [Source-tour index and local reproduction](#ch24)

---

# Part I · Origin: why the developers built this

<h2 id="ch1">Chapter 1 · Pain and insight: tutoring should not be a pile of isolated tools</h2>

### 1.1 Answer “why” first

Before reading DeepTutor’s source, read its positioning sentence (`assets/README/README_CN.md:60-62`):

> DeepTutor is an agent-native learning workspace that integrates tutoring, problem-solving, quiz generation, research, visualization, and mastery practice in one extensible system.
> **A unified runtime** — Chat, Quiz, Research, Visualize, Solve, and Mastery Path run on the same agent loop; **you switch the objective, not the engine**, and context always flows with the learner.

This is not “another ChatGPT skin.” The starting point is:

1. **Pain**: existing AI learning products split “chat / quiz / solve / notes / knowledge base” into tools that do not talk to each other; every time the learner switches a mode they drop context; personalization cannot accumulate across tasks.
2. **Insight**: real lifelong tutoring needs not more entries, but **the same agent runtime + one learning context that can flow**.
3. **Corollary**: the product shape must be a “workspace,” not a “single chat window”; the architecture shape must be “pluggable capabilities,” not “each feature writes its own pipeline.”

```mermaid
flowchart LR
    P["Pain: learning tools isolated from each other<br/>switch mode and drop context"] --> I["Insight: tutoring = the same brain<br/>switch the objective, not the engine"]
    I --> C["Constraint: one UnifiedContext<br/>+ three plugin layers (Tool / Capability / LoopCapability)"]
    C --> D["Corollary: multi-engine knowledge · three-layer memory · progressive tool disclosure<br/>Partners · Subagent · Skills · sandbox"]
```

### 1.2 Three verifiable engineering claims

| Claim | What it looks like in code |
|---|---|
| **Agent-native** | CLI / WebSocket / SDK all enter `ChatOrchestrator.handle` (`runtime/orchestrator.py:36-114`); capabilities implement `BaseCapability.run(context, stream)` (`core/capability_protocol.py:33-60`) |
| **Switch the objective, not the engine** | The top-level capability table has only 7 names, all registered into the same `CapabilityRegistry` (`runtime/bootstrap/builtin_capabilities.py:3-11`); another 5 LoopCapabilities hang directly on the chat loop (`capabilities/registry.py:13-19`) |
| **Context flows with the learner** | `UnifiedContext` one turn carries KB / attachments / memory / persona / skills / history (`core/context.py:34-84`) |

### 1.3 It is not the same species as the previous twenty in this series

The vast majority of this series answers “**how to make AI write code / do tasks for me**.” DeepTutor switched the question:

> **How to make AI accompany me in learning for a long time**—the same brain, across chat, quizzes, research, visualization, mastery practice, and it still remembers who I am.

The only thing that matches the “change-of-axis” feeling is MiroFish (crowd simulation). DeepTutor’s axis is the **education lifecycle**, not a coding session, and not a social rehearsal.

---

<h2 id="ch2">Chapter 2 · Product position and motive evidence: the original words in README / AGENTS / the paper</h2>

### 2.1 Motive-evidence table

| Evidence | Source | What it proves |
|---|---|---|
| “Lifelong Personalized Tutoring” | README title | The product spine is lifelong personalization, not one-shot Q&A |
| “One runtime for every mode … you switch the objective, not the engine” | `README.md:195` | Unified runtime is the first selling sentence |
| “a two-layer plugin model — single-shot **Tools** … and multi-stage **Capabilities**” | `AGENTS.md:5-8` | The architectural constitution is written into the manual the Agent reads |
| “All capabilities emit on a shared `StreamBus`” | `AGENTS.md:27-28` | The streaming protocol is cross-cutting infrastructure |
| “Runtime settings live in `data/user/settings/*.json` — project-root `.env` files are intentionally ignored” | `AGENTS.md:28-30` | Config source of truth is unique; refuse `.env` drift |
| “Inspectable memory — L1 traces, L2 surface summaries, and L3 synthesis” | `README.md:200` | Memory must be auditable and editable |
| “consult a live coding CLI … from any turn” | `README.md:197` | The coding Agent is a consultable object, not the host |
| v1.4.0 “every chat capability rebuilt on a single agentic engine” | README Releases | Historical evolution actively converged onto a single engine |
| v1.4.5 “a new loop-plugin framework” | README Releases | The third plugin layer (LoopCapability) is an explicit product milestone |
| Paper arXiv:2604.26962 | README News | There is an academic design statement, not merely an engineering repo |

### 2.2 Product-manager view: what experience it is selling

| Selling point | Engineering landing |
|---|---|
| One workspace covers learn / practice / research / draw | 7 Capabilities + 5 LoopCapabilities + BookEngine in parallel |
| Knowledge base can switch engines | RAG factory five engines (`services/rag/factory.py:216-256`) |
| Personalization is visible and editable | Memory workbench + integer-footnote provenance (`services/memory/document.py:1-30`) |
| Phone WeChat can also ask the teacher | Partners 16 IM channels auto-discover (`partners/channels/registry.py:18-26`) |
| When you need to write code, invite local Claude/Codex | Subagent treats the CLI as a “consultable KB” (`capabilities/subagent/`) |
| Install community skills / install 101 CLI tools | Skills (ClawHub/EduHub) + CLI Apps snapshot (`services/cli_apps/vendor/catalog.json`) |
| Use your own ChatGPT subscription | Codex OAuth per-account login (`services/codex_auth/`) |

> 🧠 **One sentence**: others are optimizing “how smart the tool call is”; DeepTutor is optimizing “**in the learning lifecycle, can context keep following the person**”—and welding that into orchestrator + UnifiedContext + three plugin layers.

---

<h2 id="ch3">Chapter 3 · Repo map and stack: numeric shape and dependency direction</h2>

### 3.1 Top-level shape

```text
DeepTutor/
├── deeptutor/           # Python backend core (151,534 lines)
│   ├── runtime/         # Orchestrator · launcher · registry (incl. deferred_tools) · bootstrap
│   ├── core/            # UnifiedContext · StreamBus · agentic loop · tool/capability protocol
│   ├── agents/          # chat / research / question / visualize / math_animator / notebook / vision_solver
│   ├── capabilities/    # solve · mastery · subagent · obsidian · explore_context (LoopCapability layer)
│   ├── services/        # llm · rag · memory · skill · partners · subagent · mcp · sandbox · cli_apps …
│   ├── partners/        # IM bus + 16 channels
│   ├── book/            # Living Book independent engine (parallel to Orchestrator)
│   ├── knowledge/       # KB management face
│   ├── learning/        # mastery engine (mastery / spaced-repetition arithmetic)
│   ├── multi_user/      # identity · authorization · audit (14 modules)
│   ├── api/             # FastAPI 33 routers + unified WebSocket
│   └── tools/           # builtin tool implementations
├── deeptutor_cli/       # Typer CLI (5,147 lines)
├── web/                 # Next.js 16 frontend (108,133 lines TS/TSX)
├── AGENTS.md / SKILL.md # Agent-native manuals (constitution for the model)
└── pyproject.toml       # package name deeptutor · version 1.5.11
```

Sorted by package bulk (Python, approximate):

| Package | ~lines | Role |
|---|---|---|
| `services/` | 65k | LLM / RAG / Memory / MCP / Skill / Sandbox / Subagent… |
| `agents/` | 17k | pipeline implementations of each Capability |
| `api/` | 16k | HTTP / WS face |
| `partners/` | 12k | IM companions |
| `book/` | 7.7k | living-book compiler |
| `tools/` | 7.6k | builtin tools |
| `learning/` | 4.7k | mastery arithmetic |
| `capabilities/` | 3.8k | LoopCapability layer |
| `core/` + `runtime/` | 7.4k | protocol and orchestration heart |
| `multi_user/` | 2.0k | authorization matrix |

### 3.2 Stack in one sentence

- **Backend**: Python 3.11–3.13 · FastAPI · homemade agentic loop · 36 provider adapters · 5 RAG engines
- **Frontend**: Next.js 16 · React 19; the browser only connects to the frontend origin; `/api/*` and `/ws/*` are server-side forwarded by `web/proxy.ts` (the container only needs to expose 3782)
- **Entries**: `deeptutor` CLI (PyPI) · Web · Python SDK (`DeepTutorApp`) · Docker/GHCR
- **Config**: `data/user/settings/*.json`, **intentionally ignore** project-root `.env` (`AGENTS.md:28-30`)

### 3.3 Dependency direction (the compass when reading code)

```mermaid
flowchart TB
    CLI["deeptutor_cli"] --> ORCH["ChatOrchestrator"]
    WS["api/unified_ws<br/>replayable turn protocol"] --> ORCH
    SDK["app.DeepTutorApp"] --> ORCH
    PART["Partners runtime"] --> ORCH
    ORCH --> CAPREG["CapabilityRegistry<br/>7 top-level capabilities"]
    ORCH --> TOOLREG["ToolRegistry"]
    CAPREG --> CAP["BaseCapability.run"]
    CAP --> LOOP["AgentLoop (chat)<br/>run_agentic_loop (deep)"]
    LOOP --> LCAP["LOOP_CAPABILITIES<br/>mastery/solve/obsidian/subagent/explore"]
    LOOP --> TOOLS["BaseTool + deferred loader"]
    LOOP --> BUS["StreamBus"]
    TOOLS --> SBX["Sandbox three-grade isolation"]
    TOOLS --> MCP["MCP manager"]
    CAP --> CTX["UnifiedContext"]
    CTX --> MEM["MemoryStore L1/L2/L3"]
    CTX --> RAG["RAG pipelines ×5"]
    CTX --> SK["SkillService"]
    BOOK["BookEngine"] -.reuse StreamBus/Registry.-> BUS
    MU["multi_user authorization"] --> CTX
```

BookEngine is **deliberately parallel** to Orchestrator (`book/__init__.py:5-9`): it reuses StreamBus / Registry, but is not registered as a capability—what it produces is a book, not one streaming answer.

---

# Part II · Mind map: how one constraint yields a dozen designs

<h2 id="ch4">Chapter 4 · Developer mind map: from “switch the objective, not the engine” to twelve designs</h2>

```mermaid
mindmap
  root((switch the objective<br/>not the engine))
    Unified entry
      ChatOrchestrator
      StreamBus fanout
      Replayable turn protocol
    Three plugin layers
      L1 Tools single call
      L2 Capabilities take over the turn
      L3 LoopCapability hangs on the chat loop
    Context decides the tool surface
      ToolMountFlags situational flags
      deferred tools + load_tools
      KnowledgeCapability exclusive surface
    Long-term personalization
      L1 JSONL traces
      L2 surface summaries
      L3 cross-surface synthesis
      Four synthesis modes
    Knowledge pluggable
      Five index engines
      Obsidian / Subagent as KB
    Where the model is not trusted
      ExploreContext objective pre-pass
      Deterministic skeleton tools
      Provider capability bits + degrade
    Reach and safety
      Partners 16 IM
      Sandbox three-grade isolation
      Multi-user authorization matrix
```

The twelve designs unpacked one by one later: Orchestrator single entry · three plugin layers · UnifiedContext · Chat “no tools means done” · Label-protocol loop · ToolMountFlags · deferred tools · Provider compatibility and DSML fallback · three-layer Memory · multi-engine knowledge · deterministic skeleton tools · replayable turn + authorization matrix.

---

<h2 id="ch5">Chapter 5 · Three-layer plugin constitution: Tools × Capabilities × LoopCapability</h2>

> ⚠️ **This chapter is a second-round re-check correction focus**: `AGENTS.md` only talks about a “two-layer plugin,” but the code really has a **third layer**—`LOOP_CAPABILITIES`. And mastery / solve are **simultaneously** second-layer and third-layer citizens; this dual identity is the key to understanding the post-v1.4 architecture.

### 5.1 Motive → constraint

If each learning mode wrote its own “search → think → answer” pipeline, modes could not share tools, memory, streaming UI. The constraint is:

> **A single action is a Tool; a multi-stage takeover of a turn is a Capability.** (`AGENTS.md:5-8`, `core/capability_protocol.py:1-8`)

### 5.2 Rejected alternatives

| Rejected | Why rejected |
|---|---|
| Each mode an independent Agent class + independent main loop | tools, memory, streaming protocol would be copied five times |
| Only Tools, no Capability | Deep Research / Math Animator multi-stage pipelines cannot be stuffed into “one function call” |
| Only hardcoded pipelines; the model cannot pick tools | lose agent-native; cannot hang MCP / Skills / CLI apps |

### 5.3 Choice: three layers

**Level 1 — Tools** (`core/tool_protocol.py:206` `BaseTool`). Builtin tools are registered in one `BUILTIN_TOOL_TYPES` table in `tools/builtin/__init__.py:1562-1607`, then split into two visibilities:

| Category | Count | Roster | Source |
|---|---|---|---|
| User-toggleable (`/settings/tools`) | **7** | `brainstorm` `web_search` `paper_search` `reason` `geogebra_analysis` `imagegen` `videogen` | `builtin/__init__.py:1627-1635` |
| Context auto-mount (from the user view “locked on”) | **15** | `rag` `kb_files` `code_execution` `read_source` `read_memory` `write_memory` `read_skill` `list_notebook` `write_note` `web_fetch` `github` `exec` `load_tools` `cron` `ask_user` | `builtin/__init__.py:1647-1663` |
| Capability-owned (mount when the capability is active) | 5+3+9+1 | Mastery 5 / Solve 3 / Obsidian 9 / Subagent 1 | each `capabilities/*/tools.py` |
| Partner-exclusive (forced mount, not configurable) | 3 | `partner_read` `partner_memorize` `partner_search` | `builtin/__init__.py:1601-1607` |

**Level 2 — Capabilities** (`BaseCapability` + `CapabilityManifest`), 7 of them, registered at `runtime/bootstrap/builtin_capabilities.py:3-11`:

| Name | Stages (manifest) |
|---|---|
| `chat` | exploring → responding |
| `mastery_path` | responding (Guided Learning) |
| `deep_solve` | planning → reasoning → writing |
| `deep_question` | ideation → generation |
| `deep_research` | rephrasing → decomposing → researching → reporting |
| `visualize` | analyzing → generating → reviewing |
| `math_animator` | concept_analysis → … → render_output |

**Level 3 — LoopCapability**, 5 of them, registered at `capabilities/registry.py:13-19`:

```python
LOOP_CAPABILITIES: tuple[LoopCapability, ...] = (
    MasteryLoopCapability(),
    SolveLoopCapability(),
    ObsidianCapability(),
    SubagentCapability(),
    ExploreContextCapability(),
)
```

Each round, `active_loop_capabilities(context)` filters the active set in **stable registration order** (`registry.py:22-25`).

### 5.4 Two sharply different plugin semantics

`capabilities/protocol.py` further splits the third layer into two classes; the difference is “**add or replace**”:

| Class | Tool-surface semantics | Members | Source |
|---|---|---|---|
| `LoopCapability` (additive) | reuse the **full** chat tool surface, only add `owned_tools`; **never** cut the user’s composer toggles | Mastery / Solve / ExploreContext | `protocol.py:19-45` |
| `KnowledgeCapability` (replace) | **exclusive** turn: only `owned_tools` + `ask_user` fallback; no chat builtins, no user toggles | Obsidian / Subagent | `protocol.py:87-105` |

Exclusivity is decided by **class membership**, not a per-instance switch: inheriting `KnowledgeCapability` is equivalent to setting `exclusive_tools` (`protocol.py:98-105`). The pipeline uses `any_exclusive_capability_active()` to decide whether to take the exclusive branch, and incidentally suppresses rag scaffolding (`registry.py:28-37`).

### 5.5 Dual identity: why mastery / solve are on both sides

This is the easiest place to misread:

- `mastery_path` / `deep_solve` are in the **Level 2** table—when the user “selects a mode” in the UI they take this path; the capability itself owns the whole turn;
- `MasteryLoopCapability` / `SolveLoopCapability` are in the **Level 3** table—when a mastery path / solve session is active on an **ordinary chat turn**, they only **append a few tools and a system block** to the chat loop.

In other words: after v1.4, a “mode” is no longer a separately started engine, but a **different assembly of the same chat loop**. That is the code meaning of the README sentence “switch the objective, not the engine.”

### 5.6 Cost

- A newcomer has to hold three plugin-layer semantics at once; `AGENTS.md` only wrote two; the third layer you only know by reading `capabilities/registry.py`;
- Exclusive strategy needs an exception rule: when selected alongside a LlamaIndex KB, `rag` / `kb_files` must coexist (issue #650, `tool_composition.py:59-61,132-138`).

---

<h2 id="ch6">Chapter 6 · UnifiedContext: the entire truth of one turn</h2>

### 6.1 Motive

Without a unified context object, CLI args, WebSocket frames, Partner inbound messages would each assemble their own prompt; personalization and permissions would necessarily drift.

### 6.2 Choice: one dataclass flowing through the whole chain

`UnifiedContext` (`core/context.py:34-84`) carries:

| Field | Use | Semantics worth noticing |
|---|---|---|
| `user_message` / `conversation_history` | this turn’s input and history | OpenAI message format |
| `enabled_tools` | user toggles | **`None` (unspecified) ≠ `[]` (explicitly all off)**; the comment writes this dead |
| `allowed_builtin_tools` | builtin auto-mount allowlist | `None` = product chat default unbounded; Partner uses it for subtraction |
| `active_capability` | selected L2 capability | empty falls back to `chat` |
| `knowledge_bases` | RAG / Subagent / Obsidian refs | three heterogeneous objects share one slot |
| `memory_context` / `persona_context` | inject into the system prompt | persona must **take effect from the first token**, hence eager inject |
| `skills_manifest` / `source_manifest` | progressively disclosed skill and attachment lists | only summary lines; full text pulled by tools |
| `attachments` | multimodal attachments | including `extracted_text` (“what the model sees” of office text) |
| `metadata` | extension seam | `turn_id` / `_subagent_state` / `_min_loop_rounds` etc. |

### 6.3 Cost

`metadata` is an intentional “extension-seam trash can”: protocol-stable fields go into the dataclass; experimental fields go into metadata. Flexible, but cross-capability conventions are maintained by comments (e.g. subagent uses `_min_loop_rounds` to raise the loop budget, `capabilities/subagent/capability.py:56-60`).

---

# Part III · Implementation: from constraint to code

<h2 id="ch7">Chapter 7 · ChatOrchestrator: three entries share one face</h2>

### 7.1 Entry convergence

`ChatOrchestrator.handle` (`runtime/orchestrator.py:36-114`):

1. Fill `session_id`
2. Resolve `active_capability or "chat"`
3. Take the implementation from `CapabilityRegistry`; if missing, emit one error event with `turn_terminal` and finish normally (`orchestrator.py:49-67`)
4. Build a `StreamBus`, register it in the global table by `turn_id` (for WS reconnect subscribe), run `capability.run(context, bus)` in the background
5. Externally `async for` fanout `StreamEvent`
6. In `finally` always emit `DONE` and `close()`, unregister the bus
7. After end, emit `CAPABILITY_COMPLETE` on the global EventBus (`orchestrator.py:116-134`)

```mermaid
sequenceDiagram
    participant U as CLI/WS/SDK/Partner
    participant O as ChatOrchestrator
    participant R as CapabilityRegistry
    participant C as Capability
    participant B as StreamBus
    U->>O: handle(UnifiedContext)
    O->>R: get(cap_name)
    O->>B: new StreamBus + register_bus(turn_id)
    O->>C: run(context, bus)
    C-->>B: stage / content / tool / sources
    B-->>U: StreamEvent…
    O->>B: DONE + close + unregister
    O->>O: publish CAPABILITY_COMPLETE
```

### 7.2 Why this is load-bearing

CLI `deeptutor run`, Web unified WS, Partner inbound, SDK calls **are ultimately the same face**. Switch UI, do not switch behavior—exactly what the architecture diagram in `AGENTS.md:12-26` wants to nail.

### 7.3 Cost

Orchestrator is extremely thin (146 lines); complexity sinks into each Capability / Pipeline. The gain is the entry is always readable; the cost is “the real product logic” is scattered in `agents/*` and `capabilities/*`; the reader easily gets lost in the 1,563-line `agentic_pipeline.py`.

---

<h2 id="ch8">Chapter 8 · Dual-track main loop: Chat’s “no tools means done” × deep capabilities’ Label protocol</h2>

DeepTutor’s most tasteful engineering reality: **it did not force unification into one kind of main loop**.

### 8.1 Track A — Chat `AgentLoop`: “this round called no tools = the answer is done”

File: `agents/chat/agent_loop.py` (module comment `1-24`, `AgentLoop` @ `171`, `run` @ `196`).

Rules extremely simple:

- Has tool calls → this round’s text is **narration** (preface of tool work); the loop continues;
- No tool calls → this round’s text **is** the final answer, `finish`;
- Every round’s text is **streamed to the user as it is generated**; at round end, `call_role` (`narration` / `finish`) tells the frontend how to render—**no need to guess mid-stream where the text is going** (`agent_loop.py:20-24`);
- `ask_user` can pause and resume inside the same turn;
- After the exploration budget is exhausted, enter at most **3** settlement rounds (`MAX_SETTLEMENT_ROUNDS = 3` @ `64`); if still not stopping, `_forced_finish` (`agent_loop.py:492`: **turn tools off** to force the model to close). Total cap is `exploration + 4` (the comment at `agent_loop.py:60-63` computes it clearly).

Two real fallback details worth copying away:

1. **Truncation continue-write**: when the provider stops because of a length cap (`finish_reason ∈ {length, max_tokens, max_output_tokens}`, `agent_loop.py:65,73-76`), do not treat it as “answered”; leave the visible prefix in the protocol and continue writing;
2. **Empty-finish nudge**: when finish-round text is empty, first send one nudge for the model to restate, only once (`nudged_empty_finish`, `agent_loop.py:361-386`).

`ChatCapability` is only a thin shell (27 lines); what actually works is `AgenticChatPipeline` (`run` @ `agentic_pipeline.py:326`) + `AgentLoop`.

> Isomorphic with Claude Code / Codex “the model decides whether to stop,” but DeepTutor defines “stop” as **the empty set of tool calls**, not a special `terminate` tool—more natural for tutoring: once it has explained clearly, it should stop.

### 8.2 Track B — `run_agentic_loop`: a Label-driven protocol state machine

File: `core/agentic/loop.py` (`run_agentic_loop` @ `173`).

The model must give a double-backtick-wrapped label on the first line of every round (`core/agentic/labels.py:3-10`). `LabelProtocol` (`loop.py:39-64`) declares five sets: `allowed` / `terminal` / `intermediate` / `final` / `tool_label`.

The most careful design point: **`final` is orthogonal to `terminal`**—a terminating label need not flow body text (e.g. `REPLAN` only bubbles the text to the upper layer); an intermediate label can also flow body text (e.g. chat’s `PAUSE`: narrate to the user without ending the turn) (`loop.py:48-56`).

The loop body is capability-agnostic; capability-related behavior is all delegated to the `LoopHost` Protocol (`loop.py:79-170`): context-window clip, per-round trace metadata, parallel tool dispatch, pause handling, terminate check, protocol-violation repair copy, budget-exhausted forced close. Three of the hooks use `getattr` to probe existence (`before_iteration` / `on_intermediate`); **an old host that does not write them will not break**—a very restrained extension method.

The label parser itself is also anti-fragile (`labels.py:34-95`): tolerate one-/three-backtick variants, zero-width-character prefixes, missing separators, and a “bare label + separator” fallback; if more than 64 characters have not matched, fall to `UNKNOWN`.

Research / Question / Solve take this track—they need **explicit stage labels** (`THINK` / `TOOL` / `FINISH` / `APPEND` / `REPLAN`); they cannot judge stage only by “whether there are tools.”

### 8.3 Motive → choice → cost

| | Chat track | Label track |
|---|---|---|
| **Motive** | conversation wants low latency, WYSIWYG | multi-stage pipelines want auditable, correctable |
| **Choice** | no tools = finish | first-line label state machine |
| **Cost** | two loop mindsets | weak models often write labels badly; need repair messages fed back |

The repo did not force-merge for “architecture looks pretty”—the tutoring main path wants smoothness; the deep-research path wants strictness.

---

<h2 id="ch9">Chapter 9 · Tool mount, layer 1: ToolMountFlags lets context decide the tool surface</h2>

### 9.1 The problem

Stuffing the full tool schema to the model every round: costs tokens, induces the model to call randomly, and makes Partner scenes unable to do permission subtraction.

### 9.2 Choice

`ToolMountFlags` + `compose_enabled_tools` (`agents/_shared/tool_composition.py:82-99` and `101-160`). The conditional-mount table is the **single source of truth** (`_CONDITIONAL_MOUNT_FLAGS`, `tool_composition.py:41-57`):

| Flag | Mounts |
|---|---|
| `has_kb` | `rag`, `kb_files` |
| `has_sources` | `read_source` |
| `has_memory` | `read_memory` |
| `has_notebooks` | `list_notebook`, `write_note` |
| `has_skills` | `read_skill` |
| `has_deferred_tools` | `load_tools` |
| `has_exec` / `has_code` | `exec`, `code_execution` |

Assembly order (`tool_composition.py:117-127`): ① user toggles (filtered by `optional_whitelist`) → ② conditional auto-mount → ③ activated capability’s owned tools → ④ always-on tools (`write_memory` / `web_fetch` / `github` / `ask_user` / `cron`).

Then stack three knobs Partner uses (`tool_composition.py:140-155`): `builtin_whitelist` (**only subtract, never add**), `forced` (unconditional append, bypass allowlist and situational gate), `suppressed` (final remove). `AUTO_MOUNTED_TOOLS` is derived directly from `CONFIGURABLE_BUILTIN_TOOL_NAMES` (`tool_composition.py:39`), so “what the settings page shows” and “what the pipeline mounts” cannot drift.

### 9.3 Contrast with this series

| Project | Tool-visibility strategy |
|---|---|
| Claude Code | permissions + prefix-cache-friendly ordering |
| Hermes | 74 tools extreme pluginization |
| Reasonix | `use_capability` single proxy isolates dynamic MCP, keeps prefix stable |
| Open Design | adapter is data; load CLI on demand |
| **DeepTutor** | **situational-flag auto-mount + user toggles + capability exclusive (then stack deferred layer 2)** |

DeepTutor’s special point: the tool surface is a **function of the learning situation** (whether there is a KB / notes / skills / sandbox), not a pure permission function.

---

<h2 id="ch10">Chapter 10 · Tool mount, layer 2: deferred tools and `load_tools` progressive disclosure</h2>

> 🆕 **This chapter is a second-round re-check fill-in**—the previous edition omitted this entirely, and it happens to be the design that talks most directly with Reasonix `use_capability` and Claude Code “keep the tool table stable.”

### 10.1 Motive and measured reason

Even with situational flags, MCP servers + 101 CLI Apps will explode the schema surface. `runtime/registry/deferred_tools.py:1-16` writes the motive bluntly:

> Keep the always-on schema surface small; **this measurably improves tool selection on weak models**, while every already-connected tool is only one cheap call away.

### 10.2 Mechanism

1. Tools marked `BaseTool.deferred` (**by default includes all MCP tools**) do not enter this round’s initial tool table;
2. The system prompt only carries **one summary line** (`render_deferred_tools_manifest`);
3. When the model needs them, builtin `load_tools` reports exact names;
4. `DeferredToolLoader` **appends** the full schema into the live `tool_schemas` list—`run_agentic_loop` rereads that list every round, so the tool is **immediately callable**;
5. Loaded names are **persisted by chat session**; subsequent turns carry them from the start.

CLI App tools get special handling: each App is its own provider and has only one tool; writing a small heading per provider would waste tokens, so they share one `("cli","")` group and write the provider id inline (`deferred_tools.py:36-40`).

```mermaid
flowchart LR
    A["MCP / CLI App tools<br/>deferred=True"] --> B["system prompt: one summary line"]
    B --> C{model judges it needs them?}
    C -- no --> D["schema surface stays small<br/>weak models select more accurately"]
    C -- yes --> E["load_tools(exact names)"]
    E --> F["schema appended into the live list"]
    F --> G["callable next round"]
    G --> H["names persisted by session"]
```

### 10.3 Five-beat

| Beat | Content |
|---|---|
| Motive | the larger the ecology, the more poisonous the always-on schema surface |
| Constraint | tools must be reachable inside “one cheap call”; they cannot actually be unavailable |
| Rejected | stuff all (weak models pick wrong) / simply not support MCP (lose the ecology) |
| Choice | one-line manifest + `load_tools` + live schema list + session memory |
| Cost | one extra round-trip; the model must first “know what it needs”; manifest descriptions must be length-capped (`MANIFEST_DESCRIPTION_MAX_CHARS`) |

---

<h2 id="ch11">Chapter 11 · Provider compatibility layer: 36 capability bits · graceful degrade · DSML text-tool fallback</h2>

> 🆕 **This chapter is a second-round re-check fill-in**.

### 11.1 36 providers are a “capability-bit” table

The `PROVIDERS` tuple in `services/provider_registry.py:118` has **36** `ProviderSpec`s, of which **11 are gateways** (gateway first; order is match priority, `provider_registry.py:7,115-118`):

| Category | Members |
|---|---|
| Gateway / compatibility layer (11) | `custom` `custom_anthropic` `azure_openai` `openrouter` `edenai` `aihubmix` `siliconflow` `novita` `atlascloud` `volcengine` `byteplus` (including two coding-plan variants) |
| First-party | `anthropic` `openai` `openai_codex` `github_copilot` `deepseek` `gemini` `zhipu` `dashscope` `moonshot` `minimax`(+`minimax_anthropic`) `mistral` `stepfun` `xiaomi_mimo` `groq` `qianfan` |
| Local | `vllm` `ollama` `lm_studio` `llama_cpp` `lemonade` `ovms` `nvidia_nim` |

The key is the fields of `ProviderSpec` (`provider_registry.py:19-56`)—it is not “name + URL,” but **per-vendor capability bits**: `supports_prompt_caching` / `supports_stream_options` / `supports_max_completion_tokens` / `strip_model_prefix` / `is_oauth` / `is_local` / `thinking_style` / `reasoning_model_patterns` / `model_overrides`. Adding one provider = adding one spec; “env vars, config match, status display are all derived from this” (`provider_registry.py:3-5`).

There is also an alias table `PROVIDER_ALIASES` (`:77`) that normalizes `azure` / `google` / `claude` / `openai-compatible` / `github-copilot` / `lm-studio` / `atlas` … to canonical names—whatever the user fills in can land.

### 11.2 Graceful degrade: error-classifier-driven retry

`services/llm/request_compat.py:1` titles it in one sentence: “**provider-error classifiers used by retry and graceful-degradation paths**.” It lowercases the provider’s message body then does substring match, judging three kinds of unsupported:

- `is_stream_options_unsupported` (`stream_options` / `unknown parameter` / `extra inputs are not permitted` …)
- `is_tool_schema_unsupported` (→ trigger DSML fallback, next section)
- `is_image_input_unsupported` (→ multimodal degrade; `llm/multimodal.py:327` comment says degrade must **persist across iterations**, otherwise images would be resent repeatedly)

This “first send optimistically, if refused then precisely degrade and remember” ages better than pre-enumerating every vendor’s quirks.

### 11.3 DSML: when the provider will not do native tool calling

`agents/chat/dsml_tool_calls.py:1-22` is the most concrete “fallback” case I have seen in this series:

Some DeepSeek deployments (especially local / source-started OpenAI-compatible endpoints) **do not advertise function calling**, so they write tool calls as markup into the **content channel**, of the form:

```text
<｜｜DSML｜｜tool_calls>
  <｜｜DSML｜｜invoke name="exec">
    <｜｜DSML｜｜parameter name="command" string="true">python -c "..."</｜｜DSML｜｜parameter>
  </｜｜DSML｜｜invoke>
</｜｜DSML｜｜tool_calls>
```

**What happens if you do not parse**: this markup streams to the user as the final answer, and the tool **never ran** (the repo hung issue #666 directly).

The answer is restrained: `extract_dsml_tool_calls` is a **pure function** (no I/O, no LLM); tag-prefix matching is loose (`<[^>]*?invoke\s+name="`); it only depends on the stable `invoke name` / `parameter name` structure—**does not bet on the exact bytes of those full-width special tokens**. Meanwhile `DSMLStreamFilter` (`dsml_tool_calls.py:62`) at the streaming stage filters the markup out of the user-visible channel, while the original text is still fed back to the model.

`InlineThinkFilter` (`agent_loop.py:83-95`) is its sister design, solving the other half of the same class of problem: some providers write reasoning in content wrapped in `<think>`, so split **in place while streaming**—“user-visible content” is **fixed once, clean everywhere** downstream (live bubble, persisted messages, the loop’s finish judgment), while the tagged original is still fed back to the model.

### 11.4 Cost

String-matching provider messages is inherently fragile (change the copy and you miss); the capability-bit table has to be maintained by hand; DSML only covers DeepSeek’s dialect; if another vendor invents a new dialect you have to add another branch.

---

<h2 id="ch12">Chapter 12 · Three-layer Memory: L1 traces → L2 surface → L3 synthesis (including four synthesis modes)</h2>

### 12.1 Layout

`services/memory/paths.py:1-8`:

```text
trace/<surface>/<YYYY-MM-DD>.jsonl        # L1 append-write
L2/<surface>.md                           # L2 per-surface summary
L3/{recent,profile,scope,preferences}.md  # L3 cross-surface
backup/<timestamp>/...                    # v1 migration archive
```

Seven surfaces (`paths.py:47-56`): `chat` `notebook` `quiz` `kb` `book` `partner` `cowriter`.

### 12.2 Facade

`MemoryStore` (`store.py:68`) is a **stateless** facade (process-level singleton-safe), write-locked by path:

- `emit` → L1
- `update_l2` / `update_l3` → consolidator
- `read_l3_concat` → for the `read_memory` tool (when there is no content, return an explicit hint string rather than empty, `store.py:53-55`)
- `overwrite_doc` / `delete_entry` → humans edit directly in the workbench
- `apply_ops_payload` → “preview → apply” two-step flow (`store.py:165-193`)

### 12.3 ⚠️ Correction: who can actually write `preferences.md`

**The previous edition was wrong**: it said “preferences must be human-editable; the model cannot secretly rewrite.” After checking source, the accurate statement is:

1. `preferences.md` **does not enter auto-synthesis**—`update_l3` explicitly refuses (`store.py:153-154`: `raise ValueError("preferences.md is not auto-consolidated")`);
2. But the model **can write**, only through a narrow gate: `write_preference` (`store.py:194-250`); the comment states “**the `write_memory` tool is the only caller**; `trace_id` is injected by the runtime”;
3. This narrow gate comes with two protections: `op` is only `add` / `edit`; `add` is **idempotent**—duplicate content is not appended again, but reports the already-existing entry (`detail="duplicate"`). The reason is in the comment: Guided Learning turns are highly tool-driven and long; the model will re-send the same `write_memory` across turns (issue #647), and `preferences.md` has no auto-synthesis to clean duplicates.

So the real design intent is not “do not let the model write,” but “**let the model write only through a narrow gate with provenance and idempotent protection**,” and this class of conclusions is never rewritten by auto-summary.

### 12.4 The precise shape of footnote provenance

L2/L3 is not free prose, but Markdown with strict invariants (`document.py:1-30`):

```markdown
## <section>
- <text> [^1][^2] <!--m_xxx-->

---

[^1]: notebook:abc
[^2]: chat:def
```

Three details easy to miss:

- Footnote labels are **integers**, assigned by first-appearance order in the full-text bullet stream; two citations of the same source **share** a label, so the rendered view has no duplicate footnote lines;
- Entry ids hide in an HTML comment `<!--m_xxx-->` at the bullet tail; they survive round-trip, for audit / dedup line views and `DELETE /entry/{id}`;
- The parser **also** accepts the old format (`[^m_xxx]: ref1, ref2`); next save migrates—existing documents will not become unreadable on upgrade.

### 12.5 Four user-visible synthesis modes

The previous edition only said “LLM consolidator.” There are actually four (`consolidator/modes/__init__.py:1-10`):

| Mode | What it does | Uses LLM? |
|---|---|---|
| `update` | chunked incremental extract of new facts | yes |
| `audit` | line-level edit against original evidence (first render a numbered, footnote-stripped view) | yes |
| `dedup` | full-text line-level iterative dedup | yes |
| `merge` | merge duplicate footnote citations | **no** (purely deterministic) |

Concurrency cap also has a clear contract: the same `(layer, key)` at most one **active** run; a second throws `RunBusyError` (`consolidator/runs.py:17,174-175`).

### 12.6 Partner memory’s privilege-crossing seam

Partner runtime can use `memory_path_service_override` (`paths.py:27-44`) so `read_memory` / `write_memory` read the **owner’s** memory, while **other services** (rag / skills / notebooks) stay in the partner scope. Without this, the IM companion would forever “not know you.” This is fine work at the intersection of multi-tenant × persona companionship.

### 12.7 Five-beat

| Beat | Content |
|---|---|
| Motive | personalization must be inspectable, cannot only hide in a vector store |
| Constraint | raw events cannot be lost; summaries are editable; every conclusion is traceable |
| Rejected | a single `PROFILE.md` (v1 already abandoned; see `_V1_FILES` in `store.py:31`) / pure vector memory |
| Choice | L1 JSONL + L2/L3 integer-footnote Markdown + four-mode consolidator + narrow-gate preference write |
| Cost | synthesis costs model calls; three-layer UI has cognitive cost; summary correctness depends on the model |

---

<h2 id="ch13">Chapter 13 · Multi-engine Knowledge: five index engines + treating vault / CLI as a KB</h2>

### 13.1 Five engines

Engine roster in `services/rag/factory.py:216-256`:

| Engine | Shape | Note |
|---|---|---|
| **LlamaIndex** | local vectors, default | the only engine that uses DeepTutor’s active embedding signature to select/read a **versioned index** (`factory.py:67-68`) |
| **PageIndex** | cloud, **no vectors** | the model reads documents through PageIndex’s **MCP tools** (`factory.py:227-228`); because the index is in the cloud, **intentionally not linkable** (`linked_kb.py:15,133-134`) |
| **GraphRAG** | graph + vectors | outputs parquet tables; the probe checks core tables exist (`index_probe.py:200`) |
| **LightRAG** | graph + vectors, multimodal | HKUDS’s own |
| **LightRAG Server** | external HTTP | no local index; naive/local/global/hybrid/mix five query modes |

Supporting trio: unified `RAGPipeline` Protocol (`pipelines/base.py:17-37`), per-engine `engine_preflight` (`preflight.py:195`, **never throws**), `embedding_signature` (`:43-47` explicitly says a graph-engine mismatch will **fail silently**, so it needs its own stamp).

### 13.2 “Knowledge base” is a product-semantics slot, not a data structure

A selected KB can be three heterogeneous objects:

| Type | Behavior |
|---|---|
| Ordinary RAG KB | mount `rag` / `kb_files` |
| Obsidian vault | `KnowledgeCapability` exclusive 9 vault tools |
| Connected Subagent | exclusive `consult_subagent` (Chapter 17) |

Putting “materials” and “a consultable expert process” into **the same slot** is the key product decision that distinguishes DeepTutor from a “RAG chatbot.”

---

<h2 id="ch14">Chapter 14 · Deterministic skeleton vs model judgment: Mastery / Solve / Obsidian tool-division philosophy</h2>

> 🆕 **This chapter is a second-round re-check fill-in**. These `tools.py` module comments hide DeepTutor’s clearest engineering-philosophy sentence; the previous edition’s one-sentence pass was too cheap.

### 14.1 The same sentence, said three times

| File | Original words | Tool count |
|---|---|---|
| `capabilities/mastery/tools.py:1-9` | “**the chat agent loop IS the tutor**; these tools let it read gates and record results, while pedagogy—what to teach, how to ask, when to explain—**is still the model’s job**; arithmetic (mastery, gates, spaced repetition) stays in the engine” | 5 |
| `capabilities/solve/tools.py:1-8` | “**the chat agent loop IS the solver**; three tools give it a deterministic spine—a plan it committed to, step-by-step done gates, bounded replan—while **how to solve** is left to the model in the loop using shared builtins” | 3 |
| `capabilities/obsidian/tools.py:1-8` | nine tools are all **thin wraps** of pure `vault` operations: six reads (nav links / search / read note / list tags) three writes (create / append / set property) | 9 |

### 14.2 Why this is a copy-worthy dividing line

Most Agent projects swing between “let the model improvise” and “hardcode the pipeline.” DeepTutor gives an operable cut:

- **The deterministic part sinks into tools**: how mastery scores are computed, when spaced repetition is due, which plan step counts as done, how many replans remain—these are **arithmetic and a state machine**; the model cannot compute them accurately and should not;
- **The judgment part is left to the loop**: what to teach, how to ask, which step to solve first, whether to look up materials—these are **pedagogy and reasoning**.

Thus `learning/` (4.7k-line mastery engine) is a pure arithmetic module; `mastery/tools.py` is only the **seam** between it and the chat loop.

### 14.3 Contrast with this series

| Project | Same-class practice |
|---|---|
| MiMo Code | Goal independent judge + four dead-loop gates (do not trust the model’s self-report of done) |
| Reasonix | Delivery Profile evidence sign-off + readiness gate |
| grok-build | Goal Mode five-piece kit |
| **DeepTutor** | **mastery gates / plan-done gates / bounded replan sink into tools; pedagogy left to the model** |

The commonality is the same sentence: **whatever can be computed accurately, do not let the model free-heart-certify.**

---

<h2 id="ch15">Chapter 15 · ExploreContext: structurally peel “understand the material” from “answer the question”</h2>

> 🆕 **This chapter is a second-round re-check fill-in**. This is the smallest LoopCapability in the whole repo, and the one that most illustrates taste.

### 15.1 Two very concrete failure observations

`capabilities/explore_context/capability.py:1-30` writes the motive as two bug-level observations:

1. **Persona contamination**: the chat loop melts “understand the attachment” and “answer the user” into one loop. When the attachment is **a conversation log between the user and another AI**, the model in the same context reads those `## Assistant` turns and then **speaks in that agent’s first-person voice**. Splitting understanding into an objective (third-person) pre-investigation **structurally** kills this confusion.
2. **Weak models simply do not read**: under native tool calling, weak models often **never actively call `read_source`**. So let a dedicated pre-pass own the read right, **and completely remove that tool from the answer loop**—the investigation is forced to happen in front, rather than being skipped.

### 15.2 Mechanism

- Activation condition: this turn carries any readable (non-image) attachment source—documents, notebook entries, book chapters, quiz-bank entries, or **cited session history** (the motive case);
- Uses the optional `pre_loop` hook (`capabilities/protocol.py:34-53`), runs **before the answer loop’s first LLM call**;
- It itself is a small agentic loop (from `explore_context/explorer.py:149`), using `read_source` to read what should be read;
- The produced objective investigation **folds into the loop’s user-message seed** (alongside KB seeds, `agent_loop.py:205-217`);
- It **does not own any of the answer loop’s tools**, **does not contribute a system block**—almost invisible.

```mermaid
flowchart LR
    A["turn carries readable attachments<br/>(especially: another AI’s conversation log)"] --> B["ExploreContext pre_loop<br/>read-only · objective · third person"]
    B --> C["investigation conclusion folds into the user-message seed"]
    C --> D["answer loop: no longer has read_source<br/>and will not contaminate into another agent"]
```

### 15.3 Five-beat

| Beat | Content |
|---|---|
| Motive | in the same loop, “read materials” contaminates the persona and execution of “answer the question” |
| Constraint | understanding must be objective, third-person, and **must** happen |
| Rejected | rely on the prompt to tell the model “please read the attachment first, do not imitate the assistant in the conversation” |
| Choice | independent read-only pre-pass + remove `read_source` from the answer loop |
| Cost | one extra LLM round-trip (cost folded into the turn’s `usage`); if the pre-conclusion runs off, it takes the whole round with it |

---

<h2 id="ch16">Chapter 16 · Partners: an IM companion on the same brain</h2>

### 16.1 What it is

Partners (formerly TutorBot, renamed in v1.4.3 and put on a “production-grade IM pipeline”) = a persistent companion with a Soul persona, hanging IM channels, running on **the same chat loop**.

### 16.2 Channel discovery: zero-import scan + diagnosable failure

`partners/channels/registry.py`:

- `discover_channel_names()` uses `pkgutil` to scan the package, **zero import**, list builtin channel names (`:18-26`), excluding `base` / `manager` / `registry`;
- `load_channel_class()` imports the module and takes the first `BaseChannel` subclass (`:29-38`);
- External plugins go through `entry_points(group="deeptutor.partners.channels")`; **builtin names take priority; plugins cannot shadow builtins** (`:55-61`);
- `discover_all_with_errors()` additionally returns an `errors` dict—**“why isn’t X in the UI” is diagnosable**, rather than silently dropping a channel (`:63-78`).

The baseline has **16** builtin channel modules: `telegram` `discord` `slack` `feishu` `wecom` `weixin` `dingtalk` `qq` `napcat` `mochat` `whatsapp` `matrix` `mattermost` `msteams` `zulip` `email`.

### 16.3 Relation to the main product

A Partner turn still constructs `UnifiedContext` and goes through Orchestrator, but three places are tightened:

- `allowed_builtin_tools` does subtraction (the owner can ban some builtins);
- Force-mount the `PARTNER_BUILTIN_TOOL_NAMES` three-piece and **suppress** chat’s `read_memory` / `write_memory` (`agentic_pipeline.py:64,624-625`)—these three are **forced, not owner-configurable** (`builtin/__init__.py:1601-1607`);
- Memory path can override to the owner scope (section 12.6).

### 16.4 Cost

Many optional deps (`pip install deeptutor[partners]`; Matrix E2EE also needs libolm); each channel’s Markdown / streaming / media semantics have to be ground separately (Telegram has to do Markdown→HTML itself, `telegram.py:110`). Product breadth is exchanged for ops and test-matrix bloat.

---

<h2 id="ch17">Chapter 17 · Subagent: treat a coding CLI as a consultable knowledge base</h2>

### 17.1 Reverse the host relationship

In this series Open Design “treats others’ CLIs as the engine”; DeepTutor plugs in differently:

> **The learning Agent is the host; Claude Code / Codex / Gemini / Kimi / opencode / MiMo are consulted sub-agents, and the entry is the “knowledge-base slot.”**

`SubagentCapability` (`capabilities/subagent/capability.py:1-13,32-44`):

- Activates when the selected KB is a connected subagent (`is_active` → `connection_for_turn(context) is not None`);
- As a `KnowledgeCapability` **exclusive** turn: only `consult_subagent` + `ask_user` fallback;
- Connection info (which backend, working directory), per-backend config, in-turn budget and session are **all server-injected**; “**the model never provides them**”—prevent privilege-crossing random working directories;
- Uses `_min_loop_rounds = budget + 2` to raise the loop budget, guaranteeing there are still rounds to write the answer after the last consult (`capability.py:25-29,56-60`).

### 17.2 Backend registry and cross-turn continuation

`services/subagent/registry.py:24-35` seven backends: `ClaudeCode` `Codex` `Gemini` `Kimi` `Opencode` `Mimo` `Partner`. The `local_cli` flag separates “local probe” from “connect from one’s own list”—`detect_all()` only probes CLIs, and **one-by-one failure does not affect the whole** (exceptions degrade to `available=False` + detail, `registry.py:51-77`).

Cross-turn, the session registry continues **the same** local agent session (`capability.py:88-95`: on first consult, seed session id from the cross-turn registry), so sidebar and in-chat consults share context.

Each `consult_subagent` call hands one question to the local CLI, and **forwards its native event stream out through the dispatcher’s `event_sink`**, so the sidebar can see that agent’s real intermediate steps (`capabilities/subagent/tools.py:1-8`). Multimodal images are only forwarded when the user explicitly turned on `forward_images` for that backend (`capability.py:96-100`).

### 17.3 Why this deserves “signature move”

The coding-agent series teaches “how to build an engine”; DeepTutor demonstrates “**how an education product treats an already-there engine as a tool**”—isomorphic with Open Design’s socket philosophy, but the socket is plugged into the **knowledge-base slot**, not a step of a design pipeline.

---

<h2 id="ch18">Chapter 18 · Four legs of the tool ecology: MCP · 101 CLI Apps · sandbox three grades · Codex OAuth</h2>

> 🆕 **This chapter is a second-round re-check fill-in**—the previous edition compressed these four things into two rows of a table; actually each has a design worth telling separately.

### 18.1 MCP: one dedicated connection task per server

`services/mcp/manager.py:1-25` tells a pit many people have stepped in: DeepTutor’s chat is a **per-turn task in the same event loop**, while an MCP session must be opened and closed **inside the same task** (the SDK’s anyio cancel scope is task-bound). So each server gets a **dedicated connection task**, end-to-end holding its own `AsyncExitStack`:

```text
connect → enter transport/session inside that task → publish adapter →
wait for shutdown event → exit stack inside the same task
```

`ensure_started()` is **lazy** (the first turn pays the connection cost, with a per-server timeout cap), then cheap; `reload()` diffs the persisted config. The module also has `oauth.py` / `secrets.py` (credentials moved out of sandbox-reachable range, v1.5.7) / `catalog/` / `session_state.py` / `pageindex_server.py`—**the PageIndex engine itself is exposed to the model through MCP**.

MCP tools default `deferred` (Chapter 10), so connecting a pile of servers will not explode the schema surface.

### 18.2 CLI Apps: 101, pinned to one reviewed commit

`services/cli_apps/`:

- The catalog is a **shipped-with-the-package snapshot** `vendor/catalog.json` (`catalog.py:31`), not fetched from the net at runtime—“one upstream failure should not take the whole catalog down” (`catalog.py:3-9`);
- Baseline snapshot: **101 apps**, from `HKUDS/CLI-Anything`, `commit = bc536c9` (2026-07-09), aggregating two registries (`registry.json` + `public_registry.json`);
- If the snapshot has **no pinned commit, refuse to offer install** (`catalog.py:57`: `refusing to offer installs`)—supply-chain discipline written into code;
- Each app is its own provider and has only one tool, hence shares the `cli` group in the deferred manifest (section 10.2).

### 18.3 Sandbox: three backends, two isolation levels, one policy gate

`services/sandbox/backends.py:1-16` three backends, each **honestly reporting** the isolation level it actually provides:

| Backend | Isolation level | Scene |
|---|---|---|
| `RunnerSidecarBackend` | **SYSTEM** | submit to an independent runner container (HTTP)—the correct answer for Docker deploy: the main app keeps minimum privilege, **never executes untrusted shell** |
| `BwrapBackend` | **SYSTEM** | Linux bare-metal `bwrap` mount namespace |
| `RestrictedSubprocessBackend` | **APPLICATION** | ordinary subprocess with env cleaned, cwd restricted; the **degrade** fallback for local development (e.g. macOS), **admin-only opt-in, because it does no OS isolation** |

`services/sandbox/service.py:1-13` turns these three grades into product policy:

- `exec_capability_available()` → decides a skill’s `requires.sandbox` gate;
- `isolation_level()` → **policy gate: SYSTEM open to everyone, APPLICATION admin-only**;
- `run()` → execute, constrained by **per-user quota** (`UserExecQuota` / `QuotaExceeded` in `quota.py`).

Contrast with this series: Codex is Seatbelt/Landlock/Windows Token three-platform OS fences; nanobot uses bwrap; DeepTutor’s distinctive move is **turning “isolation strength” into a queryable return value and using it for permission grading**—rather than pretending every deploy is equally safe.

### 18.4 Codex OAuth: use your own ChatGPT subscription

`services/codex_auth/` (`service.py:1` “Codex OAuth orchestration and managed model-catalog integration”) implements the capability from v1.5.5: log into Codex with a ChatGPT plan; from v1.5.10 **each account logs into its own Codex**; from v1.5.6 supports remote login behind an SSH tunnel. The corresponding provider side is `openai_codex` (`is_oauth=True`, section 11.1).

Product meaning: the learner need not buy another API quota—this kind of “plug into the user’s already-there subscription” design is especially important in education scenes.

---

<h2 id="ch19">Chapter 19 · Replayable turn protocol and multi-user authorization matrix</h2>

> 🆕 **This chapter is a second-round re-check fill-in**. The previous edition only wrote four characters “unified WS”; actually it is a complete turn-lifecycle protocol.

### 19.1 One endpoint, eleven message types

`api/routers/unified_ws.py:1-30`: a single `/api/v1/ws`, for “**turn-based execution and replayable streaming**.” Client message types:

| Type | Role |
|---|---|
| `message` / `start_turn` | start a new turn |
| `subscribe_turn` | subscribe to an existing turn’s events, **with `after_seq`** (replay) |
| `subscribe_session` | subscribe to a session’s currently active turn |
| `resume_from` | **after disconnect, pick up an in-flight turn** |
| `unsubscribe` | unsubscribe |
| `cancel_turn` | cancel |
| `submit_user_reply` | hand the user’s answer to a turn paused on `ask_user`, **resume the loop inside the same turn** |
| `user_input` | throw a learner answer onto StreamBus (unlock `wait_for_input`) |
| `regenerate` | rerun the last user message as a **brand-new turn**, replace the trailing assistant message, reuse session-stored capability/tools/preferences; may carry `overrides` (capability / tools / KB / language / config / notebook & history refs); error codes `regenerate_busy` / `nothing_to_regenerate` |
| `check_active_turn` | report whether there is a live turn; **and mark stale “running” persisted rows as cancelled** |

Three things carry “restart-safe”: events carry sequence numbers and can replay `after_seq`; the bus is registered by `turn_id` and can be re-subscribed (section 7.1); stale state self-heals. README v1.4.0’s “restart-safe turn runtime” lands here.

### 19.2 pause / resume is first-class

`ask_user` is not “end this round, wait for the next”; it is **same-turn pause**: the loop stops in place; after `submit_user_reply` comes back it continues with an `ask_user_resolved` instruction (`agentic_pipeline.py:873-911`), and flattens the answer into a tool message back into the conversation. For a tutoring product this is a necessity—“the teacher asks you one question, you answer, class continues” should not drop context.

### 19.3 Multi-user: a 14-module authorization matrix

`multi_user/` is not merely “optional multi-user”; it is a per-resource authorization matrix:

| Module | What it governs |
|---|---|
| `identity.py` / `context.py` / `paths.py` | identity, request context, per-user workspace paths |
| `grants.py` | grant documents (v2 structure: `models.llm` / `knowledge_bases` / `skills` / partners…) |
| `model_access.py` / `personal_models.py` | which models can be used / bring-your-own models |
| `knowledge_access.py` / `skill_access.py` / `tool_access.py` / `partner_access.py` | per-class resource visibility and usability |
| `audit.py` | audit |
| `router.py` | admin-face API |

Partner authorization semantics are written clearly: Partner stays **admin-managed** (CRUD routes admin-gated); a grant only lets the user **see and consult** a specified partner (`grants.py:22-28`). Additionally from v1.4.10 non-admin MCP tools are **denied by default**.

### 19.4 Cost

The protocol surface is large (11 message types + 33 REST routers); frontend and backend have to maintain semantics together; multi-user is an optional path; personal-workspace mindset and multi-tenant mindset coexist; when reading code you must constantly ask “whose PathService is this running on.”

---

<h2 id="ch20">Chapter 20 · Skills and BookEngine: progressively disclosed knowledge and a “living book” compiler</h2>

### 20.1 Skills: progressive disclosure + two-layer shadow

`services/skill/service.py:8-20` states two things clearly:

1. **Progressive disclosure**: skills **never stuff the whole file into the system prompt**—the prompt only has a “one skill, one line” manifest; when the model matches a task it uses `read_skill` to take the full text; the exception is skills with `always: true` in frontmatter, whose body is **eager-injected** (for “house rules” that must take effect every turn);
2. **Two layers, user shadows builtin**: `deeptutor/skills/builtin/` ships with the product, runtime read-only; `data/user/workspace/skills/` is created by API. Same name, user layer shadows builtin.

The skill-pack structure is `SKILL.md` + optional `references/` (when the execution sandbox is ready there will also be `scripts/`); `read_skill` has a hard cap to prevent huge reference files flooding context (`service.py:74`), and also path-escape checks (`service.py:182`). Community skills can be installed from ClawHub (v1.4.4, with a safety gate).

> Contrast: Claude Code’s skills, Open Design’s 20-layer prompt banding, Reasonix’s stable prefix—all three are doing the same thing: **move “maybe useful” knowledge out of always-on context**. DeepTutor did this twice on this line (skills and deferred tools).

### 20.2 BookEngine: a parallel second runtime

`book/__init__.py:5-9`: an independent runtime that **compiles** chat history / notes / knowledge bases / intent into a structured, chunked, interactive “living book”; **parallel to** `ChatOrchestrator`, reusing `ToolRegistry` / `CapabilityRegistry` / `StreamBus`.

Stages: Ideation → SourceExplorer → Spine (`spine_synthesizer` has already replaced the old `SpineAgent`) → PagePlanner → `BookCompiler`. The model layer is Pydantic `Book` / `Spine` / `Chapter` / `Page` / `Block` / `Progress` (`book/models.py`); the compiler is **deliberately isolated from the orchestrator** (`compiler.py:16`).

Why not make it a Capability? Because its product is **an asset that can be reread, with progress**, not one streaming answer—lifecycle, storage (`book/storage.py`), failure recovery are all different.

### 20.3 Other load-bearing walls (one-sentence file)

| Module | Role |
|---|---|
| `co_writer/` | multi-document collaborative writing |
| `agents/math_animator/` | Manim video pipeline (optional extra, needs LaTeX/ffmpeg) |
| `agents/notebook/` · `services/notebook/` | notebooks and quiz-bank flow-back |
| `services/persona/` | Soul / persona templates |
| `services/cron/` | scheduled tasks (`cron` tool) |
| `services/imagegen` · `videogen` · `voice` | image / video / voice generation |
| `services/parsing/` | pluggable document parse (MinerU / PyMuPDF4LLM / Docling…) |
| `agents/chat/context_budget.py:1-12` | return to the UI the window occupancy of **the request that was actually sent**; “re-deriving would make the reading drift from what was actually sent,” and this reading **never throws** |
| `core/trace.py` · `logging/` | per-round trace cards and stats |
| `i18n/` + `prompts/{en,zh}/*.yaml` | status copy and prompts bilingual-externalized |

---

# Part IV · Taste and bounds

<h2 id="ch21">Chapter 21 · Twelve decisions, five-beat recap</h2>

| # | Decision | Motive | Rejected | Choice | Main cost |
|---|---|---|---|---|---|
| 1 | Unified entry | three-end behavior consistent | each end writes its own loop | `ChatOrchestrator` (146 lines) | logic sinks into each pipeline |
| 2 | Three plugin layers | single action / take over turn / assemble loop | only two layers or all pipelines | Tool + Capability + LoopCapability | three semantics; docs only wrote two |
| 3 | UnifiedContext | prevent context drift | per-end private dict | single dataclass + metadata seam | metadata easy to bloat |
| 4 | Chat no-tools means stop | tutoring conversation naturally closes | force a `terminate` tool | `AgentLoop` + 3 settlement rounds | coexists with the Label loop |
| 5 | Label deep loop | multi-stage auditable | all use Chat loop | `run_agentic_loop` + `LoopHost` | weak models need repair |
| 6 | Situational mount tools | denoise, save tokens | full schema | `ToolMountFlags` single source of truth | flags must be maintained |
| 7 | deferred tools | ecology large but schema must be small | stuff all / not support MCP | manifest + `load_tools` + live list | one extra round-trip |
| 8 | Provider capability bits + degrade | 36 vendors’ quirks differ | only support OpenAI-compatible | spec capability bits + error classifier + DSML fallback | relies on message-body substring match, brittle |
| 9 | Three-layer memory | auditable personalization | single file / pure vectors | L1/L2/L3 + integer footnotes + four modes | synthesis cost, depends on model quality |
| 10 | Multi-engine knowledge | different materials, different indexes | bind one vendor | factory + Protocol + preflight | preflight and signature complex |
| 11 | Deterministic skeleton tools | do not let the model free-heart-certify arithmetic | all given to the prompt | gates/plan/replan sink into tools | tool surface grows |
| 12 | Replayable turn + authorization matrix | disconnect does not lose class; multi-tenant controllable | stateless requests | `after_seq` replay + resume + grants v2 | protocol surface large |

---

<h2 id="ch22">Chapter 22 · Cross-cut (education / general / coding-agent table)</h2>

| Dimension | DeepTutor | MiroFish | OpenWorker | Claude Code / Codex | Open Design | Reasonix |
|---|---|---|---|---|---|---|
| Product axis | lifelong learning workspace | crowd-event rehearsal | AI coworker | local coding agent | design host | cost-saving terminal agent |
| Main loop | **dual-track** (Chat + Label) | simulation tick | inbox async | tool loop | does not write the main loop | single loop + three-rung compaction |
| Context | UnifiedContext across modes | world state | session + inbox | session + AGENTS.md | script + adapters | stable prefix + turn tail |
| Tool visibility | situational flags + deferred two layers | — | risk labels | permissions + cache ordering | adapter is data | `use_capability` proxy |
| Memory | L1/L2/L3 + footnotes auditable | Agent memory | SQLite | files / compaction | less | compaction three rungs |
| Sandbox | **isolation level queryable + graded privilege** | — | permission grades | OS three-platform fence | — | application layer + OS |
| External engines | Subagent CLI consult (KB slot) | OASIS + Zep | connectors | itself is the engine | 25 CLI adapters | MCP |
| Reach | Web + CLI + 16 IM | Web | Slack etc. | TUI / IDE | Web | TUI/HTTP/desktop/ACP |
| License | Apache-2.0 | AGPL-3.0 | depends on base | various | depends on repo | MIT |

**One-sentence position**: DeepTutor is the first sample in this series that fully smashed “Agent engineering” into an **education-product skeleton**—it proves that unified runtime, auditable memory, progressive disclosure, isolation grading are not techniques that only serve writing code.

**Three combinations nobody else has**:

1. The “knowledge base” slot can be materials, can be a vault, **can also be a live coding Agent**;
2. Make “understand the material” an **independent objective pre-pass**, only because they observed the model would contaminate into another AI in the attachment;
3. The sandbox treats **isolation strength as a return value**, and accordingly splits `exec` into “everyone can use” vs “admin only.”

---

<h2 id="ch23">Chapter 23 · Honest bounds</h2>

1. **Bulk is huge**: 150k-line backend + 108k-line frontend. This analysis grabs the constitution and load-bearing walls, not line-by-line comments; inside `agentic_pipeline.py` (1,563 lines) there are still many unexpanded branches.
2. **Dual-track loops increase cognitive load**: reading chat and reading research are two mindsets; three plugin-layer semantics stack another layer.
3. **Provider compatibility relies on string match**: `request_compat` judging “unsupported” depends on message-body substrings; upstream changing copy will miss.
4. **RAG multi-engine = multiple real failure modes**: embedding mismatch (graph engines **silently** fail), cloud PageIndex, external LightRAG Server; preflight tries hard but ops is still heavy.
5. **Personalization depends on consolidator quality**: L2/L3 is written by LLM; footnotes guarantee traceability, not summary correctness.
6. **The weakest sandbox grade is not OS isolation**: `RestrictedSubprocessBackend` is only clean env + limit cwd; the repo itself marks it as an admin opt-in degrade path—local macOS development scenes need to be clear about this.
7. **Subagent depends on local CLIs**: without Claude/Codex installed there is no consult capability; container deploy is more trouble.
8. **Education-effect evaluation is not at the center of the repo**: engineering intensity is high, but “whether people actually learn better” depends on the paper and external experiments, cannot be endorsed by star count.
9. **Multi-user is an optional path**: default personal-workspace mindset; going multi-tenant needs another full read of `multi_user/`.
10. **This lecture’s line numbers are anchored at `456f9c2`**: upstream ships frequently (README shows about one release every 2–3 days); line numbers will drift; re-check by symbol names.

---

<h2 id="ch24">Chapter 24 · Source-tour index and local reproduction</h2>

### 24.1 🔍 Source pointer table

| You want to understand… | Read first |
|---|---|
| Overall architectural constitution (for the model) | `AGENTS.md` · root `SKILL.md` |
| Unified entry | `deeptutor/runtime/orchestrator.py` |
| Data of one turn | `deeptutor/core/context.py` |
| Capability protocol (L2) | `deeptutor/core/capability_protocol.py` · `runtime/bootstrap/builtin_capabilities.py` |
| LoopCapability protocol (L3) | `deeptutor/capabilities/protocol.py` · `capabilities/registry.py` |
| Chat main loop | `deeptutor/agents/chat/agent_loop.py` |
| Chat pipeline assembly | `deeptutor/agents/chat/agentic_pipeline.py` |
| Label generic loop | `deeptutor/core/agentic/loop.py` · `labels.py` · `labeled_step.py` |
| Tool-mount strategy | `deeptutor/agents/_shared/tool_composition.py` |
| Tool progressive disclosure | `deeptutor/runtime/registry/deferred_tools.py` |
| Builtin tool roster | `deeptutor/tools/builtin/__init__.py:1562-1663` |
| Provider capability bits | `deeptutor/services/provider_registry.py` |
| Compatibility degrade / text-tool fallback | `deeptutor/services/llm/request_compat.py` · `agents/chat/dsml_tool_calls.py` |
| Three-layer memory | `deeptutor/services/memory/{paths,store,document}.py` · `consolidator/modes/` |
| RAG factory and preflight | `deeptutor/services/rag/{factory,preflight,embedding_signature}.py` |
| Deterministic skeleton tools | `deeptutor/capabilities/{mastery,solve,obsidian}/tools.py` |
| Objective pre-investigation | `deeptutor/capabilities/explore_context/` |
| Subagent | `deeptutor/capabilities/subagent/` · `services/subagent/registry.py` |
| Partners channels | `deeptutor/partners/channels/registry.py` |
| MCP connection model | `deeptutor/services/mcp/manager.py` |
| CLI Apps snapshot | `deeptutor/services/cli_apps/catalog.py` · `vendor/catalog.json` |
| Sandbox three grades | `deeptutor/services/sandbox/{backends,service,quota}.py` |
| Codex OAuth | `deeptutor/services/codex_auth/service.py` |
| Replayable turn protocol | `deeptutor/api/routers/unified_ws.py` |
| Multi-user authorization | `deeptutor/multi_user/{grants,tool_access,knowledge_access}.py` |
| Skills | `deeptutor/services/skill/service.py` |
| Living book | `deeptutor/book/{__init__,engine,compiler,models}.py` |
| Window-occupancy reading | `deeptutor/agents/chat/context_budget.py` |
| CLI | `deeptutor_cli/main.py` |

### 24.2 Local reproduction (read-only analysis)

```bash
cd "参考项目/DeepTutor"
git rev-parse --short HEAD   # expect 456f9c2
git describe --tags          # expect v1.5.11

# re-check several key numbers in this lecture
find deeptutor -name '*.py' -print0 | xargs -0 wc -l | tail -1        # 151534
python3 -c "import re,json;d=json.load(open('deeptutor/services/cli_apps/vendor/catalog.json'));print(len(d['apps']), d['meta']['commit'])"
python3 -c "import re;s=open('deeptutor/services/provider_registry.py').read();b=s[s.index('PROVIDERS: tuple'):];print(len(re.findall(r'ProviderSpec\(\s*name=\"',b)))"   # 36
ls deeptutor/partners/channels/*.py | wc -l                            # 20 (including base/manager/registry/__init__ → 16 channels)
```

If you actually want to run it (not required for analysis):

```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
( cd web && npm ci --legacy-peer-deps )
deeptutor init && deeptutor start --dev
```

### 24.3 Handoff to other documents in this series

- Care about “do not write the main loop; treat others’ CLIs as the engine” → contrast [Open Design source analysis](./open-design-源码分析.md) with this lecture’s Chapter 17 (host-relationship reverse)
- Care about “progressive disclosure / cache-friendly” → contrast [DeepSeek-Reasonix](./DeepSeek-Reasonix-源码分析.md)’s `use_capability` with this lecture’s Chapters 10 and 20
- Care about “do not trust the model’s self-report” → contrast [MiMo Code](./MiMo-Code-源码分析.md)’s Goal judge with this lecture’s Chapters 14 and 15
- Care about “sandbox and permissions” → contrast [openai-codex](./openai-codex-源码分析.md)’s three-platform fence with this lecture’s section 18.3
- Care about “change-of-axis products” → contrast [MiroFish](./MiroFish-源码分析.md): one switched to social simulation, one switched to lifelong learning

---

## Appendix A · Version-baseline fingerprint

| Item | Value |
|---|---|
| Repo | https://github.com/HKUDS/DeepTutor |
| Tag / Commit | `v1.5.11` / `456f9c2` |
| Version source of truth | `deeptutor/__version__.py:9` |
| License | Apache-2.0 |
| Stars / Forks (analysis day) | 35,022 / 4,444 (2026-08-12) |
| Backend / CLI / frontend lines | 151,534 / 5,147 / 108,133 |
| Top-level Capability | 7 |
| LoopCapability | 5 |
| User-toggleable tools / situational auto-mount tools | 7 / 15 |
| LLM Provider (of which gateways) | 36 (11) |
| RAG engines | 5 |
| IM channels | 16 |
| Subagent backends | 7 (6 local CLI + partner) |
| CLI Apps snapshot | 101 (`CLI-Anything` @ `bc536c9`, 2026-07-09) |
| Sandbox backends / isolation grades | 3 / 2 (SYSTEM, APPLICATION) |
| WS client message types | 11 |
| REST routers | 33 |
| Docs / Paper | https://deeptutor.info/ · [arXiv:2604.26962](https://arxiv.org/abs/2604.26962) |

## Appendix B · Three most distinctive designs (for catalog cards)

1. **Switch the objective, not the engine**: 7 top-level Capabilities + 5 LoopCapabilities (of which mastery/solve dual identity) share Orchestrator / StreamBus / UnifiedContext; the tool surface is decided by situational flags + deferred two-layer progressive disclosure.
2. **Auditable three-layer memory**: L1 JSONL traces → L2 surface summaries → L3 synthesis, integer footnotes + `<!--m_xxx-->` anchors per-item provenance, four synthesis modes (including one that does not use LLM), preferences can only be written through an idempotent narrow gate.
3. **The knowledge slot can become an expert**: RAG / Obsidian vault / local coding CLI share the product semantics of “the selected knowledge base”; plus a sandbox of “isolation strength queryable and privilege graded accordingly” and 101 pin-versioned CLI Apps.

## Appendix C · Second-round re-check revision list

| # | Type | Content |
|---|---|---|
| 1 | **Correction** | `preferences.md` from “the model cannot rewrite” corrected to “does not enter auto-synthesis, but `write_memory` can write via the `write_preference` idempotent narrow gate” (`store.py:153-154,194-250`) |
| 2 | **Correction** | “two plugin layers” filled to **three layers**, list the 5 `LOOP_CAPABILITIES` members, and explain mastery/solve’s L2/L3 dual identity |
| 3 | **Correction** | line-number correction: `README_CN.md:58-67`→`60-62`; `AGENTS.md:28-29`→`27-28` (StreamBus) and `28-30` (settings); `orchestrator.py:36-86`→`36-114` |
| 4 | Fill-in | Chapter 10 deferred tools / `load_tools` progressive disclosure |
| 5 | Fill-in | Chapter 11 36 provider capability bits, graceful degrade, DSML text-tool fallback (issue #666) |
| 6 | Fill-in | Chapter 14 Mastery/Solve/Obsidian “deterministic skeleton vs model judgment” division philosophy |
| 7 | Fill-in | Chapter 15 ExploreContext’s persona-contamination and “weak models do not read attachments” two motives |
| 8 | Fill-in | Chapter 18 MCP connection-task model / 101 CLI Apps supply-chain pin / sandbox three grades two levels / Codex OAuth |
| 9 | Fill-in | Chapter 19 replayable turn protocol (11 message types, `after_seq`, resume, regenerate) and multi-user authorization matrix |
| 10 | Supplement | precise counts: 7/15 tools, 36(11) providers, 5 engines, 16 channels, 7 backends, 101 apps, 33 routers, four synthesis modes |
| 11 | Supplement | Chat loop two fallbacks (truncation continue-write, empty-finish nudge); Label protocol `final`⊥`terminal`; label-parse tolerance |
| 12 | Supplement | Appendix A fingerprint table expanded to a number table that can be re-checked item by item + section 24.2 gives re-check commands |

## Appendix D · Analysis-scope statement

This analysis is based on a shallow clone of tag `v1.5.11`, static reading and key-path item-by-item check (including the second-round re-check). **It does not cover**: all test cases, protocol-implementation details of every IM channel, interaction implementation of every frontend page, internal algorithm derivation of the `book/` compiler and `learning/` mastery arithmetic, production multi-tenant deploy ops manuals. Line numbers are relative to that baseline; if upstream rebases, use symbol names.
