> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kazzle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Инструменты

> Как приложение объявляет инструменты и как их обработчики возвращают результаты.

# Инструменты

**Инструмент** — это именованное действие, которое может вызвать ИИ, с типизированными входными параметрами — например `send_email(to, subject, body)`. Инструменты объявляются в файле `tools.ts` навыка и, для инструментов, которые предоставляет ваше приложение, поддерживаются HTTP-маршрутом в одном из ваших компонентов. Эта страница — источник истины для контракта инструмента; раздел [Навыки](/apps/skills) описывает, где находится `tools.ts`.

## Объявление инструмента

Каждая запись в `tools.ts` имеет безопасное для провайдера `name`, дружественное `displayName`, `description`, Zod-схему `input` и `target`, который указывает, что вызывает Kazzle.

```ts theme={"theme":"material-theme-darker"}
import { z } from 'zod';
import type { KazzleTool } from '@kazzle/app/tools';

export const SaveBookmarkInput = z.object({
  url: z.string().url().describe('URL to save.'),
  title: z.string().optional().describe('Optional human-readable title.'),
}).strict();

const saveBookmarkTarget = {
  type: 'app',
  component: 'api',
  path: '/tools/save-bookmark',
  method: 'POST',
  body: '${input}',
} as const;

export const tools = [
  {
    name: 'save_bookmark',
    displayName: 'Save bookmark',
    description: 'Save a bookmark URL and return a confirmation.',
    input: SaveBookmarkInput,
    target: saveBookmarkTarget,
  },
] as const satisfies readonly KazzleTool[];
```

## Целевые адреса

`target` — это переиспользуемый адрес для инструмента или кнопки требуемого действия. Прямой вызов инструмента ИИ и клик по кнопке могут использовать один и тот же объект target, поэтому оба выполняют одинаковый код.

| Целевой адрес | Поля                                                                  | Выполняется где                                                                                                       |
| ------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `app`         | `component`, `path`, `method`, опционально `query`, `headers`, `body` | В одном из ваших компонентов. Kazzle разрешает URL компонента и добавляет подписанный токен идентификации приложения. |
| `url`         | `url`, `method`, опционально `query`, `headers`, `body`               | На внешней конечной точке. Источник URL и метод — буквальные значения автора.                                         |
| `kazzle`      | `name`                                                                | Встроенный обработчик Kazzle на стороне клиента.                                                                      |

HTTP-целевые адреса используют одинаковые поля запроса:

```ts theme={"theme":"material-theme-darker"}
type RequestSpec = {
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  query?: Record<string, string>;
  headers?: Record<string, string>;
  body?: unknown;
};
```

Нет неявных значений по умолчанию для запроса. Если ваш обработчик должен получить необработанный входной параметр инструмента как JSON, напишите `body: '${input}'`.

## Ссылки

Kazzle разрешает ссылки в полях HTTP-целевого адреса `query`, `headers`, `body` и `url` перед отправкой:

* `${input}` — весь входной параметр инструмента или весь входной параметр кнопки.
* `${input.path}` — вложенное значение из входного параметра.
* `${env.NAME}` — именованная переменная окружения из объявленных `env.collection` + `env.environment` компонента приложения-владельца.

Разрешение выполняется в один проход. Если ссылка отсутствует или неизвестна, инструмент завершается с ошибкой вместо подстановки пустого значения. Используйте `$${input.name}`, когда вам нужен буквальный текст `${input.name}`.

## Запрос обработчика

Для целевого адреса `app` добавьте соответствующий маршрут в компонент-цель. С `body: '${input}'` Kazzle отправляет типизированный входной параметр как тело JSON:

```json theme={"theme":"material-theme-darker"}
{ "...": "the typed input" }
```

Целевые адреса `app` также получают:

| Заголовок             | Значение                                                            |
| --------------------- | ------------------------------------------------------------------- |
| `Authorization`       | `Bearer <identity token>` идентифицирующий пользователя + установку |
| `Kazzle-Tool-Context` | `{"source":"thread","threadId":"..."}` или `{"source":"api"}`       |

Целевые адреса приложения не могут устанавливать `Authorization` через `headers`; Kazzle владеет этим заголовком. Прочитайте контекст с помощью `toolContext(req)` из `@kazzle/app/tools`, когда обработчик должен различать поток и Tools API.

Инструмент, объявленный без соответствующего маршрута, не имеет практической пользы — добавьте оба вместе. `tools.json` не поддерживается; компилятор приложения завершается с ошибкой, если находит его.

