Настройка, расширение, параметры платформы и справочник API для Pi.

Extensions

pi может создавать расширения. Попросите его создать его для вашего варианта использования.

Extensions — это модули TypeScript, расширяющие поведение числа pi. Они могут подписываться на события жизненного цикла, регистрировать специальные инструменты, вызываемые LLM, добавлять команды и многое другое.

Размещение /reload: Поместите расширения в ~/.pi/agent/extensions/ (глобальный) или .pi/extensions/ (локальный для проекта) для автоматического обнаружения. Используйте pi -e./path.ts только для быстрых тестов. Extensions в автоматически обнаруженных локациях можно перезагрузить с помощью /reload.

Основные возможности:

  • Пользовательские инструменты – зарегистрируйте инструменты, которые LLM может вызывать через pi.registerTool().
  • Перехват событий – блокируйте или изменяйте вызовы инструментов, внедряйте контекст, настраивайте сжатие.
  • Взаимодействие с пользователем – подсказки пользователям с помощью ctx.ui (выберите, подтвердите, введите, уведомите).
  • Пользовательские компоненты пользовательского интерфейса — полные компоненты TUI с вводом с клавиатуры через ctx.ui.custom() для сложных взаимодействий.
  • Пользовательские команды — регистрируйте такие команды, как /mycommand через pi.registerCommand().
  • Постоянство сеанса – состояние хранилища, которое сохраняется при перезапуске через pi.appendEntry().
  • Пользовательский рендеринг. Управляйте тем, как вызовы инструментов/результаты и сообщения отображаются в TUI.

Примеры использования:

  • Разрешительные ворота (подтвердите до rm -rf, sudo и т. д.)
  • Git контрольная точка (тайник на каждом ходу, восстановление на ветке)
  • Защита пути (блокировка записи в .env, node_modules/)
  • Пользовательское сжатие (подведите итог разговора по-своему)
  • Сводки разговоров (см. пример summarize.ts)
  • Интерактивные инструменты (вопросы, мастера, настраиваемые диалоги)
  • Инструменты с отслеживанием состояния (списки дел, пулы соединений)
  • Внешние интеграции (наблюдатели файлов, веб-перехватчики, триггеры CI)
  • Игры, пока вы ждете (см. пример snake.ts)

См. examples/extensions/ для рабочих реализаций.

Оглавление

Быстрый старт

Создайте ~/.pi/agent/extensions/my-extension.ts:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

export default function (pi: ExtensionAPI) {
  // React to events
  pi.on("session_start", async (_event, ctx) => {
    ctx.ui.notify("Extension loaded!", "info");
  });

  pi.on("tool_call", async (event, ctx) => {
    if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
      const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
      if (!ok) return { block: true, reason: "Blocked by user" };
    }
  });

  // Register a custom tool
  pi.registerTool({
    name: "greet",
    label: "Greet",
    description: "Greet someone by name",
    parameters: Type.Object({
      name: Type.String({ description: "Name to greet" }),
    }),
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      return {
        content: [{ type: "text", text: `Hello, ${params.name}!` }],
        details: {},
      };
    },
  });

  // Register a command
  pi.registerCommand("hello", {
    description: "Say hello",
    handler: async (args, ctx) => {
      ctx.ui.notify(`Hello ${args || "world"}!`, "info");
    },
  });
}

Тест с флагом --extension (или -e):

pi -e ./my-extension.ts

Расположение расширений

Безопасность: Extensions запускается с полными системными разрешениями и может выполнять произвольный код. Устанавливайте только из источников, которым вы доверяете.

Extensions автоматически обнаруживаются в доверенных местах. Локальные записи .pi/extensions проекта загружаются только после того, как проекту доверяют.

Расположение Объем
~/.pi/agent/extensions/*.ts Глобальный (все проекты)
~/.pi/agent/extensions/*/index.ts Глобальный (подкаталог)
.pi/extensions/*.ts Проект-локальный
.pi/extensions/*/index.ts Локальный проект (подкаталог)

Дополнительные пути через settings.json:

{
  "packages": [
    "npm:@foo/bar@1.0.0",
    "git:github.com/user/repo@v1"
  ],
  "extensions": [
    "/path/to/local/extension.ts",
    "/path/to/local/extension/dir"
  ]
}

Чтобы поделиться расширениями через npm или git как пакеты pi, см. packages.md.

Доступный импорт

Упаковка Цель
@earendil-works/pi-coding-agent Типы расширений (ExtensionAPI, ExtensionContext, события)
typebox Определения схемы для параметров инструмента
@earendil-works/pi-ai Утилиты искусственного интеллекта (StringEnum для перечислений, совместимых с Google)
@earendil-works/pi-tui TUI компоненты для пользовательского рендеринга

npm зависимости тоже работают. Добавьте package.json рядом с вашим расширением (или в родительском каталоге), запустите npm install, и импорт из node_modules/ будет разрешен автоматически.

Для распределенных пакетов pi, установленных с помощью pi install (npm или git), параметры времени выполнения должны находиться в dependencies. При установке пакета по умолчанию используются производственные установки (npm install --omit=dev), поэтому devDependencies недоступны во время выполнения; когда настроен npmCommand, пакеты git используют простой install для совместимости с оболочками.

Также доступны встроенные модули Node.js (node:fs, node:path и т. д.).

Написание расширения

Расширение экспортирует заводскую функцию по умолчанию, которая получает ExtensionAPI. Фабрика может быть синхронной или асинхронной:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  // Subscribe to events
  pi.on("event_name", async (event, ctx) => {
    // ctx.ui for user interaction
    const ok = await ctx.ui.confirm("Title", "Are you sure?");
    ctx.ui.notify("Done!", "info");
    ctx.ui.setStatus("my-ext", "Processing...");  // Footer status
    ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]);  // Widget above editor (default)
  });

  // Register tools, commands, shortcuts, flags
  pi.registerTool({ ... });
  pi.registerCommand("name", { ... });
  pi.registerShortcut("ctrl+x", { ... });
  pi.registerFlag("my-flag", { ... });
}

Extensions загружаются через jiti, поэтому TypeScript работает без компиляции.

Если фабрика возвращает Promise, pi ожидает его, прежде чем продолжить запуск. Это означает, что асинхронная инициализация завершается до session_start, до resources_discover и до того, как будут сброшены регистрации поставщиков, поставленные в очередь через pi.registerProvider().

Асинхронные фабричные функции

Используйте асинхронную фабрику для однократного запуска, например для получения удаленной конфигурации или динамического обнаружения доступных моделей.

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default async function (pi: ExtensionAPI) {
  const response = await fetch("http://localhost:1234/v1/models");
  const payload = (await response.json()) as {
    data: Array<{
      id: string;
      name?: string;
      context_window?: number;
      max_tokens?: number;
    }>;
  };

  pi.registerProvider("local-openai", {
    baseUrl: "http://localhost:1234/v1",
    apiKey: "$LOCAL_OPENAI_API_KEY",
    api: "openai-completions",
    models: payload.data.map((model) => ({
      id: model.id,
      name: model.name ?? model.id,
      reasoning: false,
      input: ["text"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: model.context_window ?? 128000,
      maxTokens: model.max_tokens ?? 4096,
    })),
  });
}

Этот шаблон делает выбранные модели доступными при обычном запуске и до pi --list-models.

Долговечные ресурсы и отключение

Фабрики расширений могут запускаться в вызовах, которые никогда не запускают сеанс. Не запускайте фоновые ресурсы, такие как процессы, сокеты, средства наблюдения за файлами или таймеры, с завода.

Отложите запуск фонового ресурса до session_start или до команды/инструмента/события, которому нужен ресурс. Зарегистрируйте идемпотентный обработчик session_shutdown, чтобы закрыть любые запускаемые вами ресурсы в области сеанса.

Стили расширения

Один файл – самый простой вариант для небольших расширений:

~/.pi/agent/extensions/
└── my-extension.ts

Каталог с index.ts — для многофайловых расширений:

~/.pi/agent/extensions/
└── my-extension/
    ├── index.ts        # Entry point (exports default function)
    ├── tools.ts        # Helper module
    └── utils.ts        # Helper module

Пакет с зависимостями — для расширений, которым требуется npm пакетов:

~/.pi/agent/extensions/
└── my-extension/
    ├── package.json    # Declares dependencies and entry points
    ├── package-lock.json
    ├── node_modules/   # After npm install
    └── src/
        └── index.ts
// package.json
{
  "name": "my-extension",
  "dependencies": {
    "zod": "^3.0.0",
    "chalk": "^5.0.0"
  },
  "pi": {
    "extensions": ["./src/index.ts"]
  }
}

Запустите npm install в каталоге расширения, затем импорт из node_modules/ будет работать автоматически.

События

Обзор жизненного цикла

pi starts
  │
  ├─► project_trust (user/global and CLI extensions only, before project resources load)
  ├─► session_start { reason: "startup" }
  └─► resources_discover { reason: "startup" }
      │
      ▼
user sends prompt ─────────────────────────────────────────┐
  │                                                        │
  ├─► (extension commands checked first, bypass if found)  │
  ├─► input (can intercept, transform, or handle)          │
  ├─► (skill/template expansion if not handled)            │
  ├─► before_agent_start (can inject message, modify system prompt)
  ├─► agent_start                                          │
  ├─► message_start / message_update / message_end         │
  │                                                        │
  │   ┌─── turn (repeats while LLM calls tools) ───┐       │
  │   │                                            │       │
  │   ├─► turn_start                               │       │
  │   ├─► context (can modify messages)            │       │
  │   ├─► before_provider_headers (can mutate headers)     |
  │   ├─► before_provider_request (can inspect or replace payload)
  │   ├─► after_provider_response (status + headers, before stream consume)
  │   │                                            │       │
  │   │   LLM responds, may call tools:            │       │
  │   │     ├─► tool_execution_start               │       │
  │   │     ├─► tool_call (can block)              │       │
  │   │     ├─► tool_execution_update              │       │
  │   │     ├─► tool_result (can modify)           │       │
  │   │     └─► tool_execution_end                 │       │
  │   │                                            │       │
  │   └─► turn_end                                 │       │
  │                                                        │
  ├─► agent_end                                            │
  └─► agent_settled (no retry/compaction/follow-up left)   │
                                                           │
user sends another prompt ◄────────────────────────────────┘

/new (new session) or /resume (switch session)
  ├─► session_before_switch (can cancel)
  ├─► session_shutdown
  ├─► session_start { reason: "new" | "resume", previousSessionFile? }
  └─► resources_discover { reason: "startup" }

/fork or /clone
  ├─► session_before_fork (can cancel)
  ├─► session_shutdown
  ├─► session_start { reason: "fork", previousSessionFile }
  └─► resources_discover { reason: "startup" }

/name or pi.setSessionName()
  └─► session_info_changed

/compact or auto-compaction
  ├─► session_before_compact (can cancel or customize)
  └─► session_compact

/tree navigation
  ├─► session_before_tree (can cancel or customize)
  └─► session_tree

/model or Ctrl+P (model selection/cycling)
  ├─► thinking_level_select (if model change changes/clamps thinking level)
  └─► model_select

thinking level changes (settings, keybinding, pi.setThinkingLevel())
  └─► thinking_level_select

exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)
  └─► session_shutdown

Стартовые события

project_trust

Запускается до того, как pi решит, доверять ли проекту с динамическими конфигурациями (.pi или .agents/skills). Он запускается во время запуска и когда замена сеанса (например, /resume) входит в cwd, доверие которого не было разрешено в текущем процессе. Участвуют только пользовательские/глобальные расширения и расширения CLI -e; Локальные расширения проекта не загружаются до тех пор, пока не будет разрешено доверие.

pi.on("project_trust", async (event, ctx) => {
  // event.cwd - current working directory
  // ctx has a limited trust context: cwd, mode, hasUI, and select/confirm/input/notify UI helpers
  if (await ctx.ui.confirm("Trust project?", event.cwd)) {
    return { trusted: "yes", remember: true };
  }
  return { trusted: "undecided" };
});

Обработчик project_trust должен возвращать { trusted: "yes" | "no" | "undecided" }. Пользовательское/глобальное расширение или расширение CLI, которое возвращает "yes" или "no", принимает решение; первое решение «да/нет» побеждает и подавляет встроенный запрос доверия. Используйте remember: true, чтобы утвердить решение да/нет; в противном случае это применяется только к текущему процессу. Верните "undecided", чтобы позволить более поздним обработчикам или встроенному потоку доверия принять решение. Прежде чем запрашивать запрос, проверьте ctx.hasUI. Если ни один обработчик не возвращает да/нет, нормальное разрешение доверия продолжается: сначала применяются сохраненные решения trust.json, затем defaultProjectTrust контролирует, запрашивает ли pi, доверяет или отклоняет его по умолчанию.

Ресурсные события

resources_discover

Запускается после session_start, поэтому расширения могут предоставлять дополнительные пути к навыкам, подсказкам и темам. Путь запуска использует reason: "startup". Для перезагрузки используется reason: "reload".

pi.on("resources_discover", async (event, _ctx) => {
  // event.cwd - current working directory
  // event.reason - "startup" | "reload"
  return {
    skillPaths: ["/path/to/skills"],
    promptPaths: ["/path/to/prompts"],
    themePaths: ["/path/to/themes"],
  };
});

События сессии

См. Session Format о внутреннем устройстве хранилища сеансов и SessionManager API.

session_start

Запускается, когда сеанс запускается, загружается или перезагружается.

pi.on("session_start", async (event, ctx) => {
  // event.reason - "startup" | "reload" | "new" | "resume" | "fork"
  // event.previousSessionFile - present for "new", "resume", and "fork"
  ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
});

session_info_changed

Запускается, когда отображаемое имя текущего сеанса установлено с помощью /name, RPC или pi.setSessionName().

pi.on("session_info_changed", async (event, ctx) => {
  // event.name - current normalized name, or undefined if cleared
  ctx.ui.notify(`Session renamed: ${event.name ?? "(none)"}`, "info");
});

session_before_switch

Запускается перед началом нового сеанса (/new) или переключением сеанса (/resume).

pi.on("session_before_switch", async (event, ctx) => {
  // event.reason - "new" or "resume"
  // event.targetSessionFile - session we're switching to (only for "resume")

  if (event.reason === "new") {
    const ok = await ctx.ui.confirm("Clear?", "Delete all messages?");
    if (!ok) return { cancel: true };
  }
});

