---
title: "Agent API — ассистент в приложении правит документ"
description: "Встроенный ассистент «Спросить ИИ» с вашей моделью или свой цикл через editor.agent. Ассистент правит открытый документ на виду у пользователя, его можно остановить и отменить."
source: https://blokeditor.com/ru/docs/agent-api/
lastmod: 2026-10-30
---

Фреймворк JavaScript

Расширение и система Agent

На этой странице agent.tools(options)

# Agent API: ассистент, который правит открытый документ

Ассистент правит открытый документ через `editor.agent`. Запустить его можно двумя способами.

- Встроенный ассистент: передайте `agent.assistant.model` в конфигурации. Blok сам ведёт цикл модели и даёт пользователям строку запроса «Спросить ИИ». Ваша функция `model` обычно вызывает ваш бэкенд, поэтому ключ API не попадает в браузер.
- Свой цикл: передайте `agent.tools()` своей модели, откройте ход через `agent.begin()` и выполняйте каждый вызов инструмента. Модель, ключи API и чат остаются в вашем приложении.

Со встроенным ассистентом строку запроса открывает Cmd/Ctrl+J или кнопка «Спросить ИИ» в строчной панели инструментов. Её открывает и пробел в любом пустом блоке: абзаце, заголовке, пункте списка, цитате, переключателе, ячейке таблицы или блоке кода. В блоке кода хватит пустой строки. Модель читает и правит документ на лету теми же инструментами, что отдаёт @bloklabs/mcp.

- Строка показывает ход работы и кнопку «Остановить».
- «Оставить» закрывает строку. «Отменить» откатывает только шаги самого ассистента и никогда не трогает текст, который пользователь набрал после них.
- Прежде чем модель разблокирует заблокированную базу данных, строка спрашивает пользователя: «Оставить заблокированной» или «Разблокировать». Дальше модель идёт только после «Разблокировать», а «Стоп» и Escape означают «нет».
- Ход, открытый через `agent.begin()`, никого спросить не может, поэтому его разблокировка завершается ошибкой `APPROVAL_REQUIRED`.
- Прежде чем модель изменит у формы настройки «Поделиться», то есть кто может её заполнять, строка спрашивает пользователя. Так бывает при любой команде, отмене, повторе или восстановлении.
  - Строка называет каждое изменение и показывает кнопки «Оставить как есть» и «Разрешить». Дальше модель идёт только после «Разрешить».
  - MCP спрашивает через elicitation. Среда, которая не может спросить, завершается ошибкой `APPROVAL_REQUIRED`.
  - Модель не получает больше, чем даёт панель «Поделиться». Без `forms` в настройках инструмента базы данных публичная ссылка, анонимные ответы и доступ к ответу завершаются ошибкой `COMMAND_UNAVAILABLE` (причина `host`).
  - Синхронизацию календаря менять нельзя.
- Второй запрос продолжает тот же разговор.
- Выделенный текст и блоки, которые он захватывает, выделенные блоки или блок с курсором уходят модели как контекст.
- В режиме только для чтения строка не открывается.

«Спросить ИИ» показывает правки на лету, когда модель отдаёт ответ потоком. Новый текст появляется по словам там, куда он ляжет, а переписанный текст идёт потоком на своём месте. Каждый вызов инструмента применяется одним шагом, когда его входные данные закончились. До этого документ, сохранённые данные, отмена и соавторы не видят изменений.

Модель отдаёт поток, когда вызывает `request.onStream`. Адаптер делает это, если вы передали ему `stream`. Ваша функция `model` может вызывать его с такими событиями и в конце всё так же возвращает весь ответ:

- `{ type: 'tool_start', index, id, name }`, когда начинается вызов инструмента.
- `{ type: 'tool_input', index, json }`, пока растут его входные данные. В `json` лежит весь входной текст на этот момент.
- `{ type: 'tool_end', index }`, когда входные данные готовы.

Каждый вызов выполняется на своём `tool_end`. Поэтому ответ, который возвращает функция, должен содержать те же вызовы инструментов в том же порядке, с теми же `id` и тем же итоговым вводом, что и поток. `index` считает вызовы инструментов с 0 внутри одного ответа. «Спросить ИИ» сопоставляет вызов по `id`, а затем по `index` и имени инструмента, поэтому вызов никогда не выполняется дважды. Если итоговый ввод отличается, выполнен уже ввод из потока.

Без потока «Спросить ИИ» работает в запасном режиме. Пока модель работает, под строкой запроса видны строки-заглушки. Каждое изменение применяется целиком, а потом его новые слова проявляются поверх записанных блоков. При настройке «уменьшить движение» эти анимации выключены, а слова из потока появляются сразу, как только пришли.

