Справочник по созданию инструментов

Справочник по контрактам, которым обязан соответствовать инструмент, обращённый к модели. Если вам нужен первый инструмент по шагам, идите в Создание инструмента. packages/shell/tool-bash — боевой пример из трёх пакетов.

Минимальная форма

import { readFile } from 'node:fs/promises'
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'my-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'read_file',
    description: 'Read a file from disk.',          // это видит модель
    parameters: {
      path: { type: 'string', required: true, description: 'Absolute path' },
      limit: { type: 'number' },                     // по умолчанию необязательный
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args, exec) {
      // args ТИПИЗИРОВАН по схеме: { path: string; limit?: number }
      // exec несёт неизменяемую идентичность + токен; signal — рабочее поле
      return readFile(args.path, { encoding: 'utf8', signal: exec.signal })
    },
  }))
}

Регистрация построена на эффектах: освобождение fiber плагина снимает регистрацию инструмента. Схемы попадают в сборку системного промпта автоматически.

Правила контракта execute()

  • Аргументы проверяются за вас. defineTool сверяет сгенерированные моделью arguments с единой ParameterSchemaSpec до запуска execute (типы, обязательные ключи, литеральные ограничения, объединения «ровно один из», вложенные значения), так что внутри execute аргументы соответствуют InferArgs. Явные узлы-объекты объявляют additionalProperties: true | false; неявный корень параметров остаётся открытым. Ограничения, которые DSL не выражает, — непустые строки, положительные числа, правила между полями — вы по-прежнему проверяете руками. Инструменты на сырой JSON-схеме, зарегистрированные напрямую, сами отвечают за проверку входа.
  • Регистрация заимствует ваше определение только на чтение. Типизированный вклад в том же процессе — не граница сериализации; не меняйте его схему и не подменяйте колбэки после регистрации. schemas() материализует только явную проекцию для модели. Чтобы подменить инструмент на горячую, освободите владеющий им эффект и зарегистрируйте замену; изменяемое состояние в замыкании колбэка остаётся обычным состоянием плагина.
  • Идентичность выполнения защищена. Реестр материализует arguments как отсоединённый JSON без потерь за один рекурсивный проход, замораживает это значение до начала политики и назначает непрозрачный exec.token; callId, name, arguments, agent, token, обязательный signal со стороны вызывающего и необязательный токен parent от объемлющего транспорта остаются неизменяемыми на всём пути диспетчеризации. parent — только идентичность, живого внешнего выполнения он не открывает. Считайте args входом только для чтения. Изменяемое представление получает только обёртка вокруг диспетчеризации: она может заменить и восстановить обязательный exec.signal, чтобы задать дедлайн, но убрать его не может.
  • Объявляйте и возвращайте одно каноническое JSON-значение. output.schema использует ValueSchemaSpec, корнем может быть объект, массив, скаляр или null. execute возвращает только выведенное значение; реестр снимает его как JSON без потерь, проверяет, замораживает и передаёт в output.render(args, value). Не возвращайте блоки контента из тела и не заставляйте вызывающих вытаскивать идентификаторы и поля из прозы.
  • Исключение или невалидное значение означают isError. Реестр ловит исключения и локализует сбои схемы, рендерера, проектора метаданных и JSON без потерь до того, как отработают наблюдатели. Бросайте исключения при инфраструктурных сбоях. Успешный доменный исход выражайте в каноническом значении, даже если его Native-рендерер объясняет неидеальное состояние, например ненулевой код выхода процесса.
  • Уважайте exec.signal. Когда он срабатывает, отменяйте текущую работу.
  • Проецируйте долговечные данные карточки через presentationMeta (необязательно). output.presentationMeta(args, value) выводит воспроизводимый JSON из того же канонического значения. Ядро сохраняет его в tool/result и передаёт в presentResult, поэтому карточка, которой нужны факты времени результата — например применённые ханки write/edit, — переживает воспроизведение без сохранения канонического значения. Для вложенных диспетчеризаций Code проектор пропускается: карточек у них нет.
  • Для асинхронных уведомлений используйте exec.agent. agent.inject({ content, source: { kind: 'plugin', plugin: '<name>' } }) добавляет долговечный контекст, который увидит СЛЕДУЮЩИЙ запрос к модели, — это не пробуждение (простаивающий агент так и простаивает). Защищайтесь от освобождённых агентов (try/catch).

Длительная работа