После успешного переключения или действия нового сеанса pi выдает session_shutdown для старого экземпляра расширения, перезагружает и повторно привязывает расширения для нового сеанса, затем выдает session_start с reason: "new" | "resume" и previousSessionFile. Выполните очистку в session_shutdown, затем восстановите любое состояние памяти в session_start.

session_before_fork

Запускается при разветвлении через /fork или клонировании через /clone.

pi.on("session_before_fork", async (event, ctx) => {
  // event.entryId - ID of the selected entry
  // event.position - "before" for /fork, "at" for /clone
  return { cancel: true }; // Cancel fork/clone
  // OR
  return { skipConversationRestore: true }; // Reserved for future conversation restore control
});

После успешного разветвления или клонирования pi выдает session_shutdown для старого экземпляра расширения, перезагружает и повторно привязывает расширения для нового сеанса, затем выдает session_start с reason: "fork" и previousSessionFile. Выполните очистку в session_shutdown, затем восстановите любое состояние памяти в session_start.

сеанс_перед_компакт / сеанс_компакт

Сгорел при уплотнении. Подробности см. compaction.md.

pi.on("session_before_compact", async (event, ctx) => {
  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;

  // reason - "manual" (/compact), "threshold", or "overflow"
  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)

  // Cancel:
  return { cancel: true };

  // Custom summary:
  return {
    compaction: {
      summary: "...",
      firstKeptEntryId: preparation.firstKeptEntryId,
      tokensBefore: preparation.tokensBefore,
      // usage: summaryResponse.usage, // Optional; included in session totals
    }
  };
});

pi.on("session_compact", async (event, ctx) => {
  // event.compactionEntry - the saved compaction
  // event.fromExtension - whether extension provided it
  // event.reason - "manual" (/compact), "threshold", or "overflow"
  // event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)
});

дерево сеанса_перед_деревом / дерево_сессии

Сработало при навигации /tree. См. Sessions для ознакомления с концепциями навигации по дереву.

pi.on("session_before_tree", async (event, ctx) => {
  const { preparation, signal } = event;
  return { cancel: true };
  // OR provide custom summary:
  return {
    summary: {
      summary: "...",
      // usage: summaryResponse.usage, // Optional; included in session totals
      details: {},
    },
  };
});

pi.on("session_tree", async (event, ctx) => {
  // event.newLeafId, oldLeafId, summaryEntry, fromExtension
});

session_shutdown

Вызывается до того, как запущенная среда выполнения сеанса будет удалена. Используйте это для очистки ресурсов, открытых из session_start или других перехватчиков в области сеанса.

pi.on("session_shutdown", async (event, ctx) => {
  // event.reason - "quit" | "reload" | "new" | "resume" | "fork"
  // event.targetSessionFile - destination session for session replacement flows
  // Cleanup, save state, etc.
});

Агентские события

before_agent_start

Запускается после того, как пользователь отправляет запрос, перед циклом агента. Может вставить сообщение и/или изменить системное приглашение.

pi.on("before_agent_start", async (event, ctx) => {
  // event.prompt - user's prompt text
  // event.images - attached images (if any)
  // event.systemPrompt - current chained system prompt for this handler
  //   (includes changes from earlier before_agent_start handlers)
  // event.systemPromptOptions - structured options used to build the system prompt
  //   .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)
  //   .selectedTools - tools currently active in the prompt
  //   .toolSnippets - one-line descriptions for each tool
  //   .promptGuidelines - custom guideline bullets
  //   .appendSystemPrompt - text from --append-system-prompt flags
  //   .cwd - working directory
  //   .contextFiles - AGENTS.md files and other loaded context files
  //   .skills - loaded skills

  return {
    // Inject a persistent message (stored in session, sent to LLM)
    message: {
      customType: "my-extension",
      content: "Additional context for the LLM",
      display: true,
    },
    // Replace the system prompt for this turn (chained across extensions)
    systemPrompt: event.systemPrompt + "\n\nExtra instructions for this turn...",
  };
});

Поле systemPromptOptions предоставляет расширениям доступ к тем же структурированным данным, которые Pi использует для создания системного приглашения. Это позволяет вам проверять, что загружено Pi — пользовательские подсказки, рекомендации, фрагменты инструментов, context files, навыки — без повторного открытия ресурсов или повторного анализа флагов. Используйте его, когда вашему расширению необходимо внести глубокие и обоснованные изменения в системную подсказку, соблюдая при этом конфигурацию, предоставленную пользователем.

Внутри before_agent_start, event.systemPrompt и ctx.getSystemPrompt() оба отражают связанное системное приглашение текущего обработчика. Позже обработчики before_agent_start все еще смогут изменить его снова.

начало_агента / окончание_агента / урегулирование_агента

agent_start срабатывает, когда начинается запуск агента низкого уровня. agent_end срабатывает, когда этот запуск заканчивается, но Pi все еще может автоматически повторять попытку, автоматически сжимать и повторять попытку или продолжать с последующими сообщениями в очереди. Используйте agent_settled для интеграции статуса, если необходимо знать, что Pi не будет продолжать работать автоматически.

pi.on("agent_start", async (_event, ctx) => {});

pi.on("agent_end", async (event, ctx) => {
  // event.messages - messages from this low-level run
});

pi.on("agent_settled", async (_event, ctx) => {
  // ctx.isIdle() is true here unless another extension started a new run.
});

начало_поворота/конец_поворота

Срабатывает за каждый ход (один ответ LLM + вызовы инструментов).

pi.on("turn_start", async (event, ctx) => {
  // event.turnIndex, event.timestamp
});

pi.on("turn_end", async (event, ctx) => {
  // event.turnIndex, event.message, event.toolResults
});

начало_сообщения/обновление_сообщения/конец_сообщения

Срабатывает при обновлении жизненного цикла сообщения.

  • message_start и message_end срабатывают для сообщений пользователя, помощника и инструмента.
  • message_update срабатывает для потоковой передачи обновлений помощника.
  • Обработчики message_end могут возвращать { message } для замены окончательного сообщения. Замена должна оставить прежнюю role.
pi.on("message_start", async (event, ctx) => {
  // event.message
});

pi.on("message_update", async (event, ctx) => {
  // event.message
  // event.assistantMessageEvent (token-by-token stream event)
});

pi.on("message_end", async (event, ctx) => {
  if (event.message.role !== "assistant") return;

  return {
    message: {
      ...event.message,
      usage: {
        ...event.message.usage,
        cost: {
          ...event.message.usage.cost,
          total: 0.123,
        },
      },
    },
  };
});

начало_выполнения_инструмента/обновление_выполнения_инструмента/конец_выполнения_инструмента

Запускается из-за обновлений жизненного цикла выполнения инструмента.

В параллельном режиме инструмента:

  • tool_execution_start излучается в порядке вспомогательного источника на предполетном этапе.
  • tool_execution_update события могут чередоваться между инструментами
  • tool_execution_end выдается в порядке завершения работы с инструментом после завершения каждого инструмента.
  • События окончательного сообщения toolResult по-прежнему отправляются позже в порядке источника помощника.
pi.on("tool_execution_start", async (event, ctx) => {
  // event.toolCallId, event.toolName, event.args
});

pi.on("tool_execution_update", async (event, ctx) => {
  // event.toolCallId, event.toolName, event.args, event.partialResult
});

pi.on("tool_execution_end", async (event, ctx) => {
  // event.toolCallId, event.toolName, event.result, event.isError
});

контекст

Вызывается перед каждым вызовом LLM. Изменяйте сообщения неразрушающим образом. См. Session Format для типов сообщений.

pi.on("context", async (event, ctx) => {
  // event.messages - deep copy, safe to modify
  const filtered = event.messages.filter(m => !shouldPrune(m));
  return { messages: filtered };
});

before_provider_headers

Запускается после сборки исходящих HTTP-заголовков. Используйте его для добавления, переопределения или удаления заголовков запросов.

Обработчики мутируют event.headers на месте. Установите ключ на строку, чтобы добавить или переопределить ее, или на null, чтобы удалить ее.

pi.on("before_provider_headers", (event, ctx) => {
  // Add or override — e.g. a session id for gateway tracing/attribution
  event.headers["x-session-id"] = ctx.sessionManager.getSessionId();

  // Drop a tracking header pi adds for this call
  event.headers["X-OpenRouter-Title"] = null;
});

Запускается один раз по запросу поставщика; повторные попытки повторно используют одни и те же заголовки вместо повторного запуска перехватчика.

before_provider_request

Запускается после создания полезных данных, специфичных для поставщика, непосредственно перед отправкой запроса. Обработчики выполняются в порядке загрузки расширений. Возврат undefined сохраняет полезную нагрузку неизменной. Возврат любого другого значения заменяет полезную нагрузку для последующих обработчиков и самого запроса.

Этот хук может переписать системные инструкции на уровне провайдера или полностью удалить их. Эти изменения на уровне полезных данных не отражаются в ctx.getSystemPrompt(), который сообщает строку системного приглашения Pi, а не окончательную сериализованную полезную нагрузку поставщика.

pi.on("before_provider_request", (event, ctx) => {
  console.log(JSON.stringify(event.payload, null, 2));

  // Optional: replace payload
  // return { ...event.payload, temperature: 0 };
});

Это в основном полезно для отладки сериализации поставщика и поведения кэша.

after_provider_response

Запускается после получения HTTP-ответа и до того, как будет использовано тело его потока. Обработчики выполняются в порядке загрузки расширений.

pi.on("after_provider_response", (event, ctx) => {
  // event.status - HTTP status code
  // event.headers - normalized response headers
  if (event.status === 429) {
    console.log("rate limited", event.headers["retry-after"]);
  }
});

Доступность заголовка зависит от провайдера и транспорта. Providers что абстрактные HTTP-ответы могут не предоставлять заголовки.

Модельные события

model_select

Запускается, когда модель изменяется с помощью команды /model, циклического переключения модели (Ctrl+P) или восстановления сеанса.

pi.on("model_select", async (event, ctx) => {
  // event.model - newly selected model
  // event.previousModel - previous model (undefined if first selection)
  // event.source - "set" | "cycle" | "restore"

  const prev = event.previousModel
    ? `${event.previousModel.provider}/${event.previousModel.id}`
    : "none";
  const next = `${event.model.provider}/${event.model.id}`;

  ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, "info");
});

Используйте это для обновления элементов пользовательского интерфейса (строки состояния, нижние колонтитулы) или выполнения инициализации для конкретной модели при изменении активной модели.

think_level_select

Срабатывает, когда меняется уровень мышления. Это только уведомление; Возвращаемые значения обработчика игнорируются.

pi.on("thinking_level_select", async (event, ctx) => {
  // event.level - newly selected thinking level
  // event.previousLevel - previous thinking level

  ctx.ui.setStatus("thinking", `thinking: ${event.level}`);
});

Используйте это для обновления пользовательского интерфейса расширения, когда pi.setThinkingLevel(), изменения модели или встроенные элементы управления на уровне мышления изменяют активный уровень мышления.

События инструмента

инструмент_вызов

Запускается после tool_execution_start, до выполнения инструмента. Может блокироваться. Используйте isToolCallEventType, чтобы сузить и получить типизированные входные данные.

Перед запуском tool_call pi ожидает завершения прохождения ранее созданных событий агента через AgentSession. Это означает, что ctx.sessionManager обновлен через текущее сообщение вызова помощника.

В режиме параллельного выполнения инструмента по умолчанию вызовы родственных инструментов из одного и того же сообщения помощника предварительно проверяются последовательно, а затем выполняются одновременно. tool_call не гарантируется, что он увидит результаты родственного инструмента из того же сообщения помощника в ctx.sessionManager.

event.input является изменяемым. Измените его, чтобы исправить аргументы инструмента перед выполнением.

Поведение гарантирует:

  • Мутации event.input влияют на фактическое выполнение инструмента.
  • Более поздние обработчики tool_call видят мутации, сделанные более ранними обработчиками.
  • После мутации повторная проверка не выполняется.
  • Возвращаемые значения из tool_call управления блокировкой через { block: true, reason?: string, terminate?: boolean }
  • terminate применяется только к заблокированному вызову; агент останавливается раньше, только когда завершается работа каждого окончательного результата в пакете
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";

pi.on("tool_call", async (event, ctx) => {
  // event.toolName - "bash", "read", "write", "edit", etc.
  // event.toolCallId
  // event.input - tool parameters (mutable)

  // Built-in tools: no type params needed
  if (isToolCallEventType("bash", event)) {
    // event.input is { command: string; timeout?: number }
    event.input.command = `source ~/.profile\n${event.input.command}`;

    if (event.input.command.includes("rm -rf")) {
      return { block: true, reason: "Dangerous command", terminate: true };
    }
  }

  if (isToolCallEventType("read", event)) {
    // event.input is { path: string; offset?: number; limit?: number }
    console.log(`Reading: ${event.input.path}`);
  }
});

Ввод пользовательского ввода инструмента

Пользовательские инструменты должны экспортировать свой тип ввода:

// my-extension.ts
export type MyToolInput = Static<typeof myToolSchema>;

Используйте isToolCallEventType с параметрами явного типа:

import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
import type { MyToolInput } from "my-extension";

pi.on("tool_call", (event) => {
  if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) {
    event.input.action;  // typed
  }
});

инструмент_результат

Запускается после завершения выполнения инструмента и до появления tool_execution_end плюс событий сообщения о конечном результате работы инструмента. Можно изменить результат.

В режиме параллельного инструмента tool_result и tool_execution_end могут чередоваться в порядке завершения инструмента, в то время как последние события сообщения toolResult по-прежнему отправляются позже в порядке источника помощника.

tool_result цепочка обработчиков, подобная промежуточному программному обеспечению:

  • Обработчики выполняются в порядке загрузки расширений.
  • Каждый обработчик видит последний результат после предыдущих изменений обработчика.
  • Обработчики могут возвращать частичные исправления (content, details, isError или usage); пропущенные поля сохраняют свои текущие значения

Используйте ctx.signal для вложенной асинхронной работы внутри обработчика. Это позволяет Esc отменять вызовы модели, fetch() и другие операции с возможностью прерывания, запущенные расширением.

import { isBashToolResult } from "@earendil-works/pi-coding-agent";

