{"locale":"ru","source":{"rawBase":"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/docs","githubBase":"https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs","editBase":"https://github.com/earendil-works/pi/edit/main/packages/coding-agent/docs"},"redirects":[{"from":"/docs/latest/session","to":"/docs/latest/session-format"},{"from":"/docs/latest/tree","to":"/docs/latest/sessions"}],"fileToSlug":{"compaction.md":"compaction","containerization.md":"containerization","custom-provider.md":"custom-provider","development.md":"development","environment-variables.md":"environment-variables","extensions.md":"extensions","index.md":"index","json.md":"json","keybindings.md":"keybindings","llama-cpp.md":"llama-cpp","models.md":"models","packages.md":"packages","prompt-templates.md":"prompt-templates","providers.md":"providers","quickstart.md":"quickstart","rpc.md":"rpc","sdk.md":"sdk","security.md":"security","session-format.md":"session-format","sessions.md":"sessions","settings.md":"settings","shell-aliases.md":"shell-aliases","skills.md":"skills","terminal-setup.md":"terminal-setup","termux.md":"termux","themes.md":"themes","tmux.md":"tmux","tui.md":"tui","usage.md":"usage","windows.md":"windows"},"pages":{"ru":{"compaction":{"title":"Сжатие и суммирование ветвей","markdown":"LLM имеют ограниченные контекстные окна. Когда разговоры становятся слишком длинными, Pi использует сжатие, чтобы суммировать старый контент, сохраняя при этом недавнюю работу. На этой странице описаны как автоматическое сжатие, так и branch summarization.\n\n**Исходные файлы** ([pi-mono](https://github.com/earendil-works/pi-mono)):\n- [`packages/coding-agent/src/core/compaction/compaction.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) — логика автоматического сжатия.\n- [`packages/coding-agent/src/core/compaction/branch-summarization.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) — суммирование ветвей\n- [`packages/coding-agent/src/core/compaction/utils.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/utils.ts) — Общие утилиты (отслеживание файлов, сериализация)\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts) — Типы записи (`CompactionEntry`, `BranchSummaryEntry`)\n- [`packages/coding-agent/src/core/extensions/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts) — Типы событий расширения\n\nДля определений TypeScript в вашем проекте проверьте `node_modules/@earendil-works/pi-coding-agent/dist/`.\n\n## Обзор\n\nPi имеет два механизма суммирования:\n\n| Механизм | Курок | Цель |\n|-----------|---------|---------|\n| Уплотнение | Контекст превышает пороговое значение или `/compact` | Обобщите старые сообщения, чтобы освободить контекст. |\n| Обобщение ветвей | `/tree` навигация | Сохранять контекст при переключении ветвей |\n\nОба используют один и тот же формат структурированной сводки и кумулятивно отслеживают операции с файлами. Запросы сжатия и сводки ветвей используют новые идентификаторы сеансов маршрутизации и, если это поддерживается поставщиком, отключают запись в кэш подсказок, поскольку эти одноразовые подсказки вряд ли будут использоваться повторно.\n\n## Уплотнение\n\n### Когда это срабатывает\n\nАвтоматическое сжатие срабатывает, когда:\n\n```\ncontextTokens > contextWindow - reserveTokens\n```\n\nПо умолчанию `reserveTokens` — это 16384 токена (настраивается в `~/.pi/agent/settings.json` или `<project-dir>/.pi/settings.json`). Это оставляет место для ответа LLM.\n\nВы также можете активировать вручную с помощью `/compact [instructions]`, где дополнительные инструкции фокусируют сводку.\n\n### Как это работает\n\n1. **Найти точку отсечения**: идти назад от самого нового сообщения, накапливая оценки токенов до тех пор, пока не будет достигнуто `keepRecentTokens` (по умолчанию 20 тыс., настраивается в `~/.pi/agent/settings.json` или `<project-dir>/.pi/settings.json`).\n2. **Извлечение сообщений**: собирайте сообщения от предыдущей сохраненной границы (или начала сеанса) до точки обрезки.\n3. **Создать сводку**: вызов LLM для подведения итогов в структурированном формате, передавая предыдущую сводку в качестве итеративного контекста, если она присутствует.\n4. **Добавить запись**: сохраните `CompactionEntry` со сводкой и `firstKeptEntryId`.\n5. **Перезагрузка**: сеанс перезагружается с использованием сводки и сообщений, начиная с `firstKeptEntryId`.\n\n```\nBefore compaction:\n\n  entry:  0     1     2     3      4     5     6      7      8     9\n        ┌─────┬─────┬─────┬─────┬──────┬─────┬──── ─┬──────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┘\n                └────────┬───────┘ └──────────────┬──────────────┘\n               messagesToSummarize            kept messages\n                                   ↑\n                          firstKeptEntryId (entry 4)\n\nAfter compaction (new entry appended):\n\n  entry:  0     1     2     3      4     5     6      7      8     9     10\n        ┌─────┬─────┬─────┬─────┬──────┬─────┬──── ─┬──────┬─────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│ cmp │\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┴─────┘\n               └──────────┬──────┘ └──────────────────────┬───────────────────┘\n                 not sent to LLM                    sent to LLM\n                                                         ↑\n                                              starts from firstKeptEntryId\n\nWhat the LLM sees:\n\n  ┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐\n  │ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │\n  └────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘\n       ↑         ↑      └─────────────────┬────────────────┘\n    prompt   from cmp          messages from firstKeptEntryId\n```\n\nПри повторных уплотнениях суммарный диапазон начинается с сохраненной границы предыдущего уплотнения (`firstKeptEntryId`), а не с самой записи уплотнения, возвращаясь к записи после предыдущего уплотнения, если эту сохраненную запись невозможно найти на пути. Это сохраняет сообщения, пережившие предыдущее сжатие, включая их также в следующий проход суммирования. Pi также пересчитывает `tokensBefore` из перестроенного контекста сеанса перед записью нового `CompactionEntry`, поэтому количество токенов отражает фактический заменяемый контекст предварительного уплотнения.\n\n### Разделенные повороты\n\n«Поворот» начинается с сообщения пользователя и включает в себя все ответы помощника и вызовы инструментов до следующего сообщения пользователя. Обычно уплотнение срезается на границах поворотов.\n\nКогда один оборот превышает `keepRecentTokens`, точка отсечения оказывается в середине поворота на сообщении помощника. Это «разделенный поворот»:\n\n```\nSplit turn (one huge turn exceeds budget):\n\n  entry:  0     1     2      3     4      5      6     7      8\n        ┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐\n        │ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │\n        └─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘\n                ↑                                     ↑\n         turnStartIndex = 1                  firstKeptEntryId = 7\n                │                                     │\n                └──── turnPrefixMessages (1-6) ───────┘\n                                                      └── kept (7-8)\n\n  isSplitTurn = true\n  messagesToSummarize = []  (no complete turns before)\n  turnPrefixMessages = [usr, ass, tool, ass, tool, tool]\n```\n\nДля разделенных поворотов Pi генерирует две сводки и объединяет их:\n1. **Сводка истории**: предыдущий контекст (если таковой имеется).\n2. **Сводка префиксов поворотов**: начало раздельного поворота.\n\n### Правила обрезки точек\n\nДопустимые точки отсечения:\n- Сообщения пользователя\n- Сообщения Ассистента\n- Сообщения BashExecution\n- Пользовательские сообщения (custom_message, Branch_summary)\n\nНикогда не режьте результаты инструмента (они должны оставаться в соответствии со своим вызовом инструмента).\n\n### Структура ввода уплотнения\n\nОпределено в [`session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface CompactionEntry<T = unknown> {\n  type: \"compaction\";\n  id: string;\n  parentId: string;\n  timestamp: number;\n  summary: string;\n  firstKeptEntryId: string;\n  tokensBefore: number;\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default compaction uses this for details (from compaction.ts):\ninterface CompactionDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nExtensions может хранить любые JSON-сериализуемые данные в `details`. Сжатие по умолчанию отслеживает файловые операции, но реализации пользовательских расширений могут использовать собственную структуру. Сгенерированные и предоставленные расширениями сводки сохраняют свой LLM `usage`, если они доступны, поэтому итоговые данные сеанса включают работу по суммированию.\n\nСм. реализацию [`prepareCompaction()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) и [`compact()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts). При прямом программном обобщении `generateSummary()` возвращает текст сводки, а `generateSummaryWithUsage()` возвращает `{ text, usage }`.\n\n## Обобщение ветвей\n\n### Когда это срабатывает\n\nКогда вы используете `/tree` для перехода к другой ветке, Pi предлагает подвести итоги работы, которую вы оставляете. Это вводит контекст из левой ветки в новую ветку.\n\n### Как это работает\n\n1. **Найти общего предка**: самый глубокий узел, общий для старых и новых позиций.\n2. **Собирайте записи**: пройдите от старого листа к общему предку.\n3. **Подготовка с учетом бюджета**: включайте сообщения, не превышающие бюджет токена (сначала самые новые).\n4. **Создать сводку**: вызов LLM в структурированном формате.\n5. **Добавить запись**: сохраните `BranchSummaryEntry` в точке навигации.\n\n```\nTree before navigation:\n\n         ┌─ B ─ C ─ D (old leaf, being abandoned)\n    A ───┤\n         └─ E ─ F (target)\n\nCommon ancestor: A\nEntries to summarize: B, C, D\n\nAfter navigation with summary:\n\n         ┌─ B ─ C ─ D\n    A ───┤\n         └─ E ─ F ─ [summary of B,C,D] (new leaf)\n```\n\n### Совокупное отслеживание файлов\n\nИ сжатие, и branch summarization отслеживают файлы совокупно. При создании сводки pi извлекает файловые операции из:\n- Вызовы инструментов в суммируемых сообщениях\n- Предыдущее сжатие или сводка ветвей `details` (если есть)\n\nЭто означает, что отслеживание файлов накапливается в нескольких уплотнениях или сводках вложенных ветвей, сохраняя полную историю чтения и изменения файлов.\n\n### Структура ввода сводки филиала\n\nОпределено в [`session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface BranchSummaryEntry<T = unknown> {\n  type: \"branch_summary\";\n  id: string;\n  parentId: string;\n  timestamp: number;\n  summary: string;\n  fromId: string;      // Entry we navigated from\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default branch summarization uses this for details (from branch-summarization.ts):\ninterface BranchSummaryDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nКак и при сжатии, расширения могут хранить пользовательские данные в `details`.\n\nСм. реализацию [`collectEntriesForBranchSummary()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), [`prepareBranchEntries()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) и [`generateBranchSummary()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts).\n\n## Формат сводки\n\nИ сжатие, и branch summarization используют один и тот же структурированный формат:\n\n```markdown\n## Goal\n[What the user is trying to accomplish]\n\n## Constraints & Preferences\n- [Requirements mentioned by user]\n\n## Progress\n### Done\n- [x] [Completed tasks]\n\n### In Progress\n- [ ] [Current work]\n\n### Blocked\n- [Issues, if any]\n\n## Key Decisions\n- **[Decision]**: [Rationale]\n\n## Next Steps\n1. [What should happen next]\n\n## Critical Context\n- [Data needed to continue]\n\n<read-files>\npath/to/file1.ts\npath/to/file2.ts\n</read-files>\n\n<modified-files>\npath/to/changed.ts\n</modified-files>\n```\n\n### Сериализация сообщений\n\nПеред обобщением сообщения сериализуются в текст через [`serializeConversation()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/utils.ts):\n\n```\n[User]: What they said\n[Assistant thinking]: Internal reasoning\n[Assistant]: Response text\n[Assistant tool calls]: read(path=\"foo.ts\"); edit(path=\"bar.ts\", ...)\n[Tool result]: Output from tool\n```\n\nЭто не позволяет модели рассматривать это как диалог для продолжения.\n\nРезультаты инструмента во время сериализации усекаются до 2000 символов. Содержимое, превышающее этот предел, заменяется маркером, указывающим, сколько символов было усечено. Это удерживает запросы на обобщение в пределах разумного бюджета токенов, поскольку результаты инструментов (особенно из `read` и `bash`), как правило, вносят наибольший вклад в размер контекста.\n\n## Пользовательское суммирование с помощью Extensions\n\nExtensions может перехватывать и настраивать как уплотнение, так и branch summarization. См. [`extensions/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts) для определений типов событий.\n\n### session_before_compact\n\nЗапускается перед автосжатием или `/compact`. Можно отменить или предоставить собственное резюме. См. `SessionBeforeCompactEvent` и `CompactionPreparation` в файле типов.\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // preparation.messagesToSummarize - messages to summarize\n  // preparation.turnPrefixMessages - split turn prefix (if isSplitTurn)\n  // preparation.previousSummary - previous compaction summary\n  // preparation.fileOps - extracted file operations\n  // preparation.tokensBefore - context tokens before compaction\n  // preparation.firstKeptEntryId - where kept messages start\n  // preparation.settings - compaction settings\n\n  // branchEntries - all entries on current branch (for custom state)\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n  // signal - AbortSignal (pass to LLM calls)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"Your summary...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: { /* custom data */ },\n    }\n  };\n});\n```\n\n#### Преобразование сообщений в текст\n\nЧтобы создать сводку с помощью собственной модели, преобразуйте сообщения в текст, используя `serializeConversation`:\n\n```typescript\nimport { convertToLlm, serializeConversation } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation } = event;\n  \n  // Convert AgentMessage[] to Message[], then serialize to text\n  const conversationText = serializeConversation(\n    convertToLlm(preparation.messagesToSummarize)\n  );\n  // Returns:\n  // [User]: message text\n  // [Assistant thinking]: thinking content\n  // [Assistant]: response text\n  // [Assistant tool calls]: read(path=\"...\"); bash(command=\"...\")\n  // [Tool result]: output text\n\n  // Now send to your model for summarization\n  const { summary, usage } = await myModel.summarize(conversationText);\n  \n  return {\n    compaction: {\n      summary,\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      usage,\n    }\n  };\n});\n```\n\nСм. [custom-compaction.ts](../examples/extensions/custom-compaction.ts) полный пример с использованием другой модели.\n\n### session_before_tree\n\nСрабатывает до навигации `/tree`. Всегда срабатывает независимо от того, решил ли пользователь подвести итоги. Можно отменить навигацию или предоставить собственную сводку.\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n\n  // preparation.targetId - where we're navigating to\n  // preparation.oldLeafId - current position (being abandoned)\n  // preparation.commonAncestorId - shared ancestor\n  // preparation.entriesToSummarize - entries that would be summarized\n  // preparation.userWantsSummary - whether user chose to summarize\n\n  // Cancel navigation entirely:\n  return { cancel: true };\n\n  // Provide custom summary (only used if userWantsSummary is true):\n  if (preparation.userWantsSummary) {\n    return {\n      summary: {\n        summary: \"Your summary...\",\n        // usage: summaryResponse.usage, // Optional; included in session totals\n        details: { /* custom data */ },\n      }\n    };\n  }\n});\n```\n\nСм. `SessionBeforeTreeEvent` и `TreePreparation` в файле типов.\n\n## Настройки\n\nНастройте уплотнение в `~/.pi/agent/settings.json` или `<project-dir>/.pi/settings.json`:\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n| Параметр | По умолчанию | Описание |\n|---------|---------|-------------|\n| `enabled` | `true` | Включить автоматическое сжатие |\n| `reserveTokens` | `16384` | Токены для резерва для ответа LLM |\n| `keepRecentTokens` | `20000` | Последние токены, которые нужно сохранить (не суммируются) |\n\nОтключите автоматическое сжатие с помощью `\"enabled\": false`. Вы по-прежнему можете сжимать вручную с помощью `/compact`.","sourceFile":"compaction.md"},"containerization":{"title":"Контейнеризация","markdown":"Pi по умолчанию работает со всеми разрешениями, но в некоторых случаях вам может потребоваться больше контроля над тем, в какие каталоги Pi может выполняться запись и какие права доступа он имеет.\n\nЕсть два общих варианта. Вы можете либо\n1. запустить весь процесс `pi` в изолированной среде или\n2. запустите `pi` на хосте и направьте выполнение инструмента в изолированную среду.\n\n## Выберите узор\n\n| Шаблон | Что изолировано | Лучшее для | Примечания |\n| --- | --- | --- | --- |\n| расширение Gondolin | Встроенные инструменты и команды `!` | Локальная изоляция микро-VM при сохранении аутентификации на хосте | См. [`examples/extensions/gondolin/`](../examples/extensions/gondolin/). |\n| Обычный Docker | Весь процесс `pi` в локальном контейнере | Простая локальная изоляция | Поставщики API key входят в контейнер. |\n| OpenShell | Весь процесс `pi` в контролируемой политикой sandbox | Локальное или удаленное управление sandbox | Требуется шлюз OpenShell. |\n\nExtensions запускается везде, где выполняется процесс `pi`. Если вы запустите хост `pi` с расширением маршрутизации инструментов, другие пользовательские инструменты расширения по-прежнему будут работать на хосте, если они также не делегируют свои операции.\n\n## Gondolin\n\n[Gondolin](https://github.com/earendil-works/gondolin) — локальная микро-VM Linux.\nИспользуйте [example extension](../examples/extensions/gondolin), если вы хотите, чтобы `pi` был на хосте, но все встроенные инструменты были перенаправлены на виртуальную машину.\n\nНастраивать:\n\n```bash\ncp -R packages/coding-agent/examples/extensions/gondolin ~/.pi/agent/extensions/gondolin\ncd ~/.pi/agent/extensions/gondolin\nnpm install --ignore-scripts\n```\n\nЗапустите из проекта, который вы хотите смонтировать:\n\n```bash\ncd /path/to/project\npi -e ~/.pi/agent/extensions/gondolin\n```\n\nРасширение монтирует cwd хоста по адресу `/workspace` на виртуальной машине и переопределяет `read`, `write`, `edit`, `bash`, `grep`, `find` и `ls`.\nКоманды пользователя `!` также перенаправляются в виртуальную машину.\nИзменения файлов под `/workspace` записываются на хост.\n\nТребования: Node.js >= 23.6.0 для `@earendil-works/gondolin` плюс QEMU (требуется установка через менеджер пакетов).\n\n## Обычный Docker\n\nЗапустите весь процесс `pi` в Docker, если вам нужна простейшая граница локального контейнера.\n\n`Dockerfile.pi`:\n\n```dockerfile\nFROM node:24-bookworm-slim\n\nRUN apt-get update \\\n  && apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \\\n  && rm -rf /var/lib/apt/lists/*\nRUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\nWORKDIR /workspace\nENTRYPOINT [\"pi\"]\n```\n\nСборка и запуск:\n\n```bash\ndocker build -t pi-sandbox -f Dockerfile.pi .\n\ndocker run --rm -it \\\n  -e ANTHROPIC_API_KEY \\\n  -v \"$PWD:/workspace\" \\\n  -v pi-agent-home:/root/.pi/agent \\\n  pi-sandbox\n```\n\n`-v \"$PWD:/workspace\"` монтирует ваш текущий каталог в контейнер /workspace, так что чтение и запись в `/workspace` внутри Docker напрямую влияет на файлы вашего хоста, как в примере Gondolin.\n\nИспользуйте именованный том для `/root/.pi/agent`, если вам нужны локальные настройки и сеансы контейнера. При монтировании хоста `~/.pi/agent` контейнеру предоставляются файлы аутентификации и сеанса хоста.\n\n## OpenShell\n\nИспользуйте [NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview), если вам нужен управляемый политикой sandbox с элементами управления файловой системой, процессами, сетью, учетными данными и выводами.\nOpenShell может запускать sandboxes через локальный шлюз, поддерживаемый Docker, Podman или средой выполнения виртуальной машины, или через удаленный шлюз Kubernetes.\n\nКаждому sandbox требуется активный шлюз.\nЗарегистрируйтесь и выберите один, прежде чем создавать sandbox:\n\n```bash\nopenshell gateway add <gateway-url> --name <name>\nopenshell gateway select <name>\n```\n\nЗапустите `pi` внутри OpenShell sandbox:\n\n```bash\nopenshell sandbox create --name pi-sandbox --from pi -- pi\n```\n\nВ этом шаблоне весь процесс `pi` выполняется внутри sandbox.\nВстроенные инструменты, команды `!` и инструменты расширения выполняются внутри границы OpenShell.\n\nЕсли шлюз удален, файлы проекта не монтируются с хоста по привязке, то есть записи в sandbox не отражаются на вашем компьютере.\nКлонируйте репозиторий внутри sandbox или используйте команды передачи файлов OpenShell:\n\n```bash\nopenshell sandbox upload pi-sandbox ./repo /workspace\nopenshell sandbox download pi-sandbox /workspace/repo ./repo-out\n```\n\nПоставщики OpenShell могут хранить необработанные модели API key за пределами sandbox.\nКогда маршрутизация вывода настроена, код внутри sandbox может вызывать `https://inference.local`, и шлюз вводит настроенные учетные данные поставщика в восходящий поток.\nНастройте Pi для использования соответствующей конечной точки, совместимой с OpenAI или Anthropic, если вы хотите, чтобы трафик модели использовал этот маршрут.","sourceFile":"containerization.md"},"custom-provider":{"title":"Пользовательский Providers","markdown":"Extensions может зарегистрировать поставщиков пользовательских моделей через `pi.registerProvider()`. Это позволяет:\n\n- **Прокси** – маршрутизация запросов через корпоративные прокси или шлюзы API.\n- **Пользовательские конечные точки** – используйте развертывания локальной или частной модели.\n- **OAuth/SSO** – добавление потоков аутентификации для корпоративных поставщиков.\n- **Пользовательские APIs** — реализация потоковой передачи для нестандартных LLM APIs.\n\n## Пример Extensions\n\nСм. эти полные примеры поставщиков:\n\n- [`examples/extensions/custom-provider-anthropic/`](../examples/extensions/custom-provider-anthropic/)\n- [`examples/extensions/custom-provider-gitlab-duo/`](../examples/extensions/custom-provider-gitlab-duo/)\n\n## Оглавление\n\n- [Example Extensions](#example-extensions)\n- [Quick Reference](#quick-reference)\n- [Override Existing Provider](#override-existing-provider)\n- [Register New Provider](#register-new-provider)\n- [Unregister Provider](#unregister-provider)\n- [OAuth Support](#oauth-support)\n- [Custom Streaming API](#custom-streaming-api)\n- [Context Overflow Errors](#context-overflow-errors)\n- [Testing Your Implementation](#testing-your-implementation)\n- [Config Reference](#config-reference)\n- [Model Definition Reference](#model-definition-reference)\n\n## Краткий справочник\n\nExtensions может зарегистрировать либо полный pi-ai `Provider`, либо использовать устаревшую форму конфигурации поставщика. Если требуется настраиваемая проверка подлинности, фильтрация, обновление или потоковая передача, отдайте предпочтение полному поставщику. Pi составляет `models.json` переопределение над зарегистрированными собственными поставщиками.\n\n```typescript\nimport { createProvider, openAICompletionsApi } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(createProvider({\n    id: \"native-local\",\n    name: \"Native Local\",\n    baseUrl: \"http://localhost:8080/v1\",\n    auth: {\n      apiKey: {\n        name: \"Local server API key\",\n        async login(interaction) {\n          return {\n            type: \"api_key\",\n            key: await interaction.prompt({ type: \"secret\", message: \"API key\" })\n          };\n        },\n        async resolve({ credential }) {\n          return credential?.key\n            ? { auth: { apiKey: credential.key }, source: \"stored API key\" }\n            : undefined;\n        }\n      }\n    },\n    models: [],\n    api: openAICompletionsApi()\n  }));\n\n  // Legacy provider-config form:\n  // Override baseUrl for existing provider\n  pi.registerProvider(\"anthropic\", {\n    baseUrl: \"https://proxy.example.com\"\n  });\n\n  // Register new provider with models\n  pi.registerProvider(\"my-provider\", {\n    name: \"My Provider\",\n    baseUrl: \"https://api.example.com\",\n    apiKey: \"$MY_API_KEY\",\n    api: \"openai-completions\",\n    models: [\n      {\n        id: \"my-model\",\n        name: \"My Model\",\n        reasoning: false,\n        input: [\"text\", \"image\"],\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n        contextWindow: 128000,\n        maxTokens: 4096\n      }\n    ]\n  });\n}\n```\n\nФабрикой расширений также может быть `async`. Для динамического обнаружения моделей извлекайте и регистрируйте модели на фабрике вместо `session_start`. pi ожидает фабрику перед продолжением запуска, поэтому поставщик доступен во время интерактивного запуска и до `pi --list-models`.\n\n## Переопределить существующего поставщика\n\nСамый простой вариант использования: перенаправить существующего провайдера через прокси.\n\n```typescript\n// All Anthropic requests now go through your proxy\npi.registerProvider(\"anthropic\", {\n  baseUrl: \"https://proxy.example.com\"\n});\n\n// Add custom headers to OpenAI requests\npi.registerProvider(\"openai\", {\n  headers: {\n    \"X-Custom-Header\": \"value\"\n  }\n});\n\n// Both baseUrl and headers\npi.registerProvider(\"google\", {\n  baseUrl: \"https://ai-gateway.corp.com/google\",\n  headers: {\n    \"X-Corp-Auth\": \"$CORP_AUTH_TOKEN\"  // env var or literal\n  }\n});\n```\n\nЕсли указаны только `baseUrl` и/или `headers` (нет `models`), все существующие модели для этого поставщика сохраняются с новой конечной точкой.\n\n## Зарегистрировать нового провайдера\n\nЧтобы добавить совершенно нового провайдера, укажите `models` вместе с необходимой конфигурацией.\n\nЕсли список моделей поступает из удаленной конечной точки, используйте фабрику асинхронных расширений:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default async function (pi: ExtensionAPI) {\n  const response = await fetch(\"http://localhost:1234/v1/models\");\n  const payload = (await response.json()) as {\n    data: Array<{\n      id: string;\n      name?: string;\n      context_window?: number;\n      max_tokens?: number;\n    }>;\n  };\n\n  pi.registerProvider(\"local-openai\", {\n    baseUrl: \"http://localhost:1234/v1\",\n    apiKey: \"$LOCAL_OPENAI_API_KEY\",\n    api: \"openai-completions\",\n    models: payload.data.map((model) => ({\n      id: model.id,\n      name: model.name ?? model.id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: model.context_window ?? 128000,\n      maxTokens: model.max_tokens ?? 4096,\n    })),\n  });\n}\n```\n\nЭто регистрирует полученные модели до завершения запуска.\n\n```typescript\npi.registerProvider(\"my-llm\", {\n  baseUrl: \"https://api.my-llm.com/v1\",\n  apiKey: \"$MY_LLM_API_KEY\",  // env var reference\n  api: \"openai-completions\",  // which streaming API to use\n  models: [\n    {\n      id: \"my-llm-large\",\n      name: \"My LLM Large\",\n      reasoning: true,        // supports extended thinking\n      input: [\"text\", \"image\"],\n      cost: {\n        input: 3.0,           // $/million tokens\n        output: 15.0,\n        cacheRead: 0.3,\n        cacheWrite: 3.75\n      },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n```\n\nЕсли указан `models`, он **заменяет** все существующие модели для этого поставщика.\n\n`apiKey` и значения пользовательского заголовка используют тот же синтаксис значений конфигурации, что и `models.json`: `!command` в начале выполняет команду для всего значения, `$ENV_VAR` и `${ENV_VAR}` интерполируют переменные среды, `$` выдает литерал ``apiKey` и значения пользовательского заголовка используют тот же синтаксис значений конфигурации, что и `models.json`: `!command` в начале выполняет команду для всего значения, `$ENV_VAR` и `${ENV_VAR}` интерполируют переменные среды, `$` выдает литерал  и `$!` выдает литерал `!`.\n\n## Отменить регистрацию поставщика\n\nИспользуйте `pi.unregisterProvider(name)`, чтобы удалить провайдера, который ранее был зарегистрирован через `pi.registerProvider(name,...)`:\n\n```typescript\n// Register\npi.registerProvider(\"my-llm\", {\n  baseUrl: \"https://api.my-llm.com/v1\",\n  apiKey: \"$MY_LLM_API_KEY\",\n  api: \"openai-completions\",\n  models: [\n    {\n      id: \"my-llm-large\",\n      name: \"My LLM Large\",\n      reasoning: true,\n      input: [\"text\", \"image\"],\n      cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n\n// Later, remove it\npi.unregisterProvider(\"my-llm\");\n```\n\nПри отмене регистрации удаляются динамические модели этого поставщика, резервный вариант API key, регистрация поставщика OAuth и регистрации пользовательского обработчика потока. Любые встроенные модели или поведение поставщика, которые были переопределены, восстанавливаются.\n\nВызовы, сделанные после начальной фазы загрузки расширения, применяются немедленно, поэтому `/reload` не требуется.\n\n### API Типы\n\nПоле `api` определяет, какая реализация потоковой передачи используется:\n\n| API | Используйте для |\n|-----|---------|\n| `anthropic-messages` | Антропный Клод API и совместимые |\n| `openai-completions` | Завершения чата OpenAI API и совместимые |\n| `openai-responses` | Ответы OpenAI API |\n| `azure-openai-responses` | Ответы Azure OpenAI API |\n| `openai-codex-responses` | Ответы Кодекса OpenAI API |\n| `mistral-conversations` | Трансляция нативных завершений чата Mistral |\n| `google-generative-ai` | Генеративный искусственный интеллект Google API |\n| `google-vertex` | Google Вертекс ИИ API |\n| `bedrock-converse-stream` | Конверсы Amazon Bedrock API |\n\nБольшинство OpenAI-совместимых провайдеров работают с `openai-completions`. Используйте уровень модели `thinkingLevelMap` для уровней мышления, специфичных для модели, и `compat` для особенностей поставщика. Уровни `xhigh` и `max` являются добровольными, требуют ненулевых записей карты и могут быть разделены неподдерживаемыми пробелами:\n\n```typescript\nmodels: [{\n  id: \"custom-model\",\n  // ...\n  reasoning: true,\n  thinkingLevelMap: {              // map pi levels to provider values; null hides unsupported levels\n    minimal: null,\n    low: null,\n    medium: null,\n    high: \"default\",\n    xhigh: null,\n    max: \"max\"\n  },\n  compat: {\n    supportsDeveloperRole: false,   // use \"system\" instead of \"developer\"\n    supportsReasoningEffort: true,\n    maxTokensField: \"max_tokens\",   // instead of \"max_completion_tokens\"\n    requiresToolResultName: true,   // tool results need name field\n    thinkingFormat: \"qwen\",        // top-level enable_thinking: true\n    cacheControlFormat: \"anthropic\" // Anthropic-style cache_control markers\n  }\n}]\n```\n\nИспользуйте `openrouter` для элементов управления `reasoning: { effort }` в стиле OpenRouter. Используйте `together` для элементов управления `reasoning: { enabled }` в стиле Together; с `supportsReasoningEffort` он также отправляет `reasoning_effort`. Используйте `qwen-chat-template` для локальных Qwen-совместимых серверов, которые читают `chat_template_kwargs.enable_thinking` и нуждаются в `preserve_thinking`.\nИспользуйте `cacheControlFormat: \"anthropic\"` для поставщиков, совместимых с OpenAI, которые предоставляют кэширование подсказок в стиле Anthropic через `cache_control` в системном приглашении, последнем определении инструмента и последнем текстовом содержимом пользователя, помощника или результата инструмента.\n\nДля антропосовместимых поставщиков, использующих `api: \"anthropic-messages\"`, установите `compat.forceAdaptiveThinking: true` для моделей или поставщиков, чья восходящая модель требует адаптивного мышления (`thinking.type: \"adaptive\"` плюс `output_config.effort`). Встроенные адаптивные модели Клода устанавливают это автоматически. Установите `compat.allowEmptySignature: true` только для провайдеров, которые излучают пустые мыслительные сигнатуры и ожидают `signature: \"\"` при воспроизведении.\n\n> Примечание по миграции: Мистраль перемещен с `openai-completions` на `mistral-conversations`.\n> Используйте `mistral-conversations` для родных моделей Mistral.\n> Если вы намеренно маршрутизируете совместимые с Mistral/пользовательские конечные точки через `openai-completions`, при необходимости явно установите флаги `compat`.\n\n### Заголовок аутентификации\n\nЕсли ваш провайдер ожидает `Authorization: Bearer <key>`, но не использует стандартный API, установите `authHeader: true`:\n\n```typescript\npi.registerProvider(\"custom-api\", {\n  baseUrl: \"https://api.example.com\",\n  apiKey: \"$MY_API_KEY\",\n  authHeader: true,  // adds Authorization: Bearer header\n  api: \"openai-completions\",\n  models: [...]\n});\n```\n\nКлюч разрешается для каждого запроса. Явный заголовок запроса `Authorization` имеет приоритет над сгенерированным значением.\n\n## OAuth Поддержка\n\nДобавьте аутентификацию OAuth/SSO, которая интегрируется с `/login`:\n\n```typescript\nimport type { OAuthCredentials, OAuthLoginCallbacks } from \"@earendil-works/pi-ai\";\n\npi.registerProvider(\"corporate-ai\", {\n  baseUrl: \"https://ai.corp.com/v1\",\n  api: \"openai-responses\",\n  models: [...],\n  oauth: {\n    name: \"Corporate AI (SSO)\",\n\n    async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n      const method = await callbacks.onSelect({\n        message: \"Select login method:\",\n        options: [\n          { id: \"browser\", label: \"Browser OAuth\" },\n          { id: \"device\", label: \"Device code\" }\n        ]\n      });\n      if (!method) throw new Error(\"Login cancelled\");\n\n      let code: string;\n      if (method === \"device\") {\n        callbacks.onDeviceCode({\n          userCode: \"ABCD-1234\",\n          verificationUri: \"https://sso.corp.com/device\",\n          intervalSeconds: 5,\n          expiresInSeconds: 900\n        });\n        code = await pollDeviceCodeUntilComplete();\n      } else {\n        callbacks.onAuth({ url: \"https://sso.corp.com/authorize?...\" });\n        code = await callbacks.onPrompt({ message: \"Enter SSO code:\" });\n      }\n\n      // Exchange for tokens (your implementation)\n      const tokens = await exchangeCodeForTokens(code);\n\n      return {\n        refresh: tokens.refreshToken,\n        access: tokens.accessToken,\n        expires: Date.now() + tokens.expiresIn * 1000\n      };\n    },\n\n    async refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials> {\n      const tokens = await refreshAccessToken(credentials.refresh, signal);\n      return {\n        refresh: tokens.refreshToken ?? credentials.refresh,\n        access: tokens.accessToken,\n        expires: Date.now() + tokens.expiresIn * 1000\n      };\n    },\n\n    getApiKey(credentials: OAuthCredentials): string {\n      return credentials.access;\n    }\n  }\n});\n```\n\nПосле регистрации пользователи могут пройти аутентификацию через `/login corporate-ai`.\n\n### OAuthОбратные вызовы для входа в систему\n\nОбъект `callbacks` обеспечивает нейтральное к пользовательскому интерфейсу взаимодействие для потока, принадлежащего провайдеру:\n\n```typescript\ninterface OAuthLoginCallbacks {\n  // Open URL in browser (for OAuth redirects)\n  onAuth(params: { url: string }): void;\n\n  // Show device code (for device authorization flow)\n  onDeviceCode(params: {\n    userCode: string;\n    verificationUri: string;\n    intervalSeconds?: number;\n    expiresInSeconds?: number;\n  }): void;\n\n  // Show transient progress\n  onProgress?(message: string): void;\n\n  // Prompt user for input (for manual token entry)\n  onPrompt(params: { message: string }): Promise<string>;\n\n  // Show an interactive selector, e.g. to choose browser OAuth vs device code\n  onSelect(params: {\n    message: string;\n    options: { id: string; label: string }[];\n  }): Promise<string | undefined>;\n}\n```\n\n### OAuthУчетные данные\n\nУчетные данные сохраняются в `~/.pi/agent/auth.json`:\n\n```typescript\ninterface OAuthCredentials {\n  refresh: string;   // Refresh token (for refreshToken())\n  access: string;    // Access token (returned by getApiKey())\n  expires: number;   // Expiration timestamp in milliseconds\n}\n```\n\n## Пользовательская потоковая передача API\n\nДля провайдеров с нестандартными API внедрите `streamSimple`. Прежде чем писать свою собственную, изучите существующие реализации провайдера:\n\n**Эталонные реализации:**\n- [anthropic.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts) - Антропные сообщения API\n- [mistral.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/mistral.ts) - Мистральские разговоры API\n- [openai-completions.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-completions.ts) — Завершения чата OpenAI\n- [openai-responses.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-responses.ts) - Ответы OpenAI API\n- [google.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/google.ts) — Генеративный искусственный интеллект Google\n- [amazon-bedrock.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/amazon-bedrock.ts) — Основа AWS\n\n### Шаблон потока\n\nВсе провайдеры следуют одной и той же схеме:\n\n```typescript\nimport {\n  type AssistantMessage,\n  type AssistantMessageEventStream,\n  type Context,\n  type Model,\n  type SimpleStreamOptions,\n  calculateCost,\n  createAssistantMessageEventStream,\n} from \"@earendil-works/pi-ai\";\n\nfunction streamMyProvider(\n  model: Model<any>,\n  context: Context,\n  options?: SimpleStreamOptions\n): AssistantMessageEventStream {\n  const stream = createAssistantMessageEventStream();\n\n  (async () => {\n    // Initialize output message\n    const output: AssistantMessage = {\n      role: \"assistant\",\n      content: [],\n      api: model.api,\n      provider: model.provider,\n      model: model.id,\n      usage: {\n        input: 0,\n        output: 0,\n        cacheRead: 0,\n        cacheWrite: 0,\n        totalTokens: 0,\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n      },\n      stopReason: \"pending\",\n      timestamp: Date.now(),\n    };\n\n    try {\n      // Push start event\n      stream.push({ type: \"start\", partial: output });\n\n      // Make API request and process response...\n      // Push content events as they arrive and set stopReason from the terminal event.\n      if (output.stopReason === \"pending\") {\n        throw new Error(\"Provider stream ended without a stop reason\");\n      }\n      if (output.stopReason === \"error\" || output.stopReason === \"aborted\") {\n        throw new Error(output.errorMessage || \"An unknown error occurred\");\n      }\n\n      // Push done event\n      stream.push({\n        type: \"done\",\n        reason: output.stopReason,\n        message: output\n      });\n      stream.end();\n    } catch (error) {\n      output.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n      output.errorMessage = error instanceof Error ? error.message : String(error);\n      stream.push({ type: \"error\", reason: output.stopReason, error: output });\n      stream.end();\n    }\n  })();\n\n  return stream;\n}\n```\n\n### Типы событий\n\nОтправьте события через `stream.push()` в следующем порядке:\n\n1. `{ type: \"start\", partial: output }` — трансляция началась.\n\n2. События контента (повторяемые, трек `contentIndex` для каждого блока):\n   - `{ type: \"text_start\", contentIndex, partial }` — текстовый блок запущен.\n   - `{ type: \"text_delta\", contentIndex, delta, partial }` — текстовый фрагмент\n   - `{ type: \"text_end\", contentIndex, content, partial }` — текстовый блок завершен.\n   - `{ type: \"thinking_start\", contentIndex, partial }` — Начал думать\n   - `{ type: \"thinking_delta\", contentIndex, delta, partial }` — Мыслящий фрагмент\n   - `{ type: \"thinking_end\", contentIndex, content, partial }` — Раздумья закончились\n   - `{ type: \"toolcall_start\", contentIndex, partial }` — Начался вызов инструмента.\n   - `{ type: \"toolcall_delta\", contentIndex, delta, partial }` — вызов инструмента JSON чанк\n   - `{ type: \"toolcall_end\", contentIndex, toolCall, partial }` — вызов инструмента завершен.\n\n3. `{ type: \"done\", reason, message }` или `{ type: \"error\", reason, error }` — трансляция завершена.\n\nПоле `partial` в каждом событии содержит текущее состояние `AssistantMessage`. Обновите `output.content` по мере получения данных, затем включите `output` в качестве `partial`.\n\n### Блоки контента\n\nДобавляйте блоки контента в `output.content` по мере их поступления:\n\n```typescript\n// Text block\noutput.content.push({ type: \"text\", text: \"\" });\nstream.push({ type: \"text_start\", contentIndex: output.content.length - 1, partial: output });\n\n// As text arrives\nconst block = output.content[contentIndex];\nif (block.type === \"text\") {\n  block.text += delta;\n  stream.push({ type: \"text_delta\", contentIndex, delta, partial: output });\n}\n\n// When block completes\nstream.push({ type: \"text_end\", contentIndex, content: block.text, partial: output });\n```\n\n### Вызовы инструментов\n\nВызовы инструментов требуют накопления JSON и анализа:\n\n```typescript\n// Start tool call\noutput.content.push({\n  type: \"toolCall\",\n  id: toolCallId,\n  name: toolName,\n  arguments: {}\n});\nstream.push({ type: \"toolcall_start\", contentIndex: output.content.length - 1, partial: output });\n\n// Accumulate JSON\nlet partialJson = \"\";\npartialJson += jsonDelta;\ntry {\n  block.arguments = JSON.parse(partialJson);\n} catch {}\nstream.push({ type: \"toolcall_delta\", contentIndex, delta: jsonDelta, partial: output });\n\n// Complete\nstream.push({\n  type: \"toolcall_end\",\n  contentIndex,\n  toolCall: { type: \"toolCall\", id, name, arguments: block.arguments },\n  partial: output\n});\n```\n\n### Использование и стоимость\n\nОбновите использование из ответа API и рассчитайте стоимость:\n\n```typescript\noutput.usage.input = response.usage.input_tokens;\noutput.usage.output = response.usage.output_tokens;\noutput.usage.cacheRead = response.usage.cache_read_tokens ?? 0;\noutput.usage.cacheWrite = response.usage.cache_write_tokens ?? 0;\noutput.usage.totalTokens = output.usage.input + output.usage.output +\n                           output.usage.cacheRead + output.usage.cacheWrite;\ncalculateCost(model, output.usage);\n```\n\n### Ошибки переполнения контекста\n\nКогда запрос превышает контекстное окно модели, pi может автоматически восстановиться, сжимая диалог и повторяя попытку. Это восстановление вступает в силу только в том случае, если pi распознает сбой как переполнение.\n\nОбнаружение выполняется на основе окончательного сообщения помощника:\n\n- `stopReason === \"error\"`\n- `errorMessage` соответствует одному из известных шаблонов переполнения числа pi (см. [`packages/ai/src/utils/overflow.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/utils/overflow.ts))\n\nЕсли ваш провайдер возвращает ошибки переполнения с сообщением, которое pi не распознает, нормализуйте ошибку из того же расширения, которое регистрирует провайдера. Используйте обработчик `message_end`, чтобы переписать сообщение помощника так, чтобы его `errorMessage` начиналось с фразы, которую распознает pi. Общий запасной вариант `context_length_exceeded` — самый безопасный выбор.\n\n```typescript\nconst MY_PROVIDER_OVERFLOW_PATTERN = /your provider's overflow phrase/i;\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(\"my-provider\", { /* ... */ });\n\n  pi.on(\"message_end\", (event, ctx) => {\n    const message = event.message;\n    if (message.role !== \"assistant\") return;\n    if (message.stopReason !== \"error\") return;\n    if (\n      message.provider !== \"my-provider\" &&\n      ctx.model?.provider !== \"my-provider\"\n    )\n      return;\n\n    const errorMessage = message.errorMessage ?? \"\";\n    if (errorMessage.includes(\"context_length_exceeded\")) return;\n    if (!MY_PROVIDER_OVERFLOW_PATTERN.test(errorMessage)) return;\n\n    return {\n      message: {\n        ...message,\n        errorMessage: `context_length_exceeded: ${errorMessage}`,\n      },\n    };\n  });\n}\n```\n\n`message_end` запускается до того, как pi отслеживает сообщение помощника для автоматического сжатия, поэтому pi проверяет переписанный `errorMessage`. При этом число pi будет:\n\n1. Обнаружить переполнение от `errorMessage`.\n2. Удалите сообщение о сбое помощника из живого контекста.\n3. Запустите уплотнение.\n4. Повторите запрос один раз.\n\nТщательно охраняйте переписывание:\n\n- Присвойте его своему провайдеру (`message.provider` и `ctx.model?.provider`), чтобы несвязанные ошибки других провайдеров не были затронуты.\n- Сопоставьте шаблон, специфичный для поставщика, а не общие шаблоны переполнения pi. Перезапись ошибок ограничения скорости или регулирования (`rate limit`, `too many requests`) будет ложно запускать уплотнение вместо обычного пути повторной попытки с откатом в pi.\n- Пропустить, если `errorMessage` уже включает в себя `context_length_exceeded`, поэтому обработчик является идемпотентным.\n\n### Регистрация\n\nЗарегистрируйте свою потоковую функцию:\n\n```typescript\npi.registerProvider(\"my-provider\", {\n  baseUrl: \"https://api.example.com\",\n  apiKey: \"$MY_API_KEY\",\n  api: \"my-custom-api\",\n  models: [...],\n  streamSimple: streamMyProvider\n});\n```\n\n## Тестирование вашей реализации\n\nПроверьте своего провайдера с помощью тех же наборов тестов, которые используются встроенными провайдерами. Скопируйте и адаптируйте эти тестовые файлы из [packages/ai/test/](https://github.com/earendil-works/pi-mono/tree/main/packages/ai/test):\n\n| Тест | Цель |\n|------|---------|\n| `stream.test.ts` | Базовая потоковая передача, текстовый вывод |\n| `tokens.test.ts` | Подсчет и использование токенов |\n| `abort.test.ts` | Обработка сигнала прерывания |\n| `empty.test.ts` | Пустые/минимальные ответы |\n| `context-overflow.test.ts` | Ограничения контекстного окна |\n| `image-limits.test.ts` | Обработка ввода изображений |\n| `unicode-surrogate.test.ts` | Краевые случаи Юникода |\n| `tool-call-without-result.test.ts` | Краевые случаи вызова инструмента |\n| `image-tool-result.test.ts` | Изображения в результатах инструмента |\n| `total-tokens.test.ts` | Общий расчет токенов |\n| `cross-provider-handoff.test.ts` | Передача контекста между провайдерами |\n\nЗапустите тесты с парами поставщик/модель, чтобы проверить совместимость.\n\n## Справочник по конфигурации\n\n```typescript\ninterface ProviderConfig {\n  /** Display name for the provider in UI such as /login. */\n  name?: string;\n\n  /** API endpoint URL. Required when defining models. */\n  baseUrl?: string;\n\n  /** API key literal, env interpolation ($ENV_VAR or ${ENV_VAR}), or !command. Required when defining models (unless oauth). */\n  apiKey?: string;\n\n  /** API type for streaming. Required at provider or model level when defining models. */\n  api?: Api;\n\n  /** Custom streaming implementation for non-standard APIs. */\n  streamSimple?: (\n    model: Model<Api>,\n    context: Context,\n    options?: SimpleStreamOptions\n  ) => AssistantMessageEventStream;\n\n  /** Custom headers to include in requests. Values use the same resolution syntax as apiKey. */\n  headers?: Record<string, string>;\n\n  /** If true, adds Authorization: Bearer header with the resolved API key. */\n  authHeader?: boolean;\n\n  /** Models to register. If provided, replaces all existing models for this provider. */\n  models?: ProviderModelConfig[];\n\n  /** OAuth provider for /login support. */\n  oauth?: {\n    name: string;\n    login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;\n    refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials>;\n    getApiKey(credentials: OAuthCredentials): string;\n  };\n}\n```\n\n## Справочник по определению модели\n\n```typescript\ninterface ProviderModelConfig {\n  /** Model ID (e.g., \"claude-sonnet-4-20250514\"). */\n  id: string;\n\n  /** Display name (e.g., \"Claude 4 Sonnet\"). */\n  name: string;\n\n  /** API type override for this specific model. */\n  api?: Api;\n\n  /** API endpoint URL override for this specific model. */\n  baseUrl?: string;\n\n  /** Whether the model supports extended thinking. */\n  reasoning: boolean;\n\n  /** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */\n  thinkingLevelMap?: Partial<Record<\"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\", string | null>>;\n\n  /** Supported input types. */\n  input: (\"text\" | \"image\")[];\n\n  /** Cost per million tokens (for usage tracking). */\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n  };\n\n  /** Maximum context window size in tokens. */\n  contextWindow: number;\n\n  /** Maximum output tokens. */\n  maxTokens: number;\n\n  /** Custom headers for this specific model. */\n  headers?: Record<string, string>;\n\n  /** Compatibility settings for the selected API. */\n  compat?: {\n    // openai-completions\n    supportsStore?: boolean;\n    supportsDeveloperRole?: boolean;\n    supportsReasoningEffort?: boolean;\n    supportsUsageInStreaming?: boolean;\n    supportsFinishReason?: boolean;\n    supportsStrictMode?: boolean;\n    supportsOpenAIGrammarTools?: boolean; // openai-completions/openai-responses; false falls back to normal function tools\n    maxTokensField?: \"max_completion_tokens\" | \"max_tokens\";\n    requiresToolResultName?: boolean;\n    requiresAssistantAfterToolResult?: boolean;\n    requiresThinkingAsText?: boolean;\n    requiresReasoningContentOnAssistantMessages?: boolean;\n    thinkingFormat?: \"openai\" | \"openrouter\" | \"deepseek\" | \"together\" | \"baseten\" | \"zai\" | \"qwen\" | \"chat-template\" | \"qwen-chat-template\" | \"string-thinking\" | \"ant-ling\";\n    chatTemplateKwargs?: Record<string, string | number | boolean | null | { \"$var\": \"thinking.enabled\" | \"thinking.effort\"; omitWhenOff?: boolean }>;\n    chatTemplateArgs?: Record<string, string | number | boolean | null | { \"$var\": \"thinking.enabled\" | \"thinking.effort\"; omitWhenOff?: boolean }>;\n    cacheControlFormat?: \"anthropic\";\n    sessionAffinityFormat?: \"openai\" | \"openai-nosession\" | \"openrouter\";\n    sendSessionAffinityHeaders?: boolean;\n\n    // anthropic-messages\n    supportsEagerToolInputStreaming?: boolean;\n    supportsLongCacheRetention?: boolean;\n    sendSessionAffinityHeaders?: boolean;\n    supportsCacheControlOnTools?: boolean;\n    forceAdaptiveThinking?: boolean;\n    allowEmptySignature?: boolean;\n    supportsStrictTools?: boolean;\n  };\n}\n```\n\n`openrouter` отправляет `reasoning: { effort }`. `deepseek` отправляет `thinking: { type: \"enabled\" | \"disabled\" }` и `reasoning_effort`, когда включено. `together` отправляет `reasoning: { enabled }`, а также `reasoning_effort`, когда `supportsReasoningEffort` включено. `qwen` соответствует верхнему уровню `enable_thinking` в стиле DashScope. Используйте `qwen-chat-template` для локальных Qwen-совместимых серверов, которые читают `chat_template_kwargs.enable_thinking` и нуждаются в `preserve_thinking`. Используйте `chat-template` для настраиваемого `chat_template_kwargs`, например DeepSeek V3.x за vLLM с `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }`. Используйте `thinkingFormat: \"baseten\"` с `chatTemplateArgs`, если поставщик ожидает переключения значений ниже `chat_template_args` и при необходимости поддерживает `reasoning_effort` верхнего уровня.\n`cacheControlFormat: \"anthropic\"` применяет маркеры `cache_control` в стиле Anthropic к системному приглашению, последнему определению инструмента и последнему текстовому содержимому пользователя, помощника или результата инструмента.","sourceFile":"custom-provider.md"},"development":{"title":"Разработка","markdown":"Дополнительные рекомендации см. [AGENTS.md](https://github.com/earendil-works/pi-mono/blob/main/AGENTS.md).\n\n## Настраивать\n\n```bash\ngit clone https://github.com/earendil-works/pi-mono\ncd pi-mono\nnpm install\nnpm run build\n```\n\nЗапустить из источника:\n\n```bash\n/path/to/pi-mono/pi-test.sh\n```\n\nСкрипт можно запустить из любого каталога. Pi сохраняет текущий рабочий каталог вызывающего абонента.\n\n## Форк/Ребрендинг\n\nНастройте через `package.json`:\n\n```json\n{\n  \"piConfig\": {\n    \"name\": \"pi\",\n    \"configDir\": \".pi\"\n  }\n}\n```\n\nИзмените поля `name`, `configDir` и `bin` для вашей вилки. Влияет на баннер CLI, пути конфигурации и имена переменных среды.\n\n## Разрешение пути\n\nТри режима выполнения: npm установка, автономный двоичный файл, tsx из исходного кода.\n\n**Всегда используйте `src/config.ts`** для ресурсов пакета:\n\n```typescript\nimport { getPackageDir, getThemeDir } from \"./config.js\";\n```\n\nНикогда не используйте `__dirname` непосредственно для ресурсов пакета.\n\n## Команда отладки\n\n`/debug` (скрытый) записывает в `~/.pi/agent/pi-debug.log`:\n- Отрисовано TUI строк с кодами ANSI.\n- Последние сообщения, отправленные в LLM\n\n## Тестирование\n\n```bash\n./test.sh                         # Run non-LLM tests (no API keys needed)\nnpm test                          # Run all tests\nnpm test -- test/specific.test.ts # Run specific test\n```\n\n## Структура проекта\n\n```\npackages/\n  ai/           # LLM provider abstraction\n  agent/        # Agent loop and message types  \n  tui/          # Terminal UI components\n  coding-agent/ # CLI and interactive mode\n```","sourceFile":"development.md"},"environment-variables":{"title":"Переменные среды","markdown":"Pi использует переменные среды тремя способами:\n\n- Такие переменные, как `PI_OFFLINE`, настраивают процесс Pi.\n- Pi устанавливает `PI_CODING_AGENT`, чтобы дочерние процессы могли обнаружить, что они выполняются внутри Pi.\n- Команды, выполняемые инструментом bash, вызываемым LLM, получают переменные `PI_*`, описывающие текущий сеанс.\n\nПеременные ключа API поставщика документируются отдельно в [Providers](providers.md#environment-variables-or-auth-file).\n\n## Маркер процесса\n\nТочки входа CLI и RPC устанавливают `PI_CODING_AGENT=true`. Дочерние процессы наследуют его и могут использовать для обнаружения того, что они выполняются внутри Pi. Он не зависит от сеанса и не устанавливается автоматически, когда Pi встроен через SDK.\n\n## Среда сеанса Bash Tool\n\nКоманды, выполняемые инструментом bash, получают текущее состояние сеанса Pi:\n\n| Переменная | Описание |\n|----------|-------------|\n| `PI_SESSION_ID` | Идентификатор текущего сеанса |\n| `PI_SESSION_FILE` | Абсолютный путь к файлу текущего сеанса JSONL; не настроен для эфемерных сеансов |\n| `PI_PROVIDER` | Текущий выбранный поставщик модели |\n| `PI_MODEL` | Текущий выбранный идентификатор модели |\n| `PI_REASONING_LEVEL` | Текущий эффективный уровень рассуждения: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` или `max`. |\n\nЗначения определяются при запуске каждой команды. Таким образом, переключение моделей или изменение уровня рассуждения влияет на следующую команду bash без перезапуска Pi. `PI_PROVIDER` и `PI_MODEL` идентифицируют выбранную модель Pi, а не другую вышестоящую модель, которую маршрутизатор может выбрать внутри себя.\n\nКогда вас спросят, какая модель или поставщик работает, проверьте эти переменные вместо того, чтобы делать вывод из системного приглашения:\n\n```bash\nprintf '%s/%s\\n' \"$PI_PROVIDER\" \"$PI_MODEL\"\nprintf 'reasoning=%s session=%s\\n' \"$PI_REASONING_LEVEL\" \"$PI_SESSION_ID\"\n```\n\nФайл сеанса можно проверить напрямую, если сеанс является постоянным:\n\n```bash\nif [ -n \"$PI_SESSION_FILE\" ]; then\n  tail -n 1 \"$PI_SESSION_FILE\"\nfi\n```\n\nЭти переменные вводятся в инструмент bash, вызываемый LLM. Они не вводятся в вводимые пользователем команды `!` или `!!`.\n\n### Пользовательские инструменты Bash\n\nИнструменты Bash, созданные с помощью `createBashTool()`, по умолчанию предоставляют среду сеанса при регистрации с помощью Pi. Внедрение происходит до `spawnHook`, поэтому перехватчик получает переменные из `ctx.env`:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  spawnHook: (ctx) => ({\n    ...ctx,\n    env: { ...ctx.env, CI: \"1\" },\n  }),\n});\n```\n\nОтключите метаданные сеанса независимо от спавна:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n  spawnHook: (ctx) => ctx,\n});\n```\n\nЕсли отключено, Pi удаляет унаследованные значения для этих переменных, поэтому вложенные процессы Pi не предоставляют устаревшие метаданные родительского сеанса.\n\n## Pi Конфигурация процесса\n\nЭти переменные читаются самим Pi:\n\n| Переменная | Описание |\n|----------|-------------|\n| `PI_CODING_AGENT_DIR` | Переопределить каталог конфигурации; по умолчанию `~/.pi/agent` |\n| `PI_CODING_AGENT_SESSION_DIR` | Переопределить хранилище сеансов; переопределено `--session-dir` |\n| `PI_PACKAGE_DIR` | Переопределить каталог пакета, что полезно для путей к хранилищу Nix/Guix. |\n| `PI_OFFLINE` | Отключите сетевые операции при запуске, включая проверки обновлений, обновления пакетов и установку/обновление телеметрии. |\n| `PI_SKIP_VERSION_CHECK` | Отключить запрос последней версии `pi.dev`. |\n| `PI_TELEMETRY` | Переопределить заголовки телеметрии установки/обновления и атрибуции поставщика: `1`/`true`/`yes` или `0`/`false`/`no` |\n| `PI_CACHE_RETENTION` | Установите значение `long` для расширенного кэширования приглашений поставщика, если это поддерживается. |\n| `PI_SHARE_VIEWER_URL` | Переопределить базовый URL-адрес, используемый `/share` |\n| `PI_HARDWARE_CURSOR` | Установите значение `1`, чтобы отобразить аппаратный курсор; см. [Terminal setup](terminal-setup.md) |\n| `VISUAL`, `EDITOR` | Резервный вариант внешнего редактора, если `externalEditor` не установлено |\n| `HTTP_PROXY`, `HTTPS_PROXY` | Исходящие HTTP-запросы прокси-сервера |\n\nУчетные данные поставщика, такие как `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, а также конфигурация облачного провайдера, перечислены в [Providers](providers.md#environment-variables-or-auth-file).","sourceFile":"environment-variables.md"},"extensions":{"title":"Extensions","markdown":"> pi может создавать расширения. Попросите его создать его для вашего варианта использования.\n\n\nExtensions — это модули TypeScript, расширяющие поведение числа pi. Они могут подписываться на события жизненного цикла, регистрировать специальные инструменты, вызываемые LLM, добавлять команды и многое другое.\n\n> **Размещение /reload:** Поместите расширения в `~/.pi/agent/extensions/` (глобальный) или `.pi/extensions/` (локальный для проекта) для автоматического обнаружения. Используйте `pi -e./path.ts` только для быстрых тестов. Extensions в автоматически обнаруженных локациях можно перезагрузить с помощью `/reload`.\n\n**Основные возможности:**\n- **Пользовательские инструменты** – зарегистрируйте инструменты, которые LLM может вызывать через `pi.registerTool()`.\n- **Перехват событий** – блокируйте или изменяйте вызовы инструментов, внедряйте контекст, настраивайте сжатие.\n- **Взаимодействие с пользователем** – подсказки пользователям с помощью `ctx.ui` (выберите, подтвердите, введите, уведомите).\n- **Пользовательские компоненты пользовательского интерфейса** — полные компоненты TUI с вводом с клавиатуры через `ctx.ui.custom()` для сложных взаимодействий.\n- **Пользовательские команды** — регистрируйте такие команды, как `/mycommand` через `pi.registerCommand()`.\n- **Постоянство сеанса** – состояние хранилища, которое сохраняется при перезапуске через `pi.appendEntry()`.\n- **Пользовательский рендеринг**. Управляйте тем, как вызовы инструментов/результаты и сообщения отображаются в TUI.\n\n**Примеры использования:**\n- Разрешительные ворота (подтвердите до `rm -rf`, `sudo` и т. д.)\n- Git контрольная точка (тайник на каждом ходу, восстановление на ветке)\n- Защита пути (блокировка записи в `.env`, `node_modules/`)\n- Пользовательское сжатие (подведите итог разговора по-своему)\n- Сводки разговоров (см. пример `summarize.ts`)\n- Интерактивные инструменты (вопросы, мастера, настраиваемые диалоги)\n- Инструменты с отслеживанием состояния (списки дел, пулы соединений)\n- Внешние интеграции (наблюдатели файлов, веб-перехватчики, триггеры CI)\n- Игры, пока вы ждете (см. пример `snake.ts`)\n\nСм. [examples/extensions/](../examples/extensions/) для рабочих реализаций.\n\n## Оглавление\n\n- [Quick Start](#quick-start)\n- [Extension Locations](#extension-locations)\n- [Available Imports](#available-imports)\n- [Writing an Extension](#writing-an-extension)\n  - [Extension Styles](#extension-styles)\n- [Events](#events)\n  - [Lifecycle Overview](#lifecycle-overview)\n  - [Resource Events](#resource-events)\n  - [Session Events](#session-events)\n  - [Agent Events](#agent-events)\n  - [Model Events](#model-events)\n  - [Tool Events](#tool-events)\n- [ExtensionContext](#extensioncontext)\n- [ExtensionCommandContext](#extensioncommandcontext)\n- [ExtensionAPI Methods](#extensionapi-methods)\n- [State Management](#state-management)\n- [Custom Tools](#custom-tools)\n  - [Dynamic Tool Loading](#dynamic-tool-loading)\n- [Custom UI](#custom-ui)\n- [Error Handling](#error-handling)\n- [Mode Behavior](#mode-behavior)\n- [Examples Reference](#examples-reference)\n\n## Быстрый старт\n\nСоздайте `~/.pi/agent/extensions/my-extension.ts`:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nexport default function (pi: ExtensionAPI) {\n  // React to events\n  pi.on(\"session_start\", async (_event, ctx) => {\n    ctx.ui.notify(\"Extension loaded!\", \"info\");\n  });\n\n  pi.on(\"tool_call\", async (event, ctx) => {\n    if (event.toolName === \"bash\" && event.input.command?.includes(\"rm -rf\")) {\n      const ok = await ctx.ui.confirm(\"Dangerous!\", \"Allow rm -rf?\");\n      if (!ok) return { block: true, reason: \"Blocked by user\" };\n    }\n  });\n\n  // Register a custom tool\n  pi.registerTool({\n    name: \"greet\",\n    label: \"Greet\",\n    description: \"Greet someone by name\",\n    parameters: Type.Object({\n      name: Type.String({ description: \"Name to greet\" }),\n    }),\n    async execute(toolCallId, params, signal, onUpdate, ctx) {\n      return {\n        content: [{ type: \"text\", text: `Hello, ${params.name}!` }],\n        details: {},\n      };\n    },\n  });\n\n  // Register a command\n  pi.registerCommand(\"hello\", {\n    description: \"Say hello\",\n    handler: async (args, ctx) => {\n      ctx.ui.notify(`Hello ${args || \"world\"}!`, \"info\");\n    },\n  });\n}\n```\n\nТест с флагом `--extension` (или `-e`):\n\n```bash\npi -e ./my-extension.ts\n```\n\n## Расположение расширений\n\n> **Безопасность:** Extensions запускается с полными системными разрешениями и может выполнять произвольный код. Устанавливайте только из источников, которым вы доверяете.\n\nExtensions автоматически обнаруживаются в доверенных местах. Локальные записи `.pi/extensions` проекта загружаются только после того, как проекту доверяют.\n\n| Расположение | Объем |\n|----------|-------|\n| `~/.pi/agent/extensions/*.ts` | Глобальный (все проекты) |\n| `~/.pi/agent/extensions/*/index.ts` | Глобальный (подкаталог) |\n| `.pi/extensions/*.ts` | Проект-локальный |\n| `.pi/extensions/*/index.ts` | Локальный проект (подкаталог) |\n\nДополнительные пути через `settings.json`:\n\n```json\n{\n  \"packages\": [\n    \"npm:@foo/bar@1.0.0\",\n    \"git:github.com/user/repo@v1\"\n  ],\n  \"extensions\": [\n    \"/path/to/local/extension.ts\",\n    \"/path/to/local/extension/dir\"\n  ]\n}\n```\n\nЧтобы поделиться расширениями через npm или git как пакеты pi, см. [packages.md](packages.md).\n\n## Доступный импорт\n\n| Упаковка | Цель |\n|---------|---------|\n| `@earendil-works/pi-coding-agent` | Типы расширений (`ExtensionAPI`, `ExtensionContext`, события) |\n| `typebox` | Определения схемы для параметров инструмента |\n| `@earendil-works/pi-ai` | Утилиты искусственного интеллекта (`StringEnum` для перечислений, совместимых с Google) |\n| `@earendil-works/pi-tui` | TUI компоненты для пользовательского рендеринга |\n\nnpm зависимости тоже работают. Добавьте `package.json` рядом с вашим расширением (или в родительском каталоге), запустите `npm install`, и импорт из `node_modules/` будет разрешен автоматически.\n\nДля распределенных пакетов pi, установленных с помощью `pi install` (npm или git), параметры времени выполнения должны находиться в `dependencies`. При установке пакета по умолчанию используются производственные установки (`npm install --omit=dev`), поэтому `devDependencies` недоступны во время выполнения; когда настроен `npmCommand`, пакеты git используют простой `install` для совместимости с оболочками.\n\nТакже доступны встроенные модули Node.js (`node:fs`, `node:path` и т. д.).\n\n## Написание расширения\n\nРасширение экспортирует заводскую функцию по умолчанию, которая получает `ExtensionAPI`. Фабрика может быть синхронной или асинхронной:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  // Subscribe to events\n  pi.on(\"event_name\", async (event, ctx) => {\n    // ctx.ui for user interaction\n    const ok = await ctx.ui.confirm(\"Title\", \"Are you sure?\");\n    ctx.ui.notify(\"Done!\", \"info\");\n    ctx.ui.setStatus(\"my-ext\", \"Processing...\");  // Footer status\n    ctx.ui.setWidget(\"my-ext\", [\"Line 1\", \"Line 2\"]);  // Widget above editor (default)\n  });\n\n  // Register tools, commands, shortcuts, flags\n  pi.registerTool({ ... });\n  pi.registerCommand(\"name\", { ... });\n  pi.registerShortcut(\"ctrl+x\", { ... });\n  pi.registerFlag(\"my-flag\", { ... });\n}\n```\n\nExtensions загружаются через [jiti](https://github.com/unjs/jiti), поэтому TypeScript работает без компиляции.\n\nЕсли фабрика возвращает `Promise`, pi ожидает его, прежде чем продолжить запуск. Это означает, что асинхронная инициализация завершается до `session_start`, до `resources_discover` и до того, как будут сброшены регистрации поставщиков, поставленные в очередь через `pi.registerProvider()`.\n\n### Асинхронные фабричные функции\n\nИспользуйте асинхронную фабрику для однократного запуска, например для получения удаленной конфигурации или динамического обнаружения доступных моделей.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default async function (pi: ExtensionAPI) {\n  const response = await fetch(\"http://localhost:1234/v1/models\");\n  const payload = (await response.json()) as {\n    data: Array<{\n      id: string;\n      name?: string;\n      context_window?: number;\n      max_tokens?: number;\n    }>;\n  };\n\n  pi.registerProvider(\"local-openai\", {\n    baseUrl: \"http://localhost:1234/v1\",\n    apiKey: \"$LOCAL_OPENAI_API_KEY\",\n    api: \"openai-completions\",\n    models: payload.data.map((model) => ({\n      id: model.id,\n      name: model.name ?? model.id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: model.context_window ?? 128000,\n      maxTokens: model.max_tokens ?? 4096,\n    })),\n  });\n}\n```\n\nЭтот шаблон делает выбранные модели доступными при обычном запуске и до `pi --list-models`.\n\n### Долговечные ресурсы и отключение\n\nФабрики расширений могут запускаться в вызовах, которые никогда не запускают сеанс. Не запускайте фоновые ресурсы, такие как процессы, сокеты, средства наблюдения за файлами или таймеры, с завода.\n\nОтложите запуск фонового ресурса до `session_start` или до команды/инструмента/события, которому нужен ресурс. Зарегистрируйте идемпотентный обработчик `session_shutdown`, чтобы закрыть любые запускаемые вами ресурсы в области сеанса.\n\n### Стили расширения\n\n**Один файл** – самый простой вариант для небольших расширений:\n\n```\n~/.pi/agent/extensions/\n└── my-extension.ts\n```\n\n**Каталог с index.ts** — для многофайловых расширений:\n\n```\n~/.pi/agent/extensions/\n└── my-extension/\n    ├── index.ts        # Entry point (exports default function)\n    ├── tools.ts        # Helper module\n    └── utils.ts        # Helper module\n```\n\n**Пакет с зависимостями** — для расширений, которым требуется npm пакетов:\n\n```\n~/.pi/agent/extensions/\n└── my-extension/\n    ├── package.json    # Declares dependencies and entry points\n    ├── package-lock.json\n    ├── node_modules/   # After npm install\n    └── src/\n        └── index.ts\n```\n\n```json\n// package.json\n{\n  \"name\": \"my-extension\",\n  \"dependencies\": {\n    \"zod\": \"^3.0.0\",\n    \"chalk\": \"^5.0.0\"\n  },\n  \"pi\": {\n    \"extensions\": [\"./src/index.ts\"]\n  }\n}\n```\n\nЗапустите `npm install` в каталоге расширения, затем импорт из `node_modules/` будет работать автоматически.\n\n## События\n\n### Обзор жизненного цикла\n\n```\npi starts\n  │\n  ├─► project_trust (user/global and CLI extensions only, before project resources load)\n  ├─► session_start { reason: \"startup\" }\n  └─► resources_discover { reason: \"startup\" }\n      │\n      ▼\nuser sends prompt ─────────────────────────────────────────┐\n  │                                                        │\n  ├─► (extension commands checked first, bypass if found)  │\n  ├─► input (can intercept, transform, or handle)          │\n  ├─► (skill/template expansion if not handled)            │\n  ├─► before_agent_start (can inject message, modify system prompt)\n  ├─► agent_start                                          │\n  ├─► message_start / message_update / message_end         │\n  │                                                        │\n  │   ┌─── turn (repeats while LLM calls tools) ───┐       │\n  │   │                                            │       │\n  │   ├─► turn_start                               │       │\n  │   ├─► context (can modify messages)            │       │\n  │   ├─► before_provider_headers (can mutate headers)     |\n  │   ├─► before_provider_request (can inspect or replace payload)\n  │   ├─► after_provider_response (status + headers, before stream consume)\n  │   │                                            │       │\n  │   │   LLM responds, may call tools:            │       │\n  │   │     ├─► tool_execution_start               │       │\n  │   │     ├─► tool_call (can block)              │       │\n  │   │     ├─► tool_execution_update              │       │\n  │   │     ├─► tool_result (can modify)           │       │\n  │   │     └─► tool_execution_end                 │       │\n  │   │                                            │       │\n  │   └─► turn_end                                 │       │\n  │                                                        │\n  ├─► agent_end                                            │\n  └─► agent_settled (no retry/compaction/follow-up left)   │\n                                                           │\nuser sends another prompt ◄────────────────────────────────┘\n\n/new (new session) or /resume (switch session)\n  ├─► session_before_switch (can cancel)\n  ├─► session_shutdown\n  ├─► session_start { reason: \"new\" | \"resume\", previousSessionFile? }\n  └─► resources_discover { reason: \"startup\" }\n\n/fork or /clone\n  ├─► session_before_fork (can cancel)\n  ├─► session_shutdown\n  ├─► session_start { reason: \"fork\", previousSessionFile }\n  └─► resources_discover { reason: \"startup\" }\n\n/name or pi.setSessionName()\n  └─► session_info_changed\n\n/compact or auto-compaction\n  ├─► session_before_compact (can cancel or customize)\n  └─► session_compact\n\n/tree navigation\n  ├─► session_before_tree (can cancel or customize)\n  └─► session_tree\n\n/model or Ctrl+P (model selection/cycling)\n  ├─► thinking_level_select (if model change changes/clamps thinking level)\n  └─► model_select\n\nthinking level changes (settings, keybinding, pi.setThinkingLevel())\n  └─► thinking_level_select\n\nexit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)\n  └─► session_shutdown\n```\n\n### Стартовые события\n\n#### project_trust\n\nЗапускается до того, как pi решит, доверять ли проекту с динамическими конфигурациями (`.pi` или `.agents/skills`). Он запускается во время запуска и когда замена сеанса (например, `/resume`) входит в cwd, доверие которого не было разрешено в текущем процессе. Участвуют только пользовательские/глобальные расширения и расширения CLI `-e`; Локальные расширения проекта не загружаются до тех пор, пока не будет разрешено доверие.\n\n```typescript\npi.on(\"project_trust\", async (event, ctx) => {\n  // event.cwd - current working directory\n  // ctx has a limited trust context: cwd, mode, hasUI, and select/confirm/input/notify UI helpers\n  if (await ctx.ui.confirm(\"Trust project?\", event.cwd)) {\n    return { trusted: \"yes\", remember: true };\n  }\n  return { trusted: \"undecided\" };\n});\n```\n\nОбработчик `project_trust` должен возвращать `{ trusted: \"yes\" | \"no\" | \"undecided\" }`. Пользовательское/глобальное расширение или расширение CLI, которое возвращает `\"yes\"` или `\"no\"`, принимает решение; первое решение «да/нет» побеждает и подавляет встроенный запрос доверия. Используйте `remember: true`, чтобы утвердить решение да/нет; в противном случае это применяется только к текущему процессу. Верните `\"undecided\"`, чтобы позволить более поздним обработчикам или встроенному потоку доверия принять решение. Прежде чем запрашивать запрос, проверьте `ctx.hasUI`. Если ни один обработчик не возвращает да/нет, нормальное разрешение доверия продолжается: сначала применяются сохраненные решения `trust.json`, затем `defaultProjectTrust` контролирует, запрашивает ли pi, доверяет или отклоняет его по умолчанию.\n\n### Ресурсные события\n\n#### resources_discover\n\nЗапускается после `session_start`, поэтому расширения могут предоставлять дополнительные пути к навыкам, подсказкам и темам.\nПуть запуска использует `reason: \"startup\"`. Для перезагрузки используется `reason: \"reload\"`.\n\n```typescript\npi.on(\"resources_discover\", async (event, _ctx) => {\n  // event.cwd - current working directory\n  // event.reason - \"startup\" | \"reload\"\n  return {\n    skillPaths: [\"/path/to/skills\"],\n    promptPaths: [\"/path/to/prompts\"],\n    themePaths: [\"/path/to/themes\"],\n  };\n});\n```\n\n### События сессии\n\nСм. [Session Format](session-format.md) о внутреннем устройстве хранилища сеансов и SessionManager API.\n\n#### session_start\n\nЗапускается, когда сеанс запускается, загружается или перезагружается.\n\n```typescript\npi.on(\"session_start\", async (event, ctx) => {\n  // event.reason - \"startup\" | \"reload\" | \"new\" | \"resume\" | \"fork\"\n  // event.previousSessionFile - present for \"new\", \"resume\", and \"fork\"\n  ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? \"ephemeral\"}`, \"info\");\n});\n```\n\n#### session_info_changed\n\nЗапускается, когда отображаемое имя текущего сеанса установлено с помощью `/name`, RPC или `pi.setSessionName()`.\n\n```typescript\npi.on(\"session_info_changed\", async (event, ctx) => {\n  // event.name - current normalized name, or undefined if cleared\n  ctx.ui.notify(`Session renamed: ${event.name ?? \"(none)\"}`, \"info\");\n});\n```\n\n#### session_before_switch\n\nЗапускается перед началом нового сеанса (`/new`) или переключением сеанса (`/resume`).\n\n```typescript\npi.on(\"session_before_switch\", async (event, ctx) => {\n  // event.reason - \"new\" or \"resume\"\n  // event.targetSessionFile - session we're switching to (only for \"resume\")\n\n  if (event.reason === \"new\") {\n    const ok = await ctx.ui.confirm(\"Clear?\", \"Delete all messages?\");\n    if (!ok) return { cancel: true };\n  }\n});\n```\n\nПосле успешного переключения или действия нового сеанса pi выдает `session_shutdown` для старого экземпляра расширения, перезагружает и повторно привязывает расширения для нового сеанса, затем выдает `session_start` с `reason: \"new\" | \"resume\"` и `previousSessionFile`.\nВыполните очистку в `session_shutdown`, затем восстановите любое состояние памяти в `session_start`.\n\n#### session_before_fork\n\nЗапускается при разветвлении через `/fork` или клонировании через `/clone`.\n\n```typescript\npi.on(\"session_before_fork\", async (event, ctx) => {\n  // event.entryId - ID of the selected entry\n  // event.position - \"before\" for /fork, \"at\" for /clone\n  return { cancel: true }; // Cancel fork/clone\n  // OR\n  return { skipConversationRestore: true }; // Reserved for future conversation restore control\n});\n```\n\nПосле успешного разветвления или клонирования pi выдает `session_shutdown` для старого экземпляра расширения, перезагружает и повторно привязывает расширения для нового сеанса, затем выдает `session_start` с `reason: \"fork\"` и `previousSessionFile`.\nВыполните очистку в `session_shutdown`, затем восстановите любое состояние памяти в `session_start`.\n\n#### сеанс_перед_компакт / сеанс_компакт\n\nСгорел при уплотнении. Подробности см. [compaction.md](compaction.md).\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n    }\n  };\n});\n\npi.on(\"session_compact\", async (event, ctx) => {\n  // event.compactionEntry - the saved compaction\n  // event.fromExtension - whether extension provided it\n  // event.reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n});\n```\n\n#### дерево сеанса_перед_деревом / дерево_сессии\n\nСработало при навигации `/tree`. См. [Sessions](sessions.md) для ознакомления с концепциями навигации по дереву.\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n  return { cancel: true };\n  // OR provide custom summary:\n  return {\n    summary: {\n      summary: \"...\",\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: {},\n    },\n  };\n});\n\npi.on(\"session_tree\", async (event, ctx) => {\n  // event.newLeafId, oldLeafId, summaryEntry, fromExtension\n});\n```\n\n#### session_shutdown\n\nВызывается до того, как запущенная среда выполнения сеанса будет удалена. Используйте это для очистки ресурсов, открытых из `session_start` или других перехватчиков в области сеанса.\n\n```typescript\npi.on(\"session_shutdown\", async (event, ctx) => {\n  // event.reason - \"quit\" | \"reload\" | \"new\" | \"resume\" | \"fork\"\n  // event.targetSessionFile - destination session for session replacement flows\n  // Cleanup, save state, etc.\n});\n```\n\n### Агентские события\n\n#### before_agent_start\n\nЗапускается после того, как пользователь отправляет запрос, перед циклом агента. Может вставить сообщение и/или изменить системное приглашение.\n\n```typescript\npi.on(\"before_agent_start\", async (event, ctx) => {\n  // event.prompt - user's prompt text\n  // event.images - attached images (if any)\n  // event.systemPrompt - current chained system prompt for this handler\n  //   (includes changes from earlier before_agent_start handlers)\n  // event.systemPromptOptions - structured options used to build the system prompt\n  //   .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)\n  //   .selectedTools - tools currently active in the prompt\n  //   .toolSnippets - one-line descriptions for each tool\n  //   .promptGuidelines - custom guideline bullets\n  //   .appendSystemPrompt - text from --append-system-prompt flags\n  //   .cwd - working directory\n  //   .contextFiles - AGENTS.md files and other loaded context files\n  //   .skills - loaded skills\n\n  return {\n    // Inject a persistent message (stored in session, sent to LLM)\n    message: {\n      customType: \"my-extension\",\n      content: \"Additional context for the LLM\",\n      display: true,\n    },\n    // Replace the system prompt for this turn (chained across extensions)\n    systemPrompt: event.systemPrompt + \"\\n\\nExtra instructions for this turn...\",\n  };\n});\n```\n\nПоле `systemPromptOptions` предоставляет расширениям доступ к тем же структурированным данным, которые Pi использует для создания системного приглашения. Это позволяет вам проверять, что загружено Pi — пользовательские подсказки, рекомендации, фрагменты инструментов, context files, навыки — без повторного открытия ресурсов или повторного анализа флагов. Используйте его, когда вашему расширению необходимо внести глубокие и обоснованные изменения в системную подсказку, соблюдая при этом конфигурацию, предоставленную пользователем.\n\nВнутри `before_agent_start`, `event.systemPrompt` и `ctx.getSystemPrompt()` оба отражают связанное системное приглашение текущего обработчика. Позже обработчики `before_agent_start` все еще смогут изменить его снова.\n\n#### начало_агента / окончание_агента / урегулирование_агента\n\n`agent_start` срабатывает, когда начинается запуск агента низкого уровня. `agent_end` срабатывает, когда этот запуск заканчивается, но Pi все еще может автоматически повторять попытку, автоматически сжимать и повторять попытку или продолжать с последующими сообщениями в очереди. Используйте `agent_settled` для интеграции статуса, если необходимо знать, что Pi не будет продолжать работать автоматически.\n\n```typescript\npi.on(\"agent_start\", async (_event, ctx) => {});\n\npi.on(\"agent_end\", async (event, ctx) => {\n  // event.messages - messages from this low-level run\n});\n\npi.on(\"agent_settled\", async (_event, ctx) => {\n  // ctx.isIdle() is true here unless another extension started a new run.\n});\n```\n\n#### начало_поворота/конец_поворота\n\nСрабатывает за каждый ход (один ответ LLM + вызовы инструментов).\n\n```typescript\npi.on(\"turn_start\", async (event, ctx) => {\n  // event.turnIndex, event.timestamp\n});\n\npi.on(\"turn_end\", async (event, ctx) => {\n  // event.turnIndex, event.message, event.toolResults\n});\n```\n\n#### начало_сообщения/обновление_сообщения/конец_сообщения\n\nСрабатывает при обновлении жизненного цикла сообщения.\n\n- `message_start` и `message_end` срабатывают для сообщений пользователя, помощника и инструмента.\n- `message_update` срабатывает для потоковой передачи обновлений помощника.\n- Обработчики `message_end` могут возвращать `{ message }` для замены окончательного сообщения. Замена должна оставить прежнюю `role`.\n\n```typescript\npi.on(\"message_start\", async (event, ctx) => {\n  // event.message\n});\n\npi.on(\"message_update\", async (event, ctx) => {\n  // event.message\n  // event.assistantMessageEvent (token-by-token stream event)\n});\n\npi.on(\"message_end\", async (event, ctx) => {\n  if (event.message.role !== \"assistant\") return;\n\n  return {\n    message: {\n      ...event.message,\n      usage: {\n        ...event.message.usage,\n        cost: {\n          ...event.message.usage.cost,\n          total: 0.123,\n        },\n      },\n    },\n  };\n});\n```\n\n#### начало_выполнения_инструмента/обновление_выполнения_инструмента/конец_выполнения_инструмента\n\nЗапускается из-за обновлений жизненного цикла выполнения инструмента.\n\nВ параллельном режиме инструмента:\n- `tool_execution_start` излучается в порядке вспомогательного источника на предполетном этапе.\n- `tool_execution_update` события могут чередоваться между инструментами\n- `tool_execution_end` выдается в порядке завершения работы с инструментом после завершения каждого инструмента.\n- События окончательного сообщения `toolResult` по-прежнему отправляются позже в порядке источника помощника.\n\n```typescript\npi.on(\"tool_execution_start\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.args\n});\n\npi.on(\"tool_execution_update\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.args, event.partialResult\n});\n\npi.on(\"tool_execution_end\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.result, event.isError\n});\n```\n\n#### контекст\n\nВызывается перед каждым вызовом LLM. Изменяйте сообщения неразрушающим образом. См. [Session Format](session-format.md) для типов сообщений.\n\n```typescript\npi.on(\"context\", async (event, ctx) => {\n  // event.messages - deep copy, safe to modify\n  const filtered = event.messages.filter(m => !shouldPrune(m));\n  return { messages: filtered };\n});\n```\n\n#### before_provider_headers\n\nЗапускается после сборки исходящих HTTP-заголовков. Используйте его для добавления, переопределения или удаления заголовков запросов.\n\nОбработчики мутируют `event.headers` на месте. Установите ключ на строку, чтобы добавить или переопределить ее, или на `null`, чтобы удалить ее.\n\n```typescript\npi.on(\"before_provider_headers\", (event, ctx) => {\n  // Add or override — e.g. a session id for gateway tracing/attribution\n  event.headers[\"x-session-id\"] = ctx.sessionManager.getSessionId();\n\n  // Drop a tracking header pi adds for this call\n  event.headers[\"X-OpenRouter-Title\"] = null;\n});\n```\n\nЗапускается один раз по запросу поставщика; повторные попытки повторно используют одни и те же заголовки вместо повторного запуска перехватчика.\n\n#### before_provider_request\n\nЗапускается после создания полезных данных, специфичных для поставщика, непосредственно перед отправкой запроса. Обработчики выполняются в порядке загрузки расширений. Возврат `undefined` сохраняет полезную нагрузку неизменной. Возврат любого другого значения заменяет полезную нагрузку для последующих обработчиков и самого запроса.\n\nЭтот хук может переписать системные инструкции на уровне провайдера или полностью удалить их. Эти изменения на уровне полезных данных не отражаются в `ctx.getSystemPrompt()`, который сообщает строку системного приглашения Pi, а не окончательную сериализованную полезную нагрузку поставщика.\n\n```typescript\npi.on(\"before_provider_request\", (event, ctx) => {\n  console.log(JSON.stringify(event.payload, null, 2));\n\n  // Optional: replace payload\n  // return { ...event.payload, temperature: 0 };\n});\n```\n\nЭто в основном полезно для отладки сериализации поставщика и поведения кэша.\n\n#### after_provider_response\n\nЗапускается после получения HTTP-ответа и до того, как будет использовано тело его потока. Обработчики выполняются в порядке загрузки расширений.\n\n```typescript\npi.on(\"after_provider_response\", (event, ctx) => {\n  // event.status - HTTP status code\n  // event.headers - normalized response headers\n  if (event.status === 429) {\n    console.log(\"rate limited\", event.headers[\"retry-after\"]);\n  }\n});\n```\n\nДоступность заголовка зависит от провайдера и транспорта. Providers что абстрактные HTTP-ответы могут не предоставлять заголовки.\n\n### Модельные события\n\n#### model_select\n\nЗапускается, когда модель изменяется с помощью команды `/model`, циклического переключения модели (`Ctrl+P`) или восстановления сеанса.\n\n```typescript\npi.on(\"model_select\", async (event, ctx) => {\n  // event.model - newly selected model\n  // event.previousModel - previous model (undefined if first selection)\n  // event.source - \"set\" | \"cycle\" | \"restore\"\n\n  const prev = event.previousModel\n    ? `${event.previousModel.provider}/${event.previousModel.id}`\n    : \"none\";\n  const next = `${event.model.provider}/${event.model.id}`;\n\n  ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, \"info\");\n});\n```\n\nИспользуйте это для обновления элементов пользовательского интерфейса (строки состояния, нижние колонтитулы) или выполнения инициализации для конкретной модели при изменении активной модели.\n\n#### think_level_select\n\nСрабатывает, когда меняется уровень мышления. Это только уведомление; Возвращаемые значения обработчика игнорируются.\n\n```typescript\npi.on(\"thinking_level_select\", async (event, ctx) => {\n  // event.level - newly selected thinking level\n  // event.previousLevel - previous thinking level\n\n  ctx.ui.setStatus(\"thinking\", `thinking: ${event.level}`);\n});\n```\n\nИспользуйте это для обновления пользовательского интерфейса расширения, когда `pi.setThinkingLevel()`, изменения модели или встроенные элементы управления на уровне мышления изменяют активный уровень мышления.\n\n### События инструмента\n\n#### инструмент_вызов\n\nЗапускается после `tool_execution_start`, до выполнения инструмента. **Может блокироваться.** Используйте `isToolCallEventType`, чтобы сузить и получить типизированные входные данные.\n\nПеред запуском `tool_call` pi ожидает завершения прохождения ранее созданных событий агента через `AgentSession`. Это означает, что `ctx.sessionManager` обновлен через текущее сообщение вызова помощника.\n\nВ режиме параллельного выполнения инструмента по умолчанию вызовы родственных инструментов из одного и того же сообщения помощника предварительно проверяются последовательно, а затем выполняются одновременно. `tool_call` не гарантируется, что он увидит результаты родственного инструмента из того же сообщения помощника в `ctx.sessionManager`.\n\n`event.input` является изменяемым. Измените его, чтобы исправить аргументы инструмента перед выполнением.\n\nПоведение гарантирует:\n- Мутации `event.input` влияют на фактическое выполнение инструмента.\n- Более поздние обработчики `tool_call` видят мутации, сделанные более ранними обработчиками.\n- После мутации повторная проверка не выполняется.\n- Возвращаемые значения из `tool_call` управления блокировкой через `{ block: true, reason?: string, terminate?: boolean }`\n- `terminate` применяется только к заблокированному вызову; агент останавливается раньше, только когда завершается работа каждого окончательного результата в пакете\n\n```typescript\nimport { isToolCallEventType } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"tool_call\", async (event, ctx) => {\n  // event.toolName - \"bash\", \"read\", \"write\", \"edit\", etc.\n  // event.toolCallId\n  // event.input - tool parameters (mutable)\n\n  // Built-in tools: no type params needed\n  if (isToolCallEventType(\"bash\", event)) {\n    // event.input is { command: string; timeout?: number }\n    event.input.command = `source ~/.profile\\n${event.input.command}`;\n\n    if (event.input.command.includes(\"rm -rf\")) {\n      return { block: true, reason: \"Dangerous command\", terminate: true };\n    }\n  }\n\n  if (isToolCallEventType(\"read\", event)) {\n    // event.input is { path: string; offset?: number; limit?: number }\n    console.log(`Reading: ${event.input.path}`);\n  }\n});\n```\n\n#### Ввод пользовательского ввода инструмента\n\nПользовательские инструменты должны экспортировать свой тип ввода:\n\n```typescript\n// my-extension.ts\nexport type MyToolInput = Static<typeof myToolSchema>;\n```\n\nИспользуйте `isToolCallEventType` с параметрами явного типа:\n\n```typescript\nimport { isToolCallEventType } from \"@earendil-works/pi-coding-agent\";\nimport type { MyToolInput } from \"my-extension\";\n\npi.on(\"tool_call\", (event) => {\n  if (isToolCallEventType<\"my_tool\", MyToolInput>(\"my_tool\", event)) {\n    event.input.action;  // typed\n  }\n});\n```\n\n#### инструмент_результат\n\nЗапускается после завершения выполнения инструмента и до появления `tool_execution_end` плюс событий сообщения о конечном результате работы инструмента. **Можно изменить результат.**\n\nВ режиме параллельного инструмента `tool_result` и `tool_execution_end` могут чередоваться в порядке завершения инструмента, в то время как последние события сообщения `toolResult` по-прежнему отправляются позже в порядке источника помощника.\n\n`tool_result` цепочка обработчиков, подобная промежуточному программному обеспечению:\n- Обработчики выполняются в порядке загрузки расширений.\n- Каждый обработчик видит последний результат после предыдущих изменений обработчика.\n- Обработчики могут возвращать частичные исправления (`content`, `details`, `isError` или `usage`); пропущенные поля сохраняют свои текущие значения\n\nИспользуйте `ctx.signal` для вложенной асинхронной работы внутри обработчика. Это позволяет Esc отменять вызовы модели, `fetch()` и другие операции с возможностью прерывания, запущенные расширением.\n\n```typescript\nimport { isBashToolResult } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"tool_result\", async (event, ctx) => {\n  // event.toolName, event.toolCallId, event.input\n  // event.content, event.details, event.isError, event.usage\n\n  if (isBashToolResult(event)) {\n    // event.details is typed as BashToolDetails\n  }\n\n  const response = await fetch(\"https://example.com/summarize\", {\n    method: \"POST\",\n    body: JSON.stringify({ content: event.content }),\n    signal: ctx.signal,\n  });\n\n  // Modify result:\n  return { content: [...], details: {...}, isError: false, usage: nestedModelUsage };\n});\n```\n\n### Пользовательские события Bash\n\n#### пользователь_bash\n\nЗапускается, когда пользователь выполняет команды `!` или `!!`. **Может перехватывать.**\n\n```typescript\nimport { createLocalBashOperations } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"user_bash\", (event, ctx) => {\n  // event.command - the bash command\n  // event.excludeFromContext - true if !! prefix\n  // event.cwd - working directory\n\n  // Option 1: Provide custom operations (e.g., SSH)\n  return { operations: remoteBashOps };\n\n  // Option 2: Wrap pi's built-in local bash backend\n  const local = createLocalBashOperations();\n  return {\n    operations: {\n      exec(command, cwd, options) {\n        return local.exec(`source ~/.profile\\n${command}`, cwd, options);\n      }\n    }\n  };\n\n  // Option 3: Full replacement - return result directly\n  return { result: { output: \"...\", exitCode: 0, cancelled: false, truncated: false } };\n});\n```\n\n### Входные события\n\n#### вход\n\nЗапускается при получении пользовательского ввода, после проверки команд расширения, но до расширения навыков и шаблонов. Событие видит необработанный входной текст, поэтому `/skill:foo` и `/template` еще не раскрыты.\n\n**Порядок обработки:**\n1. Команды расширения (`/cmd`) проверяются в первую очередь — если они найдены, запускается обработчик и событие ввода пропускается.\n2. `input` пожары событий — могут перехватывать, трансформировать или обрабатывать\n3. Если не обработано: команды навыков (`/skill:name`) расширяются до содержимого навыков.\n4. Если не обработано: prompt templates (`/template`) расширяется до содержимого шаблона.\n5. Начинается обработка агента (`before_agent_start` и т. д.)\n\n```typescript\npi.on(\"input\", async (event, ctx) => {\n  // event.text - raw input (before skill/template expansion)\n  // event.images - attached images, if any\n  // event.source - \"interactive\" (typed), \"rpc\" (API), or \"extension\" (via sendUserMessage)\n  // event.streamingBehavior - \"steer\" | \"followUp\" | undefined\n  //   undefined when idle, \"steer\" for mid-stream interrupts,\n  //   \"followUp\" for messages queued until the agent finishes\n\n  // Transform: rewrite input before expansion\n  if (event.text.startsWith(\"?quick \"))\n    return { action: \"transform\", text: `Respond briefly: ${event.text.slice(7)}` };\n\n  // Handle: respond without LLM (extension shows its own feedback)\n  if (event.text === \"ping\") {\n    ctx.ui.notify(\"pong\", \"info\");\n    return { action: \"handled\" };\n  }\n\n  // Route by source: skip processing for extension-injected messages\n  if (event.source === \"extension\") return { action: \"continue\" };\n\n  // Intercept skill commands before expansion\n  if (event.text.startsWith(\"/skill:\")) {\n    // Could transform, block, or let pass through\n  }\n\n  return { action: \"continue\" };  // Default: pass through to expansion\n});\n```\n\n**Результаты:**\n- `continue` — пройти без изменений (по умолчанию, если обработчик ничего не возвращает)\n- `transform` — изменить текст/изображения, затем продолжить раскрытие\n- `handled` — полностью пропустить агент (первый обработчик, который вернет этот выигрыш)\n\nПреобразует цепочку между обработчиками. См. [input-transform.ts](../examples/extensions/input-transform.ts) и [input-transform-streaming.ts](../examples/extensions/input-transform-streaming.ts) для маршрутизации с учетом `streamingBehavior`.\n\n## Контекст расширения\n\nВсе обработчики получают `ctx: ExtensionContext`.\n\n### ctx.ui\n\nМетоды пользовательского интерфейса для взаимодействия с пользователем. Подробную информацию см. [Custom UI](#custom-ui).\n\n### ctx.mode\n\nТекущий режим работы: `\"tui\"`, `\"rpc\"`, `\"json\"` или `\"print\"`. Используйте `ctx.mode === \"tui\"` для защиты функций только терминала, таких как `custom()`, фабрики компонентов, ввод через терминал и прямой рендеринг TUI.\n\n### ctx.hasUI\n\n`true` в режимах TUI и RPC. `false` в режиме печати (`-p`) и JSON. Используйте это для защиты методов диалога (`select`, `confirm`, `input`, `editor`) и методов «выстрелил и забыл» (`notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText`), которые работают как в TUI, так и в RPC режимов. В режиме RPC некоторые методы, специфичные для TUI, не выполняются или возвращают значения по умолчанию (см. [rpc.md](rpc.md#extension-ui-protocol)).\n\n### ctx.cwd\n\nТекущий рабочий каталог.\n\nИспользуйте `CONFIG_DIR_NAME` вместо жесткого кодирования `.pi` при создании локальных путей конфигурации проекта. Дистрибутивы с ребрендингом могут использовать другое имя каталога конфигурации.\n\n```typescript\nimport { CONFIG_DIR_NAME, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { join } from \"node:path\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, \"my-extension.json\");\n    // ...\n  });\n}\n```\n\n### ctx.isProjectTrusted()\n\nВозвращает, активно ли локальное доверие проекта для текущего контекста сеанса. Сюда входят временные решения о доверии и переопределения доверия CLI, а не только сохраненные решения в глобальном хранилище доверенных сертификатов.\n\nИспользуйте это перед чтением конфигурации локального расширения проекта, которую следует учитывать только для доверенных проектов.\n\n### ctx.sessionManager\n\nДоступ только для чтения к состоянию сеанса. См. [Session Format](session-format.md) для полной версии SessionManager API и типов записей.\n\nДля `tool_call` это состояние синхронизируется через текущее сообщение помощника перед запуском обработчиков. В режиме параллельного выполнения инструмента по-прежнему не гарантируется включение результатов родственного инструмента из одного и того же сообщения помощника.\n\n```typescript\nctx.sessionManager.getEntries()             // All entries\nctx.sessionManager.getBranch()              // Current branch\nctx.sessionManager.buildContextEntries()    // Active branch entries with compaction applied\nctx.sessionManager.getLeafId()              // Current leaf entry ID\n```\n\n### ctx.modelRegistry/ctx.model/ctx.thinkingLevel/ctx.scopedModels\n\nДоступ к моделям, поставщикам и разрешенной аутентификации. `ctx.modelRegistry.getProvider(id)` возвращает эффективного поставщика pi-ai, а `getProviderAuth(id)` разрешает его текущий API key, заголовки, базовый URL-адрес и среду на уровне поставщика, не требуя загрузки модели. `ctx.model` — активная модель, а `ctx.thinkingLevel` — текущий эффективный уровень мышления.\n\n`ctx.scopedModels` — это доступный только для чтения список моделей, применимых к текущему сеансу — тот же набор, который показывает команда `/scoped-models`. Это разрешается при запуске сеанса с помощью флага `--models` CLI и настройки `enabledModels` (сопоставляется с доступным каталогом с минимальным совпадением на `provider/modelId` или пустом `modelId`). Он пуст, если область действия не настроена, что означает, что можно использовать любую доступную модель. Каждая запись имеет номер `{ model, thinkingLevel? }`, где `thinkingLevel` устанавливается только в том случае, если ее закрепил шаблон (например, `anthropic/*:high`). Используйте его для заполнения средства выбора модели, которое отражает встроенное, вместо перечисления всего каталога с помощью `ctx.modelRegistry.getAvailable()`.\n\n### ctx.signal\n\nТекущий сигнал прерывания агента или `undefined`, если ни один ход агента не активен.\n\nИспользуйте это для вложенной работы с поддержкой прерывания, запускаемой обработчиками расширений, например:\n- `fetch(..., { signal: ctx.signal })`\n- вызовы моделей, которые принимают `signal`\n- помощники файлов или процессов, которые принимают `AbortSignal`\n\n`ctx.signal` обычно определяется во время активных событий хода, таких как `tool_call`, `tool_result`, `message_update` и `turn_end`.\nОбычно это `undefined` в контекстах ожидания или отсутствия поворота, таких как события сеанса, команды расширения и ярлыки, запускаемые во время простоя pi.\n\n```typescript\npi.on(\"tool_result\", async (event, ctx) => {\n  const response = await fetch(\"https://example.com/api\", {\n    method: \"POST\",\n    body: JSON.stringify(event),\n    signal: ctx.signal,\n  });\n\n  const data = await response.json();\n  return { details: data };\n});\n```\n\n### ctx.isIdle()/ctx.abort()/ctx.hasPendingMessages()\n\nПомощники управления потоком. `ctx.isIdle()` имеет значение false, в то время как Pi обрабатывает запуск агента, автоматическую повторную попытку, повторную попытку автоматического сжатия или продолжение в очереди.\n\n### ctx.shutdown()\n\nЗапросите корректное завершение работы pi.\n\n- **Интерактивный режим:** Откладывается до тех пор, пока агент не станет бездействующим (после обработки всех находящихся в очереди управляющих и последующих сообщений).\n- Режим **RPC:** Откладывается до следующего состояния простоя (после завершения ответа на текущую команду, при ожидании следующей команды).\n- **Режим печати:** Нет операций. Процесс завершается автоматически после обработки всех запросов.\n\nПеред выходом выдает событие `session_shutdown` всем расширениям. Доступно во всех контекстах (обработчики событий, инструменты, команды, ярлыки).\n\n```typescript\npi.on(\"tool_call\", (event, ctx) => {\n  if (isFatal(event.input)) {\n    ctx.shutdown();\n  }\n});\n```\n\n### ctx.getContextUsage()\n\nВозвращает текущее использование контекста для активной модели. Использует использование последнего помощника, если он доступен, а затем оценивает токены для последующих сообщений.\n\n```typescript\nconst usage = ctx.getContextUsage();\nif (usage && usage.tokens > 100_000) {\n  // ...\n}\n```\n\n### ctx.compact()\n\nЗапустить уплотнение, не дожидаясь завершения. Используйте `onComplete` и `onError` для последующих действий.\n\n```typescript\nctx.compact({\n  customInstructions: \"Focus on recent changes\",\n  onComplete: (result) => {\n    ctx.ui.notify(\"Compaction completed\", \"info\");\n  },\n  onError: (error) => {\n    ctx.ui.notify(`Compaction failed: ${error.message}`, \"error\");\n  },\n});\n```\n\n### ctx.getSystemPrompt()\n\nВозвращает текущую строку системного приглашения Pi.\n\n- В течение `before_agent_start` это отражает цепочку изменений системных подсказок, сделанных на данный момент для текущего хода.\n- Он не включает более поздние мутации сообщения `context`.\n- Он не включает перезапись полезной нагрузки `before_provider_request`.\n- Если расширения, загруженные позже, запускаются после вашего, они все равно могут изменить то, что в конечном итоге отправляется.\n\n```typescript\npi.on(\"before_agent_start\", (event, ctx) => {\n  const prompt = ctx.getSystemPrompt();\n  console.log(`System prompt length: ${prompt.length}`);\n});\n```\n\n## РасширениеCommandContext\n\nОбработчики команд получают `ExtensionCommandContext`, который расширяет `ExtensionContext` методами управления сеансом. Они доступны только в командах, поскольку могут вызвать взаимоблокировку при вызове из обработчиков событий.\n\n### ctx.getSystemPromptOptions()\n\nВозвращает базовые входные данные Pi, которые в настоящее время используются для создания системного приглашения.\n\n```typescript\nconst options = ctx.getSystemPromptOptions();\nconst contextPaths = options.contextFiles?.map((file) => file.path) ?? [];\n```\n\nОн имеет ту же форму и изменчивость, что и `before_agent_start` `event.systemPromptOptions`: пользовательское приглашение, активные инструменты, фрагменты инструментов, инструкции по подсказкам, добавленный текст системного приглашения, cwd, загруженные context files и загруженные навыки. Он может включать полное содержимое файла контекста, поэтому относитесь к нему как к конфиденциальным локальным данным расширения и избегайте раскрытия его через списки команд, журналы или метаданные автозаполнения.\n\nЭто сообщает о текущих вводимых базовых подсказках. Он не включает в себя `before_agent_start` связанные изменения системных подсказок за ход, более поздние `context` мутации сообщений о событиях или `before_provider_request` перезапись полезной нагрузки.\n\n### ctx.waitForIdle()\n\nПодождите, пока агент полностью рассчитается, включая автоматические повторы, повторы автоматического сжатия и продолжения в очереди:\n\n```typescript\npi.registerCommand(\"my-cmd\", {\n  handler: async (args, ctx) => {\n    await ctx.waitForIdle();\n    // Agent is now idle, safe to modify session\n  },\n});\n```\n\n### ctx.newSession(варианты?)\n\nСоздайте новый сеанс:\n\n```typescript\nconst parentSession = ctx.sessionManager.getSessionFile();\nconst kickoff = \"Continue in the replacement session\";\n\nconst result = await ctx.newSession({\n  parentSession,\n  setup: async (sm) => {\n    sm.appendMessage({\n      role: \"user\",\n      content: [{ type: \"text\", text: \"Context from previous session...\" }],\n      timestamp: Date.now(),\n    });\n  },\n  withSession: async (ctx) => {\n    // Use only the replacement-session ctx here.\n    await ctx.sendUserMessage(kickoff);\n  },\n});\n\nif (result.cancelled) {\n  // An extension cancelled the new session\n}\n```\n\nПараметры:\n- `parentSession`: файл родительского сеанса для записи в новый заголовок сеанса.\n- `setup`: изменить `SessionManager` нового сеанса перед запуском `withSession`\n- `withSession`: запустить работу после переключения в новом контексте сеанса замены. Не используйте захваченную старую команду `pi` / `ctx`; см. [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns).\n\n### ctx.fork(entryId, параметры?)\n\nФорк из определенной записи, создавая новый файл сеанса:\n\n```typescript\nconst result = await ctx.fork(\"entry-id-123\", {\n  withSession: async (ctx) => {\n    // Use only the replacement-session ctx here.\n    ctx.ui.notify(\"Now in the forked session\", \"info\");\n  },\n});\nif (result.cancelled) {\n  // An extension cancelled the fork\n}\n\nconst cloneResult = await ctx.fork(\"entry-id-456\", { position: \"at\" });\nif (cloneResult.cancelled) {\n  // An extension cancelled the clone\n}\n```\n\nПараметры:\n- `position`: `\"before\"` (по умолчанию) разветвляется перед выбранным сообщением пользователя, восстанавливая это приглашение в редакторе.\n- `position`: `\"at\"` дублирует активный путь через выбранную запись без восстановления текста редактора.\n- `withSession`: запустить работу после переключения в новом контексте сеанса замены. Не используйте захваченную старую команду `pi` / `ctx`; см. [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns).\n\n### ctx.navigateTree(targetId, параметры?)\n\nПерейдите в другую точку session tree:\n\n```typescript\nconst result = await ctx.navigateTree(\"entry-id-456\", {\n  summarize: true,\n  customInstructions: \"Focus on error handling changes\",\n  replaceInstructions: false, // true = replace default prompt entirely\n  label: \"review-checkpoint\",\n});\n```\n\nПараметры:\n- `summarize`: Создавать ли сводку заброшенной ветки.\n- `customInstructions`: Пользовательские инструкции для сумматора.\n- `replaceInstructions`: Если это правда, `customInstructions` заменяет приглашение по умолчанию, а не добавляется.\n- `label`: Метка для прикрепления к сводной записи ветки (или целевой записи, если не суммируется)\n\n### ctx.switchSession(sessionPath, параметры?)\n\nПереключитесь на другой файл сеанса:\n\n```typescript\nconst result = await ctx.switchSession(\"/path/to/session.jsonl\", {\n  withSession: async (ctx) => {\n    await ctx.sendUserMessage(\"Resume work in the replacement session\");\n  },\n});\nif (result.cancelled) {\n  // An extension cancelled the switch via session_before_switch\n}\n```\n\nПараметры:\n- `withSession`: запустить работу после переключения в новом контексте сеанса замены. Не используйте захваченную старую команду `pi` / `ctx`; см. [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns).\n\nЧтобы обнаружить доступные сеансы, используйте статические методы `SessionManager.list()` или `SessionManager.listAll()`:\n\n```typescript\nimport { SessionManager } from \"@earendil-works/pi-coding-agent\";\n\npi.registerCommand(\"switch\", {\n  description: \"Switch to another session\",\n  handler: async (args, ctx) => {\n    const sessions = await SessionManager.list(ctx.cwd);\n    if (sessions.length === 0) return;\n    const choice = await ctx.ui.select(\n      \"Pick session:\",\n      sessions.map(s => s.file),\n    );\n    if (choice) {\n      await ctx.switchSession(choice, {\n        withSession: async (ctx) => {\n          ctx.ui.notify(\"Switched session\", \"info\");\n        },\n      });\n    }\n  },\n});\n```\n\n### Жизненный цикл замены сеанса и ножные пистолеты\n\n`withSession` получает новый `ReplacedSessionContext`, который расширяет `ExtensionCommandContext` асинхронными помощниками `sendMessage()` и `sendUserMessage()`, привязанными к сеансу замены.\n\nЖизненный цикл и ножи:\n- `withSession` запускается только после того, как старый сеанс выдал `session_shutdown`, старая среда выполнения была удалена, заменяющий сеанс был восстановлен, а новый экземпляр расширения уже получил `session_start`.\n- Обратный вызов по-прежнему выполняется в исходном замыкании, а не внутри нового экземпляра расширения. Это означает, что ваш старый экземпляр расширения, возможно, уже выполнил очистку после завершения работы до запуска `withSession`.\n- Захваченные старые объекты `pi`/старой команды `ctx`, привязанные к сеансу, устарели после замены и будут выброшены, если они используются. Используйте только `ctx`, переданный в `withSession` для работы с привязкой к сеансу.\n- Ранее извлеченные необработанные объекты по-прежнему остаются под вашей ответственностью. Например, если вы захватите `const sm = ctx.sessionManager` перед заменой, `sm` по-прежнему будет старым объектом `SessionManager`. Не используйте его повторно после замены.\n- Код в `withSession` должен предполагать, что любое состояние, признанное недействительным вашим обработчиком `session_shutdown`, уже исчезло. Собирайте только простые данные, которые без проблем выдерживают завершение работы, например строки, идентификаторы и сериализованную конфигурацию.\n\nБезопасный шаблон:\n\n```typescript\npi.registerCommand(\"handoff\", {\n  handler: async (_args, ctx) => {\n    const kickoff = \"Continue from the replacement session\";\n    await ctx.newSession({\n      withSession: async (ctx) => {\n        await ctx.sendUserMessage(kickoff);\n      },\n    });\n  },\n});\n```\n\nНебезопасный шаблон:\n\n```typescript\npi.registerCommand(\"handoff\", {\n  handler: async (_args, ctx) => {\n    const oldSessionManager = ctx.sessionManager;\n    await ctx.newSession({\n      withSession: async (_ctx) => {\n        // stale old objects: do not do this\n        oldSessionManager.getSessionFile();\n        pi.sendUserMessage(\"wrong\");\n      },\n    });\n  },\n});\n```\n\n### ctx.reload()\n\nЗапустите тот же процесс перезагрузки, что и `/reload`.\n\n```typescript\npi.registerCommand(\"reload-runtime\", {\n  description: \"Reload extensions, skills, prompts, themes, and context files\",\n  handler: async (_args, ctx) => {\n    await ctx.reload();\n    return;\n  },\n});\n```\n\nВажное поведение:\n- `await ctx.reload()` выдает `session_shutdown` для текущей среды выполнения расширения.\n- Затем он перезагружает ресурсы и выдает `session_start` с `reason: \"reload\"` и `resources_discover` с причиной `\"reload\"`.\n- Текущий обработчик команд продолжает работать в старом кадре вызова.\n- Код после `await ctx.reload()` по-прежнему работает из версии до перезагрузки.\n- Код после `await ctx.reload()` не должен предполагать, что старое состояние расширения в памяти все еще действительно.\n- После возврата обработчика будущие команды/события/вызовы инструментов будут использовать новую версию расширения.\n\nДля обеспечения предсказуемого поведения рассматривайте перезагрузку как терминал для этого обработчика (`await ctx.reload(); return;`).\n\nИнструменты запускаются с `ExtensionContext`, поэтому они не могут напрямую вызывать `ctx.reload()`. Используйте команду в качестве точки входа перезагрузки, а затем предоставьте инструмент, который ставит эту команду в очередь в качестве последующего сообщения пользователя.\n\nПример инструмента, который LLM может вызвать для запуска перезагрузки:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerCommand(\"reload-runtime\", {\n    description: \"Reload extensions, skills, prompts, themes, and context files\",\n    handler: async (_args, ctx) => {\n      await ctx.reload();\n      return;\n    },\n  });\n\n  pi.registerTool({\n    name: \"reload_runtime\",\n    label: \"Reload Runtime\",\n    description: \"Reload extensions, skills, prompts, themes, and context files\",\n    parameters: Type.Object({}),\n    async execute() {\n      pi.sendUserMessage(\"/reload-runtime\", { deliverAs: \"followUp\" });\n      return {\n        content: [{ type: \"text\", text: \"Queued /reload-runtime as a follow-up command.\" }],\n      };\n    },\n  });\n}\n```\n\n## РасширениеAPI Методы\n\n### pi.on(событие, обработчик)\n\nПодписывайтесь на события. См. [Events](#events) для типов событий и возвращаемых значений.\n\n### pi.registerTool (определение)\n\nЗарегистрируйте собственный инструмент, вызываемый LLM. Подробную информацию см. [Custom Tools](#custom-tools).\n\n`pi.registerTool()` работает как во время загрузки расширения, так и после запуска. Вы можете вызвать его внутри `session_start`, обработчиков команд или других обработчиков событий. Новые инструменты обновляются немедленно в том же сеансе, поэтому они появляются в `pi.getAllTools()` и могут быть вызваны из LLM без `/reload`.\n\nИспользуйте `pi.setActiveTools()`, чтобы включить или отключить инструменты (включая динамически добавляемые инструменты) во время выполнения.\n\nИспользуйте `promptSnippet`, чтобы включить пользовательский инструмент в однострочную запись в `Available tools`, и `promptGuidelines`, чтобы добавить маркеры, специфичные для инструмента, в раздел `Guidelines` по умолчанию, когда инструмент активен.\n\n**Важно!** Маркеры `promptGuidelines` добавляются в раздел `Guidelines` ровно, без префикса имени инструмента. В каждом руководстве должен быть указан инструмент, к которому он относится. Избегайте фразы «Используйте этот инструмент, когда...», поскольку LLM не может определить, какой инструмент означает «это». Вместо этого напишите «Использовать my_tool, когда...».\n\nПолный пример см. в [dynamic-tools.ts](../examples/extensions/dynamic-tools.ts).\n\n```typescript\nimport { Type } from \"typebox\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\n\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"What this tool does\",\n  promptSnippet: \"Summarize or transform text according to action\",\n  promptGuidelines: [\"Use my_tool when the user asks to summarize previously generated text.\"],\n  parameters: Type.Object({\n    action: StringEnum([\"list\", \"add\"] as const),\n    text: Type.Optional(Type.String()),\n  }),\n  prepareArguments(args) {\n    // Optional compatibility shim. Runs before schema validation.\n    // Return the current schema shape, for example to fold legacy fields\n    // into the modern parameter object.\n    return args;\n  },\n\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // Stream progress\n    onUpdate?.({ content: [{ type: \"text\", text: \"Working...\" }] });\n\n    return {\n      content: [{ type: \"text\", text: \"Done\" }],\n      details: { result: \"...\" },\n    };\n  },\n\n  // Optional: Custom rendering\n  renderCall(args, theme, context) { ... },\n  renderResult(result, options, theme, context) { ... },\n});\n```\n\n### pi.sendMessage(сообщение, параметры?)\n\nВнедрить пользовательское сообщение в сеанс. Пользовательские сообщения участвуют в контексте LLM. Для постоянного контента, содержащего только TUI, который не следует отправлять в LLM, используйте [`pi.appendEntry()`](#piappendentrycustomtype-data) с [`pi.registerEntryRenderer()`](#piregisterentryrenderercustomtype-renderer).\n\n```typescript\npi.sendMessage({\n  customType: \"my-extension\",\n  content: \"Message text\",\n  display: true,\n  details: { ... },\n}, {\n  triggerTurn: true,\n  deliverAs: \"steer\",\n});\n```\n\n**Параметры:**\n- `deliverAs` - Режим доставки:\n  - `\"steer\"` (по умолчанию) — ставит сообщение в очередь во время потоковой передачи. Доставляется после того, как текущий ход помощника завершает выполнение вызовов инструментов, до следующего вызова LLM.\n  - `\"followUp\"` — Ожидает завершения работы агента. Доставляется только тогда, когда у агента больше нет вызовов инструментов.\n  - `\"nextTurn\"` — В очереди на приглашение следующего пользователя. Ничего не прерывает и не запускает.\n- `triggerTurn: true` — если агент простаивает, немедленно вызвать ответ LLM. Применяется только к режимам `\"steer\"` и `\"followUp\"` (игнорируется для `\"nextTurn\"`).\n\n### pi.sendUserMessage(содержание, параметры?)\n\nОтправьте пользовательское сообщение агенту. В отличие от `sendMessage()`, который отправляет пользовательские сообщения, здесь отправляется фактическое пользовательское сообщение, которое выглядит так, как будто оно напечатано пользователем. Всегда вызывает поворот.\n\n```typescript\n// Simple text message\npi.sendUserMessage(\"What is 2+2?\");\n\n// With content array (text + images)\npi.sendUserMessage([\n  { type: \"text\", text: \"Describe this image:\" },\n  { type: \"image\", source: { type: \"base64\", mediaType: \"image/png\", data: \"...\" } },\n]);\n\n// During streaming - must specify delivery mode\npi.sendUserMessage(\"Focus on error handling\", { deliverAs: \"steer\" });\npi.sendUserMessage(\"And then summarize\", { deliverAs: \"followUp\" });\n```\n\n**Параметры:**\n- `deliverAs` — Требуется, когда агент ведет потоковую передачу:\n  - `\"steer\"` — ставит сообщение в очередь для доставки после того, как текущий ход помощника завершит выполнение вызовов инструментов.\n  - `\"followUp\"` — ждет, пока агент завершит все инструменты.\n\nЕсли потоковая передача не ведется, сообщение отправляется немедленно и запускает новый ход. При потоковой передаче без `deliverAs` выдает ошибку.\n\nПолный пример см. в [send-user-message.ts](../examples/extensions/send-user-message.ts).\n\n### pi.appendEntry(customType, данные?)\n\nСохранение данных расширения. Пользовательские записи НЕ участвуют в контексте LLM. В интерактивном режиме они также могут отображаться внутри стенограммы чата в сочетании с `pi.registerEntryRenderer()`.\n\n```typescript\npi.appendEntry(\"my-state\", { count: 42 });\npi.appendEntry(\"status-card\", { title: \"Indexed files\", count: 17 });\n\n// Restore on reload\npi.on(\"session_start\", async (_event, ctx) => {\n  for (const entry of ctx.sessionManager.getEntries()) {\n    if (entry.type === \"custom\" && entry.customType === \"my-state\") {\n      // Reconstruct from entry.data\n    }\n  }\n});\n```\n\n### pi.setSessionName(имя)\n\nУстановите отображаемое имя сеанса (отображается в селекторе сеанса вместо первого сообщения).\n\n```typescript\npi.setSessionName(\"Refactor auth module\");\n```\n\n### pi.getSessionName()\n\nПолучите имя текущего сеанса, если оно установлено.\n\n```typescript\nconst name = pi.getSessionName();\nif (name) {\n  console.log(`Session: ${name}`);\n}\n```\n\n### pi.setLabel(entryId, метка)\n\nУстановите или очистите метку записи. Метки — это определяемые пользователем маркеры для создания закладок и навигации (показаны в селекторе `/tree`).\n\n```typescript\n// Set a label\npi.setLabel(entryId, \"checkpoint-before-refactor\");\n\n// Clear a label\npi.setLabel(entryId, undefined);\n\n// Read labels via sessionManager\nconst label = ctx.sessionManager.getLabel(entryId);\n```\n\nМетки сохраняются в течение сеанса и выдерживают перезапуск. Используйте их, чтобы отмечать важные точки (повороты, контрольные точки) в дереве разговора.\n\n### pi.registerCommand(имя, параметры)\n\nЗарегистрируйте команду.\n\nЕсли несколько расширений регистрируют одно и то же имя команды, pi сохраняет их все и назначает числовые суффиксы вызова в порядке загрузки, например `/review:1` и `/review:2`.\n\n```typescript\npi.registerCommand(\"stats\", {\n  description: \"Show session statistics\",\n  handler: async (args, ctx) => {\n    const count = ctx.sessionManager.getEntries().length;\n    ctx.ui.notify(`${count} entries`, \"info\");\n  }\n});\n```\n\nНеобязательно: добавьте автодополнение аргументов для `/command...`:\n\n```typescript\nimport type { AutocompleteItem } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"deploy\", {\n  description: \"Deploy to an environment\",\n  getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {\n    const envs = [\"dev\", \"staging\", \"prod\"];\n    const items = envs.map((e) => ({ value: e, label: e }));\n    const filtered = items.filter((i) => i.value.startsWith(prefix));\n    return filtered.length > 0 ? filtered : null;\n  },\n  handler: async (args, ctx) => {\n    ctx.ui.notify(`Deploying: ${args}`, \"info\");\n  },\n});\n```\n\n### пи.getCommands()\n\nПолучите slash commands, доступный для вызова через `prompt` в текущем сеансе. Включает команды расширения prompt templates и команды навыков.\nСписок соответствует порядку RPC `get_commands`: сначала расширения, затем шаблоны, затем навыки.\n\n```typescript\nconst commands = pi.getCommands();\nconst bySource = commands.filter((command) => command.source === \"extension\");\nconst userScoped = commands.filter((command) => command.sourceInfo.scope === \"user\");\n```\n\nКаждая запись имеет следующую форму:\n\n```typescript\n{\n  name: string; // Invokable command name without the leading slash. May be suffixed like \"review:1\"\n  description?: string;\n  source: \"extension\" | \"prompt\" | \"skill\";\n  sourceInfo: {\n    path: string;\n    source: string;\n    scope: \"user\" | \"project\" | \"temporary\";\n    origin: \"package\" | \"top-level\";\n    baseDir?: string;\n  };\n}\n```\n\nИспользуйте `sourceInfo` в качестве поля канонического происхождения. Не делайте вывод о принадлежности на основании имен команд или анализа специальных путей.\n\nВстроенные интерактивные команды (например, `/model` и `/settings`) сюда не включены. Они обрабатываются только в интерактивном режиме.\nрежиме и не будет выполняться, если будет отправлено через `prompt`.\n\n### pi.registerMessageRenderer(customType, средство визуализации)\n\nЗарегистрируйте собственный рендерер TUI для пользовательских сообщений на своем `customType`. Пользовательские сообщения создаются с помощью `pi.sendMessage()` и участвуют в контексте LLM. См. [Custom UI](#custom-ui).\n\n### pi.registerMarkdownTransformer(трансформатор)\n\nЗарегистрируйте преобразователь для Markdown в обычном пользовательском тексте, тексте помощника и блоках мышления. Трансформаторы работают в порядке расширения нагрузки, и каждый трансформатор получает Markdown, возвращенный предыдущим трансформатором. После завершения цепочки Pi визуализирует преобразованный контент с помощью встроенного средства визуализации.\n\nПреобразователь получает строку Markdown и контекст:\n\n- `messageType` — `\"user\"`, `\"assistant\"` или `\"assistant-thinking\"`\n- `isStreaming` — `true` для частичного обновления помощника; `false` для сообщений пользователя, завершенного помощника и восстановленных сообщений.\n- `availableWidth` — точные терминальные столбцы, доступные для преобразованного содержимого Markdown.\n\nВерните преобразованный Markdown:\n\n```typescript\npi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {\n  if (isStreaming || messageType === \"assistant-thinking\") return markdown;\n  return markdown.replaceAll(\"-->\", \"→\");\n});\n```\n\nЕсли преобразователь выбрасывает, Pi сохраняет созданный на данный момент Markdown и продолжает работу со следующим преобразователем. Перехват предназначен только для отображения: исходное сообщение остается неизменным в контексте сеанса и модели. Он запускается для новых пользовательских сообщений, обновлений потоковой передачи помощника, восстановленных сообщений сеанса и изменений ширины терминала, поэтому преобразователи должны оставаться синхронными и недорогими.\n\n### pi.registerEntryRenderer(customType, средство визуализации)\n\nЗарегистрируйте пользовательский рендерер TUI для пользовательских записей с помощью `customType`. Пользовательские записи создаются с помощью `pi.appendEntry()` и не участвуют в контексте LLM.\n\n```typescript\nimport { Box, Text } from \"@earendil-works/pi-tui\";\n\npi.registerEntryRenderer(\"status-card\", (entry, { expanded }, theme) => {\n  const data = entry.data as { title: string; count: number };\n  const box = new Box(1, 1, (text) => theme.bg(\"customMessageBg\", text));\n  box.addChild(new Text(`${theme.bold(data.title)}: ${data.count}`));\n  if (expanded) {\n    box.addChild(new Text(theme.fg(\"dim\", JSON.stringify(data, null, 2))));\n  }\n  return box;\n});\n\npi.appendEntry(\"status-card\", { title: \"Indexed files\", count: 17 });\n```\n\n### pi.registerShortcut(ярлык, параметры)\n\nЗарегистрируйте сочетание клавиш. См. [keybindings.md](keybindings.md), чтобы узнать о формате ярлыков и встроенных сочетаниях клавиш.\n\n```typescript\npi.registerShortcut(\"ctrl+shift+p\", {\n  description: \"Toggle plan mode\",\n  handler: async (ctx) => {\n    ctx.ui.notify(\"Toggled!\");\n  },\n});\n```\n\n### pi.registerFlag(имя, параметры)\n\nЗарегистрируйте флаг CLI.\n\n```typescript\npi.registerFlag(\"plan\", {\n  description: \"Start in plan mode\",\n  type: \"boolean\",\n  default: false,\n});\n\n// Check value\nif (pi.getFlag(\"plan\")) {\n  // Plan mode enabled\n}\n```\n\n### pi.exec(команда, аргументы, параметры?)\n\nВыполните команду оболочки.\n\n```typescript\nconst result = await pi.exec(\"git\", [\"status\"], { signal, timeout: 5000 });\n// result.stdout, result.stderr, result.code, result.killed\n```\n\n### pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(имена)\n\nУправляйте активными инструментами. Это работает как для встроенных инструментов, так и для динамически зарегистрированных инструментов. `pi.getActiveTools()` возвращает имена активных инструментов как `string[]`; `pi.getAllTools()` возвращает метаданные для всех настроенных инструментов.\n\n```typescript\nconst active = pi.getActiveTools(); // [\"read\", \"bash\", ...]\nconst all = pi.getAllTools();\n// all = [{\n//   name: \"read\",\n//   description: \"Read file contents...\",\n//   parameters: ...,\n//   promptGuidelines: [\"Use read to examine files instead of cat or sed.\"],\n//   sourceInfo: { path: \"<builtin:read>\", source: \"builtin\", scope: \"temporary\", origin: \"top-level\" }\n// }, ...]\nconst builtinTools = all.filter((t) => t.sourceInfo.source === \"builtin\");\nconst extensionTools = all.filter((t) => t.sourceInfo.source !== \"builtin\" && t.sourceInfo.source !== \"sdk\");\npi.setActiveTools([...new Set([...active, \"my_custom_tool\"])]); // Keep current tools and enable my_custom_tool\npi.setActiveTools([\"read\", \"bash\"]); // Switch to read-only\n```\n\n`pi.getAllTools()` возвращает `name`, `description`, `parameters`, `promptGuidelines` и `sourceInfo`.\n\nТипичные значения `sourceInfo.source`:\n- `builtin` для встроенных инструментов\n- `sdk` для инструментов, прошедших через `createAgentSession({ customTools })`\n- метаданные источника расширения для инструментов, зарегистрированных расширениями\n\n### pi.setModel(модель)\n\nУстановите текущую модель. Возвращает `false`, если для модели нет API key. См. [models.md](models.md) для настройки пользовательских моделей.\n\n```typescript\nconst model = ctx.modelRegistry.find(\"anthropic\", \"claude-sonnet-4-5\");\nif (model) {\n  const success = await pi.setModel(model);\n  if (!success) {\n    ctx.ui.notify(\"No API key for this model\", \"error\");\n  }\n}\n```\n\n### pi.getThinkingLevel()/pi.setThinkingLevel(уровень)\n\nПолучите или установите уровень мышления. Уровень привязан к возможностям модели (в моделях без рассуждений всегда используется значение «выкл.»). Изменения излучают `thinking_level_select`.\n\n```typescript\nconst current = pi.getThinkingLevel();  // \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\"\npi.setThinkingLevel(\"high\");\n```\n\n### pi.events\n\nОбщая шина событий для связи между расширениями:\n\n```typescript\npi.events.on(\"my:event\", (data) => { ... });\npi.events.emit(\"my:event\", { ... });\n```\n\n### pi.registerProvider(имя, конфигурация)\n\nЗарегистрируйте или переопределите поставщика модели динамически. Полезно для прокси, пользовательских конечных точек или конфигураций модели для всей команды.\n\nВызовы, сделанные во время функции фабрики расширений, ставятся в очередь и применяются после инициализации бегуна. Вызовы, сделанные после этого — например, из обработчика команд после потока настройки пользователя — вступают в силу немедленно, не требуя `/reload`.\n\nДинамические поставщики могут реализовать `refreshModels`. Pi вызывает его во время обновления модели, синхронно публикует возвращенный список через поставщика и передает контекст канонических учетных данных/сохраненного каталога/сети/сигнала. Расширение решает, сохранять ли метаданные каталога через проверку генерации `context.publish({ persist: entry })`; живые серверы, такие как llama.cpp, могут возвращать модели, не сохраняя их.\n\n`context.signal` всегда является конкретным сигналом, и обратные вызовы провайдера должны передать его для блокировки ввода-вывода. Публичные вызовы `ModelRuntime.refresh()` и `ModelRegistry.refresh()` принимают необязательный сигнал и не ограничиваются, если он опущен; расширения и приложения сами выбирают сроки. Отмена останавливает ожидание вызывающего абонента, даже если провайдер игнорирует сигнал, но сотрудничество все равно необходимо, чтобы остановить основную работу.\n\nExtensions, которым требуется встроенная аутентификация поставщика, фильтрация, обновление или потоковая передача, могут зарегистрировать полный `Provider` из `@earendil-works/pi-ai`. Поставщик становится базой композиции, и над ним по-прежнему применяются переопределения `models.json`.\n\n```typescript\nimport { createProvider, openAICompletionsApi } from \"@earendil-works/pi-ai\";\n\nconst provider = createProvider({\n  id: \"local-server\",\n  name: \"Local Server\",\n  baseUrl: \"http://localhost:8080/v1\",\n  auth: {\n    apiKey: {\n      name: \"Local server setup\",\n      async login(interaction) {\n        return {\n          type: \"api_key\",\n          key: await interaction.prompt({ type: \"secret\", message: \"API key\" }),\n        };\n      },\n      async resolve({ credential }) {\n        return credential?.key\n          ? { auth: { apiKey: credential.key }, source: \"stored API key\" }\n          : undefined;\n      },\n    },\n  },\n  models: [],\n  api: openAICompletionsApi(),\n});\n\npi.registerProvider(provider);\n\n// Register a new provider with custom models\npi.registerProvider(\"my-proxy\", {\n  name: \"My Proxy\",\n  baseUrl: \"https://proxy.example.com\",\n  apiKey: \"$PROXY_API_KEY\",  // env var reference\n  api: \"anthropic-messages\",\n  models: [\n    {\n      id: \"claude-sonnet-4-20250514\",\n      name: \"Claude 4 Sonnet (proxy)\",\n      reasoning: false,\n      input: [\"text\", \"image\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n\n// Register a live llama.cpp catalog without persisting discovered models\npi.registerProvider(\"llama.cpp\", {\n  baseUrl: \"http://localhost:8080/v1\",\n  apiKey: \"local\",\n  api: \"openai-completions\",\n  async refreshModels({ signal }) {\n    const response = await fetch(\"http://localhost:8080/v1/models\", { signal });\n    const { data } = await response.json();\n    return data.map(({ id }) => ({\n      id,\n      name: id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 128000,\n      maxTokens: 16384\n    }));\n  }\n});\n\n// Override baseUrl for an existing provider (keeps all models)\npi.registerProvider(\"anthropic\", {\n  baseUrl: \"https://proxy.example.com\"\n});\n\n// Register provider with OAuth support for /login\npi.registerProvider(\"corporate-ai\", {\n  baseUrl: \"https://ai.corp.com\",\n  api: \"openai-responses\",\n  models: [...],\n  oauth: {\n    name: \"Corporate AI (SSO)\",\n    async login(callbacks) {\n      // Custom OAuth flow\n      callbacks.onAuth({ url: \"https://sso.corp.com/...\" });\n      const code = await callbacks.onPrompt({ message: \"Enter code:\" });\n      return { refresh: code, access: code, expires: Date.now() + 3600000 };\n    },\n    async refreshToken(credentials, signal) {\n      signal.throwIfAborted();\n      // Refresh logic\n      return credentials;\n    },\n    getApiKey(credentials) {\n      return credentials.access;\n    }\n  }\n});\n```\n\nФорма объекта принимает полное пи-ай `Provider`, включая собственное поведение `auth`, `getModels`, `refreshModels`, `filterModels`, `stream` и `streamSimple`.\n\n**Устаревшие параметры конфигурации:**\n- `name` — отображаемое имя поставщика в пользовательском интерфейсе, например `/login`.\n- `baseUrl` – API URL-адрес конечной точки. Требуется при определении моделей.\n- `apiKey` - API key буквальный, интерполяция среды (`$ENV_VAR` или `${ENV_VAR}`) или ведущий `!command`. Требуется при определении моделей (если не указано `oauth`). `$` экранирует ``apiKey` - API key буквальный, интерполяция среды (`$ENV_VAR` или `${ENV_VAR}`) или ведущий `!command`. Требуется при определении моделей (если не указано `oauth`). `$` экранирует, а `$!` экранирует литерал `!`, не запуская выполнение команды.\n- Тип `api` - API: `\"anthropic-messages\"`, `\"openai-completions\"`, `\"openai-responses\"` и т. д.\n- `headers` — Пользовательские заголовки для включения в запросы.\n- `authHeader` — если true, автоматически добавляет заголовок `Authorization: Bearer`.\n- `models` — Массив определений модели. Если предусмотрено, заменяет все существующие модели этого поставщика. В определениях моделей можно установить `baseUrl`, чтобы переопределить конечную точку поставщика для этой модели.\n- `refreshModels` — обратный вызов асинхронного динамического обнаружения. Возвращенные модели заменяют модели, предоставленные расширениями. `context.stored` содержит сохраненный снимок поставщика; используйте `context.publish({ persist: entry })` с проверкой генерации только в том случае, если обновленные данные каталога должны сохраниться. Используйте `persist: null`, чтобы удалить этот снимок.\n- Конфигурация провайдера `oauth` - OAuth для поддержки `/login`. Если этот параметр предоставлен, поставщик появится в меню входа в систему.\n- `streamSimple` — Пользовательская реализация потоковой передачи для нестандартных API.\n\nСм. [custom-provider.md](custom-provider.md) для более сложных тем: пользовательская потоковая передача APIs, OAuth подробности, справочник по определению модели.\n\n### pi.unregisterProvider(имя)\n\nУдалить ранее зарегистрированного провайдера и его модели. Встроенные модели, которые были переопределены поставщиком, восстанавливаются. Не имеет эффекта, если провайдер не был зарегистрирован.\n\nКак и `registerProvider`, это вступает в силу немедленно при вызове после начальной фазы загрузки, поэтому `/reload` не требуется.\n\n```typescript\npi.registerCommand(\"my-setup-teardown\", {\n  description: \"Remove the custom proxy provider\",\n  handler: async (_args, _ctx) => {\n    pi.unregisterProvider(\"my-proxy\");\n  },\n});\n```\n\n## Государственное управление\n\nExtensions с состоянием следует сохранить его в результате инструмента `details` для правильной поддержки ветвления:\n\n```typescript\nexport default function (pi: ExtensionAPI) {\n  let items: string[] = [];\n\n  // Reconstruct state from session\n  pi.on(\"session_start\", async (_event, ctx) => {\n    items = [];\n    for (const entry of ctx.sessionManager.getBranch()) {\n      if (entry.type === \"message\" && entry.message.role === \"toolResult\") {\n        if (entry.message.toolName === \"my_tool\") {\n          items = entry.message.details?.items ?? [];\n        }\n      }\n    }\n  });\n\n  pi.registerTool({\n    name: \"my_tool\",\n    // ...\n    async execute(toolCallId, params, signal, onUpdate, ctx) {\n      items.push(\"new item\");\n      return {\n        content: [{ type: \"text\", text: \"Added\" }],\n        details: { items: [...items] },  // Store for reconstruction\n      };\n    },\n  });\n}\n```\n\n## Пользовательские инструменты\n\nЗарегистрируйте инструменты, которые LLM может вызывать через `pi.registerTool()`. Инструменты отображаются в системной подсказке и могут иметь собственную визуализацию.\n\nИспользуйте `promptSnippet` для короткой однострочной записи в разделе `Available tools` системной подсказки по умолчанию. Если этот параметр опущен, пользовательские инструменты не попадают в этот раздел.\n\nИспользуйте `promptGuidelines`, чтобы добавить маркеры для конкретного инструмента в раздел системной подсказки по умолчанию `Guidelines`. Эти маркеры включаются только тогда, когда инструмент активен (например, после `pi.setActiveTools([...])`).\n\n**Важно!** Маркеры `promptGuidelines` добавляются в раздел `Guidelines` ровно, без префикса имени инструмента или группировки. В каждом руководстве должен быть указан инструмент, к которому он относится. Избегайте фразы «Используйте этот инструмент, когда...», поскольку LLM не может определить, какой инструмент означает «это». Вместо этого напишите «Использовать my_tool, когда...».\n\nПримечание. Некоторые модели являются идиотами и включают префикс @ в аргументы пути к инструменту. Встроенные инструменты удаляют начальный символ @ перед разрешением путей. Если ваш пользовательский инструмент принимает путь, нормализуйте также начальный символ @.\n\nЕсли ваш пользовательский инструмент изменяет файлы, используйте `withFileMutationQueue()`, чтобы он участвовал в той же очереди для каждого файла, что и встроенные `edit` и `write`. Это важно, поскольку вызовы инструментов по умолчанию выполняются параллельно. Без очереди два инструмента могут читать одно и то же старое содержимое файла, вычислять разные обновления, а затем в зависимости от того, какая запись произошла последней, перезаписывает другую.\n\nПример случая сбоя: ваш пользовательский инструмент редактирует `foo.ts`, а встроенный `edit` также изменяет `foo.ts` за один и тот же ход помощника. Если ваш инструмент не участвует в очереди, оба могут прочитать оригинал `foo.ts`, применить отдельные изменения, и одно из этих изменений будет потеряно.\n\nПередайте реальный путь к целевому файлу в `withFileMutationQueue()`, а не необработанный аргумент пользователя. Сначала разрешите его в абсолютный путь относительно `ctx.cwd` или рабочего каталога вашего инструмента. Для существующих файлов помощник канонизируется через `realpath()`, поэтому псевдонимы символических ссылок для одного и того же файла используют одну очередь. Для новых файлов используется разрешенный абсолютный путь, поскольку в `realpath()` пока ничего нет.\n\nПоставьте в очередь все окно мутации на этом целевом пути. Это включает в себя логику чтения-изменения-записи, а не только окончательную запись.\n\n```typescript\nimport { withFileMutationQueue } from \"@earendil-works/pi-coding-agent\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\n\nasync execute(_toolCallId, params, _signal, _onUpdate, ctx) {\n  const absolutePath = resolve(ctx.cwd, params.path);\n\n  return withFileMutationQueue(absolutePath, async () => {\n    await mkdir(dirname(absolutePath), { recursive: true });\n    const current = await readFile(absolutePath, \"utf8\");\n    const next = current.replace(params.oldText, params.newText);\n    await writeFile(absolutePath, next, \"utf8\");\n\n    return {\n      content: [{ type: \"text\", text: `Updated ${params.path}` }],\n      details: {},\n    };\n  });\n}\n```\n\n### Определение инструмента\n\n```typescript\nimport { Type } from \"typebox\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\nimport { Text } from \"@earendil-works/pi-tui\";\n\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"What this tool does (shown to LLM)\",\n  promptSnippet: \"List or add items in the project todo list\",\n  promptGuidelines: [\n    \"Use my_tool for todo planning instead of direct file edits when the user asks for a task list.\"\n  ],\n  parameters: Type.Object({\n    action: StringEnum([\"list\", \"add\"] as const),  // Use StringEnum for Google compatibility\n    text: Type.Optional(Type.String()),\n  }),\n  prepareArguments(args) {\n    if (!args || typeof args !== \"object\") return args;\n    const input = args as { action?: string; oldAction?: string };\n    if (typeof input.oldAction === \"string\" && input.action === undefined) {\n      return { ...input, action: input.oldAction };\n    }\n    return args;\n  },\n\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // Check for cancellation\n    if (signal?.aborted) {\n      return { content: [{ type: \"text\", text: \"Cancelled\" }] };\n    }\n\n    // Stream progress updates\n    onUpdate?.({\n      content: [{ type: \"text\", text: \"Working...\" }],\n      details: { progress: 50 },\n    });\n\n    // Run commands via pi.exec (captured from extension closure)\n    const result = await pi.exec(\"some-command\", [], { signal });\n\n    // Return result\n    return {\n      content: [{ type: \"text\", text: \"Done\" }],  // Sent to LLM\n      details: { data: result },                   // For rendering & state\n      // usage: nestedModelResponse.usage,          // Optional nested LLM usage\n      // Optional: stop after this tool batch when every finalized tool result\n      // in the batch also returns terminate: true.\n      terminate: true,\n    };\n  },\n\n  // Optional: Custom rendering\n  renderCall(args, theme, context) { ... },\n  renderResult(result, options, theme, context) { ... },\n});\n```\n\n**Учет использования.** Если инструмент выполняет вложенные вызовы LLM, верните их совокупный `Usage` как `usage`. Pi сохраняет его в результатах инструмента и включает в нижний колонтитул, `/session` и RPC итоговые данные сеанса. `tool_result` обработчики могут проверять или заменять это значение.\n\n**Сигнализация ошибок:** Чтобы пометить выполнение инструмента как неудачное (устанавливает `isError: true` для результата и сообщает об этом в LLM), выдайте ошибку из `execute`. При возврате значения никогда не устанавливается флаг ошибки, независимо от того, какие свойства вы включаете в возвращаемый объект.\n\n**Досрочное прекращение:** Возврат `terminate: true` из `execute()`, чтобы указать, что автоматический последующий вызов LLM следует пропустить после текущей партии инструментов. Это вступает в силу только тогда, когда каждый завершенный результат инструмента в этом пакете завершается. См. [examples/extensions/structured-output.ts](../examples/extensions/structured-output.ts) минимальный пример, когда агент завершает работу при последнем вызове инструмента структурированного вывода.\n\n```typescript\n// Correct: throw to signal an error\nasync execute(toolCallId, params) {\n  if (!isValid(params.input)) {\n    throw new Error(`Invalid input: ${params.input}`);\n  }\n  return { content: [{ type: \"text\", text: \"OK\" }], details: {} };\n}\n```\n\n**Важно!** Используйте `StringEnum` из `@earendil-works/pi-ai` для перечисления строк. `Type.Union`/`Type.Literal` не работает с API от Google.\n\n**Подготовка аргумента:** `prepareArguments(args)` не является обязательным. Если определено, оно выполняется до проверки схемы и до `execute()`. Используйте его, чтобы имитировать более старую принятую форму ввода, когда pi возобновляет старый сеанс, чьи сохраненные аргументы вызова инструмента больше не соответствуют текущей схеме. Верните объект, который вы хотите проверить на соответствие `parameters`. Соблюдайте строгую публичную схему. Не добавляйте устаревшие поля совместимости в `parameters` только для того, чтобы старые возобновленные сеансы работали.\n\nПример: более старый сеанс может содержать вызов инструмента `edit` с `oldText` и `newText` верхнего уровня, в то время как текущая схема принимает только `edits: [{ oldText, newText }]`.\n\n```typescript\npi.registerTool({\n  name: \"edit\",\n  label: \"Edit\",\n  description: \"Edit a single file using exact text replacement\",\n  parameters: Type.Object({\n    path: Type.String(),\n    edits: Type.Array(\n      Type.Object({\n        oldText: Type.String(),\n        newText: Type.String(),\n      }),\n    ),\n  }),\n  prepareArguments(args) {\n    if (!args || typeof args !== \"object\") return args;\n\n    const input = args as {\n      path?: string;\n      edits?: Array<{ oldText: string; newText: string }>;\n      oldText?: unknown;\n      newText?: unknown;\n    };\n\n    if (typeof input.oldText !== \"string\" || typeof input.newText !== \"string\") {\n      return args;\n    }\n\n    return {\n      ...input,\n      edits: [...(input.edits ?? []), { oldText: input.oldText, newText: input.newText }],\n    };\n  },\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // params now matches the current schema\n    return {\n      content: [{ type: \"text\", text: `Applying ${params.edits.length} edit block(s)` }],\n      details: {},\n    };\n  },\n});\n```\n\n### Переопределение встроенных инструментов\n\nExtensions может переопределить встроенные инструменты (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`), зарегистрировав инструмент с тем же именем. В интерактивном режиме отображается предупреждение, когда это происходит.\n\n```bash\n# Extension's read tool replaces built-in read\npi -e ./tool-override.ts\n```\n\nАльтернативно, используйте `--no-builtin-tools`, чтобы начать без каких-либо встроенных инструментов, оставив при этом инструменты расширения включенными:\n```bash\n# No built-in tools, only extension tools\npi --no-builtin-tools -e ./my-extension.ts\n```\n\nСм. [examples/extensions/tool-override.ts](../examples/extensions/tool-override.ts) полный пример, который переопределяет `read` с помощью ведения журнала и контроля доступа.\n\n**Рендеринг.** Наследование встроенного средства рендеринга осуществляется для каждого слота. Переопределение выполнения и переопределение рендеринга независимы. Если в вашем переопределении отсутствует `renderCall`, используется встроенный `renderCall`. Если в вашем переопределении отсутствует `renderResult`, используется встроенный `renderResult`. Если в вашем переопределении оба параметра отсутствуют, автоматически используется встроенный модуль визуализации (подсветка синтаксиса, различия и т. д.). Это позволяет использовать встроенные инструменты для ведения журналов или контроля доступа без переопределения пользовательского интерфейса.\n\n**Метаданные подсказки:** `promptSnippet` и `promptGuidelines` не наследуются от встроенного инструмента. Если ваше переопределение должно сохранять эти подсказки, определите их в переопределении явно.\n\n**Ваша реализация должна точно соответствовать форме результата**, включая тип `details`. Логика пользовательского интерфейса и сеанса зависит от этих фигур для рендеринга и отслеживания состояния.\n\nВстроенные реализации инструментов:\n- [read.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/read.ts) - `ReadToolDetails`\n- [bash.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/bash.ts) - `BashToolDetails`\n- [edit.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/edit.ts)\n- [write.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/write.ts)\n- [grep.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/grep.ts) - `GrepToolDetails`\n- [find.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/find.ts) - `FindToolDetails`\n- [ls.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/ls.ts) - `LsToolDetails`\n\n### Удаленное выполнение\n\nВстроенные инструменты поддерживают подключаемые операции для делегирования удаленным системам (SSH, контейнерам и т. д.):\n\n```typescript\nimport { createReadTool, createBashTool, type ReadOperations } from \"@earendil-works/pi-coding-agent\";\n\n// Create tool with custom operations\nconst remoteRead = createReadTool(cwd, {\n  operations: {\n    readFile: (path) => sshExec(remote, `cat ${path}`),\n    access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),\n  }\n});\n\n// Register, checking flag at execution time\npi.registerTool({\n  ...remoteRead,\n  async execute(id, params, signal, onUpdate, _ctx) {\n    const ssh = getSshConfig();\n    if (ssh) {\n      const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });\n      return tool.execute(id, params, signal, onUpdate);\n    }\n    return localRead.execute(id, params, signal, onUpdate);\n  },\n});\n```\n\n**Операционные интерфейсы:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations`\n\nДля `user_bash` расширения могут повторно использовать локальную серверную часть оболочки pi через `createLocalBashOperations()` вместо повторной реализации создания локальных процессов, разрешения оболочки и завершения дерева процессов.\n\nИнструмент bash также поддерживает перехватчик создания для настройки команды, cwd или env перед выполнением:\n\n```typescript\nimport { createBashTool } from \"@earendil-works/pi-coding-agent\";\n\nconst bashTool = createBashTool(cwd, {\n  spawnHook: ({ command, cwd, env }) => ({\n    command: `source ~/.profile\\n${command}`,\n    cwd: `/mnt/sandbox${cwd}`,\n    env: { ...env, CI: \"1\" },\n  }),\n});\n```\n\n`createBashTool()` предоставляет текущий сеанс командам через `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL` и `PI_REASONING_LEVEL`. Внедрение происходит до `spawnHook`, поэтому перехватчики получают эти значения в `env` и сохраняют их при распространении существующей среды, как указано выше. Установите `exposeSessionEnvironment: false`, чтобы отключить их:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n});\n```\n\nСм. [Bash tool session environment](environment-variables.md#bash-tool-session-environment) для семантики переменных. См. [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) полный пример SSH с флагом `--ssh`.\n\n### Усечение вывода\n\n**Инструменты ДОЛЖНЫ обрезать свои выходные данные**, чтобы не перегружать контекст LLM. Большие выходные данные могут вызвать:\n- Ошибки переполнения контекста (слишком длинный запрос)\n- Неудачи уплотнения\n- Ухудшение производительности модели\n\nВстроенный лимит составляет **50 КБ** (около 10 000 токенов) и **2000 строк**, в зависимости от того, что наступит раньше. Используйте экспортированные утилиты усечения:\n\n```typescript\nimport {\n  truncateHead,      // Keep first N lines/bytes (good for file reads, search results)\n  truncateTail,      // Keep last N lines/bytes (good for logs, command output)\n  truncateLine,      // Truncate a single line to maxBytes with ellipsis\n  formatSize,        // Human-readable size (e.g., \"50KB\", \"1.5MB\")\n  DEFAULT_MAX_BYTES, // 50KB\n  DEFAULT_MAX_LINES, // 2000\n} from \"@earendil-works/pi-coding-agent\";\n\nasync execute(toolCallId, params, signal, onUpdate, ctx) {\n  const output = await runCommand();\n\n  // Apply truncation\n  const truncation = truncateHead(output, {\n    maxLines: DEFAULT_MAX_LINES,\n    maxBytes: DEFAULT_MAX_BYTES,\n  });\n\n  let result = truncation.content;\n\n  if (truncation.truncated) {\n    // Write full output to temp file\n    const tempFile = writeTempFile(output);\n\n    // Inform the LLM where to find complete output\n    result += `\\n\\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;\n    result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;\n    result += ` Full output saved to: ${tempFile}]`;\n  }\n\n  return { content: [{ type: \"text\", text: result }] };\n}\n```\n\n**Ключевые моменты:**\n- Используйте `truncateHead` для контента, начало которого имеет значение (результаты поиска, чтение файлов).\n- Используйте `truncateTail` для контента, для которого важен конец (журналы, вывод команды)\n- Всегда сообщайте LLM, когда выходные данные обрезаются и где найти полную версию.\n- Задокументируйте пределы усечения в описании вашего инструмента.\n\nСм. [examples/extensions/truncated-tool.ts](../examples/extensions/truncated-tool.ts) для полного примера упаковки `rg` (ripgrep) с правильным усечением.\n\n### Несколько инструментов\n\nОдно расширение может зарегистрировать несколько инструментов с общим состоянием:\n\n```typescript\nexport default function (pi: ExtensionAPI) {\n  let connection = null;\n\n  pi.registerTool({ name: \"db_connect\", ... });\n  pi.registerTool({ name: \"db_query\", ... });\n  pi.registerTool({ name: \"db_close\", ... });\n\n  pi.on(\"session_shutdown\", async () => {\n    connection?.close();\n  });\n}\n```\n\n### Пользовательский рендеринг\n\nИнструменты могут предоставлять `renderCall` и `renderResult` для пользовательского отображения TUI. См. [tui.md](tui.md) для полного компонента, API и [tool-execution.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts) для описания того, как составляются ряды инструментов.\n\nПо умолчанию выходные данные инструмента заключаются в `Box`, который обрабатывает отступы и фон. Определенный `renderCall` или `renderResult` должен возвращать `Component`. Если средство рендеринга слота не определено, `tool-execution.ts` использует резервный рендеринг для этого слота.\n\nУстановите `renderShell: \"self\"`, когда инструмент должен отображать собственную оболочку вместо использования `Box` по умолчанию. Это полезно для инструментов, которым требуется полный контроль над кадрированием или поведением фона, например, для больших изображений предварительного просмотра, которые должны оставаться визуально стабильными после стабилизации инструмента.\n\n```typescript\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"Custom shell example\",\n  parameters: Type.Object({}),\n  renderShell: \"self\",\n  async execute() {\n    return { content: [{ type: \"text\", text: \"ok\" }], details: undefined };\n  },\n  renderCall(args, theme, context) {\n    return new Text(theme.fg(\"accent\", \"my custom shell\"), 0, 0);\n  },\n});\n```\n\n`renderCall` и `renderResult` каждый получает объект `context` с:\n- `args` - текущие аргументы вызова инструмента\n- `state` — общее локальное состояние строки для `renderCall` и `renderResult`\n- `lastComponent` — ранее возвращенный компонент для этого слота, если таковой имеется.\n- `invalidate()` — запросить повторную визуализацию этой строки инструмента.\n- `toolCallId`, `cwd`, `executionStarted`, `argsComplete`, `isPartial`, `expanded`, `showImages`, `isError`\n\nИспользуйте `context.state` для общего состояния между слотами. Сохраняйте локальные кэши в возвращаемом экземпляре компонента, если вы хотите повторно использовать и изменять один и тот же компонент при рендеринге.\n\n#### рендерколл\n\nОтображает вызов инструмента или заголовок:\n\n```typescript\nimport { Text } from \"@earendil-works/pi-tui\";\n\nrenderCall(args, theme, context) {\n  const text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n  let content = theme.fg(\"toolTitle\", theme.bold(\"my_tool \"));\n  content += theme.fg(\"muted\", args.action);\n  if (args.text) {\n    content += \" \" + theme.fg(\"dim\", `\"${args.text}\"`);\n  }\n  text.setText(content);\n  return text;\n}\n```\n\n#### рендерРезультат\n\nОтображает результат или выходные данные инструмента:\n\n```typescript\nrenderResult(result, { expanded, isPartial }, theme, context) {\n  if (isPartial) {\n    return new Text(theme.fg(\"warning\", \"Processing...\"), 0, 0);\n  }\n\n  if (result.details?.error) {\n    return new Text(theme.fg(\"error\", `Error: ${result.details.error}`), 0, 0);\n  }\n\n  let text = theme.fg(\"success\", \"✓ Done\");\n  if (expanded && result.details?.items) {\n    for (const item of result.details.items) {\n      text += \"\\n  \" + theme.fg(\"dim\", item);\n    }\n  }\n  return new Text(text, 0, 0);\n}\n```\n\nЕсли слот намеренно не имеет видимого содержимого, верните пустой `Component`, например пустой `Container`.\n\n#### Подсказки по сочетанию клавиш\n\nИспользуйте `keyHint()` для отображения подсказок по привязке клавиш, которые соответствуют активной конфигурации привязки клавиш:\n\n```typescript\nimport { keyHint } from \"@earendil-works/pi-coding-agent\";\n\nrenderResult(result, { expanded }, theme, context) {\n  let text = theme.fg(\"success\", \"✓ Done\");\n  if (!expanded) {\n    text += ` (${keyHint(\"app.tools.expand\", \"to expand\")})`;\n  }\n  return new Text(text, 0, 0);\n}\n```\n\nДоступные функции:\n- `keyHint(keybinding, description)` — форматирует настроенный идентификатор привязки клавиш, например `\"app.tools.expand\"` или `\"tui.select.confirm\"`.\n- `keyText(keybinding)` — возвращает необработанный настроенный текст ключа для идентификатора привязки клавиш.\n- `rawKeyHint(key, description)` — форматировать необработанную строку ключа.\n\nИспользуйте идентификаторы привязки клавиш в пространстве имен:\n- Идентификаторы агентов кодирования используют пространство имен `app.*`, например `app.tools.expand`, `app.editor.external`, `app.session.rename`.\n- Общие идентификаторы TUI используют пространство имен `tui.*`, например `tui.select.confirm`, `tui.select.cancel`, `tui.input.tab`.\n\nИсчерпывающий список идентификаторов привязок клавиш и значений по умолчанию см. в разделе [keybindings.md](keybindings.md). `keybindings.json` использует те же идентификаторы пространства имен.\n\nПользовательские редакторы и компоненты `ctx.ui.custom()` получают `keybindings: KeybindingsManager` в качестве введенного аргумента. Им следует использовать этот внедренный менеджер напрямую, а не вызывать `getKeybindings()` или `setKeybindings()`.\n\n#### Лучшие практики\n\n- Используйте `Text` с дополнением `(0, 0)`. По умолчанию Box обрабатывает отступы.\n- Используйте `\\n` для многострочного контента.\n- Дескриптор `isPartial` для потоковой передачи прогресса.\n- Поддержка `expanded` для получения подробной информации по запросу.\n- Сохраняйте компактный вид по умолчанию.\n- Прочитайте `context.args` в `renderResult` вместо копирования аргументов в `context.state`.\n- Используйте `context.state` только для данных, которые должны быть разделены между слотами вызовов и результатов.\n- Повторно используйте `context.lastComponent`, если тот же экземпляр компонента можно обновить на месте.\n- Используйте `renderShell: \"self\"` только тогда, когда вам мешает коробочная оболочка по умолчанию. В режиме собственной оболочки инструмент отвечает за собственное кадрирование, отступы и фон.\n\n#### Отступать\n\nЕсли средство рендеринга слотов не определено или выдает:\n- `renderCall`: показывает имя инструмента.\n- `renderResult`: показывает необработанный текст из `content`.\n\n### Динамическая загрузка инструмента\n\nExtensions может зарегистрировать множество инструментов, оставляя активным только небольшой начальный набор. Затем инструмент может добавлять дополнительные инструменты с помощью `pi.setActiveTools()` во время выполнения. Pi обнаруживает чисто аддитивные изменения, записывает новые доступные имена инструментов в результат этого инструмента и применяет обновленный активный набор перед следующим запросом модели.\n\nЭто работает с каждой моделью. Models со встроенной поддержкой отложенной загрузки сохраняет стабильный префикс подсказки и загружает новые определения в позицию результата инструмента. Другие модели используют резервный вариант, описанный ниже.\n\nЖизненный цикл:\n\n1. Зарегистрируйте каждый инструмент с помощью `pi.registerTool()`, чтобы он появился в `pi.getAllTools()`.\n2. Оставьте инструменты загрузчика, такие как `search_tools`, активными, а инструменты с возможностью поиска оставьте неактивными.\n3. Во время выполнения загрузчика вызовите `pi.setActiveTools([...currentTools,...matchingTools])`. Изменение должно быть аддитивным: не удаляйте активные в данный момент инструменты в одном вызове.\n4. Pi записывает, какие инструменты были добавлены в результат инструмента загрузчика.\n5. Перед следующим ответом модели Pi предоставляет добавленные определения с использованием встроенной отложенной загрузки, если она поддерживается, или обычного активного списка инструментов в противном случае.\n\nВам не нужно возвращать ссылки на инструменты конкретного поставщика или отмечать загрузчик как специальный инструмент поиска. Смена активного инструмента является сигналом. Имена, переданные в `pi.setActiveTools()`, уже должны быть зарегистрированы; неизвестные имена игнорируются.\n\n#### Models со встроенной отложенной загрузкой\n\n- **Антропный**\n  - **Models:** Sonnet, Opus, Fable версии 4.5 или новее (без Haiku)\n  - **Собственное представление:** В отложенных определениях используется `defer_loading`; точка загрузки использует контент `tool_reference`.\n- **Открытый AI**\n  - **Models:** `gpt-5.4` и более новая семья\n  - **Встроенное представление:** Pi добавляет завершенные клиентские элементы `tool_search_call` и `tool_search_output` в точке загрузки.\n\nДля проверенной пользовательской модели или прокси-сервера встроенную обработку можно включить с помощью `compat.supportsToolReferences: true` для `anthropic-messages` или `compat.supportsToolSearch: true` для `openai-responses` и `openai-codex-responses`. Оставьте их отключенными, если конечная точка и модель не принимают соответствующий собственный протокол.\n\n#### Резервное поведение\n\nДля всех других моделей и поставщиков динамическая активация по-прежнему работает: Pi обычно отправляет полный текущий список активных инструментов при следующем запросе. Модель может вызывать недавно активированные инструменты, но добавление их определений может сделать недействительным префикс кэшированного приглашения поставщика.\n\nPi также использует этот безопасный запасной вариант, когда активный набор не является чисто аддитивным, например, при замене одной группы инструментов другой. Таким образом, удаление инструментов работает, но не использует отложенную загрузку.\n\nДля обеспечения наилучшего поведения кэша оставляйте инструмент загрузчика активным на протяжении всего сеанса и добавляйте инструменты вместо замены активного набора. Также обратите внимание, что активация инструмента с помощью `promptSnippet` или `promptGuidelines` перестраивает системное приглашение; такое изменение системного запроса может сделать префикс недействительным, даже если поставщик поддерживает отложенные схемы. Лениво загружаемые инструменты обычно должны полагаться на свой инструмент `description` и опускать метаданные подсказок только для активных действий.\n\n#### Пример инструмента поиска\n\nСледующее расширение регистрирует два инструмента с возможностью поиска, удаляет их из исходного активного набора и сохраняет только `search_tools` в качестве их загрузчика. В примере используется простое сопоставление ключевых слов, но реализация поиска может использовать BM25, внедрения, удаленный каталог или маршрутизацию для конкретного проекта.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nconst SEARCHABLE_TOOL_NAMES = new Set([\"lookup_weather\", \"search_issues\"]);\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerTool({\n    name: \"lookup_weather\",\n    label: \"Lookup Weather\",\n    description: \"Look up the current weather for a city\",\n    parameters: Type.Object({ city: Type.String() }),\n    async execute(_toolCallId, params) {\n      return {\n        content: [{ type: \"text\", text: `Weather for ${params.city}: sunny` }],\n        details: {},\n      };\n    },\n  });\n\n  pi.registerTool({\n    name: \"search_issues\",\n    label: \"Search Issues\",\n    description: \"Search project issues by keyword\",\n    parameters: Type.Object({ query: Type.String() }),\n    async execute(_toolCallId, params) {\n      return {\n        content: [{ type: \"text\", text: `No open issues matching ${params.query}` }],\n        details: {},\n      };\n    },\n  });\n\n  pi.registerTool({\n    name: \"search_tools\",\n    label: \"Search Tools\",\n    description: \"Search for and enable tools relevant to a task\",\n    promptSnippet: \"Search for additional tools when the active tools cannot perform the task\",\n    promptGuidelines: [\n      \"Use search_tools when a task requires a capability that is not currently available.\",\n    ],\n    parameters: Type.Object({\n      query: Type.String({ description: \"Capability or task to search for\" }),\n      limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),\n    }),\n    async execute(_toolCallId, params) {\n      const terms = params.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);\n      const matches = pi.getAllTools()\n        .filter((tool) => SEARCHABLE_TOOL_NAMES.has(tool.name))\n        .map((tool) => ({\n          tool,\n          score: terms.reduce(\n            (score, term) =>\n              score + (`${tool.name} ${tool.description}`.toLowerCase().includes(term) ? 1 : 0),\n            0,\n          ),\n        }))\n        .filter((match) => match.score > 0)\n        .sort((a, b) => b.score - a.score)\n        .slice(0, params.limit ?? 3)\n        .map((match) => match.tool.name);\n\n      if (matches.length === 0) {\n        return {\n          content: [{ type: \"text\", text: `No tools found for: ${params.query}` }],\n          details: { matches: [] },\n        };\n      }\n\n      const active = pi.getActiveTools();\n      const added = matches.filter((name) => !active.includes(name));\n      pi.setActiveTools([...new Set([...active, ...added])]);\n\n      return {\n        content: [{\n          type: \"text\",\n          text: added.length > 0\n            ? `Loaded tools: ${added.join(\", \")}`\n            : `Matching tools already active: ${matches.join(\", \")}`,\n        }],\n        details: { matches, added },\n      };\n    },\n  });\n\n  pi.on(\"session_start\", () => {\n    // Keep searchable tools registered but initially inactive. Preserve built-ins\n    // and tools owned by other extensions, and keep the loader itself active.\n    const initialTools = pi.getActiveTools().filter(\n      (name) => !SEARCHABLE_TOOL_NAMES.has(name),\n    );\n    pi.setActiveTools([...new Set([...initialTools, \"search_tools\"])]);\n  });\n}\n```\n\nКогда `search_tools` добавляет соответствие, модель получает это определение при следующем запросе. В модели, поддерживающей встроенные функции, определение привязывается после результата поиска без изменения исходного префикса схемы инструмента. На других моделях он появляется в обычном списке инструментов по тому же следующему запросу.\n\n## Пользовательский интерфейс\n\nExtensions может взаимодействовать с пользователями с помощью методов `ctx.ui` и настраивать способ отображения сообщений/инструментов.\n\n**Информацию о пользовательских компонентах см. в разделе [tui.md](tui.md)**, где есть шаблоны копирования и вставки для:\n- Диалоги выбора (SelectList)\n- Асинхронные операции с отменой (BorderedLoader)\n- Переключатели настроек (SettingsList)\n- Индикаторы состояния (setStatus)\n- Рабочее сообщение, видимость и индикатор во время потоковой передачи (`setWorkingMessage`, `setWorkingVisible`, `setWorkingIndicator`)\n- Виджеты над/под редактором (setWidget)\n- Поставщики автозаполнения, расположенные поверх встроенного завершения косой черты/пути (addAutocompleteProvider)\n- Пользовательские нижние колонтитулы (setFooter)\n\n### Диалоги\n\n```typescript\n// Select from options\nconst choice = await ctx.ui.select(\"Pick one:\", [\"A\", \"B\", \"C\"]);\n\n// Confirm dialog\nconst ok = await ctx.ui.confirm(\"Delete?\", \"This cannot be undone\");\n\n// Text input\nconst name = await ctx.ui.input(\"Name:\", \"placeholder\");\n\n// Multi-line editor\nconst text = await ctx.ui.editor(\"Edit:\", \"prefilled text\");\n\n// Notification (non-blocking)\nctx.ui.notify(\"Done!\", \"info\");  // \"info\" | \"warning\" | \"error\"\n```\n\n#### Диалоги по времени с обратным отсчетом\n\nДиалоги поддерживают опцию `timeout`, которая автоматически закрывается с отображением обратного отсчета в реальном времени:\n\n```typescript\n// Dialog shows \"Title (5s)\" → \"Title (4s)\" → ... → auto-dismisses at 0\nconst confirmed = await ctx.ui.confirm(\n  \"Timed Confirmation\",\n  \"This dialog will auto-cancel in 5 seconds. Confirm?\",\n  { timeout: 5000 }\n);\n\nif (confirmed) {\n  // User confirmed\n} else {\n  // User cancelled or timed out\n}\n```\n\n**Возвращаемые значения по таймауту:**\n- `select()` возвращает `undefined`\n- `confirm()` возвращает `false`\n- `input()` возвращает `undefined`\n\n#### Ручное увольнение с помощью AbortSignal\n\nДля большего контроля (например, чтобы отличить тайм-аут от отмены пользователем) используйте `AbortSignal`:\n\n```typescript\nconst controller = new AbortController();\nconst timeoutId = setTimeout(() => controller.abort(), 5000);\n\nconst confirmed = await ctx.ui.confirm(\n  \"Timed Confirmation\",\n  \"This dialog will auto-cancel in 5 seconds. Confirm?\",\n  { signal: controller.signal }\n);\n\nclearTimeout(timeoutId);\n\nif (confirmed) {\n  // User confirmed\n} else if (controller.signal.aborted) {\n  // Dialog timed out\n} else {\n  // User cancelled (pressed Escape or selected \"No\")\n}\n```\n\nСм. [examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts) полные примеры.\n\n### Виджеты, статус и нижний колонтитул\n\n```typescript\n// Status in footer (persistent until cleared)\nctx.ui.setStatus(\"my-ext\", \"Processing...\");\nctx.ui.setStatus(\"my-ext\", undefined);  // Clear\n\n// Working loader (shown during streaming)\nctx.ui.setWorkingMessage(\"Thinking deeply...\");\nctx.ui.setWorkingMessage();  // Restore default\nctx.ui.setWorkingVisible(false);  // Hide the built-in working loader row entirely\nctx.ui.setWorkingVisible(true);   // Show the built-in working loader row\n\n// Working indicator (shown during streaming)\nctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg(\"accent\", \"●\")] });  // Static dot\nctx.ui.setWorkingIndicator({\n  frames: [\n    ctx.ui.theme.fg(\"dim\", \"·\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n    ctx.ui.theme.fg(\"accent\", \"●\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n  ],\n  intervalMs: 120,\n});\nctx.ui.setWorkingIndicator({ frames: [] });  // Hide indicator\nctx.ui.setWorkingIndicator();  // Restore default spinner\n\n// Widget above editor (default)\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"]);\n// Widget below editor\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"], { placement: \"belowEditor\" });\nctx.ui.setWidget(\"my-widget\", (tui, theme) => new Text(theme.fg(\"accent\", \"Custom\"), 0, 0));\nctx.ui.setWidget(\"my-widget\", undefined);  // Clear\n\n// Custom footer (replaces built-in footer entirely)\nctx.ui.setFooter((tui, theme) => ({\n  render(width) { return [theme.fg(\"dim\", \"Custom footer\")]; },\n  invalidate() {},\n}));\nctx.ui.setFooter(undefined);  // Restore built-in footer\n\n// Terminal title\nctx.ui.setTitle(\"pi - my-project\");\n\n// Editor text\nctx.ui.setEditorText(\"Prefill text\");\nconst current = ctx.ui.getEditorText();\n\n// Paste into editor (triggers paste handling, including collapse for large content)\nctx.ui.pasteToEditor(\"pasted content\");\n\n// Stack custom autocomplete behavior on top of the built-in provider\nctx.ui.addAutocompleteProvider((current) => ({\n  triggerCharacters: [\"#\"],\n  async getSuggestions(lines, line, col, options) {\n    const beforeCursor = (lines[line] ?? \"\").slice(0, col);\n    const match = beforeCursor.match(/(?:^|[ \\t])#([^\\s#]*)$/);\n    if (!match) {\n      return current.getSuggestions(lines, line, col, options);\n    }\n\n    return {\n      prefix: `#${match[1] ?? \"\"}`,\n      items: [{ value: \"#2983\", label: \"#2983\", description: \"Extension API for autocomplete\" }],\n    };\n  },\n  applyCompletion(lines, line, col, item, prefix) {\n    return current.applyCompletion(lines, line, col, item, prefix);\n  },\n  shouldTriggerFileCompletion(lines, line, col) {\n    return current.shouldTriggerFileCompletion?.(lines, line, col) ?? true;\n  },\n}));\n\n// Tool output expansion\nconst wasExpanded = ctx.ui.getToolsExpanded();\nctx.ui.setToolsExpanded(true);\nctx.ui.setToolsExpanded(wasExpanded);\n\n// Custom editor (vim mode, emacs mode, etc.)\nctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));\nconst currentEditor = ctx.ui.getEditorComponent();\nctx.ui.setEditorComponent((tui, theme, keybindings) =>\n  new WrappedEditor(tui, theme, keybindings, currentEditor?.(tui, theme, keybindings))\n);\nctx.ui.setEditorComponent(undefined);  // Restore default editor\n\n// Theme management (see themes.md for creating themes)\nconst themes = ctx.ui.getAllThemes();  // [{ name: \"dark\", path: \"/...\" | undefined }, ...]\nconst lightTheme = ctx.ui.getTheme(\"light\");  // Load without switching\nconst result = ctx.ui.setTheme(\"light\");  // Switch by name\nif (!result.success) {\n  ctx.ui.notify(`Failed: ${result.error}`, \"error\");\n}\nctx.ui.setTheme(lightTheme!);  // Or switch by Theme object\nctx.ui.theme.fg(\"accent\", \"styled text\");  // Access current theme\n```\n\nПользовательские рамки индикаторов работы отображаются дословно. Если вам нужны цвета, добавьте их в строки фрейма самостоятельно, например, с помощью `ctx.ui.theme.fg(...)`.\n\n### Автозаполнение Providers\n\nИспользуйте `ctx.ui.addAutocompleteProvider()`, чтобы разместить пользовательскую логику автозаполнения поверх встроенной косой черты и поставщика пути. Установите `triggerCharacters` для пользовательских естественных триггеров, таких как `Используйте `ctx.ui.addAutocompleteProvider()`, чтобы разместить пользовательскую логику автозаполнения поверх встроенной косой черты и поставщика пути. Установите `triggerCharacters` для пользовательских естественных триггеров, таких как.\n\nТипичный образец:\n\n- проверить текст перед курсором\n- возвращайте свои собственные предложения, когда синтаксис вашего расширения совпадает\n- в противном случае делегируйте `current.getSuggestions(...)`\n- делегировать `applyCompletion(...)`, если вам не требуется собственное поведение вставки\n\n```typescript\npi.on(\"session_start\", (_event, ctx) => {\n  ctx.ui.addAutocompleteProvider((current) => ({\n    triggerCharacters: [\"#\"],\n    async getSuggestions(lines, cursorLine, cursorCol, options) {\n      const line = lines[cursorLine] ?? \"\";\n      const beforeCursor = line.slice(0, cursorCol);\n      const match = beforeCursor.match(/(?:^|[ \\t])#([^\\s#]*)$/);\n      if (!match) {\n        return current.getSuggestions(lines, cursorLine, cursorCol, options);\n      }\n\n      return {\n        prefix: `#${match[1] ?? \"\"}`,\n        items: [\n          { value: \"#2983\", label: \"#2983\", description: \"Extension API for registering custom @ autocomplete providers\" },\n          { value: \"#2753\", label: \"#2753\", description: \"Reload stale resource settings\" },\n        ],\n      };\n    },\n\n    applyCompletion(lines, cursorLine, cursorCol, item, prefix) {\n      return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);\n    },\n\n    shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {\n      return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;\n    },\n  }));\n});\n```\n\nСм. [github-issue-autocomplete.ts](../examples/extensions/github-issue-autocomplete.ts) полный пример, который предварительно загружает последние открытые проблемы GitHub с помощью `gh issue list` и фильтрует их локально для быстрого завершения `#...`. Для этого требуется GitHub CLI (`gh`) и GitHub проверка репозитория.\n\n### Пользовательские компоненты\n\nДля сложного пользовательского интерфейса используйте `ctx.ui.custom()`. Это временно заменяет редактор вашим компонентом до тех пор, пока не будет вызван `done()`:\n\n```typescript\nimport { Text, Component } from \"@earendil-works/pi-tui\";\n\nconst result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {\n  const text = new Text(\"Press Enter to confirm, Escape to cancel\", 1, 1);\n\n  text.onKey = (key) => {\n    if (key === \"return\") done(true);\n    if (key === \"escape\") done(false);\n    return true;\n  };\n\n  return text;\n});\n\nif (result) {\n  // User pressed Enter\n}\n```\n\nОбратный вызов получает:\n- Экземпляр `tui` - TUI (для размеров экрана, управления фокусом)\n- `theme` — Текущая тема для стилизации.\n- `keybindings` — Менеджер привязки клавиш приложения (для проверки ярлыков)\n- `done(value)` — вызов закрытия компонента и возврат значения.\n\nСм. [tui.md](tui.md) для полного компонента API.\n\n#### Режим наложения (экспериментальный)\n\nПередайте `{ overlay: true }`, чтобы отобразить компонент как плавающее модальное окно поверх существующего контента, не очищая экран:\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),\n  { overlay: true }\n);\n```\n\nДля расширенного позиционирования (привязки, поля, проценты, адаптивная видимость) укажите `overlayOptions`. Используйте `onHandle` для программного управления фокусом или видимостью:\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),\n  {\n    overlay: true,\n    overlayOptions: { anchor: \"top-right\", width: \"50%\", margin: 2 },\n    onHandle: (handle) => {\n      handle.focus(); // focus this overlay and bring it to the visual front\n      // handle.unfocus({ target: editorComponent }); // release input to a specific component\n      // handle.setHidden(true/false); // toggle visibility\n      // handle.hide(); // permanently remove\n    }\n  }\n);\n```\n\nСфокусированное видимое наложение может восстановить ввод после закрытия временного пользовательского интерфейса без наложения. Если вы намеренно хотите, чтобы другой компонент сохранял входные данные, пока наложение остается видимым, вызовите `handle.unfocus({ target })`. Передача `{ target: null }` освобождает наложение без фокусировки на другом компоненте.\n\nСм. [tui.md](tui.md) полные `OverlayOptions` и `OverlayHandle`, API и [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) для примеров.\n\n### Пользовательский редактор\n\nЗамените основной редактор ввода собственной реализацией (режим vim, режим emacs и т. д.):\n\n```typescript\nimport { CustomEditor, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { matchesKey } from \"@earendil-works/pi-tui\";\n\nclass VimEditor extends CustomEditor {\n  private mode: \"normal\" | \"insert\" = \"insert\";\n\n  handleInput(data: string): void {\n    if (matchesKey(data, \"escape\") && this.mode === \"insert\") {\n      this.mode = \"normal\";\n      return;\n    }\n    if (this.mode === \"normal\" && data === \"i\") {\n      this.mode = \"insert\";\n      return;\n    }\n    super.handleInput(data);  // App keybindings + text editing\n  }\n}\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    ctx.ui.setEditorComponent((tui, theme, keybindings) =>\n      new VimEditor(tui, theme, keybindings)\n    );\n  });\n}\n```\n\n**Ключевые моменты:**\n- Расширьте `CustomEditor` (не базовый `Editor`), чтобы получить привязки клавиш приложения (Escape для отмены, ctrl+d, переключение модели).\n- Позвоните по номеру `super.handleInput(data)`, чтобы узнать ключи, с которыми вы не справляетесь.\n- Factory получает `tui`, `theme` и `keybindings` из приложения.\n- Используйте `ctx.ui.getEditorComponent()` перед `setEditorComponent()`, чтобы обернуть ранее настроенный пользовательский редактор.\n- Нажмите `undefined`, чтобы восстановить настройки по умолчанию: `ctx.ui.setEditorComponent(undefined)`\n\nЧтобы создать композицию с другим расширением, которое уже заменило редактор, сохраните предыдущую фабрику, прежде чем устанавливать свою:\n\n```typescript\nconst previous = ctx.ui.getEditorComponent();\nctx.ui.setEditorComponent((tui, theme, keybindings) =>\n  new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })\n);\n```\n\nПолный пример с индикатором режима см. в [tui.md](tui.md) Шаблоне 7.\n\n### Отображение сообщений и записей\n\nЗарегистрируйте собственный рендерер для сообщений с помощью `customType`. Используйте средства рендеринга сообщений для контента, который должен участвовать в контексте LLM:\n\n```typescript\nimport { Text } from \"@earendil-works/pi-tui\";\n\npi.registerMessageRenderer(\"my-extension\", (message, options, theme) => {\n  const { expanded, outputPad } = options;\n  let text = theme.fg(\"accent\", `[${message.customType}] `);\n  text += message.content;\n\n  if (expanded && message.details) {\n    text += \"\\n\" + theme.fg(\"dim\", JSON.stringify(message.details, null, 2));\n  }\n\n  return new Text(text, outputPad, 0);\n});\n```\n\nСообщения отправляются через `pi.sendMessage()`:\n\n```typescript\npi.sendMessage({\n  customType: \"my-extension\",  // Matches registerMessageRenderer\n  content: \"Status update\",\n  display: true,               // Show in TUI\n  details: { ... },            // Available in renderer\n});\n```\n\nДля контента, содержащего только TUI, который не следует отправлять в LLM, вместо этого визуализируйте пользовательские записи:\n\n```typescript\npi.registerEntryRenderer(\"my-card\", (entry, options, theme) => {\n  return new Text(theme.fg(\"accent\", JSON.stringify(entry.data)));\n});\n\npi.appendEntry(\"my-card\", { status: \"done\" });\n```\n\n### Цвета темы\n\nВсе функции рендеринга получают объект `theme`. См. [themes.md](themes.md) для создания собственных тем и полной цветовой палитры.\n\n```typescript\n// Foreground colors\ntheme.fg(\"toolTitle\", text)   // Tool names\ntheme.fg(\"accent\", text)      // Highlights\ntheme.fg(\"success\", text)     // Success (green)\ntheme.fg(\"error\", text)       // Errors (red)\ntheme.fg(\"warning\", text)     // Warnings (yellow)\ntheme.fg(\"muted\", text)       // Secondary text\ntheme.fg(\"dim\", text)         // Tertiary text\n\n// Text styles\ntheme.bold(text)\ntheme.italic(text)\ntheme.strikethrough(text)\n```\n\nДля подсветки синтаксиса в средствах визуализации пользовательских инструментов:\n\n```typescript\nimport { highlightCode, getLanguageFromPath } from \"@earendil-works/pi-coding-agent\";\n\n// Highlight code with explicit language\nconst highlighted = highlightCode(\"const x = 1;\", \"typescript\", theme);\n\n// Auto-detect language from file path\nconst lang = getLanguageFromPath(\"/path/to/file.rs\");  // \"rust\"\nconst highlighted = highlightCode(code, lang, theme);\n```\n\n## Обработка ошибок\n\n- Ошибки расширения регистрируются, агент продолжает работу\n- Ошибки `tool_call` блокируют инструмент (отказоустойчивость)\n- Ошибки инструмента `execute` должны сигнализироваться броском; выброшенная ошибка перехватывается, сообщается LLM с помощью `isError: true`, и выполнение продолжается\n\n## Поведение режима\n\n| Режим | `ctx.mode` | `ctx.hasUI` | Примечания |\n|------|------------|-------------|-------|\n| Интерактивный | `\"tui\"` | `true` | Полный TUI с терминальным рендерингом |\n| RPC (`--mode rpc`) | `\"rpc\"` | `true` | Диалоги и уведомления по протоколу JSON; `custom()` возвращает `undefined`. См. [rpc.md](rpc.md) |\n| JSON (`--mode json`) | `\"json\"` | `false` | Поток событий на stdout; Методы пользовательского интерфейса не требуют операций |\n| Распечатать (`-p`) | `\"print\"` | `false` | Extensions запустить, но не могу подсказать |\n\nИспользуйте `ctx.mode === \"tui\"` перед TUI, специфичными для функций (`custom()`, фабрики компонентов, ввод через терминал). Используйте `ctx.hasUI` перед методами диалога и уведомления, которые работают как в TUI, так и в RPC режимах.\n\n## Примеры\n\nВсе примеры в [examples/extensions/](../examples/extensions/).\n\n| Пример | Описание | Ключ APIs |\n|---------|-------------|----------|\n| **Инструменты** |  |  |\n| `hello.ts` | Минимальная регистрация инструмента | `registerTool` |\n| `question.ts` | Инструмент с взаимодействием с пользователем | `registerTool`, `ui.select` |\n| `questionnaire.ts` | Многошаговый мастер-инструмент | `registerTool`, `ui.custom` |\n| `todo.ts` | Инструмент с сохранением состояния и постоянством | `registerTool`, `appendEntry`, `renderResult`, события сеанса |\n| `dynamic-tools.ts` | Регистрация инструментов после запуска и во время команд | `registerTool`, `session_start`, `registerCommand` |\n| `structured-output.ts` | Окончательный инструмент структурированного вывода с `terminate: true` | `registerTool`, завершение результатов инструмента |\n| `truncated-tool.ts` | Пример усечения вывода | `registerTool`, `truncateHead` |\n| `tool-override.ts` | Переопределить встроенный инструмент чтения | `registerTool` (то же имя, что и у встроенного) |\n| **Команды** |  |  |\n| `pirate.ts` | Изменить системное приглашение за ход | `registerCommand`, `before_agent_start` |\n| `summarize.ts` | Команда сводки разговора | `registerCommand`, `ui.custom` |\n| `handoff.ts` | Передача модели между поставщиками | `registerCommand`, `ui.editor`, `ui.custom` |\n| `qna.ts` | Вопросы и ответы с пользовательским интерфейсом | `registerCommand`, `ui.custom`, `setEditorText` |\n| `send-user-message.ts` | Внедрение пользовательских сообщений | `registerCommand`, `sendUserMessage` |\n| `reload-runtime.ts` | Команда перезагрузки и передача инструмента LLM | `registerCommand`, `ctx.reload()`, `sendUserMessage` |\n| `shutdown-command.ts` | Команда плавного выключения | `registerCommand`, `shutdown()` |\n| **Мероприятия и ворота** |  |  |\n| `permission-gate.ts` | Блокируйте опасные команды | `on(\"tool_call\")`, `ui.confirm` |\n| `project-trust.ts` | Принятие решения или отсрочка доверия проекта со стороны пользователя/глобального расширения или расширения CLI. | `on(\"project_trust\")`, пользовательский интерфейс доверия, требуемый результат доверия |\n| `protected-paths.ts` | Блокировать запись по определенным путям | `on(\"tool_call\")` |\n| `confirm-destructive.ts` | Подтвердить изменения сеанса | `on(\"session_before_switch\")`, `on(\"session_before_fork\")` |\n| `dirty-repo-guard.ts` | Предупреждать о грязном репозитории git | `on(\"session_before_*\")`, `exec` |\n| `input-transform.ts` | Преобразование пользовательского ввода | `on(\"input\")` |\n| `input-transform-streaming.ts` | Входное преобразование с поддержкой потоковой передачи | `on(\"input\")`, `streamingBehavior` |\n| `model-status.ts` | React для изменения модели | `on(\"model_select\")`, `setStatus` |\n| `provider-payload.ts` | Проверка полезных данных и заголовков ответов поставщика | `on(\"before_provider_request\")`, `on(\"after_provider_response\")` |\n| `system-prompt-header.ts` | Отображение подсказки системы | `on(\"agent_start\")`, `getSystemPrompt` |\n| `claude-rules.ts` | Загрузка правил из файлов | `on(\"session_start\")`, `on(\"before_agent_start\")` |\n| `prompt-customizer.ts` | Добавьте контекстно-зависимые инструкции по инструменту, используя `systemPromptOptions` | `on(\"before_agent_start\")`, `BuildSystemPromptOptions` |\n| `file-trigger.ts` | Наблюдатель за файлами вызывает сообщения | `sendMessage` |\n| **Сжатие и сеансы** |  |  |\n| `custom-compaction.ts` | Пользовательская сводка по сжатию | `on(\"session_before_compact\")` |\n| `trigger-compact.ts` | Запуск уплотнения вручную | `compact()` |\n| `git-checkpoint.ts` | Git тайник на ходах | `on(\"turn_start\")`, `on(\"session_before_fork\")`, `exec` |\n| `git-merge-and-resolve.ts` | Извлечение, объединение и разрешение конфликтов | `on(\"agent_end\")`, `exec`, `sendUserMessage` |\n| `auto-commit-on-exit.ts` | Принять решение о выключении | `on(\"session_shutdown\")`, `exec` |\n| **Компоненты пользовательского интерфейса** |  |  |\n| `status-line.ts` | Индикатор состояния нижнего колонтитула | `setStatus`, события сеанса |\n| `working-indicator.ts` | Настройте индикатор работы потоковой передачи | `setWorkingIndicator`, `registerCommand` |\n| `github-issue-autocomplete.ts` | Добавьте `#1234` завершенных задач поверх встроенного автозаполнения, предварительно загрузив последние открытые проблемы из `gh issue list`. | `addAutocompleteProvider`, `on(\"session_start\")`, `exec` |\n| `custom-footer.ts` | Полностью заменить нижний колонтитул | `registerCommand`, `setFooter` |\n| `custom-header.ts` | Заменить стартовый заголовок | `on(\"session_start\")`, `setHeader` |\n| `modal-editor.ts` | Модальный редактор в стиле Vim | `setEditorComponent`, `CustomEditor` |\n| `rainbow-editor.ts` | Пользовательский стиль редактора | `setEditorComponent` |\n| `widget-placement.ts` | Виджет над/под редактором | `setWidget` |\n| `overlay-test.ts` | Компоненты наложения | `ui.custom` с опциями наложения |\n| `overlay-qa-tests.ts` | Комплексные накладные тесты | `ui.custom`, все параметры наложения |\n| `notify.ts` | Простые уведомления | `ui.notify` |\n| `timed-confirm.ts` | Диалоги с таймаутом | `ui.confirm` с тайм-аутом/сигналом |\n| `mac-system-theme.ts` | Автоматическое переключение темы | `setTheme`, `exec` |\n| **Комплекс Extensions** |  |  |\n| `plan-mode/` | Реализация режима полного плана | Все типы событий: `registerCommand`, `registerShortcut`, `registerFlag`, `setStatus`, `setWidget`, `sendMessage`, `setActiveTools` |\n| `preset.ts` | Сохраняемые пресеты (модель, инструменты, мышление) | `registerCommand`, `registerShortcut`, `registerFlag`, `setModel`, `setActiveTools`, `setThinkingLevel`, `appendEntry` |\n| `tools.ts` | Включить/выключить инструменты в пользовательском интерфейсе | `registerCommand`, `setActiveTools`, `SettingsList`, события сеанса |\n| **Удаленное управление и песочница** |  |  |\n| `ssh.ts` | SSH удаленное выполнение | `registerFlag`, `on(\"user_bash\")`, `on(\"before_agent_start\")`, операции с инструментом |\n| `interactive-shell.ts` | Постоянный сеанс оболочки | `on(\"user_bash\")` |\n| `sandbox/` | Выполнение инструмента в песочнице | Операции с инструментом |\n| `gondolin/` | Направьте встроенные инструменты и команды `!` в микро-VM Gondolin. | Операции с инструментом, встроенные переопределения инструмента, `on(\"user_bash\")` |\n| `subagent/` | Создание субагентов | `registerTool`, `exec` |\n| **Игры** |  |  |\n| `snake.ts` | Змеиная игра | `registerCommand`, `ui.custom`, работа с клавиатурой |\n| `space-invaders.ts` | Игра Космические захватчики | `registerCommand`, `ui.custom` |\n| `doom-overlay/` | Doom в наложении | `ui.custom` с наложением |\n| **Providers** |  |  |\n| `custom-provider-anthropic/` | Пользовательский антропный прокси | `registerProvider` |\n| `custom-provider-gitlab-duo/` | GitИнтеграция Lab Duo | `registerProvider` с OAuth |\n| **Сообщения и общение** |  |  |\n| `message-renderer.ts` | Пользовательский рендеринг сообщений | `registerMessageRenderer`, `sendMessage` |\n| `entry-renderer.ts` | Отрисовка пользовательской записи только TUI | `registerEntryRenderer`, `appendEntry` |\n| `event-bus.ts` | События между расширениями | `pi.events` |\n| **Метаданные сеанса** |  |  |\n| `session-name.ts` | Назовите сеансы для селектора | `setSessionName`, `getSessionName` |\n| `bookmark.ts` | Добавить в закладки записи для /tree | `setLabel` |\n| **Разное** |  |  |\n| `inline-bash.ts` | Встроенный bash в вызовах инструментов | `on(\"tool_call\")` |\n| `bash-spawn-hook.ts` | Отрегулируйте команду bash, cwd и env перед выполнением. | `createBashTool`, `spawnHook` |\n| `with-deps/` | Расширение с зависимостями npm | Структура пакета с `package.json` |","sourceFile":"extensions.md"},"index":{"title":"Pi Документация","markdown":"Pi — это минимальный жгут кодирования клемм. Он разработан так, чтобы оставаться небольшим по своей сути, но при этом расширяться за счет TypeScript расширений, навыков, prompt templates, тем и пакетов pi.\n\n## Быстрый старт\n\nУстановите Pi с помощью npm:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` отключает сценарии жизненного цикла зависимостей во время установки. Pi не требует сценариев установки для обычной установки npm.\n\nВ Linux или macOS вы также можете использовать установщик:\n\n```bash\ncurl -fsSL https://pi.dev/install.sh | sh\n```\n\nЧтобы удалить сам pi, используйте npm для установки Curl и npm:\n\n```bash\nnpm uninstall -g @earendil-works/pi-coding-agent\n```\n\nДля установки pnpm, Yarn или Bun используйте соответствующую глобальную команду удаления: `pnpm remove -g @earendil-works/pi-coding-agent`, `yarn global remove @earendil-works/pi-coding-agent` или `bun uninstall -g @earendil-works/pi-coding-agent`.\n\nЗатем запустите его в каталоге проекта:\n\n```bash\npi\n```\n\nАутентифицируйтесь с помощью `/login` для subscription providers или установите API key, например `ANTHROPIC_API_KEY`, перед запуском pi.\n\nПолную информацию о первом запуске см. в [Quickstart](quickstart.md).\n\n## Начните здесь\n\n- [Quickstart](quickstart.md) — установка, аутентификация и запуск первого сеанса.\n- [Using Pi](usage.md) — интерактивный режим, slash commands, context files и CLI ссылка.\n- [Providers](providers.md) - настройка подписки и API-ключа для встроенных провайдеров.\n- [llama.cpp](llama-cpp.md) — запустить локальный маршрутизатор и управлять моделями с помощью `/llama`.\n- [Security](security.md) — доверие к проекту, sandbox границы и отчеты об уязвимостях.\n- [Containerization](containerization.md) - sandbox пи с Gondolin, Docker или OpenShell.\n- [Settings](settings.md) — глобальные настройки и настройки проекта.\n- [Keybindings](keybindings.md) — сочетания клавиш по умолчанию и пользовательские сочетания клавиш.\n- [Sessions](sessions.md) — управление сеансами, ветвление и навигация по дереву.\n- [Compaction](compaction.md) - context compaction и branch summarization.\n\n## Кастомизация\n\n- [Extensions](extensions.md)–TypeScript модули для инструментов, команд, событий и пользовательского интерфейса.\n- [Skills](skills.md) — Агент Skills для многократного использования возможностей по требованию.\n- [Prompt templates](prompt-templates.md) — многоразовые подсказки, которые расширяются от slash commands.\n- [Themes](themes.md) - встроенный и пользовательский terminal themes.\n- [Pi packages](packages.md) — объединяйте и делитесь расширениями, навыками, подсказками и темами.\n- [Custom models](models.md) — добавить записи модели для поддерживаемого поставщика API.\n- [Custom providers](custom-provider.md) — реализовать пользовательские потоки API и OAuth.\n\n## Программное использование\n\n- [SDK](sdk.md) — встроить число Пи в Node.js приложения.\n- [RPC mode](rpc.md) - интегрировать по stdin/stdout JSONL.\n- [JSON event stream mode](json.md) — режим печати со структурированными событиями.\n- [TUI components](tui.md) — создать собственный пользовательский интерфейс терминала для расширений.\n\n## Ссылка\n\n- [Environment variables](environment-variables.md) - Pi конфигурация процесса и метаданные сеанса, доступные инструментам bash.\n- [Session format](session-format.md) - JSONL формат файла сеанса, типы записей и SessionManager API.\n\n## Настройка платформы\n\n- [Windows](windows.md)\n- [Termux on Android](termux.md)\n- [tmux](tmux.md)\n- [Terminal setup](terminal-setup.md)\n- [Shell aliases](shell-aliases.md)\n\n## Разработка\n\n- [Development](development.md) — локальная настройка, структура проекта и отладка.","sourceFile":"index.md"},"json":{"title":"JSON Режим потока событий","markdown":"```bash\npi --mode json \"Your prompt\"\n```\n\nВыводит все события сеанса в виде строк от JSON до stdout. Полезно для интеграции pi в другие инструменты или пользовательские интерфейсы.\n\n## Типы событий\n\nВ событиях Wire используется `JsonAgentSessionEvent`. Это соответствует\n[`AgentSessionEvent`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/agent-session.ts)\nза исключением того, что в потоковых обновлениях сообщений не учитываются накопительные снимки:\n\n```typescript\ntype WithoutPartial<T> = T extends { partial: unknown } ? Omit<T, \"partial\"> : T;\n\ntype JsonAgentSessionEvent =\n  | Exclude<AgentSessionEvent, { type: \"message_update\" }>\n  | {\n      type: \"message_update\";\n      assistantMessageEvent: WithoutPartial<AssistantMessageEvent>;\n    };\n```\n\n`queue_update` выдает все ожидающие очереди управления и отслеживания при каждом их изменении. `compaction_start` и `compaction_end` охватывают как ручное, так и автоматическое уплотнение.\n\nДругие базовые события происходят из\n[`AgentEvent`](https://github.com/earendil-works/pi-mono/blob/main/packages/agent/src/types.ts):\n\n```typescript\ntype AgentEvent =\n  // Agent lifecycle\n  | { type: \"agent_start\" }\n  | { type: \"agent_end\"; messages: AgentMessage[] }\n  // Turn lifecycle\n  | { type: \"turn_start\" }\n  | { type: \"turn_end\"; message: AgentMessage; toolResults: ToolResultMessage[] }\n  // Message lifecycle\n  | { type: \"message_start\"; message: AgentMessage }\n  | { type: \"message_update\"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }\n  | { type: \"message_end\"; message: AgentMessage }\n  // Tool execution\n  | { type: \"tool_execution_start\"; toolCallId: string; toolName: string; args: any }\n  | { type: \"tool_execution_update\"; toolCallId: string; toolName: string; args: any; partialResult: any }\n  | { type: \"tool_execution_end\"; toolCallId: string; toolName: string; result: any; isError: boolean };\n```\n\n## Типы сообщений\n\nБазовые сообщения от [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts#L134):\n- `UserMessage` (строка 134)\n- `AssistantMessage` (строка 140)\n- `ToolResultMessage` (строка 152)\n\nРасширенные сообщения от [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts#L29):\n- `BashExecutionMessage` (строка 29)\n- `CustomMessage` (строка 46)\n- `BranchSummaryMessage` (строка 55)\n- `CompactionSummaryMessage` (строка 62)\n\n## Выходной формат\n\nКаждая строка представляет собой объект JSON. Первая строка — это заголовок сеанса:\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"...\",\"cwd\":\"/path\"}\n```\n\nДалее следуют события по мере их возникновения:\n\n```json\n{\"type\":\"agent_start\"}\n{\"type\":\"turn_start\"}\n{\"type\":\"message_start\",\"message\":{\"role\":\"assistant\",\"content\":[],...}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_end\",\"message\":{...}}\n{\"type\":\"turn_end\",\"message\":{...},\"toolResults\":[]}\n{\"type\":\"agent_end\",\"messages\":[...]}\n```\n\n`message_update` записи содержат только дельту. Они опускают как совокупное поле `message`, так и\n`assistantMessageEvent.partial`, чтобы сохранить линейный размер потока. Используйте `contentIndex` и `delta`.\nсобрать живой текст, размышления или аргументы, если это необходимо. `message_end` содержит\nпоследнее авторитетное сообщение.\n\n## Пример\n\n```bash\npi --mode json \"List files\" 2>/dev/null | jq -c 'select(.type == \"message_end\")'\n```","sourceFile":"json.md"},"keybindings":{"title":"Сочетания клавиш","markdown":"Все сочетания клавиш можно настроить с помощью `~/.pi/agent/keybindings.json`. Каждое действие может быть привязано к одной или нескольким клавишам.\n\nВ файле конфигурации используются те же идентификаторы привязки клавиш в пространстве имен, которые pi использует внутри себя и которые авторы расширений используют в менеджерах `keyHint()` и внедренных `keybindings`.\n\nСтарые конфигурации, использующие идентификаторы с предварительно заданным пространством имен, такие как `cursorUp` или `expandTools`, автоматически переносятся в идентификаторы с заданным пространством имен при запуске.\n\nПосле редактирования `keybindings.json` запустите `/reload` в pi, чтобы применить изменения без перезапуска сеанса.\n\n## Ключевой формат\n\n`modifier+key`, где модификаторы — `ctrl`, `shift`, `alt`, `super` (комбинируемые), а ключи:\n\n- **Буквы:** `a-z`\n- **Цифры:** `0-9`\n- **Специальные клавиши:** `escape`, `esc`, `enter`, `return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`\n- **Функциональные клавиши:** `f1`-`f12`\n- **Символы:** `` ` ``, `-`, `=`, `[`, `]`, `\\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`\n\nКомбинации модификаторов: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `super+k`, `ctrl+super+k`, `ctrl+1` и т. д.\n\nДля привязок `super` требуется терминал, который сообщает модификатор отдельно, обычно через протокол клавиатуры Kitty. Без этой поддержки они могут не работать в терминалах.\n\n## Все действия\n\n### TUI Движение курсора редактора\n\n| Идентификатор сочетания клавиш | По умолчанию | Описание |\n|--------|---------|-------------|\n| `tui.editor.cursorUp` | `up` | Переместите курсор вверх, просматривая старую историю вверху. |\n| `tui.editor.cursorDown` | `down` | Переместите курсор вниз, просматривая новую историю внизу. |\n| `tui.editor.historyPrevious` | *(никто)* | Выберите предыдущую запись истории запросов |\n| `tui.editor.historyNext` | *(никто)* | Выберите следующую запись истории подсказок |\n| `tui.editor.cursorLeft` | `left`, `ctrl+b` | Переместить курсор влево |\n| `tui.editor.cursorRight` | `right`, `ctrl+f` | Переместить курсор вправо |\n| `tui.editor.cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | Переместить курсорное слово влево |\n| `tui.editor.cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | Переместить курсорное слово вправо |\n| `tui.editor.cursorLineStart` | `home`, `ctrl+home`, `ctrl+a` | Перейти к началу строки |\n| `tui.editor.cursorLineEnd` | `end`, `ctrl+end`, `ctrl+e` | Перейти к концу строки |\n| `tui.editor.jumpForward` | `ctrl+]` | Перейти вперед к персонажу |\n| `tui.editor.jumpBackward` | `ctrl+alt+]` | Перейти назад к персонажу |\n| `tui.editor.pageUp` | `pageUp`, `ctrl+pageUp` | Прокручивать вверх по страницам |\n| `tui.editor.pageDown` | `pageDown`, `ctrl+pageDown` | Пролистнуть вниз по странице |\n\nСпециальные действия с историей всегда изменяют записи истории, независимо от положения курсора в многострочной подсказке. Явные привязки истории имеют приоритет над действиями приложения, пока основной редактор находится в фокусе, поэтому привязка от `tui.editor.historyPrevious` до `ctrl+p` переопределяет цикличность модели в этом контексте без изменения `Ctrl+P` в селекторах.\n\n### TUI Удаление редактора\n\n| Идентификатор сочетания клавиш | По умолчанию | Описание |\n|--------|---------|-------------|\n| `tui.editor.deleteCharBackward` | `backspace` | Удалить символ задом наперед |\n| `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | Удалить символ вперед |\n| `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace` | Удалить слово задом наперед |\n| `tui.editor.deleteWordForward` | `alt+d`, `alt+delete` | Удалить слово вперед |\n| `tui.editor.deleteToLineStart` | `ctrl+u` | Удалить в начало строки |\n| `tui.editor.deleteToLineEnd` | `ctrl+k` | Удалить до конца строки |\n\n### TUI Ввод\n\n| Идентификатор сочетания клавиш | По умолчанию | Описание |\n|--------|---------|-------------|\n| `tui.input.newLine` | `shift+enter`, `ctrl+j` | Вставить новую строку |\n| `tui.input.submit` | `enter` | Отправить данные |\n| `tui.input.tab` | `tab` | Вкладка/автозаполнение |\n\n### TUI Кольцо убийства\n\n| Идентификатор сочетания клавиш | По умолчанию | Описание |\n|--------|---------|-------------|\n| `tui.editor.yank` | `ctrl+y` | Вставить последний удаленный текст |\n| `tui.editor.yankPop` | `alt+y` | Перебирать удаленный текст после извлечения |\n| `tui.editor.undo` | `ctrl+-` | Отменить последнее редактирование |\n\n### TUI Буфер обмена и выделение\n\n| Идентификатор сочетания клавиш | По умолчанию | Описание |\n|--------|---------|-------------|\n| `tui.input.copy` | `ctrl+c` | Копировать выделение |\n| `tui.select.up` | `up` | Переместить выделение вверх |\n| `tui.select.down` | `down` | Переместить выделение вниз |\n| `tui.select.pageUp` | `pageUp` | На страницу вверх в списке |\n| `tui.select.pageDown` | `pageDown` | На страницу вниз в списке |\n| `tui.select.confirm` | `enter` | Подтвердить выбор |\n| `tui.select.cancel` | `escape`, `ctrl+c` | Отменить выбор |\n\n### TUI Полноэкранный просмотр\n\nЭти действия применяются, когда интерактивный режим использует `--tui-mode fullscreen` и нацелен на основную область прокрутки стенограммы. Трекпад двумя пальцами и колесо мыши прокручивают область под указателем, возвращаясь к расшифровке через фиксированную док-станцию ​​редактора/статуса/нижнего колонтитула. Щелчок по гиперссылке OSC 8 открывает ее в обработчике по умолчанию. Перетаскивание основной кнопкой мыши выделяет текст и копирует его в буфер обмена; удерживание верхнего или нижнего края расшифровки автоматически прокручивает содержимое за кадром.\n\nПривязки полноэкранной расшифровки имеют приоритет над привязками редактора. Таким образом, немодифицированные навигационные клавиши по умолчанию управляют расшифровкой в ​​полноэкранном режиме, а их варианты `ctrl` продолжают управлять редактором. Вне полноэкранного режима оба варианта управляют редактором.\n\n| Ключ | Режим по умолчанию | Полноэкранный режим |\n|-----|--------------|-----------------|\n| `home`, `end` | Редактор | Стенограмма |\n| `ctrl+home`, `ctrl+end` | Редактор | Редактор |\n| `pageUp`, `pageDown` | Редактор | Стенограмма |\n| `ctrl+pageUp`, `ctrl+pageDown` | Редактор | Редактор |\n\nЭту маршрутизацию можно настроить с помощью обычных привязок действий. Например, `\"tui.altScreen.pageUp\": \"ctrl+pageUp\"` позволяет `pageUp` управлять редактором, а `ctrl+pageUp` управлять расшифровкой в ​​полноэкранном режиме. Свяжите `tui.altScreen.halfPageUp` и `tui.altScreen.halfPageDown` для небольших шагов транскрипции, сохраняя при этом полностраничные привязки. Настройка `\"tui.altScreen.pageUp\": []` полностью отключает этот ярлык расшифровки. Привязки пользователя заменяют значения по умолчанию для этого действия.\n\n| Идентификатор сочетания клавиш | По умолчанию | Описание |\n|--------|---------|-------------|\n| `tui.altScreen.pageUp` | `pageUp` | Прокрутить транскрипт вверх на одну страницу |\n| `tui.altScreen.pageDown` | `pageDown` | Прокрутите транскрипт вниз на одну страницу |\n| `tui.altScreen.halfPageUp` | *(никто)* | Прокрутить транскрипт вверх на полстраницы |\n| `tui.altScreen.halfPageDown` | *(никто)* | Прокрутите транскрипт вниз на полстраницы. |\n| `tui.altScreen.previousPrompt` | `ctrl+shift+up` | Перейти к предыдущему отмеченному сообщению |\n| `tui.altScreen.nextPrompt` | `ctrl+shift+down` | Перейти к следующему отмеченному сообщению |\n| `tui.altScreen.top` | `home` | Прокрутите до начала стенограммы |\n| `tui.altScreen.bottom` | `end` | Прокрутите до конца стенограммы и следуйте новым выводам. |\n\n### Приложение\n\n| Идентификатор сочетания клавиш | По умолчанию | Описание |\n|--------|---------|-------------|\n| `app.interrupt` | `escape` | Отмена/прерывание |\n| `app.clear` | `ctrl+c` | Очистить редактор (первый) / выйти (второй) |\n| `app.exit` | `ctrl+d` | Выход (когда редактор пуст) |\n| `app.suspend` | `ctrl+z` (нет в Windows) | Приостановить в фоновом режиме |\n| `app.editor.external` | `ctrl+g` | Открыть во внешнем редакторе (`externalEditor`, `$VISUAL`, `$EDITOR`, «Блокнот» в Windows или `nano` в другом месте) |\n| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` в Windows) | Вставить изображение или текст из буфера обмена |\n\n### Сессии\n\n| Идентификатор сочетания клавиш | По умолчанию | Описание |\n|--------|---------|-------------|\n| `app.session.new` | *(никто)* | Начать новый сеанс (`/new`) |\n| `app.session.tree` | *(никто)* | Открыть навигатор session tree (`/tree`) |\n| `app.session.fork` | *(никто)* | Форкнуть текущую сессию (`/fork`) |\n| `app.session.resume` | *(никто)* | Открыть окно выбора резюме сеанса (`/resume`) |\n| `app.session.togglePath` | `ctrl+p` | Переключить отображение пути |\n| `app.session.toggleSort` | `ctrl+s` | Переключить режим сортировки |\n| `app.session.toggleNamedFilter` | `ctrl+n` | Переключить фильтр только по имени |\n| `app.session.rename` | `ctrl+r` | Переименовать сеанс |\n| `app.session.delete` | `ctrl+d` | Удалить сеанс |\n| `app.session.deleteNoninvasive` | `ctrl+backspace` | Удалить сеанс, когда запрос пуст |\n\n### Models и мышление\n\n| Идентификатор сочетания клавиш | По умолчанию | Описание |\n|--------|---------|-------------|\n| `app.model.select` | `ctrl+l` | Открыть выбор модели |\n| `app.model.cycleForward` | `ctrl+p` | Переход к следующей модели |\n| `app.model.cycleBackward` | `shift+ctrl+p` | Перейти к предыдущей модели |\n| `app.thinking.cycle` | `shift+tab` | Циклический уровень мышления |\n| `app.thinking.toggle` | `ctrl+t` | Свернуть или расширить блоки мышления |\n\n### Дисплей и очередь сообщений\n\n| Идентификатор сочетания клавиш | По умолчанию | Описание |\n|--------|---------|-------------|\n| `app.tools.expand` | `ctrl+o` | Свернуть или развернуть выходные данные инструмента |\n| `app.message.copy` | `ctrl+x` | Скопируйте последнее сообщение помощника или выбранное сообщение в `/tree`. |\n| `app.message.followUp` | `alt+enter` | Сообщение о последующем сообщении в очереди |\n| `app.message.dequeue` | `alt+up` | Восстановить сообщения в очереди в редакторе |\n\n### Древовидная навигация\n\n| Идентификатор сочетания клавиш | По умолчанию | Описание |\n|--------|---------|-------------|\n| `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | Свернуть текущий сегмент ветки или перейти к началу предыдущего сегмента. |\n| `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | Разверните текущий сегмент ветки или перейдите к началу или концу следующего сегмента. |\n| `app.tree.editLabel` | `shift+l` | Редактировать метку на выбранном узле дерева |\n| `app.tree.toggleLabelTimestamp` | `shift+t` | Переключить временные метки меток в дереве |\n| `app.tree.filter.default` | `ctrl+d` | Установить фильтр дерева для просмотра по умолчанию |\n| `app.tree.filter.noTools` | `ctrl+t` | Переключить фильтр дерева, который скрывает результаты инструмента |\n| `app.tree.filter.userOnly` | `ctrl+u` | Переключить фильтр дерева, который показывает только сообщения пользователя |\n| `app.tree.filter.labeledOnly` | `ctrl+l` | Переключить фильтр дерева, который показывает только помеченные записи |\n| `app.tree.filter.all` | `ctrl+a` | Переключить фильтр дерева, который показывает все записи |\n| `app.tree.filter.cycleForward` | `ctrl+o` | Циклический фильтр дерева вперед |\n| `app.tree.filter.cycleBackward` | `shift+ctrl+o` | Циклический фильтр дерева назад |\n\n### Селектор с областью действия Models\n\nИспользуется внутри селектора моделей с ограниченной областью действия (открывается через `/scoped-models`).\n\n| Идентификатор сочетания клавиш | По умолчанию | Описание |\n|--------|---------|-------------|\n| `app.models.save` | `ctrl+s` | Сохранить текущий выбор модели в настройках |\n| `app.models.enableAll` | `ctrl+a` | Включить все модели (или все, соответствующие текущему поиску) |\n| `app.models.clearAll` | `ctrl+x` | Очистить все модели (или все, соответствующие текущему поиску) |\n| `app.models.toggleProvider` | `ctrl+p` | Переключить все модели текущего поставщика |\n| `app.models.reorderUp` | `alt+up` | Переместить выбранную модель вверх в порядке цикла |\n| `app.models.reorderDown` | `alt+down` | Переместить выбранную модель вниз в порядке цикла |\n\n## Пользовательская конфигурация\n\nСоздайте `~/.pi/agent/keybindings.json`:\n\n```json\n{\n  \"tui.editor.historyPrevious\": \"ctrl+p\",\n  \"tui.editor.historyNext\": \"ctrl+n\",\n  \"tui.editor.deleteWordBackward\": [\"ctrl+w\", \"alt+backspace\"]\n}\n```\n\nКаждое действие может иметь одну клавишу или массив клавиш. Пользовательская конфигурация переопределяет настройки по умолчанию.\n\nВ родной Windows `app.suspend` не имеет привязки по умолчанию, поскольку терминалы Windows не поддерживают управление заданиями Unix. Если вы привяжете его вручную, pi вместо приостановки отобразит сообщение о состоянии. В WSL по-прежнему применяется обычное поведение Linux `ctrl+z`/`fg`.\n\n### Пример Emacs\n\n```json\n{\n  \"tui.editor.historyPrevious\": \"ctrl+p\",\n  \"tui.editor.historyNext\": \"ctrl+n\",\n  \"tui.editor.cursorLeft\": [\"left\", \"ctrl+b\"],\n  \"tui.editor.cursorRight\": [\"right\", \"ctrl+f\"],\n  \"tui.editor.cursorWordLeft\": [\"alt+left\", \"alt+b\"],\n  \"tui.editor.cursorWordRight\": [\"alt+right\", \"alt+f\"],\n  \"tui.editor.deleteCharForward\": [\"delete\", \"ctrl+d\"],\n  \"tui.editor.deleteCharBackward\": [\"backspace\", \"ctrl+h\"],\n  \"tui.input.newLine\": [\"shift+enter\", \"ctrl+j\"]\n}\n```\n\n### Пример Вима\n\n```json\n{\n  \"tui.editor.cursorUp\": [\"up\", \"alt+k\"],\n  \"tui.editor.cursorDown\": [\"down\", \"alt+j\"],\n  \"tui.editor.cursorLeft\": [\"left\", \"alt+h\"],\n  \"tui.editor.cursorRight\": [\"right\", \"alt+l\"],\n  \"tui.editor.cursorWordLeft\": [\"alt+left\", \"alt+b\"],\n  \"tui.editor.cursorWordRight\": [\"alt+right\", \"alt+w\"]\n}\n```","sourceFile":"keybindings.md"},"llama-cpp":{"title":"llama.cpp","markdown":"Pi поддерживает сервер маршрутизатора [llama.cpp](https://github.com/ggml-org/llama.cpp). Маршрутизатор обнаруживает несколько моделей GGUF и загружает или выгружает их по требованию.\n\nИспользуйте текущую сборку llama.cpp с поддержкой маршрутизатора. Следуйте [build instructions](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) или установите [prebuilt release](https://github.com/ggml-org/llama.cpp/releases) для своей платформы.\n\n## Запустите маршрутизатор\n\nНачните с `llama-server` без `--model` или `-m`. Передача модели запускает режим одной модели вместо режима маршрутизатора.\n\n```bash\nllama-server \\\n  --models-dir ~/models \\\n  --no-models-autoload \\\n  --jinja \\\n  --host 127.0.0.1 \\\n  --port 8080 \\\n  -ngl 999 \\\n  -c 32768\n```\n\nВажные параметры:\n\n- `--models-dir ~/models` обнаруживает локальные файлы GGUF.\n- `--no-models-autoload` продолжает загружаться явно до `/llama`.\n- `--jinja` включает совместимые шаблоны чата и вызов инструментов.\n- `-ngl 999` выгружает как можно больше слоев в графический процессор.\n- `-c 32768` устанавливает контекстное окно для каждой загруженной модели. Опустите его, чтобы использовать собственный контекст модели, для которого может потребоваться значительно больше памяти.\n\nОднофайловая модель может находиться непосредственно в каталоге модели. Поместите мультимодальные и мультиосколочные модели в отдельные подкаталоги:\n\n```text\n~/models/\n├── llama-3.2-1b-Q4_K_M.gguf\n├── gemma-3-4b-it-Q4_K_M/\n│   ├── gemma-3-4b-it-Q4_K_M.gguf\n│   └── mmproj-F16.gguf\n└── large-model-Q4_K_M/\n    ├── large-model-Q4_K_M-00001-of-00003.gguf\n    ├── large-model-Q4_K_M-00002-of-00003.gguf\n    └── large-model-Q4_K_M-00003-of-00003.gguf\n```\n\nПерезагрузите маршрутизатор после добавления файлов вручную. Для размеров контекста каждой модели и других параметров используйте [llama.cpp model presets](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md#model-presets).\n\n## Настроить Pi\n\nЗапустите Pi и настройте провайдера:\n\n```text\n/login llama.cpp\n```\n\nВведите URL-адрес маршрутизатора и необязательно API key. URL-адрес по умолчанию — `http://127.0.0.1:8080`.\n\nПеременные среды могут устанавливать одни и те же значения без `/login`:\n\n```bash\nexport LLAMA_BASE_URL=http://127.0.0.1:8080\nexport LLAMA_API_KEY=optional-secret\npi\n```\n\nЕсли сервер использует API key, начните `llama-server` с соответствующего значения `--api-key`. Сохраните `--host 127.0.0.1` для локального доступа.\n\n## Управление моделями\n\nБегать:\n\n```text\n/llama\n```\n\n- Выберите выгруженную модель, чтобы загрузить ее.\n- Выберите загруженную модель, чтобы выгрузить ее.\n- Выберите **Загрузить модель…**, найдите Hugging Face, затем выберите репозиторий и квантование. Точные значения `owner/repository[:quant]` также работают.\n- Нажмите Escape во время загрузки или скачивания, чтобы подтвердить отмену.\n\nПоиск Hugging Face использует `HF_TOKEN`, если он установлен, затем проверяет `$HF_TOKEN_PATH`, `$HF_HOME/token`, `$XDG_CACHE_HOME/huggingface/token` и `~/.cache/huggingface/token`. Поиск также работает без аутентификации, при условии соблюдения более низких ограничений скорости. Pi предупреждает перед загрузкой закрытых репозиториев и дает ссылку на их страницу доступа. Сервер llama.cpp выполняет загрузку, поэтому его процесс также должен иметь `HF_TOKEN`, когда выбранному репозиторию требуется доступ.\n\nЕсли загружены другие модели, Pi спрашивает, следует ли сначала выгрузить их или оставить загруженными. Pi не выгружает модели автоматически и никогда не удаляет файлы моделей. Маршрутизатор может использоваться совместно с другими клиентами, поэтому `/llama` всегда отображает текущее состояние маршрутизатора.\n\nВ `/model` отображаются только загруженные модели. После загрузки модели запустите `/model`, чтобы выбрать ее для текущего сеанса Pi.\n\nЕсли маршрутизатор отключается, `/llama` отображает **Повторить** и **Закрыть**. Повторная попытка повторно подключается и обновляет состояние модели без повторного воспроизведения прерванной операции.\n\n## Поиск неисправностей\n\nУбедитесь, что маршрутизатор доступен:\n\n```bash\ncurl http://127.0.0.1:8080/health\ncurl http://127.0.0.1:8080/models\n```\n\n- **Нет моделей в `/llama`:** Проверьте `--models-dir`, структуру каталога и перезапустите маршрутизатор.\n- **Модель отсутствует в `/model`:** Сначала загрузите ее с помощью `/llama`.\n- **Загрузка не удалась или используется слишком много памяти:** Уменьшите `-c` или выгрузите другую модель.\n- **Сервер не в режиме маршрутизатора:** Запустите его без `--model`, `-m` или `-hf`.","sourceFile":"llama-cpp.md"},"models":{"title":"Пользовательский Models","markdown":"Добавляйте пользовательских поставщиков и модели (Ollama, vLLM, LM Studio, прокси) через `~/.pi/agent/models.json`.\n\n## Оглавление\n\n- [Minimal Example](#minimal-example)\n- [Full Example](#full-example)\n- [Supported APIs](#supported-apis)\n- [Provider Configuration](#provider-configuration)\n- [Model Configuration](#model-configuration)\n- [Overriding Built-in Providers](#overriding-built-in-providers)\n- [Per-model Overrides](#per-model-overrides)\n- [Anthropic Messages Compatibility](#anthropic-messages-compatibility)\n- [OpenAI Compatibility](#openai-compatibility)\n\n## Минимальный пример\n\nДля локальных моделей (Ollama, LM Studio, vLLM) для каждой модели требуется только `id`:\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        { \"id\": \"llama3.1:8b\" },\n        { \"id\": \"qwen2.5-coder:7b\" }\n      ]\n    }\n  }\n}\n```\n\nЗначение `apiKey` является заполнителем, поскольку Оллама его игнорирует. pi по-прежнему рассматривает модели как требующие аутентификации, прежде чем они появятся в `/model`, поэтому локальные серверы без ключа должны сохранять фиктивное значение, сохранять ключ для этого провайдера с помощью `/login` или передавать `--api-key` при выборе модели.\n\nНекоторые OpenAI-совместимые серверы не понимают роль `developer`, используемую для моделей, способных рассуждать. Для этих провайдеров установите для `compat.supportsDeveloperRole` значение `false`, чтобы pi вместо этого отправлял системное приглашение в виде сообщения `system`. Если сервер также не поддерживает `reasoning_effort`, установите также `compat.supportsReasoningEffort` на `false`.\n\nВы можете установить `compat` на уровне поставщика, чтобы применить его ко всем моделям, или на уровне модели, чтобы переопределить конкретную модель. Обычно это относится к Ollama, vLLM, SGLang и аналогичным серверам, совместимым с OpenAI.\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"compat\": {\n        \"supportsDeveloperRole\": false,\n        \"supportsReasoningEffort\": false\n      },\n      \"models\": [\n        {\n          \"id\": \"gpt-oss:20b\",\n          \"reasoning\": true\n        }\n      ]\n    }\n  }\n}\n```\n\n## Полный пример\n\nПереопределите значения по умолчанию, если вам нужны определенные значения:\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        {\n          \"id\": \"llama3.1:8b\",\n          \"name\": \"Llama 3.1 8B (Local)\",\n          \"reasoning\": false,\n          \"input\": [\"text\"],\n          \"contextWindow\": 128000,\n          \"maxTokens\": 32000,\n          \"cost\": { \"input\": 0, \"output\": 0, \"cacheRead\": 0, \"cacheWrite\": 0 }\n        }\n      ]\n    }\n  }\n}\n```\n\nФайл перезагружается каждый раз, когда вы открываете `/model`. Редактировать во время сеанса; перезагрузка не требуется.\n\n## Пример Google AI Studio\n\nИспользуйте `google-generative-ai` с `baseUrl`, чтобы добавить модели из Google AI Studio, включая пользовательские записи Gemma 4:\n\n```json\n{\n  \"providers\": {\n    \"my-google\": {\n      \"baseUrl\": \"https://generativelanguage.googleapis.com/v1beta\",\n      \"api\": \"google-generative-ai\",\n      \"apiKey\": \"$GEMINI_API_KEY\",\n      \"models\": [\n        {\n          \"id\": \"gemma-4-31b-it\",\n          \"name\": \"Gemma 4 31B\",\n          \"input\": [\"text\", \"image\"],\n          \"contextWindow\": 262144,\n          \"reasoning\": true\n        }\n      ]\n    }\n  }\n}\n```\n\n`baseUrl` требуется при добавлении пользовательских моделей к типу `google-generative-ai` API.\n\n## Поддерживается APIс\n\n| API | Описание |\n|-----|-------------|\n| `openai-completions` | Завершения чата OpenAI (наиболее совместимые) |\n| `openai-responses` | Ответы OpenAI API |\n| `anthropic-messages` | Антропные сообщения API |\n| `google-generative-ai` | Генеративный искусственный интеллект Google |\n\nУстановите `api` на уровне поставщика (по умолчанию для всех моделей) или уровне модели (переопределение для каждой модели).\n\n## Конфигурация поставщика\n\n| Поле | Описание |\n|-------|-------------|\n| `baseUrl` | API URL-адрес конечной точки |\n| `api` | тип API (см. выше) |\n| `apiKey` | Дополнительная конфигурация API key (см. разрешение значений ниже). Опустите его, если аутентификация обеспечивается с помощью `/login`/`auth.json` или CLI `--api-key`. |\n| `oauth` | Динамический тип OAuth поставщика. В настоящее время поддерживает `\"radius\"`; требуется шлюз `baseUrl`. |\n| `headers` | Пользовательские заголовки (см. разрешение значений ниже) |\n| `authHeader` | Установите `true`, чтобы автоматически добавлять `Authorization: Bearer <apiKey>`. |\n| `models` | Массив конфигураций модели |\n| `modelOverrides` | Переопределения для каждой модели для встроенных или зарегистрированных в расширении моделей этого поставщика. |\n\nДля поставщиков с `models` для конфигураций невстроенных поставщиков требуются `baseUrl` и значение `api` либо на уровне поставщика, либо на уровне модели. `apiKey` не требуется для загрузки файла: модели становятся доступными, когда аутентификация настроена через `/login`/`auth.json`, CLI `--api-key` или провайдера `apiKey`. Если аутентификация не настроена, модели загружаются, но остаются недоступными в `/model` и `--list-models`.\n\n### Разрешение значений\n\nПоля `apiKey` и `headers` поддерживают выполнение команд, интерполяцию среды и литералы:\n\n- **Команда оболочки:** `\"!command\"` в начале выполняет все значение как команду и использует stdout\n  ```json\n  \"apiKey\": \"!security find-generic-password -ws 'anthropic'\"\n  \"apiKey\": \"!op read 'op://vault/item/credential'\"\n  ```\n- **Интерполяция среды:** `\"$ENV_VAR\"` или `\"${ENV_VAR}\"` использует значение именованной переменной. Интерполяция работает внутри больших литералов.\n  ```json\n  \"apiKey\": \"$MY_API_KEY\"\n  \"apiKey\": \"${KEY_PREFIX}_${KEY_SUFFIX}\"\n  ```\n  `$FOO_BAR` — переменная `FOO_BAR`; используйте `${FOO}_BAR`, когда `BAR` — это буквальный текст. Отсутствие переменных среды делает значение неразрешенным.\n- **Эскейп:** `\"$\"` выдает литерал `\"$\"`; `\"$!\"` выдает литерал `\"!\"`, не запуская выполнение команды.\n  ```json\n  \"apiKey\": \"$$literal-dollar-prefix\"\n  \"apiKey\": \"$!literal-bang-prefix\"\n  ```\n- **Буквальное значение:** Используется напрямую. Обычные строки в верхнем регистре, такие как `MY_API_KEY`, являются литералами; используйте `$MY_API_KEY` для переменных среды.\n  ```json\n  \"apiKey\": \"sk-...\"\n  ```\n\nДля `models.json` команды оболочки обрабатываются во время запроса. pi намеренно не применяет встроенную логику TTL, устаревшего повторного использования или восстановления для произвольных команд. Разным командам нужны разные стратегии кэширования и отказов, и pi не может сделать правильный выбор.\n\nЕсли ваша команда медленная, дорогая, ограничена по скорости или должна продолжать использовать предыдущее значение при временных сбоях, оберните ее в свой собственный сценарий или команду, которая реализует желаемое поведение кэширования или TTL.\n\n`/model` проверки доступности используют настроенное присутствие аутентификации и не выполняют команды оболочки.\n\n### Пользовательские заголовки\n\n```json\n{\n  \"providers\": {\n    \"custom-proxy\": {\n      \"baseUrl\": \"https://proxy.example.com/v1\",\n      \"apiKey\": \"$MY_API_KEY\",\n      \"api\": \"anthropic-messages\",\n      \"headers\": {\n        \"x-portkey-api-key\": \"$PORTKEY_API_KEY\",\n        \"x-secret\": \"!op read 'op://vault/item/secret'\"\n      },\n      \"models\": [...]\n    }\n  }\n}\n```\n\n## Конфигурация модели\n\n| Поле | Необходимый | По умолчанию | Описание |\n|-------|----------|---------|-------------|\n| `id` | Да | — | Идентификатор модели (передается в API) |\n| `name` | Нет | `id` | Читаемая человеком этикетка модели. Используется для сопоставления (шаблоны `--model`) и отображается как вторичный подробный текст модели. |\n| `api` | Нет | `api` провайдера | Переопределить API поставщика для этой модели. |\n| `reasoning` | Нет | `false` | Поддерживает расширенное мышление |\n| `thinkingLevelMap` | Нет | опущен | Сопоставляет уровни мышления «пи» со значениями поставщика и отмечает неподдерживаемые уровни (см. ниже). |\n| `input` | Нет | `[\"text\"]` | Типы ввода: `[\"text\"]` или `[\"text\", \"image\"]` |\n| `contextWindow` | Нет | `128000` | Размер контекстного окна в токенах |\n| `maxTokens` | Нет | `16384` | Максимальное количество токенов вывода |\n| `samplingParams` | Нет | опущен | Параметры выборки дословно объединены в тело каждого запроса (см. ниже). |\n| `cost` | Нет | все нули | Ставки за миллион токенов с дополнительными ценовыми уровнями входных данных для всего запроса |\n| `compat` | Нет | провайдер `compat` | Переопределяет совместимость поставщика. Объединяется с уровнем провайдера `compat`, если оба установлены. |\n\nУровень затрат предоставляет полный набор альтернативных тарифов и применяется к полному запросу, когда общее использование входных данных (`input + cacheRead + cacheWrite`) превышает `inputTokensAbove`. При совпадении нескольких уровней выигрывает наивысший порог.\n\n```json\n{\n  \"cost\": {\n    \"input\": 5,\n    \"output\": 30,\n    \"cacheRead\": 0.5,\n    \"cacheWrite\": 6.25,\n    \"tiers\": [\n      {\n        \"inputTokensAbove\": 272000,\n        \"input\": 10,\n        \"output\": 45,\n        \"cacheRead\": 1,\n        \"cacheWrite\": 12.5\n      }\n    ]\n  }\n}\n```\n\nТекущее поведение:\n- `/model`, `--list-models`, а в интерактивном нижнем колонтитуле записи отображаются по модели `id`.\n- Настроенный `name` используется для сопоставления модели и дополнительного подробного текста модели. Он не заменяет идентификатор модели нижнего колонтитула/строки состояния.\n\n### Параметры выборки\n\n`samplingParams` — это объект свободной формы, дословно включаемый в каждое тело запроса модели после того, как поля pi задаются сами собой, поэтому его ключи выигрывают. Используйте его для отправки параметров выборки, которые pi не моделирует, включая специфичные для сервера, такие как `min_p` llama.cpp или `top_k` vLLM:\n\n```json\n{\n  \"id\": \"deepseek-v4-flash\",\n  \"samplingParams\": {\n    \"temperature\": 1.0,\n    \"top_p\": 0.95,\n    \"top_k\": 0,\n    \"min_p\": 0.0\n  }\n}\n```\n\nЕго применяют только OpenAI-совместимые API (`openai-completions`, `openai-responses`, `azure-openai-responses`); другие API игнорируют это. Ключи переопределяют именованные поля запроса pi (например, ключ `temperature` здесь превосходит температуру уровня запроса), поэтому предпочитайте его как единственный источник истинности выборки для модели. В `modelOverrides`, `samplingParams` объединяется по ключу со значением базовой модели.\n\n### Карта уровня мышления\n\nИспользуйте `thinkingLevelMap` в модели, чтобы описать элементы управления мышлением, специфичные для модели. Ключи — это уровни Пи-мышления: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Карты могут содержать дыры; например, модель может выставлять `high` и `max`, не выставляя `xhigh`.\n\nЗначения имеют три состояния:\n\n| Ценить | Значение |\n|-------|---------|\n| опущен | Стандартные уровни до `high` используют сопоставление поставщика по умолчанию; расширенные уровни `xhigh` и `max` не поддерживаются |\n| нить | Уровень поддерживается, и это значение отправляется провайдеру. |\n| `null` | Уровень не поддерживается и скрыт/пропущен/зарезан |\n\nПример модели, которая поддерживает только рассуждения «выключено», «высокое» и «максимальное»:\n\n```json\n{\n  \"id\": \"deepseek-v4-pro\",\n  \"reasoning\": true,\n  \"thinkingLevelMap\": {\n    \"minimal\": null,\n    \"low\": null,\n    \"medium\": null,\n    \"high\": \"high\",\n    \"xhigh\": null,\n    \"max\": \"max\"\n  }\n}\n```\n\nПример модели, в которой мышление невозможно отключить:\n\n```json\n{\n  \"id\": \"always-thinking-model\",\n  \"reasoning\": true,\n  \"thinkingLevelMap\": {\n    \"off\": null\n  }\n}\n```\n\nМиграция: старые конфигурации, использовавшие `compat.reasoningEffortMap`, должны переместить это сопоставление на уровень модели `thinkingLevelMap`. Используйте `null` для уровней, которые не должны отображаться в пользовательском интерфейсе.\n\n## Переопределение встроенного Providers\n\nНаправьте встроенного провайдера через прокси без переопределения модели:\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": {\n      \"baseUrl\": \"https://my-proxy.example.com/v1\"\n    }\n  }\n}\n```\n\nВсе встроенные модели Anthropic остаются доступными. Существующая аутентификация OAuth или API key продолжает работать.\n\nЧтобы объединить пользовательские модели со встроенным поставщиком, включите массив `models`:\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": {\n      \"baseUrl\": \"https://my-proxy.example.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"api\": \"anthropic-messages\",\n      \"models\": [...]\n    }\n  }\n}\n```\n\nСемантика слияния:\n- Встроенные модели сохраняются.\n- Пользовательские модели обновляются с помощью `id` внутри поставщика.\n- Если пользовательская модель `id` соответствует встроенной модели `id`, пользовательская модель заменяет эту встроенную модель.\n- Если пользовательская модель `id` новая, она добавляется вместе со встроенными моделями.\n\n## Переопределения для каждой модели\n\nИспользуйте `modelOverrides`, чтобы настроить встроенные модели и соответствующие модели, зарегистрированные в расширении, без замены полного списка моделей поставщика.\n\n```json\n{\n  \"providers\": {\n    \"openrouter\": {\n      \"modelOverrides\": {\n        \"anthropic/claude-sonnet-4\": {\n          \"name\": \"Claude Sonnet 4 (Bedrock Route)\",\n          \"compat\": {\n            \"openRouterRouting\": {\n              \"only\": [\"amazon-bedrock\"]\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n`modelOverrides` поддерживает следующие поля для каждой модели: `name`, `reasoning`, `thinkingLevelMap`, `input`, `cost` (частично), `contextWindow`, `maxTokens`, `samplingParams` (объединены по ключу), `headers`, `compat`.\n\nDirect OpenAI GPT-5.6 Sol, Terra и Luna по умолчанию используют контекстное окно `272000`, поэтому запросы остаются в пределах ценовой категории OpenAI с коротким контекстом. Чтобы включить контекстное окно OpenAI размером 1,05M, увеличьте его для каждой используемой вами модели:\n\n```json\n{\n  \"providers\": {\n    \"openai\": {\n      \"modelOverrides\": {\n        \"gpt-5.6-sol\": {\n          \"contextWindow\": 1050000\n        }\n      }\n    }\n  }\n}\n```\n\nПри переопределении сохраняются встроенные метаданные цен. Запросы с общим количеством входных токенов более 272 000 используют скорость GPT-5.6 для длинного контекста для всего запроса. При необходимости примените то же переопределение к `gpt-5.6-terra` или `gpt-5.6-luna`.\n\nЗамечания по поведению:\n- `modelOverrides` применяются к моделям встроенных поставщиков и соответствующим моделям поставщиков, зарегистрированным в расширении.\n- Неизвестные идентификаторы моделей игнорируются.\n- Вы можете комбинировать `baseUrl`/`headers` уровня поставщика с `modelOverrides`.\n- Переопределение `name` изменяет только соответствие модели и текст дополнительных сведений; в нижнем колонтитуле и списках основных моделей по-прежнему отображается модель `id`.\n- Если для поставщика также определено `models`, пользовательские модели объединяются после встроенных переопределений. Пользовательская модель с тем же `id` заменяет переопределенную запись встроенной модели.\n\n## Совместимость антропных сообщений\n\nДля провайдеров или прокси, использующих `api: \"anthropic-messages\"`, используйте `compat` для управления совместимостью запросов, специфичных для Anthropic.\n\nПо умолчанию pi отправляет `eager_input_streaming: true` для каждого инструмента. Если прокси-сервер или серверная часть, совместимая с Anthropic, отклоняет это поле, установите для `supportsEagerToolInputStreaming` значение `false`. Pi будет опускать `tools[].eager_input_streaming` и вместо этого отправлять устаревший бета-заголовок `fine-grained-tool-streaming-2025-05-14` для запросов с поддержкой инструментов.\n\nНекоторые антропные модели требуют адаптивного мышления (`thinking.type: \"adaptive\"` плюс `output_config.effort`) вместо устаревшей полезной нагрузки мышления, основанной на бюджете. Встроенные модели устанавливают это автоматически. Для пользовательских поставщиков или псевдонимов, которые направляются к этим моделям, установите от `forceAdaptiveThinking` до `true`.\n\nНекоторые провайдеры, совместимые с Anthropic, выдают блоки мышления с пустыми подписями и все равно ожидают их воспроизведения. Установите от `allowEmptySignature` до `true` только для этих поставщиков; настоящий Антропик отвергает пустые мыслительные сигнатуры.\n\nВстроенные антропные модели включают `supportsStrictTools` в метаданных модели. Пользовательские модели, совместимые с Anthropic, должны установить для него значение `true`, если их конечная точка принимает строгие определения инструмента схемы JSON.\n\n```json\n{\n  \"providers\": {\n    \"anthropic-proxy\": {\n      \"baseUrl\": \"https://proxy.example.com\",\n      \"api\": \"anthropic-messages\",\n      \"apiKey\": \"$ANTHROPIC_PROXY_KEY\",\n      \"compat\": {\n        \"supportsEagerToolInputStreaming\": false,\n        \"supportsLongCacheRetention\": true,\n        \"forceAdaptiveThinking\": true,\n        \"allowEmptySignature\": true\n      },\n      \"models\": [\n        {\n          \"id\": \"claude-opus-4-7\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"]\n        }\n      ]\n    }\n  }\n}\n```\n\n| Поле | Описание |\n|-------|-------------|\n| `supportsEagerToolInputStreaming` | Принимает ли поставщик `eager_input_streaming` для каждого инструмента. По умолчанию: `true`. Установите значение `false`, чтобы опустить это поле и использовать устаревший заголовок бета-версии детальной потоковой передачи инструмента для запросов с поддержкой инструмента. |\n| `supportsLongCacheRetention` | Принимает ли поставщик Anthropic длительное хранение кэша (`cache_control.ttl: \"1h\"`), когда срок хранения кэша равен `long`. По умолчанию: `true`. |\n| `sendSessionAffinityHeaders` | Отправлять ли `x-session-affinity` из идентификатора сеанса, когда кэширование включено. По умолчанию: определяется автоматически для известных поставщиков. |\n| `supportsCacheControlOnTools` | Принимает ли поставщик маркеры `cache_control` в стиле Anthropic в определениях инструментов. По умолчанию: `true`. |\n| `forceAdaptiveThinking` | Отправлять ли адаптивное мышление (`thinking.type: \"adaptive\"` плюс `output_config.effort`) для этой модели. Встроенные адаптивные модели устанавливают это автоматически. По умолчанию: `false`. |\n| `allowEmptySignature` | Следует ли воспроизводить пустые мыслительные подписи как `signature: \"\"` вместо преобразования мыслей в текст. По умолчанию: `false`. |\n| `supportsStrictTools` | Принимает ли поставщик строгие определения инструмента схемы JSON. По умолчанию: `false`; встроенные антропные модели позволяют использовать его в сгенерированных метаданных. |\n\n## Совместимость с OpenAI\n\nДля провайдеров с частичной совместимостью с OpenAI используйте поле `compat`.\n\n- На уровне поставщика `compat` применяет значения по умолчанию ко всем моделям этого поставщика.\n- `compat` на уровне модели переопределяет значения уровня поставщика для этой модели.\n\n```json\n{\n  \"providers\": {\n    \"local-llm\": {\n      \"baseUrl\": \"http://localhost:8080/v1\",\n      \"api\": \"openai-completions\",\n      \"compat\": {\n        \"supportsUsageInStreaming\": false,\n        \"maxTokensField\": \"max_tokens\"\n      },\n      \"models\": [...]\n    }\n  }\n}\n```\n\n| Поле | Описание |\n|-------|-------------|\n| `supportsStore` | Провайдер поддерживает поле `store` |\n| `supportsDeveloperRole` | Используйте роль `developer` и `system` |\n| `supportsReasoningEffort` | Поддержка параметра `reasoning_effort` |\n| `supportsUsageInStreaming` | Поддерживает `stream_options: { include_usage: true }` (по умолчанию: `true`) |\n| `supportsFinishReason` | Включают ли потоковые ответы `finish_reason`. Когда `false`, pi выводит `stop` или `toolUse`, когда поток заканчивается. По умолчанию: `true`. |\n| `maxTokensField` | Используйте `max_completion_tokens` или `max_tokens`. |\n| `requiresToolResultName` | Включайте `name` в сообщения о результатах работы инструмента. |\n| `requiresAssistantAfterToolResult` | Вставьте сообщение помощника перед сообщением пользователя после результатов инструмента. |\n| `requiresThinkingAsText` | Преобразуйте мыслительные блоки в обычный текст |\n| `requiresReasoningContentOnAssistantMessages` | Включать пустой `reasoning_content` во все воспроизводимые сообщения помощника, если включено рассуждение. |\n| `thinkingFormat` | Используйте `reasoning_effort`, `openrouter`, `deepseek`, `together`, `baseten`, `zai`, `qwen`, `chat-template` или `qwen-chat-template` параметры мышления. |\n| `chatTemplateKwargs` | значения `chat_template_kwargs` для `thinkingFormat: \"chat-template\"`; используйте `{ \"$var\": \"thinking.enabled\" }` или `{ \"$var\": \"thinking.effort\" }` для значений мышления, контролируемых числом Пи |\n| `chatTemplateArgs` | значения `chat_template_args` для `thinkingFormat: \"baseten\"`; используйте `{ \"$var\": \"thinking.enabled\" }` или `{ \"$var\": \"thinking.effort\" }` для значений мышления, контролируемых числом Пи |\n| `cacheControlFormat` | Используйте маркеры `cache_control` в стиле Anthropic в системной подсказке, последнем определении инструмента и текстовом содержимом последнего пользователя, помощника или результата инструмента. В настоящее время поддерживается только `anthropic`. |\n| `sendSessionAffinityHeaders` | Для `openai-completions` отправляйте заголовки привязки сеанса из идентификатора сеанса, когда кэширование включено. По умолчанию: `false`. |\n| `sessionAffinityFormat` | Для `openai-completions` и `openai-responses` формат заголовка привязки к сеансу: `openai` отправляет `session_id`/`x-client-request-id` (дополнения также `x-session-affinity`), `openai-nosession` опускает заголовок `session_id`, содержащий подчеркивание, `openrouter` отправляет `x-session-id`. Не влияет на параметр тела `prompt_cache_key`. По умолчанию: определяется автоматически. |\n| `supportsStrictMode` | Принимает ли поставщик строгие определения инструмента функции схемы JSON. Значения по умолчанию зависят от API; встроенные модели OpenAI содержат явные метаданные о возможностях. |\n| `supportsOpenAIGrammarTools` | Используют ли OpenAI-совместимые API пользовательские инструменты грамматики Lark/regex. Когда `false`, инструменты с грамматическими ограничениями возвращаются к обычным функциональным инструментам. По умолчанию: `false`; встроенный каталог моделей позволяет использовать его для моделей GPT-5+ на OpenAI, OpenAI Codex, Azure OpenAI, GitHub Copilot, opencode и Cloudflare AI Gateway. |\n| `deferredToolsMode` | Используйте отложенную сериализацию инструмента для конкретного поставщика. В настоящее время для OpenAI-совместимого формата завершения чата Кими поддерживается только `\"kimi\"`. |\n| `supportsLongCacheRetention` | Принимает ли поставщик длительное хранение кэша, когда сохранение кэша равно `long`: `prompt_cache_retention: \"24h\"` для кэширования подсказок OpenAI или `cache_control.ttl: \"1h\"`, когда `cacheControlFormat` равно `anthropic`. По умолчанию: `true`. |\n| `openRouterRouting` | Настройки маршрутизации провайдера OpenRouter. Этот объект отправляется как есть в поле `provider` поля [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). |\n| `vercelGatewayRouting` | Конфигурация маршрутизации Vercel AI Gateway для выбора провайдера (`only`, `order`) |\n\n`openrouter` использует `reasoning: { effort }`. `together` использует `reasoning: { enabled }`, а также `reasoning_effort`, когда `supportsReasoningEffort` включено. `qwen` использует верхний уровень `enable_thinking`. Используйте `qwen-chat-template` для локальных Qwen-совместимых серверов, которым требуются `chat_template_kwargs.enable_thinking` и `preserve_thinking`. Используйте `chat-template` для шаблонов чатов vLLM/Hugging Face, для которых требуется настраиваемый `chat_template_kwargs`, например `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }` для шаблонов DeepSeek V3.x. Используйте `thinkingFormat: \"baseten\"` с `chatTemplateArgs` для поставщиков, которые предоставляют элементы управления переключением через `chat_template_args` и при необходимости поддерживают `reasoning_effort` верхнего уровня.\n\n`cacheControlFormat: \"anthropic\"` предназначен для поставщиков, совместимых с OpenAI, которые предоставляют кэширование подсказок в стиле Anthropic с помощью маркеров `cache_control` в текстовом содержимом и определениях инструментов.\n\nПример:\n\n```json\n{\n  \"providers\": {\n    \"openrouter\": {\n      \"baseUrl\": \"https://openrouter.ai/api/v1\",\n      \"apiKey\": \"$OPENROUTER_API_KEY\",\n      \"api\": \"openai-completions\",\n      \"models\": [\n        {\n          \"id\": \"openrouter/anthropic/claude-3.5-sonnet\",\n          \"name\": \"OpenRouter Claude 3.5 Sonnet\",\n          \"compat\": {\n            \"openRouterRouting\": {\n              \"allow_fallbacks\": true,\n              \"require_parameters\": false,\n              \"data_collection\": \"deny\",\n              \"zdr\": true,\n              \"enforce_distillable_text\": false,\n              \"order\": [\"anthropic\", \"amazon-bedrock\", \"google-vertex\"],\n              \"only\": [\"anthropic\", \"amazon-bedrock\"],\n              \"ignore\": [\"gmicloud\", \"friendli\"],\n              \"quantizations\": [\"fp16\", \"bf16\"],\n              \"sort\": {\n                \"by\": \"price\",\n                \"partition\": \"model\"\n              },\n              \"max_price\": {\n                \"prompt\": 10,\n                \"completion\": 20\n              },\n              \"preferred_min_throughput\": {\n                \"p50\": 100,\n                \"p90\": 50\n              },\n              \"preferred_max_latency\": {\n                \"p50\": 1,\n                \"p90\": 3,\n                \"p99\": 5\n              }\n            }\n          }\n        }\n      ]\n    }\n  }\n}\n```\n\nПример шлюза Vercel AI:\n\n```json\n{\n  \"providers\": {\n    \"vercel-ai-gateway\": {\n      \"baseUrl\": \"https://ai-gateway.vercel.sh/v1\",\n      \"apiKey\": \"$AI_GATEWAY_API_KEY\",\n      \"api\": \"openai-completions\",\n      \"models\": [\n        {\n          \"id\": \"moonshotai/kimi-k2.5\",\n          \"name\": \"Kimi K2.5 (Fireworks via Vercel)\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"],\n          \"cost\": { \"input\": 0.6, \"output\": 3, \"cacheRead\": 0, \"cacheWrite\": 0 },\n          \"contextWindow\": 262144,\n          \"maxTokens\": 262144,\n          \"compat\": {\n            \"vercelGatewayRouting\": {\n              \"only\": [\"fireworks\", \"novita\"],\n              \"order\": [\"fireworks\", \"novita\"]\n            }\n          }\n        }\n      ]\n    }\n  }\n}\n```","sourceFile":"models.md"},"packages":{"title":"Pi Packages","markdown":"> pi может помочь вам создавать пакеты pi. Попросите его объединить ваши расширения, навыки, prompt templates или темы.\n\n\nВ пакеты Pi входят расширения, навыки, prompt templates и темы, поэтому вы можете поделиться ими через npm или git. Пакет может объявлять ресурсы в `package.json` по ключу `pi` или использовать обычные каталоги.\n\n## Оглавление\n\n- [Install and Manage](#install-and-manage)\n- [Package Sources](#package-sources)\n- [Creating a Pi Package](#creating-a-pi-package)\n- [Package Structure](#package-structure)\n- [Dependencies](#dependencies)\n- [Package Filtering](#package-filtering)\n- [Enable and Disable Resources](#enable-and-disable-resources)\n- [Scope and Deduplication](#scope-and-deduplication)\n\n## Установка и управление\n\n> **Безопасность:** Pi пакеты запускаются с полным доступом к системе. Extensions выполнять произвольный код, а навыки могут поручить модели выполнить любое действие, включая запуск исполняемых файлов. Просмотрите исходный код перед установкой сторонних пакетов.\n\n```bash\npi install npm:@foo/bar@1.0.0\npi install git:github.com/user/repo@v1\npi install https://github.com/user/repo  # raw URLs work too\npi install /absolute/path/to/package\npi install ./relative/path/to/package\n\npi remove npm:@foo/bar\npi list                     # show installed packages from settings\npi update                   # update pi only\npi update --all             # update pi, update packages, and reconcile pinned git refs\npi update --extensions      # update packages and reconcile pinned git refs only\npi update --models          # refresh model catalogs only\npi update --self            # update pi only\npi update --self --force    # reinstall pi even if current\npi update npm:@foo/bar      # update one package\npi update --extension npm:@foo/bar\n```\n\nЭти команды управляют пакетами pi, а `pi update` могут обновлять установку pi CLI. Чтобы удалить сам pi, см. [Quickstart](quickstart.md#uninstall).\n\nПо умолчанию `install` и `remove` записывают в настройки пользователя (`~/.pi/agent/settings.json`). Вместо этого используйте `-l` для записи в настройки проекта (`.pi/settings.json`). Настройки проекта можно передать вашей команде, и pi автоматически установит все недостающие пакеты при запуске после того, как проект станет доверенным.\n\nЧтобы попробовать пакет, не устанавливая его, используйте `--extension` или `-e`. Это устанавливается во временный каталог только для текущего запуска:\n\n```bash\npi -e npm:@foo/bar\npi -e git:github.com/user/repo\n```\n\n## Источники пакетов\n\nPi принимает три типа источника в настройках и `pi install`.\n\n### npm\n\n```\nnpm:@scope/pkg@1.2.3\nnpm:pkg\n```\n\n- Версионные спецификации закрепляются и пропускаются обновлениями пакетов (`pi update --extensions`, `pi update --all`).\n- Пользовательские установки имеют номер `~/.pi/agent/npm/`.\n- Установки проекта попадают под `.pi/npm/`.\n- Установите `npmCommand` в `settings.json`, чтобы привязать операции поиска и установки пакета npm к определенной команде оболочки, например `mise` или `asdf`.\n\nПример:\n\n```json\n{\n  \"npmCommand\": [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]\n}\n```\n\n### мерзавец\n\n```\ngit:github.com/user/repo@v1\ngit:git@github.com:user/repo@v1\nhttps://github.com/user/repo@v1\nssh://git@github.com/user/repo@v1\n```\n\n- Без префикса `git:` принимаются только URL-адреса протоколов (`https://`, `http://`, `ssh://`, `git://`).\n- С префиксом `git:` принимаются сокращенные форматы, включая `github.com/user/repo` и `git@github.com:user/repo`.\n- Поддерживаются URL-адреса HTTPS и SSH.\n- URL-адреса SSH автоматически используют настроенные вами ключи SSH (с учетом `~/.ssh/config`).\n- Для неинтерактивных запусков (например, CI) вы можете установить `GIT_TERMINAL_PROMPT=0`, чтобы отключить запросы учетных данных, и установить `GIT_SSH_COMMAND` (например, `ssh -o BatchMode=yes -o ConnectTimeout=5`), чтобы быстро завершить работу.\n- Ссылки — это закрепленные теги или коммиты. `pi update --extensions` и `pi update --all` не перемещают их на более новые ссылки, но они согласовывают существующий клон с настроенной ссылкой.\n- Используйте `pi install git:host/user/repo@new-ref`, чтобы обновить настройки и переместить существующий пакет в новую закрепленную ссылку.\n- Клонируется в `~/.pi/agent/git/<host>/<path>` (глобальный) или `.pi/git/<host>/<path>` (проект).\n- Когда сверка изменяет оформление заказа, pi сбрасывает и очищает клон, а затем запускает `npm install`, если `package.json` существует.\n\n**SSH примеры:**\n```bash\n# git@host:path shorthand (requires git: prefix)\npi install git:git@github.com:user/repo\n\n# ssh:// protocol format\npi install ssh://git@github.com/user/repo\n\n# With version ref\npi install git:git@github.com:user/repo@v1.0.0\n```\n\n### Локальные пути\n\n```\n/absolute/path/to/package\n./relative/path/to/package\n```\n\nЛокальные пути указывают на файлы или каталоги на диске и добавляются в настройки без копирования. Относительные пути разрешаются по файлу настроек, в котором они указаны. Если путь представляет собой файл, он загружается как одно расширение. Если это каталог, pi загружает ресурсы, используя правила пакета.\n\n## Создание пакета Pi\n\nДобавьте манифест `pi` в `package.json` или используйте обычные каталоги. Включите ключевое слово `pi-package` для возможности обнаружения.\n\n```json\n{\n  \"name\": \"my-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./extensions\"],\n    \"skills\": [\"./skills\"],\n    \"prompts\": [\"./prompts\"],\n    \"themes\": [\"./themes\"]\n  }\n}\n```\n\nПути указаны относительно корня пакета. Массивы поддерживают шаблоны glob и `!exclusions`.\n\n### Метаданные галереи\n\n[package gallery](https://pi.dev/packages) отображает пакеты, отмеченные `pi-package`. Добавьте поля `video` или `image`, чтобы отобразить предварительный просмотр:\n\n```json\n{\n  \"name\": \"my-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./extensions\"],\n    \"video\": \"https://example.com/demo.mp4\",\n    \"image\": \"https://example.com/screenshot.png\"\n  }\n}\n```\n\n- **видео**: только MP4. На рабочем столе автоматически воспроизводится при наведении. При нажатии открывается полноэкранный проигрыватель.\n- **изображение**: PNG, JPEG, GIF или WebP. Отображается как статический предварительный просмотр.\n\nЕсли установлены оба параметра, видео имеет приоритет.\n\n## Структура пакета\n\n### Справочники конференций\n\nЕсли манифест `pi` отсутствует, pi автоматически обнаруживает ресурсы из этих каталогов:\n\n- `extensions/` загружает файлы `.ts` и `.js`\n- `skills/` рекурсивно находит папки `SKILL.md` и загружает файлы верхнего уровня `.md` как навыки\n- `prompts/` загружает `.md` файлы\n- `themes/` загружает `.json` файлы\n\n## Зависимости\n\nСторонние зависимости времени выполнения относятся к `dependencies` в `package.json`. Зависимости, которые не регистрируют расширения, навыки, prompt templates или темы, также относятся к `dependencies`. Когда pi устанавливает пакет из npm или git, он запускает `npm install`, поэтому эти зависимости устанавливаются автоматически.\n\nPi объединяет основные пакеты для расширений и навыков. Если вы импортируете какие-либо из них, перечислите их в `peerDependencies` с диапазоном `\"*\"` и не объединяйте их: `@earendil-works/pi-ai`, `@earendil-works/pi-agent-core`, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`, `typebox`.\n\nДругие пакеты pi должны быть включены в ваш архив. Добавьте их в `dependencies` и `bundledDependencies`, затем ссылайтесь на их ресурсы через пути `node_modules/`. Pi загружает пакеты с отдельными корнями модулей, поэтому отдельные установки не конфликтуют и не используют общие модули.\n\nПример:\n\n```json\n{\n  \"dependencies\": {\n    \"shitty-extensions\": \"^1.0.1\"\n  },\n  \"bundledDependencies\": [\"shitty-extensions\"],\n  \"pi\": {\n    \"extensions\": [\"extensions\", \"node_modules/shitty-extensions/extensions\"],\n    \"skills\": [\"skills\", \"node_modules/shitty-extensions/skills\"]\n  }\n}\n```\n\n## Фильтрация пакетов\n\nОтфильтруйте то, что загружается пакетом, используя форму объекта в настройках:\n\n```json\n{\n  \"packages\": [\n    \"npm:simple-pkg\",\n    {\n      \"source\": \"npm:my-package\",\n      \"extensions\": [\"extensions/*.ts\", \"!extensions/legacy.ts\"],\n      \"skills\": [],\n      \"prompts\": [\"prompts/review.md\"],\n      \"themes\": [\"+themes/legacy.json\"]\n    }\n  ]\n}\n```\n\n`+path` и `-path` — точные пути относительно корня пакета.\n\n- Опустите ключ, чтобы загрузить все элементы этого типа.\n- Используйте `[]`, чтобы не загружать ничего из этого типа.\n- `!pattern` исключает совпадения.\n- `+path` принудительное — включает точный путь.\n- `-path` принудительно — исключает точный путь.\n- Слой фильтров поверх манифеста. Они сужают то, что уже разрешено.\n\n## Включить и отключить ресурсы\n\nИспользуйте `pi config`, чтобы включить или отключить расширения, навыки, prompt templates и темы из установленных пакетов и локальных каталогов. `pi config` запускается в глобальных настройках (`~/.pi/agent/settings.json`); нажмите Tab, чтобы переключиться между глобальным и локальным режимами проекта. Используйте `pi config -l`, чтобы начать переопределения проекта (`.pi/settings.json`), когда наследуемые глобальные ресурсы затемнены.\n\n## Область действия и дедупликация\n\nПакеты могут появляться как в глобальных настройках, так и в настройках проекта. Если один и тот же пакет появляется в обоих, запись проекта выигрывает, если только запись проекта не имеет `autoload: false`, и в этом случае она применяется как дельта по отношению к глобальной записи. Личность определяется:\n\n- npm: имя пакета.\n- git: URL-адрес репозитория без ссылки\n- локальный: разрешен абсолютный путь","sourceFile":"packages.md"},"prompt-templates":{"title":"Шаблоны подсказок","markdown":"> Пи может создать prompt templates. Попросите его создать его для вашего рабочего процесса.\n\n\nШаблоны подсказок — это Markdown фрагменты, которые расширяются до полных подсказок. Введите `/name` в редакторе, чтобы вызвать шаблон, где `name` — имя файла без `.md`.\n\n## Локации\n\nPi загружает prompt templates из:\n\n- Глобально: `~/.pi/agent/prompts/*.md`\n- Проект: `.pi/prompts/*.md` (только после того, как проекту доверяют)\n- Пакеты: `prompts/` каталогов или `pi.prompts` записей в `package.json`.\n- Настройки: `prompts` массив с файлами или каталогами.\n- CLI: `--prompt-template <path>` (повторяемый)\n\nОтключите обнаружение с помощью `--no-prompt-templates`.\n\n## Формат\n\n```markdown\n---\ndescription: Review staged git changes\n---\nReview the staged changes (`git diff --cached`). Focus on:\n- Bugs and logic errors\n- Security issues\n- Error handling gaps\n```\n\n- Имя файла становится именем команды. `review.md` становится `/review`.\n- `description` не является обязательным. Если оно отсутствует, используется первая непустая строка.\n- `argument-hint` не является обязательным. Если этот параметр установлен, подсказка отображается перед описанием в раскрывающемся списке автозаполнения.\n\n### Подсказки по аргументам\n\nИспользуйте `argument-hint` во вступительной части, чтобы отобразить ожидаемые аргументы при автозаполнении. Используйте `<angle brackets>` для обязательных аргументов и `[square brackets]` для необязательных:\n\n```markdown\n---\ndescription: Review PRs from URLs with structured issue and code analysis\nargument-hint: \"<PR-URL>\"\n---\n```\n\nЭто отображается в раскрывающемся списке автозаполнения как:\n\n```\n→ pr   <PR-URL>       — Review PRs from URLs with structured issue and code analysis\n  is   <issue>        — Analyze GitHub issues (bugs or feature requests)\n  wr   [instructions] — Finish the current task end-to-end\n  cl   — Audit changelog entries before release\n```\n\n## Использование\n\nВведите `/`, а затем имя шаблона в редакторе. Автозаполнение показывает доступные шаблоны с описаниями.\n\n```\n/review                           # Expands review.md\n/component Button                 # Expands with argument\n/component Button \"click handler\" # Multiple arguments\n```\n\n## Аргументы\n\nШаблоны поддерживают позиционные аргументы, значения по умолчанию и простую нарезку:\n\n- `$1`, `$2`,... позиционные аргументы\n- `$@` или `$ARGUMENTS` для всех соединенных аргументов\n- `${1:-default}` использует аргумент 1, если он присутствует/непустой, в противном случае `default`\n- `${@:-default}` или `${ARGUMENTS:-default}` использует все аргументы, если они присутствуют/не пусты, в противном случае `default`\n- `${@:N}` для аргументов с N-й позиции (с индексом 1)\n- `${@:N:L}` для `L` аргументов, начиная с N\n\nПример:\n\n```markdown\n---\ndescription: Create a component\n---\nCreate a React component named $1 with features: $@\n```\n\nЗначения по умолчанию полезны для необязательных аргументов:\n\n```markdown\nSummarize the current state in ${1:-7} bullet points.\n```\n\nИспользование: `/component Button \"onClick handler\" \"disabled support\"`\n\n## Правила загрузки\n\n- Обнаружение шаблонов в `prompts/` нерекурсивно.\n- Если вам нужны шаблоны в подкаталогах, добавьте их явно через настройки `prompts` или манифест пакета.","sourceFile":"prompt-templates.md"},"providers":{"title":"Providers","markdown":"Pi поддерживает поставщиков на основе подписки через поставщиков OAuth и API key через переменные среды или файл аутентификации. Встроенные каталоги поставляются с числом pi; настроенные поставщики могут обновлять новые каталоги и кэшировать их в `~/.pi/agent/models-store.json` для автономного использования.\n\n## Оглавление\n\n- [Subscriptions](#subscriptions)\n- [API Keys](#api-keys)\n- [Auth File](#auth-file)\n- [Cloud Providers](#cloud-providers)\n- [llama.cpp](#llamacpp)\n- [Custom Providers](#custom-providers)\n- [Resolution Order](#resolution-order)\n\n## Подписки\n\nИспользуйте `/login` в интерактивном режиме, затем выберите провайдера:\n\n- ChatGPT Plus/Pro (Кодекс)\n- Клод Про/Макс\n- GitHub Второй пилот\n- xAI (подписка Grok/X)\n- OpenRouter (OAuth-отчеканено API key, оплата осуществляется за счет кредитов OpenRouter)\n- Радиус\n\nИспользуйте `/logout` для очистки учетных данных. Токены хранятся в `~/.pi/agent/auth.json` и автоматически обновляются по истечении срока их действия. Вместо этого OpenRouter выпускает управляемый пользователем API key, срок действия которого не истекает автоматически.\n\n### Кодекс OpenAI\n\n- Требуется подписка ChatGPT Plus или Pro.\n- Официально одобрено OpenAI: [Codex for OSS](https://developers.openai.com/community/codex-for-oss)\n\n### Клод Про/Макс\n\nАутентификация подписки Anthropic активна для учетных записей Claude Pro/Max. Использование сторонних средств связи начинается с [extra usage](https://claude.ai/settings/usage) и оплачивается за токен, а не в соответствии с ограничениями плана Claude.\n\n### GitHub Второй пилот\n\n- Нажмите Enter для github.com или введите свой домен GitHub Enterprise Server.\n- Если вы получаете сообщение «модель не поддерживается», включите ее в VS Code: Чат Copilot → выбор модели → выберите модель → «Включить».\n\n### xAI (подписка Grok/X)\n\n- Нажмите `/login xai`, затем выберите **Использовать подписку**.\n- `XAI_API_KEY` остается доступным через **Используйте API key**\n\n### OpenRouter\n\n- Запустите `/login openrouter`, затем выберите **Войти с помощью OpenRouter**, чтобы открыть поток авторизации OpenRouter PKCE.\n- Авторизация создает управляемый пользователем OpenRouter API key, оплата которого осуществляется за счет ваших кредитов OpenRouter.\n- На удаленных/безголовых машинах (например, выше SSH) браузер не может связаться с обратным вызовом обратной связи; вместо этого вставьте окончательный URL-адрес перенаправления (или код авторизации) в приглашение для входа в систему.\n- `OPENROUTER_API_KEY` остается доступным через **Используйте API key**\n\n### Радиус\n\nRadius — это динамический шлюз `pi-messages`. `/login radius` хранит токены OAuth в `auth.json`; каталог шлюзов обновляется независимо и кэшируется в `models-store.json`. Пользовательские шлюзы Radius можно объявить в `models.json` с помощью `\"oauth\": \"radius\"` и шлюза `baseUrl`.\n\n## API Ключи\n\n### Переменные среды или файл аутентификации\n\nИспользуйте `/login` в интерактивном режиме и выберите поставщика для хранения API key в `auth.json` или установите учетные данные через переменную среды:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\n| Поставщик | Переменная среды | клавиша `auth.json` |\n|----------|----------------------|------------------|\n| антропный | `ANTHROPIC_API_KEY` | `anthropic` |\n| Муравей Линг | `ANT_LING_API_KEY` | `ant-ling` |\n| Ответы Azure OpenAI | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |\n| ОпенАИ | `OPENAI_API_KEY` | `openai` |\n| ДипСик | `DEEPSEEK_API_KEY` | `deepseek` |\n| NVIDIA НИМ | `NVIDIA_API_KEY` | `nvidia` |\n| Гугл Близнецы | `GEMINI_API_KEY` | `google` |\n| Амазонка | `AWS_BEARER_TOKEN_BEDROCK` | `amazon-bedrock` |\n| Мистраль | `MISTRAL_API_KEY` | `mistral` |\n| Грок | `GROQ_API_KEY` | `groq` |\n| Церебрас | `CEREBRAS_API_KEY` | `cerebras` |\n| Cloudflare AI-шлюз | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_GATEWAY_ID`) | `cloudflare-ai-gateway` |\n| ИИ работников Cloudflare | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`) | `cloudflare-workers-ai` |\n| xAI | `XAI_API_KEY` | `xai` |\n| OpenRouter | `OPENROUTER_API_KEY` | `openrouter` |\n| AI-шлюз Vercel | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` |\n| План кодирования ZAI (глобальный) | `ZAI_API_KEY` | `zai` |\n| План кодирования ZAI (Китай) | `ZAI_CODING_CN_API_KEY` | `zai-coding-cn` |\n| OpenCode Дзен | `OPENCODE_API_KEY` | `opencode` |\n| OpenCode Go | `OPENCODE_API_KEY` | `opencode-go` |\n| Радиус | `RADIUS_API_KEY` | `radius` |\n| Hugging Face | `HF_TOKEN` | `huggingface` |\n| Фейерверк | `FIREWORKS_API_KEY` | `fireworks` |\n| Вместе ИИ | `TOGETHER_API_KEY` | `together` |\n| Бастен | `BASETEN_API_KEY` | `baseten` |\n| Кими для кодирования | `KIMI_API_KEY` | `kimi-coding` |\n| МиниМакс | `MINIMAX_API_KEY` | `minimax` |\n| МиниМакс (Китай) | `MINIMAX_CN_API_KEY` | `minimax-cn` |\n| План Qwen Token (существующий каталог) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan` |\n| План токенов Qwen (индивидуальный) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan-individual` |\n| План токенов Qwen (Китай) | `QWEN_TOKEN_PLAN_CN_API_KEY` | `qwen-token-plan-cn` |\n| Сяоми МиМо | `XIAOMI_API_KEY` | `xiaomi` |\n| План токена Xiaomi MiMo (Китай) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | `xiaomi-token-plan-cn` |\n| План токена Xiaomi MiMo (Амстердам) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | `xiaomi-token-plan-ams` |\n| План токена Xiaomi MiMo (Сингапур) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | `xiaomi-token-plan-sgp` |\n\nСсылка на переменные среды и ключи `auth.json`: [`const envMap`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/env-api-keys.ts) в [`packages/ai/src/env-api-keys.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/env-api-keys.ts).\n\n#### Файл аутентификации\n\nСохраните учетные данные в `~/.pi/agent/auth.json`:\n\n```json\n{\n  \"anthropic\": { \"type\": \"api_key\", \"key\": \"sk-ant-...\" },\n  \"ant-ling\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"openai\": { \"type\": \"api_key\", \"key\": \"sk-...\" },\n  \"deepseek\": { \"type\": \"api_key\", \"key\": \"sk-...\" },\n  \"nvidia\": { \"type\": \"api_key\", \"key\": \"nvapi-...\" },\n  \"google\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"opencode\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"opencode-go\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"together\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"qwen-token-plan\":  { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"qwen-token-plan-individual\": { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"qwen-token-plan-cn\": { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"xiaomi\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-cn\":  { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-ams\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-sgp\": { \"type\": \"api_key\", \"key\": \"...\" }\n}\n```\n\n`qwen-token-plan-individual` использует ту же международную конечную точку и `QWEN_TOKEN_PLAN_API_KEY`, что и\n`qwen-token-plan`, но ограничивает выбор моделями, документированными для индивидуальных подписок. Существующие\nПоставщик сохраняет свой более широкий каталог для обратной совместимости. При использовании `auth.json` сохраните\nучетные данные выбранного вами провайдера; переменная среды используется обоими международными поставщиками.\n\nФайл создается с разрешениями `0600` (только чтение/запись пользователя). Учетные данные файла аутентификации имеют приоритет над переменными среды.\n\nAPI key учетные данные также могут включать значения среды на уровне поставщика. Эти значения используются перед переменными среды процесса при разрешении ключа учетных данных, заголовков поставщика/модели и конфигурации поставщика, таких как идентификаторы учетных записей Cloudflare, настройки Azure OpenAI, проект/расположение Vertex, настройки Bedrock, `PI_CACHE_RETENTION` и `HTTP_PROXY`/`HTTPS_PROXY`.\n\n```json\n{\n  \"cloudflare-ai-gateway\": {\n    \"type\": \"api_key\",\n    \"key\": \"$CLOUDFLARE_API_KEY\",\n    \"env\": {\n      \"CLOUDFLARE_API_KEY\": \"...\",\n      \"CLOUDFLARE_ACCOUNT_ID\": \"account-id\",\n      \"CLOUDFLARE_GATEWAY_ID\": \"gateway-id\"\n    }\n  }\n}\n```\n\nИспользуйте это, когда pi должен использовать настройки поставщика, отличные от настроек среды оболочки проекта.\n\n### Ключевое разрешение\n\nПоле `key` поддерживает выполнение команд, интерполяцию среды и литералы:\n\n- **Команда оболочки:** `\"!command\"` в начале выполняет все значение как команду и использует stdout (кэшируется на время существования процесса)\n  ```json\n  { \"type\": \"api_key\", \"key\": \"!security find-generic-password -ws 'anthropic'\" }\n  { \"type\": \"api_key\", \"key\": \"!op read 'op://vault/item/credential'\" }\n  ```\n- **Интерполяция среды:** `\"$ENV_VAR\"` или `\"${ENV_VAR}\"` использует значение именованной переменной. Интерполяция работает внутри больших литералов.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$MY_ANTHROPIC_KEY\" }\n  { \"type\": \"api_key\", \"key\": \"${KEY_PREFIX}_${KEY_SUFFIX}\" }\n  ```\n  `$FOO_BAR` — переменная `FOO_BAR`; используйте `${FOO}_BAR`, когда `BAR` — это буквальный текст. Отсутствие переменных среды делает значение неразрешенным.\n- **Эскейп:** `\"$\"` выдает литерал `\"$\"`; `\"$!\"` выдает литерал `\"!\"`, не запуская выполнение команды.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$$literal-dollar-prefix\" }\n  { \"type\": \"api_key\", \"key\": \"$!literal-bang-prefix\" }\n  ```\n- **Буквальное значение:** Используется напрямую. Обычные строки в верхнем регистре, такие как `MY_API_KEY`, являются литералами; используйте `$MY_API_KEY` для переменных среды.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"sk-ant-...\" }\n  { \"type\": \"api_key\", \"key\": \"public\" }\n  ```\n\nУчетные данные OAuth также хранятся здесь после `/login` и управляются автоматически.\n\n## Облако Providers\n\n### Azure OpenAI\n\n```bash\nexport AZURE_OPENAI_API_KEY=...\nexport AZURE_OPENAI_BASE_URL=https://your-resource.ai.azure.com\n# also supported: https://your-resource.cognitiveservices.azure.com\n# also supported: https://your-resource.openai.azure.com\n# root endpoints are auto-normalized to /openai/v1\n# or use resource name instead of base URL\nexport AZURE_OPENAI_RESOURCE_NAME=your-resource\n\n# Optional\nexport AZURE_OPENAI_API_VERSION=2024-02-01\nexport AZURE_OPENAI_DEPLOYMENT_NAME_MAP=gpt-4=my-gpt4,gpt-4o=my-gpt4o\n```\n\n### Амазонка\n\nИспользуйте `/login amazon-bedrock` для хранения Bedrock API key или настройте один из внешних источников учетных данных AWS ниже:\n\n```bash\n# Option 1: AWS Profile\nexport AWS_PROFILE=your-profile\n\n# Option 2: IAM Keys\nexport AWS_ACCESS_KEY_ID=AKIA...\nexport AWS_SECRET_ACCESS_KEY=...\n\n# Option 3: Bearer Token\nexport AWS_BEARER_TOKEN_BEDROCK=...\n\n# Optional region (defaults to us-east-1)\nexport AWS_REGION=us-west-2\n```\n\nТакже поддерживает роли задач ECS (`AWS_CONTAINER_CREDENTIALS_*`) и IRSA (`AWS_WEB_IDENTITY_TOKEN_FILE`).\n\n```bash\npi --provider amazon-bedrock --model us.anthropic.claude-sonnet-4-20250514-v1:0\n```\n\nКэширование подсказок включается автоматически для моделей Claude, идентификатор которых содержит узнаваемое имя модели (базовые модели и определяемые системой профили вывода). Для профилей вывода приложений (чьи ARN не содержат имя модели) установите `AWS_BEDROCK_FORCE_CACHE=1`, чтобы включить точки кэширования:\n\n```bash\nexport AWS_BEDROCK_FORCE_CACHE=1\npi --provider amazon-bedrock --model arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123\n```\n\nЕсли вы подключаетесь к прокси-серверу Bedrock API, можно использовать следующие переменные среды:\n\n```bash\n# Set the URL for the Bedrock proxy (standard AWS SDK env var)\nexport AWS_ENDPOINT_URL_BEDROCK_RUNTIME=https://my.corp.proxy/bedrock\n\n# Set if your proxy does not require authentication\nexport AWS_BEDROCK_SKIP_AUTH=1\n\n# Set if your proxy only supports HTTP/1.1\nexport AWS_BEDROCK_FORCE_HTTP1=1\n```\n\n### Cloudflare AI-шлюз\n\n`CLOUDFLARE_API_KEY` можно установить с помощью `/login`. Идентификатор учетной записи и пул шлюза можно установить как переменные среды или в объекте `env` учетных данных API key в `auth.json`.\n\n```bash\nexport CLOUDFLARE_API_KEY=...           # or use /login\nexport CLOUDFLARE_ACCOUNT_ID=...\nexport CLOUDFLARE_GATEWAY_ID=...        # create at dash.cloudflare.com → AI → AI Gateway\npi --provider cloudflare-ai-gateway --model \"claude-sonnet-4-5\"\n```\n\nМаршруты к OpenAI, Anthropic и Workers AI через Cloudflare AI Gateway. Рабочий ИИ использует унифицированные идентификаторы моделей API (`/compat`) и префиксы (`workers-ai/@cf/...`). OpenAI использует транзитный маршрут OpenAI (`/openai`) с собственными идентификаторами моделей OpenAI, такими как `gpt-5.1`. Anthropic использует транзитный маршрут Anthropic (`/anthropic`) с собственными идентификаторами моделей Anthropic, такими как `claude-sonnet-4-5`.\n\nАутентификация AI Gateway использует `CLOUDFLARE_API_KEY` вместо `cf-aig-authorization`. Аутентификация восходящего потока может быть одной из:\n\n| Режим | Запросить авторизацию | Авторизация восходящего потока |\n|------|--------------|---------------|\n| Рабочие ИИ | Только токен Cloudflare | Cloudflare-родной |\n| Единый биллинг | Только токен Cloudflare | Cloudflare обрабатывает входящую аутентификацию и списывает кредиты |\n| Сохранено BYOK | Только токен Cloudflare | Cloudflare внедряет ключи провайдера, хранящиеся на панели управления AI Gateway |\n| Встроенный BYOK | Токен Cloudflare плюс восходящий заголовок `Authorization` | Запрос предоставляет ключ вышестоящего поставщика. |\n\nДля обычного использования Pi отдайте предпочтение единому выставлению счетов или сохраненному BYOK. Для встроенного BYOK требуется настроить дополнительный восходящий заголовок `Authorization` для поставщика Cloudflare AI Gateway, например, через переопределение поставщика/модели `models.json`.\n\n### ИИ работников Cloudflare\n\n`CLOUDFLARE_API_KEY` можно установить с помощью `/login`. `CLOUDFLARE_ACCOUNT_ID` можно установить как переменную среды или в объекте `env` учетных данных API key в `auth.json`.\n\n```bash\nexport CLOUDFLARE_API_KEY=...           # or use /login\nexport CLOUDFLARE_ACCOUNT_ID=...\npi --provider cloudflare-workers-ai --model \"@cf/moonshotai/kimi-k2.6\"\n```\n\nPi автоматически устанавливает `x-session-affinity` для [prefix caching](https://developers.cloudflare.com/workers-ai/features/prompt-caching/) скидок.\n\n### Google Вертекс ИИ\n\nИспользует учетные данные приложения по умолчанию:\n\n```bash\ngcloud auth application-default login\nexport GOOGLE_CLOUD_PROJECT=your-project\nexport GOOGLE_CLOUD_LOCATION=us-central1\n```\n\nИли установите `GOOGLE_APPLICATION_CREDENTIALS` для файла ключа учетной записи службы.\n\n## llama.cpp\n\nPi поддерживает сервер маршрутизатора llama.cpp. Настройте его с помощью `/login llama.cpp`, управляйте загруженными моделями с помощью `/llama` и выберите загруженную модель с помощью `/model`.\n\nСм. [llama.cpp](llama-cpp.md) для настройки сервера, структуры каталога модели, переменных среды и использования команд.\n\n## Пользовательский Providers\n\n**С помощью models.json:** добавьте Ollama, LM Studio, vLLM или любого поставщика, говорящего на поддерживаемом языке API (завершения OpenAI, ответы OpenAI, антропные сообщения, генеративный искусственный интеллект Google). См. [models.md](models.md).\n\n**Через расширения.** Для поставщиков, которым требуются специальные реализации API или потоки OAuth, создайте расширение. См. [custom-provider.md](custom-provider.md) и [examples/extensions/custom-provider-gitlab-duo](../examples/extensions/custom-provider-gitlab-duo/).\n\n## Порядок разрешения\n\nПри разрешении учетных данных для поставщика:\n\n1. CLI `--api-key` флаг\n2. Запись `auth.json` (токен API key или OAuth)\n3. Переменная среды\n4. Пользовательские ключи поставщика от `models.json`","sourceFile":"providers.md"},"quickstart":{"title":"Быстрый старт","markdown":"Эта страница поможет вам перейти от установки к полезному первому сеансу Pi.\n\n## Установить\n\nPi распространяется как пакет npm:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` отключает сценарии жизненного цикла зависимостей во время установки. Pi не требует сценариев установки для обычной установки npm.\n\n### Удалить\n\nИспользуйте менеджер пакетов, который установил pi. Установщик Curl использует npm глобально, поэтому установки Curl и npm удаляются с помощью npm:\n\n```bash\n# curl installer or npm install -g\nnpm uninstall -g @earendil-works/pi-coding-agent\n\n# pnpm\npnpm remove -g @earendil-works/pi-coding-agent\n\n# Yarn\nyarn global remove @earendil-works/pi-coding-agent\n\n# Bun\nbun uninstall -g @earendil-works/pi-coding-agent\n```\n\nПри удалении pi настройки, учетные данные, сеансы и установленные пакеты pi остаются в `~/.pi/agent/`.\n\nЗатем запустите pi в каталоге проекта, над которым вы хотите, чтобы он работал:\n\n```bash\ncd /path/to/project\npi\n```\n\n## Аутентификация\n\nPi может использовать поставщиков ключей с subscription providers по `/login` или API через переменные среды или файл аутентификации.\n\n### Вариант 1: вход в подписку\n\nЗапустите pi и запустите:\n\n```text\n/login\n```\n\nЗатем выберите провайдера. Встроенные входы по подписке включают Claude Pro/Max, ChatGPT Plus/Pro (Codex) и GitHub Copilot.\n\n### Вариант 2: API key\n\nУстановите API key перед запуском pi:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\nВы также можете запустить `/login` и выбрать поставщика API-ключа для хранения ключа в `~/.pi/agent/auth.json`.\n\nСм. [Providers](providers.md) для всех поддерживаемых поставщиков, переменных среды и настройки облачного провайдера.\n\n## Первая сессия\n\nПосле запуска pi введите запрос и нажмите Enter:\n\n```text\nSummarize this repository and tell me how to run its checks.\n```\n\nПо умолчанию pi предоставляет модели четыре инструмента:\n\n- `read` - читать файлы\n- `write` — создать или перезаписать файлы\n- `edit` — файлы патчей\n- `bash` — запустить команды оболочки\n\nДополнительные встроенные инструменты, доступные только для чтения (`grep`, `find`, `ls`), доступны через параметры инструментов. Pi запускается в вашем текущем рабочем каталоге и может изменять файлы там. Используйте git или другой рабочий процесс контрольных точек, если вы хотите легко откатиться.\n\n## Дайте инструкции по пи-проекту\n\nPi загружает context files при запуске. Добавьте файл `AGENTS.md`, чтобы указать, как работать в проекте:\n\n```markdown\n# Project Instructions\n\n- Run `npm run check` after code changes.\n- Do not run production migrations locally.\n- Keep responses concise.\n```\n\nPi нагрузки:\n\n- `~/.pi/agent/AGENTS.md` для глобальных инструкций\n- `AGENTS.md` или `CLAUDE.md` из родительских каталогов и текущего каталога\n\nЕсли каталог содержит `AGENTS.override.md`, Pi загружает его вместо `AGENTS.md` или `CLAUDE.md` из этого каталога.\n\nПерезапустите pi или запустите `/reload` после изменения context files.\n\n## Общие вещи, которые стоит попробовать\n\n### Справочные файлы\n\nВведите `@` в редакторе для нечеткого поиска файлов или передайте файлы в командной строке:\n\n```bash\npi @README.md \"Summarize this\"\npi @src/app.ts @src/app.test.ts \"Review these together\"\n```\n\nИзображения или текст можно вставлять с помощью Ctrl+V (Alt+V в Windows); изображения также можно перетаскивать в поддерживаемые терминалы.\n\n### Запуск команд оболочки\n\nВ интерактивном режиме:\n\n```text\n!npm run lint\n```\n\nВывод команды отправляется в модель. Используйте `!!command`, чтобы запустить команду без добавления ее вывода в контекст модели.\n\n### Переключение моделей\n\nИспользуйте `/model` или Ctrl+L, чтобы выбрать модель. Используйте Shift+Tab для переключения уровня мышления. Используйте Ctrl+P/Shift+Ctrl+P для циклического переключения моделей с ограниченной областью действия.\n\n### Продолжить позже\n\nСессии сохраняются автоматически:\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse previous sessions\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Open a specific session\n```\n\nВнутри pi используйте `/resume`, `/new`, `/tree`, `/fork` и `/clone` для управления сеансами.\n\n### Неинтерактивный режим\n\nДля одноразовых подсказок:\n\n```bash\npi -p \"Summarize this codebase\"\ncat README.md | pi -p \"Summarize this text\"\npi -p @screenshot.png \"What's in this image?\"\n```\n\nИспользуйте `--mode json` для вывода событий JSON или `--mode rpc` для интеграции процесса.\n\n## Следующие шаги\n\n- [Using Pi](usage.md) — интерактивный режим, slash commands — сеансы, context files и CLI — ссылка.\n- [Providers](providers.md) — аутентификация и настройка модели.\n- [Settings](settings.md) — глобальная и проектная конфигурация.\n- [Keybindings](keybindings.md) — ярлыки и настройка.\n- [Pi Packages](packages.md) — установка общих расширений, навыков, подсказок и тем.\n\nПримечания к платформе: [Windows](windows.md), [Termux](termux.md), [tmux](tmux.md), [Terminal setup](terminal-setup.md), [Shell aliases](shell-aliases.md).","sourceFile":"quickstart.md"},"rpc":{"title":"RPC Режим","markdown":"Режим RPC обеспечивает автономную работу агента кодирования через протокол JSON через stdin/stdout. Это полезно для встраивания агента в другие приложения, IDE или пользовательские интерфейсы.\n\n**Примечание для пользователей Node.js/TypeScript**: если вы создаете приложение Node.js, рассмотрите возможность использования `AgentSession` непосредственно из `@earendil-works/pi-coding-agent` вместо создания подпроцесса. См. [`src/core/agent-session.ts`](../src/core/agent-session.ts) для API. Информацию о клиенте TypeScript на основе подпроцесса см. в [`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts).\n\n## Запуск режима RPC\n\n```bash\npi --mode rpc [options]\n```\n\nРаспространенные варианты:\n- `--provider <name>`: установите поставщика LLM (anthropic, openai, google и т. д.).\n- `--model <pattern>`: шаблон или идентификатор модели (поддерживается `provider/id` и необязательно `:<thinking>`).\n- `--name <name>` / `-n <name>`: установите отображаемое имя сеанса при запуске.\n- `--no-session`: отключить сохранение сеанса.\n- `--session-dir <path>`: Пользовательский каталог хранения сеансов.\n\n## Обзор протокола\n\n- **Команды**: JSON объектов, отправленных на stdin, по одному в строке.\n- **Ответы**: JSON объекты с `type: \"response\"`, обозначающими успех/неуспех команды.\n- **События**: события агента передаются на stdout в виде строк JSON.\n\nВсе команды поддерживают необязательное поле `id` для корреляции запроса/ответа. Если это предусмотрено, соответствующий ответ будет содержать тот же `id`. События `bash_execution_update` также включают `id` исходной команды `bash`.\n\n### Обрамление\n\nВ режиме RPC используется строгая семантика JSONL с LF (`\\n`) в качестве единственного разделителя записей.\n\nДля клиентов это важно:\n- Разделить записи только по `\\n`\n- Примите необязательный ввод `\\r\\n`, удалив конечный `\\r`\n- Не используйте общие программы чтения строк, которые рассматривают разделители Юникода как символы новой строки.\n\nВ частности, Узел `readline` не соответствует протоколу для режима RPC, поскольку он также разбивается на `U+2028` и `U+2029`, которые действительны внутри строк JSON.\n\n## Команды\n\n### Подсказка\n\n#### быстрый\n\nОтправьте агенту приглашение пользователя. Ответ на команду выдается после того, как приглашение принято, поставлено в очередь или обработано. После принятия события продолжают передаваться асинхронно.\n\n```json\n{\"id\": \"req-1\", \"type\": \"prompt\", \"message\": \"Hello, world!\"}\n```\n\nС изображениями:\n```json\n{\"type\": \"prompt\", \"message\": \"What's in this image?\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n**Во время потоковой передачи**: если агент уже осуществляет потоковую передачу, необходимо указать `streamingBehavior`, чтобы поставить сообщение в очередь:\n\n```json\n{\"type\": \"prompt\", \"message\": \"New instruction\", \"streamingBehavior\": \"steer\"}\n```\n\n- `\"steer\"`: поставить сообщение в очередь во время работы агента. Он доставляется после того, как текущий ход помощника завершает выполнение вызовов инструментов, до следующего вызова LLM.\n- `\"followUp\"`: Подождите, пока агент завершит работу. Сообщение доставляется только тогда, когда агент останавливается.\n\nЕсли агент выполняет потоковую передачу и не указано `streamingBehavior`, команда возвращает ошибку.\n\n**Команды расширения**: если сообщение является командой расширения (например, `/mycommand`), оно выполняется немедленно, даже во время потоковой передачи. Команды расширения управляют своим собственным взаимодействием с LLM через `pi.sendMessage()`.\n\n**Расширение ввода**: команды навыков (`/skill:name`) и prompt templates (`/template`) расширяются перед отправкой/постановкой в ​​очередь.\n\nОтвет:\n```json\n{\"id\": \"req-1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true}\n```\n\n`success: true` означает, что приглашение было принято, поставлено в очередь или обработано немедленно. `success: false` означает, что запрос был отклонен до принятия. О сбоях после принятия сообщается через обычный поток событий и сообщений, а не как секунду `response` для того же идентификатора запроса.\n\nПоле `images` является необязательным. Каждое изображение использует формат `ImageContent`: `{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}`.\n\n#### управлять\n\nПоставьте в очередь управляющее сообщение во время работы агента. Он доставляется после того, как текущий ход помощника завершает выполнение вызовов инструментов, до следующего вызова LLM. Команды навыков и prompt templates расширены. Команды расширения не разрешены (вместо этого используйте `prompt`).\n\n```json\n{\"type\": \"steer\", \"message\": \"Stop and do this instead\"}\n```\n\nС изображениями:\n```json\n{\"type\": \"steer\", \"message\": \"Look at this instead\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nПоле `images` является необязательным. Каждое изображение использует формат `ImageContent` (тот же, что и `prompt`).\n\nОтвет:\n```json\n{\"type\": \"response\", \"command\": \"steer\", \"success\": true}\n```\n\nСм. [set_steering_mode](#set_steering_mode) для управления обработкой рулевых сообщений.\n\n#### следовать за\n\nПоставьте в очередь последующее сообщение, которое будет обработано после завершения работы агента. Доставляется только тогда, когда у агента больше нет вызовов инструментов или сообщений управления. Команды навыков и prompt templates расширены. Команды расширения не разрешены (вместо этого используйте `prompt`).\n\n```json\n{\"type\": \"follow_up\", \"message\": \"After you're done, also do this\"}\n```\n\nС изображениями:\n```json\n{\"type\": \"follow_up\", \"message\": \"Also check this image\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nПоле `images` является необязательным. Каждое изображение использует формат `ImageContent` (тот же, что и `prompt`).\n\nОтвет:\n```json\n{\"type\": \"response\", \"command\": \"follow_up\", \"success\": true}\n```\n\nСм. [set_follow_up_mode](#set_follow_up_mode) для управления обработкой последующих сообщений.\n\n#### прерывать\n\nПрервать текущую операцию агента.\n\n```json\n{\"type\": \"abort\"}\n```\n\nОтвет:\n```json\n{\"type\": \"response\", \"command\": \"abort\", \"success\": true}\n```\n\n#### новая_сессия\n\nНачните новый сеанс. Может быть отменено обработчиком событий расширения `session_before_switch`.\n\n```json\n{\"type\": \"new_session\"}\n```\n\nС дополнительным отслеживанием родительских сеансов:\n```json\n{\"type\": \"new_session\", \"parentSession\": \"/path/to/parent-session.jsonl\"}\n```\n\nОтвет:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nЕсли продление отменено:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n### Состояние\n\n#### get_state\n\nПолучить текущее состояние сеанса.\n\n```json\n{\"type\": \"get_state\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_state\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isStreaming\": false,\n    \"isCompacting\": false,\n    \"steeringMode\": \"all\",\n    \"followUpMode\": \"one-at-a-time\",\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"sessionName\": \"my-feature-work\",\n    \"autoCompactionEnabled\": true,\n    \"messageCount\": 5,\n    \"pendingMessageCount\": 0\n  }\n}\n```\n\nПоле `model` представляет собой полный объект [Model](#model) или `null`. Поле `sessionName` — это отображаемое имя, заданное с помощью `set_session_name` или опущенное, если оно не установлено.\n\n#### get_messages\n\nПолучить все сообщения в разговоре.\n\n```json\n{\"type\": \"get_messages\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_messages\",\n  \"success\": true,\n  \"data\": {\"messages\": [...]}\n}\n```\n\nСообщения — это объекты `AgentMessage` (см. [Message Types](#message-types)).\n\n### Модель\n\n#### set_model\n\nПерейдите на конкретную модель.\n\n```json\n{\"type\": \"set_model\", \"provider\": \"anthropic\", \"modelId\": \"claude-sonnet-4-20250514\"}\n```\n\nОтвет содержит полный объект [Model](#model):\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": true,\n  \"data\": {...}\n}\n```\n\n#### цикл_модель\n\nПерейдите к следующей доступной модели. Возвращает данные `null`, если доступна только одна модель.\n\n```json\n{\"type\": \"cycle_model\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_model\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isScoped\": false\n  }\n}\n```\n\nПоле `model` представляет собой полный объект [Model](#model).\n\n#### get_available_models\n\nПеречислите все настроенные модели.\n\n```json\n{\"type\": \"get_available_models\"}\n```\n\nОтвет содержит массив полных [Model](#model) объектов:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_models\",\n  \"success\": true,\n  \"data\": {\n    \"models\": [...]\n  }\n}\n```\n\n### мышление\n\n#### set_thinking_level\n\nУстановите уровень рассуждения/мышления для моделей, которые его поддерживают.\n\n```json\n{\"type\": \"set_thinking_level\", \"level\": \"high\"}\n```\n\nУровни: `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"`.\n\n`\"xhigh\"` и `\"max\"` доступны только в том случае, если они поддерживаются выбранной моделью. Некоторые модели, включая GPT-5.6, поддерживают оба варианта.\n\nОтвет:\n```json\n{\"type\": \"response\", \"command\": \"set_thinking_level\", \"success\": true}\n```\n\n#### цикл_мышления_уровень\n\nПеребирайте доступные уровни мышления. Возвращает данные `null`, если модель не поддерживает мышление.\n\n```json\n{\"type\": \"cycle_thinking_level\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_thinking_level\",\n  \"success\": true,\n  \"data\": {\"level\": \"high\"}\n}\n```\n\n#### get_available_thinking_levels\n\nПеречислите уровни мышления, поддерживаемые текущей моделью. Возвращает `[\"off\"]` для модели без аргументированной поддержки.\n\n```json\n{\"type\": \"get_available_thinking_levels\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_thinking_levels\",\n  \"success\": true,\n  \"data\": {\n    \"levels\": [\"off\", \"minimal\", \"low\", \"medium\", \"high\"]\n  }\n}\n```\n\n### Режимы очереди\n\n#### set_steering_mode\n\nУправляйте доставкой управляющих сообщений (от `steer`).\n\n```json\n{\"type\": \"set_steering_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nРежимы:\n- `\"all\"`: доставить все сообщения рулевого управления после того, как текущий поворот ассистента завершит выполнение вызовов инструментов.\n- `\"one-at-a-time\"`: доставлять одно рулевое сообщение за каждый завершенный поворот помощника (по умолчанию).\n\nОтвет:\n```json\n{\"type\": \"response\", \"command\": \"set_steering_mode\", \"success\": true}\n```\n\n#### set_follow_up_mode\n\nКонтролируйте, как доставляются последующие сообщения (от `follow_up`).\n\n```json\n{\"type\": \"set_follow_up_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nРежимы:\n- `\"all\"`: доставлять все последующие сообщения после завершения работы агента.\n- `\"one-at-a-time\"`: доставлять одно последующее сообщение после завершения работы агента (по умолчанию).\n\nОтвет:\n```json\n{\"type\": \"response\", \"command\": \"set_follow_up_mode\", \"success\": true}\n```\n\n### Уплотнение\n\n#### компактный\n\nВручную сжимайте контекст разговора, чтобы сократить использование токенов.\n\n```json\n{\"type\": \"compact\"}\n```\n\nС индивидуальными инструкциями:\n```json\n{\"type\": \"compact\", \"customInstructions\": \"Focus on code changes\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"compact\",\n  \"success\": true,\n  \"data\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\n      \"input\": 32000,\n      \"output\": 1200,\n      \"cacheRead\": 0,\n      \"cacheWrite\": 0,\n      \"totalTokens\": 33200,\n      \"cost\": {\"input\": 0.01, \"output\": 0.02, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.03}\n    },\n    \"details\": {}\n  }\n}\n```\n\n`estimatedTokensAfter` — это эвристическая оценка перестроенного контекста сообщения сразу после сжатия, а не точное количество токенов поставщика. `usage` сообщает о вызове или вызовах LLM, которые сгенерировали сводку и могут быть опущены пользовательскими обработчиками сжатия.\n\n#### set_auto_compaction\n\nВключите или отключите автоматическое сжатие, когда контекст почти заполнен.\n\n```json\n{\"type\": \"set_auto_compaction\", \"enabled\": true}\n```\n\nОтвет:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_compaction\", \"success\": true}\n```\n\n### Повторить попытку\n\n#### set_auto_retry\n\nВключите или отключите автоматический повтор при временных ошибках (перегрузка, ограничение скорости, 5xx).\n\n```json\n{\"type\": \"set_auto_retry\", \"enabled\": true}\n```\n\nОтвет:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_retry\", \"success\": true}\n```\n\n#### abort_retry\n\nПрервать выполняющуюся повторную попытку (отменить задержку и прекратить повторную попытку).\n\n```json\n{\"type\": \"abort_retry\"}\n```\n\nОтвет:\n```json\n{\"type\": \"response\", \"command\": \"abort_retry\", \"success\": true}\n```\n\n### Баш\n\n#### bash\n\nВыполните команду оболочки и добавьте вывод в контекст разговора. Вывод потоков как событий `bash_execution_update` во время выполнения команды; ответ содержит окончательный результат.\n\n```json\n{\"id\": \"req-1\", \"type\": \"bash\", \"command\": \"ls -la\"}\n```\n\nДобавьте `id`, чтобы связать потоковые события `bash_execution_update` с этой командой.\n\nОтвет:\n```json\n{\n  \"id\": \"req-1\",\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"total 48\\ndrwxr-xr-x ...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": false\n  }\n}\n```\n\nЕсли вывод был усечен, включает `fullOutputPath`:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"truncated output...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": true,\n    \"fullOutputPath\": \"/tmp/pi-bash-abc123.log\"\n  }\n}\n```\n\n**Как результаты bash достигают LLM:**\n\nКоманда `bash` выполняется немедленно и возвращает `BashResult`. Внутри создается `BashExecutionMessage` и сохраняется в состоянии сообщения агента.\n\nПри отправке следующей команды `prompt` все сообщения (включая `BashExecutionMessage`) преобразуются перед отправкой в ​​LLM. `BashExecutionMessage` преобразуется в `UserMessage` в следующем формате:\n\n````\nRan `ls -la`\n```\nвсего 48\nдрвхр-хр-х...\n```\n````\n\nЭто означает:\n1. Вывод Bash включается в контекст LLM при **следующем приглашении**, а не сразу.\n2. Перед приглашением можно выполнить несколько команд bash; все выходы будут включены\n\n#### прервать_bash\n\nПрервать выполнение команды bash.\n\n```json\n{\"type\": \"abort_bash\"}\n```\n\nОтвет:\n```json\n{\"type\": \"response\", \"command\": \"abort_bash\", \"success\": true}\n```\n\n### Сессия\n\n#### get_session_stats\n\nПолучите информацию об использовании токенов, статистике затрат и текущем использовании контекстного окна.\n\n```json\n{\"type\": \"get_session_stats\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_session_stats\",\n  \"success\": true,\n  \"data\": {\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"userMessages\": 5,\n    \"assistantMessages\": 5,\n    \"toolCalls\": 12,\n    \"toolResults\": 12,\n    \"totalMessages\": 22,\n    \"tokens\": {\n      \"input\": 50000,\n      \"output\": 10000,\n      \"cacheRead\": 40000,\n      \"cacheWrite\": 5000,\n      \"total\": 105000\n    },\n    \"cost\": 0.45,\n    \"contextUsage\": {\n      \"tokens\": 60000,\n      \"contextWindow\": 200000,\n      \"percent\": 30\n    }\n  }\n}\n```\n\n`tokens` и `cost` включают сообщения помощника, данные об использовании, сообщаемые инструментами, а также создание сводных данных по уплотнению/ветвям в течение всего сеанса. `contextUsage` содержит фактическую текущую оценку контекстного окна, используемую для уплотнения и отображения нижнего колонтитула.\n\n`contextUsage` опускается, если модель или контекстное окно недоступны. `contextUsage.tokens` и `contextUsage.percent` равны `null` сразу после уплотнения, пока новый ответ помощника после уплотнения не предоставит действительные данные об использовании.\n\n#### экспорт_html\n\nЭкспортируйте сеанс в файл HTML.\n\n```json\n{\"type\": \"export_html\"}\n```\n\nС пользовательским путем:\n```json\n{\"type\": \"export_html\", \"outputPath\": \"/tmp/session.html\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"export_html\",\n  \"success\": true,\n  \"data\": {\"path\": \"/tmp/session.html\"}\n}\n```\n\n#### переключатель_сессия\n\nЗагрузите другой файл сеанса. Может быть отменено обработчиком событий расширения `session_before_switch`.\n\n```json\n{\"type\": \"switch_session\", \"sessionPath\": \"/path/to/session.jsonl\"}\n```\n\nОтвет:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nЕсли расширение отменило переключение:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n#### вилка\n\nСоздайте новую вилку на основе предыдущего сообщения пользователя в активной ветке. Может быть отменено обработчиком событий расширения `session_before_fork`. Возвращает текст сообщения, из которого создается ответвление.\n\n```json\n{\"type\": \"fork\", \"entryId\": \"abc123\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": false}\n}\n```\n\nЕсли расширение отменило форк:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": true}\n}\n```\n\n#### клонировать\n\nДублируйте текущую активную ветку в новый сеанс в текущей позиции. Может быть отменено обработчиком событий расширения `session_before_fork`.\n\n```json\n{\"type\": \"clone\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": false}\n}\n```\n\nЕсли расширение отменило клонирование:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": true}\n}\n```\n\n#### get_fork_messages\n\nПолучите сообщения пользователей, доступные для разветвления.\n\n```json\n{\"type\": \"get_fork_messages\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_fork_messages\",\n  \"success\": true,\n  \"data\": {\n    \"messages\": [\n      {\"entryId\": \"abc123\", \"text\": \"First prompt...\"},\n      {\"entryId\": \"def456\", \"text\": \"Second prompt...\"}\n    ]\n  }\n}\n```\n\n#### get_entries\n\nПолучите все записи сеанса в порядке добавления (за исключением заголовка сеанса). Сеанс представляет собой дерево записей со стабильными идентификаторами, доступное только для добавления, поэтому идентификатор записи работает как устойчивый курсор: передайте идентификатор последней записи, который вы видели, как `since`, чтобы получать только записи строго после него, даже при перезапуске клиента. В отличие от `get_messages`, сюда входит история до уплотнения и заброшенные ветки.\n\n```json\n{\"type\": \"get_entries\"}\n```\n\nС курсором:\n```json\n{\"type\": \"get_entries\", \"since\": \"abc123\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_entries\",\n  \"success\": true,\n  \"data\": {\n    \"entries\": [\n      {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"timestamp\": \"...\", \"message\": {\"role\": \"user\", \"...\": \"...\"}}\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n`leafId` — это идентификатор текущей конечной записи (`null` для пустого сеанса), поэтому клиент может за один проход определить, переместилась ли активная ветвь. Если `since` не соответствует ни одному идентификатору записи, ответом будет `success: false`.\n\n#### get_tree\n\nПолучите сеанс в виде дерева записей. Каждый узел равен `{entry, children, label?, labelTimestamp?}`. Правильно сформированный сеанс имеет один корень; потерянные записи (разорванная родительская цепочка) также отображаются как корни.\n\n```json\n{\"type\": \"get_tree\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_tree\",\n  \"success\": true,\n  \"data\": {\n    \"tree\": [\n      {\n        \"entry\": {\"type\": \"message\", \"id\": \"abc123\", \"parentId\": null, \"...\": \"...\"},\n        \"children\": [\n          {\"entry\": {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"...\": \"...\"}, \"children\": []}\n        ]\n      }\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n#### get_last_assistant_text\n\nПолучите текстовое содержимое последнего сообщения помощника.\n\n```json\n{\"type\": \"get_last_assistant_text\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_last_assistant_text\",\n  \"success\": true,\n  \"data\": {\"text\": \"The assistant's response...\"}\n}\n```\n\nВозвращает `{\"text\": null}`, если сообщений помощника не существует.\n\n#### set_session_name\n\nУстановите отображаемое имя для текущего сеанса. Имя появляется в списках сеансов и помогает идентифицировать сеансы.\n\n```json\n{\"type\": \"set_session_name\", \"name\": \"my-feature-work\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_session_name\",\n  \"success\": true\n}\n```\n\nИмя текущего сеанса доступно через `get_state` в поле `sessionName`. Чтобы установить исходное имя при запуске режима RPC, передайте `--name <name>` или `-n <name>` процессу `pi --mode rpc`.\n\n### Команды\n\n#### get_commands\n\nПолучите доступные команды (команды расширения, prompt templates и навыки). Их можно вызвать с помощью команды `prompt`, добавив префикс `/`.\n\n```json\n{\"type\": \"get_commands\"}\n```\n\nОтвет:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_commands\",\n  \"success\": true,\n  \"data\": {\n    \"commands\": [\n      {\"name\": \"session-name\", \"description\": \"Set or clear session name\", \"source\": \"extension\", \"path\": \"/home/user/.pi/agent/extensions/session.ts\"},\n      {\"name\": \"fix-tests\", \"description\": \"Fix failing tests\", \"source\": \"prompt\", \"location\": \"project\", \"path\": \"/home/user/myproject/.pi/agent/prompts/fix-tests.md\"},\n      {\"name\": \"skill:brave-search\", \"description\": \"Web search via Brave API\", \"source\": \"skill\", \"location\": \"user\", \"path\": \"/home/user/.pi/agent/skills/brave-search/SKILL.md\"}\n    ]\n  }\n}\n```\n\nКаждая команда имеет:\n- `name`: Имя команды (вызов с помощью `/name`)\n- `description`: удобочитаемое описание (необязательно для команд расширения).\n- `source`: Что за команда:\n  - `\"extension\"`: зарегистрировано через `pi.registerCommand()` в расширении.\n  - `\"prompt\"`: Загружено из файла шаблона приглашения `.md`.\n  - `\"skill\"`: загружается из каталога навыков (имя начинается с `skill:`)\n- `location`: Откуда оно было загружено (необязательно, не указано для расширений):\n  - `\"user\"`: Уровень пользователя (`~/.pi/agent/`)\n  - `\"project\"`: Уровень проекта (`./.pi/agent/`)\n  - `\"path\"`: явный путь через CLI или настройки.\n- `path`: Абсолютный путь к файлу источника команды (необязательно).\n\n**Примечание**. Встроенные команды TUI (`/settings`, `/hotkeys` и т. д.) не включены. Они обрабатываются только в интерактивном режиме и не будут выполняться, если отправлены через `prompt`.\n\n## События\n\nВо время работы агента события передаются в stdout как JSON строки. События обычно не включают поле `id`; `bash_execution_update` включает `id` исходной команды `bash`, если она была предоставлена.\n\n### Типы событий\n\n| Событие | Описание |\n|-------|-------------|\n| `agent_start` | Агент начинает обработку |\n| `agent_end` | Завершен один запуск агента низкого уровня (может последовать повторная попытка, уплотнение или продолжение в очереди) |\n| `agent_settled` | Запуск агента полностью решен; автоматическая повторная попытка, повторная попытка уплотнения или продолжение в очереди не сохраняются |\n| `turn_start` | Начинается новый поворот |\n| `turn_end` | Поворот завершен (включая сообщение помощника и результаты работы инструмента) |\n| `message_start` | Сообщение начинается |\n| `message_update` | Потоковое обновление (разницы в тексте/мышлении/вызовах инструментов) |\n| `message_end` | Сообщение завершено |\n| `bash_execution_update` | Прямой фрагмент вывода команды RPC bash |\n| `tool_execution_start` | Инструмент начинает выполнение |\n| `tool_execution_update` | Ход выполнения инструмента (потоковый вывод) |\n| `tool_execution_end` | Инструмент завершен |\n| `queue_update` | Очередь ожидающего управления/последующего контроля изменена |\n| `compaction_start` | Начало уплотнения |\n| `compaction_end` | Уплотнение завершено |\n| `auto_retry_start` | Начинается автоматическая повторная попытка (после временной ошибки) |\n| `auto_retry_end` | Автоматическая повторная попытка завершена (успех или окончательный отказ) |\n| `summarization_retry_scheduled` | Повторная попытка запланирована из-за временного уплотнения или ошибки суммирования сводки ветвей. |\n| `summarization_retry_attempt_start` | Начинается повторный запрос сводки |\n| `summarization_retry_finished` | Цикл повторения суммирования завершен |\n| `extension_error` | Расширение выдало ошибку |\n\n### агент_старт\n\nГенерируется, когда агент начинает обрабатывать приглашение.\n\n```json\n{\"type\": \"agent_start\"}\n```\n\n### агент_конец\n\nГенерируется при завершении одного запуска агента низкого уровня. Содержит все сообщения, созданные во время этого запуска. Если `willRetry` истинно, последует автоматическая повторная попытка.\n\n```json\n{\n  \"type\": \"agent_end\",\n  \"messages\": [...],\n  \"willRetry\": false\n}\n```\n\n### агент_поселение\n\nВыдается после завершения полного выполнения на уровне сеанса. На этом этапе Pi не будет автоматически продолжать повторную попытку, повторную попытку уплотнения или последующие сообщения в очереди.\n\n```json\n{\"type\": \"agent_settled\"}\n```\n\n### начало_поворота/конец_поворота\n\nХод состоит из одного ответа помощника, а также любых результирующих вызовов инструментов и результатов.\n\n```json\n{\"type\": \"turn_start\"}\n```\n\n```json\n{\n  \"type\": \"turn_end\",\n  \"message\": {...},\n  \"toolResults\": [...]\n}\n```\n\n### начало_сообщения/конец_сообщения\n\nГенерируется, когда сообщение начинается и завершается. Поле `message` содержит `AgentMessage`.\n\n```json\n{\"type\": \"message_start\", \"message\": {...}}\n{\"type\": \"message_end\", \"message\": {...}}\n```\n\n### message_update (потоковая передача)\n\nГенерируется во время потоковой передачи сообщений помощника. Содержит разностное событие без совокупного снимка сообщения.\n\n```json\n{\n  \"type\": \"message_update\",\n  \"assistantMessageEvent\": {\n    \"type\": \"text_delta\",\n    \"contentIndex\": 0,\n    \"delta\": \"Hello \"\n  }\n}\n```\n\nПоле `assistantMessageEvent` содержит один из следующих типов дельты:\n\n| Тип | Описание |\n|------|-------------|\n| `text_start` | Блок текстового контента запущен |\n| `text_delta` | Блок текстового контента |\n| `text_end` | Блокировка текстового контента завершена |\n| `thinking_start` | Мыслительный блок начался |\n| `thinking_delta` | Думающий фрагмент контента |\n| `thinking_end` | Мыслительный блок закончился |\n| `toolcall_start` | Начался вызов инструмента |\n| `toolcall_delta` | Часть аргументов вызова инструмента |\n| `toolcall_end` | Вызов инструмента завершен (включая полный объект `toolCall`) |\n\nПример потоковой передачи текстового ответа:\n```json\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_start\",\"contentIndex\":0}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\" world\"}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_end\",\"contentIndex\":0,\"content\":\"Hello world\"}}\n```\n\n`message_update` намеренно опускает прежнее накопительное поле `message` и\n`assistantMessageEvent.partial`. Клиенты, которым требуется живое частичное сообщение, должны его собрать.\nот `message_start` и последующих событий с использованием `contentIndex`. Угощение `message_end.message`\nкак авторитетный. Для вызовов инструментов буфер `toolcall_delta.delta`; `toolcall_end.toolCall`\nсодержит завершенный вызов.\n\n### bash_execution_update\n\nВыдается один раз для каждого выходного фрагмента прямой команды `bash`. `id` соответствует `id` команды, что позволяет клиентам связать вывод с правильной командой.\n\nСобытия пересылают весь вывод во время выполнения команды, даже если `output` окончательного ответа `bash` усекается.\n\n```json\n{\n  \"type\": \"bash_execution_update\",\n  \"id\": \"req-1\",\n  \"delta\": \"total 48\\n\"\n}\n```\n\n### начало_выполнения_инструмента/обновление_выполнения_инструмента/конец_выполнения_инструмента\n\nГенерируется, когда инструмент запускается, отслеживает ход выполнения и завершает выполнение.\n\n```json\n{\n  \"type\": \"tool_execution_start\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"args\": {\"command\": \"ls -la\"}\n}\n```\n\nВо время выполнения события `tool_execution_update` передают частичные результаты (например, вывод bash по мере их поступления):\n\n```json\n{\n  \"type\": \"tool_execution_update\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"args\": {\"command\": \"ls -la\"},\n  \"partialResult\": {\n    \"content\": [{\"type\": \"text\", \"text\": \"partial output so far...\"}],\n    \"details\": {\"truncation\": null, \"fullOutputPath\": null}\n  }\n}\n```\n\nПо завершении:\n\n```json\n{\n  \"type\": \"tool_execution_end\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"result\": {\n    \"content\": [{\"type\": \"text\", \"text\": \"total 48\\n...\"}],\n    \"details\": {...}\n  },\n  \"isError\": false\n}\n```\n\nИспользуйте `toolCallId` для корреляции событий. `partialResult` в `tool_execution_update` содержит накопленные на данный момент выходные данные (а не только дельту), что позволяет клиентам просто заменять свое отображение при каждом обновлении.\n\n### очередь_обновление\n\nГенерируется всякий раз, когда изменяется ожидающая управляющая или отслеживающая очередь.\n\n```json\n{\n  \"type\": \"queue_update\",\n  \"steering\": [\"Focus on error handling\"],\n  \"followUp\": [\"After that, summarize the result\"]\n}\n```\n\n### начало_компактирования/конец_компактирования\n\nГенерируется во время уплотнения, ручного или автоматического.\n\n```json\n{\"type\": \"compaction_start\", \"reason\": \"threshold\"}\n```\n\nПоле `reason` — это `\"manual\"`, `\"threshold\"` или `\"overflow\"`.\n\n```json\n{\n  \"type\": \"compaction_end\",\n  \"reason\": \"threshold\",\n  \"result\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\n      \"input\": 32000,\n      \"output\": 1200,\n      \"cacheRead\": 0,\n      \"cacheWrite\": 0,\n      \"totalTokens\": 33200,\n      \"cost\": {\"input\": 0.01, \"output\": 0.02, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.03}\n    },\n    \"details\": {}\n  },\n  \"aborted\": false,\n  \"willRetry\": false\n}\n```\n\nЕсли `reason` было `\"overflow\"` и сжатие прошло успешно, `willRetry` равно `true`, и агент автоматически повторит запрос.\n\nЕсли уплотнение было прервано, `result` соответствует `null`, а `aborted` соответствует `true`.\n\nЕсли сжатие не удалось (например, превышена квота API), `result` — это `null`, `aborted` — `false`, а `errorMessage` содержит описание ошибки.\n\n### auto_retry_start / auto_retry_end\n\nГенерируется, когда автоматическая повторная попытка запускается после временной ошибки (перегрузка, ограничение скорости, 5xx).\n\n```json\n{\n  \"type\": \"auto_retry_start\",\n  \"attempt\": 1,\n  \"maxAttempts\": 3,\n  \"delayMs\": 2000,\n  \"errorMessage\": \"529 {\\\"type\\\":\\\"error\\\",\\\"error\\\":{\\\"type\\\":\\\"overloaded_error\\\",\\\"message\\\":\\\"Overloaded\\\"}}\"\n}\n```\n\n```json\n{\n  \"type\": \"auto_retry_end\",\n  \"success\": true,\n  \"attempt\": 2\n}\n```\n\nПри окончательном сбое (превышено максимальное количество попыток):\n```json\n{\n  \"type\": \"auto_retry_end\",\n  \"success\": false,\n  \"attempt\": 3,\n  \"finalError\": \"529 overloaded_error: Overloaded\"\n}\n```\n\n### summarization_retry_scheduled / summarization_retry_attempt_start / summarization_retry_finished\n\nГенерируется при повторной попытке уплотнения или суммирования ветвей после временной ошибки поставщика. Для этих событий используются те же настройки повтора, что и для автоматических повторов поворота помощника.\n\n```json\n{\n  \"type\": \"summarization_retry_scheduled\",\n  \"attempt\": 1,\n  \"maxAttempts\": 3,\n  \"delayMs\": 2000,\n  \"errorMessage\": \"terminated\"\n}\n```\n\n```json\n{\n  \"type\": \"summarization_retry_attempt_start\",\n  \"source\": \"compaction\",\n  \"reason\": \"threshold\"\n}\n```\n\nДля сводок ветвей `source` соответствует `\"branchSummary\"`, а `reason` отсутствует.\n\n```json\n{\n  \"type\": \"summarization_retry_finished\"\n}\n```\n\n### расширение_ошибка\n\nГенерируется, когда расширение выдает ошибку.\n\n```json\n{\n  \"type\": \"extension_error\",\n  \"extensionPath\": \"/path/to/extension.ts\",\n  \"event\": \"tool_call\",\n  \"error\": \"Error message...\"\n}\n```\n\n## Расширение протокола пользовательского интерфейса\n\nExtensions может запрашивать взаимодействие с пользователем через `ctx.ui.select()`, `ctx.ui.confirm()` и т. д. В режиме RPC они преобразуются в подпротокол запроса/ответа поверх базового потока команд/событий.\n\nСуществует две категории методов расширения пользовательского интерфейса:\n\n- **Методы диалога** (`select`, `confirm`, `input`, `editor`): выдают `extension_ui_request` на stdout и блокируются до тех пор, пока клиент не отправит обратно `extension_ui_response` на stdin с соответствующим `id`.\n- **Методы «выстрелил и забыл»** (`notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`): выдает `extension_ui_request` на stdout, но не ожидает ответа. Клиент может отображать информацию или игнорировать ее.\n\nЕсли метод диалога включает поле `timeout`, на стороне агента будет автоматически разрешено значение по умолчанию по истечении тайм-аута. Клиенту не нужно отслеживать таймауты.\n\nНекоторые методы `ExtensionUIContext` не поддерживаются или ухудшаются в режиме RPC, поскольку они требуют прямого доступа TUI:\n- `custom()` возвращает `undefined`\n- `setWorkingMessage()`, `setWorkingIndicator()`, `setFooter()`, `setHeader()`, `setEditorComponent()`, `setToolsExpanded()` не используются\n- `getEditorText()` возвращает `\"\"`\n- `getToolsExpanded()` возвращает `false`\n- `pasteToEditor()` делегирует `setEditorText()` (без обработки вставки/свертывания)\n- `getAllThemes()` возвращает `[]`\n- `getTheme()` возвращает `undefined`\n- `setTheme()` возвращает `{ success: false, error: \"...\" }`\n\nПримечание. `ctx.mode` — это `\"rpc\"`, а `ctx.hasUI` — это `true` в режиме RPC, поскольку методы диалога и «выстрелил и забыл» функционируют через подпротокол пользовательского интерфейса расширения. Используйте `ctx.mode === \"tui\"` для защиты функций, специфичных для TUI, таких как `custom()`, для которых требуется настоящий терминал.\n\n### Запросы пользовательского интерфейса расширения (stdout)\n\nВсе запросы имеют `type: \"extension_ui_request\"`, уникальное `id` и `method` поля.\n\n#### выбирать\n\nПредложите пользователю выбрать из списка. Методы диалога с полем `timeout` включают время ожидания в миллисекундах; агент автоматически разрешает проблему с помощью `undefined`, если клиент не отвечает вовремя.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-1\",\n  \"method\": \"select\",\n  \"title\": \"Allow dangerous command?\",\n  \"options\": [\"Allow\", \"Block\"],\n  \"timeout\": 10000\n}\n```\n\nОжидаемый ответ: `extension_ui_response` с `value` (выбранная строка параметра) или `cancelled: true`.\n\n#### подтверждать\n\nЗапросите у пользователя подтверждение «да/нет».\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-2\",\n  \"method\": \"confirm\",\n  \"title\": \"Clear session?\",\n  \"message\": \"All messages will be lost.\",\n  \"timeout\": 5000\n}\n```\n\nОжидаемый ответ: `extension_ui_response` с `confirmed: true/false` или `cancelled: true`.\n\n#### вход\n\nЗапрашивайте у пользователя текст в произвольной форме.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-3\",\n  \"method\": \"input\",\n  \"title\": \"Enter a value\",\n  \"placeholder\": \"type something...\"\n}\n```\n\nОжидаемый ответ: `extension_ui_response` с `value` (введенный текст) или `cancelled: true`.\n\n#### редактор\n\nОткройте многострочный текстовый редактор с дополнительным предварительно заполненным содержимым.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-4\",\n  \"method\": \"editor\",\n  \"title\": \"Edit some text\",\n  \"prefill\": \"Line 1\\nLine 2\\nLine 3\"\n}\n```\n\nОжидаемый ответ: `extension_ui_response` с `value` (отредактированный текст) или `cancelled: true`.\n\n#### уведомить\n\nОтображение уведомления. По принципу «выстрелил и забыл», ответа не ожидается.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-5\",\n  \"method\": \"notify\",\n  \"message\": \"Command blocked by user\",\n  \"notifyType\": \"warning\"\n}\n```\n\nПоле `notifyType` — это `\"info\"`, `\"warning\"` или `\"error\"`. По умолчанию `\"info\"`, если опущено.\n\n#### setStatus\n\nУстановите или очистите запись статуса в нижнем колонтитуле/строке состояния. Выстрелил и забыл.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-6\",\n  \"method\": \"setStatus\",\n  \"statusKey\": \"my-ext\",\n  \"statusText\": \"Turn 3 running...\"\n}\n```\n\nОтправьте `statusText: undefined` (или опустите его), чтобы очистить запись статуса для этого ключа.\n\n#### установитьвиджет\n\nУстановите или очистите виджет (блок текстовых строк), отображаемый над или под редактором. Выстрелил и забыл.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-7\",\n  \"method\": \"setWidget\",\n  \"widgetKey\": \"my-ext\",\n  \"widgetLines\": [\"--- My Widget ---\", \"Line 1\", \"Line 2\"],\n  \"widgetPlacement\": \"aboveEditor\"\n}\n```\n\nОтправьте `widgetLines: undefined` (или опустите его), чтобы очистить виджет. Поле `widgetPlacement` имеет значение `\"aboveEditor\"` (по умолчанию) или `\"belowEditor\"`. В режиме RPC поддерживаются только строковые массивы; фабрики компонентов игнорируются.\n\n#### setTitle\n\nУстановите заголовок окна/вкладки терминала. Выстрелил и забыл.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-8\",\n  \"method\": \"setTitle\",\n  \"title\": \"pi - my project\"\n}\n```\n\n#### set_editor_text\n\nУстановите текст в редакторе ввода. Выстрелил и забыл.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-9\",\n  \"method\": \"set_editor_text\",\n  \"text\": \"prefilled text for the user\"\n}\n```\n\n### Ответы пользовательского интерфейса расширения (stdin)\n\nОтветы отправляются только для диалоговых методов (`select`, `confirm`, `input`, `editor`). `id` должен соответствовать запросу.\n\n#### Ответ значения (выбор, ввод, редактор)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-1\", \"value\": \"Allow\"}\n```\n\n#### Подтверждающий ответ (подтвердить)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-2\", \"confirmed\": true}\n```\n\n#### Ответ на отмену (любой диалог)\n\nОтклоните любой метод диалога. Расширение получает `undefined` (для выбора/ввода/редактирования) или `false` (для подтверждения).\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-3\", \"cancelled\": true}\n```\n\n## Обработка ошибок\n\nНеудачные команды возвращают ответ с `success: false`:\n\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": false,\n  \"error\": \"Model not found: invalid/model\"\n}\n```\n\nОшибки разбора:\n\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"parse\",\n  \"success\": false,\n  \"error\": \"Failed to parse command: Unexpected token...\"\n}\n```\n\n## Типы\n\nИсходные файлы:\n- [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `Model`, `UserMessage`, `AssistantMessage`, `ToolResultMessage`\n- [`packages/agent/src/types.ts`](../../agent/src/types.ts) - `AgentMessage`, `AgentEvent`\n- [`src/core/messages.ts`](../src/core/messages.ts) - `BashExecutionMessage`\n- [`src/modes/json-event.ts`](../src/modes/json-event.ts) - `JsonAgentSessionEvent`\n- [`src/modes/rpc/rpc-types.ts`](../src/modes/rpc/rpc-types.ts) - RPC типы команд/ответов, типы запросов/ответов расширения пользовательского интерфейса\n\n### Модель\n\n```json\n{\n  \"id\": \"claude-sonnet-4-20250514\",\n  \"name\": \"Claude Sonnet 4\",\n  \"api\": \"anthropic-messages\",\n  \"provider\": \"anthropic\",\n  \"baseUrl\": \"https://api.anthropic.com\",\n  \"reasoning\": true,\n  \"input\": [\"text\", \"image\"],\n  \"contextWindow\": 200000,\n  \"maxTokens\": 16384,\n  \"cost\": {\n    \"input\": 3.0,\n    \"output\": 15.0,\n    \"cacheRead\": 0.3,\n    \"cacheWrite\": 3.75\n  }\n}\n```\n\n### Пользовательское сообщение\n\n```json\n{\n  \"role\": \"user\",\n  \"content\": \"Hello!\",\n  \"timestamp\": 1733234567890,\n  \"attachments\": []\n}\n```\n\nПоле `content` может быть строкой или массивом блоков `TextContent`/`ImageContent`.\n\n### АссистентСообщение\n\n```json\n{\n  \"role\": \"assistant\",\n  \"content\": [\n    {\"type\": \"text\", \"text\": \"Hello! How can I help?\"},\n    {\"type\": \"thinking\", \"thinking\": \"User is greeting me...\"},\n    {\"type\": \"toolCall\", \"id\": \"call_123\", \"name\": \"bash\", \"arguments\": {\"command\": \"ls\"}}\n  ],\n  \"api\": \"anthropic-messages\",\n  \"provider\": \"anthropic\",\n  \"model\": \"claude-sonnet-4-20250514\",\n  \"usage\": {\n    \"input\": 100,\n    \"output\": 50,\n    \"cacheRead\": 0,\n    \"cacheWrite\": 0,\n    \"cost\": {\"input\": 0.0003, \"output\": 0.00075, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.00105}\n  },\n  \"stopReason\": \"stop\",\n  \"timestamp\": 1733234567890\n}\n```\n\nПричины остановки: `\"stop\"`, `\"length\"`, `\"toolUse\"`, `\"error\"`, `\"aborted\"`\n\n### ИнструментРезультатСообщение\n\n```json\n{\n  \"role\": \"toolResult\",\n  \"toolCallId\": \"call_123\",\n  \"toolName\": \"bash\",\n  \"content\": [{\"type\": \"text\", \"text\": \"total 48\\ndrwxr-xr-x ...\"}],\n  \"usage\": {\n    \"input\": 100,\n    \"output\": 50,\n    \"cacheRead\": 0,\n    \"cacheWrite\": 0,\n    \"totalTokens\": 150,\n    \"cost\": {\"input\": 0.0003, \"output\": 0.00075, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.00105}\n  },\n  \"isError\": false,\n  \"timestamp\": 1733234567890\n}\n```\n\n`usage` является необязательным и сообщает о вложенной работе LLM, выполняемой инструментом. Если он присутствует, он участвует в токенах сеанса и общей стоимости.\n\n### BashExecutionСообщение\n\nСоздается командой `bash` RPC (а не вызовами инструментов LLM):\n\n```json\n{\n  \"role\": \"bashExecution\",\n  \"command\": \"ls -la\",\n  \"output\": \"total 48\\ndrwxr-xr-x ...\",\n  \"exitCode\": 0,\n  \"cancelled\": false,\n  \"truncated\": false,\n  \"fullOutputPath\": null,\n  \"timestamp\": 1733234567890\n}\n```\n\n### Вложение\n\n```json\n{\n  \"id\": \"img1\",\n  \"type\": \"image\",\n  \"fileName\": \"photo.jpg\",\n  \"mimeType\": \"image/jpeg\",\n  \"size\": 102400,\n  \"content\": \"base64-encoded-data...\",\n  \"extractedText\": null,\n  \"preview\": null\n}\n```\n\n## Пример: базовый клиент (Python)\n\n```python\nimport subprocess\nimport json\n\nproc = subprocess.Popen(\n    [\"pi\", \"--mode\", \"rpc\", \"--no-session\"],\n    stdin=subprocess.PIPE,\n    stdout=subprocess.PIPE,\n    text=True\n)\n\ndef send(cmd):\n    proc.stdin.write(json.dumps(cmd) + \"\\n\")\n    proc.stdin.flush()\n\ndef read_events():\n    for line in proc.stdout:\n        yield json.loads(line)\n\n# Send prompt\nsend({\"type\": \"prompt\", \"message\": \"Hello!\"})\n\n# Process events\nfor event in read_events():\n    if event.get(\"type\") == \"message_update\":\n        delta = event.get(\"assistantMessageEvent\", {})\n        if delta.get(\"type\") == \"text_delta\":\n            print(delta[\"delta\"], end=\"\", flush=True)\n    \n    if event.get(\"type\") == \"agent_end\":\n        print()\n        break\n```\n\n## Пример: Интерактивный клиент (Node.js)\n\nСм. [`test/rpc-example.ts`](../test/rpc-example.ts) для полного интерактивного примера или [`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts) для реализации типизированного клиента.\n\nПолный пример обработки протокола пользовательского интерфейса расширения см. в разделе [`examples/rpc-extension-ui.ts`](../examples/rpc-extension-ui.ts), который сочетается с расширением [`examples/extensions/rpc-demo.ts`](../examples/extensions/rpc-demo.ts).\n\n```javascript\nconst { spawn } = require(\"child_process\");\nconst { StringDecoder } = require(\"string_decoder\");\n\nconst agent = spawn(\"pi\", [\"--mode\", \"rpc\", \"--no-session\"]);\n\nfunction attachJsonlReader(stream, onLine) {\n    const decoder = new StringDecoder(\"utf8\");\n    let buffer = \"\";\n\n    stream.on(\"data\", (chunk) => {\n        buffer += typeof chunk === \"string\" ? chunk : decoder.write(chunk);\n\n        while (true) {\n            const newlineIndex = buffer.indexOf(\"\\n\");\n            if (newlineIndex === -1) break;\n\n            let line = buffer.slice(0, newlineIndex);\n            buffer = buffer.slice(newlineIndex + 1);\n            if (line.endsWith(\"\\r\")) line = line.slice(0, -1);\n            onLine(line);\n        }\n    });\n\n    stream.on(\"end\", () => {\n        buffer += decoder.end();\n        if (buffer.length > 0) {\n            onLine(buffer.endsWith(\"\\r\") ? buffer.slice(0, -1) : buffer);\n        }\n    });\n}\n\nattachJsonlReader(agent.stdout, (line) => {\n    const event = JSON.parse(line);\n\n    if (event.type === \"message_update\") {\n        const { assistantMessageEvent } = event;\n        if (assistantMessageEvent.type === \"text_delta\") {\n            process.stdout.write(assistantMessageEvent.delta);\n        }\n    }\n});\n\n// Send prompt\nagent.stdin.write(JSON.stringify({ type: \"prompt\", message: \"Hello\" }) + \"\\n\");\n\n// Abort on Ctrl+C\nprocess.on(\"SIGINT\", () => {\n    agent.stdin.write(JSON.stringify({ type: \"abort\" }) + \"\\n\");\n});\n```","sourceFile":"rpc.md"},"sdk":{"title":"SDK","markdown":"> pi может помочь вам использовать SDK. Попросите его построить интеграцию для вашего варианта использования.\n\n\nSDK обеспечивает программный доступ к возможностям агента pi. Используйте его для встраивания pi в другие приложения, создания пользовательских интерфейсов или интеграции с автоматизированными рабочими процессами.\n\n**Примеры использования:**\n- Создайте собственный пользовательский интерфейс (веб, настольный компьютер, мобильный телефон)\n- Интегрируйте возможности агента в существующие приложения\n- Создавайте автоматизированные конвейеры с аргументацией агента\n- Создавайте собственные инструменты, которые создают субагенты.\n- Программное тестирование поведения агента\n\nСм. [examples/sdk/](../examples/sdk/) для рабочих примеров от минимального до полного контроля.\n\n## Быстрый старт\n\n```typescript\nimport { createAgentSession, ModelRuntime, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create();\nconst { session } = await createAgentSession({\n  sessionManager: SessionManager.inMemory(),\n  modelRuntime,\n});\n\nsession.subscribe((event) => {\n  if (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n    process.stdout.write(event.assistantMessageEvent.delta);\n  }\n});\n\nawait session.prompt(\"What files are in the current directory?\");\n```\n\n## Установка\n\n```bash\nnpm install @earendil-works/pi-coding-agent\n```\n\nSDK включен в основной пакет. Никакой отдельной установки не требуется.\n\n## Основные понятия\n\n### создатьАгентСессион()\n\nОсновная заводская функция для одного `AgentSession`.\n\n`createAgentSession()` использует `ResourceLoader` для предоставления расширений, навыков, prompt templates, тем и context files. Если вы его не предоставите, при стандартном обнаружении будет использоваться `DefaultResourceLoader`.\n\n```typescript\nimport { createAgentSession, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\n// Minimal: defaults with DefaultResourceLoader\nconst { session } = await createAgentSession();\n\n// Custom: override specific options\nconst { session } = await createAgentSession({\n  model: myModel,\n  tools: [\"read\", \"bash\"],\n  sessionManager: SessionManager.inMemory(),\n});\n```\n\n### Агентсессия\n\nСеанс управляет жизненным циклом агента, историей сообщений, состоянием модели, сжатием и потоковой передачей событий.\n\n```typescript\ninterface AgentSession {\n  // Send a prompt and wait for completion\n  prompt(text: string, options?: PromptOptions): Promise<void>;\n\n  // Queue messages during streaming\n  steer(text: string): Promise<void>;\n  followUp(text: string): Promise<void>;\n\n  // Subscribe to events (returns unsubscribe function)\n  subscribe(listener: (event: AgentSessionEvent) => void): () => void;\n\n  // Session info\n  sessionFile: string | undefined;\n  sessionId: string;\n\n  // Model control\n  setModel(model: Model): Promise<void>;\n  setThinkingLevel(level: ThinkingLevel): void;\n  cycleModel(): Promise<ModelCycleResult | undefined>;\n  cycleThinkingLevel(): ThinkingLevel | undefined;\n\n  // State access\n  agent: Agent;\n  model: Model | undefined;\n  thinkingLevel: ThinkingLevel;\n  messages: AgentMessage[];\n  isStreaming: boolean;\n\n  // In-place tree navigation within the current session file\n  navigateTree(targetId: string, options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string }): Promise<{ editorText?: string; cancelled: boolean }>;\n\n  // Compaction\n  compact(customInstructions?: string): Promise<CompactionResult>;\n  abortCompaction(): void;\n\n  // Abort current operation\n  abort(): Promise<void>;\n\n  // Cleanup\n  dispose(): void;\n}\n```\n\nЗамены сеансов API, такие как новый сеанс, возобновление, ветвление и импорт, происходят с `AgentSessionRuntime`, а не с `AgentSession`.\n\n### createAgentSessionRuntime() и AgentSessionRuntime\n\nИспользуйте среду выполнения API, когда вам нужно заменить активный сеанс и перестроить состояние среды выполнения, связанное с cwd.\nЭто тот же слой, который используется во встроенных интерактивных режимах, режимах печати и RPC.\n\n`createAgentSessionRuntime()` принимает фабрику времени выполнения плюс начальную цель cwd/session. Фабрика закрывается по глобальным фиксированным входным данным, воссоздает службы, привязанные к cwd, для эффективного cwd, сопоставляет параметры сеанса с этими службами и возвращает полный результат времени выполнения.\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({\n      services,\n      sessionManager,\n      sessionStartEvent,\n    })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\n\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n```\n\n`AgentSessionRuntime` владеет заменой активной среды выполнения:\n\n- `newSession()`\n- `switchSession()`\n- `fork()`\n- клон проходит через `fork(entryId, { position: \"at\" })`\n- `importFromJsonl()`\n\nВажное поведение:\n\n- `runtime.session` изменений после этих операций\n- подписки на события привязаны к конкретному `AgentSession`, поэтому после замены подпишитесь повторно\n- если вы используете расширения, позвоните еще раз по номеру `runtime.session.bindExtensions(...)` для нового сеанса\n- создание возвращает диагностику `runtime.diagnostics`\n- если создание или замена среды выполнения завершается неудачей, метод выдает ошибку, и вызывающая сторона решает, как с этим справиться.\n\n```typescript\nlet session = runtime.session;\nlet unsubscribe = session.subscribe(() => {});\n\nawait runtime.newSession();\n\nunsubscribe();\nsession = runtime.session;\nunsubscribe = session.subscribe(() => {});\n```\n\n### Подсказки и очередь сообщений\n\n`PromptOptions` управляет расширением подсказки, поведением очереди во время потоковой передачи и подсказками предполетных уведомлений:\n\n```typescript\ninterface PromptOptions {\n  expandPromptTemplates?: boolean;\n  images?: ImageContent[];\n  streamingBehavior?: \"steer\" | \"followUp\";\n  source?: InputSource;\n  preflightResult?: (success: boolean) => void;\n}\n```\n\n`preflightResult` вызывается один раз за каждый вызов `prompt()`:\n\n- `true` когда приглашение было принято, поставлено в очередь или обработано немедленно\n- `false`, когда предварительная предполетная проверка отклонена до принятия\n\nОн срабатывает до разрешения `prompt()`. `prompt()` по-прежнему разрешается только после завершения полного принятого запуска, включая повторные попытки. О сбоях после приемки сообщается через обычный поток событий и сообщений, а не через `preflightResult(false)`.\n\nМетод `prompt()` обрабатывает prompt templates, команды расширения и отправку сообщений:\n\n```typescript\n// Basic prompt (when not streaming)\nawait session.prompt(\"What files are here?\");\n\n// With images\nawait session.prompt(\"What's in this image?\", {\n  images: [{ type: \"image\", source: { type: \"base64\", mediaType: \"image/png\", data: \"...\" } }]\n});\n\n// During streaming: must specify how to queue the message\nawait session.prompt(\"Stop and do this instead\", { streamingBehavior: \"steer\" });\nawait session.prompt(\"After you're done, also check X\", { streamingBehavior: \"followUp\" });\n```\n\n**Поведение:**\n- **Команды расширения** (например, `/mycommand`): выполняются немедленно, даже во время потоковой передачи. Они управляют своим собственным взаимодействием с LLM через `pi.sendMessage()`.\n- **На основе файлов prompt templates** (из файлов `.md`): расширяется до их содержимого перед отправкой или постановкой в ​​очередь.\n- **Во время потоковой передачи без `streamingBehavior`**: выдает ошибку. Используйте `steer()` или `followUp()` напрямую или укажите опцию.\n- **`preflightResult(true)`**: означает, что приглашение было принято, поставлено в очередь или обработано немедленно.\n- **`preflightResult(false)`**: означает, что предполетная подготовка отклонена до принятия.\n\nДля явной организации очереди во время потоковой передачи:\n\n```typescript\n// Queue a steering message for delivery after the current assistant turn finishes its tool calls\nawait session.steer(\"New instruction\");\n\n// Wait for agent to finish (delivered only when agent stops)\nawait session.followUp(\"After you're done, also do this\");\n```\n\nИ `steer()`, и `followUp()` расширяют файловый prompt templates, но возникает ошибка при командах расширения (команды расширения не могут быть поставлены в очередь).\n\n### Агент и состояние агента\n\nКласс `Agent` (из `@earendil-works/pi-agent-core`) управляет основным взаимодействием LLM. Доступ к нему осуществляется через `session.agent`.\n\n```typescript\n// Access current state\nconst state = session.agent.state;\n\n// state.messages: AgentMessage[] - conversation history\n// state.model: Model - current model\n// state.thinkingLevel: ThinkingLevel - current thinking level\n// state.systemPrompt: string - system prompt\n// state.tools: AgentTool[] - available tools\n// state.streamingMessage?: AgentMessage - current partial assistant message\n// state.errorMessage?: string - latest assistant error\n\n// Replace messages (useful for branching or restoration)\nsession.agent.state.messages = messages; // copies the top-level array\n\n// Replace tools\nsession.agent.state.tools = tools; // copies the top-level array\n\n// Wait for agent to finish processing\nawait session.agent.waitForIdle();\n```\n\n### События\n\nПодпишитесь на события, чтобы получать потоковые выходные данные и уведомления о жизненном цикле.\n\n```typescript\nsession.subscribe((event) => {\n  switch (event.type) {\n    // Streaming text from assistant\n    case \"message_update\":\n      if (event.assistantMessageEvent.type === \"text_delta\") {\n        process.stdout.write(event.assistantMessageEvent.delta);\n      }\n      if (event.assistantMessageEvent.type === \"thinking_delta\") {\n        // Thinking output (if thinking enabled)\n      }\n      break;\n    \n    // Tool execution\n    case \"tool_execution_start\":\n      console.log(`Tool: ${event.toolName}`);\n      break;\n    case \"tool_execution_update\":\n      // Streaming tool output\n      break;\n    case \"tool_execution_end\":\n      console.log(`Result: ${event.isError ? \"error\" : \"success\"}`);\n      break;\n    \n    // Message lifecycle\n    case \"message_start\":\n      // New message starting\n      break;\n    case \"message_end\":\n      // Message complete\n      break;\n    \n    // Agent lifecycle\n    case \"agent_start\":\n      // Agent started processing prompt\n      break;\n    case \"agent_end\":\n      // Agent finished (event.messages contains new messages)\n      break;\n    \n    // Turn lifecycle (one LLM response + tool calls)\n    case \"turn_start\":\n      break;\n    case \"turn_end\":\n      // event.message: assistant response\n      // event.toolResults: tool results from this turn\n      break;\n    \n    // Session events (queue, compaction, retry)\n    case \"queue_update\":\n      console.log(event.steering, event.followUp);\n      break;\n    case \"compaction_start\":\n    case \"compaction_end\":\n    case \"auto_retry_start\":\n    case \"auto_retry_end\":\n    case \"summarization_retry_scheduled\":\n    case \"summarization_retry_attempt_start\":\n    case \"summarization_retry_finished\":\n      break;\n  }\n});\n```\n\n## Справочник по опциям\n\n### Каталоги\n\n```typescript\nconst { session } = await createAgentSession({\n  // Working directory for DefaultResourceLoader discovery\n  cwd: process.cwd(), // default\n  \n  // Global config directory\n  agentDir: \"~/.pi/agent\", // default (expands ~)\n});\n```\n\n`cwd` используется `DefaultResourceLoader` для:\n- Расширения проекта (`.pi/extensions/`)\n- Навыки проекта:\n  - `.pi/skills/`\n  - `.agents/skills/` в `cwd` и каталогах предков (до корня репозитория git или корня файловой системы, если он не находится в репозитории)\n- Подсказки проекта (`.pi/prompts/`)\n- Контекстные файлы (`AGENTS.md` при переходе от cwd)\n- Именование каталога сеанса\n\n`agentDir` используется `DefaultResourceLoader` для:\n- Глобальные расширения (`extensions/`)\n- Глобальные навыки:\n  - `skills/` под `agentDir` (например, `~/.pi/agent/skills/`)\n  - `~/.agents/skills/`\n- Глобальные подсказки (`prompts/`)\n- Файл глобального контекста (`AGENTS.md`)\n- Настройки (`settings.json`)\n- Пользовательские модели (`models.json`)\n- Полномочия (`auth.json`)\n- Сессии (`sessions/`)\n\nКогда вы передаете пользовательские `ResourceLoader`, `cwd` и `agentDir`, они больше не контролируют обнаружение ресурсов. Они по-прежнему влияют на именование сеансов и разрешение пути к инструменту.\n\n### Модель\n\n```typescript\nimport { getModel } from \"@earendil-works/pi-ai\";\nimport { ModelRuntime } from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create();\n\n// Find specific built-in model (doesn't check if API key exists)\nconst opus = getModel(\"anthropic\", \"claude-opus-4-5\");\nif (!opus) throw new Error(\"Model not found\");\n\n// Find any model by provider/id, including custom models from models.json\n// (doesn't check if API key exists)\nconst customModel = modelRuntime.getModel(\"my-provider\", \"my-model\");\n\n// Get only models that have valid authentication configured\nconst available = await modelRuntime.getAvailable();\n\nconst { session } = await createAgentSession({\n  model: opus,\n  thinkingLevel: \"medium\", // off, minimal, low, medium, high, xhigh, max\n  \n  // Models for cycling (Ctrl+P in interactive mode)\n  scopedModels: [\n    { model: opus, thinkingLevel: \"high\" },\n    { model: haiku, thinkingLevel: \"off\" },\n  ],\n  \n  modelRuntime,\n});\n```\n\nЕсли модель не указана:\n1. Пытается восстановиться из сеанса (если продолжается)\n2. Использует настройки по умолчанию\n3. Возвращается к первой доступной модели.\n\nЧтобы соответствовать анализу модели CLI, используйте экспортированные помощники преобразователя:\n\n```typescript\nimport {\n  resolveCliModel,\n  resolveModelScopeWithDiagnostics,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst cliModel = resolveCliModel({\n  cliModel: \"anthropic/claude-opus-4-5:high\",\n  modelRuntime,\n});\nif (cliModel.error) throw new Error(cliModel.error);\nif (cliModel.warning) console.warn(cliModel.warning);\n\nconst { scopedModels, diagnostics } = await resolveModelScopeWithDiagnostics(\n  [\"anthropic/*:high\", \"gpt-5\"],\n  modelRuntime,\n);\nfor (const diagnostic of diagnostics) {\n  console.warn(diagnostic.message);\n}\n```\n\n`resolveCliModel()` использует все зарегистрированные модели, поэтому первая установка в стиле `--api-key` может разрешить модель до того, как будет сохранена сохраненная аутентификация. `resolveModelScopeWithDiagnostics()` соответствует семантике `--models` и `enabledModels`, возвращая предупреждения вместо их печати.\n\n> См. [examples/sdk/02-custom-model.ts](../examples/sdk/02-custom-model.ts)\n\n### API Клавиши и OAuth\n\nПриоритет разрешения аутентификации (обрабатывается `ModelRuntime`):\n1. Переопределения во время выполнения (через `setRuntimeApiKey`, не сохраняются)\n2. Хранимые учетные данные в `auth.json` (API keys или OAuth токенах)\n3. Переменные среды (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY` и т. д.)\n4. Резервный преобразователь (для пользовательских ключей поставщика от `models.json`)\n\n```typescript\nimport { InMemoryCredentialStore } from \"@earendil-works/pi-ai\";\nimport { createAgentSession, ModelRuntime } from \"@earendil-works/pi-coding-agent\";\n\n// Default: uses ~/.pi/agent/auth.json and ~/.pi/agent/models.json\nconst modelRuntime = await ModelRuntime.create();\n\n// Provider-owned auth methods and current status\nfor (const provider of modelRuntime.getProviders()) {\n  const status = await modelRuntime.checkAuth(provider.id);\n  console.log(provider.name, provider.auth, status);\n}\n\n// Runtime API key override (not persisted to disk)\nawait modelRuntime.setRuntimeApiKey(\"anthropic\", \"sk-my-temp-key\");\n\n// Custom credential and model locations\nconst customRuntime = await ModelRuntime.create({\n  authPath: \"/my/app/auth.json\",\n  modelsPath: \"/my/app/models.json\",\n});\n\n// Or inject any pi-ai CredentialStore\nconst credentials = new InMemoryCredentialStore();\nconst inMemoryRuntime = await ModelRuntime.create({ credentials });\n\nconst { session } = await createAgentSession({\n  modelRuntime: customRuntime,\n});\n```\n\n`login()`, `logout()`, `setRuntimeApiKey()` и `removeRuntimeApiKey()` разрешаются после того, как кэшированный/встроенный каталог, состав и снимок доступности затронутого поставщика становятся локально согласованными. Они не ждут свежести удаленного каталога. Если учетные данные были зафиксированы, но локальная синхронизация не удалась, они отклоняются вместе с экспортированным `CredentialSynchronizationError`; проверьте его поля `providerId`, `operation`, `credential` и `cause` вместо того, чтобы слепо повторять мутацию учетных данных.\n\nПубличные операции модели/аутентификации и `ModelRuntime.create({ signal })` принимают необязательные сигналы прерывания и не ограничены, если их опустить. SDK приложения имеют собственную политику сроков обновления удаленного каталога:\n\n```typescript\nconst signal = AbortSignal.timeout(15_000);\nconst result = await modelRuntime.refresh({\n  providers: [\"anthropic\"],\n  signal,\n});\nif (result.aborted) console.warn(\"Catalog refresh timed out; using cached models\");\nfor (const [providerId, error] of result.errors) {\n  console.warn(`Could not refresh ${providerId}:`, error);\n}\n```\n\nНеудачное обновление сети или истечение времени ожидания не отменяет успешную операцию с учетными данными. `refresh()` запускает новое поколение поставщика, поэтому оно не ожидает более старого остановленного обновления, а устаревшие поколения не могут публиковаться позже.\n\n> См. [examples/sdk/09-api-keys-and-oauth.ts](../examples/sdk/09-api-keys-and-oauth.ts)\n\n### Системная подсказка\n\nИспользуйте `ResourceLoader`, чтобы отменить системное приглашение:\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  systemPromptOverride: () => \"You are a helpful assistant.\",\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> См. [examples/sdk/03-custom-prompt.ts](../examples/sdk/03-custom-prompt.ts)\n\n### Инструменты\n\nУкажите, какие встроенные инструменты включить:\n\n- Имена встроенных инструментов: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`\n- Встроенные модули по умолчанию: `read`, `bash`, `edit`, `write`.\n- `noTools: \"all\"` отключает все инструменты\n- `noTools: \"builtin\"` отключает встроенные модули по умолчанию, сохраняя при этом расширение и пользовательские инструменты.\n- `excludeTools` отключает определенные имена встроенных, расширенных или пользовательских инструментов после применения любого белого списка `tools`.\n\nИнструмент `edit` возвращает `details.diff` для дисплея TUI Pi и `details.patch` в качестве стандартного унифицированного патча для потребителей SDK.\n\n```typescript\nimport { createAgentSession } from \"@earendil-works/pi-coding-agent\";\n\n// Read-only mode\nconst { session } = await createAgentSession({\n  tools: [\"read\", \"grep\", \"find\", \"ls\"],\n});\n\n// Pick specific tools\nconst { session } = await createAgentSession({\n  tools: [\"read\", \"bash\", \"grep\"],\n});\n\n// Disable one tool while keeping the rest available\nconst { session } = await createAgentSession({\n  excludeTools: [\"ask_question\"],\n});\n```\n\n#### Инструменты с пользовательским cwd\n\nКогда вы передаете пользовательский `cwd`, `createAgentSession()` создает выбранные встроенные инструменты для этого cwd.\n\n```typescript\nimport { createAgentSession, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\nconst cwd = \"/path/to/project\";\n\n// Use default tools for custom cwd\nconst { session } = await createAgentSession({\n  cwd,\n  sessionManager: SessionManager.inMemory(cwd),\n});\n\n// Or pick specific tools for custom cwd\nconst { session } = await createAgentSession({\n  cwd,\n  tools: [\"read\", \"bash\", \"grep\"],\n  sessionManager: SessionManager.inMemory(cwd),\n});\n```\n\n> См. [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)\n\n### Пользовательские инструменты\n\n```typescript\nimport { Type } from \"typebox\";\nimport { createAgentSession, defineTool } from \"@earendil-works/pi-coding-agent\";\n\n// Inline custom tool\nconst myTool = defineTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"Does something useful\",\n  parameters: Type.Object({\n    input: Type.String({ description: \"Input value\" }),\n  }),\n  execute: async (_toolCallId, params) => ({\n    content: [{ type: \"text\", text: `Result: ${params.input}` }],\n    details: {},\n  }),\n});\n\n// Pass custom tools directly\nconst { session } = await createAgentSession({\n  customTools: [myTool],\n});\n```\n\nИспользуйте `defineTool()` для отдельных определений и массивов, таких как `customTools: [myTool]`. Встроенный `pi.registerTool({... })` уже правильно определяет типы параметров.\n\nПользовательские инструменты, передаваемые через `customTools`, объединяются с инструментами, зарегистрированными в расширении. Extensions, загруженный ResourceLoader, также может регистрировать инструменты через `pi.registerTool()`.\n\nЕсли вы передадите `tools`, укажите имя каждого пользовательского инструмента или инструмента расширения, который вы хотите включить, например `tools: [\"read\", \"bash\", \"my_tool\"]`.\n\n> См. [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)\n\n### Extensions\n\nExtensions загружаются `ResourceLoader`. `DefaultResourceLoader` обнаруживает расширения из источников расширений `~/.pi/agent/extensions/`, `.pi/extensions/` и settings.json.\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  additionalExtensionPaths: [\"/path/to/my-extension.ts\"],\n  extensionFactories: [\n    (pi) => {\n      pi.on(\"agent_start\", () => {\n        console.log(\"[Inline Extension] Agent starting\");\n      });\n    },\n  ],\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\nExtensions может регистрировать инструменты, подписываться на события, добавлять команды и многое другое. Полный текст API см. в [extensions.md](extensions.md).\n\n**Именованные встроенные расширения.** По умолчанию встроенные фабрики отображаются как `<inline:1>`, `<inline:2>` и т. д. в списке запуска Extensions. Чтобы вместо этого отображать описательное имя, оберните фабрику:\n\n```typescript\nimport type { InlineExtension } from \"@earendil-works/pi-coding-agent\";\n\nconst myProvider: InlineExtension = {\n  name: \"my-provider\",\n  factory: (pi) => {\n    pi.on(\"agent_start\", () => {\n      console.log(\"[my-provider] Agent starting\");\n    });\n  },\n};\n\nconst loader = new DefaultResourceLoader({\n  extensionFactories: [myProvider],\n});\n```\n\nЭто отображается как `<inline:my-provider>` вместо `<inline:1>`. Голые заводские функции по-прежнему принимаются для обеспечения обратной совместимости.\n\n**Шина событий:** Extensions может общаться через `pi.events`. Передайте общий от `eventBus` до `DefaultResourceLoader`, если вам нужно излучать или прослушивать снаружи:\n\n```typescript\nimport { createEventBus, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst eventBus = createEventBus();\nconst loader = new DefaultResourceLoader({\n  eventBus,\n});\nawait loader.reload();\n\neventBus.on(\"my-extension:status\", (data) => console.log(data));\n```\n\n> См. [examples/sdk/06-extensions.ts](../examples/sdk/06-extensions.ts) и [docs/extensions.md](extensions.md).\n\n### Skills\n\n```typescript\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  type Skill,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst customSkill: Skill = {\n  name: \"my-skill\",\n  description: \"Custom instructions\",\n  filePath: \"/path/to/SKILL.md\",\n  baseDir: \"/path/to\",\n  source: \"custom\",\n};\n\nconst loader = new DefaultResourceLoader({\n  skillsOverride: (current) => ({\n    skills: [...current.skills, customSkill],\n    diagnostics: current.diagnostics,\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> См. [examples/sdk/04-skills.ts](../examples/sdk/04-skills.ts)\n\n### Контекстные файлы\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  agentsFilesOverride: (current) => ({\n    agentsFiles: [\n      ...current.agentsFiles,\n      { path: \"/virtual/AGENTS.md\", content: \"# Guidelines\\n\\n- Be concise\" },\n    ],\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> См. [examples/sdk/07-context-files.ts](../examples/sdk/07-context-files.ts)\n\n### Слэш-команды\n\n```typescript\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  type PromptTemplate,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst customCommand: PromptTemplate = {\n  name: \"deploy\",\n  description: \"Deploy the application\",\n  source: \"(custom)\",\n  content: \"# Deploy\\n\\n1. Build\\n2. Test\\n3. Deploy\",\n};\n\nconst loader = new DefaultResourceLoader({\n  promptsOverride: (current) => ({\n    prompts: [...current.prompts, customCommand],\n    diagnostics: current.diagnostics,\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> См. [examples/sdk/08-prompt-templates.ts](../examples/sdk/08-prompt-templates.ts)\n\n### Управление сеансами\n\nВ сеансах используется древовидная структура со связями `id`/`parentId`, что обеспечивает ветвление на месте.\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSession,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\n// In-memory (no persistence)\nconst { session } = await createAgentSession({\n  sessionManager: SessionManager.inMemory(),\n});\n\n// New persistent session\nconst { session: persisted } = await createAgentSession({\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\n// Continue most recent\nconst { session: continued, modelFallbackMessage } = await createAgentSession({\n  sessionManager: SessionManager.continueRecent(process.cwd()),\n});\nif (modelFallbackMessage) {\n  console.log(\"Note:\", modelFallbackMessage);\n}\n\n// Open specific file\nconst { session: opened } = await createAgentSession({\n  sessionManager: SessionManager.open(\"/path/to/session.jsonl\"),\n});\n\n// List sessions\nconst currentProjectSessions = await SessionManager.list(process.cwd());\nconst allSessions = await SessionManager.listAll(process.cwd());\n\n// Session replacement API for /new, /resume, /fork, /clone, and import flows.\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({\n      services,\n      sessionManager,\n      sessionStartEvent,\n    })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\n\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\n// Replace the active session with a fresh one\nawait runtime.newSession();\n\n// Replace the active session with another saved session\nawait runtime.switchSession(\"/path/to/session.jsonl\");\n\n// Replace the active session with a fork from a specific user entry\nawait runtime.fork(\"entry-id\");\n\n// Clone the active path through a specific entry\nawait runtime.fork(\"entry-id\", { position: \"at\" });\n```\n\n**Дерево SessionManager API:**\n\n```typescript\nconst sm = SessionManager.open(\"/path/to/session.jsonl\");\n\n// Session listing\nconst currentProjectSessions = await SessionManager.list(process.cwd());\nconst allSessions = await SessionManager.listAll(process.cwd());\n\n// Tree traversal\nconst entries = sm.getEntries();        // All entries (excludes header)\nconst tree = sm.getTree();              // Full tree structure\nconst path = sm.getPath();              // Path from root to current leaf\nconst leaf = sm.getLeafEntry();         // Current leaf entry\nconst entry = sm.getEntry(id);          // Get entry by ID\nconst children = sm.getChildren(id);    // Direct children of entry\n\n// Labels\nconst label = sm.getLabel(id);          // Get label for entry\nsm.appendLabelChange(id, \"checkpoint\"); // Set label\n\n// Branching\nsm.branch(entryId);                     // Move leaf to earlier entry\nsm.branchWithSummary(id, \"Summary...\");  // Branch with context summary\nsm.createBranchedSession(leafId);       // Extract path to new file\n```\n\n> См. [examples/sdk/11-sessions.ts](../examples/sdk/11-sessions.ts) и [Session Format](session-format.md).\n\n### Управление настройками\n\n```typescript\nimport { createAgentSession, SettingsManager, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\n// Default: loads from files (global + project merged)\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.create(),\n});\n\n// With overrides\nconst settingsManager = SettingsManager.create();\nsettingsManager.applyOverrides({\n  compaction: { enabled: false },\n  retry: { enabled: true, maxRetries: 5 },\n});\nconst { session } = await createAgentSession({ settingsManager });\n\n// In-memory (no file I/O, for testing)\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }),\n  sessionManager: SessionManager.inMemory(),\n});\n\n// Custom directories\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.create(\"/custom/cwd\", \"/custom/agent\"),\n});\n```\n\n**Статические фабрики:**\n- `SettingsManager.create(cwd?, agentDir?)` — Загрузка из файлов\n- `SettingsManager.inMemory(settings?)` — Нет файлового ввода-вывода\n\n**Настройки для конкретного проекта:**\n\nНастройки загружаются из двух мест и объединяются:\n1. Глобально: `~/.pi/agent/settings.json`\n2. Проект: `<cwd>/.pi/settings.json`\n\nПроект переопределяет глобальный. Вложенные объекты объединяют ключи. Сеттеры по умолчанию изменяют глобальные настройки.\n\n**Семантика постоянства и обработки ошибок:**\n\n- Геттеры/сеттеры настроек синхронны для состояния в памяти.\n- Сеттеры ставят в очередь постоянную запись асинхронно.\n- Вызовите `await settingsManager.flush()`, когда вам нужна граница устойчивости (например, перед завершением процесса или перед утверждением содержимого файла в тестах).\n- `SettingsManager` не выводит ошибки ввода-вывода настроек. Используйте `settingsManager.drainErrors()` и сообщите о них на уровне вашего приложения.\n\n> См. [examples/sdk/10-settings.ts](../examples/sdk/10-settings.ts)\n\n## Ресурслоадер\n\nИспользуйте `DefaultResourceLoader`, чтобы найти расширения, навыки, подсказки, темы и context files.\n\n```typescript\nimport {\n  DefaultResourceLoader,\n  getAgentDir,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  cwd,\n  agentDir: getAgentDir(),\n});\nawait loader.reload();\n\nconst extensions = loader.getExtensions();\nconst skills = loader.getSkills();\nconst prompts = loader.getPrompts();\nconst themes = loader.getThemes();\nconst contextFiles = loader.getAgentsFiles().agentsFiles;\n```\n\n## Возвращаемое значение\n\n`createAgentSession()` возвращает:\n\n```typescript\ninterface CreateAgentSessionResult {\n  // The session\n  session: AgentSession;\n  \n  // Extensions result (for runner setup)\n  extensionsResult: LoadExtensionsResult;\n  \n  // Warning if session model couldn't be restored\n  modelFallbackMessage?: string;\n}\n\ninterface LoadExtensionsResult {\n  extensions: Extension[];\n  errors: Array<{ path: string; error: string }>;\n  runtime: ExtensionRuntime;\n}\n```\n\n## Полный пример\n\n```typescript\nimport { getModel } from \"@earendil-works/pi-ai\";\nimport { Type } from \"typebox\";\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  defineTool,\n  ModelRuntime,\n  SessionManager,\n  SettingsManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create({\n  authPath: \"/custom/agent/auth.json\",\n  modelsPath: \"/custom/agent/models.json\",\n});\nif (process.env.MY_KEY) {\n  await modelRuntime.setRuntimeApiKey(\"anthropic\", process.env.MY_KEY);\n}\n\n// Inline tool\nconst statusTool = defineTool({\n  name: \"status\",\n  label: \"Status\",\n  description: \"Get system status\",\n  parameters: Type.Object({}),\n  execute: async () => ({\n    content: [{ type: \"text\", text: `Uptime: ${process.uptime()}s` }],\n    details: {},\n  }),\n});\n\nconst model = getModel(\"anthropic\", \"claude-opus-4-5\");\nif (!model) throw new Error(\"Model not found\");\n\n// In-memory settings with overrides\nconst settingsManager = SettingsManager.inMemory({\n  compaction: { enabled: false },\n  retry: { enabled: true, maxRetries: 2 },\n});\n\nconst loader = new DefaultResourceLoader({\n  cwd: process.cwd(),\n  agentDir: \"/custom/agent\",\n  settingsManager,\n  systemPromptOverride: () => \"You are a minimal assistant. Be concise.\",\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({\n  cwd: process.cwd(),\n  agentDir: \"/custom/agent\",\n\n  model,\n  thinkingLevel: \"off\",\n  modelRuntime,\n\n  tools: [\"read\", \"bash\", \"status\"],\n  customTools: [statusTool],\n  resourceLoader: loader,\n\n  sessionManager: SessionManager.inMemory(),\n  settingsManager,\n});\n\nsession.subscribe((event) => {\n  if (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n    process.stdout.write(event.assistantMessageEvent.delta);\n  }\n});\n\nawait session.prompt(\"Get status and list files.\");\n```\n\n## Режимы запуска\n\nSDK экспортирует утилиты режима запуска для создания пользовательских интерфейсов поверх `createAgentSession()`:\n\n### Интерактивный режим\n\nПолный интерактивный режим TUI с редактором, историей чата и всеми встроенными командами:\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  InteractiveMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nconst mode = new InteractiveMode(runtime, {\n  migratedProviders: [],\n  modelFallbackMessage: undefined,\n  initialMessage: \"Hello\",\n  initialImages: [],\n  initialMessages: [],\n});\n\nawait mode.run();\n```\n\n### запуститьPrintMode\n\nОднократный режим: отправка подсказок, вывод результата, выход:\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  runPrintMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nawait runPrintMode(runtime, {\n  mode: \"text\",\n  initialMessage: \"Hello\",\n  initialImages: [],\n  messages: [\"Follow up\"],\n});\n```\n\n### запуститьRpcMode\n\nРежим JSON-RPC для интеграции подпроцессов:\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  runRpcMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nawait runRpcMode(runtime);\n```\n\nСм. [RPC documentation](rpc.md) для протокола JSON.\n\n## RPC Альтернативный режим\n\nДля интеграции на основе подпроцессов без использования SDK используйте CLI напрямую:\n\n```bash\npi --mode rpc --no-session\n```\n\nСм. [RPC documentation](rpc.md) для протокола JSON.\n\nSDK предпочтительнее, если:\n- Вам нужна безопасность типов\n- Вы находитесь в том же процессе Node.js\n- Вам нужен прямой доступ к состоянию агента\n- Вы хотите программно настроить инструменты/расширения.\n\nРежим RPC предпочтителен, если:\n- Вы интегрируетесь с другого языка\n- Вам нужна изоляция процессов\n- Вы создаете клиент, не зависящий от языка.\n\n## Экспорт\n\nОсновная точка входа экспорта:\n\n```typescript\n// Factory\ncreateAgentSession\ncreateAgentSessionRuntime\nAgentSessionRuntime\n\n// Auth and Models\nModelRuntime // implements pi-ai Models and owns credential storage\nModelRegistry // synchronous extension compatibility facade\nCredentialSynchronizationError\nresolveCliModel\nresolveModelScopeWithDiagnostics\n\n// Resource loading\nDefaultResourceLoader\ntype ResourceLoader\ncreateEventBus\n\n// Constants and helpers\nCONFIG_DIR_NAME\ndefineTool\ngetAgentDir\ngetPackageDir\ngetReadmePath\ngetDocsPath\ngetExamplesPath\n\n// Session management\nSessionManager\nSettingsManager\n\n// Tool factories\ncreateCodingTools\ncreateReadOnlyTools\ncreateReadTool, createBashTool, createEditTool, createWriteTool\ncreateGrepTool, createFindTool, createLsTool\n\n// Types\ntype CreateAgentSessionOptions\ntype CreateAgentSessionResult\ntype ExtensionFactory\ntype InlineExtension\ntype ExtensionAPI\ntype ToolDefinition\ntype Skill\ntype PromptTemplate\ntype Tool\n```\n\nДля типов расширений см. [extensions.md](extensions.md) для полного API.","sourceFile":"sdk.md"},"security":{"title":"Безопасность","markdown":"Pi — локальный агент кодирования. Он запускается с разрешениями учетной записи пользователя, который его запускает, и рассматривает файлы, доступные для записи этим пользователем, как находящиеся внутри той же локальной границы доверия.\n\n## Проект Траст\n\nДоверие проекта контролирует, загружает ли pi локальные настройки, ресурсы, пакеты и расширения проекта. Это не sandbox, и он не ограничивает то, что модель может запрашивать у инструментов после того, как вы начнете работать в каталоге.\n\nPi считает, что в проекте есть ресурсы, требующие доверия, когда он находит любой из них в текущем рабочем каталоге:\n\n- `.pi/settings.json`\n- `.pi/extensions`, `.pi/skills`, `.pi/prompts` или `.pi/themes`\n- `.pi/SYSTEM.md` или `.pi/APPEND_SYSTEM.md`\n- проект `.agents/skills` в текущем каталоге или каталоге-предке\n\nПустой каталог `.pi` не считается ресурсом проекта, требующим доверия.\n\nКогда интерактивный сеанс начинается в проекте с ресурсами, которые требуют доверия и не имеют сохраненного решения для текущего каталога или родительского каталога, число pi следует за `defaultProjectTrust` из глобальных настроек. Значение по умолчанию — `\"ask\"`, которое спрашивает, можно ли доверять проекту, когда пользовательский интерфейс доступен. Сохраненные решения хранятся в каноническом каталоге в `~/.pi/agent/trust.json`, и ближайшее сохраненное решение по текущему или родительскому пути применяется до глобального значения по умолчанию.\n\nДоверие к проекту позволяет pi загружать ресурсы проекта, требующие доверия, в том числе:\n\n- `.pi/settings.json`\n- `.pi` ресурсы, такие как расширения, навыки, prompt templates, темы и файлы системных подсказок.\n- отсутствующие пакеты проекта, настроенные через настройки проекта\n- локальные расширения проекта и расширения, управляемые пакетом проекта\n\nСнижение доверия пропускает защищенные ресурсы. Файлы контекста, такие как `AGENTS.override.md`, `AGENTS.md` и `CLAUDE.md`, загружаются независимо от доверия проекта, если загрузка контекста не отключена. Прежде чем доверие будет разрешено, pi загружает только context files, пользовательские/глобальные расширения и CLI `-e` расширения. Пользовательские/глобальные расширения и расширения CLI могут обрабатывать событие `project_trust`; решение принадлежит первому расширению, которое возвращает решение да/нет.\n\nВ неинтерактивных режимах (`-p`, `--mode json` и `--mode rpc`) запрос доверия не отображается. Без применимого сохраненного решения о доверии `defaultProjectTrust: \"ask\"` и `\"never\"` игнорируют такие ресурсы, тогда как `\"always\"` доверяет им. Используйте `--approve`/`-a` или `--no-approve`/`-na`, чтобы переопределить доверие проекта на один запуск.\n\n## Нет встроенной песочницы\n\nPi не включает встроенный sandbox. Встроенные инструменты могут читать файлы, записывать файлы, редактировать файлы и запускать команды оболочки с разрешениями процесса pi. Extensions — это модули TypeScript, которые работают с теми же разрешениями. Установка пакетов, команды оболочки, языковые серверы, тестовые команды и другие инструменты разработчика ведут себя как обычные локальные процессы.\n\nЭто намеренно. Pi предназначен для работы с локальными деревьями исходного кода, вызова цепочек инструментов проекта и интеграции с существующей средой разработки пользователя. Частичный внутрипроцессный sandbox легко принять за границу безопасности, хотя он все еще зависит от оболочки хоста, файловой системы, менеджеров пакетов, учетных данных и кода расширения. Настоящая изоляция должна исходить от операционной системы или границы виртуализации/контейнера.\n\nДоверие к проекту — это всего лишь защита входной нагрузки. Это не позволяет репозиторию незаметно изменять настройки или расширения pi до того, как вы это одобрите. Это не делает ненадежный код, ненадежные запросы или выходные данные ненадежной модели безопасными. Оперативное внедрение из файлов репозитория, комментариев, документации, context files или выходных данных сборки является ожидаемым риском для локального агента и не может быть надежно предотвращено с помощью pi.\n\n## Выполнение ненадежной или неконтролируемой работы\n\nДля ненадежных репозиториев, сгенерированного кода, который вы не собираетесь тщательно отслеживать, или для автоматической автоматизации запускайте pi в изолированной среде. Используйте контейнер, виртуальную машину, микро-VM, удаленную sandbox или управляемую политикой sandbox только с файлами и учетными данными, необходимыми для задачи.\n\nОбщие шаблоны описаны в [Containerization](containerization.md):\n\n- запустить весь процесс `pi` внутри контейнера/sandbox\n- запустить хост pi, одновременно перенаправляя выполнение встроенного инструмента в микро-VM Gondolin\n- монтировать только те пути к рабочей области, к которым должен иметь доступ агент\n- избегайте монтирования хоста `~/.pi/agent`, если только контейнер не должен иметь доступ к сеансам хоста, настройкам и учетным данным\n- передайте минимально необходимые API key или используйте недолговечные учетные данные\n- ограничить доступ к сети, когда задача в этом не нуждается\n- просмотрите различия и выходные данные, прежде чем копировать результаты обратно в доверенные системы.\n\nЕсли вы привязываете чтение/запись рабочей области хоста, записи изнутри контейнера или виртуальной машины все равно могут изменять файлы хоста. Используйте монтирование только для чтения или копируйте файлы в sandbox и из него, если вам нужна более надежная защита от непреднамеренной записи.\n\n## Сообщение о проблемах безопасности\n\nЧтобы сообщить о проблеме безопасности, перейдите в репозиторий [Security Policy](https://github.com/earendil-works/pi-mono/blob/main/SECURITY.md). Не открывайте общедоступную проблему для отчетов, чувствительных к безопасности.\n\nОжидаемое поведение локального агента, отсутствие встроенного sandbox, быстрое внедрение из ненадежного контента и поведение установленных пользователем расширений или навыков, как правило, находятся за пределами границ безопасности, если только отчет не демонстрирует реальный обход границы привилегий или не показывает, как pi предоставляет доступ, которого еще не было у локального пользователя.","sourceFile":"security.md"},"session-format":{"title":"Формат файла сеанса","markdown":"Сессии сохраняются в виде файлов JSONL (JSON Lines). Каждая строка представляет собой объект JSON с полем `type`. Записи сеанса образуют древовидную структуру с помощью полей `id`/`parentId`, что позволяет осуществлять ветвление на месте без создания новых файлов.\n\n## Местоположение файла\n\n```\n~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl\n```\n\nГде `<path>` — рабочий каталог, где `/` заменено на `-`.\n\n## Удаление сеансов\n\nСессии можно удалить, удалив их файлы `.jsonl` в папке `~/.pi/agent/sessions/`.\n\nPi также поддерживает интерактивное удаление сеансов с `/resume` (выберите сеанс и нажмите `Ctrl+D`, затем подтвердите). Если доступно, pi использует `trash` CLI, чтобы избежать окончательного удаления.\n\n## Версия сеанса\n\nСессии имеют поле версии в заголовке:\n\n- **Версия 1**: линейная последовательность ввода (устаревшая, автоматически переносится при загрузке)\n- **Версия 2**: Древовидная структура со связями `id`/`parentId`.\n- **Версия 3**: Роль `hookMessage` переименована в `custom` (объединение расширений).\n\nСуществующие сеансы автоматически переносятся в текущую версию (v3) при загрузке.\n\n## Исходные файлы\n\nИсточник на GitHub ([pi-mono](https://github.com/earendil-works/pi-mono)):\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts) — типы записей сеанса и SessionManager.\n- [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts) — Расширенные типы сообщений (BashExecutionMessage, CustomMessage и т. д.)\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts) — Базовые типы сообщений (UserMessage, AssistantMessage, ToolResultMessage)\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/agent/src/types.ts) — тип объединения AgentMessage\n\nДля определений TypeScript в вашем проекте проверьте `node_modules/@earendil-works/pi-coding-agent/dist/` и `node_modules/@earendil-works/pi-ai/dist/`.\n\n## Типы сообщений\n\nЗаписи сеанса содержат `AgentMessage` объектов. Понимание этих типов необходимо для анализа сеансов и написания расширений.\n\n### Блоки контента\n\nСообщения содержат массивы типизированных блоков контента:\n\n```typescript\ninterface TextContent {\n  type: \"text\";\n  text: string;\n}\n\ninterface ImageContent {\n  type: \"image\";\n  data: string;      // base64 encoded\n  mimeType: string;  // e.g., \"image/jpeg\", \"image/png\"\n}\n\ninterface ThinkingContent {\n  type: \"thinking\";\n  thinking: string;\n}\n\ninterface ToolCall {\n  type: \"toolCall\";\n  id: string;\n  name: string;\n  arguments: Record<string, any>;\n}\n```\n\n### Базовые типы сообщений (из pi-ai)\n\n```typescript\ninterface UserMessage {\n  role: \"user\";\n  content: string | (TextContent | ImageContent)[];\n  timestamp: number;  // Unix ms\n}\n\ninterface AssistantMessage {\n  role: \"assistant\";\n  content: (TextContent | ThinkingContent | ToolCall)[];\n  api: string;\n  provider: string;\n  model: string;\n  usage: Usage;\n  stopReason: \"stop\" | \"length\" | \"toolUse\" | \"error\" | \"aborted\";\n  errorMessage?: string;\n  timestamp: number;\n}\n\ninterface ToolResultMessage {\n  role: \"toolResult\";\n  toolCallId: string;\n  toolName: string;\n  content: (TextContent | ImageContent)[];\n  details?: any;      // Tool-specific metadata\n  usage?: Usage;      // Nested LLM work performed by the tool\n  isError: boolean;\n  timestamp: number;\n}\n\ninterface Usage {\n  input: number;\n  output: number;\n  cacheRead: number;\n  cacheWrite: number;\n  totalTokens: number;\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n    total: number;\n  };\n}\n```\n\nЭкспортированный тип pi-ai `StopReason` также включает `\"pending\"`, но это значение зарезервировано для частичных сообщений в потоковых событиях. Сообщения терминала `done`/`error` заменяют его причиной завершения до того, как pi сохранит сообщение помощника, поэтому `\"pending\"` никогда не должно появляться в сеансе JSONL.\n\n### Расширенные типы сообщений (из pi-coding-agent)\n\n```typescript\ninterface BashExecutionMessage {\n  role: \"bashExecution\";\n  command: string;\n  output: string;\n  exitCode: number | undefined;\n  cancelled: boolean;\n  truncated: boolean;\n  fullOutputPath?: string;\n  excludeFromContext?: boolean;  // true for !! prefix commands\n  timestamp: number;\n}\n\ninterface CustomMessage {\n  role: \"custom\";\n  customType: string;            // Extension identifier\n  content: string | (TextContent | ImageContent)[];\n  display: boolean;              // Show in TUI\n  details?: any;                 // Extension-specific metadata\n  timestamp: number;\n}\n\ninterface BranchSummaryMessage {\n  role: \"branchSummary\";\n  summary: string;\n  fromId: string;                // Entry we branched from\n  timestamp: number;\n}\n\ninterface CompactionSummaryMessage {\n  role: \"compactionSummary\";\n  summary: string;\n  tokensBefore: number;\n  timestamp: number;\n}\n```\n\n### АгентСообщение Союз\n\n```typescript\ntype AgentMessage =\n  | UserMessage\n  | AssistantMessage\n  | ToolResultMessage\n  | BashExecutionMessage\n  | CustomMessage\n  | BranchSummaryMessage\n  | CompactionSummaryMessage;\n```\n\n## Входная база\n\nВсе записи (кроме `SessionHeader`) расширяют `SessionEntryBase`:\n\n```typescript\ninterface SessionEntryBase {\n  type: string;\n  id: string;           // 8-char hex ID\n  parentId: string | null;  // Parent entry ID (null for first entry)\n  timestamp: string;    // ISO timestamp\n}\n```\n\n## Типы входа\n\n### Заголовок сеанса\n\nПервая строка файла. Только метаданные, а не часть дерева (без `id`/`parentId`).\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\"}\n```\n\nДля сеансов с родителем (созданных через `/fork`, `/clone` или `newSession({ parentSession })`):\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\",\"parentSession\":\"/path/to/original/session.jsonl\"}\n```\n\n### Сеансмессажеэнтри\n\nСообщение в разговоре. Поле `message` содержит `AgentMessage`.\n\n```json\n{\"type\":\"message\",\"id\":\"a1b2c3d4\",\"parentId\":\"prev1234\",\"timestamp\":\"2024-12-03T14:00:01.000Z\",\"message\":{\"role\":\"user\",\"content\":\"Hello\"}}\n{\"type\":\"message\",\"id\":\"b2c3d4e5\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:00:02.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Hi!\"}],\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{...},\"stopReason\":\"stop\"}}\n{\"type\":\"message\",\"id\":\"c3d4e5f6\",\"parentId\":\"b2c3d4e5\",\"timestamp\":\"2024-12-03T14:00:03.000Z\",\"message\":{\"role\":\"toolResult\",\"toolCallId\":\"call_123\",\"toolName\":\"bash\",\"content\":[{\"type\":\"text\",\"text\":\"output\"}],\"isError\":false}}\n```\n\n### ModelChangeEntry\n\nГенерируется, когда пользователь переключает модели в середине сеанса.\n\n```json\n{\"type\":\"model_change\",\"id\":\"d4e5f6g7\",\"parentId\":\"c3d4e5f6\",\"timestamp\":\"2024-12-03T14:05:00.000Z\",\"provider\":\"openai\",\"modelId\":\"gpt-4o\"}\n```\n\n### МышлениеУровеньИзмененияВход\n\nГенерируется, когда пользователь меняет уровень мышления/рассуждения.\n\n```json\n{\"type\":\"thinking_level_change\",\"id\":\"e5f6g7h8\",\"parentId\":\"d4e5f6g7\",\"timestamp\":\"2024-12-03T14:06:00.000Z\",\"thinkingLevel\":\"high\"}\n```\n\n### Запись уплотнения\n\nСоздается при сжатии контекста. Сохраняет сводку предыдущих сообщений.\n\n```json\n{\"type\":\"compaction\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:10:00.000Z\",\"summary\":\"User discussed X, Y, Z...\",\"firstKeptEntryId\":\"c3d4e5f6\",\"tokensBefore\":50000}\n```\n\nНовые сжатия, генерируемые жгутами, встраивают сохраненный контекст после сжатия непосредственно в запись вместо `firstKeptEntryId`:\n\n```json\n{\"type\":\"compaction\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:10:00.000Z\",\"summary\":\"User discussed X, Y, Z...\",\"tokensBefore\":50000,\"retainedTail\":[{\"role\":\"user\",\"content\":\"latest request\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"latest reply\"}],\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{...},\"stopReason\":\"stop\"}]}\n```\n\nНеобязательные поля:\n- `usage`: использование LLM при создании сводки; включено в токен сеанса и общую стоимость\n- `retainedTail`: Материализованный `AgentMessage[]` сохраняется после уплотнения. Это необязательно только для обратной совместимости со старыми сеансами. Новые сжатия, генерируемые жгутами, включают его, поэтому мы можем перестроить контекст из этой контрольной точки, не проходя старые записи перед записью уплотнения.\n- `details`: данные, специфичные для реализации (например, `{ readFiles: string[], modifiedFiles: string[] }` по умолчанию или пользовательские данные для расширений).\n- `fromHook`: `true`, если сгенерировано расширением, `false`/`undefined`, если сгенерировано pi (устаревшее имя поля)\n- `firstKeptEntryId`: для совместимости со старым форматом ввода.\n\n### ФилиалСводкаЗапись\n\nСоздается при переключении ветвей через `/tree` с помощью LLM, сгенерированного сводными данными левой ветки до общего предка. Захватывает контекст заброшенного пути.\n\n```json\n{\"type\":\"branch_summary\",\"id\":\"g7h8i9j0\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:15:00.000Z\",\"fromId\":\"f6g7h8i9\",\"summary\":\"Branch explored approach A...\"}\n```\n\nНеобязательные поля:\n- `usage`: использование LLM при создании сводки; включено в токен сеанса и общую стоимость\n- `details`: данные отслеживания файлов (`{ readFiles: string[], modifiedFiles: string[] }`) по умолчанию или пользовательские данные для расширений.\n- `fromHook`: `true`, если сгенерировано расширением, `false`/`undefined`, если сгенерировано pi (устаревшее имя поля)\n\n### CustomEntry\n\nСохранение состояния расширения. НЕ участвует в контексте LLM.\n\n```json\n{\"type\":\"custom\",\"id\":\"h8i9j0k1\",\"parentId\":\"g7h8i9j0\",\"timestamp\":\"2024-12-03T14:20:00.000Z\",\"customType\":\"my-extension\",\"data\":{\"count\":42}}\n```\n\nИспользуйте `customType`, чтобы идентифицировать записи вашего расширения при перезагрузке. В интерактивном режиме пользовательские записи могут отображаться с помощью `pi.registerEntryRenderer(customType, renderer)`, но они по-прежнему не участвуют в контексте LLM.\n\n### CustomMessageEntry\n\nСообщения, внедренные в расширение, которые ДЕЙСТВИТЕЛЬНО участвуют в контексте LLM.\n\n```json\n{\"type\":\"custom_message\",\"id\":\"i9j0k1l2\",\"parentId\":\"h8i9j0k1\",\"timestamp\":\"2024-12-03T14:25:00.000Z\",\"customType\":\"my-extension\",\"content\":\"Injected context...\",\"display\":true}\n```\n\nПоля:\n- `content`: строка или `(TextContent | ImageContent)[]` (то же, что UserMessage)\n- `display`: `true` = показывать в TUI с особым стилем, `false` = скрыто\n- `details`: дополнительные метаданные, специфичные для расширения (не отправляются в LLM).\n\n### МеткаEntry\n\nПользовательская закладка/маркер для записи.\n\n```json\n{\"type\":\"label\",\"id\":\"j0k1l2m3\",\"parentId\":\"i9j0k1l2\",\"timestamp\":\"2024-12-03T14:30:00.000Z\",\"targetId\":\"a1b2c3d4\",\"label\":\"checkpoint-1\"}\n```\n\nУстановите от `label` до `undefined`, чтобы очистить метку.\n\n### Сеансинфоэнтри\n\nМетаданные сеанса (например, отображаемое имя, определяемое пользователем). Устанавливается с помощью `/name`, `--name`/`-n` или `pi.setSessionName()` в расширениях.\n\n```json\n{\"type\":\"session_info\",\"id\":\"k1l2m3n4\",\"parentId\":\"j0k1l2m3\",\"timestamp\":\"2024-12-03T14:35:00.000Z\",\"name\":\"Refactor auth module\"}\n```\n\nИмя сеанса отображается в селекторе сеансов (`/resume`) вместо первого сообщения, если оно установлено.\n\n## Древовидная структура\n\nЗаписи образуют дерево:\n- Первая запись имеет `parentId: null`\n- Каждая последующая запись указывает на своего родителя через `parentId`.\n- Ветвление создает новых дочерних элементов из более ранней записи.\n- «Лист» — текущая позиция в дереве.\n\n```\n[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf\n                                                            │\n                                                            └─ [branch_summary] ─── [user msg] ← alternate branch\n```\n\n## Построение контекста\n\n`buildContextEntries()` проходит от текущего листа к корню, создавая активный список записей, соблюдая сжатие:\n\n1. Собирает все записи на пути\n2. Если на пути находится `CompactionEntry`:\n   - Сначала включает запись уплотнения\n   - Если присутствует `retainedTail`, он действует как автономная контрольная точка, и записи после сжатия включаются.\n   - В противном случае включаются записи от `firstKeptEntryId` до уплотнения.\n   - Затем включаются записи после уплотнения.\n3. Сохраняет записи, не относящиеся к сообщениям, в выбранном диапазоне, чтобы интерактивный режим мог их отображать.\n\n`buildSessionContext()` основывается на этом списке записей для создания списка сообщений для LLM:\n\n1. Извлекает настройки текущей модели и уровня мышления из полного пути.\n2. Преобразует выбранные записи в сообщения:\n   - `message` -> сохранено `AgentMessage`\n   - `compaction` -> `compactionSummary` плюс `retainedTail`, если присутствует\n   - `branch_summary` -> `branchSummary`\n   - `custom_message` -> `CustomMessage`\n   - `custom` -> нет контекстного сообщения\n\nЭто заставляет новые уплотнения действовать как автономные контрольные точки. `retainedTail` является необязательным только для того, чтобы старые сеансы, в которых хранится только `firstKeptEntryId`, продолжали загружаться правильно.\n\n## Пример синтаксического анализа\n\n```typescript\nimport { readFileSync } from \"fs\";\n\nconst lines = readFileSync(\"session.jsonl\", \"utf8\").trim().split(\"\\n\");\n\nfor (const line of lines) {\n  const entry = JSON.parse(line);\n\n  switch (entry.type) {\n    case \"session\":\n      console.log(`Session v${entry.version ?? 1}: ${entry.id}`);\n      break;\n    case \"message\":\n      console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);\n      break;\n    case \"compaction\":\n      console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);\n      break;\n    case \"branch_summary\":\n      console.log(`[${entry.id}] Branch from ${entry.fromId}`);\n      break;\n    case \"custom\":\n      console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);\n      break;\n    case \"custom_message\":\n      console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);\n      break;\n    case \"label\":\n      console.log(`[${entry.id}] Label \"${entry.label}\" on ${entry.targetId}`);\n      break;\n    case \"model_change\":\n      console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);\n      break;\n    case \"thinking_level_change\":\n      console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);\n      break;\n  }\n}\n```\n\n## Менеджер сеансов API\n\nКлючевые методы работы с сессиями программно.\n\n### Статические методы создания\n- `SessionManager.create(cwd, sessionDir?)` — Новая сессия\n- `SessionManager.open(path, sessionDir?)` — Открыть существующий файл сеанса.\n- `SessionManager.continueRecent(cwd, sessionDir?)` — Продолжить последнюю версию или создать новую.\n- `SessionManager.inMemory(cwd?)` — Файл не сохраняется.\n- `SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)` — Форк сессии из другого проекта\n\n### Статические методы листинга\n- `SessionManager.list(cwd, sessionDir?, onProgress?)` — список сеансов для каталога.\n- `SessionManager.listAll(onProgress?)` — список всех сеансов во всех проектах.\n\n### Методы экземпляра — Управление сеансами\n- `newSession(options?)` — Начать новую сессию (варианты: `{ parentSession?: string }`)\n- `setSessionFile(path)` — переключиться на другой файл сеанса.\n- `createBranchedSession(leafId)` — Извлечь ветку в новый файл сеанса.\n\n### Методы экземпляра — добавление (все идентификаторы возвращаемых записей)\n- `appendMessage(message)` — Добавить сообщение\n- `appendThinkingLevelChange(level)` — Зафиксируйте изменение мышления.\n- `appendModelChange(provider, modelId)` — Запись изменения модели.\n- `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` — Добавить уплотнение\n- `appendCustomEntry(customType, data?)` — Состояние расширения (не в контексте)\n- `appendSessionInfo(name)` — Установить отображаемое имя сеанса.\n- `appendCustomMessageEntry(customType, content, display, details?)` — Сообщение расширения (в контексте)\n- `appendLabelChange(targetId, label)` — Установить/очистить метку\n\n### Методы экземпляра — навигация по дереву\n- `getLeafId()` - Текущая позиция\n- `getLeafEntry()` — Получить текущую листовую запись.\n- `getEntry(id)` — Получить запись по идентификатору\n- `getBranch(fromId?)` — Пройти от входа до корня\n- `getTree()` — Получить полную древовидную структуру\n- `getChildren(parentId)` — Получить прямых детей\n- `getLabel(id)` — Получить метку для входа.\n- `branch(entryId)` — Переместить лист на более раннюю запись.\n- `resetLeaf()` — Сбросить лист до нуля (перед любыми записями)\n- `branchWithSummary(entryId, summary, details?, fromHook?)` — Ветка с контекстной сводкой.\n\n### Методы экземпляра — контекст и информация\n- `buildContextEntries()` — получить активные записи ветвей с примененным сжатием.\n- `buildSessionContext()` — Получайте сообщения, уровень мышления и модель для LLM.\n- `getEntries()` — Все записи (кроме заголовка)\n- `getHeader()` — метаданные заголовка сеанса.\n- `getSessionName()` — Получить отображаемое имя из последней записи session_info.\n- `getCwd()` — Рабочий каталог.\n- `getSessionDir()` — Каталог хранения сеансов.\n- `getSessionId()` — UUID сеанса\n- `getSessionFile()` — путь к файлу сеанса (не определен для хранения в памяти)\n- `isPersisted()` — сохраняется ли сессия на диске","sourceFile":"session-format.md"},"sessions":{"title":"Сессии","markdown":"Pi сохраняет разговоры как сеансы, чтобы вы могли продолжить работу, перейти от предыдущих ходов и вернуться к предыдущим путям.\n\n## Хранилище сеансов\n\nСеансы автоматически сохраняются в `~/.pi/agent/sessions/`, организованные по рабочему каталогу. Каждый сеанс представляет собой файл JSONL с древовидной структурой.\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse and select from past sessions\npi --no-session        # Ephemeral mode; do not save\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Use a specific session file or partial session ID\npi --fork <path|id>    # Fork a session file or partial session ID into a new session\n```\n\nИспользуйте `/session` в интерактивном режиме, чтобы просмотреть текущий файл сеанса, идентификатор сеанса, количество сообщений, токены и стоимость.\n\nИнформацию о формате файла JSONL и SessionManager API см. в [Session Format](session-format.md).\n\n## Команды сеанса\n\n| Команда | Описание |\n|---------|-------------|\n| `/resume` | Просмотрите и выберите предыдущие сеансы |\n| `/new` | Начать новый сеанс |\n| `/name <name>` | Установите отображаемое имя текущего сеанса |\n| `/session` | Показать информацию о сеансе |\n| `/tree` | Навигация по текущему session tree |\n| `/fork` | Создать новый сеанс на основе предыдущего сообщения пользователя. |\n| `/clone` | Дублируйте текущую активную ветку в новый сеанс. |\n| `/compact [prompt]` | Обобщить старый контекст; см. [Compaction](compaction.md) |\n| `/export [file]` | Экспорт сеанса в HTML |\n| `/share` | Загрузить как частную суть GitHub с общей HTML-ссылкой. |\n\n## Возобновление и удаление сеансов\n\n`/resume` открывает интерактивный выбор сеанса для текущего проекта. `pi -r` открывает тот же сборщик при запуске.\n\nВ пикере вы можете:\n\n- поиск, набрав\n- переключить отображение пути с помощью Ctrl+P\n- переключить режим сортировки с помощью Ctrl+S\n- фильтровать именованные сеансы с помощью Ctrl+N\n- переименовать с помощью Ctrl+R\n- удалить с помощью Ctrl+D, затем подтвердить\n\nЕсли доступно, pi использует `trash` CLI для удаления вместо окончательного удаления файлов.\n\n## Именование сеансов\n\nИспользуйте `/name <name>`, чтобы установить удобочитаемое имя сеанса:\n\n```text\n/name Refactor auth module\n```\n\nЗадайте имя при запуске с помощью `--name` или `-n`:\n\n```bash\npi --name \"Refactor auth module\"\npi --name \"CI audit\" -p \"Review this build failure\"\n```\n\nИменованные сеансы легче найти в `/resume` и `pi -r`.\n\n## Ветвление с `/tree`\n\nСессии хранятся в виде деревьев. Каждая запись имеет `id` и `parentId`, а текущая позиция является активным листом. `/tree` позволяет перейти к любой предыдущей точке и продолжить оттуда, не создавая новый файл.\n\n<p align=\"center\"><img src=\"images/tree-view.png\" alt=\"Tree View\" width=\"600\"></p>\n\nПример формы:\n\n```text\n├─ user: \"Hello, can you help...\"\n│  └─ assistant: \"Of course! I can...\"\n│     ├─ user: \"Let's try approach A...\"\n│     │  └─ assistant: \"For approach A...\"\n│     │     └─ user: \"That worked...\"  ← active\n│     └─ user: \"Actually, approach B...\"\n│        └─ assistant: \"For approach B...\"\n```\n\n### Элементы управления деревом\n\n| Ключ | Действие |\n|-----|--------|\n| ↑/↓ | Навигация по видимым записям |\n| ←/→ | Страница вверх/вниз |\n| Ctrl+ ←/Ctrl+→ или Alt+ ←/Alt+→ | Складывайте/разворачивайте или прыгайте между сегментами ветвей |\n| Шифт+Л | Установить или удалить метку для выбранной записи |\n| Шифт+Т | Переключить временные метки ярлыков |\n| Входить | Выберите запись |\n| Выход/Ctrl+C | Отмена |\n| Ctrl+О | Режим циклического фильтра |\n\nРежимы фильтрации: по умолчанию, без инструментов, только для пользователя, только с метками и все. Настройте значение по умолчанию с помощью `treeFilterMode` в [Settings](settings.md).\n\n### Поведение выбора\n\nВыбор пользователя или пользовательского сообщения:\n\n1. Перемещает лист к родительскому элементу выбранного сообщения.\n2. Помещает выделенный текст сообщения в редактор.\n3. Позволяет редактировать и повторно отправлять, создавая новую ветку.\n\nВыбор помощника, инструмента, уплотнителя или другой непользовательской записи:\n\n1. Перемещает лист к этой записи.\n2. Оставляет редактор пустым.\n3. Позволяет продолжить с этого момента.\n\nВыбор сообщения корневого пользователя сбрасывает лист до пустого разговора и помещает исходное приглашение в редактор.\n\n## `/tree`, `/fork` и `/clone`\n\n| Особенность | `/tree` | `/fork` | `/clone` |\n|---------|---------|---------|----------|\n| Выход | Тот же файл сеанса | Новый файл сеанса | Новый файл сеанса |\n| Вид | Полное дерево | Селектор сообщений пользователя | Текущая активная ветка |\n| Типичное использование | Изучите существующие альтернативы | Начать новый сеанс с предыдущего приглашения | Дублируйте текущую работу, прежде чем продолжить |\n| Краткое содержание | Необязательная сводка ветвей | Никто | Никто |\n\nИспользуйте `/tree`, если хотите объединить альтернативы. Используйте `/fork` или `/clone`, если вам нужен отдельный файл сеанса.\n\n## Резюме филиалов\n\nКогда `/tree` переключается с одной ветви на другую, pi может суммировать заброшенную ветвь и прикрепить это резюме к новой позиции. Это сохраняет важный контекст пути, который вы покинули, без повторного воспроизведения всей ветки.\n\nПри появлении запроса выберите один из:\n\n1. нет резюме\n2. подвести итоги с помощью подсказки по умолчанию\n3. подведите итоги с помощью инструкций по индивидуальному фокусу\n\nСм. [Compaction](compaction.md) для branch summarization внутренних деталей и удлинителей.\n\n## Формат сессии\n\nФайлы сеансов имеют номер JSONL и содержат записи сообщений, изменения модели, изменения на уровне мышления, метки, уплотнения, сводки ветвей и записи расширений.\n\nИнформацию о парсерах, расширениях, использовании SDK и полном сеансе SessionManager API см. в [Session Format](session-format.md).","sourceFile":"sessions.md"},"settings":{"title":"Настройки","markdown":"Pi использует файлы настроек JSON, в которых настройки проекта переопределяют глобальные настройки.\n\n| Расположение | Объем |\n|----------|-------|\n| `~/.pi/agent/settings.json` | Глобальный (все проекты) |\n| `.pi/settings.json` | Проект (текущий каталог) |\n\nРедактируйте напрямую или используйте `/settings` для общих опций.\n\n## Проект Траст\n\nПри интерактивном запуске pi спрашивает, прежде чем доверять папке проекта, которая содержит локальные настройки проекта, ресурсы или проект `.agents/skills` и не имеет сохраненного решения для этой папки или родительской папки в `~/.pi/agent/trust.json`. Доверие к проекту позволяет pi загружать ресурсы `.pi/settings.json` и `.pi`, устанавливать недостающие пакеты проекта и выполнять расширения проекта.\n\nВ неинтерактивных режимах (`-p`, `--mode json` и `--mode rpc`) запрос доверия не отображается. Без применимого сохраненного решения о доверии они используют `defaultProjectTrust` из глобальных настроек: `ask` (по умолчанию) и `never` игнорируют эти ресурсы проекта, а `always` доверяют им. Нажмите `--approve`/`-a` или `--no-approve`/`-na`, чтобы отменить доверие проекта на один запуск.\n\nЕсли никакое расширение или сохраненное решение не применимо, `defaultProjectTrust` управляет резервным поведением. Установите его на `\"ask\"`, `\"always\"` или `\"never\"` в `~/.pi/agent/settings.json` или измените его с помощью `/settings`.\n\n`pi config` и команды пакета используют один и тот же поток доверия проекта, за исключением того, что `pi update` никогда не запрашивает. Нажмите `--approve`, чтобы доверять локальным настройкам проекта для одной команды, или `--no-approve`, чтобы игнорировать их.\n\nИспользуйте `/trust` в интерактивном режиме, чтобы сохранить решение о доверии проекта для будущих сеансов, включая доверие к непосредственной родительской папке. Пишется только `~/.pi/agent/trust.json`; текущий сеанс не перезагружается, поэтому перезапустите pi, чтобы изменения вступили в силу.\n\n## Все настройки\n\n### Модель и мышление\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `defaultProvider` | нить | - | Поставщик по умолчанию (например, `\"anthropic\"`, `\"openai\"`) |\n| `defaultModel` | нить | - | Идентификатор модели по умолчанию |\n| `defaultThinkingLevel` | нить | - | `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"` |\n| `hideThinkingBlock` | логическое значение | `false` | Скрыть мыслительные блоки в выводе |\n| `showCacheMissNotices` | логическое значение | `false` | Показывать уведомления о расшифровке при значительных промахах в кэше подсказок. |\n| `thinkingBudgets` | объект | - | Пользовательские бюджеты токенов на каждый уровень мышления |\n\n#### мышлениеБюджеты\n\n```json\n{\n  \"thinkingBudgets\": {\n    \"minimal\": 1024,\n    \"low\": 4096,\n    \"medium\": 10240,\n    \"high\": 32768\n  }\n}\n```\n\n### Пользовательский интерфейс и дисплей\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `theme` | нить | `\"dark\"` | Название темы (`\"dark\"`, `\"light\"` или пользовательское) |\n| `externalEditor` | нить | `$VISUAL`, затем `$EDITOR`, затем «Блокнот» в Windows или `nano` в другом месте. | Команда внешнего редактора Ctrl+G; имеет приоритет над переменными среды |\n| `quietStartup` | логическое значение | `false` | Скрыть заголовок запуска |\n| `defaultProjectTrust` | нить | `\"ask\"` | Доверительное поведение резервного проекта: `\"ask\"`, `\"always\"` или `\"never\"`. Только глобальная настройка |\n| `collapseChangelog` | логическое значение | `false` | Показывать сокращенный журнал изменений после обновлений |\n| `enableInstallTelemetry` | логическое значение | `true` | Отправьте анонимный пинг версии установки/обновления после первой установки или обновлений, обнаруженных в журнале изменений. Это не контролирует проверки обновлений. |\n| `enableAnalytics` | логическое значение | `false` | Согласитесь на обмен аналитическими данными. В настоящее время запрашивается только во время первоначальной экспериментальной настройки (`PI_EXPERIMENTAL=1`) |\n| `trackingId` | нить | - | Идентификатор отслеживания Google Analytics, генерируется при включении `enableAnalytics`. |\n| `doubleEscapeAction` | нить | `\"tree\"` | Действие для двойного выхода: `\"tree\"`, `\"fork\"` или `\"none\"`. |\n| `treeFilterMode` | нить | `\"default\"` | Фильтр по умолчанию для `/tree`: `\"default\"`, `\"no-tools\"`, `\"user-only\"`, `\"labeled-only\"`, `\"all\"` |\n| `editorPaddingX` | число | `0` | Горизонтальное заполнение для редактора ввода (0-3) |\n| `outputPad` | число | `1` | Горизонтальное заполнение для сообщений пользователя, сообщений помощника и мыслей (0 или 1) |\n| `autocompleteMaxVisible` | число | `5` | Максимальное количество видимых элементов в раскрывающемся списке автозаполнения (3–20) |\n| `showHardwareCursor` | логическое значение | `false` | Покажите курсор терминала, пока TUI позиционирует его для поддержки IME. |\n| `tuiMode` | нить | `\"regular\"` | Интерактивный режим TUI: `\"regular\"` или экспериментальный `\"fullscreen\"`. Изменения из `/settings` вступают в силу немедленно; `--tui-mode` переопределяет этот параметр при запуске |\n| `fullscreenExitOutput` | нить | `\"transcript\"` | Вывод полноэкранного выхода: `\"transcript\"` печатает окончательную расшифровку и подсказку о возобновлении, а `\"resume-hint\"` восстанавливает предыдущий экран и печатает только подсказку о возобновлении. Не имеет эффекта в обычном режиме TUI. |\n| `fullscreenScrollbar` | нить | `\"auto\"` | Полоса прокрутки полноэкранной расшифровки: `\"auto\"` временно показывает ее во время прокрутки, `\"always\"` резервирует крайний правый столбец и сохраняет его видимым, а `\"hidden\"` скрывает его. Не имеет эффекта в обычном режиме TUI. |\n\nДля VS Code добавьте `--wait`, чтобы число pi возобновилось после выхода из редактора:\n\n```json\n{\n  \"externalEditor\": \"code --wait\"\n}\n```\n\n### Телеметрия и проверка обновлений\n\n`enableInstallTelemetry` контролирует только анонимный пинг установки/обновления до `https://pi.dev/api/report-install`. Отказ от телеметрии не отключает проверку обновлений; Pi по-прежнему может получить `https://pi.dev/api/latest-version` для поиска последней версии.\n\nУстановите `PI_SKIP_VERSION_CHECK=1`, чтобы отключить проверку обновления версии Pi. Используйте `--offline` или `PI_OFFLINE=1`, чтобы отключить все описанные здесь сетевые операции при запуске, включая проверки обновлений, проверки обновлений пакетов и телеметрию установки/обновления.\n\n### Сеть\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `httpProxy` | нить | - | URL-адрес HTTP-прокси применяется как `HTTP_PROXY` и `HTTPS_PROXY`. Только глобальная настройка. |\n\n```json\n{\n  \"httpProxy\": \"http://127.0.0.1:7890\"\n}\n```\n\n### Предупреждения\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `warnings.anthropicExtraUsage` | логическое значение | `true` | Показывать предупреждение, когда для аутентификации подписки Anthropic может использоваться платное дополнительное использование. |\n\n```json\n{\n  \"warnings\": {\n    \"anthropicExtraUsage\": false\n  }\n}\n```\n\n### Уплотнение\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `compaction.enabled` | логическое значение | `true` | Включить автоматическое сжатие |\n| `compaction.reserveTokens` | число | `16384` | Токены зарезервированы для ответа LLM |\n| `compaction.keepRecentTokens` | число | `20000` | Последние токены, которые нужно сохранить (не суммируются) |\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n### Резюме филиала\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `branchSummary.reserveTokens` | число | `16384` | Токены зарезервированы для branch summarization |\n| `branchSummary.skipPrompt` | логическое значение | `false` | Пропустить «Обобщить ветку?» подсказка при навигации по `/tree` (по умолчанию нет сводки) |\n\n### Повторить попытку\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `retry.enabled` | логическое значение | `true` | Включить автоматическую повторную попытку на уровне агента при временных ошибках |\n| `retry.maxRetries` | число | `3` | Максимальное количество повторных попыток на уровне агента |\n| `retry.baseDelayMs` | число | `2000` | Базовая задержка для экспоненциальной задержки на уровне агента (2 с, 4 с, 8 с) |\n| `retry.provider.timeoutMs` | число | SDK по умолчанию | Тайм-аут запроса поставщика/SDK в миллисекундах |\n| `retry.provider.maxRetries` | число | `0` | Поставщик/SDK повторных попыток |\n| `retry.provider.maxRetryDelayMs` | число | `60000` | Максимальная запрашиваемая сервером задержка перед сбоем (60 с) |\n\nКогда провайдер запрашивает задержку повтора дольше, чем `retry.provider.maxRetryDelayMs`, запрос немедленно завершается с ошибкой, а не молча ждет. Установите его на `0`, чтобы отключить ограничение.\n\nОставьте `retry.provider.maxRetries` на `0`, если повторные попытки на уровне поставщика явно не требуются. Установка значения выше `0` может привести к тому, что SDK/повторные попытки поставщика будут обрабатывать ошибки превышения лимита использования до того, как Pi увидит их, что может заблокировать агент до тех пор, пока в некоторых обстоятельствах квота провайдера не будет сброшена.\n\n```json\n{\n  \"retry\": {\n    \"enabled\": true,\n    \"maxRetries\": 3,\n    \"baseDelayMs\": 2000,\n    \"provider\": {\n      \"timeoutMs\": 3600000,\n      \"maxRetries\": 0,\n      \"maxRetryDelayMs\": 60000\n    }\n  }\n}\n```\n\n### Доставка сообщений\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `steeringMode` | нить | `\"one-at-a-time\"` | Как отправляются управляющие сообщения: `\"all\"` или `\"one-at-a-time\"`. |\n| `followUpMode` | нить | `\"one-at-a-time\"` | Как отправляются последующие сообщения: `\"all\"` или `\"one-at-a-time\"`. |\n| `transport` | нить | `\"auto\"` | Предпочтительный транспорт для поставщиков, поддерживающих несколько видов транспорта: `\"sse\"`, `\"websocket\"`, `\"websocket-cached\"` или `\"auto\"`. |\n| `httpIdleTimeoutMs` | число | `300000` | Тайм-аут простоя заголовка/тела HTTP в миллисекундах, также используется провайдерами с явными тайм-аутами простоя потока. Установите значение `0`, чтобы отключить. |\n| `websocketConnectTimeoutMs` | число | `15000` | Тайм-аут подключения/открытия WebSocket в миллисекундах для поставщиков, поддерживающих транспорты WebSocket. Установите значение `0`, чтобы отключить. |\n\n### Терминал и изображения\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `terminal.showImages` | логическое значение | `true` | Показать изображения в терминале (если поддерживается) |\n| `terminal.imageWidthCells` | число | `60` | Предпочтительная ширина встроенного изображения в терминальных ячейках |\n| `terminal.clearOnShrink` | логическое значение | `false` | Очищать пустые строки при сжатии содержимого (может вызвать мерцание) |\n| `images.autoResize` | логическое значение | `true` | Измените размер изображения до максимального размера 2000x2000. Применяется к вложениям `@file`, `read` и изображениям, возвращаемым инструментами. |\n| `images.blockImages` | логическое значение | `false` | Заблокировать отправку всех изображений в LLM |\n\n### Оболочка\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `shellPath` | нить | - | Пользовательский путь к оболочке (например, для Cygwin в Windows); поддерживает ведущий `~` для домашнего каталога |\n| `shellCommandPrefix` | нить | - | Префикс для каждой команды bash (например, `\"shopt -s expand_aliases\"`) |\n| `npmCommand` | нить[] | - | Команда argv, используемая для операций поиска/установки пакета npm (например, `[\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]`) |\n\n```json\n{\n  \"npmCommand\": [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]\n}\n```\n\n`npmCommand` используется для всех операций менеджера пакетов npm, включая установку, удаление и установку зависимостей внутри пакетов git. Пакеты npm на уровне пользователя устанавливаются в `~/.pi/agent/npm/`; Пакеты npm в рамках проекта устанавливаются в `.pi/npm/`. Используйте записи в стиле argv именно так, как должен быть запущен процесс. Когда настроен `npmCommand`, при установке зависимостей пакетов git используется простой `install`, чтобы избежать использования флагов, специфичных для npm, в оболочках или альтернативных менеджерах пакетов.\n\n### Сессии\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `sessionDir` | нить | - | Каталог, в котором хранятся файлы сеанса. Принимает абсолютные или относительные пути плюс `~`. |\n\n```json\n{ \"sessionDir\": \".pi/sessions\" }\n```\n\nЕсли несколько источников указывают каталог сеанса, приоритет имеет значение `--session-dir`, `PI_CODING_AGENT_SESSION_DIR`, а затем `sessionDir` в файле settings.json.\n\n### Модель Велоспорт\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `enabledModels` | нить[] | - | Шаблоны моделей для циклического переключения Ctrl+P (тот же формат, что и флаг `--models` CLI) |\n\n```json\n{\n  \"enabledModels\": [\"claude-*\", \"gpt-4o\", \"gemini-2*\"]\n}\n```\n\n### Markdown\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `markdown.codeBlockIndent` | нить | `\"  \"` | Отступы для блоков кода |\n| `markdown.mermaid` | нить | `\"streaming\"` | Режим рендеринга русалки: `\"off\"`, `\"final\"` или `\"streaming\"`. |\n\n### Ресурсы\n\nЭти настройки определяют, откуда загружать расширения, навыки, подсказки и темы.\n\nПути в `~/.pi/agent/settings.json` разрешаются относительно `~/.pi/agent`. Пути в `.pi/settings.json` разрешаются относительно `.pi`. Поддерживаются абсолютные пути и `~`.\n\n| Параметр | Тип | По умолчанию | Описание |\n|---------|------|---------|-------------|\n| `packages` | множество | `[]` | npm/git пакеты для загрузки ресурсов из |\n| `extensions` | нить[] | `[]` | Пути к файлам или каталогам локальных расширений |\n| `skills` | нить[] | `[]` | Пути или каталоги локальных файлов навыков |\n| `prompts` | нить[] | `[]` | Пути или каталоги локальных шаблонов приглашений |\n| `themes` | нить[] | `[]` | Пути или каталоги к файлам локальной темы |\n| `enableSkillCommands` | логическое значение | `true` | Зарегистрируйте навыки как команды `/skill:name` |\n\nМассивы поддерживают шаблоны glob и исключения. Используйте `!pattern`, чтобы исключить. Используйте `+path`, чтобы принудительно включить точный путь, и `-path`, чтобы принудительно исключить точный путь.\n\n#### пакеты\n\nСтроковая форма загружает все ресурсы из пакета:\n\n```json\n{\n  \"packages\": [\"pi-skills\", \"@org/my-extension\"]\n}\n```\n\nФорма объекта фильтрует, какие ресурсы загружать:\n\n```json\n{\n  \"packages\": [\n    {\n      \"source\": \"pi-skills\",\n      \"skills\": [\"brave-search\", \"transcribe\"],\n      \"extensions\": []\n    }\n  ]\n}\n```\n\nСм. [packages.md](packages.md) для получения подробной информации об управлении пакетами.\n\n## Пример\n\n```json\n{\n  \"defaultProvider\": \"anthropic\",\n  \"defaultModel\": \"claude-sonnet-4-20250514\",\n  \"defaultThinkingLevel\": \"medium\",\n  \"theme\": \"dark\",\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  },\n  \"retry\": {\n    \"enabled\": true,\n    \"maxRetries\": 3\n  },\n  \"enabledModels\": [\"claude-*\", \"gpt-4o\"],\n  \"warnings\": {\n    \"anthropicExtraUsage\": true\n  },\n  \"packages\": [\"pi-skills\"]\n}\n```\n\n## Переопределения проекта\n\nНастройки проекта (`.pi/settings.json`) переопределяют глобальные настройки. Вложенные объекты объединяются:\n\n```json\n// ~/.pi/agent/settings.json (global)\n{\n  \"theme\": \"dark\",\n  \"compaction\": { \"enabled\": true, \"reserveTokens\": 16384 }\n}\n\n// .pi/settings.json (project)\n{\n  \"compaction\": { \"reserveTokens\": 8192 }\n}\n\n// Result\n{\n  \"theme\": \"dark\",\n  \"compaction\": { \"enabled\": true, \"reserveTokens\": 8192 }\n}\n```","sourceFile":"settings.md"},"shell-aliases":{"title":"Псевдонимы оболочки","markdown":"Pi запускает bash в неинтерактивном режиме (`bash -c`), который по умолчанию не расширяет псевдонимы.\n\nЧтобы включить псевдонимы оболочки, добавьте к `~/.pi/agent/settings.json`:\n\n```json\n{\n  \"shellCommandPrefix\": \"shopt -s expand_aliases\\neval \\\"$(grep '^alias ' ~/.zshrc)\\\"\"\n}\n```\n\nНастройте путь (`~/.zshrc`, `~/.bashrc` и т. д.) в соответствии с конфигурацией вашей оболочки.","sourceFile":"shell-aliases.md"},"skills":{"title":"Skills","markdown":"> Пи может создавать навыки. Попросите его создать его для вашего варианта использования.\n\n\nSkills — это автономные пакеты возможностей, которые агент загружает по требованию. Навык предоставляет специализированные рабочие процессы, инструкции по настройке, вспомогательные сценарии и справочную документацию для конкретных задач.\n\nPi реализует [Agent Skills standard](https://agentskills.io/specification), предупреждая о большинстве нарушений, но оставаясь снисходительным. Pi позволяет именам навыков отличаться от их родительского каталога, хотя стандарт это запрещает; это правило неоптимально для общих каталогов навыков, используемых в нескольких системах агентов.\n\n## Оглавление\n\n- [Locations](#locations)\n- [How Skills Work](#how-skills-work)\n- [Skill Commands](#skill-commands)\n- [Skill Structure](#skill-structure)\n- [Frontmatter](#frontmatter)\n- [Validation](#validation)\n- [Example](#example)\n- [Skill Repositories](#skill-repositories)\n\n## Локации\n\n> **Безопасность:** Skills может указать модели выполнить любое действие и может включать исполняемый код, который вызывает модель. Перед использованием просмотрите содержание навыков.\n\nPi загружает навыки из:\n\n- Глобальный:\n  - `~/.pi/agent/skills/`\n  - `~/.agents/skills/`\n- Проект (только после того, как проекту доверяют):\n  - `.pi/skills/`\n  - `.agents/skills/` в `cwd` и каталогах предков (до корня репозитория git или корня файловой системы, если он не находится в репозитории)\n- Пакеты: `skills/` каталогов или `pi.skills` записей в `package.json`.\n- Настройки: `skills` массив с файлами или каталогами.\n- CLI: `--skill <path>` (повторяемый, аддитивный даже с `--no-skills`)\n\nПравила открытия:\n- В `~/.pi/agent/skills/` и `.pi/skills/` файлы прямого корня `.md` обнаруживаются как отдельные навыки.\n- Во всех локациях навыков рекурсивно обнаруживаются каталоги, содержащие `SKILL.md`.\n- В `~/.agents/skills/` и проекте `.agents/skills/` корневые файлы `.md` игнорируются.\n\nОтключите обнаружение с помощью `--no-skills` (явные пути `--skill` все равно загружаются).\n\n### Использование Skills из других обвязок\n\nЧтобы использовать навыки из Claude Code или OpenAI Codex, добавьте их каталоги в настройки:\n\n```json\n{\n  \"skills\": [\n    \"~/.claude/skills\",\n    \"~/.codex/skills\"\n  ]\n}\n```\n\nДля навыков Claude Code на уровне проекта добавьте к `.pi/settings.json`:\n\n```json\n{\n  \"skills\": [\"../.claude/skills\"]\n}\n```\n\n## Как работает Skills\n\n1. При запуске pi сканирует расположение навыков и извлекает имена и описания.\n2. Системная подсказка включает доступные навыки в формате XML согласно [specification](https://agentskills.io/integrate-skills)\n3. Когда задача совпадает, агент использует `read` для загрузки полного SKILL.md (модели не всегда делают это; используйте подсказку или `/skill:name`, чтобы принудительно это сделать)\n4. Агент следует инструкциям, используя относительные пути для ссылки на сценарии и ресурсы.\n\nЭто прогрессивное раскрытие: только описания всегда находятся в контексте, полные инструкции загружаются по требованию.\n\n## Команды навыков\n\nSkills зарегистрируйте как `/skill:name` команды:\n\n```bash\n/skill:brave-search           # Load and execute the skill\n/skill:pdf-tools extract      # Load skill with arguments\n```\n\nАргументы после команды добавляются к содержимому навыка как `User: <args>`.\n\nПереключайте команды навыков с помощью `/settings` в интерактивном режиме или `settings.json`:\n\n```json\n{\n  \"enableSkillCommands\": true\n}\n```\n\n## Структура навыков\n\nНавык — это каталог с файлом `SKILL.md`. Все остальное — произвольная форма.\n\n```\nmy-skill/\n├── SKILL.md              # Required: frontmatter + instructions\n├── scripts/              # Helper scripts\n│   └── process.sh\n├── references/           # Detailed docs loaded on-demand\n│   └── api-reference.md\n└── assets/\n    └── template.json\n```\n\n### Формат SKILL.md\n\n````markdown\n---\nname: my-skill\ndescription: What this skill does and when to use it. Be specific.\n---\n\n# My Skill\n\n## Setup\n\nRun once before first use:\n```bash\ncd /path/to/skill && npm установить\n```\n\n## Usage\n\n```bash\n./scripts/process.sh <input>\n```\n````\n\nИспользуйте относительные пути из каталога навыков:\n\n```markdown\nSee [the reference guide](references/REFERENCE.md) for details.\n```\n\n## Фронтматтер\n\nСогласно [Agent Skills specification](https://agentskills.io/specification#frontmatter-required):\n\n| Поле | Необходимый | Описание |\n|-------|----------|-------------|\n| `name` | Да | Макс. 64 символа. Строчные буквы a–z, 0–9, дефисы. В отличие от стандарта, Pi не требует, чтобы это соответствовало родительскому каталогу, поскольку это стандартное требование неоптимально для общих каталогов навыков. |\n| `description` | Да | Макс. 1024 символа. Что делает навык и когда его использовать. |\n| `license` | Нет | Имя лицензии или ссылка на связанный файл. |\n| `compatibility` | Нет | Макс. 500 символов. Требования к окружающей среде. |\n| `metadata` | Нет | Произвольное сопоставление ключ-значение. |\n| `allowed-tools` | Нет | Список предварительно одобренных инструментов (экспериментальный), разделенный пробелами. |\n| `disable-model-invocation` | Нет | Когда `true`, навык скрыт из системной подсказки. Пользователи должны использовать `/skill:name`. |\n\n### Правила имени\n\n- 1–64 символа\n- Только строчные буквы, цифры и дефисы\n- Нет начальных/конечных дефисов\n- Нет последовательных дефисов\nPi не требует, чтобы имя соответствовало родительскому каталогу. Стандарт агента Skills поддерживает это требование, но это требование неоптимально для общих каталогов навыков, используемых несколькими инструментами.\n\nДопустимо: `pdf-processing`, `data-analysis`, `code-review`\nНедопустимо: `PDF-Processing`, `-pdf`, `pdf--processing`\n\n### Описание Лучшие практики\n\nОписание определяет, когда агент загружает навык. Будьте конкретны.\n\nХороший:\n```yaml\ndescription: Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs. Use when working with PDF documents.\n```\n\nБедный:\n```yaml\ndescription: Helps with PDFs.\n```\n\n## Валидация\n\nPi проверяет навыки на соответствие стандарту агента Skills. Большинство проблем выдают предупреждения, но навык все равно загружается:\n\n- Имя превышает 64 символа или содержит недопустимые символы.\n- Имя начинается/оканчивается дефисом или содержит последовательные дефисы.\n- Описание превышает 1024 символа\n\nНеизвестные поля заголовка игнорируются.\n\n**Исключение:** Skills с отсутствующим описанием не загружаются.\n\nСтолкновения имен (одно и то же имя в разных местах) предупреждают и сохраняют первый найденный навык.\n\n## Пример\n\n```\nbrave-search/\n├── SKILL.md\n├── search.js\n└── content.js\n```\n\n**SKILL.md:**\n````markdown\n---\nname: brave-search\ndescription: Web search and content extraction via Brave Search API. Use for searching documentation, facts, or any web content.\n---\n\n# Brave Search\n\n## Setup\n\n```bash\ncd /path/to/brave-search && npm установить\n```\n\n## Search\n\n```bash\n./search.js \"query\" # Базовый поиск\n./search.js \"query\" --content # Включить содержимое страницы\n```\n\n## Extract Page Content\n\n```bash\n./content.js https://example.com\n```\n````\n\n## Хранилища навыков\n\n- [Anthropic Skills](https://github.com/anthropics/skills) - Обработка документов (docx, pdf, pptx, xlsx), веб-разработка\n- [Pi Skills](https://github.com/badlogic/pi-skills) — веб-поиск, автоматизация браузера, Google APIs, транскрипция","sourceFile":"skills.md"},"terminal-setup":{"title":"Настройка терминала","markdown":"Pi использует [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) для надежного обнаружения ключа-модификатора. Большинство современных терминалов поддерживают этот протокол, но некоторые требуют настройки.\n\n## Китти, iTerm2\n\nРаботайте нестандартно.\n\n## Apple Терминал\n\nPi включает расширенные ключевые отчеты, если они доступны. Если Terminal.app по-прежнему отправляет простой возврат для `Shift+Enter`, pi использует резервный локальный модификатор macOS, чтобы рассматривать этот возврат как `Shift+Enter`.\n\nЭтот запасной вариант работает только тогда, когда pi работает на том же Mac, что и Terminal.app. Он не может обнаружить локальную клавиатуру через удаленный SSH.\n\n## Призрачный\n\nДобавьте в свою конфигурацию Ghostty (`~/Library/Application Support/com.mitchellh.ghostty/config` в macOS, `~/.config/ghostty/config` в Linux):\n\n```\nkeybind = alt+backspace=text:\\x1b\\x7f\n```\n\nВ более старых версиях Claude Code могло быть добавлено это сопоставление Ghostty:\n\n```\nkeybind = shift+enter=text:\\n\n```\n\nЭто сопоставление отправляет необработанный байт перевода строки. Внутри pi это неотличимо от `Ctrl+J`, поэтому tmux и pi больше не видят реального ключевого события `shift+enter`.\n\nЕсли Claude Code 2.x или новее является единственной причиной, по которой вы добавили это сопоставление, вы можете удалить его, если только вы не хотите использовать Claude Code в tmux, где оно все еще требует этого сопоставления Ghostty.\n\nPi связывает `Ctrl+J` как псевдоним новой строки по умолчанию, поэтому `Shift+Enter` продолжает работать в tmux через это переназначение без дополнительной настройки pi.\n\n## ВезТерм\n\nWezTerm обычно работает «из коробки» для `Shift+Enter` через xterm ModifyOtherKeys. Чтобы явно использовать протокол клавиатуры Kitty, создайте `~/.wezterm.lua`:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.enable_kitty_keyboard = true\nreturn config\n```\n\nВ macOS WezTerm по умолчанию привязывает `Option+Enter` к полноэкранному режиму. Чтобы использовать `Option+Enter` для постановки в очередь отслеживания pi, добавьте переопределение этого ключа:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.keys = {\n  {\n    key = 'Enter',\n    mods = 'ALT',\n    action = wezterm.action.SendString('\\x1b[13;3u'),\n  },\n}\nreturn config\n```\n\nЕсли у вас уже есть таблица `config.keys`, добавьте в нее запись.\n\nВ WSL WezTerm может потребоваться видимый аппаратный курсор для позиционирования окна-кандидата IME. Если кандидаты CJK IME не следуют за текстовым курсором, установите `PI_HARDWARE_CURSOR=1` перед запуском pi или установите от `showHardwareCursor` до `true` в настройках.\n\n## рвение\n\nAlacritty обычно работает «из коробки» для `Shift+Enter`. В macOS `Option+Enter` может отображаться как обычный `Enter`. Чтобы использовать `Option+Enter` для последующей очереди Pi, добавьте к `~/.config/alacritty/alacritty.toml`:\n\n```toml\n[[keyboard.bindings]]\nkey = \"Enter\"\nmods = \"Alt\"\nchars = \"\\u001b[13;3u\"\n```\n\nПерезапустите Alacritty после изменения конфигурации.\n\n## VS Code (интегрированный терминал)\n\nVS Code 1.109.5 и новее по умолчанию включают протокол клавиатуры Kitty во встроенном терминале, поэтому `Shift+Enter` должно работать «из коробки».\n\nВерсии VS Code старше 1.109.5 требуют явного назначения клавиш терминала для `Shift+Enter`.\n\n`keybindings.json` локации:\n- macOS: `~/Library/Application Support/Code/User/keybindings.json`\n- Линукс: `~/.config/Code/User/keybindings.json`\n- Окна: `%APPDATA%\\\\Code\\\\User\\\\keybindings.json`\n\nДобавьте к `keybindings.json`:\n\n```json\n{\n  \"key\": \"shift+enter\",\n  \"command\": \"workbench.action.terminal.sendSequence\",\n  \"args\": { \"text\": \"\\u001b[13;2u\" },\n  \"when\": \"terminalFocus\"\n}\n```\n\n## Терминал Windows\n\nДобавьте в `settings.json` (Ctrl+Shift+ или Настройки → Открыть файл JSON), чтобы перенаправить измененные клавиши Enter, которые использует pi:\n\n```json\n{\n  \"actions\": [\n    {\n      \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;2u\" },\n      \"keys\": \"shift+enter\"\n    },\n    {\n      \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;3u\" },\n      \"keys\": \"alt+enter\"\n    }\n  ]\n}\n```\n\n- `Shift+Enter` вставляет новую строку.\n- Терминал Windows по умолчанию привязывает `Alt+Enter` к полноэкранному режиму. Это не позволяет pi получить `Alt+Enter` для последующей постановки в очередь.\n- Переназначение `Alt+Enter` на `sendInput` вместо этого перенаправляет настоящий ключевой аккорд на число «пи».\n\nЕсли у вас уже есть массив `actions`, добавьте в него объекты. Если старое полноэкранное поведение сохраняется, полностью закройте и снова откройте терминал Windows.\n\n## xfce4-терминал, терминатор\n\nЭти терминалы имеют ограниченную поддержку escape-последовательностей. Модифицированные клавиши Enter, такие как `Ctrl+Enter` и `Shift+Enter`, невозможно отличить от простых `Enter`, что не позволяет работать пользовательским сочетаниям клавиш, таким как `submit: [\"ctrl+enter\"]`.\n\nДля получения наилучших результатов используйте терминал, поддерживающий протокол клавиатуры Kitty:\n- [Kitty](https://sw.kovidgoyal.net/kitty/)\n- [Ghostty](https://ghostty.org/)\n- [WezTerm](https://wezfurlong.org/wezterm/)\n- [iTerm2](https://iterm2.com/)\n- [Alacritty](https://github.com/alacritty/alacritty) (требуется компиляция с поддержкой протокола Kitty)\n\n## IntelliJ IDEA (интегрированный терминал)\n\nВстроенный терминал имеет ограниченную поддержку escape-последовательностей. Shift+Enter нельзя отличить от Enter в терминале IntelliJ.\n\nЕсли вы хотите, чтобы аппаратный курсор был виден, установите `PI_HARDWARE_CURSOR=1` перед запуском pi (по умолчанию отключено для совместимости).\n\nДля получения наилучших результатов рассмотрите возможность использования специального эмулятора терминала.","sourceFile":"terminal-setup.md"},"termux":{"title":"Termux (Android) Настройка","markdown":"Pi работает на Android через [Termux](https://termux.dev/), эмулятор терминала и среду Linux для Android.\n\n## Предварительные условия\n\n1. Установите [Termux](https://github.com/termux/termux-app#installation) из GitHub или F-Droid (не из Google Play, эта версия устарела)\n2. Установите [Termux:API](https://github.com/termux/termux-api#installation) из GitHub или F-Droid для интеграции буфера обмена и других устройств.\n\n## Установка\n\n```bash\n# Update packages\npkg update && pkg upgrade\n\n# Install dependencies\npkg install nodejs termux-api git\n\n# Install pi\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\n# Create config directory\nmkdir -p ~/.pi/agent\n\n# Run pi\npi\n```\n\n## Поддержка буфера обмена\n\nОперации с буфером обмена используют `termux-clipboard-set` и `termux-clipboard-get` при работе в Termux. Чтобы они работали, необходимо установить приложение Termux:API.\n\nБуфер обмена изображений не поддерживается на Termux (функция вставки изображений `ctrl+v` не работает).\n\n## Пример AGENTS.md для Termux\n\nСоздайте `~/.pi/agent/AGENTS.md`, чтобы помочь агенту понять среду Termux:\n\n````markdown\n# Agent Environment: Termux on Android\n\n## Location\n- **OS**: Android (Termux terminal emulator)\n- **Home**: `/data/data/com.termux/files/home`\n- **Prefix**: `/data/data/com.termux/files/usr`\n- **Shared storage**: `/storage/emulated/0` (Downloads, Documents, etc.)\n\n## Opening URLs\n```bash\ntermux-open-url \"https://example.com\"\n```\n\n## Opening Files\n```bash\ntermux-open file.pdf # Открывается с помощью приложения по умолчанию\ntermux-open --chooser image.jpg # Выбрать приложение\n```\n\n## Clipboard\n```bash\ntermux-clipboard-set \"текст\" # Копировать\ntermux-clipboard-get # Вставить\n```\n\n## Notifications\n```bash\ntermux-notification -t «Название» -c «Содержимое»\n```\n\n## Device Info\n```bash\ntermux-battery-status # Информация о батарее\ntermux-wifi-connectioninfo # информация о Wi-Fi\ntermux-telephony-deviceinfo # Информация об устройстве\n```\n\n## Sharing\n```bash\ntermux-share -a send file.txt # Поделиться файлом\n```\n\n## Other Useful Commands\n```bash\ntermux-toast \"message\" # Быстрое всплывающее окно с тостом\ntermux-vibrate # Вибрация устройства\ntermux-tts-speak \"привет\" # Преобразование текста в речь\ntermux-camera-photo out.jpg # Сделать фото\n```\n\n## Notes\n- Termux:API app must be installed for `termux-*` commands\n- Use `pkg install termux-api` for the command-line tools\n- Storage permission needed for `/storage/emulated/0` access\n````\n\n## Ограничения\n\n- **Буфер обмена изображений отсутствует**: Termux буфер обмена API поддерживает только текст\n- **Нет собственных двоичных файлов**: некоторые дополнительные собственные зависимости (например, модуль буфера обмена) недоступны в Android ARM64 и пропускаются во время установки.\n- **Доступ к хранилищу**: чтобы получить доступ к файлам в `/storage/emulated/0` (загрузки и т. д.), запустите `termux-setup-storage` один раз, чтобы предоставить разрешения.\n\n## Поиск неисправностей\n\n### Буфер обмена не работает\n\nУбедитесь, что оба приложения установлены:\n1. Termux (из GitHub или F-Droid)\n2. Termux:API (из GitHub или F-Droid)\n\nЗатем установите инструменты CLI:\n```bash\npkg install termux-api\n```\n\n### Доступ к общему хранилищу запрещен.\n\nЗапустите один раз, чтобы предоставить разрешения на хранение:\n```bash\ntermux-setup-storage\n```\n\n### Node.js проблемы с установкой\n\nЕсли npm не работает, попробуйте очистить кеш:\n```bash\nnpm cache clean --force\n```","sourceFile":"termux.md"},"themes":{"title":"Темы","markdown":"> pi может создавать темы. Попросите его создать его для вашей установки.\n\n\nТемы — это файлы JSON, определяющие цвета для TUI.\n\n## Оглавление\n\n- [Locations](#locations)\n- [Selecting a Theme](#selecting-a-theme)\n- [Creating a Custom Theme](#creating-a-custom-theme)\n- [Theme Format](#theme-format)\n- [Color Tokens](#color-tokens)\n- [Color Values](#color-values)\n- [Tips](#tips)\n\n## Локации\n\nPi загружает темы из:\n\n- Встроенные: `dark`, `light`\n- Глобально: `~/.pi/agent/themes/*.json`\n- Проект: `.pi/themes/*.json` (только после того, как проекту доверяют)\n- Пакеты: `themes/` каталогов или `pi.themes` записей в `package.json`.\n- Настройки: `themes` массив с файлами или каталогами.\n- CLI: `--theme <path>` (повторяемый)\n\nОтключите обнаружение с помощью `--no-themes`.\n\n## Выбор темы\n\nВыберите тему с помощью `/settings` или `settings.json`:\n\n```json\n{\n  \"theme\": \"my-theme\"\n}\n```\n\nПри первом запуске pi определяет фон вашего терминала и по умолчанию устанавливает значение `dark` или `light`.\n\n## Создание пользовательской темы\n\n1. Создайте файл темы:\n\n```bash\nmkdir -p ~/.pi/agent/themes\nvim ~/.pi/agent/themes/my-theme.json\n```\n\n2. Определите тему со всеми необходимыми цветами (см. [Color Tokens](#color-tokens)):\n\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json\",\n  \"name\": \"my-theme\",\n  \"vars\": {\n    \"primary\": \"#00aaff\",\n    \"secondary\": 242\n  },\n  \"colors\": {\n    \"accent\": \"primary\",\n    \"border\": \"primary\",\n    \"borderAccent\": \"#00ffff\",\n    \"borderMuted\": \"secondary\",\n    \"success\": \"#00ff00\",\n    \"error\": \"#ff0000\",\n    \"warning\": \"#ffff00\",\n    \"muted\": \"secondary\",\n    \"dim\": 240,\n    \"text\": \"\",\n    \"thinkingText\": \"secondary\",\n    \"selectedBg\": \"#2d2d30\",\n    \"scrollbarThumb\": \"#555566\",\n    \"userMessageBg\": \"#2d2d30\",\n    \"userMessageText\": \"\",\n    \"customMessageBg\": \"#2d2d30\",\n    \"customMessageText\": \"\",\n    \"customMessageLabel\": \"primary\",\n    \"toolPendingBg\": \"#1e1e2e\",\n    \"toolSuccessBg\": \"#1e2e1e\",\n    \"toolErrorBg\": \"#2e1e1e\",\n    \"toolTitle\": \"primary\",\n    \"toolOutput\": \"\",\n    \"mdHeading\": \"#ffaa00\",\n    \"mdLink\": \"primary\",\n    \"mdLinkUrl\": \"secondary\",\n    \"mdCode\": \"#00ffff\",\n    \"mdCodeBlock\": \"\",\n    \"mdCodeBlockBorder\": \"secondary\",\n    \"mdQuote\": \"secondary\",\n    \"mdQuoteBorder\": \"secondary\",\n    \"mdHr\": \"secondary\",\n    \"mdListBullet\": \"#00ffff\",\n    \"toolDiffAdded\": \"#00ff00\",\n    \"toolDiffRemoved\": \"#ff0000\",\n    \"toolDiffContext\": \"secondary\",\n    \"syntaxComment\": \"secondary\",\n    \"syntaxKeyword\": \"primary\",\n    \"syntaxFunction\": \"#00aaff\",\n    \"syntaxVariable\": \"#ffaa00\",\n    \"syntaxString\": \"#00ff00\",\n    \"syntaxNumber\": \"#ff00ff\",\n    \"syntaxType\": \"#00aaff\",\n    \"syntaxOperator\": \"primary\",\n    \"syntaxPunctuation\": \"secondary\",\n    \"thinkingOff\": \"secondary\",\n    \"thinkingMinimal\": \"primary\",\n    \"thinkingLow\": \"#00aaff\",\n    \"thinkingMedium\": \"#00ffff\",\n    \"thinkingHigh\": \"#ff00ff\",\n    \"thinkingXhigh\": \"#ff0000\",\n    \"thinkingMax\": \"#ff0088\",\n    \"bashMode\": \"#ffaa00\"\n  }\n}\n```\n\n3. Выберите тему с помощью `/settings`.\n\n**Горячая перезагрузка.** Когда вы редактируете активный в данный момент файл пользовательской темы, pi автоматически перезагружает его для немедленной визуальной обратной связи.\n\n## Формат темы\n\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json\",\n  \"name\": \"my-theme\",\n  \"vars\": {\n    \"blue\": \"#0066cc\",\n    \"gray\": 242\n  },\n  \"colors\": {\n    \"accent\": \"blue\",\n    \"muted\": \"gray\",\n    \"text\": \"\",\n    ...\n  }\n}\n```\n\n- `name` обязателен, должен быть уникальным и не должен содержать `/`.\n- `vars` не является обязательным. Определите здесь повторно используемые цвета, а затем укажите их в `colors`.\n- `colors` должен определять все 51 требуемый токен. `thinkingMax` не является обязательным и возвращается к `thinkingXhigh`; `scrollbarThumb` не является обязательным и возвращается к `selectedBg`.\n\nПоле `$schema` включает автозаполнение и проверку редактора.\n\n## Цветные жетоны\n\nКаждая тема должна определять все 51 требуемый жетон цвета. `thinkingMax` и `scrollbarThumb` являются необязательными для совместимости с существующими темами; если они опущены, они используют `thinkingXhigh` и `selectedBg` соответственно.\n\n### Базовый пользовательский интерфейс (11 цветов)\n\n| Токен | Цель |\n|-------|---------|\n| `accent` | Основной акцент (логотип, выбранные элементы, курсор) |\n| `border` | Нормальные границы |\n| `borderAccent` | Выделенные границы |\n| `borderMuted` | Тонкие границы (редактор) |\n| `success` | Состояние успеха |\n| `error` | Состояния ошибок |\n| `warning` | Предупреждающие состояния |\n| `muted` | Вторичный текст |\n| `dim` | Третичный текст |\n| `text` | Текст по умолчанию (обычно `\"\"`) |\n| `thinkingText` | Текст блока мышления |\n\n### Фоны и контент (11 обязательны, 1 по желанию)\n\n| Токен | Цель |\n|-------|---------|\n| `selectedBg` | Фон выбранной линии |\n| `scrollbarThumb` | Полноэкранный фон большого пальца полосы прокрутки; необязательно, возвращается к `selectedBg` |\n| `userMessageBg` | Фон сообщения пользователя |\n| `userMessageText` | Текст сообщения пользователя |\n| `customMessageBg` | Фон сообщения расширения |\n| `customMessageText` | Текст сообщения расширения |\n| `customMessageLabel` | Метка сообщения расширения |\n| `toolPendingBg` | Ящик для инструментов (на рассмотрении) |\n| `toolSuccessBg` | Ящик для инструментов (успех) |\n| `toolErrorBg` | Ящик для инструментов (ошибка) |\n| `toolTitle` | Название инструмента |\n| `toolOutput` | Текст вывода инструмента |\n\n### Markdown (10 цветов)\n\n| Токен | Цель |\n|-------|---------|\n| `mdHeading` | Заголовки |\n| `mdLink` | Текст ссылки |\n| `mdLinkUrl` | URL-адрес ссылки |\n| `mdCode` | Встроенный код |\n| `mdCodeBlock` | Содержимое блока кода |\n| `mdCodeBlockBorder` | Заборы из кодовых блоков |\n| `mdQuote` | Текст цитаты |\n| `mdQuoteBorder` | Граница цитаты |\n| `mdHr` | Горизонтальное правило |\n| `mdListBullet` | Список маркеров |\n\n### Различия инструментов (3 цвета)\n\n| Токен | Цель |\n|-------|---------|\n| `toolDiffAdded` | Добавлены строки |\n| `toolDiffRemoved` | Удалены строки |\n| `toolDiffContext` | Контекстные строки |\n\n### Подсветка синтаксиса (9 цветов)\n\n| Токен | Цель |\n|-------|---------|\n| `syntaxComment` | Комментарии |\n| `syntaxKeyword` | Ключевые слова |\n| `syntaxFunction` | Имена функций |\n| `syntaxVariable` | Переменные |\n| `syntaxString` | Струны |\n| `syntaxNumber` | Числа |\n| `syntaxType` | Типы |\n| `syntaxOperator` | Операторы |\n| `syntaxPunctuation` | Пунктуация |\n\n### Границы уровня мышления (6 обязательны, 1 по желанию)\n\nЦвета границ редактора обозначают уровень мышления (визуальная иерархия от незаметного до заметного):\n\n| Токен | Цель |\n|-------|---------|\n| `thinkingOff` | Размышляя |\n| `thinkingMinimal` | Минимальное мышление |\n| `thinkingLow` | Низкое мышление |\n| `thinkingMedium` | Среднее мышление |\n| `thinkingHigh` | Высокое мышление |\n| `thinkingXhigh` | Очень высокое мышление |\n| `thinkingMax` | Максимальное мышление; необязательно, возвращается к `thinkingXhigh` |\n\n### Режим Bash (1 цвет)\n\n| Токен | Цель |\n|-------|---------|\n| `bashMode` | Граница редактора в режиме bash (префикс `!`) |\n\n### Экспорт HTML (необязательно)\n\nРаздел `export` управляет цветами для вывода HTML `/export`. Если этот параметр опущен, цвета получаются из `userMessageBg`.\n\n```json\n{\n  \"export\": {\n    \"pageBg\": \"#18181e\",\n    \"cardBg\": \"#1e1e24\",\n    \"infoBg\": \"#3c3728\"\n  }\n}\n```\n\n## Цветовые значения\n\nПоддерживаются четыре формата:\n\n| Формат | Пример | Описание |\n|--------|---------|-------------|\n| Шестигранник | `\"#ff0000\"` | 6-значный шестнадцатеричный RGB |\n| 256 цветов | `39` | xterm 256-индекс цветовой палитры (0-255) |\n| Переменная | `\"primary\"` | Ссылка на запись `vars` |\n| По умолчанию | `\"\"` | Цвет терминала по умолчанию |\n\n### 256-цветовая палитра\n\n- `0-15`: базовые цвета ANSI (зависят от терминала).\n- `16-231`: RGB-куб 6×6×6 (`16 + 36×R + 6×G + B`, где R,G,B равны 0–5)\n- `232-255`: шкала оттенков серого.\n\n### Совместимость терминалов\n\nPi использует 24-битные цвета RGB. Большинство современных терминалов поддерживают это (iTerm2, Kitty, WezTerm, Windows Terminal, VS Code). Для старых терминалов, поддерживающих только 256 цветов, число pi возвращается к ближайшему приближению.\n\nПроверьте поддержку truecolor:\n\n```bash\necho $COLORTERM  # Should output \"truecolor\" or \"24bit\"\n```\n\n## Советы\n\n**Тёмные терминалы**. Используйте яркие, насыщенные цвета с более высокой контрастностью.\n\n**Светлые терминалы**. Используйте более темные, приглушенные цвета с меньшей контрастностью.\n\n**Цветовая гармония.** Начните с базовой палитры (Nord, Gruvbox, Tokyo Night), определите ее в `vars` и последовательно используйте ссылки.\n\n**Тестирование.** Проверьте свою тему с различными типами сообщений, состояниями инструментов, содержимым уценки и длинным текстом.\n\n**Код VS:** Установите от `terminal.integrated.minimumContrastRatio` до `1` для получения точных цветов.\n\n## Примеры\n\nПосмотрите встроенные темы:\n- [dark.json](../src/modes/interactive/theme/dark.json)\n- [light.json](../src/modes/interactive/theme/light.json)","sourceFile":"themes.md"},"tmux":{"title":"tmux Настройка","markdown":"Pi работает внутри tmux, но tmux по умолчанию удаляет информацию о модификаторах из определенных клавиш. Без конфигурации `Shift+Enter` и `Ctrl+Enter` обычно неотличимы от обычного `Enter`.\n\n## Рекомендуемая конфигурация\n\nДобавьте к `~/.tmux.conf`:\n\n```tmux\nset -g extended-keys on\nset -g extended-keys-format csi-u\n```\n\nЗатем полностью перезапустите tmux:\n\n```bash\ntmux kill-server\ntmux\n```\n\nPi автоматически запрашивает расширенные отчеты о клавишах, когда протокол клавиатуры Kitty недоступен. С помощью `extended-keys-format csi-u`, tmux пересылаются измененные ключи в формате CSI-u, который является наиболее надежной конфигурацией. Для опции `extended-keys-format` требуется tmux 3.5 или более поздняя версия.\n\n## Почему рекомендуется `csi-u`\n\nТолько с:\n\n```tmux\nset -g extended-keys on\n```\n\ntmux по умолчанию равно `extended-keys-format xterm`. Когда приложение запрашивает расширенный отчет о ключах, измененные ключи пересылаются в формате xterm `modifyOtherKeys`, например:\n\n- `Ctrl+C` → `\\x1b[27;5;99~`\n- `Ctrl+D` → `\\x1b[27;5;100~`\n- `Ctrl+Enter` → `\\x1b[27;5;13~`\n\nПри использовании `extended-keys-format csi-u` пересылаются те же ключи, что и:\n\n- `Ctrl+C` → `\\x1b[99;5u`\n- `Ctrl+D` → `\\x1b[100;5u`\n- `Ctrl+Enter` → `\\x1b[13;5u`\n\nPi поддерживает оба формата, но `csi-u` — рекомендуемая настройка tmux.\n\n## Что это исправляет\n\nБез расширенных клавиш tmux измененные клавиши Enter сворачиваются в устаревшие последовательности:\n\n| Ключ | Без дополнительных ключей | С `csi-u` |\n|-----|-----------------|--------------|\n| Входить | `\\r` | `\\r` |\n| Shift+Ввод | `\\r` | `\\x1b[13;2u` |\n| Ctrl+Ввод | `\\r` | `\\x1b[13;5u` |\n| Alt/Option+Ввод | `\\x1b\\r` | `\\x1b[13;3u` |\n\nЭто влияет на сочетания клавиш по умолчанию (`Enter` для отправки, `Shift+Enter` для новой строки) и любые пользовательские сочетания клавиш с использованием модифицированного Enter.\n\n## Требования\n\n- tmux 3.5 или новее для `extended-keys-format csi-u` (запустите `tmux -V`, чтобы проверить)\n- Эмулятор терминала, поддерживающий расширенные ключи (Ghostty, Kitty, iTerm2, WezTerm, Windows Terminal)\n\nДля tmux 3.2–3.4 опустите `extended-keys-format csi-u`; Pi по-прежнему поддерживает формат xterm `modifyOtherKeys` по умолчанию для tmux.","sourceFile":"tmux.md"},"tui":{"title":"TUI Компоненты","markdown":"> pi может создавать компоненты TUI. Попросите его создать его для вашего варианта использования.\n\n\nExtensions и специальные инструменты могут отображать пользовательские компоненты TUI для интерактивных пользовательских интерфейсов. На этой странице описана система компонентов и доступные строительные блоки.\n\n**Источник:** [`@earendil-works/pi-tui`](https://github.com/earendil-works/pi-mono/tree/main/packages/tui)\n\n## Компонентный интерфейс\n\nВсе компоненты реализуют:\n\n```typescript\ninterface Component {\n  render(width: number): string[];\n  handleInput?(data: string): void;\n  wantsKeyRelease?: boolean;\n  invalidate(): void;\n}\n```\n\n| Метод | Описание |\n|--------|-------------|\n| `render(width)` | Возвращает массив строк (по одной на строку). Каждая строка **не должна превышать `width`**. |\n| `handleInput?(data)` | Получать ввод с клавиатуры, когда компонент находится в фокусе. |\n| `wantsKeyRelease?` | Если это правда, компонент получает события выпуска ключа (протокол Kitty). По умолчанию: ложь. |\n| `invalidate()` | Очистить кэшированное состояние рендеринга. Позвонил по поводу изменения темы. |\n\nTUI добавляет полный сброс SGR и сброс OSC 8 в конце каждой отображаемой строки. Стили не переносятся через строки. Если вы создаете многострочный текст со стилями, повторно примените стили для каждой строки или используйте `wrapTextWithAnsi()`, чтобы стили сохранялись для каждой перенесенной строки.\n\n## Фокусируемый интерфейс (поддержка IME)\n\nКомпоненты, отображающие текстовый курсор и нуждающиеся в поддержке IME (редактор метода ввода), должны реализовывать интерфейс `Focusable`:\n\n```typescript\nimport { CURSOR_MARKER, type Component, type Focusable } from \"@earendil-works/pi-tui\";\n\nclass MyInput implements Component, Focusable {\n  focused: boolean = false;  // Set by TUI when focus changes\n  \n  render(width: number): string[] {\n    const marker = this.focused ? CURSOR_MARKER : \"\";\n    // Emit marker right before the fake cursor\n    return [`> ${beforeCursor}${marker}\\x1b[7m${atCursor}\\x1b[27m${afterCursor}`];\n  }\n}\n```\n\nКогда компонент `Focusable` имеет фокус, TUI:\n1. Устанавливает `focused = true` на компоненте.\n2. Сканирует визуализированный вывод на наличие `CURSOR_MARKER` (Escape-последовательность APC нулевой ширины).\n3. Помещает курсор аппаратного терминала в это место.\n4. Показывает аппаратный курсор только тогда, когда `showHardwareCursor` включено.\n\nПо умолчанию курсор остается скрытым. Это сохраняет отрисовку поддельного курсора, но при этом позиционирует аппаратный курсор для терминалов, которые отслеживают окна-кандидаты IME со скрытыми курсорами. Некоторым терминалам требуется видимый аппаратный курсор для позиционирования IME; включите его с помощью `showHardwareCursor`, `setShowHardwareCursor(true)` или `PI_HARDWARE_CURSOR=1`. Встроенные компоненты `Editor` и `Input` уже реализуют этот интерфейс.\n\n### Компоненты контейнера со встроенными входными данными\n\nКогда компонент контейнера (диалог, селектор и т. д.) содержит дочерний элемент `Input` или `Editor`, контейнер должен реализовать `Focusable` и передать состояние фокуса дочернему элементу. В противном случае аппаратный курсор не будет расположен правильно для ввода IME.\n\n```typescript\nimport { Container, type Focusable, Input } from \"@earendil-works/pi-tui\";\n\nclass SearchDialog extends Container implements Focusable {\n  private searchInput: Input;\n\n  // Focusable implementation - propagate to child input for IME cursor positioning\n  private _focused = false;\n  get focused(): boolean {\n    return this._focused;\n  }\n  set focused(value: boolean) {\n    this._focused = value;\n    this.searchInput.focused = value;\n  }\n\n  constructor() {\n    super();\n    this.searchInput = new Input();\n    this.addChild(this.searchInput);\n  }\n}\n```\n\nБез этого распространения при вводе с использованием IME (китайского, японского, корейского и т. д.) окно-кандидат будет отображаться в неправильном положении на экране.\n\n## Использование компонентов\n\n**В расширениях** через `ctx.ui.custom()`:\n\n```typescript\npi.on(\"session_start\", async (_event, ctx) => {\n  const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>\n    new MyComponent({\n      theme,\n      keybindings,\n      onChange: () => tui.requestRender(),\n      onSelect: (value) => done(value),\n      onCancel: () => done(null),\n    })\n  );\n});\n```\n\n**В пользовательских инструментах** через `ctx.ui.custom()`:\n\n```typescript\nasync execute(toolCallId, params, signal, onUpdate, ctx) {\n  const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>\n    new MyComponent({\n      theme,\n      keybindings,\n      onChange: () => tui.requestRender(),\n      onSelect: (value) => done(value),\n      onCancel: () => done(null),\n    })\n  );\n  // Use result...\n}\n```\n\n## Наложения\n\nНаложения отображают компоненты поверх существующего контента, не очищая экран. Передайте от `{ overlay: true }` до `ctx.ui.custom()`:\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyDialog({ onClose: done }),\n  { overlay: true }\n);\n```\n\nДля позиционирования и размера используйте `overlayOptions`:\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new SidePanel({ onClose: done }),\n  {\n    overlay: true,\n    overlayOptions: {\n      // Size: number or percentage string\n      width: \"50%\",          // 50% of terminal width\n      minWidth: 40,          // minimum 40 columns\n      maxHeight: \"80%\",      // max 80% of terminal height\n\n      // Position: anchor-based (default: \"center\")\n      anchor: \"right-center\", // 9 positions: center, top-left, top-center, etc.\n      offsetX: -2,            // offset from anchor\n      offsetY: 0,\n\n      // Or percentage/absolute positioning\n      row: \"25%\",            // 25% from top\n      col: 10,               // column 10\n\n      // Margins\n      margin: 2,             // all sides, or { top, right, bottom, left }\n\n      // Responsive: hide on narrow terminals\n      visible: (termWidth, termHeight) => termWidth >= 80,\n    },\n    // Get handle for programmatic focus and visibility control\n    onHandle: (handle) => {\n      // handle.focus() - focus this overlay and bring it to the visual front\n      // handle.unfocus() - release input to normal fallback\n      // handle.unfocus({ target }) - release input to a specific component or null\n      // handle.setHidden(true/false) - toggle visibility\n      // handle.hide() - permanently remove\n    },\n  }\n);\n```\n\n### Наложение фокуса\n\nСфокусированное видимое наложение сохраняет право собственности на входные данные во временном пользовательском интерфейсе без наложения. Если наложение открывает другой компонент `ctx.ui.custom()` без `{ overlay: true }`, этот замещающий пользовательский интерфейс получает входные данные, пока он активен; когда он закрывается, сфокусированное наложение может вернуть ввод.\n\nИспользуйте `handle.unfocus()`, когда видимое наложение должно перестать владеть входными данными и позволить TUI вернуться к другому видимому наложению захвата или предыдущей цели фокуса. Используйте `handle.unfocus({ target })`, когда определенный компонент должен получать входные данные, в то время как наложение остается видимым. Умышленная передача `{ target: null }` не оставляет сфокусированного компонента до тех пор, пока фокус не будет установлен снова.\n\n### Жизненный цикл наложения\n\nКомпоненты наложения удаляются при закрытии. Не используйте ссылки повторно — создавайте новые экземпляры:\n\n```typescript\n// Wrong - stale reference\nlet menu: MenuComponent;\nawait ctx.ui.custom((_, __, ___, done) => {\n  menu = new MenuComponent(done);\n  return menu;\n}, { overlay: true });\nsetActiveComponent(menu);  // Disposed\n\n// Correct - re-call to re-show\nconst showMenu = () => ctx.ui.custom((_, __, ___, done) => \n  new MenuComponent(done), { overlay: true });\n\nawait showMenu();  // First show\nawait showMenu();  // \"Back\" = just call again\n```\n\nСм. [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) для подробных примеров, охватывающих привязки, поля, размещение, адаптивную видимость и анимацию.\n\n## Встроенные компоненты\n\nИмпорт из `@earendil-works/pi-tui`:\n\n```typescript\nimport { Text, Box, Container, Spacer, Markdown } from \"@earendil-works/pi-tui\";\n```\n\n### Текст\n\nМногострочный текст с переносом слов.\n\n```typescript\nconst text = new Text(\n  \"Hello World\",    // content\n  1,                // paddingX (default: 1)\n  1,                // paddingY (default: 1)\n  (s) => bgGray(s)  // optional background function\n);\ntext.setText(\"Updated\");\n```\n\n### Коробка\n\nКонтейнер с отступом и цветом фона.\n\n```typescript\nconst box = new Box(\n  1,                // paddingX\n  1,                // paddingY\n  (s) => bgGray(s)  // background function\n);\nbox.addChild(new Text(\"Content\", 0, 0));\nbox.setBgFn((s) => bgBlue(s));\n```\n\n### Контейнер\n\nГруппирует дочерние компоненты по вертикали.\n\n```typescript\nconst container = new Container();\ncontainer.addChild(component1);\ncontainer.addChild(component2);\ncontainer.removeChild(component1);\n```\n\n### Проставка\n\nПустое вертикальное пространство.\n\n```typescript\nconst spacer = new Spacer(2);  // 2 empty lines\n```\n\n### Markdown\n\nОтображает уценку с подсветкой синтаксиса.\n\n```typescript\nconst md = new Markdown(\n  \"# Title\\n\\nSome **bold** text\",\n  1,        // paddingX\n  1,        // paddingY\n  theme     // MarkdownTheme (see below)\n);\nmd.setText(\"Updated markdown\");\n```\n\n### Изображение\n\nОтрисовывает изображения в поддерживаемых терминалах (Kitty, iTerm2, Ghostty, WezTerm, Warp).\n\n```typescript\nconst image = new Image(\n  base64Data,   // base64-encoded image\n  \"image/png\",  // MIME type\n  theme,        // ImageTheme\n  { maxWidthCells: 80, maxHeightCells: 24 }\n);\n```\n\n## Ввод с клавиатуры\n\nИспользуйте `matchesKey()` для обнаружения ключей:\n\n```typescript\nimport { matchesKey, Key } from \"@earendil-works/pi-tui\";\n\nhandleInput(data: string) {\n  if (matchesKey(data, Key.up)) {\n    this.selectedIndex--;\n  } else if (matchesKey(data, Key.enter)) {\n    this.onSelect?.(this.selectedIndex);\n  } else if (matchesKey(data, Key.escape)) {\n    this.onCancel?.();\n  } else if (matchesKey(data, Key.ctrl(\"c\"))) {\n    // Ctrl+C\n  }\n}\n```\n\n**Идентификаторы ключей** (используйте `Key.*` для автозаполнения или строковые литералы):\n- Основные клавиши: `Key.enter`, `Key.escape`, `Key.tab`, `Key.space`, `Key.backspace`, `Key.delete`, `Key.home`, `Key.end`\n- Клавиши со стрелками: `Key.up`, `Key.down`, `Key.left`, `Key.right`\n- С модификаторами: `Key.ctrl(\"c\")`, `Key.shift(\"tab\")`, `Key.alt(\"left\")`, `Key.ctrlShift(\"p\")`\n- Строковый формат также работает: `\"enter\"`, `\"ctrl+c\"`, `\"shift+tab\"`, `\"ctrl+shift+p\"`.\n\n## Ширина линии\n\n**Критический:** Каждая строка от `render()` не должна превышать параметр `width`.\n\n```typescript\nimport { visibleWidth, truncateToWidth } from \"@earendil-works/pi-tui\";\n\nrender(width: number): string[] {\n  // Truncate long lines\n  return [truncateToWidth(this.text, width)];\n}\n```\n\nУтилиты:\n- `visibleWidth(str)` — Получить ширину дисплея (игнорирует коды ANSI)\n- `truncateToWidth(str, width, ellipsis?)` – усечение с необязательным многоточием.\n- `wrapTextWithAnsi(str, width)` — перенос слов с сохранением кодов ANSI.\n\n## Создание пользовательских компонентов\n\nПример: интерактивный селектор\n\n```typescript\nimport {\n  matchesKey, Key,\n  truncateToWidth, visibleWidth\n} from \"@earendil-works/pi-tui\";\n\nclass MySelector {\n  private items: string[];\n  private selected = 0;\n  private cachedWidth?: number;\n  private cachedLines?: string[];\n  \n  public onSelect?: (item: string) => void;\n  public onCancel?: () => void;\n\n  constructor(items: string[]) {\n    this.items = items;\n  }\n\n  handleInput(data: string): void {\n    if (matchesKey(data, Key.up) && this.selected > 0) {\n      this.selected--;\n      this.invalidate();\n    } else if (matchesKey(data, Key.down) && this.selected < this.items.length - 1) {\n      this.selected++;\n      this.invalidate();\n    } else if (matchesKey(data, Key.enter)) {\n      this.onSelect?.(this.items[this.selected]);\n    } else if (matchesKey(data, Key.escape)) {\n      this.onCancel?.();\n    }\n  }\n\n  render(width: number): string[] {\n    if (this.cachedLines && this.cachedWidth === width) {\n      return this.cachedLines;\n    }\n\n    this.cachedLines = this.items.map((item, i) => {\n      const prefix = i === this.selected ? \"> \" : \"  \";\n      return truncateToWidth(prefix + item, width);\n    });\n    this.cachedWidth = width;\n    return this.cachedLines;\n  }\n\n  invalidate(): void {\n    this.cachedWidth = undefined;\n    this.cachedLines = undefined;\n  }\n}\n```\n\nИспользование в расширении:\n\n```typescript\npi.registerCommand(\"pick\", {\n  description: \"Pick an item\",\n  handler: async (_args, ctx) => {\n    const items = [\"Option A\", \"Option B\", \"Option C\"];\n    const selected = await ctx.ui.custom<string | null>((tui, _theme, _keybindings, done) => {\n      const selector = new MySelector(items);\n      selector.onSelect = done;\n      selector.onCancel = () => done(null);\n\n      return {\n        render: (width) => selector.render(width),\n        handleInput: (data) => {\n          selector.handleInput(data);\n          tui.requestRender();\n        },\n        invalidate: () => selector.invalidate(),\n      };\n    });\n\n    if (selected !== null) {\n      ctx.ui.notify(`Selected: ${selected}`, \"info\");\n    }\n  }\n});\n```\n\n## Тематика\n\nКомпоненты принимают объекты темы для стилизации.\n\n**В `renderCall`/`renderResult`** используйте параметр `theme`:\n\n```typescript\nrenderResult(result, options, theme, context) {\n  // Use theme.fg() for foreground colors\n  return new Text(theme.fg(\"success\", \"Done!\"), 0, 0);\n  \n  // Use theme.bg() for background colors\n  const styled = theme.bg(\"toolPendingBg\", theme.fg(\"accent\", \"text\"));\n}\n```\n\n**Цвета переднего плана** (`theme.fg(color, text)`):\n\n| Категория | Цвета |\n|----------|--------|\n| Общий | `text`, `accent`, `muted`, `dim` |\n| Статус | `success`, `error`, `warning` |\n| Границы | `border`, `borderAccent`, `borderMuted` |\n| Сообщения | `userMessageText`, `customMessageText`, `customMessageLabel` |\n| Инструменты | `toolTitle`, `toolOutput` |\n| Различия | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |\n| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |\n| Синтаксис | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |\n| мышление | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `thinkingMax` |\n| Режимы | `bashMode` |\n\n**Цвета фона** (`theme.bg(color, text)`):\n\n`selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`\n\n**Для Markdown** используйте `getMarkdownTheme()`:\n\n```typescript\nimport { getMarkdownTheme } from \"@earendil-works/pi-coding-agent\";\nimport { Markdown } from \"@earendil-works/pi-tui\";\n\nrenderResult(result, options, theme, context) {\n  const mdTheme = getMarkdownTheme();\n  return new Markdown(result.details.markdown, 0, 0, mdTheme);\n}\n```\n\n**Для пользовательских компонентов** определите собственный интерфейс темы:\n\n```typescript\ninterface MyTheme {\n  selected: (s: string) => string;\n  normal: (s: string) => string;\n}\n```\n\n## Ведение журнала отладки\n\nУстановите `PI_TUI_WRITE_LOG`, чтобы захватить необработанный поток ANSI, записанный в stdout.\n\n```bash\nPI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx packages/tui/test/chat-simple.ts\n```\n\n## Производительность\n\nКэшируйте отображаемый вывод, если это возможно:\n\n```typescript\nclass CachedComponent {\n  private cachedWidth?: number;\n  private cachedLines?: string[];\n\n  render(width: number): string[] {\n    if (this.cachedLines && this.cachedWidth === width) {\n      return this.cachedLines;\n    }\n    // ... compute lines ...\n    this.cachedWidth = width;\n    this.cachedLines = lines;\n    return lines;\n  }\n\n  invalidate(): void {\n    this.cachedWidth = undefined;\n    this.cachedLines = undefined;\n  }\n}\n```\n\nВызовите `invalidate()` при изменении состояния, затем используйте введенный `tui.requestRender()`, чтобы запустить повторный рендеринг.\n\n## Аннулирование и изменение темы\n\nКогда тема меняется, TUI вызывает `invalidate()` для всех компонентов, чтобы очистить их кеши. Компоненты должны правильно реализовать `invalidate()`, чтобы изменения темы вступили в силу.\n\n### Проблема\n\nЕсли компонент предварительно преобразует цвета темы в строки (через `theme.fg()`, `theme.bg()` и т. д.) и кэширует их, кэшированные строки содержат escape-коды ANSI из старой темы. Простой очистки кэша рендеринга недостаточно, если компонент хранит тематический контент отдельно.\n\n**Неправильный подход** (цвета темы не обновляются):\n\n```typescript\nclass BadComponent extends Container {\n  private content: Text;\n\n  constructor(message: string, theme: Theme) {\n    super();\n    // Pre-baked theme colors stored in Text component\n    this.content = new Text(theme.fg(\"accent\", message), 1, 0);\n    this.addChild(this.content);\n  }\n  // No invalidate override - parent's invalidate only clears\n  // child render caches, not the pre-baked content\n}\n```\n\n### Решение\n\nКомпоненты, которые создают контент с использованием цветов темы, должны перестроить этот контент при вызове `invalidate()`:\n\n```typescript\nclass GoodComponent extends Container {\n  private message: string;\n  private content: Text;\n\n  constructor(message: string) {\n    super();\n    this.message = message;\n    this.content = new Text(\"\", 1, 0);\n    this.addChild(this.content);\n    this.updateDisplay();\n  }\n\n  private updateDisplay(): void {\n    // Rebuild content with current theme\n    this.content.setText(theme.fg(\"accent\", this.message));\n  }\n\n  override invalidate(): void {\n    super.invalidate();  // Clear child caches\n    this.updateDisplay(); // Rebuild with new theme\n  }\n}\n```\n\n### Шаблон: восстановление при признании недействительным\n\nДля компонентов со сложным содержимым:\n\n```typescript\nclass ComplexComponent extends Container {\n  private data: SomeData;\n\n  constructor(data: SomeData) {\n    super();\n    this.data = data;\n    this.rebuild();\n  }\n\n  private rebuild(): void {\n    this.clear();  // Remove all children\n\n    // Build UI with current theme\n    this.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Title\")), 1, 0));\n    this.addChild(new Spacer(1));\n\n    for (const item of this.data.items) {\n      const color = item.active ? \"success\" : \"muted\";\n      this.addChild(new Text(theme.fg(color, item.label), 1, 0));\n    }\n  }\n\n  override invalidate(): void {\n    super.invalidate();\n    this.rebuild();\n  }\n}\n```\n\n### Когда это важно\n\nЭтот шаблон необходим, когда:\n\n1. **Цвета темы предварительной обработки** – использование `theme.fg()` или `theme.bg()` для создания стилизованных строк, хранящихся в дочерних компонентах.\n2. **Подсветка синтаксиса** – использование `highlightCode()`, которое применяет цвета синтаксиса на основе темы.\n3. **Сложные макеты** – создание деревьев дочерних компонентов со встроенными цветами темы.\n\nЭтот шаблон НЕ нужен, если:\n\n1. **Использование обратных вызовов темы** – передача функций типа `(text) => theme.fg(\"accent\", text)`, которые вызываются во время рендеринга.\n2. **Простые контейнеры**. Просто группируйте другие компоненты без добавления тематического контента.\n3. **Рендеринг без сохранения состояния** – вычисление новых тематических результатов при каждом вызове `render()` (без кэширования).\n\n## Общие шаблоны\n\nЭти шаблоны охватывают наиболее распространенные потребности пользовательского интерфейса в расширениях. **Скопируйте эти шаблоны вместо того, чтобы создавать их с нуля.**\n\n### Шаблон 1: Диалоговое окно выбора (SelectList)\n\nЧтобы позволить пользователям выбирать из списка опций. Используйте `SelectList` от `@earendil-works/pi-tui` с `DynamicBorder` для кадрирования.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { DynamicBorder } from \"@earendil-works/pi-coding-agent\";\nimport { Container, type SelectItem, SelectList, Text } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"pick\", {\n  handler: async (_args, ctx) => {\n    const items: SelectItem[] = [\n      { value: \"opt1\", label: \"Option 1\", description: \"First option\" },\n      { value: \"opt2\", label: \"Option 2\", description: \"Second option\" },\n      { value: \"opt3\", label: \"Option 3\" },  // description is optional\n    ];\n\n    const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {\n      const container = new Container();\n\n      // Top border\n      container.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n      // Title\n      container.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Pick an Option\")), 1, 0));\n\n      // SelectList with theme\n      const selectList = new SelectList(items, Math.min(items.length, 10), {\n        selectedPrefix: (t) => theme.fg(\"accent\", t),\n        selectedText: (t) => theme.fg(\"accent\", t),\n        description: (t) => theme.fg(\"muted\", t),\n        scrollInfo: (t) => theme.fg(\"dim\", t),\n        noMatch: (t) => theme.fg(\"warning\", t),\n      });\n      selectList.onSelect = (item) => done(item.value);\n      selectList.onCancel = () => done(null);\n      container.addChild(selectList);\n\n      // Help text\n      container.addChild(new Text(theme.fg(\"dim\", \"↑↓ navigate • enter select • esc cancel\"), 1, 0));\n\n      // Bottom border\n      container.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n      return {\n        render: (w) => container.render(w),\n        invalidate: () => container.invalidate(),\n        handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); },\n      };\n    });\n\n    if (result) {\n      ctx.ui.notify(`Selected: ${result}`, \"info\");\n    }\n  },\n});\n```\n\n**Примеры:** [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)\n\n### Шаблон 2. Асинхронная операция с отменой (BorderedLoader)\n\nДля операций, которые требуют времени и должны быть отменены. `BorderedLoader` показывает счетчик и обрабатывает escape для отмены.\n\n```typescript\nimport { BorderedLoader } from \"@earendil-works/pi-coding-agent\";\n\npi.registerCommand(\"fetch\", {\n  handler: async (_args, ctx) => {\n    const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {\n      const loader = new BorderedLoader(tui, theme, \"Fetching data...\");\n      loader.onAbort = () => done(null);\n\n      // Do async work\n      fetchData(loader.signal)\n        .then((data) => done(data))\n        .catch(() => done(null));\n\n      return loader;\n    });\n\n    if (result === null) {\n      ctx.ui.notify(\"Cancelled\", \"info\");\n    } else {\n      ctx.ui.setEditorText(result);\n    }\n  },\n});\n```\n\n**Примеры:** [qna.ts](../examples/extensions/qna.ts), [handoff.ts](../examples/extensions/handoff.ts)\n\n### Шаблон 3: Настройки/переключатели (SettingsList)\n\nДля переключения нескольких настроек. Используйте `SettingsList` от `@earendil-works/pi-tui` с `getSettingsListTheme()`.\n\n```typescript\nimport { getSettingsListTheme } from \"@earendil-works/pi-coding-agent\";\nimport { Container, type SettingItem, SettingsList, Text } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"settings\", {\n  handler: async (_args, ctx) => {\n    const items: SettingItem[] = [\n      { id: \"verbose\", label: \"Verbose mode\", currentValue: \"off\", values: [\"on\", \"off\"] },\n      { id: \"color\", label: \"Color output\", currentValue: \"on\", values: [\"on\", \"off\"] },\n    ];\n\n    await ctx.ui.custom((_tui, theme, _kb, done) => {\n      const container = new Container();\n      container.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Settings\")), 1, 1));\n\n      const settingsList = new SettingsList(\n        items,\n        Math.min(items.length + 2, 15),\n        getSettingsListTheme(),\n        (id, newValue) => {\n          // Handle value change\n          ctx.ui.notify(`${id} = ${newValue}`, \"info\");\n        },\n        () => done(undefined),  // On close\n        { enableSearch: true }, // Optional: enable fuzzy search by label\n      );\n      container.addChild(settingsList);\n\n      return {\n        render: (w) => container.render(w),\n        invalidate: () => container.invalidate(),\n        handleInput: (data) => settingsList.handleInput?.(data),\n      };\n    });\n  },\n});\n```\n\n**Примеры:** [tools.ts](../examples/extensions/tools.ts)\n\n### Схема 4: постоянный индикатор состояния\n\nПоказывать статус в нижнем колонтитуле, который сохраняется при рендеринге. Хорошо подходит для индикаторов режима.\n\n```typescript\n// Set status (shown in footer)\nctx.ui.setStatus(\"my-ext\", ctx.ui.theme.fg(\"accent\", \"● active\"));\n\n// Clear status\nctx.ui.setStatus(\"my-ext\", undefined);\n```\n\n**Примеры:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts)\n\n### Схема 4б: Настройка рабочего индикатора\n\nНастройте встроенный рабочий индикатор, отображаемый во время потоковой передачи ответа pi.\n\n```typescript\n// Static indicator\nctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg(\"accent\", \"●\")] });\n\n// Custom animated indicator\nctx.ui.setWorkingIndicator({\n  frames: [\n    ctx.ui.theme.fg(\"dim\", \"·\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n    ctx.ui.theme.fg(\"accent\", \"●\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n  ],\n  intervalMs: 120,\n});\n\n// Hide the indicator entirely\nctx.ui.setWorkingIndicator({ frames: [] });\n\n// Restore pi's default spinner\nctx.ui.setWorkingIndicator();\n```\n\nЭто влияет только на индикатор нормальной работы потоковой передачи. Загрузчики сжатия и повтора сохраняют свой встроенный стиль. Пользовательские фреймы отображаются дословно, поэтому расширения должны добавлять свои собственные цвета при необходимости.\n\n**Примеры:** [working-indicator.ts](../examples/extensions/working-indicator.ts)\n\n### Схема 5: виджеты выше/ниже редактора\n\nПоказывать постоянный контент над или под редактором ввода. Хорошо подходит для списков дел и прогресса.\n\n```typescript\n// Simple string array (above editor by default)\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"]);\n\n// Render below the editor\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"], { placement: \"belowEditor\" });\n\n// Or with theme\nctx.ui.setWidget(\"my-widget\", (_tui, theme) => {\n  const lines = items.map((item, i) =>\n    item.done\n      ? theme.fg(\"success\", \"✓ \") + theme.fg(\"muted\", item.text)\n      : theme.fg(\"dim\", \"○ \") + item.text\n  );\n  return {\n    render: () => lines,\n    invalidate: () => {},\n  };\n});\n\n// Clear\nctx.ui.setWidget(\"my-widget\", undefined);\n```\n\n**Примеры:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)\n\n### Шаблон 6: Пользовательский нижний колонтитул\n\nЗамените нижний колонтитул. `footerData` предоставляет данные, которые иначе не доступны расширениям.\n\n```typescript\nctx.ui.setFooter((tui, theme, footerData) => ({\n  invalidate() {},\n  render(width: number): string[] {\n    // footerData.getGitBranch(): string | null\n    // footerData.getExtensionStatuses(): ReadonlyMap<string, string>\n    return [`${ctx.model?.id} (${footerData.getGitBranch() || \"no git\"})`];\n  },\n  dispose: footerData.onBranchChange(() => tui.requestRender()), // reactive\n}));\n\nctx.ui.setFooter(undefined); // restore default\n```\n\nСтатистика токенов доступна через `ctx.sessionManager.getBranch()` и `ctx.model`.\n\n**Примеры:** [custom-footer.ts](../examples/extensions/custom-footer.ts)\n\n### Шаблон 7: Пользовательский редактор (режим vim и т. д.)\n\nЗамените основной редактор ввода собственной реализацией. Полезно для модального редактирования (vim), различных сочетаний клавиш (emacs) или специальной обработки ввода.\n\n```typescript\nimport { CustomEditor, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { matchesKey, truncateToWidth } from \"@earendil-works/pi-tui\";\n\ntype Mode = \"normal\" | \"insert\";\n\nclass VimEditor extends CustomEditor {\n  private mode: Mode = \"insert\";\n\n  handleInput(data: string): void {\n    // Escape: switch to normal mode, or pass through for app handling\n    if (matchesKey(data, \"escape\")) {\n      if (this.mode === \"insert\") {\n        this.mode = \"normal\";\n        return;\n      }\n      // In normal mode, escape aborts agent (handled by CustomEditor)\n      super.handleInput(data);\n      return;\n    }\n\n    // Insert mode: pass everything to CustomEditor\n    if (this.mode === \"insert\") {\n      super.handleInput(data);\n      return;\n    }\n\n    // Normal mode: vim-style navigation\n    switch (data) {\n      case \"i\": this.mode = \"insert\"; return;\n      case \"h\": super.handleInput(\"\\x1b[D\"); return; // Left\n      case \"j\": super.handleInput(\"\\x1b[B\"); return; // Down\n      case \"k\": super.handleInput(\"\\x1b[A\"); return; // Up\n      case \"l\": super.handleInput(\"\\x1b[C\"); return; // Right\n    }\n    // Pass unhandled keys to super (ctrl+c, etc.), but filter printable chars\n    if (data.length === 1 && data.charCodeAt(0) >= 32) return;\n    super.handleInput(data);\n  }\n\n  render(width: number): string[] {\n    const lines = super.render(width);\n    // Add mode indicator to bottom border (use truncateToWidth for ANSI-safe truncation)\n    if (lines.length > 0) {\n      const label = this.mode === \"normal\" ? \" NORMAL \" : \" INSERT \";\n      const lastLine = lines[lines.length - 1]!;\n      // Pass \"\" as ellipsis to avoid adding \"...\" when truncating\n      lines[lines.length - 1] = truncateToWidth(lastLine, width - label.length, \"\") + label;\n    }\n    return lines;\n  }\n}\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    // Factory receives the TUI, theme, and keybindings from the app\n    ctx.ui.setEditorComponent((tui, theme, keybindings) =>\n      new VimEditor(tui, theme, keybindings)\n    );\n  });\n}\n```\n\n**Ключевые моменты:**\n\n- **Расширьте `CustomEditor`** (не базовый `Editor`), чтобы получить привязки клавиш приложения (Escape для прерывания, ctrl+d для выхода, переключение модели и т. д.).\n- **Позвоните по номеру `super.handleInput(data)`**, чтобы узнать ключи, которыми вы не пользуетесь.\n- **Фабричный шаблон**: `setEditorComponent` получает фабричную функцию, которая получает `tui`, `theme` и `keybindings`.\n- **Введите `undefined`**, чтобы восстановить редактор по умолчанию: `ctx.ui.setEditorComponent(undefined)`\n\n**Примеры:** [modal-editor.ts](../examples/extensions/modal-editor.ts)\n\n## Ключевые правила\n\n1. **Всегда используйте тему из обратного вызова**. Не импортируйте тему напрямую. Используйте `theme` из обратного вызова `ctx.ui.custom((tui, theme, keybindings, done) =>...)`.\n\n2. **Всегда вводите параметр цвета DynamicBorder** — напишите `(s: string) => theme.fg(\"accent\", s)`, а не `(s) => theme.fg(\"accent\", s)`.\n\n3. **Вызов tui.requestRender() после изменения состояния** — В `handleInput` вызовите `tui.requestRender()` после обновления состояния.\n\n4. **Вернуть объект с тремя методами** — Пользовательским компонентам требуется `{ render, invalidate, handleInput }`.\n\n5. **Использовать существующие компоненты** — `SelectList`, `SettingsList`, `BorderedLoader` охватывают 90 % случаев. Не восстанавливайте их.\n\n## Примеры\n\n- **Интерфейс выбора**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) — SelectList с рамкой DynamicBorder\n- **Асинхронность с отменой**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) — BorderedLoader для вызовов LLM.\n- **Переключение настроек**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) — список настроек для включения/отключения инструмента.\n- **Индикаторы состояния**: [examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) — setStatus и setWidget.\n- **Индикатор работы**: [examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setWorkingIndicator\n- **Пользовательский нижний колонтитул**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) — setFooter со статистикой\n- **Пользовательский редактор**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) — модальное редактирование в стиле Vim.\n- **Игра «Змея»**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) — Полная версия игры с вводом с клавиатуры и игровым циклом.\n- **Рендеринг пользовательского инструмента**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) — renderCall и renderResult","sourceFile":"tui.md"},"usage":{"title":"Использование Pi","markdown":"На этой странице собраны сведения о повседневном использовании, которые не умещаются на странице быстрого запуска.\n\n## Интерактивный режим\n\n<p align=\"center\"><img src=\"images/interactive-mode.png\" alt=\"Interactive Mode\" width=\"600\"></p>\n\nИнтерфейс имеет четыре основные области:\n\n- **Заголовок запуска** – ярлыки, загруженные context files, prompt templates, навыки и расширения.\n- **Сообщения** – сообщения пользователя, ответы помощника, вызовы инструментов, результаты работы инструментов, уведомления, ошибки и пользовательский интерфейс расширений.\n- **Редактор** — место ввода текста; цвет границы указывает на текущий уровень мышления\n- **Нижний колонтитул** — рабочий каталог, имя сеанса, использование токена/кэша, стоимость, использование контекста и текущая модель. Итоговые данные включают ответы помощника, данные об использовании, сообщаемые инструментами, и генерацию сводных данных.\n\nРедактор можно временно заменить встроенным пользовательским интерфейсом, например `/settings`, или пользовательским пользовательским интерфейсом расширения.\n\n### Возможности редактора\n\n| Особенность | Как |\n|---------|-----|\n| Ссылка на файл | Введите `@` для нечеткого поиска файлов проекта. |\n| Завершение пути | Нажмите Tab, чтобы завершить пути. |\n| Многострочный ввод | Shift+Enter или Ctrl+Enter в терминале Windows |\n| Копировать ответ | Ctrl+X копирует последнее сообщение помощника; в `/tree` копируется выбранное сообщение |\n| Изображения | Вставьте с помощью Ctrl+V, Alt+V в Windows или перетащите в терминал. |\n| Команда оболочки | `!command` запускается и отправляет выходные данные в модель |\n| Скрытая команда оболочки | `!!command` выполняется без отправки вывода в модель |\n| Внешний редактор | Ctrl+G открывает `externalEditor`, `$VISUAL`, `$EDITOR`, Блокнот в Windows или `nano` в другом месте. |\n\nСм. [Keybindings](keybindings.md) для просмотра всех ярлыков и настроек.\n\n## Слэш-команды\n\nВведите `/` в редакторе, чтобы открыть завершение команды. Extensions позволяет регистрировать собственные команды, навыки доступны как `/skill:name`, а prompt templates расширяется с помощью `/templatename`.\n\n| Команда | Описание |\n|---------|-------------|\n| `/login`, `/logout` | Управление учетными данными ключа OAuth или API |\n| [`/llama`](llama-cpp.md) | Загрузка, загрузка и выгрузка моделей маршрутизаторов llama.cpp |\n| `/model` | Переключение моделей |\n| `/scoped-models` | Включить/отключить модели для циклического переключения Ctrl+P |\n| `/settings` | Уровень мышления, тема, доставка сообщения, транспорт |\n| `/resume` | Piск с предыдущих сессий |\n| `/new` | Начать новый сеанс |\n| `/name <name>` | Установить отображаемое имя сеанса |\n| `/session` | Показать файл сеанса, идентификатор, сообщения, токены и стоимость. |\n| `/tree` | Перейти к любой точке сеанса и продолжить оттуда. |\n| `/trust` | Сохранить решение о доверии проекта для будущих сеансов. |\n| `/fork` | Создать новый сеанс на основе предыдущего сообщения пользователя. |\n| `/clone` | Дублируйте текущую активную ветку в новый сеанс. |\n| `/compact [prompt]` | Сжатие контекста вручную, опционально с пользовательскими инструкциями |\n| `/copy` | Скопировать последнее сообщение помощника в буфер обмена |\n| `/export [file]` | Экспортировать сеанс в HTML или JSONL |\n| `/import <file>` | Импортируйте и возобновите сеанс из файла JSONL. |\n| `/share` | Загрузить как частную суть GitHub с общей HTML-ссылкой. |\n| `/reload` | Перезагрузите сочетания клавиш, расширения, навыки, подсказки, темы и context files. |\n| `/hotkeys` | Показать все сочетания клавиш |\n| `/changelog` | Отображать историю версий |\n| `/quit` | Выйти из пи |\n\n## Очередь сообщений\n\nВы можете отправлять сообщения, пока агент еще работает:\n\n- **Ввод** ставит в очередь сообщение управления, доставляемое после того, как текущий ход помощника завершает выполнение вызовов инструментов.\n- **Alt+Enter** ставит в очередь последующее сообщение, доставляемое после того, как агент завершит всю работу.\n- **Escape** прерывает работу и восстанавливает сообщения в очереди в редактор.\n- **Alt+Up** возвращает сообщения из очереди в редактор.\n\nВ терминале Windows сочетание клавиш Alt+Enter по умолчанию работает в полноэкранном режиме. Переназначьте его, как описано в [Terminal setup](terminal-setup.md), если вы хотите, чтобы pi получил ярлык.\n\nНастройте доставку в [Settings](settings.md) с помощью `steeringMode` и `followUpMode`.\n\n## Сессии\n\nСеансы автоматически сохраняются в `~/.pi/agent/sessions/`, организованные по рабочему каталогу.\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse and select a session\npi --no-session        # Ephemeral mode; do not save\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Use a specific session file or session ID\npi --fork <path|id>    # Fork a session into a new session file\n```\n\nПолезные команды сеанса:\n\n- `/session` показывает текущий файл и идентификатор сеанса.\n- `/tree` перемещается по внутреннему файлу session tree и может суммировать заброшенные ветки.\n- `/fork` создает новый сеанс на основе предыдущего сообщения пользователя.\n- `/clone` дублирует текущую активную ветвь в новый файл сеанса.\n- `/compact` объединяет старые сообщения в свободный контекст.\n\nПодробности см. [Sessions](sessions.md) и [Compaction](compaction.md).\n\n## Контекстные файлы\n\nPi загружает `AGENTS.md` или `CLAUDE.md` при запуске из:\n\n- `~/.pi/agent/AGENTS.md` для глобальных инструкций\n- родительские каталоги, переход из текущего рабочего каталога\n- текущий каталог\n\nЕсли каталог содержит `AGENTS.override.md`, Pi загружает его вместо `AGENTS.md` или `CLAUDE.md` из этого каталога. Контекстные файлы из других каталогов по-прежнему располагаются нормально.\n\nИспользуйте context files для обозначения соглашений проекта, команд, правил безопасности и предпочтений. Отключите загрузку с помощью `--no-context-files` или `-nc`.\n\n### Файлы системных подсказок\n\nЗамените системное приглашение по умолчанию на:\n\n- `.pi/SYSTEM.md` для проекта\n- `~/.pi/agent/SYSTEM.md` во всем мире\n\nДобавьте к приглашению по умолчанию, не заменяя его на `APPEND_SYSTEM.md` в любом месте.\n\n### Проект Траст\n\nПри интерактивном запуске pi спрашивает, прежде чем доверять папке проекта, которая содержит локальные настройки проекта, ресурсы или проект `.agents/skills` и не имеет сохраненного решения для этой папки или родительской папки в `~/.pi/agent/trust.json`. Доверие к проекту позволяет pi загружать ресурсы `.pi/settings.json` и `.pi`, устанавливать недостающие пакеты проекта и выполнять расширения проекта.\n\nПеред принятием решения о доверии pi загружает только context files, пользовательские/глобальные расширения и CLI `-e` расширения, чтобы они могли обработать событие `project_trust`. Локальные расширения проекта, расширения, управляемые пакетом проекта, и параметры проекта загружаются только после того, как проект становится доверенным. Это разделение также применяется при переключении на сеанс от другого cwd, доверие которого не было разрешено в текущем процессе.\n\nВ неинтерактивных режимах (`-p`, `--mode json` и `--mode rpc`) запрос доверия не отображается. Без применимого сохраненного решения о доверии они используют `defaultProjectTrust` из глобальных настроек: `ask` (по умолчанию) и `never` игнорируют эти ресурсы проекта, а `always` доверяют им. Нажмите `--approve`/`-a` или `--no-approve`/`-na`, чтобы отменить доверие проекта на один запуск.\n\nЕсли никакое расширение или сохраненное решение не применимо, `defaultProjectTrust` управляет резервным поведением. Установите его на `\"ask\"`, `\"always\"` или `\"never\"` в `~/.pi/agent/settings.json` или измените его с помощью `/settings`.\n\n`pi config` и команды пакета используют один и тот же поток доверия проекта, за исключением того, что `pi update` никогда не запрашивает. Нажмите `--approve`, чтобы доверять локальным настройкам проекта для одной команды, или `--no-approve`, чтобы игнорировать их.\n\nИспользуйте `/trust` в интерактивном режиме, чтобы сохранить решение о доверии проекта для будущих сеансов, включая доверие к непосредственной родительской папке. Пишется только `~/.pi/agent/trust.json`; текущий сеанс не перезагружается, поэтому перезапустите pi, чтобы изменения вступили в силу.\n\n\n## Экспорт и обмен сеансами\n\nИспользуйте `/export [file]`, чтобы записать сеанс в HTML.\n\nИспользуйте `/share`, чтобы загрузить личную суть GitHub с общей HTML-ссылкой.\n\nЕсли вы используете pi для работы с открытым исходным кодом и хотите публиковать сеансы исследований моделей, подсказок, инструментов и оценок, см. [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf). Он публикует сеансы в наборах данных Hugging Face.\n\n## CLI Ссылка\n\n```bash\npi [options] [@files...] [messages...]\n```\n\n### Команды пакета\n\n```bash\npi install <source> [-l]     # Install package, -l for project-local\npi remove <source> [-l]      # Remove package\npi uninstall <source> [-l]   # Alias for remove\npi update [source|self|pi]   # Update pi only, or one package source\npi update --all              # Update pi and packages; reconcile pinned git refs\npi update --extensions       # Update packages only; reconcile pinned git refs\npi update --models           # Refresh model catalogs only\npi update --self             # Update pi only\npi update --extension <src>  # Update one package\npi list                      # List installed packages\npi config                    # Enable/disable package resources\n```\n\nЭти команды управляют пакетами pi, а `pi update` могут обновлять установку pi CLI. Чтобы удалить сам pi, см. [Quickstart](quickstart.md#uninstall). `pi config` и команды пакета проекта принимают `--approve`/`--no-approve`, чтобы доверять или игнорировать локальные настройки проекта для одной команды. `pi update` никогда не требует доверия к проекту.\n\nСм. [Pi Packages](packages.md) для получения информации об источниках пакетов и примечаниях по безопасности.\n\n### Режимы\n\n| Флаг | Описание |\n|------|-------------|\n| по умолчанию | Интерактивный режим |\n| `-p`, `--print` | Распечатать ответ и выйти |\n| `--mode json` | Выведите все события в виде строк JSON; см. [JSON mode](json.md) |\n| `--mode rpc` | режим RPC вместо stdin/stdout; см. [RPC mode](rpc.md) |\n| `--export <in> [out]` | Экспорт сеанса в HTML |\n\nВ режиме печати pi также читает переданный по конвейеру stdin и объединяет его с начальным приглашением:\n\n```bash\ncat README.md | pi -p \"Summarize this text\"\n```\n\n### Варианты модели\n\n| Вариант | Описание |\n|--------|-------------|\n| `--provider <name>` | Поставщик, например `anthropic`, `openai` или `google`. |\n| `--model <pattern>` | Шаблон или идентификатор модели; поддерживает `provider/id` и необязательно `:<thinking>` |\n| `--api-key <key>` | API key, переопределение переменных среды |\n| `--thinking <level>` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` |\n| `--models <patterns>` | Шаблоны, разделенные запятыми, для циклического переключения Ctrl+P |\n| `--list-models [search]` | Список доступных моделей |\n\n### Параметры сеанса\n\n| Вариант | Описание |\n|--------|-------------|\n| `-c`, `--continue` | Продолжить последний сеанс |\n| `-r`, `--resume` | Просмотрите и выберите сеанс |\n| `--session <путь\\ | идентификатор>` | Используйте определенный файл сеанса или частичный UUID. |\n| `--fork <путь\\ | идентификатор>` | Форкнуть файл сеанса или частичный UUID в новый сеанс. |\n| `--session-dir <dir>` | Пользовательский каталог хранения сеансов |\n| `--no-session` | Эфемерный режим; не сохранять |\n| `--name <name>`, `-n <name>` | Установить отображаемое имя сеанса при запуске |\n\n### Параметры инструмента\n\n| Вариант | Описание |\n|--------|-------------|\n| `--tools <list>`, `-t <list>` | Список разрешенных встроенных, расширенных и пользовательских инструментов |\n| `--exclude-tools <list>`, `-xt <list>` | Отключите определенные встроенные, расширенные и пользовательские инструменты. |\n| `--no-builtin-tools`, `-nbt` | Отключите встроенные инструменты, но оставьте расширения/пользовательские инструменты включенными. |\n| `--no-tools`, `-nt` | Отключить все инструменты |\n\nВстроенные инструменты: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`.\n\n### Параметры ресурса\n\n| Вариант | Описание |\n|--------|-------------|\n| `-e`, `--extension <source>` | Загрузите расширение по пути, npm или git; повторяемый |\n| `--no-extensions` | Отключить обнаружение расширений |\n| `--skill <path>` | Загрузите навык; повторяемый |\n| `--no-skills` | Отключить обнаружение навыков |\n| `--prompt-template <path>` | Загрузите шаблон приглашения; повторяемый |\n| `--no-prompt-templates` | Отключить обнаружение шаблонов приглашений |\n| `--theme <path>` | Загрузите тему; повторяемый |\n| `--no-themes` | Отключить обнаружение тем |\n| `--no-context-files`, `-nc` | Отключить обнаружение `AGENTS.md` и `CLAUDE.md`. |\n\nКомбинируйте `--no-*` с явными флагами, чтобы загрузить именно то, что вам нужно, игнорируя настройки. Пример:\n\n```bash\npi --no-extensions -e ./my-extension.ts\n```\n\n### Другие варианты\n\n| Вариант | Описание |\n|--------|-------------|\n| `--system-prompt <text>` | Заменить приглашение по умолчанию; context files и навыки все еще добавляются |\n| `--append-system-prompt <text>` | Добавить в системную подсказку |\n| `--tui-mode <mode>` | Режим TUI: `regular` (по умолчанию) или экспериментальный `fullscreen` |\n| `--verbose` | Принудительный подробный запуск |\n| `-a`, `--approve` | Доверять локальным файлам проекта для этого запуска |\n| `-na`, `--no-approve` | Игнорировать локальные файлы проекта для этого запуска |\n| `-h`, `--help` | Показать справку |\n| `-v`, `--version` | Показать версию |\n\nВ режиме `fullscreen` расшифровка прокручивается внутри области просмотра терминала, в то время как сообщения в очереди, рабочее состояние, виджеты расширений, редактор и нижний колонтитул остаются зафиксированными внизу. Ввод с помощью мыши/трекпада прокручивает область под указателем; Действия в области просмотра клавиатуры всегда остаются доступными. Встроенные изображения работают в терминалах, поддерживающих графический протокол Kitty, включая Kitty и Ghostty. В iTerm2 они отображаются как текстовые заполнители, поскольку его протокол встроенных изображений не может удалять или обрезать места размещения во время прокрутки, принадлежащей приложению. В режиме `regular` pi использует главный экран и обратную прокрутку, принадлежащую терминалу, а встроенные изображения iTerm2 продолжают отображаться нормально.\n\nУстановите режим **TUI** в `/settings`, чтобы немедленно переключаться между `regular` и `fullscreen` и выбрать режим по умолчанию для будущих сеансов. **Вывод выхода из полноэкранного режима** определяет, будет ли выход из полноэкранного режима печатать окончательную расшифровку или восстанавливать предыдущий экран и печатать только подсказку о возобновлении сеанса.\n\n### Аргументы файла\n\nПрефикс файлов с `@`, чтобы включить их в сообщение:\n\n```bash\npi @prompt.md \"Answer this\"\npi -p @screenshot.png \"What's in this image?\"\npi @code.ts @test.ts \"Review these files\"\n```\n\n### Примеры\n\n```bash\n# Interactive with initial prompt\npi \"List all .ts files in src/\"\n\n# Non-interactive\npi -p \"Summarize this codebase\"\n\n# Non-interactive with piped stdin\ncat README.md | pi -p \"Summarize this text\"\n\n# Named one-shot session\npi --name \"release audit\" -p \"Audit this repository\"\n\n# Different model\npi --provider openai --model gpt-4o \"Help me refactor\"\n\n# Model with provider prefix\npi --model openai/gpt-4o \"Help me refactor\"\n\n# Model with thinking level shorthand\npi --model sonnet:high \"Solve this complex problem\"\n\n# Limit model cycling\npi --models \"claude-*,gpt-4o\"\n\n# Read-only mode\npi --tools read,grep,find,ls -p \"Review the code\"\n\n# Disable one extension or built-in tool while keeping the rest available\npi --exclude-tools ask_question\n```\n\n## Принципы проектирования\n\nPi сохраняет ядро ​​небольшим и помещает поведение, специфичное для рабочего процесса, в расширения, навыки, prompt templates и пакеты.\n\nОн намеренно не включает встроенные MCP, субагенты, всплывающие окна с разрешениями, режим планирования, задачи или фоновый bash. Вы можете создать или установить эти рабочие процессы в виде расширений или пакетов или использовать внешние инструменты, такие как контейнеры и tmux.\n\nДля полного обоснования прочитайте [blog post](https://mariozechner.at/posts/2025-11-30-pi-coding-agent/).","sourceFile":"usage.md"},"windows":{"title":"Настройка Windows","markdown":"Для Pi требуется оболочка bash в Windows. Проверенные места (по порядку):\n\n1. Пользовательский путь от `~/.pi/agent/settings.json`\n2. Git Баш (`C:\\Program Files\\Git\\bin\\bash.exe`)\n3. `bash.exe` в PATH (Cygwin, MSYS2, WSL)\n\nДля большинства пользователей достаточно [Git for Windows](https://git-scm.com/download/win).\n\n## Пользовательский путь к оболочке\n\n```json\n{\n  \"shellPath\": \"C:\\\\cygwin64\\\\bin\\\\bash.exe\"\n}\n```","sourceFile":"windows.md"}}},"navigation":{"ru":[{"title":"Начало","items":[{"title":"Pi Документация","path":"/docs/latest","slug":"index"},{"title":"Быстрый старт","path":"/docs/latest/quickstart","slug":"quickstart"},{"title":"Использование Pi","path":"/docs/latest/usage","slug":"usage"},{"title":"Providers","path":"/docs/latest/providers","slug":"providers"},{"title":"Безопасность","path":"/docs/latest/security","slug":"security"},{"title":"Контейнеризация","path":"/docs/latest/containerization","slug":"containerization"},{"title":"Настройки","path":"/docs/latest/settings","slug":"settings"},{"title":"Сочетания клавиш","path":"/docs/latest/keybindings","slug":"keybindings"},{"title":"Сессии","path":"/docs/latest/sessions","slug":"sessions"},{"title":"Сжатие и суммирование ветвей","path":"/docs/latest/compaction","slug":"compaction"}]},{"title":"Настройка","items":[{"title":"Extensions","path":"/docs/latest/extensions","slug":"extensions"},{"title":"Skills","path":"/docs/latest/skills","slug":"skills"},{"title":"Шаблоны подсказок","path":"/docs/latest/prompt-templates","slug":"prompt-templates"},{"title":"Темы","path":"/docs/latest/themes","slug":"themes"},{"title":"Pi Packages","path":"/docs/latest/packages","slug":"packages"},{"title":"Пользовательский Models","path":"/docs/latest/models","slug":"models"},{"title":"Пользовательский Providers","path":"/docs/latest/custom-provider","slug":"custom-provider"}]},{"title":"Справочник","items":[{"title":"Формат файла сеанса","path":"/docs/latest/session-format","slug":"session-format"}]},{"title":"Программное использование","items":[{"title":"SDK","path":"/docs/latest/sdk","slug":"sdk"},{"title":"RPC Режим","path":"/docs/latest/rpc","slug":"rpc"},{"title":"JSON Режим потока событий","path":"/docs/latest/json","slug":"json"},{"title":"TUI Компоненты","path":"/docs/latest/tui","slug":"tui"}]},{"title":"Настройка платформы","items":[{"title":"Настройка Windows","path":"/docs/latest/windows","slug":"windows"},{"title":"Termux (Android) Настройка","path":"/docs/latest/termux","slug":"termux"},{"title":"tmux Настройка","path":"/docs/latest/tmux","slug":"tmux"},{"title":"Настройка терминала","path":"/docs/latest/terminal-setup","slug":"terminal-setup"},{"title":"Псевдонимы оболочки","path":"/docs/latest/shell-aliases","slug":"shell-aliases"}]},{"title":"Разработка","items":[{"title":"Разработка","path":"/docs/latest/development","slug":"development"}]}]}}
