> ## 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` — це повторно використовувана адреса для інструменту або кнопки обов'язкової дії. Прямий виклик інструменту ШІ та клік кнопки можуть використовувати той самий об'єкт цілі, тому обидва запускають той самий код.

| Ціль     | Поля                                                                  | Запускається де                                                                                               |
| -------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `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`** — опціонально. Коротке резюме з багатим текстом, відтворене в карточці інструменту (react-markdown; сирий HTML екранується). Добре для речення, невеликого списку, вбудованого посилання. Голий відносний шлях відтворюється як мертвий попередньо відформатований текст — посилання повинні бути **абсолютними**.
* **`embedUrl`** — опціонально. **Абсолютна** URL, яку служить ваш додаток; карточка відтворює її в пісочниці iframe. Це те, як ви показуєте справжній, повноширокий інтерфейс — екран підключення, панель керування, діаграму. Ваш додаток розміщує та володіє сторінкою, тому вона може бути повністю інтерактивною проти вашого власного бекенду, файлів cookie та 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` після натискання кнопки. Відповідні клієнти бачать кнопки автора; невідповідні клієнти все ще бачать Пропустити/Скасувати плюс «Продовжити на {пристрій}». Кнопка без `target` подає свої `input` як результат інструменту. Кнопка з цільовою адресою спочатку запускає цю ціль, потім використовує результат цілі як результат інструменту.

### Багатий інтерфейс — вбудуйте сторінку, розміщену додатком

Коли результат інструменту є візуальним або інтерактивним (екран підключення, діаграма, панель керування резюме), служіть сторінку з одного з ваших компонентів та повертайте її **абсолютну** 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 (її власне походження, файли cookie та скрипти). Щоб змінити розмір карточки, опублікуйте її висоту батьківському — карточка слухає це та змінює розмір (обмежено 70% вікна перегляду):

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

Віддавайте перевагу `embedUrl` над випусканням рядків HTML: ваш додаток уже служить сторінкам, інтерфейс залишається інтерактивним та версіонованим з вашим кодом, і жодні артефакти за виклик не записуються ніде.