Открывайте run_in_background через конфиг производителя, а регистрируйте через ctx.jobs.start({ kind, label, owner: exec.agent, run }). Реестр отклоняет уже отменённый вызов до тела производителя; рантайм проверяет владение и доступность контроллера задач до того, как run() начнёт работу, а затем выдаёт идентификатор, ограждение сессии, общие инструменты управления, уведомления и очистку владельца. Успешная фоновая ветка возвращает типизированный канонический дескриптор вроде { kind: 'background', jobId }; её Native-рендерер может оставить человеческую прозу вроде started background job bash-1, но режим PTC никогда не должен разбирать эту прозу, чтобы достать идентификатор.

Спецификация задания отдаёт синхронный cancel, done, который не отклоняется и завершается после очистки ресурсов, и вывод одним из двух способов: pull-источники output, которые реестр перекачивает в кольцевой буфер вывода задания, либо push через JobHandle, который получает стартовая функция. Потребляющее чтение для модели из этого буфера оформляет dsh-tool-jobs. Уже отменённый вызов — это ошибка, потому что нет задачи, чей идентификатор удовлетворил бы успешной схеме вывода. Как только ctx.jobs.start() опубликовал идентификатор, используйте сигнал отмены, принадлежащий задаче, а не exec.signal: более поздняя отмена внешнего вызова прекращает ожидание вызова, но не убивает опубликованную работу; этим временем жизни владеют job_kill, освобождение владельца и остановка сервиса. Работа на переднем плане остаётся привязанной к exec.signal. См. Agent Note о рантайме фоновых заданий и dsh-tool-bash как пример потокового производителя.

Политика выполнения и наблюдение

Старайтесь не встраивать политику развёртывания в инструмент. Используйте tools/pre-execute для расширяемой политики allow/deny/ask (пример шлюза разрешений), ctx.tools.guard() для окончательного монотонного запрета, который последующие слушатели не отменят, tools/execute, чтобы обернуть диспетчеризацию дедлайном, повтором или сбором метрик, tools/post-execute, чтобы заменить контент представления или возвращённое значение, заблокировать результат или прикрепить контекст для модели, и tools/result, чтобы наблюдать неизменяемый нормализованный исход. Замена контента оставляет программный доступ к value нетронутым; политика конфиденциальности блокирует или заменяет значение. Реализация песочницы может работать и внутри исполнителя самого инструмента; README dsh-tools определяет входы, порядок, возвращаемые значения и поведение при сбое для каждой точки расширения.

Режим PTC получает ваш инструмент бесплатно

В режиме PTC каждый видимый зарегистрированный инструмент доступен как await tools.<name>(args) без дополнительной интеграции. Сгенерированные ToolArgsMap и ToolOutputMap выводят точные типы аргументов и канонического возврата из тех же схем, а вызовы заново проходят обычный конвейер выполнения. Успешный вызов разрешается в итоговое каноническое JSON-значение после политики, а не в отрендеренный Native-контент. Неудачный вызов отклоняется с настоящим ToolCallError; программы могут смотреть только его name, toolName и человекочитаемое message, но не внутренние коды ошибок или объединение отказов.

Проектируйте output.schema как полезный программный API: возвращайте дескрипторы и поля напрямую, допускайте корень-скаляр, массив или null, когда это честное значение, а человеческое объяснение держите в output.render. Промежуточные значения локальны для выполнения, не сохраняются, не усекаются под промпт и не имеют лимита по байтам, поэтому честные границы получения данных у производителя и память процесса по-прежнему важны. Через настраиваемый лимит вывода и конвейер сброса для модели проходят только внешние логи и результат run_code.

Как ваш инструмент рендерится в UI

output.render инструмента возвращает контент для модели; его UI-карточка — отдельная забота, объявленная через чистые проекции представления и необязательные методы presentCall / presentResult. Проектируйте их вместе с каноническим значением. Инструмент без UI-представления получает общую карточку (заголовок = имя инструмента, сырые аргументы как вход).