## Ответ обработчика

Возвращайте простой текст или JSON с до трёх каналов:

```json theme={"theme":"material-theme-darker"}
{ "content": "...", "markdown": "...", "embedUrl": "https://..." }
```

* **`content`** — простой результат, который читает ИИ (передаётся модели). Обязателен.
* **`markdown`** — опционально. Краткое резюме в формате rich-text, отображаемое в карточке инструмента (react-markdown; необработанный HTML экранируется). Хорошо подходит для предложения, небольшого списка, встроенной ссылки. Голый относительный путь отображается как мёртвый предварительно отформатированный текст — ссылки должны быть **абсолютными**.
* **`embedUrl`** — опционально. **Абсолютный** URL, который предоставляет ваше приложение; карточка отображает его в изолированном iframe. Это способ показать реальный полноширинный интерфейс — экран подключения, панель управления, диаграмму. Ваше приложение размещает и владеет страницей, поэтому она может быть полностью интерактивной с вашим собственным бэкендом, cookies и OAuth. Ничего не записывается на диск.

`embedUrl` имеет приоритет над `markdown`, когда оба установлены. Всегда сохраняйте значимый `content` — это то, что читает ИИ.

```ts theme={"theme":"material-theme-darker"}
if (req.method === 'POST' && new URL(req.url).pathname === '/tools/save-bookmark') {
  const input = await req.json();
  // ...do the work...
  return Response.json({ content: `Saved ${input.title}` });
}
```

## Требуемое действие

Инструмент приложения может приостановить поток, когда требуется действие пользователя (или устройства). Возвращайте `type: 'action_required'` с заголовком карточки и кнопками. Действительно только когда `toolContext(req).source === 'thread'` — через Tools API (`source: 'api'`) возвращайте обычную ошибку домена.

```ts theme={"theme":"material-theme-darker"}
import { toolContext, isThreadToolInvocation } from '@kazzle/app/tools';

const context = toolContext(req);
if (!isThreadToolInvocation(context)) {
  return Response.json(
    { code: 'connect_required', error: 'Gmail must be connected before this tool can run.' },
    { status: 409 },
  );
}

return Response.json({
  type: 'action_required',
  title: 'Connect Gmail',
  description: 'Connect Gmail before Kazzle can search messages.',
  // Optional: only this device may run the gated buttons
  // assignee: { computerId: '<from computer { list: {} }>' },
  elements: [
    {
      type: 'button',
      id: 'connect',
      label: 'Connect Gmail',
      variant: 'primary',
      input: { scope: 'gmail.readonly' },
      target: {
        type: 'app',
        component: 'api',
        path: '/oauth/start',
        method: 'POST',
        body: '${input}',
      },
    },
  ],
});
```

Kazzle сохраняет карточку при вызове инструмента и возобновляет работу через `/chat/resume` после нажатия кнопки. Соответствующие клиенты видят кнопки автора; несоответствующие клиенты всё ещё видят Skip/Cancel плюс "Continue on {device}". Кнопка без `target` отправляет свой `input` как результат инструмента. Кнопка с целевым адресом сначала выполняет этот целевой адрес, затем использует результат целевого адреса как результат инструмента.

### Rich UI — встроенная страница, размещённая приложением

Когда результат инструмента визуален или интерактивен (экран подключения, диаграмма, панель сводки), предоставляйте страницу из одного из ваших компонентов и возвращайте её **абсолютный** URL как `embedUrl`. Создавайте URL из внедрённого URL компонента — никогда не используйте относительный путь.

```ts theme={"theme":"material-theme-darker"}
// A tool that needs the user to connect their account:
return Response.json({
  content: "Gmail isn't connected yet — the user needs to connect it.",
  embedUrl: `${process.env.KAZZLE_APP_COMPONENT_URL}/connect?token=${identity}`,
});
```

Страница выполняется в iframe с кросс-ориджином (её собственный источник, cookies и скрипты). Чтобы изменить размер карточки, отправьте её высоту родителю — карточка слушает это и изменяет размер (ограничено 70% высоты viewport):

```ts theme={"theme":"material-theme-darker"}
// inside the embedded page
parent.postMessage({ __kazzle_height: document.body.scrollHeight }, '*');
```

Предпочитайте `embedUrl` вместо выдачи HTML-строк: ваше приложение уже предоставляет страницы, интерфейс остаётся интерактивным и версионируется вместе с вашим кодом, и никакие артефакты для каждого вызова не записываются никуда.
