Subject: HKUDS/DeepTutor (product name DeepTutor) Baseline: tag
v1.5.11· commit456f9c2(release: v1.5.11) · single source of version truthdeeptutor/__version__.py:9License: Apache-2.0 (LICENSE) Code size (baseline):deeptutor/151,534 lines of Python (excluding top-leveltests/);deeptutor_cli/5,147 lines;web/108,133 lines of TS/TSX (464 files); whole repo 1,057.pyfiles 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 @ 456f9c2Star 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 ofpreferences.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 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
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:
- 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.
- Insight: real lifelong tutoring needs not more entries, but the same agent runtime + one learning context that can flow.
- 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.
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.
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 byweb/proxy.ts(the container only needs to expose 3782) - Entries:
deeptutorCLI (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
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.
Chapter 5 · Three-layer plugin constitution: Tools × Capabilities × LoopCapability
⚠️ This chapter is a second-round re-check correction focus:
AGENTS.mdonly 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_solveare 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/SolveLoopCapabilityare 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.mdonly wrote two; the third layer you only know by readingcapabilities/registry.py; - Exclusive strategy needs an exception rule: when selected alongside a LlamaIndex KB,
rag/kb_filesmust coexist (issue #650,tool_composition.py:59-61,132-138).
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
Chapter 7 · ChatOrchestrator: three entries share one face
7.1 Entry convergence
ChatOrchestrator.handle (runtime/orchestrator.py:36-114):
- Fill
session_id - Resolve
active_capability or "chat" - Take the implementation from
CapabilityRegistry; if missing, emit one error event withturn_terminaland finish normally (orchestrator.py:49-67) - Build a
StreamBus, register it in the global table byturn_id(for WS reconnect subscribe), runcapability.run(context, bus)in the background - Externally
async forfanoutStreamEvent - In
finallyalways emitDONEandclose(), unregister the bus - After end, emit
CAPABILITY_COMPLETEon 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.
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_usercan 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 isexploration + 4(the comment atagent_loop.py:60-63computes it clearly).
Two real fallback details worth copying away:
- 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; - 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
terminatetool—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.
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.
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_capabilityand 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
- Tools marked
BaseTool.deferred(by default includes all MCP tools) do not enter this round’s initial tool table; - The system prompt only carries one summary line (
render_deferred_tools_manifest); - When the model needs them, builtin
load_toolsreports exact names; DeferredToolLoaderappends the full schema into the livetool_schemaslist—run_agentic_looprereads that list every round, so the tool is immediately callable;- 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) |
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:327comment 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.
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→ L1update_l2/update_l3→ consolidatorread_l3_concat→ for theread_memorytool (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 workbenchapply_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:
preferences.mddoes not enter auto-synthesis—update_l3explicitly refuses (store.py:153-154:raise ValueError("preferences.md is not auto-consolidated"));- But the model can write, only through a narrow gate:
write_preference(store.py:194-250); the comment states “thewrite_memorytool is the only caller;trace_idis injected by the runtime”; - This narrow gate comes with two protections:
opis onlyadd/edit;addis 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 samewrite_memoryacross turns (issue #647), andpreferences.mdhas 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 andDELETE /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 |
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.”
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.pymodule 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.
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:
- 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
## Assistantturns and then speaks in that agent’s first-person voice. Splitting understanding into an objective (third-person) pre-investigation structurally kills this confusion. - 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_loophook (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), usingread_sourceto 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 |
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()usespkgutilto scan the package, zero import, list builtin channel names (:18-26), excludingbase/manager/registry;load_channel_class()imports the module and takes the firstBaseChannelsubclass (: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 anerrorsdict—“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_toolsdoes subtraction (the owner can ban some builtins);- Force-mount the
PARTNER_BUILTIN_TOOL_NAMESthree-piece and suppress chat’sread_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.
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
KnowledgeCapabilityexclusive turn: onlyconsult_subagent+ask_userfallback; - 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 + 2to 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.
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
cligroup 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’srequires.sandboxgate;isolation_level()→ policy gate: SYSTEM open to everyone, APPLICATION admin-only;run()→ execute, constrained by per-user quota (UserExecQuota/QuotaExceededinquota.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.
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.”
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:
- 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_skillto take the full text; the exception is skills withalways: truein frontmatter, whose body is eager-injected (for “house rules” that must take effect every turn); - 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
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 |
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:
- The “knowledge base” slot can be materials, can be a vault, can also be a live coding Agent;
- Make “understand the material” an independent objective pre-pass, only because they observed the model would contaminate into another AI in the attachment;
- The sandbox treats isolation strength as a return value, and accordingly splits
execinto “everyone can use” vs “admin only.”
Chapter 23 · Honest bounds
- 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. - Dual-track loops increase cognitive load: reading chat and reading research are two mindsets; three plugin-layer semantics stack another layer.
- Provider compatibility relies on string match:
request_compatjudging “unsupported” depends on message-body substrings; upstream changing copy will miss. - 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.
- Personalization depends on consolidator quality: L2/L3 is written by LLM; footnotes guarantee traceability, not summary correctness.
- The weakest sandbox grade is not OS isolation:
RestrictedSubprocessBackendis 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. - Subagent depends on local CLIs: without Claude/Codex installed there is no consult capability; container deploy is more trouble.
- 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.
- Multi-user is an optional path: default personal-workspace mindset; going multi-tenant needs another full read of
multi_user/. - 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.
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_capabilitywith 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
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 |
Appendix B · Three most distinctive designs (for catalog cards)
- 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.
- 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. - 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.