pi.on("tool_result", async (event, ctx) => {
  // event.toolName, event.toolCallId, event.input
  // event.content, event.details, event.isError, event.usage

  if (isBashToolResult(event)) {
    // event.details is typed as BashToolDetails
  }

  const response = await fetch("https://example.com/summarize", {
    method: "POST",
    body: JSON.stringify({ content: event.content }),
    signal: ctx.signal,
  });

  // Modify result:
  return { content: [...], details: {...}, isError: false, usage: nestedModelUsage };
});

Пользовательские события Bash

пользователь_bash

Запускается, когда пользователь выполняет команды ! или !!. Может перехватывать.

import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";

pi.on("user_bash", (event, ctx) => {
  // event.command - the bash command
  // event.excludeFromContext - true if !! prefix
  // event.cwd - working directory

  // Option 1: Provide custom operations (e.g., SSH)
  return { operations: remoteBashOps };

  // Option 2: Wrap pi's built-in local bash backend
  const local = createLocalBashOperations();
  return {
    operations: {
      exec(command, cwd, options) {
        return local.exec(`source ~/.profile\n${command}`, cwd, options);
      }
    }
  };

  // Option 3: Full replacement - return result directly
  return { result: { output: "...", exitCode: 0, cancelled: false, truncated: false } };
});

Входные события

вход

Запускается при получении пользовательского ввода, после проверки команд расширения, но до расширения навыков и шаблонов. Событие видит необработанный входной текст, поэтому /skill:foo и /template еще не раскрыты.

Порядок обработки:

  1. Команды расширения (/cmd) проверяются в первую очередь — если они найдены, запускается обработчик и событие ввода пропускается.
  2. input пожары событий — могут перехватывать, трансформировать или обрабатывать
  3. Если не обработано: команды навыков (/skill:name) расширяются до содержимого навыков.
  4. Если не обработано: prompt templates (/template) расширяется до содержимого шаблона.
  5. Начинается обработка агента (before_agent_start и т. д.)
pi.on("input", async (event, ctx) => {
  // event.text - raw input (before skill/template expansion)
  // event.images - attached images, if any
  // event.source - "interactive" (typed), "rpc" (API), or "extension" (via sendUserMessage)
  // event.streamingBehavior - "steer" | "followUp" | undefined
  //   undefined when idle, "steer" for mid-stream interrupts,
  //   "followUp" for messages queued until the agent finishes

  // Transform: rewrite input before expansion
  if (event.text.startsWith("?quick "))
    return { action: "transform", text: `Respond briefly: ${event.text.slice(7)}` };

  // Handle: respond without LLM (extension shows its own feedback)
  if (event.text === "ping") {
    ctx.ui.notify("pong", "info");
    return { action: "handled" };
  }

  // Route by source: skip processing for extension-injected messages
  if (event.source === "extension") return { action: "continue" };

  // Intercept skill commands before expansion
  if (event.text.startsWith("/skill:")) {
    // Could transform, block, or let pass through
  }

  return { action: "continue" };  // Default: pass through to expansion
});

Результаты:

  • continue — пройти без изменений (по умолчанию, если обработчик ничего не возвращает)
  • transform — изменить текст/изображения, затем продолжить раскрытие
  • handled — полностью пропустить агент (первый обработчик, который вернет этот выигрыш)

Преобразует цепочку между обработчиками. См. input-transform.ts и input-transform-streaming.ts для маршрутизации с учетом streamingBehavior.

Контекст расширения

Все обработчики получают ctx: ExtensionContext.

ctx.ui

Методы пользовательского интерфейса для взаимодействия с пользователем. Подробную информацию см. Custom UI.

ctx.mode

Текущий режим работы: "tui", "rpc", "json" или "print". Используйте ctx.mode === "tui" для защиты функций только терминала, таких как custom(), фабрики компонентов, ввод через терминал и прямой рендеринг TUI.

ctx.hasUI

true в режимах TUI и RPC. false в режиме печати (-p) и JSON. Используйте это для защиты методов диалога (select, confirm, input, editor) и методов «выстрелил и забыл» (notify, setStatus, setWidget, setTitle, setEditorText), которые работают как в TUI, так и в RPC режимов. В режиме RPC некоторые методы, специфичные для TUI, не выполняются или возвращают значения по умолчанию (см. rpc.md).

ctx.cwd

Текущий рабочий каталог.

Используйте CONFIG_DIR_NAME вместо жесткого кодирования .pi при создании локальных путей конфигурации проекта. Дистрибутивы с ребрендингом могут использовать другое имя каталога конфигурации.

import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { join } from "node:path";

export default function (pi: ExtensionAPI) {
  pi.on("session_start", (_event, ctx) => {
    const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, "my-extension.json");
    // ...
  });
}

ctx.isProjectTrusted()

Возвращает, активно ли локальное доверие проекта для текущего контекста сеанса. Сюда входят временные решения о доверии и переопределения доверия CLI, а не только сохраненные решения в глобальном хранилище доверенных сертификатов.

Используйте это перед чтением конфигурации локального расширения проекта, которую следует учитывать только для доверенных проектов.

ctx.sessionManager

Доступ только для чтения к состоянию сеанса. См. Session Format для полной версии SessionManager API и типов записей.

Для tool_call это состояние синхронизируется через текущее сообщение помощника перед запуском обработчиков. В режиме параллельного выполнения инструмента по-прежнему не гарантируется включение результатов родственного инструмента из одного и того же сообщения помощника.

ctx.sessionManager.getEntries()             // All entries
ctx.sessionManager.getBranch()              // Current branch
ctx.sessionManager.buildContextEntries()    // Active branch entries with compaction applied
ctx.sessionManager.getLeafId()              // Current leaf entry ID

ctx.modelRegistry/ctx.model/ctx.thinkingLevel/ctx.scopedModels

Доступ к моделям, поставщикам и разрешенной аутентификации. ctx.modelRegistry.getProvider(id) возвращает эффективного поставщика pi-ai, а getProviderAuth(id) разрешает его текущий API key, заголовки, базовый URL-адрес и среду на уровне поставщика, не требуя загрузки модели. ctx.model — активная модель, а ctx.thinkingLevel — текущий эффективный уровень мышления.

