Справочник по созданию инструментов
Справочник по контрактам, которым обязан соответствовать инструмент, обращённый к модели. Если вам нужен первый инструмент по шагам, идите в Создание инструмента. 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-fswrite/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-fsread.)searchдаёт результат поиска, восстановленный из сохранённогоresult.meta: совпадения, сгруппированные по файлам (shape: 'matches', grep) или плоский список путей (shape: 'paths', glob), плюсtruncated/total, чтобы UI никогда не показывал усечённый результат как полный. Вид не несёт текста результата (UI без карточки поиска откатывается к сырому контенту результата), и вида вызоваsearchнет — ожидание вызова поиска остаётся общей карточкой, потому что совпадения появляются только послеexecute. (tool-fs-searchgrep/glob.)webдаёт завершённое обращение к вебу, различаемое поkind: 'search' | 'fetch'(структурированные источники поиска или сводка загрузки), выведенное изresult.meta; тела текста в нём нет, поэтому UI без возможностиwebоткатывается к сырому контенту результата. (tool-webweb_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 и попадающее в релиз, требует собранного покрытия, которое там описано.