Оба метода возвращают намерение рендера с тегом card — выберите вид карточки под то, что делает инструмент:

  • presentCall(args) → ToolCallView (карточка ОЖИДАНИЯ):
    • { card: 'generic', title, kind?, rawInput?, content?, locations? } — по умолчанию. Задайте kind ради иконки (read/search/…); задайте locations: [{ path, line? }] для каждого файла, которого касается инструмент, чтобы подходящий редактор следовал за ним или прыгал к нему.
    • { card: 'terminal', title, description?, cwd? } — ваш вызов И ЕСТЬ команда оболочки. title — команда, description рендерится над карточкой терминала. (tool-bash.)
    • { card: 'diff', title, diffs, locations? } — ваш вызов создаёт или меняет файл. diffs: [{ path, oldText, newText }] (oldText: null для нового файла) рендерится как встроенная карточка диффа. (tool-fs write/edit.)
  • presentResult(args, { content, isError, meta? }) возвращает завершённую карточку:
    • generic даёт необязательный заголовок и контент.
    • terminal даёт сырой вывод и необязательные метаданные выхода; каждый UI рендерит свой полноценный или запасной вид.
    • diff даёт применённые ханки, часто выведенные через output.presentationMeta и перенесённые в сохранённом result.meta, чтобы воспроизведение их восстановило. Мутирующие инструменты сохраняют результат-дифф, потому что завершённый вид замещает карточку ожидания.
    • read даёт завершённое окно файла, восстановленное из сохранённого result.meta: path файла, offset с отсчётом от 1, возвращённые lines (каждая с номером строки в файле), totalLines и необязательную подсказку подсветки lang; UI без возможности read откатывается к сырому контенту результата. Вида вызова read нет — ожидание вызова чтения остаётся общей карточкой, потому что контент появляется только после execute. (tool-fs read.)
    • search даёт результат поиска, восстановленный из сохранённого result.meta: совпадения, сгруппированные по файлам (shape: 'matches', grep) или плоский список путей (shape: 'paths', glob), плюс truncated/total, чтобы UI никогда не показывал усечённый результат как полный. Вид не несёт текста результата (UI без карточки поиска откатывается к сырому контенту результата), и вида вызова search нет — ожидание вызова поиска остаётся общей карточкой, потому что совпадения появляются только после execute. (tool-fs-search grep/glob.)
    • web даёт завершённое обращение к вебу, различаемое по kind: 'search' | 'fetch' (структурированные источники поиска или сводка загрузки), выведенное из result.meta; тела текста в нём нет, поэтому UI без возможности web откатывается к сырому контенту результата. (tool-web web_search/web_fetch.)

Жёсткие правила (нарушите — укусят):

  • Чистота. Эти методы работают и при живом стриминге, И при ВОСПРОИЗВЕДЕНИИ лога сессии, поэтому обязаны быть чистыми функциями от args (+ результата) — НИКАКОГО I/O, НИКАКОГО чтения состояния сессии, НИКАКИХ часов и случайных чисел. Дифф выводится из аргументов (write использует oldText: null, потому что у презентера времени вызова нет прежнего содержимого файла); контекст сессии подставляет UI-адаптер, а не инструмент. Если вам захотелось старое содержимое файла или рабочий каталог внутри presentCall — остановитесь: это место в долговечных метаданных результата или в адаптере, а не в презентере.
  • UI-форматирование не попадает в результат для модели. Блок ```console, дифф, относительный путь — ничему из этого не место в каноническом значении или Native-контенте только ради UI. output.render владеет прозой для модели; presentationMeta вместе с презентерами карточек владеют воспроизводимым состоянием UI. Результат-вид terminal несёт сырой вывод, а запасную обёртку добавляет адаптер.
  • defineTool мягко проверяет путь отображения. При искажённых или устаревших залогированных аргументах обёртка возвращает undefined (общая карточка как запасной вариант), а не бросает исключение — отображение никогда не должно ронять воспроизведение.

Нейтральный словарь живёт в dsh-tools; инструменты никогда не импортируют типы UI или транспорта. Потребители этого API отображают каждый card в собственный вид. Дизайн и его обоснование — в Agent Note об объединении намерений рендера; эталонные реализации — dsh-tool-fs (generic/diff) и dsh-tool-bash (terminal).

Представление в Web Client

Встроенный Web Client не использует presentCall и presentResult. page и follow сессии передают сырые события tool/call и tool/result, включая сохранённый result.meta. Плагин Client регистрирует своё сетевое имя инструмента в слоте с ключом tool.call.toolview и выводит свойства компонента из аргументов ToolCallBlock, контента, ошибки, метаданных, parentCallId существующей диспетчеризации PTC и фактов о пути сессии. Эти сетевые значения он проверяет локально и возвращает общую строку для искажённого или неподдерживаемого входа.

Используйте output.presentationMeta(args, value), когда существующей Web-карточке нужны ограниченные структурированные факты о результате, которые контент для модели не может сохранить без потерь. Не храните в метаданных свойства React или выбранную карточку, не импортируйте реализацию Host-инструмента в браузерный бандл и не заводите ещё один реестр презентеров Client. Одни только методы представления на стороне Host специализированную Web-карточку не добавляют. Agent Note о представлении, выводимом на стороне Client определяет требования к владению, запасному варианту и эквивалентности.

Проверка

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

перевод и редактура:

обновлено

⌕ ESC