ctx.scopedModels — это доступный только для чтения список моделей, применимых к текущему сеансу — тот же набор, который показывает команда /scoped-models. Это разрешается при запуске сеанса с помощью флага --models CLI и настройки enabledModels (сопоставляется с доступным каталогом с минимальным совпадением на provider/modelId или пустом modelId). Он пуст, если область действия не настроена, что означает, что можно использовать любую доступную модель. Каждая запись имеет номер { model, thinkingLevel? }, где thinkingLevel устанавливается только в том случае, если ее закрепил шаблон (например, anthropic/*:high). Используйте его для заполнения средства выбора модели, которое отражает встроенное, вместо перечисления всего каталога с помощью ctx.modelRegistry.getAvailable().

ctx.signal

Текущий сигнал прерывания агента или undefined, если ни один ход агента не активен.

Используйте это для вложенной работы с поддержкой прерывания, запускаемой обработчиками расширений, например:

  • fetch(..., { signal: ctx.signal })
  • вызовы моделей, которые принимают signal
  • помощники файлов или процессов, которые принимают AbortSignal

ctx.signal обычно определяется во время активных событий хода, таких как tool_call, tool_result, message_update и turn_end. Обычно это undefined в контекстах ожидания или отсутствия поворота, таких как события сеанса, команды расширения и ярлыки, запускаемые во время простоя pi.

pi.on("tool_result", async (event, ctx) => {
  const response = await fetch("https://example.com/api", {
    method: "POST",
    body: JSON.stringify(event),
    signal: ctx.signal,
  });

  const data = await response.json();
  return { details: data };
});

ctx.isIdle()/ctx.abort()/ctx.hasPendingMessages()

Помощники управления потоком. ctx.isIdle() имеет значение false, в то время как Pi обрабатывает запуск агента, автоматическую повторную попытку, повторную попытку автоматического сжатия или продолжение в очереди.

ctx.shutdown()

Запросите корректное завершение работы pi.

  • Интерактивный режим: Откладывается до тех пор, пока агент не станет бездействующим (после обработки всех находящихся в очереди управляющих и последующих сообщений).
  • Режим RPC: Откладывается до следующего состояния простоя (после завершения ответа на текущую команду, при ожидании следующей команды).
  • Режим печати: Нет операций. Процесс завершается автоматически после обработки всех запросов.

Перед выходом выдает событие session_shutdown всем расширениям. Доступно во всех контекстах (обработчики событий, инструменты, команды, ярлыки).

pi.on("tool_call", (event, ctx) => {
  if (isFatal(event.input)) {
    ctx.shutdown();
  }
});

ctx.getContextUsage()

Возвращает текущее использование контекста для активной модели. Использует использование последнего помощника, если он доступен, а затем оценивает токены для последующих сообщений.

const usage = ctx.getContextUsage();
if (usage && usage.tokens > 100_000) {
  // ...
}

ctx.compact()

Запустить уплотнение, не дожидаясь завершения. Используйте onComplete и onError для последующих действий.

ctx.compact({
  customInstructions: "Focus on recent changes",
  onComplete: (result) => {
    ctx.ui.notify("Compaction completed", "info");
  },
  onError: (error) => {
    ctx.ui.notify(`Compaction failed: ${error.message}`, "error");
  },
});

ctx.getSystemPrompt()

Возвращает текущую строку системного приглашения Pi.

  • В течение before_agent_start это отражает цепочку изменений системных подсказок, сделанных на данный момент для текущего хода.
  • Он не включает более поздние мутации сообщения context.
  • Он не включает перезапись полезной нагрузки before_provider_request.
  • Если расширения, загруженные позже, запускаются после вашего, они все равно могут изменить то, что в конечном итоге отправляется.
pi.on("before_agent_start", (event, ctx) => {
  const prompt = ctx.getSystemPrompt();
  console.log(`System prompt length: ${prompt.length}`);
});

РасширениеCommandContext

Обработчики команд получают ExtensionCommandContext, который расширяет ExtensionContext методами управления сеансом. Они доступны только в командах, поскольку могут вызвать взаимоблокировку при вызове из обработчиков событий.

ctx.getSystemPromptOptions()

Возвращает базовые входные данные Pi, которые в настоящее время используются для создания системного приглашения.

const options = ctx.getSystemPromptOptions();
const contextPaths = options.contextFiles?.map((file) => file.path) ?? [];

Он имеет ту же форму и изменчивость, что и before_agent_start event.systemPromptOptions: пользовательское приглашение, активные инструменты, фрагменты инструментов, инструкции по подсказкам, добавленный текст системного приглашения, cwd, загруженные context files и загруженные навыки. Он может включать полное содержимое файла контекста, поэтому относитесь к нему как к конфиденциальным локальным данным расширения и избегайте раскрытия его через списки команд, журналы или метаданные автозаполнения.

Это сообщает о текущих вводимых базовых подсказках. Он не включает в себя before_agent_start связанные изменения системных подсказок за ход, более поздние context мутации сообщений о событиях или before_provider_request перезапись полезной нагрузки.

ctx.waitForIdle()

Подождите, пока агент полностью рассчитается, включая автоматические повторы, повторы автоматического сжатия и продолжения в очереди:

pi.registerCommand("my-cmd", {
  handler: async (args, ctx) => {
    await ctx.waitForIdle();
    // Agent is now idle, safe to modify session
  },
});

ctx.newSession(варианты?)

Создайте новый сеанс:

const parentSession = ctx.sessionManager.getSessionFile();
const kickoff = "Continue in the replacement session";

const result = await ctx.newSession({
  parentSession,
  setup: async (sm) => {
    sm.appendMessage({
      role: "user",
      content: [{ type: "text", text: "Context from previous session..." }],
      timestamp: Date.now(),
    });
  },
  withSession: async (ctx) => {
    // Use only the replacement-session ctx here.
    await ctx.sendUserMessage(kickoff);
  },
});

if (result.cancelled) {
  // An extension cancelled the new session
}

Параметры:

  • parentSession: файл родительского сеанса для записи в новый заголовок сеанса.
  • setup: изменить SessionManager нового сеанса перед запуском withSession
  • withSession: запустить работу после переключения в новом контексте сеанса замены. Не используйте захваченную старую команду pi / ctx; см. Session replacement lifecycle and footguns.

ctx.fork(entryId, параметры?)

Форк из определенной записи, создавая новый файл сеанса:

const result = await ctx.fork("entry-id-123", {
  withSession: async (ctx) => {
    // Use only the replacement-session ctx here.
    ctx.ui.notify("Now in the forked session", "info");
  },
});
if (result.cancelled) {
  // An extension cancelled the fork
}

const cloneResult = await ctx.fork("entry-id-456", { position: "at" });
if (cloneResult.cancelled) {
  // An extension cancelled the clone
}

Параметры:

  • position: "before" (по умолчанию) разветвляется перед выбранным сообщением пользователя, восстанавливая это приглашение в редакторе.
  • position: "at" дублирует активный путь через выбранную запись без восстановления текста редактора.
  • withSession: запустить работу после переключения в новом контексте сеанса замены. Не используйте захваченную старую команду pi / ctx; см. Session replacement lifecycle and footguns.

ctx.navigateTree(targetId, параметры?)

Перейдите в другую точку session tree:

const result = await ctx.navigateTree("entry-id-456", {
  summarize: true,
  customInstructions: "Focus on error handling changes",
  replaceInstructions: false, // true = replace default prompt entirely
  label: "review-checkpoint",
});

Параметры:

  • summarize: Создавать ли сводку заброшенной ветки.
  • customInstructions: Пользовательские инструкции для сумматора.
  • replaceInstructions: Если это правда, customInstructions заменяет приглашение по умолчанию, а не добавляется.
  • label: Метка для прикрепления к сводной записи ветки (или целевой записи, если не суммируется)

ctx.switchSession(sessionPath, параметры?)

Переключитесь на другой файл сеанса:

const result = await ctx.switchSession("/path/to/session.jsonl", {
  withSession: async (ctx) => {
    await ctx.sendUserMessage("Resume work in the replacement session");
  },
});
if (result.cancelled) {
  // An extension cancelled the switch via session_before_switch
}

Параметры:

  • withSession: запустить работу после переключения в новом контексте сеанса замены. Не используйте захваченную старую команду pi / ctx; см. Session replacement lifecycle and footguns.

Чтобы обнаружить доступные сеансы, используйте статические методы SessionManager.list() или SessionManager.listAll():

import { SessionManager } from "@earendil-works/pi-coding-agent";

pi.registerCommand("switch", {
  description: "Switch to another session",
  handler: async (args, ctx) => {
    const sessions = await SessionManager.list(ctx.cwd);
    if (sessions.length === 0) return;
    const choice = await ctx.ui.select(
      "Pick session:",
      sessions.map(s => s.file),
    );
    if (choice) {
      await ctx.switchSession(choice, {
        withSession: async (ctx) => {
          ctx.ui.notify("Switched session", "info");
        },
      });
    }
  },
});

Жизненный цикл замены сеанса и ножные пистолеты

withSession получает новый ReplacedSessionContext, который расширяет ExtensionCommandContext асинхронными помощниками sendMessage() и sendUserMessage(), привязанными к сеансу замены.

Жизненный цикл и ножи:

  • withSession запускается только после того, как старый сеанс выдал session_shutdown, старая среда выполнения была удалена, заменяющий сеанс был восстановлен, а новый экземпляр расширения уже получил session_start.
  • Обратный вызов по-прежнему выполняется в исходном замыкании, а не внутри нового экземпляра расширения. Это означает, что ваш старый экземпляр расширения, возможно, уже выполнил очистку после завершения работы до запуска withSession.
  • Захваченные старые объекты pi/старой команды ctx, привязанные к сеансу, устарели после замены и будут выброшены, если они используются. Используйте только ctx, переданный в withSession для работы с привязкой к сеансу.
  • Ранее извлеченные необработанные объекты по-прежнему остаются под вашей ответственностью. Например, если вы захватите const sm = ctx.sessionManager перед заменой, sm по-прежнему будет старым объектом SessionManager. Не используйте его повторно после замены.
  • Код в withSession должен предполагать, что любое состояние, признанное недействительным вашим обработчиком session_shutdown, уже исчезло. Собирайте только простые данные, которые без проблем выдерживают завершение работы, например строки, идентификаторы и сериализованную конфигурацию.

Безопасный шаблон:

pi.registerCommand("handoff", {
  handler: async (_args, ctx) => {
    const kickoff = "Continue from the replacement session";
    await ctx.newSession({
      withSession: async (ctx) => {
        await ctx.sendUserMessage(kickoff);
      },
    });
  },
});

Небезопасный шаблон:

pi.registerCommand("handoff", {
  handler: async (_args, ctx) => {
    const oldSessionManager = ctx.sessionManager;
    await ctx.newSession({
      withSession: async (_ctx) => {
        // stale old objects: do not do this
        oldSessionManager.getSessionFile();
        pi.sendUserMessage("wrong");
      },
    });
  },
});

ctx.reload()

Запустите тот же процесс перезагрузки, что и /reload.

pi.registerCommand("reload-runtime", {
  description: "Reload extensions, skills, prompts, themes, and context files",
  handler: async (_args, ctx) => {
    await ctx.reload();
    return;
  },
});

Важное поведение:

  • await ctx.reload() выдает session_shutdown для текущей среды выполнения расширения.
  • Затем он перезагружает ресурсы и выдает session_start с reason: "reload" и resources_discover с причиной "reload".
  • Текущий обработчик команд продолжает работать в старом кадре вызова.
  • Код после await ctx.reload() по-прежнему работает из версии до перезагрузки.
  • Код после await ctx.reload() не должен предполагать, что старое состояние расширения в памяти все еще действительно.
  • После возврата обработчика будущие команды/события/вызовы инструментов будут использовать новую версию расширения.

Для обеспечения предсказуемого поведения рассматривайте перезагрузку как терминал для этого обработчика (await ctx.reload(); return;).

Инструменты запускаются с ExtensionContext, поэтому они не могут напрямую вызывать ctx.reload(). Используйте команду в качестве точки входа перезагрузки, а затем предоставьте инструмент, который ставит эту команду в очередь в качестве последующего сообщения пользователя.

Пример инструмента, который LLM может вызвать для запуска перезагрузки:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

export default function (pi: ExtensionAPI) {
  pi.registerCommand("reload-runtime", {
    description: "Reload extensions, skills, prompts, themes, and context files",
    handler: async (_args, ctx) => {
      await ctx.reload();
      return;
    },
  });

  pi.registerTool({
    name: "reload_runtime",
    label: "Reload Runtime",
    description: "Reload extensions, skills, prompts, themes, and context files",
    parameters: Type.Object({}),
    async execute() {
      pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
      return {
        content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
      };
    },
  });
}

РасширениеAPI Методы

pi.on(событие, обработчик)

Подписывайтесь на события. См. Events для типов событий и возвращаемых значений.

pi.registerTool (определение)

Зарегистрируйте собственный инструмент, вызываемый LLM. Подробную информацию см. Custom Tools.

pi.registerTool() работает как во время загрузки расширения, так и после запуска. Вы можете вызвать его внутри session_start, обработчиков команд или других обработчиков событий. Новые инструменты обновляются немедленно в том же сеансе, поэтому они появляются в pi.getAllTools() и могут быть вызваны из LLM без /reload.

Используйте pi.setActiveTools(), чтобы включить или отключить инструменты (включая динамически добавляемые инструменты) во время выполнения.

Используйте promptSnippet, чтобы включить пользовательский инструмент в однострочную запись в Available tools, и promptGuidelines, чтобы добавить маркеры, специфичные для инструмента, в раздел Guidelines по умолчанию, когда инструмент активен.

Важно! Маркеры promptGuidelines добавляются в раздел Guidelines ровно, без префикса имени инструмента. В каждом руководстве должен быть указан инструмент, к которому он относится. Избегайте фразы «Используйте этот инструмент, когда...», поскольку LLM не может определить, какой инструмент означает «это». Вместо этого напишите «Использовать my_tool, когда...».

Полный пример см. в dynamic-tools.ts.

import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";

pi.registerTool({
  name: "my_tool",
  label: "My Tool",
  description: "What this tool does",
  promptSnippet: "Summarize or transform text according to action",
  promptGuidelines: ["Use my_tool when the user asks to summarize previously generated text."],
  parameters: Type.Object({
    action: StringEnum(["list", "add"] as const),
    text: Type.Optional(Type.String()),
  }),
  prepareArguments(args) {
    // Optional compatibility shim. Runs before schema validation.
    // Return the current schema shape, for example to fold legacy fields
    // into the modern parameter object.
    return args;
  },

  async execute(toolCallId, params, signal, onUpdate, ctx) {
    // Stream progress
    onUpdate?.({ content: [{ type: "text", text: "Working..." }] });

    return {
      content: [{ type: "text", text: "Done" }],
      details: { result: "..." },
    };
  },

  // Optional: Custom rendering
  renderCall(args, theme, context) { ... },
  renderResult(result, options, theme, context) { ... },
});

pi.sendMessage(сообщение, параметры?)

Внедрить пользовательское сообщение в сеанс. Пользовательские сообщения участвуют в контексте LLM. Для постоянного контента, содержащего только TUI, который не следует отправлять в LLM, используйте pi.appendEntry() с pi.registerEntryRenderer().

pi.sendMessage({
  customType: "my-extension",
  content: "Message text",
  display: true,
  details: { ... },
}, {
  triggerTurn: true,
  deliverAs: "steer",
});

Параметры:

  • deliverAs - Режим доставки:
    • "steer" (по умолчанию) — ставит сообщение в очередь во время потоковой передачи. Доставляется после того, как текущий ход помощника завершает выполнение вызовов инструментов, до следующего вызова LLM.
    • "followUp" — Ожидает завершения работы агента. Доставляется только тогда, когда у агента больше нет вызовов инструментов.
    • "nextTurn" — В очереди на приглашение следующего пользователя. Ничего не прерывает и не запускает.
  • triggerTurn: true — если агент простаивает, немедленно вызвать ответ LLM. Применяется только к режимам "steer" и "followUp" (игнорируется для "nextTurn").

pi.sendUserMessage(содержание, параметры?)

Отправьте пользовательское сообщение агенту. В отличие от sendMessage(), который отправляет пользовательские сообщения, здесь отправляется фактическое пользовательское сообщение, которое выглядит так, как будто оно напечатано пользователем. Всегда вызывает поворот.

// Simple text message
pi.sendUserMessage("What is 2+2?");

// With content array (text + images)
pi.sendUserMessage([
  { type: "text", text: "Describe this image:" },
  { type: "image", source: { type: "base64", mediaType: "image/png", data: "..." } },
]);

// During streaming - must specify delivery mode
pi.sendUserMessage("Focus on error handling", { deliverAs: "steer" });
pi.sendUserMessage("And then summarize", { deliverAs: "followUp" });

Параметры:

  • deliverAs — Требуется, когда агент ведет потоковую передачу:
    • "steer" — ставит сообщение в очередь для доставки после того, как текущий ход помощника завершит выполнение вызовов инструментов.
    • "followUp" — ждет, пока агент завершит все инструменты.

Если потоковая передача не ведется, сообщение отправляется немедленно и запускает новый ход. При потоковой передаче без deliverAs выдает ошибку.

Полный пример см. в send-user-message.ts.

pi.appendEntry(customType, данные?)

Сохранение данных расширения. Пользовательские записи НЕ участвуют в контексте LLM. В интерактивном режиме они также могут отображаться внутри стенограммы чата в сочетании с pi.registerEntryRenderer().

pi.appendEntry("my-state", { count: 42 });
pi.appendEntry("status-card", { title: "Indexed files", count: 17 });

// Restore on reload
pi.on("session_start", async (_event, ctx) => {
  for (const entry of ctx.sessionManager.getEntries()) {
    if (entry.type === "custom" && entry.customType === "my-state") {
      // Reconstruct from entry.data
    }
  }
});

pi.setSessionName(имя)

Установите отображаемое имя сеанса (отображается в селекторе сеанса вместо первого сообщения).

pi.setSessionName("Refactor auth module");

pi.getSessionName()

Получите имя текущего сеанса, если оно установлено.

const name = pi.getSessionName();
if (name) {
  console.log(`Session: ${name}`);
}

pi.setLabel(entryId, метка)

Установите или очистите метку записи. Метки — это определяемые пользователем маркеры для создания закладок и навигации (показаны в селекторе /tree).

// Set a label
pi.setLabel(entryId, "checkpoint-before-refactor");

// Clear a label
pi.setLabel(entryId, undefined);

// Read labels via sessionManager
const label = ctx.sessionManager.getLabel(entryId);

Метки сохраняются в течение сеанса и выдерживают перезапуск. Используйте их, чтобы отмечать важные точки (повороты, контрольные точки) в дереве разговора.

pi.registerCommand(имя, параметры)

Зарегистрируйте команду.

Если несколько расширений регистрируют одно и то же имя команды, pi сохраняет их все и назначает числовые суффиксы вызова в порядке загрузки, например /review:1 и /review:2.

pi.registerCommand("stats", {
  description: "Show session statistics",
  handler: async (args, ctx) => {
    const count = ctx.sessionManager.getEntries().length;
    ctx.ui.notify(`${count} entries`, "info");
  }
});

Необязательно: добавьте автодополнение аргументов для /command...:

import type { AutocompleteItem } from "@earendil-works/pi-tui";

pi.registerCommand("deploy", {
  description: "Deploy to an environment",
  getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
    const envs = ["dev", "staging", "prod"];
    const items = envs.map((e) => ({ value: e, label: e }));
    const filtered = items.filter((i) => i.value.startsWith(prefix));
    return filtered.length > 0 ? filtered : null;
  },
  handler: async (args, ctx) => {
    ctx.ui.notify(`Deploying: ${args}`, "info");
  },
});

пи.getCommands()

Получите slash commands, доступный для вызова через prompt в текущем сеансе. Включает команды расширения prompt templates и команды навыков. Список соответствует порядку RPC get_commands: сначала расширения, затем шаблоны, затем навыки.

const commands = pi.getCommands();
const bySource = commands.filter((command) => command.source === "extension");
const userScoped = commands.filter((command) => command.sourceInfo.scope === "user");

Каждая запись имеет следующую форму:

{
  name: string; // Invokable command name without the leading slash. May be suffixed like "review:1"
  description?: string;
  source: "extension" | "prompt" | "skill";
  sourceInfo: {
    path: string;
    source: string;
    scope: "user" | "project" | "temporary";
    origin: "package" | "top-level";
    baseDir?: string;
  };
}

Используйте sourceInfo в качестве поля канонического происхождения. Не делайте вывод о принадлежности на основании имен команд или анализа специальных путей.

Встроенные интерактивные команды (например, /model и /settings) сюда не включены. Они обрабатываются только в интерактивном режиме. режиме и не будет выполняться, если будет отправлено через prompt.

pi.registerMessageRenderer(customType, средство визуализации)

Зарегистрируйте собственный рендерер TUI для пользовательских сообщений на своем customType. Пользовательские сообщения создаются с помощью pi.sendMessage() и участвуют в контексте LLM. См. Custom UI.

pi.registerMarkdownTransformer(трансформатор)

Зарегистрируйте преобразователь для Markdown в обычном пользовательском тексте, тексте помощника и блоках мышления. Трансформаторы работают в порядке расширения нагрузки, и каждый трансформатор получает Markdown, возвращенный предыдущим трансформатором. После завершения цепочки Pi визуализирует преобразованный контент с помощью встроенного средства визуализации.

Преобразователь получает строку Markdown и контекст:

  • messageType"user", "assistant" или "assistant-thinking"
  • isStreamingtrue для частичного обновления помощника; false для сообщений пользователя, завершенного помощника и восстановленных сообщений.
  • availableWidth — точные терминальные столбцы, доступные для преобразованного содержимого Markdown.

Верните преобразованный Markdown:

pi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {
  if (isStreaming || messageType === "assistant-thinking") return markdown;
  return markdown.replaceAll("-->", "→");
});

Если преобразователь выбрасывает, Pi сохраняет созданный на данный момент Markdown и продолжает работу со следующим преобразователем. Перехват предназначен только для отображения: исходное сообщение остается неизменным в контексте сеанса и модели. Он запускается для новых пользовательских сообщений, обновлений потоковой передачи помощника, восстановленных сообщений сеанса и изменений ширины терминала, поэтому преобразователи должны оставаться синхронными и недорогими.

pi.registerEntryRenderer(customType, средство визуализации)

Зарегистрируйте пользовательский рендерер TUI для пользовательских записей с помощью customType. Пользовательские записи создаются с помощью pi.appendEntry() и не участвуют в контексте LLM.

import { Box, Text } from "@earendil-works/pi-tui";

pi.registerEntryRenderer("status-card", (entry, { expanded }, theme) => {
  const data = entry.data as { title: string; count: number };
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
  box.addChild(new Text(`${theme.bold(data.title)}: ${data.count}`));
  if (expanded) {
    box.addChild(new Text(theme.fg("dim", JSON.stringify(data, null, 2))));
  }
  return box;
});

pi.appendEntry("status-card", { title: "Indexed files", count: 17 });

pi.registerShortcut(ярлык, параметры)

Зарегистрируйте сочетание клавиш. См. keybindings.md, чтобы узнать о формате ярлыков и встроенных сочетаниях клавиш.

pi.registerShortcut("ctrl+shift+p", {
  description: "Toggle plan mode",
  handler: async (ctx) => {
    ctx.ui.notify("Toggled!");
  },
});

pi.registerFlag(имя, параметры)

Зарегистрируйте флаг CLI.

pi.registerFlag("plan", {
  description: "Start in plan mode",
  type: "boolean",
  default: false,
});

// Check value
if (pi.getFlag("plan")) {
  // Plan mode enabled
}

pi.exec(команда, аргументы, параметры?)

Выполните команду оболочки.

const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
// result.stdout, result.stderr, result.code, result.killed

pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(имена)

Управляйте активными инструментами. Это работает как для встроенных инструментов, так и для динамически зарегистрированных инструментов. pi.getActiveTools() возвращает имена активных инструментов как string[]; pi.getAllTools() возвращает метаданные для всех настроенных инструментов.

const active = pi.getActiveTools(); // ["read", "bash", ...]
const all = pi.getAllTools();
// all = [{
//   name: "read",
//   description: "Read file contents...",
//   parameters: ...,
//   promptGuidelines: ["Use read to examine files instead of cat or sed."],
//   sourceInfo: { path: "<builtin:read>", source: "builtin", scope: "temporary", origin: "top-level" }
// }, ...]
const builtinTools = all.filter((t) => t.sourceInfo.source === "builtin");
const extensionTools = all.filter((t) => t.sourceInfo.source !== "builtin" && t.sourceInfo.source !== "sdk");
pi.setActiveTools([...new Set([...active, "my_custom_tool"])]); // Keep current tools and enable my_custom_tool
pi.setActiveTools(["read", "bash"]); // Switch to read-only

pi.getAllTools() возвращает name, description, parameters, promptGuidelines и sourceInfo.

Типичные значения sourceInfo.source:

  • builtin для встроенных инструментов
  • sdk для инструментов, прошедших через createAgentSession({ customTools })
  • метаданные источника расширения для инструментов, зарегистрированных расширениями

pi.setModel(модель)

Установите текущую модель. Возвращает false, если для модели нет API key. См. models.md для настройки пользовательских моделей.

const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5");
if (model) {
  const success = await pi.setModel(model);
  if (!success) {
    ctx.ui.notify("No API key for this model", "error");
  }
}

pi.getThinkingLevel()/pi.setThinkingLevel(уровень)

Получите или установите уровень мышления. Уровень привязан к возможностям модели (в моделях без рассуждений всегда используется значение «выкл.»). Изменения излучают thinking_level_select.

const current = pi.getThinkingLevel();  // "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
pi.setThinkingLevel("high");

pi.events

Общая шина событий для связи между расширениями:

pi.events.on("my:event", (data) => { ... });
pi.events.emit("my:event", { ... });

pi.registerProvider(имя, конфигурация)

Зарегистрируйте или переопределите поставщика модели динамически. Полезно для прокси, пользовательских конечных точек или конфигураций модели для всей команды.

Вызовы, сделанные во время функции фабрики расширений, ставятся в очередь и применяются после инициализации бегуна. Вызовы, сделанные после этого — например, из обработчика команд после потока настройки пользователя — вступают в силу немедленно, не требуя /reload.

Динамические поставщики могут реализовать refreshModels. Pi вызывает его во время обновления модели, синхронно публикует возвращенный список через поставщика и передает контекст канонических учетных данных/сохраненного каталога/сети/сигнала. Расширение решает, сохранять ли метаданные каталога через проверку генерации context.publish({ persist: entry }); живые серверы, такие как llama.cpp, могут возвращать модели, не сохраняя их.

context.signal всегда является конкретным сигналом, и обратные вызовы провайдера должны передать его для блокировки ввода-вывода. Публичные вызовы ModelRuntime.refresh() и ModelRegistry.refresh() принимают необязательный сигнал и не ограничиваются, если он опущен; расширения и приложения сами выбирают сроки. Отмена останавливает ожидание вызывающего абонента, даже если провайдер игнорирует сигнал, но сотрудничество все равно необходимо, чтобы остановить основную работу.

Extensions, которым требуется встроенная аутентификация поставщика, фильтрация, обновление или потоковая передача, могут зарегистрировать полный Provider из @earendil-works/pi-ai. Поставщик становится базой композиции, и над ним по-прежнему применяются переопределения models.json.

import { createProvider, openAICompletionsApi } from "@earendil-works/pi-ai";

const provider = createProvider({
  id: "local-server",
  name: "Local Server",
  baseUrl: "http://localhost:8080/v1",
  auth: {
    apiKey: {
      name: "Local server setup",
      async login(interaction) {
        return {
          type: "api_key",
          key: await interaction.prompt({ type: "secret", message: "API key" }),
        };
      },
      async resolve({ credential }) {
        return credential?.key
          ? { auth: { apiKey: credential.key }, source: "stored API key" }
          : undefined;
      },
    },
  },
  models: [],
  api: openAICompletionsApi(),
});

pi.registerProvider(provider);

// Register a new provider with custom models
pi.registerProvider("my-proxy", {
  name: "My Proxy",
  baseUrl: "https://proxy.example.com",
  apiKey: "$PROXY_API_KEY",  // env var reference
  api: "anthropic-messages",
  models: [
    {
      id: "claude-sonnet-4-20250514",
      name: "Claude 4 Sonnet (proxy)",
      reasoning: false,
      input: ["text", "image"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: 200000,
      maxTokens: 16384
    }
  ]
});

// Register a live llama.cpp catalog without persisting discovered models
pi.registerProvider("llama.cpp", {
  baseUrl: "http://localhost:8080/v1",
  apiKey: "local",
  api: "openai-completions",
  async refreshModels({ signal }) {
    const response = await fetch("http://localhost:8080/v1/models", { signal });
    const { data } = await response.json();
    return data.map(({ id }) => ({
      id,
      name: id,
      reasoning: false,
      input: ["text"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: 128000,
      maxTokens: 16384
    }));
  }
});

// Override baseUrl for an existing provider (keeps all models)
pi.registerProvider("anthropic", {
  baseUrl: "https://proxy.example.com"
});

// Register provider with OAuth support for /login
pi.registerProvider("corporate-ai", {
  baseUrl: "https://ai.corp.com",
  api: "openai-responses",
  models: [...],
  oauth: {
    name: "Corporate AI (SSO)",
    async login(callbacks) {
      // Custom OAuth flow
      callbacks.onAuth({ url: "https://sso.corp.com/..." });
      const code = await callbacks.onPrompt({ message: "Enter code:" });
      return { refresh: code, access: code, expires: Date.now() + 3600000 };
    },
    async refreshToken(credentials, signal) {
      signal.throwIfAborted();
      // Refresh logic
      return credentials;
    },
    getApiKey(credentials) {
      return credentials.access;
    }
  }
});

Форма объекта принимает полное пи-ай Provider, включая собственное поведение auth, getModels, refreshModels, filterModels, stream и streamSimple.

Устаревшие параметры конфигурации:

  • name — отображаемое имя поставщика в пользовательском интерфейсе, например /login.
  • baseUrl – API URL-адрес конечной точки. Требуется при определении моделей.
  • apiKey - API key буквальный, интерполяция среды ($ENV_VAR или ${ENV_VAR}) или ведущий !command. Требуется при определении моделей (если не указано oauth). $ экранирует ``apiKey - API key буквальный, интерполяция среды ($ENV_VARили${ENV_VAR}) или ведущий !command. Требуется при определении моделей (если не указано oauth). $экранирует, а$!экранирует литерал!`, не запуская выполнение команды.
  • Тип api - API: "anthropic-messages", "openai-completions", "openai-responses" и т. д.
  • headers — Пользовательские заголовки для включения в запросы.
  • authHeader — если true, автоматически добавляет заголовок Authorization: Bearer.
  • models — Массив определений модели. Если предусмотрено, заменяет все существующие модели этого поставщика. В определениях моделей можно установить baseUrl, чтобы переопределить конечную точку поставщика для этой модели.
  • refreshModels — обратный вызов асинхронного динамического обнаружения. Возвращенные модели заменяют модели, предоставленные расширениями. context.stored содержит сохраненный снимок поставщика; используйте context.publish({ persist: entry }) с проверкой генерации только в том случае, если обновленные данные каталога должны сохраниться. Используйте persist: null, чтобы удалить этот снимок.
  • Конфигурация провайдера oauth - OAuth для поддержки /login. Если этот параметр предоставлен, поставщик появится в меню входа в систему.
  • streamSimple — Пользовательская реализация потоковой передачи для нестандартных API.

См. custom-provider.md для более сложных тем: пользовательская потоковая передача APIs, OAuth подробности, справочник по определению модели.

pi.unregisterProvider(имя)

Удалить ранее зарегистрированного провайдера и его модели. Встроенные модели, которые были переопределены поставщиком, восстанавливаются. Не имеет эффекта, если провайдер не был зарегистрирован.

Как и registerProvider, это вступает в силу немедленно при вызове после начальной фазы загрузки, поэтому /reload не требуется.

pi.registerCommand("my-setup-teardown", {
  description: "Remove the custom proxy provider",
  handler: async (_args, _ctx) => {
    pi.unregisterProvider("my-proxy");
  },
});

Государственное управление

Extensions с состоянием следует сохранить его в результате инструмента details для правильной поддержки ветвления:

export default function (pi: ExtensionAPI) {
  let items: string[] = [];

  // Reconstruct state from session
  pi.on("session_start", async (_event, ctx) => {
    items = [];
    for (const entry of ctx.sessionManager.getBranch()) {
      if (entry.type === "message" && entry.message.role === "toolResult") {
        if (entry.message.toolName === "my_tool") {
          items = entry.message.details?.items ?? [];
        }
      }
    }
  });

  pi.registerTool({
    name: "my_tool",
    // ...
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      items.push("new item");
      return {
        content: [{ type: "text", text: "Added" }],
        details: { items: [...items] },  // Store for reconstruction
      };
    },
  });
}

Пользовательские инструменты

Зарегистрируйте инструменты, которые LLM может вызывать через pi.registerTool(). Инструменты отображаются в системной подсказке и могут иметь собственную визуализацию.

Используйте promptSnippet для короткой однострочной записи в разделе Available tools системной подсказки по умолчанию. Если этот параметр опущен, пользовательские инструменты не попадают в этот раздел.

Используйте promptGuidelines, чтобы добавить маркеры для конкретного инструмента в раздел системной подсказки по умолчанию Guidelines. Эти маркеры включаются только тогда, когда инструмент активен (например, после pi.setActiveTools([...])).

Важно! Маркеры promptGuidelines добавляются в раздел Guidelines ровно, без префикса имени инструмента или группировки. В каждом руководстве должен быть указан инструмент, к которому он относится. Избегайте фразы «Используйте этот инструмент, когда...», поскольку LLM не может определить, какой инструмент означает «это». Вместо этого напишите «Использовать my_tool, когда...».

Примечание. Некоторые модели являются идиотами и включают префикс @ в аргументы пути к инструменту. Встроенные инструменты удаляют начальный символ @ перед разрешением путей. Если ваш пользовательский инструмент принимает путь, нормализуйте также начальный символ @.

Если ваш пользовательский инструмент изменяет файлы, используйте withFileMutationQueue(), чтобы он участвовал в той же очереди для каждого файла, что и встроенные edit и write. Это важно, поскольку вызовы инструментов по умолчанию выполняются параллельно. Без очереди два инструмента могут читать одно и то же старое содержимое файла, вычислять разные обновления, а затем в зависимости от того, какая запись произошла последней, перезаписывает другую.

Пример случая сбоя: ваш пользовательский инструмент редактирует foo.ts, а встроенный edit также изменяет foo.ts за один и тот же ход помощника. Если ваш инструмент не участвует в очереди, оба могут прочитать оригинал foo.ts, применить отдельные изменения, и одно из этих изменений будет потеряно.

Передайте реальный путь к целевому файлу в withFileMutationQueue(), а не необработанный аргумент пользователя. Сначала разрешите его в абсолютный путь относительно ctx.cwd или рабочего каталога вашего инструмента. Для существующих файлов помощник канонизируется через realpath(), поэтому псевдонимы символических ссылок для одного и того же файла используют одну очередь. Для новых файлов используется разрешенный абсолютный путь, поскольку в realpath() пока ничего нет.

Поставьте в очередь все окно мутации на этом целевом пути. Это включает в себя логику чтения-изменения-записи, а не только окончательную запись.

import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";

async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
  const absolutePath = resolve(ctx.cwd, params.path);

  return withFileMutationQueue(absolutePath, async () => {
    await mkdir(dirname(absolutePath), { recursive: true });
    const current = await readFile(absolutePath, "utf8");
    const next = current.replace(params.oldText, params.newText);
    await writeFile(absolutePath, next, "utf8");

    return {
      content: [{ type: "text", text: `Updated ${params.path}` }],
      details: {},
    };
  });
}

Определение инструмента

import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
import { Text } from "@earendil-works/pi-tui";

pi.registerTool({
  name: "my_tool",
  label: "My Tool",
  description: "What this tool does (shown to LLM)",
  promptSnippet: "List or add items in the project todo list",
  promptGuidelines: [
    "Use my_tool for todo planning instead of direct file edits when the user asks for a task list."
  ],
  parameters: Type.Object({
    action: StringEnum(["list", "add"] as const),  // Use StringEnum for Google compatibility
    text: Type.Optional(Type.String()),
  }),
  prepareArguments(args) {
    if (!args || typeof args !== "object") return args;
    const input = args as { action?: string; oldAction?: string };
    if (typeof input.oldAction === "string" && input.action === undefined) {
      return { ...input, action: input.oldAction };
    }
    return args;
  },

  async execute(toolCallId, params, signal, onUpdate, ctx) {
    // Check for cancellation
    if (signal?.aborted) {
      return { content: [{ type: "text", text: "Cancelled" }] };
    }

    // Stream progress updates
    onUpdate?.({
      content: [{ type: "text", text: "Working..." }],
      details: { progress: 50 },
    });

    // Run commands via pi.exec (captured from extension closure)
    const result = await pi.exec("some-command", [], { signal });

    // Return result
    return {
      content: [{ type: "text", text: "Done" }],  // Sent to LLM
      details: { data: result },                   // For rendering & state
      // usage: nestedModelResponse.usage,          // Optional nested LLM usage
      // Optional: stop after this tool batch when every finalized tool result
      // in the batch also returns terminate: true.
      terminate: true,
    };
  },

  // Optional: Custom rendering
  renderCall(args, theme, context) { ... },
  renderResult(result, options, theme, context) { ... },
});

Учет использования. Если инструмент выполняет вложенные вызовы LLM, верните их совокупный Usage как usage. Pi сохраняет его в результатах инструмента и включает в нижний колонтитул, /session и RPC итоговые данные сеанса. tool_result обработчики могут проверять или заменять это значение.

Сигнализация ошибок: Чтобы пометить выполнение инструмента как неудачное (устанавливает isError: true для результата и сообщает об этом в LLM), выдайте ошибку из execute. При возврате значения никогда не устанавливается флаг ошибки, независимо от того, какие свойства вы включаете в возвращаемый объект.

Досрочное прекращение: Возврат terminate: true из execute(), чтобы указать, что автоматический последующий вызов LLM следует пропустить после текущей партии инструментов. Это вступает в силу только тогда, когда каждый завершенный результат инструмента в этом пакете завершается. См. examples/extensions/structured-output.ts минимальный пример, когда агент завершает работу при последнем вызове инструмента структурированного вывода.

// Correct: throw to signal an error
async execute(toolCallId, params) {
  if (!isValid(params.input)) {
    throw new Error(`Invalid input: ${params.input}`);
  }
  return { content: [{ type: "text", text: "OK" }], details: {} };
}

Важно! Используйте StringEnum из @earendil-works/pi-ai для перечисления строк. Type.Union/Type.Literal не работает с API от Google.

Подготовка аргумента: prepareArguments(args) не является обязательным. Если определено, оно выполняется до проверки схемы и до execute(). Используйте его, чтобы имитировать более старую принятую форму ввода, когда pi возобновляет старый сеанс, чьи сохраненные аргументы вызова инструмента больше не соответствуют текущей схеме. Верните объект, который вы хотите проверить на соответствие parameters. Соблюдайте строгую публичную схему. Не добавляйте устаревшие поля совместимости в parameters только для того, чтобы старые возобновленные сеансы работали.

Пример: более старый сеанс может содержать вызов инструмента edit с oldText и newText верхнего уровня, в то время как текущая схема принимает только edits: [{ oldText, newText }].

pi.registerTool({
  name: "edit",
  label: "Edit",
  description: "Edit a single file using exact text replacement",
  parameters: Type.Object({
    path: Type.String(),
    edits: Type.Array(
      Type.Object({
        oldText: Type.String(),
        newText: Type.String(),
      }),
    ),
  }),
  prepareArguments(args) {
    if (!args || typeof args !== "object") return args;

    const input = args as {
      path?: string;
      edits?: Array<{ oldText: string; newText: string }>;
      oldText?: unknown;
      newText?: unknown;
    };

    if (typeof input.oldText !== "string" || typeof input.newText !== "string") {
      return args;
    }

    return {
      ...input,
      edits: [...(input.edits ?? []), { oldText: input.oldText, newText: input.newText }],
    };
  },
  async execute(toolCallId, params, signal, onUpdate, ctx) {
    // params now matches the current schema
    return {
      content: [{ type: "text", text: `Applying ${params.edits.length} edit block(s)` }],
      details: {},
    };
  },
});

Переопределение встроенных инструментов

Extensions может переопределить встроенные инструменты (read, bash, edit, write, grep, find, ls), зарегистрировав инструмент с тем же именем. В интерактивном режиме отображается предупреждение, когда это происходит.

# Extension's read tool replaces built-in read
pi -e ./tool-override.ts

Альтернативно, используйте --no-builtin-tools, чтобы начать без каких-либо встроенных инструментов, оставив при этом инструменты расширения включенными:

# No built-in tools, only extension tools
pi --no-builtin-tools -e ./my-extension.ts

См. examples/extensions/tool-override.ts полный пример, который переопределяет read с помощью ведения журнала и контроля доступа.

Рендеринг. Наследование встроенного средства рендеринга осуществляется для каждого слота. Переопределение выполнения и переопределение рендеринга независимы. Если в вашем переопределении отсутствует renderCall, используется встроенный renderCall. Если в вашем переопределении отсутствует renderResult, используется встроенный renderResult. Если в вашем переопределении оба параметра отсутствуют, автоматически используется встроенный модуль визуализации (подсветка синтаксиса, различия и т. д.). Это позволяет использовать встроенные инструменты для ведения журналов или контроля доступа без переопределения пользовательского интерфейса.

Метаданные подсказки: promptSnippet и promptGuidelines не наследуются от встроенного инструмента. Если ваше переопределение должно сохранять эти подсказки, определите их в переопределении явно.

Ваша реализация должна точно соответствовать форме результата, включая тип details. Логика пользовательского интерфейса и сеанса зависит от этих фигур для рендеринга и отслеживания состояния.

Встроенные реализации инструментов:

Удаленное выполнение

Встроенные инструменты поддерживают подключаемые операции для делегирования удаленным системам (SSH, контейнерам и т. д.):

import { createReadTool, createBashTool, type ReadOperations } from "@earendil-works/pi-coding-agent";

// Create tool with custom operations
const remoteRead = createReadTool(cwd, {
  operations: {
    readFile: (path) => sshExec(remote, `cat ${path}`),
    access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),
  }
});

// Register, checking flag at execution time
pi.registerTool({
  ...remoteRead,
  async execute(id, params, signal, onUpdate, _ctx) {
    const ssh = getSshConfig();
    if (ssh) {
      const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });
      return tool.execute(id, params, signal, onUpdate);
    }
    return localRead.execute(id, params, signal, onUpdate);
  },
});

Операционные интерфейсы: ReadOperations, WriteOperations, EditOperations, BashOperations, LsOperations, GrepOperations, FindOperations

Для user_bash расширения могут повторно использовать локальную серверную часть оболочки pi через createLocalBashOperations() вместо повторной реализации создания локальных процессов, разрешения оболочки и завершения дерева процессов.

Инструмент bash также поддерживает перехватчик создания для настройки команды, cwd или env перед выполнением:

import { createBashTool } from "@earendil-works/pi-coding-agent";

const bashTool = createBashTool(cwd, {
  spawnHook: ({ command, cwd, env }) => ({
    command: `source ~/.profile\n${command}`,
    cwd: `/mnt/sandbox${cwd}`,
    env: { ...env, CI: "1" },
  }),
});

createBashTool() предоставляет текущий сеанс командам через PI_SESSION_ID, PI_SESSION_FILE, PI_PROVIDER, PI_MODEL и PI_REASONING_LEVEL. Внедрение происходит до spawnHook, поэтому перехватчики получают эти значения в env и сохраняют их при распространении существующей среды, как указано выше. Установите exposeSessionEnvironment: false, чтобы отключить их:

const bashTool = createBashTool(cwd, {
  exposeSessionEnvironment: false,
});

См. Bash tool session environment для семантики переменных. См. examples/extensions/ssh.ts полный пример SSH с флагом --ssh.

Усечение вывода

Инструменты ДОЛЖНЫ обрезать свои выходные данные, чтобы не перегружать контекст LLM. Большие выходные данные могут вызвать:

  • Ошибки переполнения контекста (слишком длинный запрос)
  • Неудачи уплотнения
  • Ухудшение производительности модели

Встроенный лимит составляет 50 КБ (около 10 000 токенов) и 2000 строк, в зависимости от того, что наступит раньше. Используйте экспортированные утилиты усечения:

import {
  truncateHead,      // Keep first N lines/bytes (good for file reads, search results)
  truncateTail,      // Keep last N lines/bytes (good for logs, command output)
  truncateLine,      // Truncate a single line to maxBytes with ellipsis
  formatSize,        // Human-readable size (e.g., "50KB", "1.5MB")
  DEFAULT_MAX_BYTES, // 50KB
  DEFAULT_MAX_LINES, // 2000
} from "@earendil-works/pi-coding-agent";

async execute(toolCallId, params, signal, onUpdate, ctx) {
  const output = await runCommand();

  // Apply truncation
  const truncation = truncateHead(output, {
    maxLines: DEFAULT_MAX_LINES,
    maxBytes: DEFAULT_MAX_BYTES,
  });

  let result = truncation.content;

  if (truncation.truncated) {
    // Write full output to temp file
    const tempFile = writeTempFile(output);

    // Inform the LLM where to find complete output
    result += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
    result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
    result += ` Full output saved to: ${tempFile}]`;
  }

  return { content: [{ type: "text", text: result }] };
}

Ключевые моменты:

  • Используйте truncateHead для контента, начало которого имеет значение (результаты поиска, чтение файлов).
  • Используйте truncateTail для контента, для которого важен конец (журналы, вывод команды)
  • Всегда сообщайте LLM, когда выходные данные обрезаются и где найти полную версию.
  • Задокументируйте пределы усечения в описании вашего инструмента.

См. examples/extensions/truncated-tool.ts для полного примера упаковки rg (ripgrep) с правильным усечением.

Несколько инструментов

Одно расширение может зарегистрировать несколько инструментов с общим состоянием:

export default function (pi: ExtensionAPI) {
  let connection = null;

  pi.registerTool({ name: "db_connect", ... });
  pi.registerTool({ name: "db_query", ... });
  pi.registerTool({ name: "db_close", ... });

  pi.on("session_shutdown", async () => {
    connection?.close();
  });
}

Пользовательский рендеринг

Инструменты могут предоставлять renderCall и renderResult для пользовательского отображения TUI. См. tui.md для полного компонента, API и tool-execution.ts для описания того, как составляются ряды инструментов.

По умолчанию выходные данные инструмента заключаются в Box, который обрабатывает отступы и фон. Определенный renderCall или renderResult должен возвращать Component. Если средство рендеринга слота не определено, tool-execution.ts использует резервный рендеринг для этого слота.

Установите renderShell: "self", когда инструмент должен отображать собственную оболочку вместо использования Box по умолчанию. Это полезно для инструментов, которым требуется полный контроль над кадрированием или поведением фона, например, для больших изображений предварительного просмотра, которые должны оставаться визуально стабильными после стабилизации инструмента.

pi.registerTool({
  name: "my_tool",
  label: "My Tool",
  description: "Custom shell example",
  parameters: Type.Object({}),
  renderShell: "self",
  async execute() {
    return { content: [{ type: "text", text: "ok" }], details: undefined };
  },
  renderCall(args, theme, context) {
    return new Text(theme.fg("accent", "my custom shell"), 0, 0);
  },
});

renderCall и renderResult каждый получает объект context с:

  • args - текущие аргументы вызова инструмента
  • state — общее локальное состояние строки для renderCall и renderResult
  • lastComponent — ранее возвращенный компонент для этого слота, если таковой имеется.
  • invalidate() — запросить повторную визуализацию этой строки инструмента.
  • toolCallId, cwd, executionStarted, argsComplete, isPartial, expanded, showImages, isError

Используйте context.state для общего состояния между слотами. Сохраняйте локальные кэши в возвращаемом экземпляре компонента, если вы хотите повторно использовать и изменять один и тот же компонент при рендеринге.

рендерколл

Отображает вызов инструмента или заголовок:

import { Text } from "@earendil-works/pi-tui";

renderCall(args, theme, context) {
  const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
  let content = theme.fg("toolTitle", theme.bold("my_tool "));
  content += theme.fg("muted", args.action);
  if (args.text) {
    content += " " + theme.fg("dim", `"${args.text}"`);
  }
  text.setText(content);
  return text;
}

рендерРезультат

Отображает результат или выходные данные инструмента:

renderResult(result, { expanded, isPartial }, theme, context) {
  if (isPartial) {
    return new Text(theme.fg("warning", "Processing..."), 0, 0);
  }

  if (result.details?.error) {
    return new Text(theme.fg("error", `Error: ${result.details.error}`), 0, 0);
  }

  let text = theme.fg("success", "✓ Done");
  if (expanded && result.details?.items) {
    for (const item of result.details.items) {
      text += "\n  " + theme.fg("dim", item);
    }
  }
  return new Text(text, 0, 0);
}

Если слот намеренно не имеет видимого содержимого, верните пустой Component, например пустой Container.

Подсказки по сочетанию клавиш

Используйте keyHint() для отображения подсказок по привязке клавиш, которые соответствуют активной конфигурации привязки клавиш:

import { keyHint } from "@earendil-works/pi-coding-agent";

renderResult(result, { expanded }, theme, context) {
  let text = theme.fg("success", "✓ Done");
  if (!expanded) {
    text += ` (${keyHint("app.tools.expand", "to expand")})`;
  }
  return new Text(text, 0, 0);
}

Доступные функции:

  • keyHint(keybinding, description) — форматирует настроенный идентификатор привязки клавиш, например "app.tools.expand" или "tui.select.confirm".
  • keyText(keybinding) — возвращает необработанный настроенный текст ключа для идентификатора привязки клавиш.
  • rawKeyHint(key, description) — форматировать необработанную строку ключа.

Используйте идентификаторы привязки клавиш в пространстве имен:

  • Идентификаторы агентов кодирования используют пространство имен app.*, например app.tools.expand, app.editor.external, app.session.rename.
  • Общие идентификаторы TUI используют пространство имен tui.*, например tui.select.confirm, tui.select.cancel, tui.input.tab.

Исчерпывающий список идентификаторов привязок клавиш и значений по умолчанию см. в разделе keybindings.md. keybindings.json использует те же идентификаторы пространства имен.

Пользовательские редакторы и компоненты ctx.ui.custom() получают keybindings: KeybindingsManager в качестве введенного аргумента. Им следует использовать этот внедренный менеджер напрямую, а не вызывать getKeybindings() или setKeybindings().

Лучшие практики

  • Используйте Text с дополнением (0, 0). По умолчанию Box обрабатывает отступы.
  • Используйте \n для многострочного контента.
  • Дескриптор isPartial для потоковой передачи прогресса.
  • Поддержка expanded для получения подробной информации по запросу.
  • Сохраняйте компактный вид по умолчанию.
  • Прочитайте context.args в renderResult вместо копирования аргументов в context.state.
  • Используйте context.state только для данных, которые должны быть разделены между слотами вызовов и результатов.
  • Повторно используйте context.lastComponent, если тот же экземпляр компонента можно обновить на месте.
  • Используйте renderShell: "self" только тогда, когда вам мешает коробочная оболочка по умолчанию. В режиме собственной оболочки инструмент отвечает за собственное кадрирование, отступы и фон.

Отступать

Если средство рендеринга слотов не определено или выдает:

  • renderCall: показывает имя инструмента.
  • renderResult: показывает необработанный текст из content.

Динамическая загрузка инструмента

Extensions может зарегистрировать множество инструментов, оставляя активным только небольшой начальный набор. Затем инструмент может добавлять дополнительные инструменты с помощью pi.setActiveTools() во время выполнения. Pi обнаруживает чисто аддитивные изменения, записывает новые доступные имена инструментов в результат этого инструмента и применяет обновленный активный набор перед следующим запросом модели.

Это работает с каждой моделью. Models со встроенной поддержкой отложенной загрузки сохраняет стабильный префикс подсказки и загружает новые определения в позицию результата инструмента. Другие модели используют резервный вариант, описанный ниже.

Жизненный цикл:

  1. Зарегистрируйте каждый инструмент с помощью pi.registerTool(), чтобы он появился в pi.getAllTools().
  2. Оставьте инструменты загрузчика, такие как search_tools, активными, а инструменты с возможностью поиска оставьте неактивными.
  3. Во время выполнения загрузчика вызовите pi.setActiveTools([...currentTools,...matchingTools]). Изменение должно быть аддитивным: не удаляйте активные в данный момент инструменты в одном вызове.
  4. Pi записывает, какие инструменты были добавлены в результат инструмента загрузчика.
  5. Перед следующим ответом модели Pi предоставляет добавленные определения с использованием встроенной отложенной загрузки, если она поддерживается, или обычного активного списка инструментов в противном случае.

Вам не нужно возвращать ссылки на инструменты конкретного поставщика или отмечать загрузчик как специальный инструмент поиска. Смена активного инструмента является сигналом. Имена, переданные в pi.setActiveTools(), уже должны быть зарегистрированы; неизвестные имена игнорируются.

Models со встроенной отложенной загрузкой

  • Антропный
    • Models: Sonnet, Opus, Fable версии 4.5 или новее (без Haiku)
    • Собственное представление: В отложенных определениях используется defer_loading; точка загрузки использует контент tool_reference.
  • Открытый AI
    • Models: gpt-5.4 и более новая семья
    • Встроенное представление: Pi добавляет завершенные клиентские элементы tool_search_call и tool_search_output в точке загрузки.

Для проверенной пользовательской модели или прокси-сервера встроенную обработку можно включить с помощью compat.supportsToolReferences: true для anthropic-messages или compat.supportsToolSearch: true для openai-responses и openai-codex-responses. Оставьте их отключенными, если конечная точка и модель не принимают соответствующий собственный протокол.

Резервное поведение

Для всех других моделей и поставщиков динамическая активация по-прежнему работает: Pi обычно отправляет полный текущий список активных инструментов при следующем запросе. Модель может вызывать недавно активированные инструменты, но добавление их определений может сделать недействительным префикс кэшированного приглашения поставщика.

Pi также использует этот безопасный запасной вариант, когда активный набор не является чисто аддитивным, например, при замене одной группы инструментов другой. Таким образом, удаление инструментов работает, но не использует отложенную загрузку.

Для обеспечения наилучшего поведения кэша оставляйте инструмент загрузчика активным на протяжении всего сеанса и добавляйте инструменты вместо замены активного набора. Также обратите внимание, что активация инструмента с помощью promptSnippet или promptGuidelines перестраивает системное приглашение; такое изменение системного запроса может сделать префикс недействительным, даже если поставщик поддерживает отложенные схемы. Лениво загружаемые инструменты обычно должны полагаться на свой инструмент description и опускать метаданные подсказок только для активных действий.

Пример инструмента поиска

Следующее расширение регистрирует два инструмента с возможностью поиска, удаляет их из исходного активного набора и сохраняет только search_tools в качестве их загрузчика. В примере используется простое сопоставление ключевых слов, но реализация поиска может использовать BM25, внедрения, удаленный каталог или маршрутизацию для конкретного проекта.

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

const SEARCHABLE_TOOL_NAMES = new Set(["lookup_weather", "search_issues"]);

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "lookup_weather",
    label: "Lookup Weather",
    description: "Look up the current weather for a city",
    parameters: Type.Object({ city: Type.String() }),
    async execute(_toolCallId, params) {
      return {
        content: [{ type: "text", text: `Weather for ${params.city}: sunny` }],
        details: {},
      };
    },
  });

  pi.registerTool({
    name: "search_issues",
    label: "Search Issues",
    description: "Search project issues by keyword",
    parameters: Type.Object({ query: Type.String() }),
    async execute(_toolCallId, params) {
      return {
        content: [{ type: "text", text: `No open issues matching ${params.query}` }],
        details: {},
      };
    },
  });

  pi.registerTool({
    name: "search_tools",
    label: "Search Tools",
    description: "Search for and enable tools relevant to a task",
    promptSnippet: "Search for additional tools when the active tools cannot perform the task",
    promptGuidelines: [
      "Use search_tools when a task requires a capability that is not currently available.",
    ],
    parameters: Type.Object({
      query: Type.String({ description: "Capability or task to search for" }),
      limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),
    }),
    async execute(_toolCallId, params) {
      const terms = params.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
      const matches = pi.getAllTools()
        .filter((tool) => SEARCHABLE_TOOL_NAMES.has(tool.name))
        .map((tool) => ({
          tool,
          score: terms.reduce(
            (score, term) =>
              score + (`${tool.name} ${tool.description}`.toLowerCase().includes(term) ? 1 : 0),
            0,
          ),
        }))
        .filter((match) => match.score > 0)
        .sort((a, b) => b.score - a.score)
        .slice(0, params.limit ?? 3)
        .map((match) => match.tool.name);

      if (matches.length === 0) {
        return {
          content: [{ type: "text", text: `No tools found for: ${params.query}` }],
          details: { matches: [] },
        };
      }

      const active = pi.getActiveTools();
      const added = matches.filter((name) => !active.includes(name));
      pi.setActiveTools([...new Set([...active, ...added])]);

      return {
        content: [{
          type: "text",
          text: added.length > 0
            ? `Loaded tools: ${added.join(", ")}`
            : `Matching tools already active: ${matches.join(", ")}`,
        }],
        details: { matches, added },
      };
    },
  });

  pi.on("session_start", () => {
    // Keep searchable tools registered but initially inactive. Preserve built-ins
    // and tools owned by other extensions, and keep the loader itself active.
    const initialTools = pi.getActiveTools().filter(
      (name) => !SEARCHABLE_TOOL_NAMES.has(name),
    );
    pi.setActiveTools([...new Set([...initialTools, "search_tools"])]);
  });
}

Когда search_tools добавляет соответствие, модель получает это определение при следующем запросе. В модели, поддерживающей встроенные функции, определение привязывается после результата поиска без изменения исходного префикса схемы инструмента. На других моделях он появляется в обычном списке инструментов по тому же следующему запросу.

Пользовательский интерфейс

Extensions может взаимодействовать с пользователями с помощью методов ctx.ui и настраивать способ отображения сообщений/инструментов.

Информацию о пользовательских компонентах см. в разделе tui.md, где есть шаблоны копирования и вставки для:

  • Диалоги выбора (SelectList)
  • Асинхронные операции с отменой (BorderedLoader)
  • Переключатели настроек (SettingsList)
  • Индикаторы состояния (setStatus)
  • Рабочее сообщение, видимость и индикатор во время потоковой передачи (setWorkingMessage, setWorkingVisible, setWorkingIndicator)
  • Виджеты над/под редактором (setWidget)
  • Поставщики автозаполнения, расположенные поверх встроенного завершения косой черты/пути (addAutocompleteProvider)
  • Пользовательские нижние колонтитулы (setFooter)

Диалоги

// Select from options
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);

// Confirm dialog
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");

// Text input
const name = await ctx.ui.input("Name:", "placeholder");

// Multi-line editor
const text = await ctx.ui.editor("Edit:", "prefilled text");

// Notification (non-blocking)
ctx.ui.notify("Done!", "info");  // "info" | "warning" | "error"

Диалоги по времени с обратным отсчетом

Диалоги поддерживают опцию timeout, которая автоматически закрывается с отображением обратного отсчета в реальном времени:

// Dialog shows "Title (5s)" → "Title (4s)" → ... → auto-dismisses at 0
const confirmed = await ctx.ui.confirm(
  "Timed Confirmation",
  "This dialog will auto-cancel in 5 seconds. Confirm?",
  { timeout: 5000 }
);

if (confirmed) {
  // User confirmed
} else {
  // User cancelled or timed out
}

Возвращаемые значения по таймауту:

  • select() возвращает undefined
  • confirm() возвращает false
  • input() возвращает undefined

Ручное увольнение с помощью AbortSignal

Для большего контроля (например, чтобы отличить тайм-аут от отмены пользователем) используйте AbortSignal:

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

const confirmed = await ctx.ui.confirm(
  "Timed Confirmation",
  "This dialog will auto-cancel in 5 seconds. Confirm?",
  { signal: controller.signal }
);

clearTimeout(timeoutId);

if (confirmed) {
  // User confirmed
} else if (controller.signal.aborted) {
  // Dialog timed out
} else {
  // User cancelled (pressed Escape or selected "No")
}

См. examples/extensions/timed-confirm.ts полные примеры.

Виджеты, статус и нижний колонтитул

// Status in footer (persistent until cleared)
ctx.ui.setStatus("my-ext", "Processing...");
ctx.ui.setStatus("my-ext", undefined);  // Clear

// Working loader (shown during streaming)
ctx.ui.setWorkingMessage("Thinking deeply...");
ctx.ui.setWorkingMessage();  // Restore default
ctx.ui.setWorkingVisible(false);  // Hide the built-in working loader row entirely
ctx.ui.setWorkingVisible(true);   // Show the built-in working loader row

// Working indicator (shown during streaming)
ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] });  // Static dot
ctx.ui.setWorkingIndicator({
  frames: [
    ctx.ui.theme.fg("dim", "·"),
    ctx.ui.theme.fg("muted", "•"),
    ctx.ui.theme.fg("accent", "●"),
    ctx.ui.theme.fg("muted", "•"),
  ],
  intervalMs: 120,
});
ctx.ui.setWorkingIndicator({ frames: [] });  // Hide indicator
ctx.ui.setWorkingIndicator();  // Restore default spinner

// Widget above editor (default)
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
// Widget below editor
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
ctx.ui.setWidget("my-widget", (tui, theme) => new Text(theme.fg("accent", "Custom"), 0, 0));
ctx.ui.setWidget("my-widget", undefined);  // Clear

// Custom footer (replaces built-in footer entirely)
ctx.ui.setFooter((tui, theme) => ({
  render(width) { return [theme.fg("dim", "Custom footer")]; },
  invalidate() {},
}));
ctx.ui.setFooter(undefined);  // Restore built-in footer

// Terminal title
ctx.ui.setTitle("pi - my-project");

// Editor text
ctx.ui.setEditorText("Prefill text");
const current = ctx.ui.getEditorText();

// Paste into editor (triggers paste handling, including collapse for large content)
ctx.ui.pasteToEditor("pasted content");

// Stack custom autocomplete behavior on top of the built-in provider
ctx.ui.addAutocompleteProvider((current) => ({
  triggerCharacters: ["#"],
  async getSuggestions(lines, line, col, options) {
    const beforeCursor = (lines[line] ?? "").slice(0, col);
    const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
    if (!match) {
      return current.getSuggestions(lines, line, col, options);
    }

    return {
      prefix: `#${match[1] ?? ""}`,
      items: [{ value: "#2983", label: "#2983", description: "Extension API for autocomplete" }],
    };
  },
  applyCompletion(lines, line, col, item, prefix) {
    return current.applyCompletion(lines, line, col, item, prefix);
  },
  shouldTriggerFileCompletion(lines, line, col) {
    return current.shouldTriggerFileCompletion?.(lines, line, col) ?? true;
  },
}));

// Tool output expansion
const wasExpanded = ctx.ui.getToolsExpanded();
ctx.ui.setToolsExpanded(true);
ctx.ui.setToolsExpanded(wasExpanded);

// Custom editor (vim mode, emacs mode, etc.)
ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
const currentEditor = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
  new WrappedEditor(tui, theme, keybindings, currentEditor?.(tui, theme, keybindings))
);
ctx.ui.setEditorComponent(undefined);  // Restore default editor

// Theme management (see themes.md for creating themes)
const themes = ctx.ui.getAllThemes();  // [{ name: "dark", path: "/..." | undefined }, ...]
const lightTheme = ctx.ui.getTheme("light");  // Load without switching
const result = ctx.ui.setTheme("light");  // Switch by name
if (!result.success) {
  ctx.ui.notify(`Failed: ${result.error}`, "error");
}
ctx.ui.setTheme(lightTheme!);  // Or switch by Theme object
ctx.ui.theme.fg("accent", "styled text");  // Access current theme

Пользовательские рамки индикаторов работы отображаются дословно. Если вам нужны цвета, добавьте их в строки фрейма самостоятельно, например, с помощью ctx.ui.theme.fg(...).

Автозаполнение Providers

Используйте ctx.ui.addAutocompleteProvider(), чтобы разместить пользовательскую логику автозаполнения поверх встроенной косой черты и поставщика пути. Установите triggerCharacters для пользовательских естественных триггеров, таких как Используйте ctx.ui.addAutocompleteProvider(), чтобы разместить пользовательскую логику автозаполнения поверх встроенной косой черты и поставщика пути. Установите triggerCharacters` для пользовательских естественных триггеров, таких как.

Типичный образец:

  • проверить текст перед курсором
  • возвращайте свои собственные предложения, когда синтаксис вашего расширения совпадает
  • в противном случае делегируйте current.getSuggestions(...)
  • делегировать applyCompletion(...), если вам не требуется собственное поведение вставки
pi.on("session_start", (_event, ctx) => {
  ctx.ui.addAutocompleteProvider((current) => ({
    triggerCharacters: ["#"],
    async getSuggestions(lines, cursorLine, cursorCol, options) {
      const line = lines[cursorLine] ?? "";
      const beforeCursor = line.slice(0, cursorCol);
      const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
      if (!match) {
        return current.getSuggestions(lines, cursorLine, cursorCol, options);
      }

      return {
        prefix: `#${match[1] ?? ""}`,
        items: [
          { value: "#2983", label: "#2983", description: "Extension API for registering custom @ autocomplete providers" },
          { value: "#2753", label: "#2753", description: "Reload stale resource settings" },
        ],
      };
    },

    applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
      return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
    },

    shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
      return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
    },
  }));
});

См. github-issue-autocomplete.ts полный пример, который предварительно загружает последние открытые проблемы GitHub с помощью gh issue list и фильтрует их локально для быстрого завершения #.... Для этого требуется GitHub CLI (gh) и GitHub проверка репозитория.

Пользовательские компоненты

Для сложного пользовательского интерфейса используйте ctx.ui.custom(). Это временно заменяет редактор вашим компонентом до тех пор, пока не будет вызван done():

import { Text, Component } from "@earendil-works/pi-tui";

const result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {
  const text = new Text("Press Enter to confirm, Escape to cancel", 1, 1);

  text.onKey = (key) => {
    if (key === "return") done(true);
    if (key === "escape") done(false);
    return true;
  };

  return text;
});

if (result) {
  // User pressed Enter
}

Обратный вызов получает:

  • Экземпляр tui - TUI (для размеров экрана, управления фокусом)
  • theme — Текущая тема для стилизации.
  • keybindings — Менеджер привязки клавиш приложения (для проверки ярлыков)
  • done(value) — вызов закрытия компонента и возврат значения.

См. tui.md для полного компонента API.

Режим наложения (экспериментальный)

Передайте { overlay: true }, чтобы отобразить компонент как плавающее модальное окно поверх существующего контента, не очищая экран:

const result = await ctx.ui.custom<string | null>(
  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
  { overlay: true }
);

Для расширенного позиционирования (привязки, поля, проценты, адаптивная видимость) укажите overlayOptions. Используйте onHandle для программного управления фокусом или видимостью:

const result = await ctx.ui.custom<string | null>(
  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
  {
    overlay: true,
    overlayOptions: { anchor: "top-right", width: "50%", margin: 2 },
    onHandle: (handle) => {
      handle.focus(); // focus this overlay and bring it to the visual front
      // handle.unfocus({ target: editorComponent }); // release input to a specific component
      // handle.setHidden(true/false); // toggle visibility
      // handle.hide(); // permanently remove
    }
  }
);

Сфокусированное видимое наложение может восстановить ввод после закрытия временного пользовательского интерфейса без наложения. Если вы намеренно хотите, чтобы другой компонент сохранял входные данные, пока наложение остается видимым, вызовите handle.unfocus({ target }). Передача { target: null } освобождает наложение без фокусировки на другом компоненте.

См. tui.md полные OverlayOptions и OverlayHandle, API и overlay-qa-tests.ts для примеров.

Пользовательский редактор

Замените основной редактор ввода собственной реализацией (режим vim, режим emacs и т. д.):

import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { matchesKey } from "@earendil-works/pi-tui";

class VimEditor extends CustomEditor {
  private mode: "normal" | "insert" = "insert";

  handleInput(data: string): void {
    if (matchesKey(data, "escape") && this.mode === "insert") {
      this.mode = "normal";
      return;
    }
    if (this.mode === "normal" && data === "i") {
      this.mode = "insert";
      return;
    }
    super.handleInput(data);  // App keybindings + text editing
  }
}

export default function (pi: ExtensionAPI) {
  pi.on("session_start", (_event, ctx) => {
    ctx.ui.setEditorComponent((tui, theme, keybindings) =>
      new VimEditor(tui, theme, keybindings)
    );
  });
}

Ключевые моменты:

  • Расширьте CustomEditor (не базовый Editor), чтобы получить привязки клавиш приложения (Escape для отмены, ctrl+d, переключение модели).
  • Позвоните по номеру super.handleInput(data), чтобы узнать ключи, с которыми вы не справляетесь.
  • Factory получает tui, theme и keybindings из приложения.
  • Используйте ctx.ui.getEditorComponent() перед setEditorComponent(), чтобы обернуть ранее настроенный пользовательский редактор.
  • Нажмите undefined, чтобы восстановить настройки по умолчанию: ctx.ui.setEditorComponent(undefined)

Чтобы создать композицию с другим расширением, которое уже заменило редактор, сохраните предыдущую фабрику, прежде чем устанавливать свою:

const previous = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
  new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })
);

Полный пример с индикатором режима см. в tui.md Шаблоне 7.

Отображение сообщений и записей

Зарегистрируйте собственный рендерер для сообщений с помощью customType. Используйте средства рендеринга сообщений для контента, который должен участвовать в контексте LLM:

import { Text } from "@earendil-works/pi-tui";

pi.registerMessageRenderer("my-extension", (message, options, theme) => {
  const { expanded, outputPad } = options;
  let text = theme.fg("accent", `[${message.customType}] `);
  text += message.content;

  if (expanded && message.details) {
    text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2));
  }

  return new Text(text, outputPad, 0);
});

Сообщения отправляются через pi.sendMessage():

pi.sendMessage({
  customType: "my-extension",  // Matches registerMessageRenderer
  content: "Status update",
  display: true,               // Show in TUI
  details: { ... },            // Available in renderer
});

Для контента, содержащего только TUI, который не следует отправлять в LLM, вместо этого визуализируйте пользовательские записи:

pi.registerEntryRenderer("my-card", (entry, options, theme) => {
  return new Text(theme.fg("accent", JSON.stringify(entry.data)));
});

pi.appendEntry("my-card", { status: "done" });

Цвета темы

Все функции рендеринга получают объект theme. См. themes.md для создания собственных тем и полной цветовой палитры.

// Foreground colors
theme.fg("toolTitle", text)   // Tool names
theme.fg("accent", text)      // Highlights
theme.fg("success", text)     // Success (green)
theme.fg("error", text)       // Errors (red)
theme.fg("warning", text)     // Warnings (yellow)
theme.fg("muted", text)       // Secondary text
theme.fg("dim", text)         // Tertiary text

// Text styles
theme.bold(text)
theme.italic(text)
theme.strikethrough(text)

Для подсветки синтаксиса в средствах визуализации пользовательских инструментов:

import { highlightCode, getLanguageFromPath } from "@earendil-works/pi-coding-agent";

// Highlight code with explicit language
const highlighted = highlightCode("const x = 1;", "typescript", theme);

// Auto-detect language from file path
const lang = getLanguageFromPath("/path/to/file.rs");  // "rust"
const highlighted = highlightCode(code, lang, theme);

Обработка ошибок

  • Ошибки расширения регистрируются, агент продолжает работу
  • Ошибки tool_call блокируют инструмент (отказоустойчивость)
  • Ошибки инструмента execute должны сигнализироваться броском; выброшенная ошибка перехватывается, сообщается LLM с помощью isError: true, и выполнение продолжается

Поведение режима

Режим ctx.mode ctx.hasUI Примечания
Интерактивный "tui" true Полный TUI с терминальным рендерингом
RPC (--mode rpc) "rpc" true Диалоги и уведомления по протоколу JSON; custom() возвращает undefined. См. rpc.md
JSON (--mode json) "json" false Поток событий на stdout; Методы пользовательского интерфейса не требуют операций
Распечатать (-p) "print" false Extensions запустить, но не могу подсказать

Используйте ctx.mode === "tui" перед TUI, специфичными для функций (custom(), фабрики компонентов, ввод через терминал). Используйте ctx.hasUI перед методами диалога и уведомления, которые работают как в TUI, так и в RPC режимах.

Примеры

Все примеры в examples/extensions/.

Пример Описание Ключ APIs
Инструменты
hello.ts Минимальная регистрация инструмента registerTool
question.ts Инструмент с взаимодействием с пользователем registerTool, ui.select
questionnaire.ts Многошаговый мастер-инструмент registerTool, ui.custom
todo.ts Инструмент с сохранением состояния и постоянством registerTool, appendEntry, renderResult, события сеанса
dynamic-tools.ts Регистрация инструментов после запуска и во время команд registerTool, session_start, registerCommand
structured-output.ts Окончательный инструмент структурированного вывода с terminate: true registerTool, завершение результатов инструмента
truncated-tool.ts Пример усечения вывода registerTool, truncateHead
tool-override.ts Переопределить встроенный инструмент чтения registerTool (то же имя, что и у встроенного)
Команды
pirate.ts Изменить системное приглашение за ход registerCommand, before_agent_start
summarize.ts Команда сводки разговора registerCommand, ui.custom
handoff.ts Передача модели между поставщиками registerCommand, ui.editor, ui.custom
qna.ts Вопросы и ответы с пользовательским интерфейсом registerCommand, ui.custom, setEditorText
send-user-message.ts Внедрение пользовательских сообщений registerCommand, sendUserMessage
reload-runtime.ts Команда перезагрузки и передача инструмента LLM registerCommand, ctx.reload(), sendUserMessage
shutdown-command.ts Команда плавного выключения registerCommand, shutdown()
Мероприятия и ворота
permission-gate.ts Блокируйте опасные команды on("tool_call"), ui.confirm
project-trust.ts Принятие решения или отсрочка доверия проекта со стороны пользователя/глобального расширения или расширения CLI. on("project_trust"), пользовательский интерфейс доверия, требуемый результат доверия
protected-paths.ts Блокировать запись по определенным путям on("tool_call")
confirm-destructive.ts Подтвердить изменения сеанса on("session_before_switch"), on("session_before_fork")
dirty-repo-guard.ts Предупреждать о грязном репозитории git on("session_before_*"), exec
input-transform.ts Преобразование пользовательского ввода on("input")
input-transform-streaming.ts Входное преобразование с поддержкой потоковой передачи on("input"), streamingBehavior
model-status.ts React для изменения модели on("model_select"), setStatus
provider-payload.ts Проверка полезных данных и заголовков ответов поставщика on("before_provider_request"), on("after_provider_response")
system-prompt-header.ts Отображение подсказки системы on("agent_start"), getSystemPrompt
claude-rules.ts Загрузка правил из файлов on("session_start"), on("before_agent_start")
prompt-customizer.ts Добавьте контекстно-зависимые инструкции по инструменту, используя systemPromptOptions on("before_agent_start"), BuildSystemPromptOptions
file-trigger.ts Наблюдатель за файлами вызывает сообщения sendMessage
Сжатие и сеансы
custom-compaction.ts Пользовательская сводка по сжатию on("session_before_compact")
trigger-compact.ts Запуск уплотнения вручную compact()
git-checkpoint.ts Git тайник на ходах on("turn_start"), on("session_before_fork"), exec
git-merge-and-resolve.ts Извлечение, объединение и разрешение конфликтов on("agent_end"), exec, sendUserMessage
auto-commit-on-exit.ts Принять решение о выключении on("session_shutdown"), exec
Компоненты пользовательского интерфейса
status-line.ts Индикатор состояния нижнего колонтитула setStatus, события сеанса
working-indicator.ts Настройте индикатор работы потоковой передачи setWorkingIndicator, registerCommand
github-issue-autocomplete.ts Добавьте #1234 завершенных задач поверх встроенного автозаполнения, предварительно загрузив последние открытые проблемы из gh issue list. addAutocompleteProvider, on("session_start"), exec
custom-footer.ts Полностью заменить нижний колонтитул registerCommand, setFooter
custom-header.ts Заменить стартовый заголовок on("session_start"), setHeader
modal-editor.ts Модальный редактор в стиле Vim setEditorComponent, CustomEditor
rainbow-editor.ts Пользовательский стиль редактора setEditorComponent
widget-placement.ts Виджет над/под редактором setWidget
overlay-test.ts Компоненты наложения ui.custom с опциями наложения
overlay-qa-tests.ts Комплексные накладные тесты ui.custom, все параметры наложения
notify.ts Простые уведомления ui.notify
timed-confirm.ts Диалоги с таймаутом ui.confirm с тайм-аутом/сигналом
mac-system-theme.ts Автоматическое переключение темы setTheme, exec
Комплекс Extensions
plan-mode/ Реализация режима полного плана Все типы событий: registerCommand, registerShortcut, registerFlag, setStatus, setWidget, sendMessage, setActiveTools
preset.ts Сохраняемые пресеты (модель, инструменты, мышление) registerCommand, registerShortcut, registerFlag, setModel, setActiveTools, setThinkingLevel, appendEntry
tools.ts Включить/выключить инструменты в пользовательском интерфейсе registerCommand, setActiveTools, SettingsList, события сеанса
Удаленное управление и песочница
ssh.ts SSH удаленное выполнение registerFlag, on("user_bash"), on("before_agent_start"), операции с инструментом
interactive-shell.ts Постоянный сеанс оболочки on("user_bash")
sandbox/ Выполнение инструмента в песочнице Операции с инструментом
gondolin/ Направьте встроенные инструменты и команды ! в микро-VM Gondolin. Операции с инструментом, встроенные переопределения инструмента, on("user_bash")
subagent/ Создание субагентов registerTool, exec
Игры
snake.ts Змеиная игра registerCommand, ui.custom, работа с клавиатурой
space-invaders.ts Игра Космические захватчики registerCommand, ui.custom
doom-overlay/ Doom в наложении ui.custom с наложением
Providers
custom-provider-anthropic/ Пользовательский антропный прокси registerProvider
custom-provider-gitlab-duo/ GitИнтеграция Lab Duo registerProvider с OAuth
Сообщения и общение
message-renderer.ts Пользовательский рендеринг сообщений registerMessageRenderer, sendMessage
entry-renderer.ts Отрисовка пользовательской записи только TUI registerEntryRenderer, appendEntry
event-bus.ts События между расширениями pi.events
Метаданные сеанса
session-name.ts Назовите сеансы для селектора setSessionName, getSessionName
bookmark.ts Добавить в закладки записи для /tree setLabel
Разное
inline-bash.ts Встроенный bash в вызовах инструментов on("tool_call")
bash-spawn-hook.ts Отрегулируйте команду bash, cwd и env перед выполнением. createBashTool, spawnHook
with-deps/ Расширение с зависимостями npm Структура пакета с package.json