"description":"TaskManager is the multi-agent evolution of TodoWrite. Same core concept (a list of items with statuses) but with critical additions: file persistence (survives crashes), dependency tracking (blocks/blockedBy), ownership (which agent is working on what), and multi-process safety. TodoWrite was designed for a single agent tracking its own work in memory. TaskManager is designed for a team of agents coordinating through the filesystem. The API is intentionally similar so the conceptual upgrade path is clear.",
"alternatives":"Keeping TodoWrite for single-agent use and adding TaskManager only for multi-agent scenarios would avoid breaking the single-agent experience. But maintaining two systems with overlapping functionality increases complexity. TaskManager is a strict superset of TodoWrite -- a single agent using TaskManager just ignores the multi-agent features.",
"id":"task-default-todo-coexistence",
"title":"Task as Course Default, Todo Still Useful",
"description":"TaskManager extends the Todo mental model and becomes the default workflow from s07 onward in this course. This 'default' is a course sequencing choice, not a universal runtime default claim. Both track work items with statuses, but TaskManager adds file persistence (survives crashes), dependency tracking (blocks/blockedBy), ownership fields, and multi-process coordination. Todo remains useful for short, linear, one-shot tracking where heavyweight coordination is unnecessary.",
"alternatives":"Using only Todo keeps the model minimal but weak for long-running or collaborative work. Using only Task everywhere maximizes consistency but can feel heavy for tiny one-off tasks. Reminder signals are omission-reduction hints, not implicit mode switches; Task/Todo choice should still come from workflow intent and available tools.",
"description":"TaskManager は Todo のメンタルモデルを拡張し、本コースでは s07 以降のデフォルトになる。どちらもステータス付き作業項目を扱うが、TaskManager にはファイル永続化(クラッシュ耐性)、依存関係追跡(blocks/blockedBy)、owner、マルチプロセス協調がある。Todo は短く直線的な単発作業では引き続き有効。"
}
},
{
"id":"task-write-discipline",
"title":"Durability Needs Write Discipline",
"description":"File persistence reduces context loss, but it does not remove concurrent-write risks by itself. Before writing task state, reload the JSON, validate expected status/dependency fields, and then save atomically. This prevents one agent from silently overwriting another agent's transition.",
"alternatives":"Blind overwrite writes are simpler but can corrupt coordination state under parallel execution. A database with optimistic locking would enforce stronger safety, but the course keeps file-based state for zero-dependency teaching.",
"title":"Reminder Is Advisory, Not a Mode Switch",
"description":"Reminder signals should be treated as omission-reduction hints, not as control-plane switches. Choosing Task vs Todo should come from workflow intent and currently available tools, while reminders only nudge usage when tracking appears stale.",
"alternatives":"Treating reminders as implicit mode selectors looks convenient, but it hides decision boundaries and makes behavior harder to reason about during long sessions.",
"description":"Use Todo for short one-session linear checklists. Use Task for cross-session work, dependencies, or teammate coordination. If uncertain, start with Task because downscoping is cheaper than migrating state mid-run.",
"alternatives":"Always using Todo keeps the model minimal but breaks durability and collaboration. Always using Task maximizes consistency but may feel heavy for tiny one-shot notes.",
"description":"The task board remains shared and centralized in `.tasks/`, while file edits happen in per-task worktree directories. This separation preserves global visibility (who owns what, what is done) without forcing everyone to edit inside one mutable directory. Coordination stays simple because there is one board, and execution stays safe because each lane is isolated.",
"alternatives":"A single shared workspace is simpler but causes edit collisions and mixed git state. Fully independent task stores per lane avoid collisions but lose team-level visibility and make planning harder.",
"description":"`.worktrees/index.json` records each worktree's name, path, branch, task_id, and status. This makes lifecycle state inspectable and recoverable even after context compression or process restarts. The index also provides a deterministic source for list/status/remove operations.",
"alternatives":"Relying only on `git worktree list` removes local bookkeeping but loses task binding metadata and custom lifecycle states. Keeping all state only in memory is simpler in code but breaks recoverability.",
"description":"This course runtime uses lane-scoped cwd routing (`worktree_run(name, command)`). Other runtimes may choose session-level cwd switches. The design goal is predictable lane context with a re-entry guard when already inside an active worktree context.",
"alternatives":"Global cwd mutation is easy to implement but can leak context across parallel work. Allowing silent re-entry makes lifecycle ownership ambiguous and complicates teardown behavior.",
"zh":{
"title":"按通道 cwd 路由 + 禁止重入",
"description":"本课程运行时采用按通道 `cwd` 路由(`worktree_run(name, command)`)。其他运行时也可能选择会话级 cwd 切换。设计目标是让并行通道可预测,并在已处于 active worktree 上下文时通过重入保护避免二次进入。"
"description":"Lifecycle events are appended to `.worktrees/events.jsonl` (`worktree.create.*`, `worktree.remove.*`, `task.completed`). This turns hidden transitions into queryable records and makes failures explicit (`*.failed`) instead of silent.",
"alternatives":"Relying only on console logs is lighter but fragile during long sessions and hard to audit. A full event bus infrastructure is powerful but heavier than needed for this teaching baseline.",
"title":"Hook-Style Extensions via Event Triplets",
"description":"Treat `before/after/failed` lifecycle emissions as extension points. Keep source-of-truth state writes in task/worktree files, and run side effects (audit, notification, policy checks) in event consumers.",
"alternatives":"Embedding every side effect directly in create/remove logic couples concerns tightly and makes failure handling harder. Moving source-of-truth to event replay is also risky without strict idempotency/repair semantics.",
"description":"`worktree_remove(..., complete_task=true)` allows a single closeout step: remove the isolated directory and mark the bound task completed. In this course model, closeout remains an explicit tool-driven transition (`worktree_keep` / `worktree_remove`) rather than hidden automatic cleanup. This reduces dangling state where a task says done but its temporary lane remains active (or the reverse).",
"alternatives":"Keeping closeout fully manual gives flexibility but increases operational drift. Fully automatic removal on every completion risks deleting a workspace before final review.",
"title":"Event Stream Is Observability Side-Channel",
"description":"Lifecycle events improve auditability, but the source of truth remains task/worktree state files. Events should be read as transition traces, not as a replacement state machine.",
"alternatives":"Using logs alone hides structured transitions; using events as the only state source risks drift when replay/repair semantics are undefined.",
"content":"# s01: The Agent Loop\n\n> The entire secret of AI coding agents is a while loop that feeds tool results back to the model until the model decides to stop.\n\n## The Problem\n\nWhy can't a language model just answer a coding question? Because coding\nrequires _interaction with the real world_. The model needs to read files,\nrun tests, check errors, and iterate. A single prompt-response pair cannot\ndo this.\n\nWithout the agent loop, you would have to copy-paste outputs back into the\nmodel yourself. The user becomes the loop. The agent loop automates this:\ncall the model, execute whatever tools it asks for, feed the results back,\nrepeat until the model says \"I'm done.\"\n\nConsider a simple task: \"Create a Python file that prints hello.\" The model\nneeds to (1) decide to write a file, (2) write it, (3) verify it works.\nThat is three tool calls minimum. Without a loop, each one requires manual\nhuman intervention.\n\n## The Solution\n\n```\n+----------+ +-------+ +---------+\n| User | ---> | LLM | ---> | Tool |\n| prompt | | | | execute |\n+----------+ +---+---+ +----+----+\n ^ |\n | tool_result |\n +---------------+\n (loop continues)\n\nThe loop terminates when stop_reason != \"tool_use\".\nThat single condition is the entire control flow.\n```\n\n## How It Works\n\n1. The user provides a prompt. It becomes the first message.\n\n```python\nhistory.append({\"role\": \"user\", \"content\": query})\n```\n\n2. The messages array is sent to the LLM along with the tool definitions.\n\n```python\nresponse = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n)\n```\n\n3. The assistant response is appended to messages.\n\n```python\nmessages.append({\"role\": \"assistant\", \"content\": response.content})\n```\n\n4. We check the stop reason. If the model did not call a tool, the loop\n ends. This is the only exit condition.\n\n```python\nif response.stop_reason != \"tool_use\":\n return\n```\n\n5. For each tool_use block in the response, execute the tool (bash in this\n session) and collect results.\n\n```python\nfor block in response.content:\n if block.type == \"tool_use\":\n output = run_bash(block.input[\"command\"])\n results.append({\n \"type\": \"tool_result\",\n \"tool_use_id\": block.id,\n \"content\": output,\n })\n```\n\n6. The results are appended as a user message, and the loop continues.\n\n```python\nmessages.append({\"role\": \"user\", \"content\": results})\n```\n\n## Key Code\n\nThe minimum viable agent -- the entire pattern in under 30 lines\n(from `agents/s01_agent_loop.py`, lines 66-86):\n\n```python\ndef agent_loop(messages: list):\n while True:\n response = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n )\n messages.append({\"role\": \"assistant\", \"content\": response.content})\n if response.stop_reason != \"tool_use\":\n return\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n output = run_bash(block.input[\"command\"])\n results.append({\n \"type\": \"tool_result\",\n \"tool_use_id\": block.id,\n \"content\": output,\n })\n messages.append({\"role\": \"user\", \"content\": results})\n```\n\n## What Changed\n\nThis is session 1 -- the starting point. There is no prior session.\n\n| Component | Before | After |\n|---------------|------------|--------------------------------|\n| Agent loop | (none) | `while True` + stop_reason |\n| Tools | (none) | `bash` (one tool) |\n| Messages | (none) | Accumulating list |\n| Control flow | (none) | `stop_reason != \"tool_use\"` |\n\n## Design Rationale\n\nThis loop is the universal foundation of all LLM-based agents. Production implementations add error handling, token counting, streaming, and retry logic, but the fundamental structure is unchanged. The simplicity is the point: one exit condition (`stop_reason != \"tool_use\"`) controls the entire flow. Everything else in this course -- tools, planning, compression, teams -- layers on top of this loop without modifying it. Understanding this loop means understanding every agent.\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s01_agent_loop.py\n```\n\nExample prompts to try:\n\n1. `Create a file called hello.py that prints \"Hello, World!\"`\n2. `List all Python files in this directory`\n3. `What is the current git branch?`\n4. `Create a directory called test_output and write 3 files in it`\n"
"content":"# s01: The Agent Loop\n\n> The core of a coding agent is a while loop that feeds tool results back to the model until the model decides to stop.\n\n## The Problem\n\nWhy can't a language model just answer a coding question? Because coding\nrequires _interaction with the real world_. The model needs to read files,\nrun tests, check errors, and iterate. A single prompt-response pair cannot\ndo this.\n\nWithout the agent loop, you would have to copy-paste outputs back into the\nmodel yourself. The user becomes the loop. The agent loop automates this:\ncall the model, execute whatever tools it asks for, feed the results back,\nrepeat until the model says \"I'm done.\"\n\nConsider a simple task: \"Create a Python file that prints hello.\" The model\nneeds to (1) decide to write a file, (2) write it, (3) verify it works.\nThat is three tool calls minimum. Without a loop, each one requires manual\nhuman intervention.\n\n## The Solution\n\n```\n+----------+ +-------+ +---------+\n| User | ---> | LLM | ---> | Tool |\n| prompt | | | | execute |\n+----------+ +---+---+ +----+----+\n ^ |\n | tool_result |\n +---------------+\n (loop continues)\n\nThe loop terminates when stop_reason != \"tool_use\".\nThat single condition is the entire control flow.\n```\n\n## How It Works\n\n1. The user provides a prompt. It becomes the first message.\n\n```python\nhistory.append({\"role\": \"user\", \"content\": query})\n```\n\n2. The messages array is sent to the LLM along with the tool definitions.\n\n```python\nresponse = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n)\n```\n\n3. The assistant response is appended to messages.\n\n```python\nmessages.append({\"role\": \"assistant\", \"content\": response.content})\n```\n\n4. We check the stop reason. If the model did not call a tool, the loop\n ends. In this minimal lesson implementation, this is the only loop exit\n condition.\n\n```python\nif response.stop_reason != \"tool_use\":\n return\n```\n\n5. For each tool_use block in the response, execute the tool (bash in this\n session) and collect results.\n\n```python\nfor block in response.content:\n if block.type == \"tool_use\":\n output = run_bash(block.input[\"command\"])\n results.append({\n \"type\": \"tool_result\",\n \"tool_use_id\": block.id,\n \"content\": output,\n })\n```\n\n6. The results are appended as a user message, and the loop continues.\n\n```python\nmessages.append({\"role\": \"user\", \"content\": results})\n```\n\n## Key Code\n\nThe minimum viable agent -- the entire pattern in under 30 lines\n(from `agents/s01_agent_loop.py`, lines 66-86):\n\n```python\ndef agent_loop(messages: list):\n while True:\n response = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n )\n messages.append({\"role\": \"assistant\", \"content\": response.content})\n if response.stop_reason != \"tool_use\":\n return\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n output = run_bash(block.input[\"command\"])\n results.append({\n \"type\": \"tool_result\",\n \"tool_use_id\": block.id,\n \"content\": output,\n })\n messages.append({\"role\": \"user\", \"content\": results})\n```\n\n## What Changed\n\nThis is session 1 -- the starting point. There is no prior session.\n\n| Component | Before | After |\n|---------------|------------|--------------------------------|\n| Agent loop | (none) | `while True` + stop_reason |\n| Tools | (none) | `bash` (one tool) |\n| Messages | (none) | Accumulating list |\n| Control flow | (none) | `stop_reason != \"tool_use\"` |\n\n## Design Rationale\n\nThis loop is the foundation of LLM-based agents. Production implementations add error handling, token counting, streaming, retry logic, permission policy, and lifecycle orchestration, but the core interaction pattern still starts here. The simplicity is the point for this session: in this minimal implementation, one exit condition (`stop_reason != \"tool_use\"`) controls the flow we need to learn first. Everything else in this course layers on top of this loop. Understanding this loop gives you the base model, not the full production architecture.\n\n## Try It\n\n```sh\ncd learn-claude-code\npython agents/s01_agent_loop.py\n```\n\nExample prompts to try:\n\n1. `Create a file called hello.py that prints \"Hello, World!\"`\n2.`ListallPythonfilesinthisdirectory`\n3.`WhatisthecurrentgLine truncated
},
{
"version":"s02",
@@ -15,43 +15,43 @@
"version":"s03",
"locale":"en",
"title":"s03: TodoWrite",
"content":"# s03: TodoWrite\n\n> A TodoManager lets the agent track its own progress, and a nag reminder injection forces it to keep updating when it forgets.\n\n## The Problem\n\nWhen an agent works on a multi-step task, it often loses track of what it\nhas done and what remains. Without explicit planning, the model might repeat\nwork, skip steps, or wander off on tangents. The user has no visibility\ninto the agent's internal plan.\n\nThis is worse than it sounds. Long conversations cause the model to \"drift\"\n-- the system prompt fades in influence as the context window fills with\ntool results. A 10-step refactoring task might complete steps 1-3, then\nthe model starts improvising because it forgot steps 4-10 existed.\n\nThe solution is structured state: a TodoManager that the model writes to\nexplicitly. The model creates a plan, marks items in_progress as it works,\nand marks them completed when done. A nag reminder injects a nudge if the\nmodel goes 3+ rounds without updating its todos.\n\nTeaching simplification: the nag threshold of 3 rounds is set low for\nteaching visibility. Production agents typically use a higher threshold\naround 10 to avoid excessive prompting.\n\n## The Solution\n\n```\n+----------+ +-------+ +---------+\n| User | ---> | LLM | ---> | Tools |\n| prompt | | | | + todo |\n+----------+ +---+---+ +----+----+\n ^ |\n | tool_result |\n +---------------+\n |\n +-----------+-----------+\n | TodoManager state |\n | [ ] task A |\n | [>] task B <- doing |\n | [x] task C |\n +-----------------------+\n |\n if rounds_since_todo >= 3:\n inject <reminder> into tool_result\n```\n\n## How It Works\n\n1. The TodoManager validates and stores a list of items with statuses.\n Only one item can be `in_progress` at a time.\n\n```python\nclass TodoManager:\n def __init__(self):\n self.items = []\n\n def update(self, items: list) -> str:\n validated = []\n in_progress_count = 0\n for item in items:\n status = item.get(\"status\", \"pending\")\n if status == \"in_progress\":\n in_progress_count += 1\n validated.append({\n \"id\": item[\"id\"],\n \"text\": item[\"text\"],\n \"status\": status,\n })\n if in_progress_count > 1:\n raise ValueError(\"Only one task can be in_progress\")\n self.items = validated\n return self.render()\n```\n\n2. The `todo` tool is added to the dispatch map like any other tool.\n\n```python\nTOOL_HANDLERS = {\n \"bash\": lambda **kw: run_bash(kw[\"command\"]),\n # ...other tools...\n \"todo\": lambda **kw: TODO.update(kw[\"items\"]),\n}\n```\n\n3. The nag reminder injects a `<reminder>` tag into the tool_result\n messages when the model goes 3+ rounds without calling `todo`.\n\n```python\ndef agent_loop(messages: list):\n rounds_since_todo = 0\n while True:\n if rounds_since_todo >= 3 and messages:\n last = messages[-1]\n if (last[\"role\"] == \"user\"\n and isinstance(last.get(\"content\"), list)):\n last[\"content\"].insert(0, {\n \"type\": \"text\",\n \"text\": \"<reminder>Update your todos.</reminder>\",\n })\n # ... rest of loop ...\n rounds_since_todo = 0 if used_todo else rounds_since_todo + 1\n```\n\n4. The system prompt instructs the model to use todos for planning.\n\n```python\nSYSTEM = f\"\"\"You are a coding agent at {WORKDIR}.\nUse the todo tool to plan multi-step tasks.\nMark in_progress before starting, completed when done.\nPrefer tools over prose.\"\"\"\n```\n\n## Key Code\n\nThe TodoManager and nag injection (from `agents/s03_todo_write.py`,\nlines 51-85 and 158-187):\n\n```python\nclass TodoManager:\n def update(self, items: list) -> str:\n validated = []\n in_progress_count = 0\n for item in items:\n status = item.get(\"status\", \"pending\")\n if status == \"in_progress\":\n in_progress_count += 1\n validated.append({\n \"id\": item[\"id\"],\n \"text\": item[\"text\"],\n \"status\": status,\n })\n if in_progress_count > 1:\n raise ValueError(\"Only one in_progress\")\n self.items = validated\n return self.render()\n\n# In agent_loop:\nif rounds_since_todo >= 3:\n last[\"content\"].insert(0, {\n \"type\": \"text\",\n \"text\": \"<reminder>Update your todos.</reminder>\",\n})\n```\n\n##WhatChangedFroms02\n\n|Component|BLine truncated
"content":"# s03: TodoWrite\n\n> A TodoManager lets the agent track its own progress, and a nag reminder injection forces it to keep updating when it forgets.\n\n## The Problem\n\nWhen an agent works on a multi-step task, it often loses track of what it\nhas done and what remains. Without explicit planning, the model might repeat\nwork, skip steps, or wander off on tangents. The user has no visibility\ninto the agent's internal plan.\n\nThis is worse than it sounds. Long conversations cause the model to \"drift\"\n-- the system prompt fades in influence as the context window fills with\ntool results. A 10-step refactoring task might complete steps 1-3, then\nthe model starts improvising because it forgot steps 4-10 existed.\n\nThe solution is structured state: a TodoManager that the model writes to\nexplicitly. The model creates a plan, marks items in_progress as it works,\nand marks them completed when done. A nag reminder injects a nudge if the\nmodel goes 3+ rounds without updating its todos.\n\nNote: the nag threshold of 3 rounds is low for visibility. Production systems tune higher. From s07, this course switches to the Task board for durable multi-step work; TodoWrite remains available for quick checklists.\n\n## The Solution\n\n```\n+----------+ +-------+ +---------+\n| User | ---> | LLM | ---> | Tools |\n| prompt | | | | + todo |\n+----------+ +---+---+ +----+----+\n ^ |\n | tool_result |\n +---------------+\n |\n +-----------+-----------+\n | TodoManager state |\n | [ ] task A |\n | [>] task B <- doing |\n | [x] task C |\n +-----------------------+\n |\n if rounds_since_todo >= 3:\n inject <reminder> into tool_result\n```\n\n## How It Works\n\n1. The TodoManager validates and stores a list of items with statuses.\n Only one item can be `in_progress` at a time.\n\n```python\nclass TodoManager:\n def __init__(self):\n self.items = []\n\n def update(self, items: list) -> str:\n validated = []\n in_progress_count = 0\n for item in items:\n status = item.get(\"status\", \"pending\")\n if status == \"in_progress\":\n in_progress_count += 1\n validated.append({\n \"id\": item[\"id\"],\n \"text\": item[\"text\"],\n \"status\": status,\n })\n if in_progress_count > 1:\n raise ValueError(\"Only one task can be in_progress\")\n self.items = validated\n return self.render()\n```\n\n2. The `todo` tool is added to the dispatch map like any other tool.\n\n```python\nTOOL_HANDLERS = {\n \"bash\": lambda **kw: run_bash(kw[\"command\"]),\n # ...other tools...\n \"todo\": lambda **kw: TODO.update(kw[\"items\"]),\n}\n```\n\n3. The nag reminder injects a `<reminder>` tag into the tool_result\n messages when the model goes 3+ rounds without calling `todo`.\n\n```python\ndef agent_loop(messages: list):\n rounds_since_todo = 0\n while True:\n if rounds_since_todo >= 3 and messages:\n last = messages[-1]\n if (last[\"role\"] == \"user\"\n and isinstance(last.get(\"content\"), list)):\n last[\"content\"].insert(0, {\n \"type\": \"text\",\n \"text\": \"<reminder>Update your todos.</reminder>\",\n })\n # ... rest of loop ...\n rounds_since_todo = 0 if used_todo else rounds_since_todo + 1\n```\n\n4. The system prompt instructs the model to use todos for planning.\n\n```python\nSYSTEM = f\"\"\"You are a coding agent at {WORKDIR}.\nUse the todo tool to plan multi-step tasks.\nMark in_progress before starting, completed when done.\nPrefer tools over prose.\"\"\"\n```\n\n## Key Code\n\nThe TodoManager and nag injection (from `agents/s03_todo_write.py`,\nlines 51-85 and 158-187):\n\n```python\nclass TodoManager:\n def update(self, items: list) -> str:\n validated = []\n in_progress_count = 0\n for item in items:\n status = item.get(\"status\", \"pending\")\n if status == \"in_progress\":\n in_progress_count += 1\n validated.append({\n \"id\": item[\"id\"],\n \"text\": item[\"text\"],\n \"status\": status,\n })\n if in_progress_count > 1:\n raise ValueError(\"Only one in_progress\")\n self.items = validated\n return self.render()\n\n# In agent_loop:\nif rounds_since_todo >= 3:\n last[\"content\"].insert(0, {\n \"type\": \"text\",\n \"text\": \"<reminder>Update your todos.</reminder>\",\n})\n```\n\n##WhatChangLine truncated
},
{
"version":"s04",
"locale":"en",
"title":"s04: Subagents",
"content":"# s04: Subagents\n\n> A subagent runs with a fresh messages list, shares the filesystem with the parent, and returns only a summary -- keeping the parent context clean.\n\n## The Problem\n\nAs the agent works, its messages array grows. Every tool call, every file\nread, every bash output accumulates. After 20-30 tool calls, the context\nwindow is crowded with irrelevant history. Reading a 500-line file to\nanswer a quick question permanently adds 500 lines to the context.\n\nThis is particularly bad for exploratory tasks. \"What testing framework\ndoes this project use?\" might require reading 5 files, but the parent\nagent does not need all 5 file contents in its history -- it just needs\nthe answer: \"pytest with conftest.py configuration.\"\n\nThe solution is process isolation: spawn a child agent with `messages=[]`.\nThe child explores, reads files, runs commands. When it finishes, only its\nfinal text response returns to the parent. The child's entire message\nhistory is discarded.\n\n## The Solution\n\n```\nParent agent Subagent\n+------------------+ +------------------+\n| messages=[...] | | messages=[] | <-- fresh\n| | dispatch | |\n| tool: task | ---------->| while tool_use: |\n| prompt=\"...\" | | call tools |\n| | summary | append results |\n| result = \"...\" | <--------- | return last text |\n+------------------+ +------------------+\n |\nParent context stays clean.\nSubagent context is discarded.\n```\n\n## How It Works\n\n1. The parent agent gets a `task` tool that triggers subagent spawning.\n The child gets all base tools except `task` (no recursive spawning).\n\n```python\nPARENT_TOOLS = CHILD_TOOLS + [\n {\"name\": \"task\",\n \"description\": \"Spawn a subagent with fresh context.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"prompt\": {\"type\": \"string\"},\n \"description\": {\"type\": \"string\"},\n },\n \"required\": [\"prompt\"],\n }},\n]\n```\n\n2. The subagent starts with a fresh messages list containing only\n the delegated prompt. It shares the same filesystem.\n\n```python\ndef run_subagent(prompt: str) -> str:\n sub_messages = [{\"role\": \"user\", \"content\": prompt}]\n for _ in range(30): # safety limit\n response = client.messages.create(\n model=MODEL, system=SUBAGENT_SYSTEM,\n messages=sub_messages,\n tools=CHILD_TOOLS, max_tokens=8000,\n )\n sub_messages.append({\n \"role\": \"assistant\", \"content\": response.content\n })\n if response.stop_reason != \"tool_use\":\n break\n # execute tools, append results...\n```\n\n3. Only the final text returns to the parent. The child's 30+ tool\n call history is discarded.\n\n```python\n return \"\".join(\n b.text for b in response.content if hasattr(b, \"text\")\n ) or \"(no summary)\"\n```\n\n4. The parent receives this summary as a normal tool_result.\n\n```python\nif block.name == \"task\":\n output = run_subagent(block.input[\"prompt\"])\nresults.append({\n \"type\": \"tool_result\",\n \"tool_use_id\": block.id,\n \"content\": str(output),\n})\n```\n\n## Key Code\n\nThe subagent function (from `agents/s04_subagent.py`,\nlines 110-128):\n\n```python\ndef run_subagent(prompt: str) -> str:\n sub_messages = [{\"role\": \"user\", \"content\": prompt}]\n for _ in range(30):\n response = client.messages.create(\n model=MODEL, system=SUBAGENT_SYSTEM,\n messages=sub_messages,\n tools=CHILD_TOOLS, max_tokens=8000,\n )\n sub_messages.append({\"role\": \"assistant\",\n \"content\": response.content})\n if response.stop_reason != \"tool_use\":\n break\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS.get(block.name)\n output = handler(**block.input)\n results.append({\"type\": \"tool_result\",\n \"tool_use_id\": block.id,\n \"content\": str(output)[:50000]})\n sub_messages.append({\"role\": \"user\", \"content\": results})\n return \"\".join(\n b.text for b in response.content if hasattr(b, \"text\")\n ) or \"(no summary)\"\n```\n\n##WhatChangedFroms03\n\n|Component|Before(s03)|After(s04)|\n|----------------|------------------|---------------------------|\n|Tools|5|5(base)+task(parent)|\n|Context|Singleshared|Parent+childisolation|\n|Subagent|None|`run_subagent()`function|\n|Returnvalue|N/A|SummarytextonlyLine truncated
"content":"# s04: Subagents\n\n> A subagent runs with a fresh messages list, shares the filesystem with the parent, and returns only a summary -- keeping the parent context clean.\n\n## The Problem\n\nAs the agent works, its messages array grows. Every tool call, every file\nread, every bash output accumulates. After 20-30 tool calls, the context\nwindow is crowded with irrelevant history. Reading a 500-line file to\nanswer a quick question permanently adds 500 lines to the context.\n\nThis is particularly bad for exploratory tasks. \"What testing framework\ndoes this project use?\" might require reading 5 files, but the parent\nagent does not need all 5 file contents in its history -- it just needs\nthe answer: \"pytest with conftest.py configuration.\"\n\nIn this course, a practical solution is fresh-context isolation: spawn a child agent with `messages=[]`.\nThe child explores, reads files, runs commands. When it finishes, only its\nfinal text response returns to the parent. The child's entire message\nhistory is discarded.\n\n## The Solution\n\n```\nParent agent Subagent\n+------------------+ +------------------+\n| messages=[...] | | messages=[] | <-- fresh\n| | dispatch | |\n| tool: task | ---------->| while tool_use: |\n| prompt=\"...\" | | call tools |\n| | summary | append results |\n| result = \"...\" | <--------- | return last text |\n+------------------+ +------------------+\n |\nParent context stays clean.\nSubagent context is discarded.\n```\n\n## How It Works\n\n1. The parent agent gets a `task` tool that triggers subagent spawning.\n The child gets all base tools except `task` (no recursive spawning).\n\n```python\nPARENT_TOOLS = CHILD_TOOLS + [\n {\"name\": \"task\",\n \"description\": \"Spawn a subagent with fresh context.\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"prompt\": {\"type\": \"string\"},\n \"description\": {\"type\": \"string\"},\n },\n \"required\": [\"prompt\"],\n }},\n]\n```\n\n2. The subagent starts with a fresh messages list containing only\n the delegated prompt. It shares the same filesystem.\n\n```python\ndef run_subagent(prompt: str) -> str:\n sub_messages = [{\"role\": \"user\", \"content\": prompt}]\n for _ in range(30): # safety limit\n response = client.messages.create(\n model=MODEL, system=SUBAGENT_SYSTEM,\n messages=sub_messages,\n tools=CHILD_TOOLS, max_tokens=8000,\n )\n sub_messages.append({\n \"role\": \"assistant\", \"content\": response.content\n })\n if response.stop_reason != \"tool_use\":\n break\n # execute tools, append results...\n```\n\n3. Only the final text returns to the parent. The child's 30+ tool\n call history is discarded.\n\n```python\n return \"\".join(\n b.text for b in response.content if hasattr(b, \"text\")\n ) or \"(no summary)\"\n```\n\n4. The parent receives this summary as a normal tool_result.\n\n```python\nif block.name == \"task\":\n output = run_subagent(block.input[\"prompt\"])\nresults.append({\n \"type\": \"tool_result\",\n \"tool_use_id\": block.id,\n \"content\": str(output),\n})\n```\n\n## Key Code\n\nThe subagent function (from `agents/s04_subagent.py`,\nlines 110-128):\n\n```python\ndef run_subagent(prompt: str) -> str:\n sub_messages = [{\"role\": \"user\", \"content\": prompt}]\n for _ in range(30):\n response = client.messages.create(\n model=MODEL, system=SUBAGENT_SYSTEM,\n messages=sub_messages,\n tools=CHILD_TOOLS, max_tokens=8000,\n )\n sub_messages.append({\"role\": \"assistant\",\n \"content\": response.content})\n if response.stop_reason != \"tool_use\":\n break\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS.get(block.name)\n output = handler(**block.input)\n results.append({\"type\": \"tool_result\",\n \"tool_use_id\": block.id,\n \"content\": str(output)[:50000]})\n sub_messages.append({\"role\": \"user\", \"content\": results})\n return \"\".join(\n b.text for b in response.content if hasattr(b, \"text\")\n ) or \"(no summary)\"\n```\n\n##WhatChangedFroms03\n\n|Component|Before(s03)|After(s04)|\n|----------------|------------------|---------------------------|\n|Tools|5|5(base)+task(parent)|\n|Context|Singleshared|Parent+childisolation|\n|Subagent|None|`run_subagent()`function|\n|Returnvalue|N/ALine truncated
},
{
"version":"s05",
"locale":"en",
"title":"s05: Skills",
"content":"# s05: Skills\n\n> Two-layer skill injection avoids system prompt bloat by putting skill names in the system prompt (cheap) and full skill bodies in tool_result (on demand).\n\n## The Problem\n\nYou want the agent to follow specific workflows for different domains:\ngit conventions, testing patterns, code review checklists. The naive\napproach is to put everything in the system prompt. But the system prompt\nhas limited effective attention -- too much text and the model starts\nignoring parts of it.\n\nIf you have 10 skills at 2000 tokens each, that is 20,000 tokens of system\nprompt. The model pays attention to the beginning and end but skims the\nmiddle. Worse, most of those skills are irrelevant to any given task. A\nfile editing task does not need the git workflow instructions.\n\nThe two-layer approach solves this: Layer 1 puts short skill descriptions\nin the system prompt (~100 tokens per skill). Layer 2 loads the full skill\nbody into a tool_result only when the model calls `load_skill`. The model\nlearns what skills exist (cheap) and loads them on demand (only when\nrelevant).\n\n## The Solution\n\n```\nSystem prompt (Layer 1 -- always present):\n+--------------------------------------+\n| You are a coding agent. |\n| Skills available: |\n| - git: Git workflow helpers | ~100 tokens/skill\n| - test: Testing best practices |\n+--------------------------------------+\n\nWhen model calls load_skill(\"git\"):\n+--------------------------------------+\n| tool_result (Layer 2 -- on demand): |\n| <skill name=\"git\"> |\n| Full git workflow instructions... | ~2000 tokens\n| Step 1: ... |\n| Step 2: ... |\n| </skill> |\n+--------------------------------------+\n```\n\n## How It Works\n\n1. Skill files live in `.skills/` as Markdown with YAML frontmatter.\n\n```\n.skills/\n git.md # ---\\n description: Git workflow\\n ---\\n ...\n test.md # ---\\n description: Testing patterns\\n ---\\n ...\n```\n\n2. The SkillLoader parses frontmatter and separates metadata from body.\n\n```python\nclass SkillLoader:\n def _parse_frontmatter(self, text: str) -> tuple:\n match = re.match(\n r\"^---\\n(.*?)\\n---\\n(.*)\", text, re.DOTALL\n )\n if not match:\n return {}, text\n meta = {}\n for line in match.group(1).strip().splitlines():\n if \":\" in line:\n key, val = line.split(\":\", 1)\n meta[key.strip()] = val.strip()\n return meta, match.group(2).strip()\n```\n\n3. Layer 1: `get_descriptions()` returns short lines for the system prompt.\n\n```python\ndef get_descriptions(self) -> str:\n lines = []\n for name, skill in self.skills.items():\n desc = skill[\"meta\"].get(\"description\", \"No description\")\n lines.append(f\" - {name}: {desc}\")\n return \"\\n\".join(lines)\n\nSYSTEM = f\"\"\"You are a coding agent at {WORKDIR}.\nSkills available:\n{SKILL_LOADER.get_descriptions()}\"\"\"\n```\n\n4. Layer 2: `get_content()` returns the full body wrapped in `<skill>` tags.\n\n```python\ndef get_content(self, name: str) -> str:\n skill = self.skills.get(name)\n if not skill:\n return f\"Error: Unknown skill '{name}'.\"\n return f\"<skill name=\\\"{name}\\\">\\n{skill['body']}\\n</skill>\"\n```\n\n5. The `load_skill` tool is just another entry in the dispatch map.\n\n```python\nTOOL_HANDLERS = {\n # ...base tools...\n \"load_skill\": lambda **kw: SKILL_LOADER.get_content(kw[\"name\"]),\n}\n```\n\n## Key Code\n\nThe SkillLoader class (from `agents/s05_skill_loading.py`,\nlines 51-97):\n\n```python\nclass SkillLoader:\n def __init__(self, skills_dir: Path):\n self.skills = {}\n for f in sorted(skills_dir.glob(\"*.md\")):\n text = f.read_text()\n meta, body = self._parse_frontmatter(text)\n self.skills[f.stem] = {\n \"meta\": meta, \"body\": body\n }\n\n def get_descriptions(self) -> str:\n lines = []\n for name, skill in self.skills.items():\n desc = skill[\"meta\"].get(\"description\", \"\")\n lines.append(f\" - {name}: {desc}\")\n return \"\\n\".join(lines)\n\n def get_content(self, name: str) -> str:\n skill = self.skills.get(name)\n if not skill:\n return f\"Error: Unknown skill '{name}'.\"\n return (f\"<skill name=\\\"{name}\\\">\\n\"\n f\"{skill['body']}\\n</skill>\")\n```\n\n##WhatChangedFroms04\n\n|Component|Before(s04)|After(s05)|\n|----------------|------------------|----------------------------|\n|Tools|5(base+task)|5(base+load_skill)|\n|Systemprompt|Staticstring|+skilldescriptions|\n|Knowledge|None|.skills/*.mdfilesLine truncated
"content":"# s05: Skills\n\n> Two-layer skill injection avoids system prompt bloat by putting skill names in the system prompt (cheap) and full skill bodies in tool_result (on demand).\n\n## The Problem\n\nYou want the agent to follow specific workflows for different domains:\ngit conventions, testing patterns, code review checklists. The naive\napproach is to put everything in the system prompt. But the system prompt\nhas limited effective attention -- too much text and the model starts\nignoring parts of it.\n\nIf you have 10 skills at 2000 tokens each, that is 20,000 tokens of system\nprompt. The model pays attention to the beginning and end but skims the\nmiddle. Worse, most of those skills are irrelevant to any given task. A\nfile editing task does not need the git workflow instructions.\n\nThe two-layer approach solves this: Layer 1 puts short skill descriptions\nin the system prompt (~100 tokens per skill). Layer 2 loads the full skill\nbody into a tool_result only when the model calls `load_skill`. The model\nlearns what skills exist (cheap) and loads them on demand (only when\nrelevant).\n\n## The Solution\n\n```\nSystem prompt (Layer 1 -- always present):\n+--------------------------------------+\n| You are a coding agent. |\n| Skills available: |\n| - git: Git workflow helpers | ~100 tokens/skill\n| - test: Testing best practices |\n+--------------------------------------+\n\nWhen model calls load_skill(\"git\"):\n+--------------------------------------+\n| tool_result (Layer 2 -- on demand): |\n| <skill name=\"git\"> |\n| Full git workflow instructions... | ~2000 tokens\n| Step 1: ... |\n| Step 2: ... |\n| </skill> |\n+--------------------------------------+\n```\n\n## How It Works\n\n1. Skill files live in `.skills/` as Markdown with YAML frontmatter.\n\n```\n.skills/\n git.md # ---\\n description: Git workflow\\n ---\\n ...\n test.md # ---\\n description: Testing patterns\\n ---\\n ...\n```\n\n2. The SkillLoader parses frontmatter and separates metadata from body.\n\n```python\nclass SkillLoader:\n def _parse_frontmatter(self, text: str) -> tuple:\n match = re.match(\n r\"^---\\n(.*?)\\n---\\n(.*)\", text, re.DOTALL\n )\n if not match:\n return {}, text\n meta = {}\n for line in match.group(1).strip().splitlines():\n if \":\" in line:\n key, val = line.split(\":\", 1)\n meta[key.strip()] = val.strip()\n return meta, match.group(2).strip()\n```\n\n3. Layer 1: `get_descriptions()` returns short lines for the system prompt.\n\n```python\ndef get_descriptions(self) -> str:\n lines = []\n for name, skill in self.skills.items():\n desc = skill[\"meta\"].get(\"description\", \"No description\")\n lines.append(f\" - {name}: {desc}\")\n return \"\\n\".join(lines)\n\nSYSTEM = f\"\"\"You are a coding agent at {WORKDIR}.\nSkills available:\n{SKILL_LOADER.get_descriptions()}\"\"\"\n```\n\n4. Layer 2: `get_content()` returns the full body wrapped in `<skill>` tags.\n\n```python\ndef get_content(self, name: str) -> str:\n skill = self.skills.get(name)\n if not skill:\n return f\"Error: Unknown skill '{name}'.\"\n return f\"<skill name=\\\"{name}\\\">\\n{skill['body']}\\n</skill>\"\n```\n\n5. The `load_skill` tool is just another entry in the dispatch map.\n\n```python\nTOOL_HANDLERS = {\n # ...base tools...\n \"load_skill\": lambda **kw: SKILL_LOADER.get_content(kw[\"name\"]),\n}\n```\n\n## Key Code\n\nThe SkillLoader class (from `agents/s05_skill_loading.py`,\nlines 51-97):\n\n```python\nclass SkillLoader:\n def __init__(self, skills_dir: Path):\n self.skills = {}\n for f in sorted(skills_dir.glob(\"*.md\")):\n text = f.read_text()\n meta, body = self._parse_frontmatter(text)\n self.skills[f.stem] = {\n \"meta\": meta, \"body\": body\n }\n\n def get_descriptions(self) -> str:\n lines = []\n for name, skill in self.skills.items():\n desc = skill[\"meta\"].get(\"description\", \"\")\n lines.append(f\" - {name}: {desc}\")\n return \"\\n\".join(lines)\n\n def get_content(self, name: str) -> str:\n skill = self.skills.get(name)\n if not skill:\n return f\"Error: Unknown skill '{name}'.\"\n return (f\"<skill name=\\\"{name}\\\">\\n\"\n f\"{skill['body']}\\n</skill>\")\n```\n\n##WhatChangedFroms04\n\n|Component|Before(s04)|After(s05)|\n|----------------|------------------|----------------------------|\n|Tools|5(base+task)|5(base+load_skill)|\n|Systemprompt|Staticstring|+skilldescriptions|\n|Knowledge|None|.skills/*.mdfilesLine truncated
},
{
"version":"s06",
"locale":"en",
"title":"s06: Compact",
"content":"# s06: Compact\n\n> A three-layer compression pipeline lets the agent work indefinitely by strategically forgetting old tool results, auto-summarizing when tokens exceed a threshold, and allowing manual compression on demand.\n\n## The Problem\n\nThe context window is finite. After enough tool calls, the messages array\nexceeds the model's context limit and the API call fails. Even before\nhitting the hard limit, performance degrades: the model becomes slower,\nless accurate, and starts ignoring earlier messages.\n\nA 200,000 token context window sounds large, but a single `read_file` on\na 1000-line source file consumes ~4000 tokens. After reading 30 files and\nrunning 20 bash commands, you are at 100,000+ tokens. The agent cannot\nwork on large codebases without some form of compression.\n\nThe three-layer pipeline addresses this with increasing aggressiveness:\nLayer 1 (micro-compact) silently replaces old tool results every turn.\nLayer 2 (auto-compact) triggers a full summarization when tokens exceed\na threshold. Layer 3 (manual compact) lets the model trigger compression\nitself.\n\nTeaching simplification: the token estimation here uses a rough\ncharacters/4 heuristic. Production systems use proper tokenizer\nlibraries for accurate counts.\n\n## The Solution\n\n```\nEvery turn:\n+------------------+\n| Tool call result |\n+------------------+\n |\n v\n[Layer 1: micro_compact] (silent, every turn)\n Replace tool_result > 3 turns old\n with \"[Previous: used {tool_name}]\"\n |\n v\n[Check: tokens > 50000?]\n | |\n no yes\n | |\n v v\ncontinue [Layer 2: auto_compact]\n Save transcript to .transcripts/\n LLM summarizes conversation.\n Replace all messages with [summary].\n |\n v\n [Layer 3: compact tool]\n Model calls compact explicitly.\n Same summarization as auto_compact.\n```\n\n## How It Works\n\n1. **Layer 1 -- micro_compact**: Before each LLM call, find all\n tool_result entries older than the last 3 and replace their content.\n\n```python\ndef micro_compact(messages: list) -> list:\n tool_results = []\n for i, msg in enumerate(messages):\n if msg[\"role\"] == \"user\" and isinstance(msg.get(\"content\"), list):\n for j, part in enumerate(msg[\"content\"]):\n if isinstance(part, dict) and part.get(\"type\") == \"tool_result\":\n tool_results.append((i, j, part))\n if len(tool_results) <= KEEP_RECENT:\n return messages\n to_clear = tool_results[:-KEEP_RECENT]\n for _, _, part in to_clear:\n if len(part.get(\"content\", \"\")) > 100:\n tool_id = part.get(\"tool_use_id\", \"\")\n tool_name = tool_name_map.get(tool_id, \"unknown\")\n part[\"content\"] = f\"[Previous: used {tool_name}]\"\n return messages\n```\n\n2. **Layer 2 -- auto_compact**: When estimated tokens exceed 50,000,\n save the full transcript and ask the LLM to summarize.\n\n```python\ndef auto_compact(messages: list) -> list:\n TRANSCRIPT_DIR.mkdir(exist_ok=True)\n transcript_path = TRANSCRIPT_DIR / f\"transcript_{int(time.time())}.jsonl\"\n with open(transcript_path, \"w\") as f:\n for msg in messages:\n f.write(json.dumps(msg, default=str) + \"\\n\")\n response = client.messages.create(\n model=MODEL,\n messages=[{\"role\": \"user\", \"content\":\n \"Summarize this conversation for continuity...\"\n + json.dumps(messages, default=str)[:80000]}],\n max_tokens=2000,\n )\n summary = response.content[0].text\n return [\n {\"role\": \"user\", \"content\": f\"[Compressed]\\n\\n{summary}\"},\n {\"role\": \"assistant\", \"content\": \"Understood. Continuing.\"},\n]\n```\n\n3.**Layer3--manualcompact**:The`compact`tooltriggersthesame\nsummarizationondemand.\n\n```python\nifmanual_compact:\nmessages[:]=auto_compact(messages)\n```\n\n4.Theagentloopintegratesallthreelayers.\n\n```python\ndefagent_loop(messages:list):\nwhileTrue:\nmicro_compact(messages)\nifestimate_tokens(messages)>THRESHOLD:\nmessages[:]=auto_compact(messages)\nresponse=client.messages.create(...)\n#...toolexecution...\nifmanual_compact:\nmessages[:]=auto_compact(messages)\n```\n\n##KeyCode\n\nThethree-layerpipeline(from`agents/s06_context_compact.py`,\nlines67-93and189-223):\n\n```python\nTHRESHOLD=50000\nKEEP_RECENT=3\n\ndefmicro_compact(messages):\n#Replaceoldtoolresultswithplaceholders\n...\n\ndefauto_compact(messages):\n#Savetranscript,LLMsummarize,replacemessages\n...\n\ndefagent_loop(messages):\nwhileTrue:\nmicro_compact(messages)#Layer1\nLine truncated
"content":"# s06: Compact\n\n> A three-layer compression pipeline lets the agent work indefinitely by strategically forgetting old tool results, auto-summarizing when tokens exceed a threshold, and allowing manual compression on demand.\n\n## The Problem\n\nThe context window is finite. After enough tool calls, the messages array\nexceeds the model's context limit and the API call fails. Even before\nhitting the hard limit, performance degrades: the model becomes slower,\nless accurate, and starts ignoring earlier messages.\n\nA 200,000 token context window sounds large, but a single `read_file` on\na 1000-line source file consumes ~4000 tokens. After reading 30 files and\nrunning 20 bash commands, you are at 100,000+ tokens. The agent cannot\nwork on large codebases without some form of compression.\n\nThe three-layer pipeline addresses this with increasing aggressiveness:\nLayer 1 (micro-compact) silently replaces old tool results every turn.\nLayer 2 (auto-compact) triggers a full summarization when tokens exceed\na threshold. Layer 3 (manual compact) lets the model trigger compression\nitself.\n\nTeaching simplification: the token estimation here uses a rough\ncharacters/4 heuristic. Production systems use proper tokenizer\nlibraries for accurate counts.\n\n## The Solution\n\n```\nEvery turn:\n+------------------+\n| Tool call result |\n+------------------+\n |\n v\n[Layer 1: micro_compact] (silent, every turn)\n Replace tool_result > 3 turns old\n with \"[Previous: used {tool_name}]\"\n |\n v\n[Check: tokens > 50000?]\n | |\n no yes\n | |\n v v\ncontinue [Layer 2: auto_compact]\n Save transcript to .transcripts/\n LLM summarizes conversation.\n Replace all messages with [summary].\n |\n v\n [Layer 3: compact tool]\n Model calls compact explicitly.\n Same summarization as auto_compact.\n```\n\n## How It Works\n\n1. **Layer 1 -- micro_compact**: Before each LLM call, find all\n tool_result entries older than the last 3 and replace their content.\n\n```python\ndef micro_compact(messages: list) -> list:\n tool_results = []\n for i, msg in enumerate(messages):\n if msg[\"role\"] == \"user\" and isinstance(msg.get(\"content\"), list):\n for j, part in enumerate(msg[\"content\"]):\n if isinstance(part, dict) and part.get(\"type\") == \"tool_result\":\n tool_results.append((i, j, part))\n if len(tool_results) <= KEEP_RECENT:\n return messages\n to_clear = tool_results[:-KEEP_RECENT]\n for _, _, part in to_clear:\n if len(part.get(\"content\", \"\")) > 100:\n tool_id = part.get(\"tool_use_id\", \"\")\n tool_name = tool_name_map.get(tool_id, \"unknown\")\n part[\"content\"] = f\"[Previous: used {tool_name}]\"\n return messages\n```\n\n2. **Layer 2 -- auto_compact**: When estimated tokens exceed 50,000,\n save the full transcript and ask the LLM to summarize.\n\n```python\ndef auto_compact(messages: list) -> list:\n TRANSCRIPT_DIR.mkdir(exist_ok=True)\n transcript_path = TRANSCRIPT_DIR / f\"transcript_{int(time.time())}.jsonl\"\n with open(transcript_path, \"w\") as f:\n for msg in messages:\n f.write(json.dumps(msg, default=str) + \"\\n\")\n response = client.messages.create(\n model=MODEL,\n messages=[{\"role\": \"user\", \"content\":\n \"Summarize this conversation for continuity...\"\n + json.dumps(messages, default=str)[:80000]}],\n max_tokens=2000,\n )\n summary = response.content[0].text\n return [\n {\"role\": \"user\", \"content\": f\"[Compressed]\\n\\n{summary}\"},\n {\"role\": \"assistant\", \"content\": \"Understood. Continuing.\"},\n]\n```\n\n3.**Layer3--manualcompact**:The`compact`tooltriggersthesame\nsummarizationondemand.\n\n```python\nifmanual_compact:\nmessages[:]=auto_compact(messages)\n```\n\n4.Theagentloopintegratesallthreelayers.\n\n```python\ndefagent_loop(messages:list):\nwhileTrue:\nmicro_compact(messages)\nifestimate_tokens(messages)>THRESHOLD:\nmessages[:]=auto_compact(messages)\nresponse=client.messages.create(...)\n#...toolexecution...\nifmanual_compact:\nmessages[:]=auto_compact(messages)\n```\n\n##KeyCode\n\nThethree-layerpipeline(from`agents/s06_context_compact.py`,\nlines67-93and189-223):\n\n```python\nTHRESHOLD=50000\nKEEP_RECENT=3\n\ndefmicro_compact(messages):\n#Replaceoldtoolresultswithplaceholders\n...\n\ndefauto_compact(messages):\n#Savetranscript,LLMsummarize,replacemessages\n...\n\ndefagent_loop(messages):\nwhileTrue:\nmicro_compact(messages)#Layer1\nLine truncated
},
{
"version":"s07",
"locale":"en",
"title":"s07: Tasks",
"content":"# s07: Tasks\n\n> Tasks persist as JSON files on the filesystem with a dependency graph, so they survive context compression and can be shared across agents.\n\n## The Problem\n\nIn-memory state like TodoManager (s03) is lost when the context is\ncompressed (s06). After auto_compact replaces messages with a summary,\nthe todo list is gone. The agent has to reconstruct it from the summary\ntext, which is lossy and error-prone.\n\nThis is the critical s06-to-s07 bridge: TodoManager items die with\ncompression; file-based tasks don't. Moving state to the filesystem\nmakes it compression-proof.\n\nMore fundamentally, in-memory state is invisible to other agents.\nWhen we eventually build teams (s09+), teammates need a shared task\nboard. In-memory data structures are process-local.\n\nThe solution is to persist tasks as JSON files in `.tasks/`. Each task\nis a separate file with an ID, subject, status, and dependency graph.\nCompleting task 1 automatically unblocks task 2 if task 2 has\n`blockedBy: [1]`. The file system becomes the source of truth.\n\n## The Solution\n\n```\n.tasks/\n task_1.json {\"id\":1, \"status\":\"completed\", ...}\n task_2.json {\"id\":2, \"blockedBy\":[1], \"status\":\"pending\"}\n task_3.json {\"id\":3, \"blockedBy\":[2], \"status\":\"pending\"}\n\nDependency resolution:\n+----------+ +----------+ +----------+\n| task 1 | --> | task 2 | --> | task 3 |\n| complete | | blocked | | blocked |\n+----------+ +----------+ +----------+\n | ^\n +--- completing task 1 removes it from\n task 2's blockedBy list\n```\n\n## How It Works\n\n1. The TaskManager provides CRUD operations. Each task is a JSON file.\n\n```python\nclass TaskManager:\n def create(self, subject: str, description: str = \"\") -> str:\n task = {\n \"id\": self._next_id,\n \"subject\": subject,\n \"description\": description,\n \"status\": \"pending\",\n \"blockedBy\": [],\n \"blocks\": [],\n \"owner\": \"\",\n }\n self._save(task)\n self._next_id += 1\n return json.dumps(task, indent=2)\n```\n\n2. When a task is marked completed, `_clear_dependency` removes its ID\n from all other tasks' `blockedBy` lists.\n\n```python\ndef _clear_dependency(self, completed_id: int):\n for f in self.dir.glob(\"task_*.json\"):\n task = json.loads(f.read_text())\n if completed_id in task.get(\"blockedBy\", []):\n task[\"blockedBy\"].remove(completed_id)\n self._save(task)\n```\n\n3. The `update` method handles status changes and bidirectional dependency\n wiring.\n\n```python\ndef update(self, task_id, status=None,\n add_blocked_by=None, add_blocks=None):\n task = self._load(task_id)\n if status:\n task[\"status\"] = status\n if status == \"completed\":\n self._clear_dependency(task_id)\n if add_blocks:\n task[\"blocks\"] = list(set(task[\"blocks\"] + add_blocks))\n for blocked_id in add_blocks:\n blocked = self._load(blocked_id)\n if task_id not in blocked[\"blockedBy\"]:\n blocked[\"blockedBy\"].append(task_id)\n self._save(blocked)\n self._save(task)\n```\n\n4. Four task tools are added to the dispatch map.\n\n```python\nTOOL_HANDLERS = {\n # ...base tools...\n \"task_create\": lambda **kw: TASKS.create(kw[\"subject\"]),\n \"task_update\": lambda **kw: TASKS.update(kw[\"task_id\"],\n kw.get(\"status\")),\n \"task_list\": lambda **kw: TASKS.list_all(),\n \"task_get\": lambda **kw: TASKS.get(kw[\"task_id\"]),\n}\n```\n\n## Key Code\n\nThe TaskManager with dependency graph (from `agents/s07_task_system.py`,\nlines 46-123):\n\n```python\nclass TaskManager:\n def __init__(self, tasks_dir: Path):\n self.dir = tasks_dir\n self.dir.mkdir(exist_ok=True)\n self._next_id = self._max_id() + 1\n\n def _load(self, task_id: int) -> dict:\n path = self.dir / f\"task_{task_id}.json\"\n return json.loads(path.read_text())\n\n def _save(self, task: dict):\n path = self.dir / f\"task_{task['id']}.json\"\n path.write_text(json.dumps(task, indent=2))\n\n def create(self, subject, description=\"\"):\n task = {\"id\": self._next_id, \"subject\": subject,\n \"status\": \"pending\", \"blockedBy\": [],\n \"blocks\": [], \"owner\": \"\"}\n self._save(task)\n self._next_id += 1\n return json.dumps(task, indent=2)\n\n def _clear_dependency(self, completed_id):\n for f in self.dir.glob(\"task_*.json\"):\n task = json.loads(f.read_text())\n if completed_id in task.get(\"blockedBy\", []):\n task[\"blockedBy\"].remove(completed_id)\nself._save(task)\n```\n\n##WhatChangedFroms06\n\n|Component|BeLine truncated
"content":"# s07: Tasks\n\n> Tasks are persisted as JSON files with a dependency graph, so state survives context compression and can be shared across agents.\n\n## Problem\n\nIn-memory state (for example the TodoManager from s03) is fragile under compression (s06). Once earlier turns are compacted into summaries, in-memory todo state is gone.\n\ns06 -> s07 is the key transition:\n\n1. Todo list state in memory is conversational and lossy.\n2. Task board state on disk is durable and recoverable.\n\nA second issue is visibility: in-memory structures are process-local, so teammates cannot reliably share that state.\n\n## When to Use Task vs Todo\n\nFrom s07 onward, Task is the default. Todo remains for short linear checklists.\n\n## Quick Decision Matrix\n\n| Situation | Prefer | Why |\n|---|---|---|\n| Short, single-session checklist | Todo | Lowest ceremony, fastest capture |\n| Cross-session work, dependencies, or teammates | Task | Durable state, dependency graph, shared visibility |\n| Unsure which one to use | Task | Easier to simplify later than migrate mid-run |\n\n## Solution\n\n```\n.tasks/\n task_1.json {\"id\":1, \"status\":\"completed\", ...}\n task_2.json {\"id\":2, \"blockedBy\":[1], \"status\":\"pending\"}\n task_3.json {\"id\":3, \"blockedBy\":[2], \"status\":\"pending\"}\n\nDependency resolution:\n+----------+ +----------+ +----------+\n| task 1 | --> | task 2 | --> | task 3 |\n| complete | | blocked | | blocked |\n+----------+ +----------+ +----------+\n | ^\n +--- completing task 1 removes it from\n task 2's blockedBy list\n```\n\n## How It Works\n\n1. TaskManager provides CRUD with one JSON file per task.\n\n```python\nclass TaskManager:\n def create(self, subject: str, description: str = \"\") -> str:\n task = {\n \"id\": self._next_id,\n \"subject\": subject,\n \"description\": description,\n \"status\": \"pending\",\n \"blockedBy\": [],\n \"blocks\": [],\n \"owner\": \"\",\n }\n self._save(task)\n self._next_id += 1\n return json.dumps(task, indent=2)\n```\n\n2. Completing a task clears that dependency from other tasks.\n\n```python\ndef _clear_dependency(self, completed_id: int):\n for f in self.dir.glob(\"task_*.json\"):\n task = json.loads(f.read_text())\n if completed_id in task.get(\"blockedBy\", []):\n task[\"blockedBy\"].remove(completed_id)\n self._save(task)\n```\n\n3. `update` handles status transitions and dependency wiring.\n\n```python\ndef update(self, task_id, status=None,\n add_blocked_by=None, add_blocks=None):\n task = self._load(task_id)\n if status:\n task[\"status\"] = status\n if status == \"completed\":\n self._clear_dependency(task_id)\n if add_blocks:\n task[\"blocks\"] = list(set(task[\"blocks\"] + add_blocks))\n for blocked_id in add_blocks:\n blocked = self._load(blocked_id)\n if task_id not in blocked[\"blockedBy\"]:\n blocked[\"blockedBy\"].append(task_id)\n self._save(blocked)\n self._save(task)\n```\n\n4. Task tools are added to the dispatch map.\n\n```python\nTOOL_HANDLERS = {\n # ...base tools...\n \"task_create\": lambda **kw: TASKS.create(kw[\"subject\"]),\n \"task_update\": lambda **kw: TASKS.update(kw[\"task_id\"],\n kw.get(\"status\")),\n \"task_list\": lambda **kw: TASKS.list_all(),\n \"task_get\": lambda **kw: TASKS.get(kw[\"task_id\"]),\n}\n```\n\n## Key Code\n\nTaskManager with dependency graph (from `agents/s07_task_system.py`, lines 46-123):\n\n```python\nclass TaskManager:\n def __init__(self, tasks_dir: Path):\n self.dir = tasks_dir\n self.dir.mkdir(exist_ok=True)\n self._next_id = self._max_id() + 1\n\n def _load(self, task_id: int) -> dict:\n path = self.dir / f\"task_{task_id}.json\"\n return json.loads(path.read_text())\n\n def _save(self, task: dict):\n path = self.dir / f\"task_{task['id']}.json\"\n path.write_text(json.dumps(task, indent=2))\n\n def create(self, subject, description=\"\"):\n task = {\"id\": self._next_id, \"subject\": subject,\n \"status\": \"pending\", \"blockedBy\": [],\n \"blocks\": [], \"owner\": \"\"}\n self._save(task)\n self._next_id += 1\n return json.dumps(task, indent=2)\n\n def _clear_dependency(self, completed_id):\n for f in self.dir.glob(\"task_*.json\"):\n task = json.loads(f.read_text())\n if completed_id in task.get(\"blockedBy\", []):\n task[\"blockedBy\"].remove(completed_id)\nself._save(task)\n```\n\n##WhatChangedFroms06\n\n|Component|Before(s06)|After(s07)|\n|---|---|---|\n|Tools|5|8(`task_create/update/list/get`)|\n|StLine truncated
},
{
"version":"s08",
"locale":"en",
"title":"s08: Background Tasks",
"content":"# s08: Background Tasks\n\n> A BackgroundManager runs commands in separate threads and drains a notification queue before each LLM call, so the agent never blocks on long-running operations.\n\n## The Problem\n\nSome commands take minutes: `npm install`, `pytest`, `docker build`. With\na blocking agent loop, the model sits idle waiting for the subprocess to\nfinish. It cannot do anything else. If the user asked \"install dependencies\nand while that runs, create the config file,\" the agent would install\nfirst, _then_ create the config -- sequentially, not in parallel.\n\nThe agent needs concurrency. Not full multi-threading of the agent loop\nitself, but the ability to fire off a long command and continue working\nwhile it runs. When the command finishes, its result should appear\nnaturally in the conversation.\n\nThe solution is a BackgroundManager that runs commands in daemon threads\nand collects results in a notification queue. Before each LLM call, the\nqueue is drained and results are injected into the messages.\n\n## The Solution\n\n```\nMain thread Background thread\n+-----------------+ +-----------------+\n| agent loop | | task executes |\n| ... | | ... |\n| [LLM call] <---+------- | enqueue(result) |\n| ^drain queue | +-----------------+\n+-----------------+\n\nTimeline:\nAgent --[spawn A]--[spawn B]--[other work]----\n | |\n v v\n [A runs] [B runs] (parallel)\n | |\n +-- notification queue --+\n |\n [results injected before\n next LLM call]\n```\n\n## How It Works\n\n1. The BackgroundManager tracks tasks and maintains a thread-safe\n notification queue.\n\n```python\nclass BackgroundManager:\n def __init__(self):\n self.tasks = {}\n self._notification_queue = []\n self._lock = threading.Lock()\n```\n\n2. `run()` starts a daemon thread and returns a task_id immediately.\n\n```python\ndef run(self, command: str) -> str:\n task_id = str(uuid.uuid4())[:8]\n self.tasks[task_id] = {\n \"status\": \"running\",\n \"result\": None,\n \"command\": command,\n }\n thread = threading.Thread(\n target=self._execute,\n args=(task_id, command),\n daemon=True,\n )\n thread.start()\n return f\"Background task {task_id} started\"\n```\n\n3. The thread target `_execute` runs the subprocess and pushes\n results to the notification queue.\n\n```python\ndef _execute(self, task_id: str, command: str):\n try:\n r = subprocess.run(command, shell=True, cwd=WORKDIR,\n capture_output=True, text=True, timeout=300)\n output = (r.stdout + r.stderr).strip()[:50000]\n status = \"completed\"\n except subprocess.TimeoutExpired:\n output = \"Error: Timeout (300s)\"\n status = \"timeout\"\n self.tasks[task_id][\"status\"] = status\n self.tasks[task_id][\"result\"] = output\n with self._lock:\n self._notification_queue.append({\n \"task_id\": task_id,\n \"status\": status,\n \"result\": output[:500],\n })\n```\n\n4. `drain_notifications()` returns and clears pending results.\n\n```python\ndef drain_notifications(self) -> list:\n with self._lock:\n notifs = list(self._notification_queue)\n self._notification_queue.clear()\n return notifs\n```\n\n5. The agent loop drains notifications before each LLM call.\n\n```python\ndef agent_loop(messages: list):\n while True:\n notifs = BG.drain_notifications()\n if notifs and messages:\n notif_text = \"\\n\".join(\n f\"[bg:{n['task_id']}] {n['status']}: \"\n f\"{n['result']}\" for n in notifs\n )\n messages.append({\"role\": \"user\",\n \"content\": f\"<background-results>\"\n f\"\\n{notif_text}\\n\"\n f\"</background-results>\"})\n messages.append({\"role\": \"assistant\",\n \"content\": \"Noted background results.\"})\n response = client.messages.create(...)\n```\n\n## Key Code\n\nThe BackgroundManager (from `agents/s08_background_tasks.py`, lines 49-107):\n\n```python\nclass BackgroundManager:\n def __init__(self):\n self.tasks = {}\n self._notification_queue = []\n self._lock = threading.Lock()\n\n def run(self, command: str) -> str:\n task_id = str(uuid.uuid4())[:8]\n self.tasks[task_id] = {\"status\": \"running\",\n \"result\": None,\n \"command\": command}\n thread = threading.Thread(\n target=self._execute,\n args=(task_id, command), daemon=True)\n thread.start()\n return f\"BackgrounLine truncated
"content":"# s08: Background Tasks\n\n> A BackgroundManager runs commands in separate threads and drains a notification queue before each LLM call, so the agent never blocks on long-running operations.\n\n## The Problem\n\nSome commands take minutes: `npm install`, `pytest`, `docker build`. With\na blocking agent loop, the model sits idle waiting for the subprocess to\nfinish. It cannot do anything else. If the user asked \"install dependencies\nand while that runs, create the config file,\" the agent would install\nfirst, _then_ create the config -- sequentially, not in parallel.\n\nThe agent needs concurrency. Not full multi-threading of the agent loop\nitself, but the ability to fire off a long command and continue working\nwhile it runs. When the command finishes, its result should appear\nnaturally in the conversation.\n\nThe solution is a BackgroundManager that runs commands in daemon threads\nand collects results in a notification queue. Before each LLM call, the\nqueue is drained and results are injected into the messages.\n\n## The Solution\n\n```\nMain thread Background thread\n+-----------------+ +-----------------+\n| agent loop | | task executes |\n| ... | | ... |\n| [LLM call] <---+------- | enqueue(result) |\n| ^drain queue | +-----------------+\n+-----------------+\n\nTimeline:\nAgent --[spawn A]--[spawn B]--[other work]----\n | |\n v v\n [A runs] [B runs] (parallel)\n | |\n +-- notification queue --+\n |\n [results injected before\n next LLM call]\n```\n\n## How It Works\n\n1. The BackgroundManager tracks tasks and maintains a thread-safe\n notification queue.\n\n```python\nclass BackgroundManager:\n def __init__(self):\n self.tasks = {}\n self._notification_queue = []\n self._lock = threading.Lock()\n```\n\n2. `run()` starts a daemon thread and returns a task_id immediately.\n\n```python\ndef run(self, command: str) -> str:\n task_id = str(uuid.uuid4())[:8]\n self.tasks[task_id] = {\n \"status\": \"running\",\n \"result\": None,\n \"command\": command,\n }\n thread = threading.Thread(\n target=self._execute,\n args=(task_id, command),\n daemon=True,\n )\n thread.start()\n return f\"Background task {task_id} started\"\n```\n\n3. The thread target `_execute` runs the subprocess and pushes\n results to the notification queue.\n\n```python\ndef _execute(self, task_id: str, command: str):\n try:\n r = subprocess.run(command, shell=True, cwd=WORKDIR,\n capture_output=True, text=True, timeout=300)\n output = (r.stdout + r.stderr).strip()[:50000]\n status = \"completed\"\n except subprocess.TimeoutExpired:\n output = \"Error: Timeout (300s)\"\n status = \"timeout\"\n self.tasks[task_id][\"status\"] = status\n self.tasks[task_id][\"result\"] = output\n with self._lock:\n self._notification_queue.append({\n \"task_id\": task_id,\n \"status\": status,\n \"result\": output[:500],\n })\n```\n\n4. `drain_notifications()` returns and clears pending results.\n\n```python\ndef drain_notifications(self) -> list:\n with self._lock:\n notifs = list(self._notification_queue)\n self._notification_queue.clear()\n return notifs\n```\n\n5. The agent loop drains notifications before each LLM call.\n\n```python\ndef agent_loop(messages: list):\n while True:\n notifs = BG.drain_notifications()\n if notifs and messages:\n notif_text = \"\\n\".join(\n f\"[bg:{n['task_id']}] {n['status']}: \"\n f\"{n['result']}\" for n in notifs\n )\n messages.append({\"role\": \"user\",\n \"content\": f\"<background-results>\"\n f\"\\n{notif_text}\\n\"\n f\"</background-results>\"})\n messages.append({\"role\": \"assistant\",\n \"content\": \"Noted background results.\"})\n response = client.messages.create(...)\n```\n\n## Key Code\n\nThe BackgroundManager (from `agents/s08_background_tasks.py`, lines 49-107):\n\n```python\nclass BackgroundManager:\n def __init__(self):\n self.tasks = {}\n self._notification_queue = []\n self._lock = threading.Lock()\n\n def run(self, command: str) -> str:\n task_id = str(uuid.uuid4())[:8]\n self.tasks[task_id] = {\"status\": \"running\",\n \"result\": None,\n \"command\": command}\n thread = threading.Thread(\n target=self._execute,\n args=(task_id, command), daemon=True)\n thread.start()\n return f\"BackgrounLine truncated
},
{
"version":"s09",
"locale":"en",
"title":"s09: Agent Teams",
"content":"# s09: Agent Teams\n\n> Persistent teammates with JSONL inboxes turn isolated agents into a communicating team -- spawn, message, broadcast, and drain.\n\n## The Problem\n\nSubagents (s04) are disposable: spawn, work, return summary, die. They\nhave no identity, no memory between invocations, and no way to receive\nfollow-up instructions. Background tasks (s08) run shell commands but\ncannot make LLM-guided decisions or communicate findings.\n\nFor real teamwork you need three things: (1) persistent agents that\nsurvive beyond a single prompt, (2) identity and lifecycle management,\nand (3) a communication channel between agents. Without messaging, even\npersistent teammates are deaf and mute -- they can work in parallel but\nnever coordinate.\n\nThe solution combines a TeammateManager for spawning persistent named\nagents with a MessageBus using JSONL inbox files. Each teammate runs\nits own agent loop in a thread, checks its inbox before every LLM call,\nand can send messages to any other teammate or the lead.\n\nNote on the s06-to-s07 bridge: TodoManager items from s03 die with\ncompression (s06). File-based tasks (s07) survive compression because\nthey live on disk. Teams build on this same principle -- config.json and\ninbox files persist outside the context window.\n\n## The Solution\n\n```\nTeammate lifecycle:\n spawn -> WORKING -> IDLE -> WORKING -> ... -> SHUTDOWN\n\nCommunication:\n .team/\n config.json <- team roster + statuses\n inbox/\n alice.jsonl <- append-only, drain-on-read\n bob.jsonl\n lead.jsonl\n\n +--------+ send(\"alice\",\"bob\",\"...\") +--------+\n | alice | -----------------------------> | bob |\n | loop | bob.jsonl << {json_line} | loop |\n +--------+ +--------+\n ^ |\n | BUS.read_inbox(\"alice\") |\n +---- alice.jsonl -> read + drain ---------+\n\n5 message types:\n+-------------------------+------------------------------+\n| message | Normal text between agents |\n| broadcast | Sent to all teammates |\n| shutdown_request | Request graceful shutdown |\n| shutdown_response | Approve/reject shutdown |\n| plan_approval_response | Approve/reject plan |\n+-------------------------+------------------------------+\n```\n\n## How It Works\n\n1. The TeammateManager maintains config.json with the team roster.\n Each member has a name, role, and status.\n\n```python\nclass TeammateManager:\n def __init__(self, team_dir: Path):\n self.dir = team_dir\n self.dir.mkdir(exist_ok=True)\n self.config_path = self.dir / \"config.json\"\n self.config = self._load_config()\n self.threads = {}\n```\n\n2. `spawn()` creates a teammate and starts its agent loop in a thread.\n Re-spawning an idle teammate reactivates it.\n\n```python\ndef spawn(self, name: str, role: str, prompt: str) -> str:\n member = self._find_member(name)\n if member:\n if member[\"status\"] not in (\"idle\", \"shutdown\"):\n return f\"Error: '{name}' is currently {member['status']}\"\n member[\"status\"] = \"working\"\n else:\n member = {\"name\": name, \"role\": role, \"status\": \"working\"}\n self.config[\"members\"].append(member)\n self._save_config()\n thread = threading.Thread(\n target=self._teammate_loop,\n args=(name, role, prompt), daemon=True)\n self.threads[name] = thread\n thread.start()\n return f\"Spawned teammate '{name}' (role: {role})\"\n```\n\n3. The MessageBus handles JSONL inbox files. `send()` appends a JSON\n line; `read_inbox()` reads all lines and drains the file.\n\n```python\nclass MessageBus:\n def send(self, sender, to, content,\n msg_type=\"message\", extra=None):\n msg = {\"type\": msg_type, \"from\": sender,\n \"content\": content,\n \"timestamp\": time.time()}\n if extra:\n msg.update(extra)\n with open(self.dir / f\"{to}.jsonl\", \"a\") as f:\n f.write(json.dumps(msg) + \"\\n\")\n return f\"Sent {msg_type} to {to}\"\n\n def read_inbox(self, name):\n path = self.dir / f\"{name}.jsonl\"\n if not path.exists():\n return \"[]\"\n msgs = [json.loads(l)\n for l in path.read_text().strip().splitlines()\n if l]\n path.write_text(\"\") # drain\n return json.dumps(msgs, indent=2)\n```\n\n4. Each teammate checks its inbox before every LLM call and injects\n received messages into the conversation context.\n\n```python\ndef _teammate_loop(self, name, role, prompt):\n sys_prompt = f\"You are '{name}', role: {role}, at {WORKDIR}.\"\n messages = [{\"role\": \"user\",Line truncated
"content":"# s09: Agent Teams\n\n> Persistent teammates with JSONL inboxes are one teaching protocol for turning isolated agents into a communicating team -- spawn, message, broadcast, and drain.\n\n## The Problem\n\nSubagents (s04) are disposable: spawn, work, return summary, die. They\nhave no identity, no memory between invocations, and no way to receive\nfollow-up instructions. Background tasks (s08) run shell commands but\ncannot make LLM-guided decisions or communicate findings.\n\nFor real teamwork you need three things: (1) persistent agents that\nsurvive beyond a single prompt, (2) identity and lifecycle management,\nand (3) a communication channel between agents. Without messaging, even\npersistent teammates are deaf and mute -- they can work in parallel but\nnever coordinate.\n\nThe solution combines a TeammateManager for spawning persistent named\nagents with a MessageBus using JSONL inbox files. Each teammate runs\nits own agent loop in a thread, checks its inbox before every LLM call,\nand can send messages to any other teammate or the lead.\n\nNote on the s06-to-s07 bridge: TodoManager items from s03 die with\ncompression (s06). File-based tasks (s07) survive compression because\nthey live on disk. Teams build on this same principle -- config.json and\ninbox files persist outside the context window.\n\n## The Solution\n\n```\nTeammate lifecycle:\n spawn -> WORKING -> IDLE -> WORKING -> ... -> SHUTDOWN\n\nCommunication:\n .team/\n config.json <- team roster + statuses\n inbox/\n alice.jsonl <- append-only, drain-on-read\n bob.jsonl\n lead.jsonl\n\n +--------+ send(\"alice\",\"bob\",\"...\") +--------+\n | alice | -----------------------------> | bob |\n | loop | bob.jsonl << {json_line} | loop |\n +--------+ +--------+\n ^ |\n | BUS.read_inbox(\"alice\") |\n +---- alice.jsonl -> read + drain ---------+\n\n5 message types:\n+-------------------------+------------------------------+\n| message | Normal text between agents |\n| broadcast | Sent to all teammates |\n| shutdown_request | Request graceful shutdown |\n| shutdown_response | Approve/reject shutdown |\n| plan_approval_response | Approve/reject plan |\n+-------------------------+------------------------------+\n```\n\n## How It Works\n\n1. The TeammateManager maintains config.json with the team roster.\n Each member has a name, role, and status.\n\n```python\nclass TeammateManager:\n def __init__(self, team_dir: Path):\n self.dir = team_dir\n self.dir.mkdir(exist_ok=True)\n self.config_path = self.dir / \"config.json\"\n self.config = self._load_config()\n self.threads = {}\n```\n\n2. `spawn()` creates a teammate and starts its agent loop in a thread.\n Re-spawning an idle teammate reactivates it.\n\n```python\ndef spawn(self, name: str, role: str, prompt: str) -> str:\n member = self._find_member(name)\n if member:\n if member[\"status\"] not in (\"idle\", \"shutdown\"):\n return f\"Error: '{name}' is currently {member['status']}\"\n member[\"status\"] = \"working\"\n else:\n member = {\"name\": name, \"role\": role, \"status\": \"working\"}\n self.config[\"members\"].append(member)\n self._save_config()\n thread = threading.Thread(\n target=self._teammate_loop,\n args=(name, role, prompt), daemon=True)\n self.threads[name] = thread\n thread.start()\n return f\"Spawned teammate '{name}' (role: {role})\"\n```\n\n3. The MessageBus handles JSONL inbox files. `send()` appends a JSON\n line; `read_inbox()` reads all lines and drains the file.\n\n```python\nclass MessageBus:\n def send(self, sender, to, content,\n msg_type=\"message\", extra=None):\n msg = {\"type\": msg_type, \"from\": sender,\n \"content\": content,\n \"timestamp\": time.time()}\n if extra:\n msg.update(extra)\n with open(self.dir / f\"{to}.jsonl\", \"a\") as f:\n f.write(json.dumps(msg) + \"\\n\")\n return f\"Sent {msg_type} to {to}\"\n\n def read_inbox(self, name):\n path = self.dir / f\"{name}.jsonl\"\n if not path.exists():\n return \"[]\"\n msgs = [json.loads(l)\n for l in path.read_text().strip().splitlines()\n if l]\n path.write_text(\"\") # drain\n return json.dumps(msgs, indent=2)\n```\n\n4. Each teammate checks its inbox before every LLM call and injects\n received messages into the conversation context.\n\n```python\ndef _teammate_loop(self, name, role, prompt):\n sys_prompt = f\"You are '{name}', role: {role}, at {WORKDIR}.\"\nLine truncated
},
{
"version":"s10",
@@ -63,61 +63,67 @@
"version":"s11",
"locale":"en",
"title":"s11: Autonomous Agents",
"content":"# s11: Autonomous Agents\n\n> An idle cycle with task board polling lets teammates find and claim work themselves, with identity re-injection after context compression.\n\n## The Problem\n\nIn s09-s10, teammates only work when explicitly told to. The lead must\nspawn each teammate with a specific prompt. If the task board has 10\nunclaimed tasks, the lead must manually assign each one. This does not\nscale.\n\nTrue autonomy means teammates find work themselves. When a teammate\nfinishes its current task, it should scan the task board for unclaimed\nwork, claim a task, and start working -- without any instruction from\nthe lead.\n\nBut autonomous agents face a subtlety: after context compression, the\nagent might forget who it is. If the messages are summarized, the\noriginal system prompt identity (\"you are alice, role: coder\") fades.\nIdentity re-injection solves this by inserting an identity block at the\nstart of compressed contexts.\n\nTeaching simplification: the token estimation used here is rough\n(characters / 4). Production systems use proper tokenizer libraries.\nThe nag threshold of 3 rounds (from s03) is set low for teaching\nvisibility; production agents typically use a higher threshold around 10.\n\n## The Solution\n\n```\nTeammate lifecycle with idle cycle:\n\n+-------+\n| spawn |\n+---+---+\n |\n v\n+-------+ tool_use +-------+\n| WORK | <------------- | LLM |\n+---+---+ +-------+\n |\n | stop_reason != tool_use\n | (or idle tool called)\n v\n+--------+\n| IDLE | poll every 5s for up to 60s\n+---+----+\n |\n +---> check inbox --> message? ----------> WORK\n |\n +---> scan .tasks/ --> unclaimed? -------> claim -> WORK\n |\n +---> 60s timeout ----------------------> SHUTDOWN\n\nIdentity re-injection after compression:\n if len(messages) <= 3:\n messages.insert(0, identity_block)\n \"You are 'alice', role: coder, team: my-team\"\n```\n\n## How It Works\n\n1. The teammate loop has two phases: WORK and IDLE. WORK runs the\n standard agent loop. When the LLM stops calling tools (or calls\n the `idle` tool), the teammate enters the IDLE phase.\n\n```python\ndef _loop(self, name, role, prompt):\n while True:\n # -- WORK PHASE --\n messages = [{\"role\": \"user\", \"content\": prompt}]\n for _ in range(50):\n inbox = BUS.read_inbox(name)\n for msg in inbox:\n if msg.get(\"type\") == \"shutdown_request\":\n self._set_status(name, \"shutdown\")\n return\n messages.append(...)\n response = client.messages.create(...)\n if response.stop_reason != \"tool_use\":\n break\n # execute tools...\n if idle_requested:\n break\n\n # -- IDLE PHASE --\n self._set_status(name, \"idle\")\n resume = self._idle_poll(name, messages)\n if not resume:\n self._set_status(name, \"shutdown\")\n return\n self._set_status(name, \"working\")\n```\n\n2. The idle phase polls the inbox and task board in a loop.\n\n```python\ndef _idle_poll(self, name, messages):\n polls = IDLE_TIMEOUT // POLL_INTERVAL # 60s / 5s = 12\n for _ in range(polls):\n time.sleep(POLL_INTERVAL)\n # Check inbox for new messages\n inbox = BUS.read_inbox(name)\n if inbox:\n messages.append({\"role\": \"user\",\n \"content\": f\"<inbox>{inbox}</inbox>\"})\n return True\n # Scan task board for unclaimed tasks\n unclaimed = scan_unclaimed_tasks()\n if unclaimed:\n task = unclaimed[0]\n claim_task(task[\"id\"], name)\n messages.append({\"role\": \"user\",\n \"content\": f\"<auto-claimed>Task #{task['id']}: \"\n f\"{task['subject']}</auto-claimed>\"})\n return True\n return False # timeout -> shutdown\n```\n\n3. Task board scanning looks for pending, unowned, unblocked tasks.\n\n```python\ndef scan_unclaimed_tasks() -> list:\n TASKS_DIR.mkdir(exist_ok=True)\n unclaimed = []\n for f in sorted(TASKS_DIR.glob(\"task_*.json\")):\n task = json.loads(f.read_text())\n if (task.get(\"status\") == \"pending\"\n and not task.get(\"owner\")\n and not task.get(\"blockedBy\")):\n unclaimed.append(task)\n return unclaimed\n\ndef claim_task(task_id: int, owner: str):\n path = TASKS_DIR / f\"task_{task_id}.json\"\n task = json.loads(path.read_text())\n task[\"status\"] = \"in_progress\"\n task[\"owner\"] = owner\n path.write_text(json.dumps(task, indent=2))\n```\n\n4. Identity re-injection inserts an identity block when the context\n is too short, indicating compression has occurred.\n\n```python\ndef make_identity_block(name, role, team_name):\n return {\"role\": \"user\",\nLine truncated
"content":"# s11: Autonomous Agents\n\n> An idle cycle with task board polling lets teammates find and claim work themselves, with identity re-injection after context compression.\n\n## The Problem\n\nIn s09-s10, teammates only work when explicitly told to. The lead must\nspawn each teammate with a specific prompt. If the task board has 10\nunclaimed tasks, the lead must manually assign each one. This does not\nscale.\n\nTrue autonomy means teammates find work themselves. When a teammate\nfinishes its current task, it should scan the task board for unclaimed\nwork, claim a task, and start working -- without any instruction from\nthe lead.\n\nBut autonomous agents face a subtlety: after context compression, the\nagent might forget who it is. If the messages are summarized, the\noriginal system prompt identity (\"you are alice, role: coder\") fades.\nIdentity re-injection solves this by inserting an identity block at the\nstart of compressed contexts.\n\nNote: token estimation here uses characters/4 (rough). The nag threshold of 3 rounds is low for teaching visibility.\n\n## The Solution\n\n```\nTeammate lifecycle with idle cycle:\n\n+-------+\n| spawn |\n+---+---+\n |\n v\n+-------+ tool_use +-------+\n| WORK | <------------- | LLM |\n+---+---+ +-------+\n |\n | stop_reason != tool_use\n | (or idle tool called)\n v\n+--------+\n| IDLE | poll every 5s for up to 60s\n+---+----+\n |\n +---> check inbox --> message? ----------> WORK\n |\n +---> scan .tasks/ --> unclaimed? -------> claim -> WORK\n |\n +---> 60s timeout ----------------------> SHUTDOWN\n\nIdentity re-injection after compression:\n if len(messages) <= 3:\n messages.insert(0, identity_block)\n \"You are 'alice', role: coder, team: my-team\"\n```\n\n## How It Works\n\n1. The teammate loop has two phases: WORK and IDLE. WORK runs the\n standard agent loop. When the LLM stops calling tools (or calls\n the `idle` tool), the teammate enters the IDLE phase.\n\n```python\ndef _loop(self, name, role, prompt):\n while True:\n # -- WORK PHASE --\n messages = [{\"role\": \"user\", \"content\": prompt}]\n for _ in range(50):\n inbox = BUS.read_inbox(name)\n for msg in inbox:\n if msg.get(\"type\") == \"shutdown_request\":\n self._set_status(name, \"shutdown\")\n return\n messages.append(...)\n response = client.messages.create(...)\n if response.stop_reason != \"tool_use\":\n break\n # execute tools...\n if idle_requested:\n break\n\n # -- IDLE PHASE --\n self._set_status(name, \"idle\")\n resume = self._idle_poll(name, messages)\n if not resume:\n self._set_status(name, \"shutdown\")\n return\n self._set_status(name, \"working\")\n```\n\n2. The idle phase polls the inbox and task board in a loop.\n\n```python\ndef _idle_poll(self, name, messages):\n polls = IDLE_TIMEOUT // POLL_INTERVAL # 60s / 5s = 12\n for _ in range(polls):\n time.sleep(POLL_INTERVAL)\n # Check inbox for new messages\n inbox = BUS.read_inbox(name)\n if inbox:\n messages.append({\"role\": \"user\",\n \"content\": f\"<inbox>{inbox}</inbox>\"})\n return True\n # Scan task board for unclaimed tasks\n unclaimed = scan_unclaimed_tasks()\n if unclaimed:\n task = unclaimed[0]\n claim_task(task[\"id\"], name)\n messages.append({\"role\": \"user\",\n \"content\": f\"<auto-claimed>Task #{task['id']}: \"\n f\"{task['subject']}</auto-claimed>\"})\n return True\n return False # timeout -> shutdown\n```\n\n3. Task board scanning looks for pending, unowned, unblocked tasks.\n\n```python\ndef scan_unclaimed_tasks() -> list:\n TASKS_DIR.mkdir(exist_ok=True)\n unclaimed = []\n for f in sorted(TASKS_DIR.glob(\"task_*.json\")):\n task = json.loads(f.read_text())\n if (task.get(\"status\") == \"pending\"\n and not task.get(\"owner\")\n and not task.get(\"blockedBy\")):\n unclaimed.append(task)\n return unclaimed\n\ndef claim_task(task_id: int, owner: str):\n path = TASKS_DIR / f\"task_{task_id}.json\"\n task = json.loads(path.read_text())\n task[\"status\"] = \"in_progress\"\n task[\"owner\"] = owner\n path.write_text(json.dumps(task, indent=2))\n```\n\n4. Identity re-injection inserts an identity block when the context\n is too short, indicating compression has occurred.\n\n```python\ndef make_identity_block(name, role, team_name):\n return {\"role\": \"user\",\n \"content\": f\"<identity>You are '{name}', \"\n f\"role: {role}, team: {team_name}. \"\n f\"Continueyourwork.<Line truncated
},
{
"version":"s12",
"locale":"en",
"title":"s12: Worktree + Task Isolation",
"content":"# s12: Worktree + Task Isolation\n\n> Isolate by directory, coordinate by task ID -- tasks are the control plane, worktrees are the execution plane, and an event stream makes every lifecycle step observable.\n\n## The Problem\n\nBy s11, agents can claim and complete tasks autonomously. But every task runs in one shared directory. Ask two agents to refactor different modules at the same time and you hit three failure modes:\n\nAgent A edits `auth.py`. Agent B edits `auth.py`. Neither knows the other touched it. Unstaged changes collide, task status says \"in_progress\" but the directory is a mess, and when something breaks there is no way to roll back one agent's work without destroying the other's. The task board tracks _what to do_ but has no opinion about _where to do it_.\n\nThe fix is to separate the two concerns. Tasks manage goals. Worktrees manage execution context. Bind them by task ID, and each agent gets its own directory, its own branch, and a clean teardown path.\n\n## The Solution\n\n```\nControl Plane (.tasks/) Execution Plane (.worktrees/)\n+---------------------------+ +---------------------------+\n| task_1.json | | index.json |\n| id: 1 | | name: \"auth-refactor\" |\n| subject: \"Auth refactor\"| bind | path: \".worktrees/...\" |\n| status: \"in_progress\" | <----> | branch: \"wt/auth-...\" |\n| worktree: \"auth-refactor\"| | task_id: 1 |\n+---------------------------+ | status: \"active\" |\n +---------------------------+\n| task_2.json | | |\n| id: 2 | bind | name: \"ui-login\" |\n| subject: \"Login page\" | <----> | task_id: 2 |\n| worktree: \"ui-login\" | | status: \"active\" |\n+---------------------------+ +---------------------------+\n |\n +---------------------------+\n | events.jsonl (append-only)|\n | worktree.create.before |\n | worktree.create.after |\n | worktree.remove.after |\n | task.completed |\n +---------------------------+\n```\n\nThree state layers make this work:\n\n1. **Control plane** (`.tasks/task_*.json`) -- what is assigned, in progress, or done. Key fields: `id`, `subject`, `status`, `owner`, `worktree`.\n2. **Execution plane** (`.worktrees/index.json`) -- where commands run and whether the workspace is still valid. Key fields: `name`, `path`, `branch`, `task_id`, `status`.\n3. **Runtime state** (in-memory) -- per-turn execution continuity: `current_task`, `current_worktree`, `tool_result`, `error`.\n\n## How It Works\n\nThe lifecycle has five steps. Each step is a tool call.\n\n1. **Create a task.** Persist the goal first. The task starts as `pending` with an empty `worktree` field.\n\n```python\ntask = {\n \"id\": self._next_id,\n \"subject\": subject,\n \"status\": \"pending\",\n \"owner\": \"\",\n \"worktree\": \"\",\n}\nself._save(task)\n```\n\n2. **Create a worktree.** Allocate an isolated directory and branch. If you pass `task_id`, the task auto-advances to `in_progress` and the binding is written to both sides.\n\n```python\nself._run_git([\"worktree\", \"add\", \"-b\", branch, str(path), base_ref])\n\nentry = {\n \"name\": name,\n \"path\": str(path),\n \"branch\": branch,\n \"task_id\": task_id,\n \"status\": \"active\",\n}\nidx[\"worktrees\"].append(entry)\nself._save_index(idx)\n\niftask_idisnotNone:\nself.tasks.bind_worktree(task_id,name)\n```\n\n3.**Runcommandsintheworktree.**`worktree_run`sets`cwd`totheworktreepath.Editshappenintheisolateddirectory,notthesharedworkspace.\n\n```python\nr=subprocess.run(\ncommand,\nshell=True,\ncwd=path,\ncapture_output=True,\ntext=True,\ntimeout=300,\n)\n```\n\n4.**Observe.**`worktree_status`showsgitstateinsidetheisolatedcontext.`worktree_events`queriestheappend-onlyeventstream.\n\n5.**Closeout.**Twochoices:\n-`worktree_keep(name)`--preservethedirectory,marklifecycleas`kept`.\n-`worktree_remove(name,complete_task=True)`--removethedirectory,completetheboundtask,unbind,andemit`task.completed`.Thisisthecloseoutpattern:onecallhandlesteardownandtaskcompletiontogether.\n\n##StateMachines\n\n```\nTask:pending------->in_progress------->completed\n(worktree_create(worktree_remove\nwithtask_id)withcomplete_task=true)\n\nWorktree:absent-------->active----------->removed|kept\nLine truncated
},
{
"version":"s01",
"locale":"zh",
"title":"s01: Agent Loop (智能体循环)",
"content":"# s01: Agent Loop (智能体循环)\n\n> AI 编程智能体的全部秘密就是一个 while 循环 -- 把工具执行结果反馈给模型, 直到模型决定停止。\n\n## 问题\n\n为什么语言模型不能直接回答编程问题? 因为编程需要**与真实世界交互**。模型需要读取文件、运行测试、检查错误、反复迭代。单次的提示-响应交互无法做到这些。\n\n没有 agent loop, 你就得手动把输出复制粘贴回模型。用户自己变成了那个循环。Agent loop 将这个过程自动化: 调用模型, 执行它要求的工具, 把结果送回去, 重复 -- 直到模型说 \"我完成了\"。\n\n考虑一个简单任务: \"创建一个打印 hello 的 Python 文件。\" 模型需要 (1) 决定写文件, (2) 写入文件, (3) 验证是否正常工作。至少三次工具调用。没有循环的话, 每一次都需要人工干预。\n\n## 解决方案\n\n```\n+----------+ +-------+ +---------+\n| User | ---> | LLM | ---> | Tool |\n| prompt | | | | execute |\n+----------+ +---+---+ +----+----+\n ^ |\n | tool_result |\n +---------------+\n (loop continues)\n\nThe loop terminates when stop_reason != \"tool_use\".\nThat single condition is the entire control flow.\n```\n\n## 工作原理\n\n1. 用户提供一个 prompt, 成为第一条消息。\n\n```python\nhistory.append({\"role\": \"user\", \"content\": query})\n```\n\n2. 消息数组连同工具定义一起发送给 LLM。\n\n```python\nresponse = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n)\n```\n\n3. 助手的响应被追加到消息列表中。\n\n```python\nmessages.append({\"role\": \"assistant\", \"content\": response.content})\n```\n\n4. 检查 stop_reason。如果模型没有调用工具, 循环结束。这是唯一的退出条件。\n\n```python\nif response.stop_reason != \"tool_use\":\n return\n```\n\n5. 对响应中的每个 tool_use 块, 执行工具 (本节课中是 bash) 并收集结果。\n\n```python\nfor block in response.content:\n if block.type == \"tool_use\":\n output = run_bash(block.input[\"command\"])\n results.append({\n \"type\": \"tool_result\",\n \"tool_use_id\": block.id,\n \"content\": output,\n })\n```\n\n6. 结果作为 user 消息追加, 循环继续。\n\n```python\nmessages.append({\"role\": \"user\", \"content\": results})\n```\n\n## 核心代码\n\n最小可行智能体 -- 不到 30 行代码实现整个模式\n(来自 `agents/s01_agent_loop.py`, 第 66-86 行):\n\n```python\ndef agent_loop(messages: list):\n while True:\n response = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n )\n messages.append({\"role\": \"assistant\", \"content\": response.content})\n if response.stop_reason != \"tool_use\":\n return\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n output = run_bash(block.input[\"command\"])\n results.append({\n \"type\": \"tool_result\",\n \"tool_use_id\": block.id,\n \"content\": output,\n })\n messages.append({\"role\": \"user\", \"content\": results})\n```\n\n## 变更内容\n\n这是第 1 节课 -- 起点。没有前置课程。\n\n| 组件 | 之前 | 之后 |\n|---------------|------------|--------------------------------|\n| Agent loop | (无) | `while True` + stop_reason |\n| Tools | (无) | `bash` (单一工具) |\n| Messages | (无) | 累积式消息列表 |\n| Control flow | (无) | `stop_reason != \"tool_use\"` |\n\n## 设计原理\n\n这个循环是所有基于 LLM 的智能体的通用基础。生产实现会增加错误处理、token 计数、流式输出和重试逻辑, 但基本结构不变。简洁性就是重点: 一个退出条件 (`stop_reason != \"tool_use\"`) 控制整个流程。本课程中的所有其他内容 -- 工具、规划、压缩、团队 -- 都是在这个循环之上叠加, 而不修改它。理解这个循环就是理解所有智能体。\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s01_agent_loop.py\n```\n\n可以尝试的提示:\n\n1. `Create a file called hello.py that prints \"Hello, World!\"`\n2. `List all Python files in this directory`\n3. `What is the current git branch?`\n4. `Create a directory called test_output and write 3 files in it`\n"
"content":"# s01: Agent Loop (智能体循环)\n\n> AI 编程智能体的核心是一个 while 循环 -- 把工具执行结果反馈给模型, 直到模型决定停止。\n\n## 问题\n\n为什么语言模型不能直接回答编程问题? 因为编程需要**与真实世界交互**。模型需要读取文件、运行测试、检查错误、反复迭代。单次的提示-响应交互无法做到这些。\n\n没有 agent loop, 你就得手动把输出复制粘贴回模型。用户自己变成了那个循环。Agent loop 将这个过程自动化: 调用模型, 执行它要求的工具, 把结果送回去, 重复 -- 直到模型说 \"我完成了\"。\n\n考虑一个简单任务: \"创建一个打印 hello 的 Python 文件。\" 模型需要 (1) 决定写文件, (2) 写入文件, (3) 验证是否正常工作。至少三次工具调用。没有循环的话, 每一次都需要人工干预。\n\n## 解决方案\n\n```\n+----------+ +-------+ +---------+\n| User | ---> | LLM | ---> | Tool |\n| prompt | | | | execute |\n+----------+ +---+---+ +----+----+\n ^ |\n | tool_result |\n +---------------+\n (loop continues)\n\nThe loop terminates when stop_reason != \"tool_use\".\nThat single condition is the entire control flow.\n```\n\n## 工作原理\n\n1. 用户提供一个 prompt, 成为第一条消息。\n\n```python\nhistory.append({\"role\": \"user\", \"content\": query})\n```\n\n2. 消息数组连同工具定义一起发送给 LLM。\n\n```python\nresponse = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n)\n```\n\n3. 助手的响应被追加到消息列表中。\n\n```python\nmessages.append({\"role\": \"assistant\", \"content\": response.content})\n```\n\n4. 检查 stop_reason。如果模型没有调用工具, 循环结束。在本节最小实现里, 这是唯一的循环退出条件。\n\n```python\nif response.stop_reason != \"tool_use\":\n return\n```\n\n5. 对响应中的每个 tool_use 块, 执行工具 (本节课中是 bash) 并收集结果。\n\n```python\nfor block in response.content:\n if block.type == \"tool_use\":\n output = run_bash(block.input[\"command\"])\n results.append({\n \"type\": \"tool_result\",\n \"tool_use_id\": block.id,\n \"content\": output,\n })\n```\n\n6. 结果作为 user 消息追加, 循环继续。\n\n```python\nmessages.append({\"role\": \"user\", \"content\": results})\n```\n\n## 核心代码\n\n最小可行智能体 -- 不到 30 行代码实现整个模式\n(来自 `agents/s01_agent_loop.py`, 第 66-86 行):\n\n```python\ndef agent_loop(messages: list):\n while True:\n response = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n )\n messages.append({\"role\": \"assistant\", \"content\": response.content})\n if response.stop_reason != \"tool_use\":\n return\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n output = run_bash(block.input[\"command\"])\n results.append({\n \"type\": \"tool_result\",\n \"tool_use_id\": block.id,\n \"content\": output,\n })\n messages.append({\"role\": \"user\", \"content\": results})\n```\n\n## 变更内容\n\n这是第 1 节课 -- 起点。没有前置课程。\n\n| 组件 | 之前 | 之后 |\n|---------------|------------|--------------------------------|\n| Agent loop | (无) | `while True` + stop_reason |\n| Tools | (无) | `bash` (单一工具) |\n| Messages | (无) | 累积式消息列表 |\n| Control flow | (无) | `stop_reason != \"tool_use\"` |\n\n## 设计原理\n\n这个循环是所有基于 LLM 的智能体基础。生产实现还会增加错误处理、token 计数、流式输出、重试、权限策略与生命周期编排, 但核心交互模式仍从这里开始。本节强调简洁性: 在本节最小实现里, 一个退出条件 (`stop_reason != \"tool_use\"`) 就能支撑我们先学会主流程。本课程中的其他内容都在这个循环上叠加。理解这个循环是建立基础心智模型, 不是完整的生产架构。\n\n## 试一试\n\n```sh\ncd learn-claude-code\npython agents/s01_agent_loop.py\n```\n\n可以尝试的提示:\n\n1. `Create a file called hello.py that prints \"Hello, World!\"`\n2. `List all Python files in this directory`\n3. `What is the current git branch?`\n4. `Create a directory called test_output and write 3 files in it`\n"
"annotation":"In this teaching runtime, commands route by lane-scoped cwd; other runtimes may use session-level directory switches. The invariant is explicit execution context."
"annotation":"Closeout is explicit tool-driven state transition: mix keep/remove decisions and query lifecycle events in one pass"
},
{
"type":"system_event",
"content":"worktree.keep emitted for ui-login\nworktree.remove.before/after emitted for auth-refactor\ntask.completed emitted for #1\n.worktrees/events.jsonl appended",
"annotation":"Lifecycle transitions become explicit records while task/worktree files remain source-of-truth"
"home":{"hero_title":"Learn Claude Code","hero_subtitle":"Build an AI coding agent from scratch, one concept at a time","start":"Start Learning","core_pattern":"The Core Pattern","core_pattern_desc":"Every AI coding agent shares the same loop: call the model, execute tools, feed results back. Everything else is details.","learning_path":"Learning Path","learning_path_desc":"11 progressive sessions, from a simple loop to full autonomous teams","layers_title":"Architectural Layers","layers_desc":"Five orthogonal concerns that compose into a complete agent","loc":"LOC","learn_more":"Learn More","versions_in_layer":"versions","message_flow":"Message Growth","message_flow_desc":"Watch the messages array grow as the agent loop executes"},
"home":{"hero_title":"Learn Claude Code","hero_subtitle":"Build a nano Claude Code-like agent from 0 to 1, one mechanism at a time","start":"Start Learning","core_pattern":"The Core Pattern","core_pattern_desc":"Every AI coding agent shares the same loop: call the model, execute tools, feed results back. Production systems add policy, permissions, and lifecycle layers on top.","learning_path":"Learning Path","learning_path_desc":"12 progressive sessions, from a simple loop to isolated autonomous execution","layers_title":"Architectural Layers","layers_desc":"Five orthogonal concerns that compose into a complete agent","loc":"LOC","learn_more":"Learn More","versions_in_layer":"versions","message_flow":"Message Growth","message_flow_desc":"Watch the messages array grow as the agent loop executes"},
"home":{"hero_title":"Learn Claude Code","hero_subtitle":"AIコーディングエージェントをゼロから構築、一つずつ概念を追加","start":"学習を始める","core_pattern":"コアパターン","core_pattern_desc":"全てのAIコーディングエージェントは同じループを共有:モデルを呼び出し、ツールを実行し、結果をフィードバック。他は全て詳細。","learning_path":"学習パス","learning_path_desc":"11の段階的セッション、シンプルなループから完全自律チームまで","layers_title":"アーキテクチャ層","layers_desc":"5つの直交する関心事が完全なエージェントを構成","loc":"行","learn_more":"詳細を見る","versions_in_layer":"バージョン","message_flow":"メッセージの増加","message_flow_desc":"エージェントループ実行時のメッセージ配列の成長を観察"},
"home":{"hero_title":"Learn Claude Code","hero_subtitle":"0 から 1 へ nano Claude Code-like agent を構築し、毎回 1 つの仕組みを追加","start":"学習を始める","core_pattern":"コアパターン","core_pattern_desc":"すべてのAIコーディングエージェントは同じループを共有する:モデルを呼び出し、ツールを実行し、結果を返す。実運用ではこの上にポリシー、権限、ライフサイクル層が重なる。","learning_path":"学習パス","learning_path_desc":"12の段階的セッション、シンプルなループから分離された自律実行まで","layers_title":"アーキテクチャ層","layers_desc":"5つの直交する関心事が完全なエージェントを構成","loc":"行","learn_more":"詳細を見る","versions_in_layer":"バージョン","message_flow":"メッセージの増加","message_flow_desc":"エージェントループ実行時のメッセージ配列の成長を観察"},
"home":{"hero_title":"Learn Claude Code","hero_subtitle":"从零构建 AI 编程 Agent,每次只加一个概念","start":"开始学习","core_pattern":"核心模式","core_pattern_desc":"所有 AI 编程 Agent 共享同一个循环:调用模型、执行工具、回传结果。其他都是细节。","learning_path":"学习路径","learning_path_desc":"11 个渐进式课程,从简单循环到完整自主团队","layers_title":"架构层次","layers_desc":"五个正交关注点组合成完整的 Agent","loc":"行","learn_more":"了解更多","versions_in_layer":"个版本","message_flow":"消息增长","message_flow_desc":"观察 Agent 循环执行时消息数组的增长"},
"home":{"hero_title":"Learn Claude Code","hero_subtitle":"从 0 到 1 构建 nano Claude Code-like agent,每次只加一个机制","start":"开始学习","core_pattern":"核心模式","core_pattern_desc":"所有 AI 编程 Agent 共享同一个循环:调用模型、执行工具、回传结果。生产级系统会在其上叠加策略、权限和生命周期层。","learning_path":"学习路径","learning_path_desc":"12 个渐进式课程,从简单循环到隔离化自治执行","layers_title":"架构层次","layers_desc":"五个正交关注点组合成完整的 Agent","loc":"行","learn_more":"了解更多","versions_in_layer":"个版本","message_flow":"消息增长","message_flow_desc":"观察 Agent 循环执行时消息数组的增长"},
s01:{title:"The Agent Loop",subtitle:"Bash is All You Need",coreAddition:"Single-tool agent loop",keyInsight:"The entire agent is a while loop + one tool",layer:"tools",prevVersion: null},
s02:{title:"Tools",subtitle:"The Loop Didn't Change",coreAddition:"Tool dispatch map",keyInsight:"Adding tools means adding handlers, the loop stays the same",layer:"tools",prevVersion:"s01"},
s01:{title:"The Agent Loop",subtitle:"Bash is All You Need",coreAddition:"Single-tool agent loop",keyInsight:"The minimal agent kernel is a while loop + one tool",layer:"tools",prevVersion: null},
s02:{title:"Tools",subtitle:"The Loop Didn't Change",coreAddition:"Tool dispatch map",keyInsight:"Adding tools means adding handlers, not rewriting the loop",layer:"tools",prevVersion:"s01"},
s03:{title:"TodoWrite",subtitle:"Plan Before You Act",coreAddition:"TodoManager + nag reminder",keyInsight:"Visible plans improve task completion and accountability",layer:"planning",prevVersion:"s02"},
s04:{title:"Subagents",subtitle:"Fresh Context via Task Tool",coreAddition:"Subagent spawn with isolated messages[]",keyInsight:"Process isolation = context isolation",layer:"planning",prevVersion:"s03"},
s04:{title:"Subagents",subtitle:"Process Isolation = Context Isolation",coreAddition:"Subagent spawn with isolated messages[]",keyInsight:"Process isolation gives context isolation for free",layer:"planning",prevVersion:"s03"},
s05:{title:"Skills",subtitle:"SKILL.md + tool_result Injection",coreAddition:"SkillLoader + two-layer injection",keyInsight:"Skills inject via tool_result, not system prompt",layer:"planning",prevVersion:"s04"},
s07:{title:"Tasks",subtitle:"Persistent CRUD with Dependencies",coreAddition:"TaskManager with file-based state + dependency graph",keyInsight:"File-based state survives context compression",layer:"planning",prevVersion:"s06"},
s10:{title:"Team Protocols",subtitle:"Shutdown + Plan Approval",coreAddition:"request_id correlation for two protocols",keyInsight:"Same request-response pattern, two applications",layer:"collaboration",prevVersion:"s09"},
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.