English lecture · source-traced

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*

EN
lecture
source
file:line
no
embellishment
中文
sibling

Subject: 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 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.


Part 1

Contents

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

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 5. Three-layer plugin constitution: Tools × Capabilities × LoopCapability 6. UnifiedContext: the entire truth of one turn

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

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


Part I · Origin: why the developers built this

Part 2

Chapter 1 · Pain and insight: tutoring should not be a pile of isolated tools

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


Part 3

Chapter 2 · Product position and motive evidence: the original words in README / AGENTS / the paper

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.


Part 4

Chapter 3 · Repo map and stack: numeric shape and dependency direction

3.1 Top-level shape

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)

flowchart TB
    CLI["deeptutor_cli"] --> ORCH["ChatOrchestrator"]
    WS["api/unified_ws
replayable turn protocol"] --> ORCH SDK["app.DeepTutorApp"] --> ORCH PART["Partners runtime"] --> ORCH ORCH --> CAPREG["CapabilityRegistry
7 top-level capabilities"] ORCH --> TOOLREG["ToolRegistry"] CAPREG --> CAP["BaseCapability.run"] CAP --> LOOP["AgentLoop (chat)
run_agentic_loop (deep)"] LOOP --> LCAP["LOOP_CAPABILITIES
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

Part 5

Chapter 4 · Developer mind map: from “switch the objective, not the engine” to twelve designs

mindmap
  root((switch the objective
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.


Part 6

Chapter 5 · Three-layer plugin constitution: Tools × Capabilities × LoopCapability

⚠️ 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:

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

Part 7

Chapter 6 · UnifiedContext: the entire truth of one turn

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

Part 8

Chapter 7 · ChatOrchestrator: three entries share one face

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


Part 9

Chapter 8 · Dual-track main loop: Chat’s “no tools means done” × deep capabilities’ Label protocol

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.


Part 10

Chapter 9 · Tool mount, layer 1: ToolMountFlags lets context decide the tool surface

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.


Part 11

Chapter 10 · Tool mount, layer 2: deferred tools and `load_tools` progressive disclosure

🆕 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).

flowchart LR
    A["MCP / CLI App tools
deferred=True"] --> B["system prompt: one summary line"] B --> C{model judges it needs them?} C -- no --> D["schema surface stays small
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)

Part 12

Chapter 11 · Provider compatibility layer: 36 capability bits · graceful degrade · DSML text-tool fallback

🆕 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 ProviderSpecs, 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:

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


Part 13

Chapter 12 · Three-layer Memory: L1 traces → L2 surface → L3 synthesis (including four synthesis modes)

12.1 Layout

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

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):

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

Part 14

Chapter 13 · Multi-engine Knowledge: five index engines + treating vault / CLI as a KB

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


Part 15

Chapter 14 · Deterministic skeleton vs model judgment: Mastery / Solve / Obsidian tool-division philosophy

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


Part 16

Chapter 15 · ExploreContext: structurally peel “understand the material” from “answer the question”

🆕 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.
flowchart LR
    A["turn carries readable attachments
(especially: another AI’s conversation log)"] --> B["ExploreContext pre_loop
read-only · objective · third person"] B --> C["investigation conclusion folds into the user-message seed"] C --> D["answer loop: no longer has read_source
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

Part 17

Chapter 16 · Partners: an IM companion on the same brain

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.


Part 18

Chapter 17 · Subagent: treat a coding CLI as a consultable knowledge base

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.


Part 19

Chapter 18 · Four legs of the tool ecology: MCP · 101 CLI Apps · sandbox three grades · Codex OAuth

🆕 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:

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.


Part 20

Chapter 19 · Replayable turn protocol and multi-user authorization matrix

🆕 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.”


Part 21

Chapter 20 · Skills and BookEngine: progressively disclosed knowledge and a “living book” compiler

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

Part 22

Chapter 21 · Twelve decisions, five-beat recap

# 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

Part 23

Chapter 22 · Cross-cut (education / general / coding-agent table)

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

Part 24

Chapter 23 · Honest bounds

  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.

Part 25

Chapter 24 · Source-tour index and local reproduction

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)

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):

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 with this lecture’s Chapter 17 (host-relationship reverse)
  • Care about “progressive disclosure / cache-friendly” → contrast DeepSeek-Reasonix’s use_capability with this lecture’s Chapters 10 and 20
  • Care about “do not trust the model’s self-report” → contrast MiMo Code’s Goal judge with this lecture’s Chapters 14 and 15
  • Care about “sandbox and permissions” → contrast openai-codex’s three-platform fence with this lecture’s section 18.3
  • Care about “change-of-axis products” → contrast MiroFish: one switched to social simulation, one switched to lifelong learning

Part 26

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
Part 27

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.
Part 28

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
Part 29

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.

↑