Пользователь видит, где работает агент. Агент никогда не двигает курсор или выделение пользователя.

В редакторе агент может вставить HTML командой `html.insert`. Он получает те же блоки, что и человек, который вставил этот HTML: та же очистка, тот же санитайзер и те же правила вставки инструментов. Скрипты, обработчики событий и небезопасные ссылки удаляются. У @bloklabs/mcp и серверных сред нет конвейера вставки, поэтому там `html.insert` недоступна.

Агент может комментировать строки баз данных от имени человека, для которого работает. Полное чтение строки показывает её открытые обсуждения в поле `comments`, отдельно от содержимого страницы. У них есть пометка, что это слова других людей, поэтому модель никогда не выполняет инструкции из них.

- `database.addComment`, `database.replyToComment` и `database.resolveComment` работают с любым открытым обсуждением.
- `database.editComment` и `database.deleteComment` работают только с собственными комментариями человека.
- Решённое обсуждение пропадает из чтения и больше не принимает записей. Открыть его снова нельзя, как и в панели комментариев.
- Запись комментария не попадает в историю отмены, как и комментарий человека. Поэтому команды комментариев идут в отдельном пакете.
- В редакторе агент комментирует только там, где может сам человек. Его останавливают хук `comments` или `permissions`, которые запрещают `comment`.
- @bloklabs/mcp и исполнитель на C# разрешают комментарии по умолчанию. По сохранённым данным нельзя понять, ведёт ли хост комментарии в своей системе.
  - Такой хост должен отключить их флагом `--no-comments` (или `comments: false`) либо `BuiltInComments = false`.
  - Иначе люди не увидят комментарии агента.
- Каждый комментарий подписан человеком как автором и агентом как соавтором.

В редакторе правки агента в строках базы данных делают то же, что правки человека. Новые строки, правки, перемещения и удаления доходят до `adapter` хоста и запускают автоматизации, ИИ-автозаполнение и `automations.onTrigger`. Агент также может нажать свойство-кнопку командой `database.pressButton` и отправить форму командой `database.submitForm`.

- Нажатие выполняет шаги кнопки по порядку, как щелчок. Каждое «Показать подтверждение» спрашивает пользователя в панели Ask AI текстом самой кнопки. Ход, открытый через `agent.begin()`, спросить никого не может, поэтому такое нажатие завершается ошибкой `APPROVAL_REQUIRED`.
- Нажатие, которое вызывает `buttons.onAction` или `buttons.onNotify`, сначала спрашивает пользователя и показывает имя действия, получателей и сообщение, которые получит хост. Хост узнаёт о нём после записи и получает только поля самого шага. Нажатие не открывает ни страниц, ни ссылок и не отправляет сообщений агенту.
- Отправка проходит проверки самой формы, создаёт ту же строку, что и кнопка «Отправить» у человека, и вызывает `forms.onSubmit`. Закрытая форма её отклоняет. Анонимная тоже: агент отвечает только от имени пользователя.
- В @bloklabs/mcp и серверных средах автоматизации, кнопки и хуки форм не работают. Там эти две команды недоступны с причиной `runtime`.

Кнопка, которую нажимает человек, может отправить сообщение встроенному ассистенту шагом «Send message to agent». Этот шаг доступен, только если задан `agent.assistant.model`. Прежде чем кнопка что-то запишет, человек видит сообщение и подтверждает его: сообщение написал тот, кто редактировал кнопку, а ассистент действует от имени нажавшего. Затем выполняются записи кнопки, и ассистент получает сообщение и id строки, поэтому «эта страница» означает эту строку. Если ассистент занят или выдал ошибку, появляется уведомление.

В @bloklabs/mcp агент может загрузить файл командами `image.uploadFile`, `video.uploadFile`, `audio.uploadFile` и `file.uploadFile`, если хост передал свой загрузчик в `uploads`. Файл проходит через этот загрузчик с теми же ограничениями по типу и размеру, что и загрузка человека, и новый блок получает то же, что дала бы загрузка человека. Агент передаёт байты в base64 или, через stdio, путь внутри `uploads.root` хоста. Без загрузчика эти команды недоступны с причиной `host`. Редактор и серверные среды пока не принимают загрузки агента.

Если строка «Спросить ИИ» открылась за краем окна, она один раз прокручивает страницу, чтобы её было видно. Во время работы страница не прокручивается, сколько бы агент ни написал. Пользователь по-прежнему может прокручивать её сам. Если место строки уходит из окна, строка остаётся в 16px от края окна, поверх текста.

Одно нажатие Cmd+Z отменяет один непрерывный отрезок работы агента.

