diff --git a/README-ja.md b/README-ja.md index 919d2f4be..6fd809e48 100644 --- a/README-ja.md +++ b/README-ja.md @@ -1,6 +1,6 @@ # Learn Claude Code -- 0 から 1 へ構築する nano Claude Code-like agent -[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md) +[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md) | [Русский](./README-ru.md) ``` THE AGENT PATTERN @@ -84,7 +84,7 @@ def agent_loop(messages): このリポジトリは、nano Claude Code-like agent を 0->1 で構築・学習するための教材プロジェクトです。 学習を優先するため、以下の本番メカニズムは意図的に簡略化または省略しています。 -- 完全なイベント / Hook バス (例: PreToolUse, SessionStart/End, ConfigChange)。 +- 完全なイベント / Hook バス (例: PreToolUse, SessionStart/End, ConfigChange)。 s12 では教材用に最小の追記型ライフサイクルイベントのみ実装している。 - ルールベースの権限ガバナンスと信頼フロー - セッションライフサイクル制御 (resume/fork) と高度な worktree ライフサイクル制御 @@ -153,7 +153,7 @@ s08 バックグラウンドタスク [6] s10 チームプロトコル learn-claude-code/ | |-- agents/ # Python リファレンス実装 (s01-s12 + s_full 総括) -|-- docs/{en,zh,ja}/ # メンタルモデル優先のドキュメント (3言語) +|-- docs/{en,zh,ru,ja}/ # メンタルモデル優先のドキュメント (3言語) |-- web/ # インタラクティブ学習プラットフォーム (Next.js) |-- skills/ # s05 の Skill ファイル +-- .github/workflows/ci.yml # CI: 型チェック + ビルド diff --git a/README-ru.md b/README-ru.md new file mode 100644 index 000000000..f0d7d1576 --- /dev/null +++ b/README-ru.md @@ -0,0 +1,238 @@ +[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md) | [Русский](./README-ru.md) + +# Learn Claude Code -- Nano-агент в духе Claude Code, построенный с нуля + +``` + ПАТТЕРН АГЕНТА + ============== + + Пользователь --> messages[] --> LLM --> ответ + | + stop_reason == "tool_use"? + / \ + да нет + | | + выполнить инструменты вернуть текст + добавить результаты + вернуться к циклу ---------> messages[] + + + Это минимальный цикл. Каждому AI-агенту для программирования нужен этот цикл. + В production-агентах добавляются слои политик, разрешений и жизненного цикла. +``` + +**12 прогрессивных сессий — от простого цикла до изолированного автономного выполнения.** +**Каждая сессия добавляет один механизм. У каждого механизма есть своё кредо.** + +> **s01**   *«Одного цикла и Bash достаточно»* — один инструмент + один цикл = агент +> +> **s02**   *«Добавить инструмент — значит добавить один обработчик»* — цикл не меняется; новые инструменты регистрируются в dispatch map +> +> **s03**   *«Агент без плана дрейфует»* — сначала составь список шагов, потом выполняй; эффективность удваивается +> +> **s04**   *«Разбивай большие задачи; каждая подзадача получает чистый контекст»* — субагенты используют независимый messages[], сохраняя основной разговор чистым +> +> **s05**   *«Загружай знания когда нужно, а не заранее»* — внедряй через tool_result, а не в системный промпт +> +> **s06**   *«Контекст заполнится; нужен способ освободить место»* — трёхуровневая стратегия сжатия для бесконечных сессий +> +> **s07**   *«Разбивай большие цели на маленькие задачи, упорядочивай их, сохраняй на диск»* — файловый граф задач с зависимостями, закладывающий основу для многоагентного взаимодействия +> +> **s08**   *«Запускай медленные операции в фоне; агент продолжает думать»* — потоки-демоны выполняют команды, инжектируют уведомления по завершению +> +> **s09**   *«Когда задача слишком велика для одного — делегируй команде»* — постоянные участники + асинхронные почтовые ящики +> +> **s10**   *«Участникам нужны общие правила общения»* — один паттерн запрос-ответ управляет всеми переговорами +> +> **s11**   *«Участники сами просматривают доску и берут задачи»* — лидеру не нужно назначать каждую +> +> **s12**   *«Каждый работает в своей директории, без помех»* — задачи управляют целями, worktree управляют директориями, связанными по ID + +--- + +## Основной паттерн + +```python +def agent_loop(messages): + while True: + response = client.messages.create( + model=MODEL, system=SYSTEM, + messages=messages, tools=TOOLS, + ) + messages.append({"role": "assistant", + "content": response.content}) + + if response.stop_reason != "tool_use": + return + + results = [] + for block in response.content: + if block.type == "tool_use": + output = TOOL_HANDLERS[block.name](**block.input) + results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": output, + }) + messages.append({"role": "user", "content": results}) +``` + +Каждая сессия добавляет один механизм поверх этого цикла — не меняя сам цикл. + +## Область охвата (важно) + +Этот репозиторий — учебный проект 0→1 для построения nano-агента в духе Claude Code. +Намеренно упрощены или опущены некоторые production-механизмы: + +- Полные шины событий/хуков (например, PreToolUse, SessionStart/End, ConfigChange). + s12 включает только минимальный append-only поток событий жизненного цикла для обучения. +- Управление разрешениями на основе правил и рабочие процессы доверия +- Управление жизненным циклом сессии (resume/fork) и расширенное управление жизненным циклом worktree +- Полные детали MCP runtime (transport/OAuth/resource subscribe/polling) + +Относитесь к протоколу JSONL-почтового ящика команды в этом репозитории как к учебной реализации, а не как к утверждению о каких-либо конкретных production-внутренностях. + +## Быстрый старт + +```sh +git clone https://github.com/shareAI-lab/learn-claude-code +cd learn-claude-code +pip install -r requirements.txt +cp .env.example .env # Укажите ваш ANTHROPIC_API_KEY в .env + +python agents/s01_agent_loop.py # Начните здесь +python agents/s12_worktree_task_isolation.py # Конечная точка прогрессии +python agents/s_full.py # Финальная работа: все механизмы вместе +``` + +### Веб-платформа + +Интерактивные визуализации, пошаговые диаграммы, просмотр исходников и документация. + +```sh +cd web && npm install && npm run dev # http://localhost:3000 +``` + +## Путь обучения + +``` +Фаза 1: ЦИКЛ Фаза 2: ПЛАНИРОВАНИЕ И ЗНАНИЯ +================== ============================== +s01 Цикл агента [1] s03 TodoWrite [5] + while + stop_reason TodoManager + nag reminder + | | + +-> s02 Использование [4] s04 Субагенты [5] + инструментов fresh messages[] per child + dispatch map: name->handler | + s05 Skills [5] + SKILL.md via tool_result + | + s06 Context Compact [5] + 3-layer compression + +Фаза 3: ПЕРСИСТЕНТНОСТЬ Фаза 4: КОМАНДЫ +================== ===================== +s07 Задачи [8] s09 Команды агентов [9] + file-based CRUD + deps graph teammates + JSONL mailboxes + | | +s08 Фоновые задачи [6] s10 Протоколы команд [12] + daemon threads + notify queue shutdown + plan approval FSM + | + s11 Автономные агенты [14] + idle cycle + auto-claim + | + s12 Изоляция Worktree [16] + task coordination + optional isolated execution lanes + + [N] = количество инструментов +``` + +## Архитектура + +``` +learn-claude-code/ +| +|-- agents/ # Python-реализации (s01-s12 + финальная работа s_full) +|-- docs/{en,zh,ru,ja}/ # Документация с приоритетом ментальных моделей (3 языка) +|-- web/ # Интерактивная учебная платформа (Next.js) +|-- skills/ # Файлы навыков для s05 ++-- .github/workflows/ci.yml # CI: typecheck + build +``` + +## Документация + +Сначала ментальная модель: проблема, решение, ASCII-диаграмма, минимальный код. +Доступна на [English](./docs/en/) | [中文](./docs/zh/) | [日本語](./docs/ja/) | [Русский](./docs/ru/). + +| Сессия | Тема | Кредо | +|--------|------|-------| +| [s01](./docs/ru/s01-the-agent-loop.md) | Цикл агента | *Одного цикла и Bash достаточно* | +| [s02](./docs/ru/s02-tool-use.md) | Использование инструментов | *Добавить инструмент — значит добавить один обработчик* | +| [s03](./docs/ru/s03-todo-write.md) | TodoWrite | *Агент без плана дрейфует* | +| [s04](./docs/ru/s04-subagent.md) | Субагенты | *Разбивай большие задачи; каждая подзадача получает чистый контекст* | +| [s05](./docs/ru/s05-skill-loading.md) | Навыки | *Загружай знания когда нужно, а не заранее* | +| [s06](./docs/ru/s06-context-compact.md) | Сжатие контекста | *Контекст заполнится; нужен способ освободить место* | +| [s07](./docs/ru/s07-task-system.md) | Задачи | *Разбивай большие цели на маленькие задачи, упорядочивай их, сохраняй на диск* | +| [s08](./docs/ru/s08-background-tasks.md) | Фоновые задачи | *Запускай медленные операции в фоне; агент продолжает думать* | +| [s09](./docs/ru/s09-agent-teams.md) | Команды агентов | *Когда задача слишком велика для одного — делегируй команде* | +| [s10](./docs/ru/s10-team-protocols.md) | Протоколы команд | *Участникам нужны общие правила общения* | +| [s11](./docs/ru/s11-autonomous-agents.md) | Автономные агенты | *Участники сами просматривают доску и берут задачи* | +| [s12](./docs/ru/s12-worktree-task-isolation.md) | Worktree + Изоляция задач | *Каждый работает в своей директории, без помех* | + +## Что дальше — от понимания к созданию продуктов + +После 12 сессий вы понимаете, как работает агент изнутри. Два способа применить эти знания: + +### Kode Agent CLI -- CLI-агент для программирования с открытым исходным кодом + +> `npm i -g @shareai-lab/kode` + +Поддержка Skills и LSP, готов к работе на Windows, совместим с GLM / MiniMax / DeepSeek и другими открытыми моделями. Устанавливай и работай. + +GitHub: **[shareAI-lab/Kode-cli](https://github.com/shareAI-lab/Kode-cli)** + +### Kode Agent SDK -- Встраивание возможностей агента в ваше приложение + +Официальный Claude Code Agent SDK взаимодействует с полным CLI-процессом под капотом — каждый параллельный пользователь означает отдельный терминальный процесс. Kode SDK — это автономная библиотека без накладных расходов на процесс для каждого пользователя, встраиваемая в бэкенды, браузерные расширения, встраиваемые устройства или любую среду выполнения. + +GitHub: **[shareAI-lab/Kode-agent-sdk](https://github.com/shareAI-lab/Kode-agent-sdk)** + +--- + +## Родственный репозиторий: от *сессий по запросу* до *всегда активного помощника* + +Агент, которому учит этот репозиторий, является **одноразовым** — открой терминал, дай задачу, закрой когда сделано, следующая сессия начинается с нуля. Это модель Claude Code. + +[OpenClaw](https://github.com/openclaw/openclaw) доказал другую возможность: поверх того же ядра агента два механизма превращают агента из «ткни, чтобы он двигался» в «он просыпается каждые 30 секунд в поисках работы»: + +- **Heartbeat** — каждые 30 секунд система отправляет агенту сообщение, чтобы проверить, есть ли что-то для выполнения. Ничего? Снова засыпает. Что-то есть? Действует немедленно. +- **Cron** — агент может планировать свои будущие задачи, которые выполняются автоматически в нужное время. + +Добавьте многоканальную маршрутизацию IM (WhatsApp / Telegram / Slack / Discord, 13+ платформ), постоянную контекстную память и систему личности Soul — и агент превращается из одноразового инструмента в всегда активного персонального AI-помощника. + +**[claw0](https://github.com/shareAI-lab/claw0)** — наш учебный репозиторий-компаньон, который разбирает эти механизмы с нуля: + +``` +claw agent = ядро агента + heartbeat + cron + IM-чат + память + soul +``` + +``` +learn-claude-code claw0 +(ядро среды выполнения агента: (проактивный всегда активный помощник: + цикл, инструменты, планирование, heartbeat, cron, IM-каналы, + команды, изоляция worktree) память, личность soul) +``` + +## О проекте +
+ +Отсканируйте WeChat, чтобы подписаться на нас, +или подписывайтесь в X: [shareAI-Lab](https://x.com/baicai003) + +## Лицензия + +MIT + +--- + +**Модель и есть агент. Наша задача — дать ей инструменты и не мешать.** diff --git a/README-zh.md b/README-zh.md index 3cd2cfc68..dd536c78f 100644 --- a/README-zh.md +++ b/README-zh.md @@ -1,6 +1,6 @@ # Learn Claude Code -- 从 0 到 1 构建 nano Claude Code-like agent -[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md) +[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md) | [Русский](./README-ru.md) ``` THE AGENT PATTERN @@ -84,7 +84,7 @@ def agent_loop(messages): 本仓库是一个 0->1 的学习型项目,用于从零构建 nano Claude Code-like agent。 为保证学习路径清晰,仓库有意简化或省略了部分生产机制: -- 完整事件 / Hook 总线 (例如 PreToolUse、SessionStart/End、ConfigChange)。 +- 完整事件 / Hook 总线 (例如 PreToolUse、SessionStart/End、ConfigChange)。 s12 仅提供教学用途的最小 append-only 生命周期事件流。 - 基于规则的权限治理与信任流程 - 会话生命周期控制 (resume/fork) 与更完整的 worktree 生命周期控制 @@ -153,7 +153,7 @@ s08 后台任务 [6] s10 团队协议 [12] learn-claude-code/ | |-- agents/ # Python 参考实现 (s01-s12 + s_full 总纲) -|-- docs/{en,zh,ja}/ # 心智模型优先的文档 (3 种语言) +|-- docs/{en,zh,ru,ja}/ # 心智模型优先的文档 (3 种语言) |-- web/ # 交互式学习平台 (Next.js) |-- skills/ # s05 的 Skill 文件 +-- .github/workflows/ci.yml # CI: 类型检查 + 构建 diff --git a/README.md b/README.md index 9018362c4..7ed4b17b1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md) +[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md) | [Русский](./README-ru.md) + # Learn Claude Code -- A nano Claude Code-like agent, built from 0 to 1 ``` @@ -83,7 +84,7 @@ Every session layers one mechanism on top of this loop -- without changing the l This repository is a 0->1 learning project for building a nano Claude Code-like agent. It intentionally simplifies or omits several production mechanisms: -- Full event/hook buses (for example PreToolUse, SessionStart/End, ConfigChange). +- Full event/hook buses (for example PreToolUse, SessionStart/End, ConfigChange). s12 includes only a minimal append-only lifecycle event stream for teaching. - Rule-based permission governance and trust workflows - Session lifecycle controls (resume/fork) and advanced worktree lifecycle controls @@ -152,7 +153,7 @@ s08 Background Tasks [6] s10 Team Protocols [12] learn-claude-code/ | |-- agents/ # Python reference implementations (s01-s12 + s_full capstone) -|-- docs/{en,zh,ja}/ # Mental-model-first documentation (3 languages) +|-- docs/{en,zh,ru,ja}/ # Mental-model-first documentation (3 languages) |-- web/ # Interactive learning platform (Next.js) |-- skills/ # Skill files for s05 +-- .github/workflows/ci.yml # CI: typecheck + build @@ -225,8 +226,8 @@ learn-claude-code claw0 ## About
-Scan with Wechat to fellow us, -or fellow on X: [shareAI-Lab](https://x.com/baicai003) +Scan with Wechat to fellow us, +or fellow on X: [shareAI-Lab](https://x.com/baicai003) ## License diff --git a/docs/ru/s01-the-agent-loop.md b/docs/ru/s01-the-agent-loop.md new file mode 100644 index 000000000..4f9b94a1d --- /dev/null +++ b/docs/ru/s01-the-agent-loop.md @@ -0,0 +1,114 @@ +# s01: Цикл агента + +`[ s01 ] s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` + +> *«Одного цикла и Bash достаточно»* — один инструмент + один цикл = агент. + +## Проблема + +Языковая модель умеет рассуждать о коде, но не может *взаимодействовать* с реальным миром — читать файлы, запускать тесты, проверять ошибки. Без цикла каждый вызов инструмента требует, чтобы вы вручную копировали результаты обратно. Вы сами становитесь циклом. + +## Решение + +``` ++----------+ +-------+ +----------+ +| Пользов.| ---> | LLM | ---> | Инструм. | +| запрос | | | | выполнить| ++----------+ +---+---+ +----+-----+ + ^ | + | tool_result | + +----------------+ + (цикл до stop_reason != "tool_use") +``` + +Одно условие выхода управляет всем потоком. Цикл работает, пока модель продолжает вызывать инструменты. + +## Как это работает + +1. Запрос пользователя становится первым сообщением. + +```python +messages.append({"role": "user", "content": query}) +``` + +2. Отправляем сообщения и определения инструментов в LLM. + +```python +response = client.messages.create( + model=MODEL, system=SYSTEM, messages=messages, + tools=TOOLS, max_tokens=8000, +) +``` + +3. Добавляем ответ ассистента. Проверяем `stop_reason` — если модель не вызвала инструмент, цикл завершён. + +```python +messages.append({"role": "assistant", "content": response.content}) +if response.stop_reason != "tool_use": + return +``` + +4. Выполняем каждый вызов инструмента, собираем результаты, добавляем как сообщение пользователя. Возвращаемся к шагу 2. + +```python +results = [] +for block in response.content: + if block.type == "tool_use": + output = run_bash(block.input["command"]) + results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": output, + }) +messages.append({"role": "user", "content": results}) +``` + +Собрано в одну функцию: + +```python +def agent_loop(query): + messages = [{"role": "user", "content": query}] + while True: + response = client.messages.create( + model=MODEL, system=SYSTEM, messages=messages, + tools=TOOLS, max_tokens=8000, + ) + messages.append({"role": "assistant", "content": response.content}) + + if response.stop_reason != "tool_use": + return + + results = [] + for block in response.content: + if block.type == "tool_use": + output = run_bash(block.input["command"]) + results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": output, + }) + messages.append({"role": "user", "content": results}) +``` + +Это весь агент менее чем в 30 строках. Всё остальное в этом курсе надстраивается сверху — не меняя сам цикл. + +## Что изменилось + +| Компонент | До | После | +|---------------|------------|--------------------------------| +| Цикл агента | (нет) | `while True` + stop_reason | +| Инструменты | (нет) | `bash` (один инструмент) | +| Сообщения | (нет) | Накапливаемый список | +| Управление | (нет) | `stop_reason != "tool_use"` | + +## Попробуйте + +```sh +cd learn-claude-code +python agents/s01_agent_loop.py +``` + +1. `Create a file called hello.py that prints "Hello, World!"` +2. `List all Python files in this directory` +3. `What is the current git branch?` +4. `Create a directory called test_output and write 3 files in it` diff --git a/docs/ru/s02-tool-use.md b/docs/ru/s02-tool-use.md new file mode 100644 index 000000000..34d52d49a --- /dev/null +++ b/docs/ru/s02-tool-use.md @@ -0,0 +1,97 @@ +# s02: Использование инструментов + +`s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` + +> *«Добавить инструмент — значит добавить один обработчик»* — цикл не меняется; новые инструменты регистрируются в dispatch map. + +## Проблема + +Имея только `bash`, агент использует командную оболочку для всего. `cat` обрезает результат непредсказуемо, `sed` ломается на спецсимволах, и каждый вызов bash — это неограниченная поверхность угроз безопасности. Специализированные инструменты вроде `read_file` и `write_file` позволяют применять ограничения путей на уровне инструмента. + +Ключевое понимание: добавление инструментов не требует изменения цикла. + +## Решение + +``` ++----------+ +-------+ +--------------------+ +| Пользов.| ---> | LLM | ---> | Dispatch инструм. | +| запрос | | | | { | ++----------+ +---+---+ | bash: run_bash | + ^ | read: run_read | + | | write: run_wr | + +-----------+ edit: run_edit | + tool_result | } | + +--------------------+ + +Dispatch map — словарь: {имя_инструмента: функция_обработчик}. +Один lookup заменяет любую цепочку if/elif. +``` + +## Как это работает + +1. Каждый инструмент получает функцию-обработчик. Ограничение путей предотвращает выход за пределы рабочей директории. + +```python +def safe_path(p: str) -> Path: + path = (WORKDIR / p).resolve() + if not path.is_relative_to(WORKDIR): + raise ValueError(f"Path escapes workspace: {p}") + return path + +def run_read(path: str, limit: int = None) -> str: + text = safe_path(path).read_text() + lines = text.splitlines() + if limit and limit < len(lines): + lines = lines[:limit] + return "\n".join(lines)[:50000] +``` + +2. Dispatch map связывает имена инструментов с обработчиками. + +```python +TOOL_HANDLERS = { + "bash": lambda **kw: run_bash(kw["command"]), + "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), + "write_file": lambda **kw: run_write(kw["path"], kw["content"]), + "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], + kw["new_text"]), +} +``` + +3. В цикле находим обработчик по имени. Тело цикла само по себе не изменилось с s01. + +```python +for block in response.content: + if block.type == "tool_use": + handler = TOOL_HANDLERS.get(block.name) + output = handler(**block.input) if handler \ + else f"Unknown tool: {block.name}" + results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": output, + }) +``` + +Добавить инструмент = добавить обработчик + добавить запись в схему. Цикл никогда не меняется. + +## Что изменилось по сравнению с s01 + +| Компонент | До (s01) | После (s02) | +|----------------|-----------------------|----------------------------| +| Инструменты | 1 (только bash) | 4 (bash, read, write, edit)| +| Dispatch | Жёсткий вызов bash | Словарь `TOOL_HANDLERS` | +| Безопасность | Нет | Sandbox `safe_path()` | +| Цикл агента | Без изменений | Без изменений | + +## Попробуйте + +```sh +cd learn-claude-code +python agents/s02_tool_use.py +``` + +1. `Read the file requirements.txt` +2. `Create a file called greet.py with a greet(name) function` +3. `Edit greet.py to add a docstring to the function` +4. `Read greet.py to verify the edit worked` diff --git a/docs/ru/s03-todo-write.md b/docs/ru/s03-todo-write.md new file mode 100644 index 000000000..bee2c8248 --- /dev/null +++ b/docs/ru/s03-todo-write.md @@ -0,0 +1,94 @@ +# s03: TodoWrite + +`s01 > s02 > [ s03 ] s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` + +> *«Агент без плана дрейфует»* — сначала составь список шагов, потом выполняй. + +## Проблема + +На многошаговых задачах модель теряет нить. Она повторяет работу, пропускает шаги или отклоняется от курса. Длинные разговоры усугубляют проблему — системный промпт тускнеет, пока результаты инструментов заполняют контекст. При 10-шаговом рефакторинге модель может выполнить шаги 1-3, а потом начать импровизировать, потому что забыла шаги 4-10. + +## Решение + +``` ++----------+ +-------+ +----------+ +| Пользов.| ---> | LLM | ---> | Инструм. | +| запрос | | | | + todo | ++----------+ +---+---+ +----+-----+ + ^ | + | tool_result | + +----------------+ + | + +-----------+-----------+ + | Состояние TodoManager | + | [ ] задача А | + | [>] задача Б <- сейчас| + | [x] задача В | + +-----------------------+ + | + если rounds_since_todo >= 3: + добавить в tool_result +``` + +## Как это работает + +1. TodoManager хранит элементы со статусами. Только один элемент может быть `in_progress` одновременно. + +```python +class TodoManager: + def update(self, items: list) -> str: + validated, in_progress_count = [], 0 + for item in items: + status = item.get("status", "pending") + if status == "in_progress": + in_progress_count += 1 + validated.append({"id": item["id"], "text": item["text"], + "status": status}) + if in_progress_count > 1: + raise ValueError("Only one task can be in_progress") + self.items = validated + return self.render() +``` + +2. Инструмент `todo` добавляется в dispatch map как любой другой. + +```python +TOOL_HANDLERS = { + # ...базовые инструменты... + "todo": lambda **kw: TODO.update(kw["items"]), +} +``` + +3. Напоминание-«напоминалка» вставляет подсказку, если модель 3+ раундов не вызывает `todo`. + +```python +if rounds_since_todo >= 3 and messages: + last = messages[-1] + if last["role"] == "user" and isinstance(last.get("content"), list): + last["content"].insert(0, { + "type": "text", + "text": "Update your todos.", + }) +``` + +Ограничение «только один in_progress одновременно» обеспечивает последовательный фокус. Напоминание создаёт подотчётность. + +## Что изменилось по сравнению с s02 + +| Компонент | До (s02) | После (s03) | +|----------------|------------------|----------------------------| +| Инструменты | 4 | 5 (+todo) | +| Планирование | Нет | TodoManager со статусами | +| Напоминание | Нет | `` после 3 раундов| +| Цикл агента | Простой dispatch | + счётчик rounds_since_todo| + +## Попробуйте + +```sh +cd learn-claude-code +python agents/s03_todo_write.py +``` + +1. `Refactor the file hello.py: add type hints, docstrings, and a main guard` +2. `Create a Python package with __init__.py, utils.py, and tests/test_utils.py` +3. `Review all Python files and fix any style issues` diff --git a/docs/ru/s04-subagent.md b/docs/ru/s04-subagent.md new file mode 100644 index 000000000..95421256e --- /dev/null +++ b/docs/ru/s04-subagent.md @@ -0,0 +1,92 @@ +# s04: Субагенты + +`s01 > s02 > s03 > [ s04 ] s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` + +> *«Разбивай большие задачи; каждая подзадача получает чистый контекст»* — субагенты используют независимый messages[], сохраняя основной разговор чистым. + +## Проблема + +По мере работы агента его массив messages растёт. Каждое прочитанное файл, каждый вывод bash остаётся в контексте навсегда. «Какой тестовый фреймворк использует этот проект?» может потребовать чтения 5 файлов, но родителю нужен только ответ: «pytest». + +## Решение + +``` +Родительский агент Субагент ++------------------+ +------------------+ +| messages=[...] | | messages=[] | <-- чистый +| | dispatch | | +| tool: task | ----------> | while tool_use: | +| prompt="..." | | вызов инструм. | +| | summary | добавить рез. | +| result = "..." | <---------- | вернуть текст | ++------------------+ +------------------+ + +Контекст родителя остаётся чистым. Контекст субагента отбрасывается. +``` + +## Как это работает + +1. Родитель получает инструмент `task`. Дочерний агент получает все базовые инструменты кроме `task` (без рекурсивного порождения). + +```python +PARENT_TOOLS = CHILD_TOOLS + [ + {"name": "task", + "description": "Spawn a subagent with fresh context.", + "input_schema": { + "type": "object", + "properties": {"prompt": {"type": "string"}}, + "required": ["prompt"], + }}, +] +``` + +2. Субагент стартует с `messages=[]` и запускает собственный цикл. Только финальный текст возвращается родителю. + +```python +def run_subagent(prompt: str) -> str: + sub_messages = [{"role": "user", "content": prompt}] + for _ in range(30): # ограничение безопасности + response = client.messages.create( + model=MODEL, system=SUBAGENT_SYSTEM, + messages=sub_messages, + tools=CHILD_TOOLS, max_tokens=8000, + ) + sub_messages.append({"role": "assistant", + "content": response.content}) + if response.stop_reason != "tool_use": + break + results = [] + for block in response.content: + if block.type == "tool_use": + handler = TOOL_HANDLERS.get(block.name) + output = handler(**block.input) + results.append({"type": "tool_result", + "tool_use_id": block.id, + "content": str(output)[:50000]}) + sub_messages.append({"role": "user", "content": results}) + return "".join( + b.text for b in response.content if hasattr(b, "text") + ) or "(no summary)" +``` + +Вся история сообщений дочернего агента (возможно, 30+ вызовов инструментов) отбрасывается. Родитель получает одноабзацную сводку как обычный `tool_result`. + +## Что изменилось по сравнению с s03 + +| Компонент | До (s03) | После (s04) | +|----------------|------------------|---------------------------| +| Инструменты | 5 | 5 (base) + task (parent) | +| Контекст | Единый общий | Изоляция родитель + дочерний| +| Субагент | Нет | Функция `run_subagent()` | +| Возвращаемое | Н/Д | Только сводный текст | + +## Попробуйте + +```sh +cd learn-claude-code +python agents/s04_subagent.py +``` + +1. `Use a subtask to find what testing framework this project uses` +2. `Delegate: read all .py files and summarize what each one does` +3. `Use a task to create a new module, then verify it from here` diff --git a/docs/ru/s05-skill-loading.md b/docs/ru/s05-skill-loading.md new file mode 100644 index 000000000..3f5b2e805 --- /dev/null +++ b/docs/ru/s05-skill-loading.md @@ -0,0 +1,106 @@ +# s05: Навыки + +`s01 > s02 > s03 > s04 > [ s05 ] s06 | s07 > s08 > s09 > s10 > s11 > s12` + +> *«Загружай знания когда нужно, а не заранее»* — внедряй через tool_result, а не в системный промпт. + +## Проблема + +Вы хотите, чтобы агент следовал специфичным для домена рабочим процессам: git-конвенциям, паттернам тестирования, чеклистам code review. Помещать всё в системный промпт расходует токены на неиспользуемые навыки. 10 навыков по 2000 токенов каждый = 20 000 токенов, большинство из которых не релевантны ни одной конкретной задаче. + +## Решение + +``` +Системный промпт (Слой 1 — всегда присутствует): ++--------------------------------------+ +| Ты — агент для программирования. | +| Доступные навыки: | +| - git: Помощники для git-процессов | ~100 токенов/навык +| - test: Лучшие практики тестирования| ++--------------------------------------+ + +Когда модель вызывает load_skill("git"): ++--------------------------------------+ +| tool_result (Слой 2 — по требованию):| +| | +| Полные инструкции для git... | ~2000 токенов +| Шаг 1: ... | +| | ++--------------------------------------+ +``` + +Слой 1: *имена* навыков в системном промпте (дёшево). Слой 2: полное *содержимое* через tool_result (по требованию). + +## Как это работает + +1. Каждый навык — директория с `SKILL.md` с YAML-метаданными. + +``` +skills/ + pdf/ + SKILL.md # ---\n name: pdf\n description: Process PDF files\n ---\n ... + code-review/ + SKILL.md # ---\n name: code-review\n description: Review code\n ---\n ... +``` + +2. SkillLoader сканирует файлы `SKILL.md`, используя имя директории как идентификатор навыка. + +```python +class SkillLoader: + def __init__(self, skills_dir: Path): + self.skills = {} + for f in sorted(skills_dir.rglob("SKILL.md")): + text = f.read_text() + meta, body = self._parse_frontmatter(text) + name = meta.get("name", f.parent.name) + self.skills[name] = {"meta": meta, "body": body} + + def get_descriptions(self) -> str: + lines = [] + for name, skill in self.skills.items(): + desc = skill["meta"].get("description", "") + lines.append(f" - {name}: {desc}") + return "\n".join(lines) + + def get_content(self, name: str) -> str: + skill = self.skills.get(name) + if not skill: + return f"Error: Unknown skill '{name}'." + return f"\n{skill['body']}\n" +``` + +3. Слой 1 идёт в системный промпт. Слой 2 — просто ещё один обработчик инструмента. + +```python +SYSTEM = f"""You are a coding agent at {WORKDIR}. +Skills available: +{SKILL_LOADER.get_descriptions()}""" + +TOOL_HANDLERS = { + # ...базовые инструменты... + "load_skill": lambda **kw: SKILL_LOADER.get_content(kw["name"]), +} +``` + +Модель узнаёт, какие навыки существуют (дёшево), и загружает их когда нужно (дорого). + +## Что изменилось по сравнению с s04 + +| Компонент | До (s04) | После (s05) | +|----------------|------------------|----------------------------| +| Инструменты | 5 (base + task) | 5 (base + load_skill) | +| Системный промпт| Статическая строка| + описания навыков | +| Знания | Нет | Файлы skills/\*/SKILL.md | +| Внедрение | Нет | Двухслойное (system + result)| + +## Попробуйте + +```sh +cd learn-claude-code +python agents/s05_skill_loading.py +``` + +1. `What skills are available?` +2. `Load the agent-builder skill and follow its instructions` +3. `I need to do a code review -- load the relevant skill first` +4. `Build an MCP server using the mcp-builder skill` diff --git a/docs/ru/s06-context-compact.md b/docs/ru/s06-context-compact.md new file mode 100644 index 000000000..ef3edf4c4 --- /dev/null +++ b/docs/ru/s06-context-compact.md @@ -0,0 +1,123 @@ +# s06: Сжатие контекста + +`s01 > s02 > s03 > s04 > s05 > [ s06 ] | s07 > s08 > s09 > s10 > s11 > s12` + +> *«Контекст заполнится; нужен способ освободить место»* — трёхуровневая стратегия сжатия для бесконечных сессий. + +## Проблема + +Контекстное окно конечно. Один `read_file` на файл в 1000 строк стоит ~4000 токенов. После чтения 30 файлов и 20 bash-команд вы достигаете 100 000+ токенов. Агент не может работать с большими кодовыми базами без сжатия. + +## Решение + +Три уровня с нарастающей агрессивностью: + +``` +Каждый ход: ++------------------+ +| Результат вызова | ++------------------+ + | + v +[Уровень 1: micro_compact] (тихо, каждый ход) + Заменить tool_result старше 3 ходов + на "[Previous: used {tool_name}]" + | + v +[Проверка: токенов > 50000?] + | | + нет да + | | + v v +продолжить [Уровень 2: auto_compact] + Сохранить стенограмму в .transcripts/ + LLM суммирует разговор. + Заменить все сообщения на [сводку]. + | + v + [Уровень 3: инструмент compact] + Модель вызывает compact явно. + То же суммирование, что и auto_compact. +``` + +## Как это работает + +1. **Уровень 1 — micro_compact**: Перед каждым вызовом LLM заменяем старые результаты инструментов заглушками. + +```python +def micro_compact(messages: list) -> list: + tool_results = [] + for i, msg in enumerate(messages): + if msg["role"] == "user" and isinstance(msg.get("content"), list): + for j, part in enumerate(msg["content"]): + if isinstance(part, dict) and part.get("type") == "tool_result": + tool_results.append((i, j, part)) + if len(tool_results) <= KEEP_RECENT: + return messages + for _, _, part in tool_results[:-KEEP_RECENT]: + if len(part.get("content", "")) > 100: + part["content"] = f"[Previous: used {tool_name}]" + return messages +``` + +2. **Уровень 2 — auto_compact**: Когда токенов больше порога, сохраняем полную стенограмму на диск, затем просим LLM суммировать. + +```python +def auto_compact(messages: list) -> list: + # Сохраняем стенограмму для восстановления + transcript_path = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl" + with open(transcript_path, "w") as f: + for msg in messages: + f.write(json.dumps(msg, default=str) + "\n") + # LLM суммирует + response = client.messages.create( + model=MODEL, + messages=[{"role": "user", "content": + "Summarize this conversation for continuity..." + + json.dumps(messages, default=str)[:80000]}], + max_tokens=2000, + ) + return [ + {"role": "user", "content": f"[Compressed]\n\n{response.content[0].text}"}, + {"role": "assistant", "content": "Understood. Continuing."}, + ] +``` + +3. **Уровень 3 — ручной compact**: Инструмент `compact` запускает то же суммирование по требованию. + +4. Цикл интегрирует все три уровня: + +```python +def agent_loop(messages: list): + while True: + micro_compact(messages) # Уровень 1 + if estimate_tokens(messages) > THRESHOLD: + messages[:] = auto_compact(messages) # Уровень 2 + response = client.messages.create(...) + # ... выполнение инструментов ... + if manual_compact: + messages[:] = auto_compact(messages) # Уровень 3 +``` + +Стенограммы сохраняют полную историю на диске. Ничего по-настоящему не теряется — просто перемещается за пределы активного контекста. + +## Что изменилось по сравнению с s05 + +| Компонент | До (s05) | После (s06) | +|----------------|------------------|----------------------------| +| Инструменты | 5 | 5 (base + compact) | +| Управление конт.| Нет | Трёхуровневое сжатие | +| Micro-compact | Нет | Старые результаты -> заглушки| +| Auto-compact | Нет | Триггер по порогу токенов | +| Стенограммы | Нет | Сохраняются в .transcripts/| + +## Попробуйте + +```sh +cd learn-claude-code +python agents/s06_context_compact.py +``` + +1. `Read every Python file in the agents/ directory one by one` (следите, как micro-compact заменяет старые результаты) +2. `Keep reading files until compression triggers automatically` +3. `Use the compact tool to manually compress the conversation` diff --git a/docs/ru/s07-task-system.md b/docs/ru/s07-task-system.md new file mode 100644 index 000000000..f1697016d --- /dev/null +++ b/docs/ru/s07-task-system.md @@ -0,0 +1,125 @@ +# s07: Система задач + +`s01 > s02 > s03 > s04 > s05 > s06 | [ s07 ] s08 > s09 > s10 > s11 > s12` + +> *«Разбивай большие цели на маленькие задачи, упорядочивай их, сохраняй на диск»* — файловый граф задач с зависимостями, закладывающий основу для многоагентного взаимодействия. + +## Проблема + +TodoManager из s03 — плоский чеклист в памяти: без порядка, без зависимостей, без статусов кроме «сделано/нет». Реальные цели имеют структуру — задача Б зависит от задачи А, задачи В и Г могут выполняться параллельно, задача Д ждёт завершения обеих В и Г. + +Без явных связей агент не может понять, что готово, что заблокировано, а что может выполняться параллельно. И поскольку список живёт только в памяти, сжатие контекста (s06) полностью его стирает. + +## Решение + +Превратить чеклист в **граф задач**, сохранённый на диске. Каждая задача — JSON-файл со статусом, зависимостями (`blockedBy`) и зависящими задачами (`blocks`). Граф в любой момент отвечает на три вопроса: + +- **Что готово?** — задачи со статусом `pending` и пустым `blockedBy`. +- **Что заблокировано?** — задачи, ждущие незавершённых зависимостей. +- **Что сделано?** — задачи со статусом `completed`, завершение которых автоматически разблокирует зависящие задачи. + +``` +.tasks/ + task_1.json {"id":1, "status":"completed"} + task_2.json {"id":2, "blockedBy":[1], "status":"pending"} + task_3.json {"id":3, "blockedBy":[1], "status":"pending"} + task_4.json {"id":4, "blockedBy":[2,3], "status":"pending"} + +Граф задач (DAG): + +----------+ + +--> | задача 2 | --+ + | | pending | | ++----------+ +----------+ +--> +----------+ +| задача 1 | | задача 4 | +| completed| --> +----------+ +--> | blocked | ++----------+ | задача 3 | --+ +----------+ + | pending | + +----------+ + +Порядок: задача 1 должна завершиться до 2 и 3 +Параллелизм: задачи 2 и 3 могут выполняться одновременно +Зависимости: задача 4 ждёт завершения обеих 2 и 3 +Статусы: pending -> in_progress -> completed +``` + +Этот граф задач становится координационным каркасом для всего, что идёт после s07: фоновое выполнение (s08), многоагентные команды (s09+) и изоляция worktree (s12) — все читают и пишут в эту же структуру. + +## Как это работает + +1. **TaskManager**: один JSON-файл на задачу, CRUD с графом зависимостей. + +```python +class TaskManager: + def __init__(self, tasks_dir: Path): + self.dir = tasks_dir + self.dir.mkdir(exist_ok=True) + self._next_id = self._max_id() + 1 + + def create(self, subject, description=""): + task = {"id": self._next_id, "subject": subject, + "status": "pending", "blockedBy": [], + "blocks": [], "owner": ""} + self._save(task) + self._next_id += 1 + return json.dumps(task, indent=2) +``` + +2. **Разрешение зависимостей**: завершение задачи удаляет её ID из списка `blockedBy` всех других задач, автоматически разблокируя зависящие. + +```python +def _clear_dependency(self, completed_id): + for f in self.dir.glob("task_*.json"): + task = json.loads(f.read_text()) + if completed_id in task.get("blockedBy", []): + task["blockedBy"].remove(completed_id) + self._save(task) +``` + +3. **Статус + связывание зависимостей**: `update` обрабатывает переходы и рёбра зависимостей. + +```python +def update(self, task_id, status=None, + add_blocked_by=None, add_blocks=None): + task = self._load(task_id) + if status: + task["status"] = status + if status == "completed": + self._clear_dependency(task_id) + self._save(task) +``` + +4. Четыре инструмента задач добавляются в dispatch map. + +```python +TOOL_HANDLERS = { + # ...базовые инструменты... + "task_create": lambda **kw: TASKS.create(kw["subject"]), + "task_update": lambda **kw: TASKS.update(kw["task_id"], kw.get("status")), + "task_list": lambda **kw: TASKS.list_all(), + "task_get": lambda **kw: TASKS.get(kw["task_id"]), +} +``` + +Начиная с s07, граф задач — стандартный инструмент для многошаговой работы. Todo из s03 остаётся для быстрых односессионных чеклистов. + +## Что изменилось по сравнению с s06 + +| Компонент | До (s06) | После (s07) | +|---|---|---| +| Инструменты | 5 | 8 (`task_create/update/list/get`) | +| Модель планирования | Плоский чеклист (в памяти) | Граф задач с зависимостями (на диске) | +| Связи | Нет | Рёбра `blockedBy` + `blocks` | +| Отслеживание статусов | Да/нет | `pending` -> `in_progress` -> `completed` | +| Персистентность | Теряется при сжатии | Переживает сжатие и перезапуски | + +## Попробуйте + +```sh +cd learn-claude-code +python agents/s07_task_system.py +``` + +1. `Create 3 tasks: "Setup project", "Write code", "Write tests". Make them depend on each other in order.` +2. `List all tasks and show the dependency graph` +3. `Complete task 1 and then list tasks to see task 2 unblocked` +4. `Create a task board for refactoring: parse -> transform -> emit -> test, where transform and emit can run in parallel after parse` diff --git a/docs/ru/s08-background-tasks.md b/docs/ru/s08-background-tasks.md new file mode 100644 index 000000000..d1efcc6b4 --- /dev/null +++ b/docs/ru/s08-background-tasks.md @@ -0,0 +1,107 @@ +# s08: Фоновые задачи + +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > [ s08 ] s09 > s10 > s11 > s12` + +> *«Запускай медленные операции в фоне; агент продолжает думать»* — потоки-демоны выполняют команды, инжектируют уведомления по завершению. + +## Проблема + +Некоторые команды занимают минуты: `npm install`, `pytest`, `docker build`. С блокирующим циклом модель простаивает в ожидании. Если пользователь просит «установи зависимости и пока они устанавливаются, создай файл конфигурации», агент выполняет их последовательно, а не параллельно. + +## Решение + +``` +Основной поток Фоновый поток ++-----------------+ +-----------------+ +| цикл агента | | subprocess runs | +| ... | | ... | +| [Вызов LLM] <--+-------- | enqueue(result) | +| ^сброс очереди | +-----------------+ ++-----------------+ + +Временная шкала: +Агент --[запуск А]--[запуск Б]--[другая работа]---- + | | + v v + [А работает] [Б работает] (параллельно) + | | + +-- результаты инжектируются перед следующим вызовом LLM --+ +``` + +## Как это работает + +1. BackgroundManager отслеживает задачи с потокобезопасной очередью уведомлений. + +```python +class BackgroundManager: + def __init__(self): + self.tasks = {} + self._notification_queue = [] + self._lock = threading.Lock() +``` + +2. `run()` запускает поток-демон и немедленно возвращает управление. + +```python +def run(self, command: str) -> str: + task_id = str(uuid.uuid4())[:8] + self.tasks[task_id] = {"status": "running", "command": command} + thread = threading.Thread( + target=self._execute, args=(task_id, command), daemon=True) + thread.start() + return f"Background task {task_id} started" +``` + +3. Когда подпроцесс завершается, его результат попадает в очередь уведомлений. + +```python +def _execute(self, task_id, command): + try: + r = subprocess.run(command, shell=True, cwd=WORKDIR, + capture_output=True, text=True, timeout=300) + output = (r.stdout + r.stderr).strip()[:50000] + except subprocess.TimeoutExpired: + output = "Error: Timeout (300s)" + with self._lock: + self._notification_queue.append({ + "task_id": task_id, "result": output[:500]}) +``` + +4. Цикл агента сбрасывает уведомления перед каждым вызовом LLM. + +```python +def agent_loop(messages: list): + while True: + notifs = BG.drain_notifications() + if notifs: + notif_text = "\n".join( + f"[bg:{n['task_id']}] {n['result']}" for n in notifs) + messages.append({"role": "user", + "content": f"\n{notif_text}\n" + f""}) + messages.append({"role": "assistant", + "content": "Noted background results."}) + response = client.messages.create(...) +``` + +Цикл остаётся однопоточным. Параллелизуется только ввод-вывод подпроцессов. + +## Что изменилось по сравнению с s07 + +| Компонент | До (s07) | После (s08) | +|----------------|------------------|----------------------------| +| Инструменты | 8 | 6 (base + background_run + check)| +| Выполнение | Только блокирующее| Блокирующее + фоновые потоки| +| Уведомления | Нет | Очередь сбрасывается в цикле| +| Параллелизм | Нет | Потоки-демоны | + +## Попробуйте + +```sh +cd learn-claude-code +python agents/s08_background_tasks.py +``` + +1. `Run "sleep 5 && echo done" in the background, then create a file while it runs` +2. `Start 3 background tasks: "sleep 2", "sleep 4", "sleep 6". Check their status.` +3. `Run pytest in the background and keep working on other things` diff --git a/docs/ru/s09-agent-teams.md b/docs/ru/s09-agent-teams.md new file mode 100644 index 000000000..6b97584e4 --- /dev/null +++ b/docs/ru/s09-agent-teams.md @@ -0,0 +1,125 @@ +# s09: Команды агентов + +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > [ s09 ] s10 > s11 > s12` + +> *«Когда задача слишком велика для одного — делегируй команде»* — постоянные участники + асинхронные почтовые ящики. + +## Проблема + +Субагенты (s04) одноразовые: породил, поработал, вернул сводку, завершился. Нет идентичности, нет памяти между вызовами. Фоновые задачи (s08) выполняют shell-команды, но не могут принимать решения с помощью LLM. + +Для реальной командной работы нужны: (1) постоянные агенты, переживающие один промпт, (2) управление идентичностью и жизненным циклом, (3) канал связи между агентами. + +## Решение + +``` +Жизненный цикл участника: + spawn -> WORKING -> IDLE -> WORKING -> ... -> SHUTDOWN + +Коммуникация: + .team/ + config.json <- список команды + статусы + inbox/ + alice.jsonl <- append-only, очистка при чтении + bob.jsonl + lead.jsonl + + +--------+ send("alice","bob","...") +--------+ + | alice | -----------------------------> | bob | + | loop | bob.jsonl << {json_line} | loop | + +--------+ +--------+ + ^ | + | BUS.read_inbox("alice") | + +---- alice.jsonl -> read + drain ---------+ +``` + +## Как это работает + +1. TeammateManager поддерживает config.json со списком команды. + +```python +class TeammateManager: + def __init__(self, team_dir: Path): + self.dir = team_dir + self.dir.mkdir(exist_ok=True) + self.config_path = self.dir / "config.json" + self.config = self._load_config() + self.threads = {} +``` + +2. `spawn()` создаёт участника и запускает его цикл агента в потоке. + +```python +def spawn(self, name: str, role: str, prompt: str) -> str: + member = {"name": name, "role": role, "status": "working"} + self.config["members"].append(member) + self._save_config() + thread = threading.Thread( + target=self._teammate_loop, + args=(name, role, prompt), daemon=True) + thread.start() + return f"Spawned teammate '{name}' (role: {role})" +``` + +3. MessageBus: JSONL-почтовые ящики с дозаписью. `send()` добавляет JSON-строку; `read_inbox()` читает всё и очищает. + +```python +class MessageBus: + def send(self, sender, to, content, msg_type="message", extra=None): + msg = {"type": msg_type, "from": sender, + "content": content, "timestamp": time.time()} + if extra: + msg.update(extra) + with open(self.dir / f"{to}.jsonl", "a") as f: + f.write(json.dumps(msg) + "\n") + + def read_inbox(self, name): + path = self.dir / f"{name}.jsonl" + if not path.exists(): return "[]" + msgs = [json.loads(l) for l in path.read_text().strip().splitlines() if l] + path.write_text("") # очистить + return json.dumps(msgs, indent=2) +``` + +4. Каждый участник проверяет почтовый ящик перед каждым вызовом LLM, вставляя полученные сообщения в контекст. + +```python +def _teammate_loop(self, name, role, prompt): + messages = [{"role": "user", "content": prompt}] + for _ in range(50): + inbox = BUS.read_inbox(name) + if inbox != "[]": + messages.append({"role": "user", + "content": f"{inbox}"}) + messages.append({"role": "assistant", + "content": "Noted inbox messages."}) + response = client.messages.create(...) + if response.stop_reason != "tool_use": + break + # выполнить инструменты, добавить результаты... + self._find_member(name)["status"] = "idle" +``` + +## Что изменилось по сравнению с s08 + +| Компонент | До (s08) | После (s09) | +|----------------|------------------|----------------------------| +| Инструменты | 6 | 9 (+spawn/send/read_inbox) | +| Агенты | Один | Лид + N участников | +| Персистентность| Нет | config.json + JSONL-ящики | +| Потоки | Фоновые команды | Полные циклы агентов в потоках| +| Жизненный цикл | Fire-and-forget | idle -> working -> idle | +| Коммуникация | Нет | message + broadcast | + +## Попробуйте + +```sh +cd learn-claude-code +python agents/s09_agent_teams.py +``` + +1. `Spawn alice (coder) and bob (tester). Have alice send bob a message.` +2. `Broadcast "status update: phase 1 complete" to all teammates` +3. `Check the lead inbox for any messages` +4. Введите `/team`, чтобы увидеть список команды со статусами +5. Введите `/inbox`, чтобы вручную проверить почтовый ящик лида diff --git a/docs/ru/s10-team-protocols.md b/docs/ru/s10-team-protocols.md new file mode 100644 index 000000000..f5cb0557c --- /dev/null +++ b/docs/ru/s10-team-protocols.md @@ -0,0 +1,104 @@ +# s10: Протоколы команды + +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > [ s10 ] s11 > s12` + +> *«Участникам нужны общие правила общения»* — один паттерн запрос-ответ управляет всеми переговорами. + +## Проблема + +В s09 участники работают и общаются, но им не хватает структурированной координации: + +**Завершение работы**: Убить поток — значит оставить файлы наполовину записанными и config.json устаревшим. Нужно рукопожатие: лид запрашивает, участник одобряет (заканчивает и выходит) или отказывает (продолжает работу). + +**Утверждение плана**: Когда лид говорит «отрефактори модуль аутентификации», участник начинает немедленно. Для рискованных изменений лид должен сначала проверить план. + +Оба случая имеют одинаковую структуру: одна сторона отправляет запрос с уникальным ID, другая отвечает, ссылаясь на тот же ID. + +## Решение + +``` +Протокол завершения Протокол утверждения плана +================== ========================== + +Лид Участник Участник Лид + | | | | + |--shutdown_req-->| |--plan_req------>| + | {req_id:"abc"} | | {req_id:"xyz"} | + | | | | + |<--shutdown_resp-| |<--plan_resp-----| + | {req_id:"abc", | | {req_id:"xyz", | + | approve:true} | | approve:true} | + +Общий автомат состояний (FSM): + [pending] --approve--> [approved] + [pending] --reject---> [rejected] + +Трекеры: + shutdown_requests = {req_id: {target, status}} + plan_requests = {req_id: {from, plan, status}} +``` + +## Как это работает + +1. Лид инициирует завершение, генерируя request_id и отправляя через почтовый ящик. + +```python +shutdown_requests = {} + +def handle_shutdown_request(teammate: str) -> str: + req_id = str(uuid.uuid4())[:8] + shutdown_requests[req_id] = {"target": teammate, "status": "pending"} + BUS.send("lead", teammate, "Please shut down gracefully.", + "shutdown_request", {"request_id": req_id}) + return f"Shutdown request {req_id} sent (status: pending)" +``` + +2. Участник получает запрос и отвечает approve/reject. + +```python +if tool_name == "shutdown_response": + req_id = args["request_id"] + approve = args["approve"] + shutdown_requests[req_id]["status"] = "approved" if approve else "rejected" + BUS.send(sender, "lead", args.get("reason", ""), + "shutdown_response", + {"request_id": req_id, "approve": approve}) +``` + +3. Утверждение плана следует идентичному паттерну. Участник отправляет план (генерирует request_id), лид рассматривает (ссылаясь на тот же request_id). + +```python +plan_requests = {} + +def handle_plan_review(request_id, approve, feedback=""): + req = plan_requests[request_id] + req["status"] = "approved" if approve else "rejected" + BUS.send("lead", req["from"], feedback, + "plan_approval_response", + {"request_id": request_id, "approve": approve}) +``` + +Один FSM, два применения. Одна машина состояний `pending -> approved | rejected` обрабатывает любой протокол запрос-ответ. + +## Что изменилось по сравнению с s09 + +| Компонент | До (s09) | После (s10) | +|----------------|------------------|------------------------------| +| Инструменты | 9 | 12 (+shutdown_req/resp +plan)| +| Завершение | Естественный выход| Рукопожатие запрос-ответ | +| Гейтирование планов| Нет | Отправка/проверка с утверждением| +| Корреляция | Нет | request_id на запрос | +| FSM | Нет | pending -> approved/rejected | + +## Попробуйте + +```sh +cd learn-claude-code +python agents/s10_team_protocols.py +``` + +1. `Spawn alice as a coder. Then request her shutdown.` +2. `List teammates to see alice's status after shutdown approval` +3. `Spawn bob with a risky refactoring task. Review and reject his plan.` +4. `Spawn charlie, have him submit a plan, then approve it.` +5. Введите `/team` для мониторинга статусов diff --git a/docs/ru/s11-autonomous-agents.md b/docs/ru/s11-autonomous-agents.md new file mode 100644 index 000000000..6317cec29 --- /dev/null +++ b/docs/ru/s11-autonomous-agents.md @@ -0,0 +1,140 @@ +# s11: Автономные агенты + +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > [ s11 ] s12` + +> *«Участники сами просматривают доску и берут задачи»* — лидеру не нужно назначать каждую. + +## Проблема + +В s09-s10 участники работают только когда им явно говорят. Лид должен порождать каждого с конкретным промптом. 10 незатребованных задач на доске? Лид назначает каждую вручную. Это не масштабируется. + +Истинная автономность: участники сами просматривают доску задач, забирают незатребованные задачи, работают над ними, потом ищут ещё. + +Одна тонкость: после сжатия контекста (s06) агент может забыть, кто он. Повторная инжекция идентичности решает эту проблему. + +## Решение + +``` +Жизненный цикл участника с idle-циклом: + ++-------+ +| spawn | ++---+---+ + | + v ++-------+ tool_use +-------+ +| WORK | <------------- | LLM | ++---+---+ +-------+ + | + | stop_reason != tool_use (или вызван инструмент idle) + v ++--------+ +| IDLE | опрос каждые 5с до 60с ++---+----+ + | + +---> проверить inbox --> сообщение? ----------> WORK + | + +---> сканировать .tasks/ --> незатребованное? -> забрать -> WORK + | + +---> таймаут 60с ----------------------> SHUTDOWN + +Повторная инжекция идентичности после сжатия: + если len(messages) <= 3: + messages.insert(0, identity_block) +``` + +## Как это работает + +1. Цикл участника имеет две фазы: WORK и IDLE. Когда LLM перестаёт вызывать инструменты (или вызывает `idle`), участник переходит в IDLE. + +```python +def _loop(self, name, role, prompt): + while True: + # -- ФАЗА WORK -- + messages = [{"role": "user", "content": prompt}] + for _ in range(50): + response = client.messages.create(...) + if response.stop_reason != "tool_use": + break + # выполнить инструменты... + if idle_requested: + break + + # -- ФАЗА IDLE -- + self._set_status(name, "idle") + resume = self._idle_poll(name, messages) + if not resume: + self._set_status(name, "shutdown") + return + self._set_status(name, "working") +``` + +2. Фаза idle опрашивает почтовый ящик и доску задач в цикле. + +```python +def _idle_poll(self, name, messages): + for _ in range(IDLE_TIMEOUT // POLL_INTERVAL): # 60с / 5с = 12 + time.sleep(POLL_INTERVAL) + inbox = BUS.read_inbox(name) + if inbox: + messages.append({"role": "user", + "content": f"{inbox}"}) + return True + unclaimed = scan_unclaimed_tasks() + if unclaimed: + claim_task(unclaimed[0]["id"], name) + messages.append({"role": "user", + "content": f"Task #{unclaimed[0]['id']}: " + f"{unclaimed[0]['subject']}"}) + return True + return False # таймаут -> завершение +``` + +3. Сканирование доски задач: найти задачи со статусом pending, без владельца, без блокировок. + +```python +def scan_unclaimed_tasks() -> list: + unclaimed = [] + for f in sorted(TASKS_DIR.glob("task_*.json")): + task = json.loads(f.read_text()) + if (task.get("status") == "pending" + and not task.get("owner") + and not task.get("blockedBy")): + unclaimed.append(task) + return unclaimed +``` + +4. Повторная инжекция идентичности: когда контекст слишком короткий (произошло сжатие), вставляем блок идентичности. + +```python +if len(messages) <= 3: + messages.insert(0, {"role": "user", + "content": f"You are '{name}', role: {role}, " + f"team: {team_name}. Continue your work."}) + messages.insert(1, {"role": "assistant", + "content": f"I am {name}. Continuing."}) +``` + +## Что изменилось по сравнению с s10 + +| Компонент | До (s10) | После (s11) | +|----------------|------------------|----------------------------| +| Инструменты | 12 | 14 (+idle, +claim_task) | +| Автономность | По указанию лида | Самоорганизующиеся | +| Фаза idle | Нет | Опрос inbox + доски задач | +| Захват задач | Только вручную | Авто-захват незатребованных| +| Идентичность | Системный промпт | + повторная инжекция после сжатия| +| Таймаут | Нет | 60с idle -> авто-завершение| + +## Попробуйте + +```sh +cd learn-claude-code +python agents/s11_autonomous_agents.py +``` + +1. `Create 3 tasks on the board, then spawn alice and bob. Watch them auto-claim.` +2. `Spawn a coder teammate and let it find work from the task board itself` +3. `Create tasks with dependencies. Watch teammates respect the blocked order.` +4. Введите `/tasks` для просмотра доски задач с владельцами +5. Введите `/team` для мониторинга статуса работы участников diff --git a/docs/ru/s12-worktree-task-isolation.md b/docs/ru/s12-worktree-task-isolation.md new file mode 100644 index 000000000..dd227cdaf --- /dev/null +++ b/docs/ru/s12-worktree-task-isolation.md @@ -0,0 +1,119 @@ +# s12: Worktree + Изоляция задач + +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > [ s12 ]` + +> *«Каждый работает в своей директории, без помех»* — задачи управляют целями, worktree управляют директориями, связанными по ID. + +## Проблема + +К s11 агенты могут самостоятельно захватывать и выполнять задачи. Но каждая задача выполняется в одной общей директории. Два агента, рефакторящих разные модули одновременно, столкнутся: агент А редактирует `config.py`, агент Б редактирует `config.py`, несохранённые изменения смешиваются, и ни один не может откатить чисто. + +Доска задач отслеживает *что делать*, но не имеет мнения о *где это делать*. Решение: дать каждой задаче собственную git worktree директорию. Задачи управляют целями, worktree управляют контекстом выполнения. Связываем их по ID задачи. + +## Решение + +``` +Плоскость управления (.tasks/) Плоскость выполнения (.worktrees/) ++------------------+ +------------------------+ +| task_1.json | | auth-refactor/ | +| status: in_progress <------> branch: wt/auth-refactor +| worktree: "auth-refactor" | task_id: 1 | ++------------------+ +------------------------+ +| task_2.json | | ui-login/ | +| status: pending <------> branch: wt/ui-login +| worktree: "ui-login" | task_id: 2 | ++------------------+ +------------------------+ + | + index.json (реестр worktree) + events.jsonl (лог жизненного цикла) + +Автоматы состояний: + Задача: pending -> in_progress -> completed + Worktree: absent -> active -> removed | kept +``` + +## Как это работает + +1. **Создать задачу.** Сначала сохраняем цель. + +```python +TASKS.create("Implement auth refactor") +# -> .tasks/task_1.json status=pending worktree="" +``` + +2. **Создать worktree и привязать к задаче.** Передача `task_id` автоматически переводит задачу в `in_progress`. + +```python +WORKTREES.create("auth-refactor", task_id=1) +# -> git worktree add -b wt/auth-refactor .worktrees/auth-refactor HEAD +# -> index.json получает новую запись, task_1.json получает worktree="auth-refactor" +``` + +Привязка записывает состояние с обеих сторон: + +```python +def bind_worktree(self, task_id, worktree): + task = self._load(task_id) + task["worktree"] = worktree + if task["status"] == "pending": + task["status"] = "in_progress" + self._save(task) +``` + +3. **Выполнять команды в worktree.** `cwd` указывает на изолированную директорию. + +```python +subprocess.run(command, shell=True, cwd=worktree_path, + capture_output=True, text=True, timeout=300) +``` + +4. **Завершить работу.** Два варианта: + - `worktree_keep(name)` — сохранить директорию для дальнейшего использования. + - `worktree_remove(name, complete_task=True)` — удалить директорию, завершить привязанную задачу, записать событие. Один вызов обрабатывает демонтаж + завершение. + +```python +def remove(self, name, force=False, complete_task=False): + self._run_git(["worktree", "remove", wt["path"]]) + if complete_task and wt.get("task_id") is not None: + self.tasks.update(wt["task_id"], status="completed") + self.tasks.unbind_worktree(wt["task_id"]) + self.events.emit("task.completed", ...) +``` + +5. **Поток событий.** Каждый шаг жизненного цикла записывается в `.worktrees/events.jsonl`: + +```json +{ + "event": "worktree.remove.after", + "task": {"id": 1, "status": "completed"}, + "worktree": {"name": "auth-refactor", "status": "removed"}, + "ts": 1730000000 +} +``` + +Записываемые события: `worktree.create.before/after/failed`, `worktree.remove.before/after/failed`, `worktree.keep`, `task.completed`. + +После сбоя состояние восстанавливается из `.tasks/` + `.worktrees/index.json` на диске. Память разговора изменчива; состояние файлов долговечно. + +## Что изменилось по сравнению с s11 + +| Компонент | До (s11) | После (s12) | +|--------------------|----------------------------|----------------------------------------------| +| Координация | Доска задач (владелец/статус)| Доска задач + явная привязка worktree | +| Область выполнения | Общая директория | Изолированная директория для каждой задачи | +| Восстанавливаемость| Только статус задачи | Статус задачи + индекс worktree | +| Демонтаж | Завершение задачи | Завершение задачи + явный keep/remove | +| Видимость ЖЦ | Неявно в логах | Явные события в `.worktrees/events.jsonl` | + +## Попробуйте + +```sh +cd learn-claude-code +python agents/s12_worktree_task_isolation.py +``` + +1. `Create tasks for backend auth and frontend login page, then list tasks.` +2. `Create worktree "auth-refactor" for task 1, then bind task 2 to a new worktree "ui-login".` +3. `Run "git status --short" in worktree "auth-refactor".` +4. `Keep worktree "ui-login", then list worktrees and inspect events.` +5. `Remove worktree "auth-refactor" with complete_task=true, then list tasks/worktrees/events.` diff --git a/web/scripts/extract-content.ts b/web/scripts/extract-content.ts index 6e35badd9..842abf8f6 100644 --- a/web/scripts/extract-content.ts +++ b/web/scripts/extract-content.ts @@ -103,9 +103,11 @@ function countLoc(lines: string[]): number { // docs/en/s01-the-agent-loop.md -> "en" // docs/zh/s01-the-agent-loop.md -> "zh" // docs/ja/s01-the-agent-loop.md -> "ja" -function detectLocale(relPath: string): "en" | "zh" | "ja" { +// docs/ru/s01-the-agent-loop.md -> "ru" +function detectLocale(relPath: string): "en" | "zh" | "ja" | "ru" { if (relPath.startsWith("zh/") || relPath.startsWith("zh\\")) return "zh"; if (relPath.startsWith("ja/") || relPath.startsWith("ja\\")) return "ja"; + if (relPath.startsWith("ru/") || relPath.startsWith("ru\\")) return "ru"; return "en"; } @@ -219,7 +221,7 @@ function main() { const docs: DocContent[] = []; if (fs.existsSync(DOCS_DIR)) { - const localeDirs = ["en", "zh", "ja"]; + const localeDirs = ["en", "zh", "ja", "ru"]; let totalDocFiles = 0; for (const locale of localeDirs) { @@ -245,7 +247,7 @@ function main() { const titleMatch = content.match(/^#\s+(.+)$/m); const title = titleMatch ? titleMatch[1] : filename; - docs.push({ version, locale: locale as "en" | "zh" | "ja", title, content }); + docs.push({ version, locale: locale as "en" | "zh" | "ja" | "ru", title, content }); } } diff --git a/web/src/app/[locale]/layout.tsx b/web/src/app/[locale]/layout.tsx index 62e7dd60b..cbe174804 100644 --- a/web/src/app/[locale]/layout.tsx +++ b/web/src/app/[locale]/layout.tsx @@ -4,10 +4,11 @@ import { Header } from "@/components/layout/header"; import en from "@/i18n/messages/en.json"; import zh from "@/i18n/messages/zh.json"; import ja from "@/i18n/messages/ja.json"; +import ru from "@/i18n/messages/ru.json"; import "../globals.css"; -const locales = ["en", "zh", "ja"]; -const metaMessages: Record = { en, zh, ja }; +const locales = ["en", "zh", "ja", "ru"]; +const metaMessages: Record = { en, zh, ja, ru }; export function generateStaticParams() { return locales.map((locale) => ({ locale })); diff --git a/web/src/components/layout/header.tsx b/web/src/components/layout/header.tsx index dd724d198..97755e9e1 100644 --- a/web/src/components/layout/header.tsx +++ b/web/src/components/layout/header.tsx @@ -17,6 +17,7 @@ const LOCALES = [ { code: "en", label: "EN" }, { code: "zh", label: "中文" }, { code: "ja", label: "日本語" }, + { code: "ru", label: "RU" }, ]; export function Header() { diff --git a/web/src/i18n/messages/ru.json b/web/src/i18n/messages/ru.json new file mode 100644 index 000000000..982f89794 --- /dev/null +++ b/web/src/i18n/messages/ru.json @@ -0,0 +1,76 @@ +{ + "meta": { "title": "Learn Claude Code", "description": "Построй nano-агент в духе Claude Code с нуля — один механизм за раз" }, + "nav": { "home": "Главная", "timeline": "Путь обучения", "compare": "Сравнение", "layers": "Слои", "github": "GitHub" }, + "home": { "hero_title": "Learn Claude Code", "hero_subtitle": "Построй nano-агент в духе Claude Code с нуля — один механизм за раз", "start": "Начать обучение", "core_pattern": "Основной паттерн", "core_pattern_desc": "Все AI-агенты для программирования разделяют один цикл: вызов модели, выполнение инструментов, возврат результатов. Production-системы добавляют слои политик, разрешений и жизненного цикла сверху.", "learning_path": "Путь обучения", "learning_path_desc": "12 прогрессивных сессий — от простого цикла до изолированного автономного выполнения", "layers_title": "Архитектурные слои", "layers_desc": "Пять ортогональных аспектов, составляющих полноценного агента", "loc": "строк", "learn_more": "Подробнее", "versions_in_layer": "версий", "message_flow": "Рост сообщений", "message_flow_desc": "Наблюдайте за ростом массива messages по мере выполнения цикла агента" }, + "version": { "loc": "строк кода", "tools": "инструментов", "new": "Новое", "prev": "Предыдущий", "next": "Следующий", "view_source": "Исходный код", "view_diff": "Показать изменения", "design_decisions": "Решения при проектировании", "whats_new": "Что нового", "tutorial": "Руководство", "simulator": "Симулятор цикла агента", "execution_flow": "Поток выполнения", "architecture": "Архитектура", "concept_viz": "Визуализация концепции", "alternatives": "Рассмотренные альтернативы", "tab_learn": "Учиться", "tab_simulate": "Симуляция", "tab_code": "Код", "tab_deep_dive": "Глубокое погружение" }, + "sim": { "play": "Играть", "pause": "Пауза", "step": "Шаг", "reset": "Сброс", "speed": "Скорость", "step_of": "из" }, + "timeline": { "title": "Путь обучения", "subtitle": "s01 — s12: прогрессивный дизайн агента", "layer_legend": "Легенда слоёв", "loc_growth": "Рост объёма кода", "learn_more": "Подробнее" }, + "layers": { + "title": "Архитектурные слои", + "subtitle": "Пять ортогональных аспектов, составляющих полноценного агента", + "tools": "Что агент УМЕЕТ делать. Фундамент: инструменты дают модели возможности взаимодействовать с миром.", + "planning": "Как организована работа. От простых todo-списков до доски задач с зависимостями, общей для агентов.", + "memory": "Удержание контекста в заданных пределах. Стратегии сжатия позволяют агентам работать бесконечно без потери связности.", + "concurrency": "Неблокирующее выполнение. Фоновые потоки и шина уведомлений для параллельной работы.", + "collaboration": "Координация многих агентов. Команды, обмен сообщениями и автономные участники, мыслящие самостоятельно." + }, + "compare": { + "title": "Сравнение версий", + "subtitle": "Посмотрите, что изменилось между любыми двумя версиями", + "select_a": "Версия А", + "select_b": "Версия Б", + "loc_delta": "Разница строк", + "lines": "строк", + "new_tools_in_b": "Новые инструменты в Б", + "new_classes_in_b": "Новые классы в Б", + "new_functions_in_b": "Новые функции в Б", + "tool_comparison": "Сравнение инструментов", + "only_in": "Только в", + "shared": "Общие", + "none": "Нет", + "source_diff": "Разница исходного кода", + "empty_hint": "Выберите две версии выше для сравнения.", + "architecture": "Архитектура" + }, + "diff": { + "new_classes": "Новые классы", + "new_tools": "Новые инструменты", + "new_functions": "Новые функции", + "loc_delta": "Разница строк" + }, + "sessions": { + "s01": "Цикл агента", + "s02": "Инструменты", + "s03": "TodoWrite", + "s04": "Субагенты", + "s05": "Навыки", + "s06": "Сжатие контекста", + "s07": "Система задач", + "s08": "Фоновые задачи", + "s09": "Команды агентов", + "s10": "Протоколы команды", + "s11": "Автономные агенты", + "s12": "Worktree + Изоляция задач" + }, + "layer_labels": { + "tools": "Инструменты и выполнение", + "planning": "Планирование и координация", + "memory": "Управление памятью", + "concurrency": "Параллелизм", + "collaboration": "Совместная работа" + }, + "viz": { + "s01": "Цикл while агента", + "s02": "Dispatch map инструментов", + "s03": "Система напоминаний TodoWrite", + "s04": "Изоляция контекста субагента", + "s05": "Загрузка навыков по требованию", + "s06": "Трёхуровневое сжатие контекста", + "s07": "Граф зависимостей задач", + "s08": "Дорожки фоновых задач", + "s09": "Почтовые ящики команды агентов", + "s10": "FSM-протоколы команды", + "s11": "Цикл автономного агента", + "s12": "Изоляция задач через Worktree" + } +} diff --git a/web/src/lib/i18n-server.ts b/web/src/lib/i18n-server.ts index 885008f44..76f53ebf2 100644 --- a/web/src/lib/i18n-server.ts +++ b/web/src/lib/i18n-server.ts @@ -1,10 +1,11 @@ import en from "@/i18n/messages/en.json"; import zh from "@/i18n/messages/zh.json"; import ja from "@/i18n/messages/ja.json"; +import ru from "@/i18n/messages/ru.json"; type Messages = typeof en; -const messagesMap: Record = { en, zh, ja }; +const messagesMap: Record = { en, zh, ja, ru }; export function getTranslations(locale: string, namespace: string) { const messages = messagesMap[locale] || en; diff --git a/web/src/lib/i18n.tsx b/web/src/lib/i18n.tsx index 9ab61e058..4f6dd00ba 100644 --- a/web/src/lib/i18n.tsx +++ b/web/src/lib/i18n.tsx @@ -3,10 +3,11 @@ import { createContext, useContext, ReactNode } from "react"; import en from "@/i18n/messages/en.json"; import zh from "@/i18n/messages/zh.json"; import ja from "@/i18n/messages/ja.json"; +import ru from "@/i18n/messages/ru.json"; type Messages = typeof en; -const messagesMap: Record = { en, zh, ja }; +const messagesMap: Record = { en, zh, ja, ru }; const I18nContext = createContext<{ locale: string; messages: Messages }>({ locale: "en", diff --git a/web/src/types/agent-data.ts b/web/src/types/agent-data.ts index 7cf01a04d..ed3ba7b44 100644 --- a/web/src/types/agent-data.ts +++ b/web/src/types/agent-data.ts @@ -25,7 +25,7 @@ export interface VersionDiff { export interface DocContent { version: string; - locale: "en" | "zh" | "ja"; + locale: "en" | "zh" | "ja" | "ru"; title: string; content: string; // raw markdown }