← Создание агентов на Cloudflare / agents / harnesses / think
Клиентские инструменты
Think поддерживает инструменты, которые выполняются в браузере. Клиент отправляет сериализуемые схемы инструментов в теле запроса чата, Think объединяет их с серверными инструментами, и когда LLM вызывает клиентский инструмент, вызов направляется клиенту для выполнения.
Определение клиентских инструментов
Для динамических инструментов на стороне клиента передайте tools к useAgentChat. Инструменты с execute функции регистрируются на сервере как инструменты, выполняемые на стороне клиента:
const { messages, sendMessage } = useAgentChat({
agent,
tools: {
getUserTimezone: {
description: "Get the user's timezone from their browser",
parameters: {},
execute: async () => {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
},
},
getClipboard: {
description: "Read text from the user's clipboard",
parameters: {},
execute: async () => {
return navigator.clipboard.readText();
},
},
},
});const { messages, sendMessage } = useAgentChat({
agent,
tools: {
getUserTimezone: {
description: "Get the user's timezone from their browser",
parameters: {},
execute: async () => {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
},
},
getClipboard: {
description: "Read text from the user's clipboard",
parameters: {},
execute: async () => {
return navigator.clipboard.readText();
},
},
},
});Клиентские инструменты: это инструменты без execute функцию на сервере, у них есть только схема. Когда LLM формирует вызов инструмента для одного из них, Think направляет его клиенту.
В большинстве приложений лучше определять инструменты на сервере и использовать onToolCall для выполнения только в браузере. tools опция наиболее полезна для SDK или платформ, где браузер сам определяет доступный набор инструментов во время выполнения.
Клиентские инструменты через RPC дочернего агента chat() путь
Когда родительский агент делегирует задачу дочернему агенту Think через RPC с chat() (а не браузерный WebSocket), нет WebSocket для передачи clientTools или для отправки результатов инструментов обратно. Передавайте их через ChatOptions вместо этого:
await child.chat(message, callback, {
signal,
clientTools: [
{
name: "get_user_timezone",
description: "Get the caller's timezone",
parameters: { type: "object" },
},
],
onClientToolCall: async ({ toolName, input }) => {
// Run the client tool wherever the parent can — return its output.
return runClientTool(toolName, input);
},
});await child.chat(message, callback, {
signal,
clientTools: [
{
name: "get_user_timezone",
description: "Get the caller's timezone",
parameters: { type: "object" },
},
],
onClientToolCall: async ({ toolName, input }) => {
// Run the client tool wherever the parent can — return its output.
return runClientTool(toolName, input);
},
});clientToolsрегистрирует схемы инструментов для этого хода, точно так же, как WebSocketclientToolsполе.onClientToolCallвыполняет вызов клиентского инструмента и возвращает его результат. Модель может вызвать клиентский инструмент, получить результат и продолжить работу, и всё это в рамках одногоchat()вызов.
Если вы не укажете onClientToolCall, инструменты регистрируются, но не имеют результата: вызов модели передаётся через колбэк потока, и ход завершается с незавершённым вызовом инструмента (у колбэка RPC-потока нет собственного канала для входящего результата). Укажите onClientToolCall каждый раз, когда должен завершиться полный цикл запроса и ответа.
Замечания о поведении
- Восстановление: схемы и
onClientToolCallexecutor существуют только в рамках одного хода и никогда не сохраняются (executor представляет собой живую RPC-ссылку, которая исчезает вместе с изолятом, и, в отличие от пути через WebSocket, здесь нет клиента, который мог бы повторно воспроизвестиtool-resultпосле вытеснения). Если вытеснение прерывает ход в момент выполнения клиентского вызова инструмента, восстановление чата завершает осиротевший вызов с ошибкой (как серверный инструмент), и модель продолжает работу. Чтобы выполнить всё заново корректно, родитель повторно вызываетchat()наclientToolsиonClientToolCallснова. - Ошибки: если
onClientToolCallвыбрасывает исключение, сбой передаётся модели как ошибка инструмента (output-error) и ход продолжается: он не приводит к сбою хода. - Сериализация: значение, возвращённое из
onClientToolCallстановится выводом инструмента, поэтому оно должно быть сериализуемым в JSON (оно передаётся обратно через RPC и попадает в контекст модели). - Без шлюза подтверждения: Клиентские RPC-инструменты выполняются немедленно через
onClientToolCall. Процесс подтверждения через WebSocket (needsApproval) не применяется на этом пути. Если она вам нужна, контролируйте выполнение прямо внутри своего executor. - Приоритет имени: клиентские инструменты объединяются после серверных, поэтому клиентский инструмент с тем же именем, что и серверный (например, инструмент workspace), переопределяет его в рамках этого хода: так же, как и в случае с WebSocket.
- Прервать: прерывании хода через
signalостанавливает цикл, но выполняющийсяonClientToolCallсам по себе не отменяется: ход завершается только после того, как разрешится текущий вызов.
Поток подтверждения
Обрабатывайте выполнение инструментов на стороне браузера в клиенте с помощью onToolCall:
useAgentChat({
agent,
onToolCall: async ({ toolCall, addToolOutput }) => {
if (toolCall.toolName === "read") {
const result = await readFromBrowser(toolCall.input);
addToolOutput({
toolCallId: toolCall.toolCallId,
output: result,
});
}
},
});useAgentChat({
agent,
onToolCall: async ({ toolCall, addToolOutput }) => {
if (toolCall.toolName === "read") {
const result = await readFromBrowser(toolCall.input);
addToolOutput({
toolCallId: toolCall.toolCallId,
output: result,
});
}
},
});Автопродолжение
После получения результата клиентского инструмента Think автоматически продолжает разговор без нового сообщения пользователя. У хода продолжения есть continuation: true в TurnContext, которую вы можете использовать в beforeTurn чтобы настроить выбор модели или инструментов.
Когда за один ход выполняется сразу несколько клиентских вызовов инструментов, Think ожидает all их результатов перед запуском единого продолжения, вместо того чтобы запускать по одному продолжению на каждый результат. Немедленный запрос на возобновление, поступивший в тот момент, когда продолжение уже ожидает выполнения, присоединяется к этому ожидающему продолжению, а не запускает дубликат, и на стороне сервера needsApproval продолжения надёжно возобновляются, как только подтверждение зафиксировано.
Переживает перезапуски в ожидании участия человека
Durable Object может быть вытеснен в любой момент, в том числе пока ход приостановлен на запросе подтверждения или клиентском вызове инструмента. Think устойчивое восстановление всегда включён. SDK считает такой ход ожидающим человека, а не зависшим. Он приостанавливает ход вместо того, чтобы завершить его с ошибкой. Итоговое подтверждение пользователя или результат инструмента возобновляет разговор.
Какие взаимодействия не учитываются в бюджетах восстановления, см. в Ходы, ожидающие человека, не опечатываются.
Параллелизм сообщений
messageConcurrency свойство определяет, как ведут себя перекрывающиеся отправки пользователя, когда ход чата уже активен.
| Стратегия | Поведение |
|---|---|
"queue" |
Ставить в очередь каждую отправку и обрабатывать их по порядку. По умолчанию. |
"latest" |
Сохраняется только последняя из перекрывающихся отправок; вытесненные отправки по прежнему сохраняют свои сообщения пользователя, но не запускают ход модели |
"merge" |
Ставить в очередь параллельные отправки, а затем объединять их последние сообщения пользователя в один общий ход перед запуском последнего хода из очереди |
"drop" |
Полностью игнорируйте перекрывающиеся отправки. Сообщения не сохраняются. |
{ strategy: "debounce", debounceMs?: number } |
Trailing-edge latest с окном ожидания (по умолчанию 750 мс). |
import { Think } from "@cloudflare/think";
export class SearchAgent extends Think {
messageConcurrency = "latest";
getModel() {
/* ... */
}
}import { Think } from "@cloudflare/think";
import type { MessageConcurrency } from "@cloudflare/think";
export class SearchAgent extends Think<Env> {
override messageConcurrency: MessageConcurrency = "latest";
getModel() {
/* ... */
}
}Трансляция между вкладками
Think рассылает потоковые ответы всем подключённым клиентам WebSocket. Если к одному и тому же агенту подключено несколько вкладок браузера, все вкладки видят потоковый ответ в реальном времени. Состояния вызовов инструментов (ожидание, результат, подтверждение) рассылаются во все вкладки.
Программный chat() ходов и clearMessages() также транслируют обновления сообщений подключённым useAgentChat клиентов, поэтому клиенты в браузере остаются синхронизированными без переподключения.