Обновлено 30 окт. 2026 г. [Редактировать на GitHub](https://github.com/JackUait/blok/blob/main/docs/src/components/api/api-data.ts)

### Как получить экземпляр редактора

Методы ниже вызываются на редакторе, созданном через new Blok(). Они доступны после того, как разрешится editor.isReady.

TypeScript

```
// You already hold the instance returned by the constructor.
const editor = new Blok({ holder: 'editor' });
await editor.isReady;

// Call any API method on it.
editor.caret.setToLastBlock('end');
```

## Методы

### agent.tools(options)

Promise<AgentAnthropicTool[] | AgentOpenAITool[]>

Описания инструментов `blok_read`, `blok_describe` и `blok_execute`. Передайте их модели как есть.

- `format`: `'anthropic'` или `'openai'` (Responses API).
- `schema`: как `blok_execute` описывает свои команды.
  - `'mixed'` (по умолчанию) даёт частым командам полные аргументы, а остальные только называет.
  - `'envelope'` называет все команды.
  - `'full'` перечисляет аргументы каждой команды и бросает `AgentToolsTooLargeError`, если результат слишком большой.
- `strict`: добавляет `strict: true` к `blok_read` и `blok_describe`. Бросает `AgentToolsStrictError`, если схему нельзя сделать строгой. `blok_execute` его никогда не получает.

TypeScript

```
const tools = await editor.agent.tools({ format: 'anthropic' });
```

### agent.guidance()

Promise<string>

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

TypeScript

```
const system = await editor.agent.guidance();
```

### agent.begin(options)

AgentTurn

Открывает ход и возвращает его. В одном редакторе может быть открыт только один ход, поэтому второй вызов бросает `AgentTurnOpenError`. Код агента загружается при первом использовании или до `isReady`, если в конфиге задан `agent`. Ход, открытый до загрузки, ставит вызовы в очередь и выполняет их, когда код загрузится. Пока ни один ожидаемый вызов этого хода не завершился, его `describe()` бросает ошибку.

- `agent`: `id`, `name` и необязательные `color` и `onBehalfOf` агента. Имя видит пользователь.
  - У блока, который агент добавил или изменил, два автора. В `createdBy` и `lastEditedBy` записан человек (`onBehalfOf`, иначе ваш `user.id`). В `createdWith` и `lastEditedWith` записан идентификатор агента.
  - Если человек неизвестен, в `createdBy` и `lastEditedBy` записан только идентификатор агента.
- `follow`: прокручивает к блоку агента, пока пользователь не прокрутит, не начнёт печатать или не кликнет. По умолчанию `false`.
- `attributeTo`: `'user'` записывает только человека, без `createdWith` и `lastEditedWith`.

TypeScript

```
const tools = await editor.agent.tools({ format: 'anthropic' });
const system = await editor.agent.guidance();
const turn = editor.agent.begin({ agent: { id: 'assistant', name: 'Assistant' } });

stopButton.onclick = () => turn.stop();

try {
  // Your own model loop. For each tool_use block:
  //   const result = await turn.call(block.name, block.input);
  //   reply with { type: 'tool_result', tool_use_id: block.id,
  //                content: result.content, is_error: result.isError }
} finally {
  turn.end();
}
```

### turn.call(name, input)

Promise<AgentToolResult>

Выполняет один вызов инструмента от модели и возвращает `{ content, isError }`. Отправьте `content` обратно как результат инструмента. Ошибка модели приходит с `isError: true`, чтобы модель могла её исправить.

TypeScript

```
const result = await turn.call(block.name, block.input);
```

### turn.end()

void

Закрывает ход, когда модель закончила. Вызовы после этого возвращают ошибку `CANCELLED`.

TypeScript

```
turn.end();
```

### turn.stop()

void

Закрывает ход и отменяет вызовы, которые ещё не начались. Подключите его к кнопке «Стоп». Уже применённые изменения остаются, и пользователь может их отменить.

TypeScript

```
stopButton.onclick = () => turn.stop();
```

### agent.state

AgentState

`{ status: 'idle' }` или `{ status: 'working', turn }`. Это тот же объект, пока состояние не изменится, поэтому он работает с `useSyncExternalStore`.

TypeScript

```
const state = useSyncExternalStore(editor.agent.subscribe, () => editor.agent.state);
```

### agent.subscribe(listener)

() => void

Вызывает `listener` при каждом изменении `state`. Возвращает функцию отписки.

TypeScript

```
const unsubscribe = editor.agent.subscribe(() => render(editor.agent.state));
```

### agent.ask(prompt, options)

AgentRun

Отправляет `prompt` встроенному ассистенту и возвращает сеанс. Модель читает и правит документ на лету, как из строки «Спросить ИИ».

- `options.context`: отправляет модели выделенный текст и блоки, которые он захватывает, выделенные блоки или блок с курсором. По умолчанию `true`.
- `options.thinking`: задаёт уровень рассуждения для этого запуска. Без него запуск берёт `thinking.default`. Запросы из строки «Спросить ИИ» берут уровень, выбранный в ней.

Бросает исключение, если `agent.assistant` не задан или ход уже открыт. Бросает `TypeError`, если настройка `thinking` неверна или `options.thinking` не входит в число известных уровней. Дождитесь `run.result`, затем вызовите `run.end()`, чтобы оставить правки, или `run.discard()`, чтобы их отменить.

TypeScript

```
const run = editor.agent.ask('Turn this list into a table');
const result = await run.result;

if (result.status === 'done') {
  run.end();
} else {
  await run.discard();
}
```

### agent.openAssistant(options?)

void

Открывает строку запроса «Спросить ИИ», как Cmd/Ctrl+J. Подходит для своей кнопки «Спросить ИИ» в интерфейсе. Передайте `{ prompt }`, чтобы сразу отправить этот запрос, как будто его ввёл пользователь. Ничего не делает, если `agent.assistant` не задан или редактор в режиме только для чтения.

TypeScript

```
askAiButton.onclick = () => editor.agent.openAssistant();
summarizeButton.onclick = () => editor.agent.openAssistant({ prompt: 'Summarize this page' });
```

### run.result

Promise<AgentRunResult>

Последний ответ. Каждый `followUp()` заменяет его.

Разрешается в `{ status, reply, error, changed }`. `reply` содержит последний текст модели, а `changed` перечисляет идентификаторы блоков, которые затронул сеанс. `status` принимает одно из этих значений:

- `done`: модель ответила.
- `stopped`: был вызван `stop()`.
- `refused`: модель отказалась.
- `truncated`: вывод модели оборвался.
- `step-limit`: сеанс исчерпал `maxSteps` вызовов модели.
- `failed`: вызов модели бросил исключение. Причина в `error`.

TypeScript

```
const { status, reply, changed } = await run.result;
```

### run.followUp(prompt)

Promise<AgentRunResult>

Задаёт новый вопрос в том же разговоре, поэтому модель видит прежние запросы и ответы. Разрешается в новый ответ и заменяет `run.result`. Бросает исключение, пока предыдущий ответ ещё выполняется.

TypeScript

```
const next = await run.followUp('Make it shorter');
```

### run.stop()

void

Останавливает текущий ответ. Уже применённые правки остаются, а сеанс остаётся открытым для `followUp()`. Подключите его к своей кнопке «Остановить».

TypeScript

```
stopButton.onclick = () => run.stop();
```

### run.discard()

Promise<boolean>

Останавливает сеанс, отменяет его правки и завершает его. Разрешается в `true`, только если отменены все правки сеанса.

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

Каждую подстраницу, которую сеанс сохранил, возвращает в прежний вид. Ещё раз вызывает хуки `rename` и `setIcon` инструмента страницы со старым названием или значком.

Подстраница, изменённая после последней записи сеанса, остаётся как есть. Запись, которую нельзя откатить, тоже остаётся. Так ничья поздняя правка не пропадёт. Если шаг или подстраница остались, разрешается в `false`.

TypeScript

```
const undone = await run.discard();
```

### run.end()

void

Оставляет правки и закрывает сеанс. После этого редактор может начать новый ход.

TypeScript

```
run.end();
```

TypeScript

```
// Browser: the model function in your editor config.
const config = {
  agent: {
    assistant: {
      name: 'Assistant',
      model: async ({ system, messages, tools, signal }) => {
        const response = await fetch('/api/ai', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ system, messages, tools }),
          signal, // Stop aborts the request.
        });
        if (!response.ok) throw new Error(`AI request failed: ${response.status}`);
        return response.json(); // { content, stop_reason }
      },
    },
  },
};

// Server (Node, Express): the API key stays here.
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic(); // Reads ANTHROPIC_API_KEY.

app.post('/api/ai', async (req, res) => {
  const { system, messages, tools } = req.body;
  // Stop in the editor aborts the fetch; stop paying for the answer too.
  const controller = new AbortController();
  res.on('close', () => controller.abort());

  try {
    const message = await client.messages
      .stream({ model: 'claude-opus-5-5', max_tokens: 64000, system, messages, tools }, { signal: controller.signal })
      .finalMessage();
    res.json({ content: message.content, stop_reason: message.stop_reason });
  } catch (error) {
    if (!res.headersSent && !controller.signal.aborted) res.status(502).json({ error: String(error) });
  }
});
```
