Перейти к содержимому
ФреймворкJavaScript

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

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

Методы ниже вызываются на редакторе, созданном через 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) });
  }
});