This guide is the deep reference for developers, architects, and contributors working on Learn Claude Code — and for anyone deploying its web platform. It documents the harness architecture each lesson builds, the on-disk state each mechanism uses, the web application's structure and build pipeline, the security model (including what is deliberately left unguarded), code patterns, testing, and known limitations.
For installation and how to take the course, see the companion User Guide. This document assumes you want to understand or extend the implementation.
1. Executive Summary
What it is. A 0-to-1 curriculum in harness engineering — the discipline of building the operational environment around an agent model. The project's thesis: agency comes from the model (trained weights), while the harness (tools, knowledge, observation, action interfaces, permissions) is what engineers build. An agent product = model + harness.
What it contains. Two coordinated deliverables:
- A Python course — twenty standalone, runnable agent programs (
s01_agent_loop/code.py…s20_comprehensive/code.py). Each is the previous program plus exactly one new harness mechanism, layered around an agent loop that never changes. - A web platform (
web/) — a Next.js 16 static site that renders the course with interactive per-mechanism visualizations, code diffs, and architecture diagrams, in English / Chinese / Japanese.
Two tracks. The canonical current track is the root-level s01–s20 folders (20 mechanisms). A legacy 12-lesson track survives in agents/ and docs/, and the live web app currently renders that legacy track. Chapter numbers differ between tracks.
Tech footprint. Python ≥ 3.10 with three dependencies (anthropic, python-dotenv, pyyaml). The web app is Next.js 16 / React 19 / Tailwind 4, statically exported. No database, no server runtime for the lessons — the filesystem is the only persistence layer (.memory/, .tasks/, .mailboxes/, .worktrees/, .transcripts/, .scheduled_tasks.json).
Design center. Every mechanism is additive and legible: carried-over code is labelled # FROM sNN (unchanged), and each file documents in its module docstring exactly what it adds. Production concerns (rule-based permission governance, full hook bus, real MCP transport/OAuth, session resume/fork) are intentionally simplified — the project is explicit that it teaches "key designs," not a production-grade clone.
2. System Architecture
2.1 The two halves
┌──────────────────────────────────────────────────────────────────────┐
│ TEACHING CODE (Python) │
│ │
│ s01_agent_loop/code.py ── s02 ── s03 ── … ── s20_comprehensive │
│ │ each = prev + one mechanism │
│ │ talks to: Anthropic-compatible Messages API │
│ │ persists to: ./.memory ./.tasks ./.mailboxes ./.worktrees … │
│ │
│ agents/ (legacy 12-lesson runnable copies + s_full.py) │
│ skills/ (agent-builder, code-review, mcp-builder, pdf — used by s07)│
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ WEB PLATFORM (web/, Next.js 16, static export) │
│ │
│ docs/{en,zh,ja}/*.md ──► scripts/extract-content.ts (predev/prebuild)│
│ │ │ generates web/src/data/generated/ │
│ ▼ ▼ │
│ App Router ──► visualizations/ + simulator/ + diff/ + i18n/ │
│ (static HTML out/) │
└──────────────────────────────────────────────────────────────────────┘
The two halves are loosely coupled: the web app consumes the docs/ markdown (legacy track) at build time via an extraction script; the Python lessons have no dependency on the web app.
2.2 The canonical agent loop
Every lesson is built around one invariant pattern (this is the literal core; later lessons wrap it but never alter its shape):
def agent_loop(messages):
while True:
response = client.messages.create(
model=MODEL, system=SYSTEM,
messages=messages, tools=TOOLS, max_tokens=8000,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use": # model is done
return
results = []
for block in response.content:
if block.type == "tool_use":
output = TOOL_HANDLERS[block.name](**block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
messages.append({"role": "user", "content": results}) # feed back, loop
The model decides when to call tools and when to stop (stop_reason); the harness only executes what's asked and feeds results back. The tool_use / tool_result contract (matching tool_use_id) is the load-bearing protocol — several known issues (see §11) stem from accidentally breaking that pairing during compaction.
2.3 The layered mechanism model
The web app's web/src/lib/constants.ts defines the authoritative grouping of the twenty mechanisms into five architectural layers. This is the project's own conceptual map:
| Layer | Color | Mechanisms |
|---|---|---|
| Tools & Execution | #3B82F6 |
s01, s02, s03, s04 |
| Planning & Control | #10B981 |
s05, s06, s07, s10, s11 |
| Memory Management | #8B5CF6 |
s08, s09 |
| Concurrency & Scheduling | #F59E0B |
s13, s14 |
| Multi-Agent Platform | #EF4444 |
s12, s15, s16, s17, s18, s19, s20 |
The same file also stores per-version metadata (title, subtitle, coreAddition, keyInsight, layer, prevVersion) for all twenty chapters and the canonical VERSION_ORDER (s01…s20) — this is the single source the web UI uses to render the learning path and navigation.
3. Repository Structure
learn-claude-code/
├── README.md / README-zh.md / README-ja.md # project thesis + chapter index
├── LICENSE # MIT
├── requirements.txt # anthropic, python-dotenv, pyyaml
├── .env.example # required+optional env, provider table
│
├── s01_agent_loop/ … s20_comprehensive/ # CURRENT 20-lesson track
│ ├── README.md # Chinese narrative (source)
│ ├── README.en.md # English translation
│ ├── README.ja.md # Japanese translation
│ ├── code.py # standalone runnable agent
│ └── images/ # SVG diagrams (some chapters)
│ (s05_todo_write/example/hello.py — a sample target file)
│
├── agents/ # LEGACY 12-lesson runnable copies
│ ├── s01_agent_loop.py … s12_worktree_task_isolation.py
│ ├── s_full.py # legacy "everything" agent
│ └── __init__.py
│
├── skills/ # SKILL.md packs loaded by s07
│ ├── agent-builder/ (+ references/, scripts/)
│ ├── code-review/
│ ├── mcp-builder/
│ └── pdf/
│
├── docs/{en,zh,ja}/ # LEGACY 12-lesson docs (web source)
│ └── s01-the-agent-loop.md … s12-worktree-task-isolation.md
│
├── tests/ # pytest smoke tests
│ ├── test_agents_smoke.py
│ ├── test_s_full_background.py
│ └── test_todo_write_string_input.py
│
└── web/ # Next.js 16 learning platform
├── next.config.ts # output:"export", trailingSlash, images.unoptimized
├── package.json # predev/prebuild → npm run extract
├── scripts/extract-content.ts
└── src/
├── app/[locale]/(learn)/… # App Router, localized routes
├── components/visualizations/ # per-mechanism animated viz (entry: index.tsx)
├── components/{simulator,diff,timeline,architecture,code,docs,ui,layout}/
├── data/{annotations,scenarios,generated}/
├── i18n/messages/{en,zh,ja}.json
└── lib/{constants.ts, i18n.tsx, i18n-server.ts, utils.ts}
Note
The legacy-to-current chapter mapping is documented in the root
README.md. Notable shifts: legacys03→currents05(TodoWrite), legacys06→currents08(Context Compact), legacys07→currents12(Task System), legacys12→currents18(Worktree). Mechanismss03,s04,s09,s10,s11,s14,s19,s20are new in the current track.
4. The Harness — Architecture Deep Dive
This section documents each mechanism's internals: the data structures it introduces, the on-disk state it owns, and the integration point in the loop. Mechanisms compose additively, so later chapters inherit everything described earlier.
4.1 Tools & Execution (s01–s04)
s01 — Agent Loop. One tool (bash), one while loop. run_bash runs subprocess.run(shell=True, timeout=120), returns combined stdout+stderr truncated to 50 000 chars, with a tiny inline deny-list. MODEL is read from os.environ["MODEL_ID"] (hard requirement); the client is Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL")).
s02 — Tool Use. Adds read_file, write_file, edit_file, glob, and the central TOOL_HANDLERS dispatch map. Introduces safe_path() — resolves a path against WORKDIR and raises ValueError("Path escapes workspace") if it escapes. The loop changes one line: output = TOOL_HANDLERS[block.name](**block.input).
s03 — Permission. A three-gate pipeline before execution:
- Gate 1 — deny-list:
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if=", "> /dev/sda"](bash only). - Gate 2 — rule matching:
PERMISSION_RULESflag writes outside the workspace and destructive bash keywords (rm,> /etc/,chmod 777). - Gate 3 — user approval: interactive
Allow? [y/N]prompt. Integration is one line:if not check_permission(block): continue.
s04 — Hooks. Extracts cross-cutting logic onto a HOOKS registry keyed by event: UserPromptSubmit, PreToolUse, PostToolUse, Stop. register_hook(event, cb) / trigger_hooks(event, *args); a hook returning non-None short-circuits (used to block a tool call). The s03 permission check is reimplemented as a PreToolUse hook. The loop no longer contains the permission logic — it calls trigger_hooks("PreToolUse", block).
4.2 Planning & Control (s05, s06, s07, s10, s11)
s05 — TodoWrite. Adds the todo_write tool over in-memory CURRENT_TODOS (each item {content, status} where status ∈ pending|in_progress|completed). _normalize_todos accepts a list or a JSON/ast.literal_eval string (robustness against the model passing a stringified array — see the dedicated test). A "nag" counter injects <reminder>Update your todos.</reminder> after 3 tool rounds without a todo_write.
s06 — Subagent. Adds the task tool → spawn_subagent(description). The sub-agent runs its own loop with a fresh messages=[{"role":"user","content":description}], a distinct SUB_SYSTEM, and a restricted SUB_TOOLS set that excludes task (no recursion). Capped at 30 turns. Returns only the final text (with a fallback search backward for the last assistant text, else "Subagent stopped after 30 turns…"). Intermediate history is discarded — this is the context-isolation lesson.
s07 — Skill Loading. Two-level knowledge injection. At startup _scan_skills() walks skills/*/SKILL.md, parses YAML frontmatter (name, description), and builds SKILL_REGISTRY. The system prompt carries only names+descriptions (cheap, ~100 tokens each via build_system()); the full body is fetched on demand by the load_skill(name) tool (registry lookup — no path traversal). Bundled skills: agent-builder, code-review, mcp-builder, pdf.
s10 — System Prompt. Runtime prompt assembly. PROMPT_SECTIONS (identity, tools, workspace, memory) are selected and joined by assemble_system_prompt(context) based on real state (e.g. memory section loads only if .memory/MEMORY.md exists). get_system_prompt(context) caches via json.dumps(context, sort_keys=True) — the docstring explicitly notes Python's hash() is unsuitable (process randomization, fails on nested structures). update_context() re-derives context after each tool round.
s11 — Error Recovery. Wraps the model call in classified recovery:
- Path 1 —
max_tokens: escalate8000 → 64000(retry without appending truncated output), then up toMAX_RECOVERY_RETRIES=3continuation prompts. - Path 2 — prompt too long:
reactive_compactonce (trims to last 5 messages with a marker). - Path 3 — 429/529:
with_retrydoes exponential backoff with jitter (BASE_DELAY_MS=500, cap 32 s,MAX_RETRIES=10); afterMAX_CONSECUTIVE_529=3it switches toFALLBACK_MODEL_IDif set. State lives in aRecoveryStateobject. RaisesRuntimeError("Max retries … exceeded")at the ceiling.
4.3 Memory Management (s08, s09)
s08 — Context Compact. A four-layer pipeline run before each model call, "cheap first, expensive last" (order matches Claude Code's source: budget → snip → micro → auto):
- L1
snip_compact— whenlen(messages) > 50, keep 3 head + tail, replace the middle with a[snipped N messages]marker. - L2
micro_compact— replace all but the lastKEEP_RECENT=3tool results (>120 chars) with a placeholder. - L3
tool_result_budget— when the latest turn's tool results exceed 200 KB, spill the largest (>PERSIST_THRESHOLD=30000) to.task_outputs/tool-results/<id>.txtand leave a<persisted-output>preview. - L4
compact_history— ifestimate_size(messages) > CONTEXT_LIMIT=50000, write a transcript to.transcripts/and replace history with a one-call LLM summary. - Emergency
reactive_compact— on an API "prompt too long", summarize + keep the last 5 messages, retry once. Adds acompacttool that triggers L4 on demand.
s09 — Memory. Persistent cross-session knowledge in .memory/. Three subsystems:
- Selection —
select_relevant_memories()asks the model (with keyword fallback) which memory files match the recent conversation;load_memories()injects the chosen files' content into the current turn. - Extraction — after each turn,
extract_memories()runs over a pre-compression snapshot and writes new{name, type, description, body}files (types:user|feedback|project|reference) with YAML frontmatter;_rebuild_index()keepsMEMORY.md(one line per memory) in sync. - Consolidation — when files ≥
CONSOLIDATE_THRESHOLD=10,consolidate_memories()merges duplicates / drops stale entries via an LLM call, targeting ≤30 memories. TheMEMORY.mdindex is injected into the system prompt every turn (cheap); full bodies load on demand.
4.4 Concurrency & Scheduling (s13, s14)
s13 — Background Tasks. threading.Thread daemon execution. should_run_background() honors an explicit run_in_background tool param, else falls back to is_slow_operation() (keyword heuristic: install, build, test, deploy, pip install, npm install, pytest, make, …). start_background_task() dispatches to a daemon worker, returns a bg_NNNN id immediately, and stores results under a threading.Lock. collect_background_results() drains completed jobs and injects <task_notification> blocks into the next user message — deliberately not reusing the original tool_use_id.
s14 — Cron Scheduler. Four cooperating layers:
- Scheduler thread (
cron_scheduler_loop) polls every 1 s, matches jobs, and appends fired jobs tocron_queue. Uses a date-aware"%Y-%m-%d %H:%M"marker (_last_fired) so a job fires at most once per calendar minute. Per-job errors are caught so one bad job can't kill the thread. - Queue —
cron_queuedecouples the scheduler from the agent. - Queue processor (
queue_processor_loop) wakes the agent (underagent_lock, non-blocking acquire) when work is queued and the agent is idle. - Consumer —
agent_loopdrainsconsume_cron_queue()and injects[Scheduled] <prompt>messages.CronJob(id, cron, prompt, recurring, durable).cron_matchesimplements 5-field matching with*,*/step, ranges, and lists, and standard DOM/DOW OR semantics.validate_cronbounds-checks each field. Durable jobs persist to.scheduled_tasks.jsonand reload (with re-validation) on startup.
4.5 Multi-Agent Platform (s12, s15–s20)
s12 — Task System. The durable foundation for multi-agent work. Task(id, subject, description, status, owner, blockedBy) persisted one-JSON-file-per-task in .tasks/. can_start() treats missing dependencies as blocked; claim_task() sets owner + in_progress (rejecting already-claimed); complete_task() reports newly unblocked downstream tasks. Five tools: create_task, list_tasks, get_task, claim_task, complete_task.
s15 — Agent Teams. A file-based MessageBus over .mailboxes/*.jsonl. spawn_teammate_thread runs a teammate in a background thread with its own simplified loop (bash, read, write, send_message); teammate→Lead messages are injected into the Lead's history (not merely printed). Permissions "bubble" — teammates run the same PreToolUse hooks. (Teaching version caps teammate rounds; production uses an idle loop, introduced next.)
s16 — Team Protocols. Request/response contracts with correlation. ProtocolState(request_id, type, sender, status, created_at) + a pending_requests registry. dispatch_message routes by type to handlers (handle_shutdown_request, handle_plan_response, …); match_response correlates replies by request_id with type validation. Adds shutdown handshake and plan request/approval; teammates switch to an idle loop (wait for inbox) instead of exiting after N rounds.
s17 — Autonomous Agents. Self-organization. scan_unclaimed_tasks finds pending, unowned tasks whose deps are complete; teammates auto-claim and run a WORK → IDLE → SHUTDOWN lifecycle (idle poll ~5 s; shutdown dispatch when idle ~60 s). Identity is re-injected after context compression so a teammate doesn't forget who it is.
s18 — Worktree Isolation. Filesystem isolation to match the context isolation. Task gains a worktree: str | None field. validate_worktree_name rejects path traversal and illegal chars (1–40 chars: letters, digits, ., _, -). create_worktree does git worktree add under .worktrees/<name> on branch wt/<name>; tasks bind by name; an event log is written to .worktrees/events.jsonl. run_git returns (ok, output) and logs events only on success. Requires a git repo (RuntimeError otherwise). Tools: create_worktree, remove_worktree, keep_worktree.
s19 — MCP Plugin. External-tool bridge. MCPClient discovers tools from a server and assemble_tool_pool merges builtin + MCP tools into one dynamic pool. Tool names are normalized to mcp__{server}__{tool}; MCP tools carry readOnly/destructive annotations. Because the pool is dynamic, prompt caching is disabled for the assembled tools. (The transport here is a mock handler; real MCP transport, OAuth, resource subscription, and polling are out of scope — see §11.)
s20 — Comprehensive Agent. The integration finale: every mechanism above in one harness around the same loop. It re-tunes some constants for the combined setting (e.g. ESCALATED_MAX_TOKENS=16000, MAX_RETRIES=3, MAX_CONSECUTIVE_529=2) and adds a thread-safe terminal_print that redraws the prompt line when background threads emit output during interactive use.
5. The Web Platform
5.1 Stack & build model
| Concern | Choice |
|---|---|
| Framework | Next.js 16.1.6 (App Router) |
| UI | React 19.2.3, Tailwind CSS 4, framer-motion 12 (animation), lucide-react (icons) |
| Markdown | unified + remark-parse / remark-gfm / remark-rehype + rehype-raw / rehype-highlight / rehype-stringify |
| Diffing | diff 8 |
| Output | Static export (output: "export", trailingSlash: true, images.unoptimized: true) → plain HTML in out/ |
| Scripting | tsx runs scripts/extract-content.ts |
next.config.ts:
const nextConfig: NextConfig = {
output: "export",
images: { unoptimized: true },
trailingSlash: true,
};
Because the site is a static export, it has no server runtime — it can be hosted on any static host or CDN.
5.2 Content pipeline
package.json wires a pre-step into both dev and build:
"scripts": {
"extract": "tsx scripts/extract-content.ts",
"predev": "npm run extract",
"dev": "next dev",
"prebuild":"npm run extract",
"build": "next build",
"start": "next start"
}
npm run dev and npm run build therefore always regenerate extracted content first (into web/src/data/generated/) from the docs/ markdown. Operators never run extraction manually.
Important
The web app currently sources the legacy
docs/(s01–s12) track, while the canonical learning material is the root-levels01–s20folders. Keep this in mind when reconciling what the site shows against the current chapters.
5.3 Internationalization
A lightweight, dependency-free i18n: JSON message catalogs at web/src/i18n/messages/{en,zh,ja}.json, a client I18nProvider/useTranslations context (lib/i18n.tsx), and a server variant (lib/i18n-server.ts). Both fall back to English on a missing key, then to the raw key. Routes are localized under app/[locale]/(learn)/….
5.4 Visualization architecture
web/src/components/visualizations/index.tsx is the entry point: a Record<version, React.lazy(...)> map (s01–s20) wrapped in Suspense, so each mechanism's animated diagram is code-split and loaded on demand. lib/constants.ts drives the navigation (VERSION_ORDER, VERSION_META, LAYERS). Supporting component groups: simulator/ (interactive agent simulation), diff/ (chapter-to-chapter code diffs), timeline/, architecture/, code/, docs/, plus ui/ and layout/ primitives. Data lives in web/src/data/{annotations,scenarios,generated}/.
6. Requirements & Configuration Reference
6.1 Environment variables
| Variable | Required | Read by | Purpose |
|---|---|---|---|
ANTHROPIC_API_KEY |
Yes | Anthropic SDK (implicit) | API credential (or provider key when ANTHROPIC_BASE_URL is set) |
MODEL_ID |
Yes | os.environ["MODEL_ID"] (every lesson) |
Model to call; default claude-sonnet-4-6 |
ANTHROPIC_BASE_URL |
No | os.getenv (s07+ and all later) |
Point at an Anthropic-compatible provider |
FALLBACK_MODEL_ID |
No | s11, s20 | Model to switch to after repeated 529 overloads |
Note
When
ANTHROPIC_BASE_URLis set, the lessons proactivelyos.environ.pop("ANTHROPIC_AUTH_TOKEN", None)so the provider key inANTHROPIC_API_KEYis authoritative.
6.2 Provider matrix (from .env.example)
| Provider | MODEL_ID |
International ANTHROPIC_BASE_URL |
China-mainland base URL |
|---|---|---|---|
| Anthropic | claude-sonnet-4-6 |
(default) | (default) |
| MiniMax | MiniMax-M2.5 |
https://api.minimax.io/anthropic |
https://api.minimaxi.com/anthropic |
| GLM (Zhipu) | glm-5 |
https://api.z.ai/api/anthropic |
https://open.bigmodel.cn/api/anthropic |
| Kimi (Moonshot) | kimi-k2.5 |
https://api.moonshot.ai/anthropic |
https://api.moonshot.cn/anthropic |
| DeepSeek | deepseek-chat |
https://api.deepseek.com/anthropic |
(same global endpoint) |
6.3 On-disk state (created in the working directory)
| Path | Owner mechanism | Contents |
|---|---|---|
.memory/ + MEMORY.md |
s09 | Memory files (md + YAML frontmatter) and the index |
.tasks/task_*.json |
s12+ | One JSON file per task |
.scheduled_tasks.json |
s14 | Durable cron jobs |
.mailboxes/*.jsonl |
s15+ | Teammate message bus |
.worktrees/<name> + .worktrees/events.jsonl |
s18 | Git worktrees + event log |
.transcripts/transcript_*.jsonl |
s08+ | Pre-compaction history snapshots |
.task_outputs/tool-results/<id>.txt |
s08+ | Spilled large tool outputs |
Tip
To reset any lesson to a clean state, delete the relevant dot-directories from your working directory. They are regenerated on demand.
7. Deployment (Web Platform)
The lessons are not "deployed" — they run locally. Only the web platform is deployable, and because it is a static export, deployment is straightforward.
cd web
npm install
npm run build # runs extract → next build → static HTML in out/
Deployment checklist:
- [ ] Node.js (current LTS) and npm available on the build host.
- [ ]
npm run buildcompletes and producesweb/out/. - [ ] Serve
web/out/from any static host or CDN (e.g. a bucket + CDN, Nginx, GitHub Pages, Vercel static). - [ ] Because
trailingSlash: true, ensure your host servespath/→path/index.html. - [ ] Because
images.unoptimized: true, no image-optimization server is needed. - [ ] Verify all three locales (
/en,/zh,/ja) render.
Caution
There is no backend, no auth, and no secrets in the web build — it is purely static content. Do not add the Python
.env(API keys) anywhere intoweb/; the website never calls the model API.
8. Security Analysis
This is a teaching harness. Several mechanisms are exactly the security controls being taught — and the project is explicit that some production protections are intentionally omitted. Treat the agent as a program that runs arbitrary shell commands on your machine.
8.1 Controls that exist
| Control | Where | What it stops |
|---|---|---|
| Hard deny-list | s03/s04+ (DENY_LIST) |
rm -rf /, sudo, shutdown, reboot, mkfs, dd if=, raw device writes |
| Destructive-command prompt | s03/s04 | rm , > /etc/, chmod 777 → interactive approval |
| Workspace sandboxing | s02+ (safe_path) |
File reads/writes resolving outside WORKDIR → ValueError: Path escapes workspace |
| Worktree name validation | s18 (validate_worktree_name) |
Path traversal / illegal characters in worktree names |
| Skill lookup by registry | s07 (load_skill) |
Path traversal via skill names (registry lookup, not filesystem path join) |
| Sub-agent recursion cap | s06 | Runaway recursion (no task tool in SUB_TOOLS; 30-turn limit) |
| MCP tool annotations | s19 | Surfaces readOnly/destructive so a harness can gate accordingly |
8.2 Intentional gaps (do not rely on this as a sandbox)
The project's own "Scope" section lists these as deliberately simplified or omitted:
- Full event/hook bus (
PreToolUse,SessionStart/End,ConfigChange) — only minimal lifecycle events are wired. - Rule-based permission governance and trust workflows — the deny-list + interactive approval is a teaching subset, not a policy engine.
- Session lifecycle controls (resume/fork) and complete worktree lifecycle handling.
- Real MCP runtime — transport, OAuth, resource subscription, and polling are mocked.
- The JSONL mailbox protocol is a teaching implementation, not a claim about any production internal.
Warning
subprocess.run(shell=True, …)executes whatever the model emits. The deny-list is substring-based and easily bypassed by a determined or confused model (e.g. obfuscated commands). Run lessons in a disposable directory / VM, never against production systems or sensitive data. This is acceptable for a learning tool and is itself part of what the permission chapters teach you to harden.
8.3 Secret handling
Credentials live only in the Python-side .env (gitignored). The web build contains no secrets and makes no API calls. There is no telemetry; lessons talk only to the configured model endpoint.
9. Code Quality & Patterns
- Additive, legible layering. Each
code.pyis the previous file plus one section, with carried-over code explicitly marked# FROM sNN (unchanged)and a module docstring enumerating the diff. This makes the codebase a teaching artifact where a chapter-to-chapterdiffis the lesson. - Dispatch-map extensibility. Capabilities register into
TOOL_HANDLERS; the loop body is invariant. Adding a tool = add an entry, not edit the loop. - Filesystem as state machine. No DB, no broker. Tasks, memory, mailboxes, worktrees, schedules, and transcripts are all plain files/dirs — durable, inspectable, and resettable by deletion. (This mirrors a pattern production agents also use.)
- Dataclasses for records.
Task,CronJob,ProtocolState,RecoveryStateare dataclasses serialized withasdict+json. - Defensive parsing.
_normalize_todosaccepts both real lists and stringified JSON/Python;_parse_frontmattertolerates missing/invalid YAML; cron fields are bounds-validated before use. - Threading model. Background tasks (s13), the cron scheduler/queue processor (s14), and teammates (s15+) all use daemon threads with
threading.Lockaround shared dicts and anagent_lockto serialize agent turns. - Deterministic prompt caching. s10 caches the assembled system prompt via
json.dumps(sort_keys=True)rather thanhash()(documented rationale: process-randomized hashing and unhashable nested structures).
Conventions: Python ≥ 3.10 syntax (str | None, Path.is_relative_to); ANSI color in terminal output; module docstrings carry ASCII architecture diagrams; bilingual prompts (some chapters print Chinese REPL hints).
10. Testing Strategy
tests/ contains focused smoke tests (pytest), not exhaustive coverage:
| Test | Verifies |
|---|---|
test_agents_smoke.py |
The legacy agents/ modules import/run end-to-end without crashing |
test_s_full_background.py |
The legacy "everything" agent's background-task path |
test_todo_write_string_input.py |
todo_write accepts a stringified-JSON todos arg (the _normalize_todos robustness path) |
Note
A community "Production Readiness Scan" (issue #169) scored the repo low on testing/error-handling — by design. These are teaching programs optimized for clarity, not a production library; the test suite is a sanity net, not a guarantee. Contributors adding mechanisms should add at least a smoke test that the lesson imports and completes a trivial turn.
11. Known Limitations & Improvement Areas
Drawn from the project's "Scope" section and its open community issues:
- Compaction can split
tool_use/tool_resultpairs (issues #325, and the400 BadRequestreports #345/#55). Breaking that pairing makes the next request invalid. A robust fix keeps eachtool_useblock adjacent to its matchingtool_resultwhen trimming/snipping. 400 BadRequestin s13 background tasks (#345) — likely the same pairing problem under concurrent notification injection.s09memorybuild_system()placement (#343) — noted as being rebuilt inside a loop where it could be hoisted; an efficiency cleanup.s10build_system_reminderusage (#279) — example code references a reminder builder whose call site is incomplete.- Teammate edge cases (#291) — a Lead can exit early if a teammate runs a long tool-less task and the Lead sees no tool calls; the idle/shutdown heuristics need tightening for long teammate work.
- Context-compression vs. task-system design discussion (#174) — open design debate about the s08/s12 trade-offs.
- Intentionally simplified for teaching: full hook bus, rule-based permission governance, session resume/fork, complete worktree lifecycle, and real MCP transport/OAuth/subscription. These are the natural "productionization" exercises after the course.
- Permission deny-list is substring-based — adequate for teaching, trivially bypassable in adversarial use; a real harness needs structured command parsing and policy rules.
Tip
The README also points to the maintainers' productization follow-ons (a CLI coding agent and an embeddable agent SDK) and a sister tutorial on "always-on" harnesses (heartbeat + cron + IM channels + memory + personality) — the recommended next steps once the twenty mechanisms are understood.
12. Quick Reference
Run a lesson: python sNN_<name>/code.py · quit with q/exit/empty line.
Required env: ANTHROPIC_API_KEY, MODEL_ID (default claude-sonnet-4-6). Optional: ANTHROPIC_BASE_URL, FALLBACK_MODEL_ID.
Deps: pip install -r requirements.txt (anthropic, python-dotenv, pyyaml).
Web: cd web && npm install && npm run dev (→ :3000) · build: npm run build (→ web/out/).
Mechanism → tools added:
| Chapter | Adds | New tools |
|---|---|---|
| s01 | Agent loop | bash |
| s02 | Tool dispatch | read_file, write_file, edit_file, glob |
| s03 | Permission gates | — |
| s04 | Hook registry | — |
| s05 | Planning | todo_write |
| s06 | Sub-agents | task |
| s07 | Skills | load_skill |
| s08 | Compaction | compact |
| s09 | Memory | — (.memory/) |
| s10 | Prompt assembly | — |
| s11 | Error recovery | — |
| s12 | Task board | create_task, list_tasks, get_task, claim_task, complete_task |
| s13 | Background exec | run_in_background param on bash |
| s14 | Cron | schedule_cron, list_crons, cancel_cron |
| s15 | Teams | spawn_teammate, send_message, check_inbox |
| s16 | Protocols | request_shutdown, request_plan, review_plan, submit_plan |
| s17 | Autonomy | (teammate gains list_tasks, claim_task, complete_task) |
| s18 | Worktrees | create_worktree, remove_worktree, keep_worktree |
| s19 | MCP | mcp__{server}__{tool} (dynamic) |
| s20 | Everything | all of the above |
On-disk state: .memory/ · .tasks/ · .scheduled_tasks.json · .mailboxes/ · .worktrees/ · .transcripts/ · .task_outputs/.
License: MIT.