Tool Calls

Bir ajanın ne yaptığını adım başına bir satırla gösterin. Okuma ve aramalar kısa bir özete katlanırken düzenlemeler, komutlar, onaylar ve hatalar görünür kalır. Her adım gerçek bir görünüme açılır: dosya, fark, terminal ya da sonuçlar. Onay yerine gerekçe verme seçeneği dahil, her AI SDK araç durumu kapsanır.

Ajanlar yanıt vermeden önce düzinelerce küçük adım atabilir. Her birini göstermek yanıtı gömer, gizlemek ise ajanı kara kutu gibi hissettirir. Tool Calls her adıma, “Read components/search.tsx” ya da “Searched for useResults, 3 results” gibi cümle gibi okunan sakin bir satır verir.

Art arda çalışan okumalar, aramalar ve sorgular “Explored 6 files” gibi tek bir özete katlanır. Bir şeyi değiştiren, karar gerektiren ya da başarısız olan her şey, çalışırken canlı bir zamanlayıcıyla kendi satırında kalır. Gerçek işi görmek için herhangi bir adımı açın: dosya, fark, terminal ya da sonuçlar.

Her AI SDK araç durumunun, onaylar dahil, kendi görünümü ve ifadesi vardır. Onaylama; Allow, Deny, Always allow ve sözlerinizi gerekçe olarak geri gönderen “Tell it what to do instead” seçeneklerini, klavyeden ⌘↵ ve ⌘⌫ ile sunar. getToolPartStatus araç parçalarını sizin için eşler ve hazır görünümler arama sonuçlarını, dosya listelerini, çoktan seçmeli soruları ve ham JSON'u kapsar.

  1. Pro registry'yi components.json dosyasına ekleyin

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Token'ınızı ekleyin

    Hesap sayfanızda bir token oluşturun ve .env.local dosyasına HEXTAUI_PRO_TOKEN olarak ekleyin.

  3. Bloğu ekleyin

    pnpm dlx shadcn@latest add @hextaui-pro/tool-calls

AI SDK ile

getToolPartStatus ile araç parçalarını adımlara dönüştürün ve her araç için bir kind ve view seçin. Sohbet akışı durunca stopped geçin; böylece yarıda kesilen bir çağrı sonsuza dek dönmek yerine Cancelled olarak görünür.

"use client"

import type * as React from "react"
import { useChat } from "@ai-sdk/react"
import {
  getToolName,
  isToolUIPart,
  lastAssistantMessageIsCompleteWithApprovalResponses,
  type DynamicToolUIPart,
  type ToolUIPart,
  type UIMessage,
} from "ai"

import { CodeBlock } from "../code-block/code-block"
import { CodeTerminal } from "../code-block/code-terminal"
import type { ToolCallProps } from "@/components/blocks/tool-calls/tool-call"
import { ToolCalls } from "@/components/blocks/tool-calls/tool-group"
import {
  getToolPartApproval,
  getToolPartStatus,
} from "@/components/blocks/tool-calls/tool-part"
import { ToolJson } from "@/components/blocks/tool-calls/tool-views"

type ToolPart = ToolUIPart | DynamicToolUIPart
type Fields = Record<string, string | number | undefined>

function describe(part: ToolPart): Partial<ToolCallProps> {
  const input = (part.input ?? {}) as Fields
  const output = (part.state === "output-available" ? part.output : {}) as Fields
  const done = part.state === "output-available"

  switch (getToolName(part)) {
    case "readFile":
      return {
        kind: "read",
        subject: String(input.path ?? ""),
        content: done ? (
          <CodeBlock
            code={String(output.content)}
            filename={String(input.path)}
            lineNumbers
          />
        ) : undefined,
      }
    case "editFile":
      return {
        kind: "edit",
        subject: String(input.path ?? ""),
        content: input.diff ? (
          <CodeBlock
            code={String(input.diff)}
            filename={String(input.path)}
            streaming={part.state === "input-streaming"}
            diff
          />
        ) : undefined,
      }
    case "runCommand":
      return {
        kind: "run",
        subject: String(input.command ?? ""),
        exitCode: done ? Number(output.exitCode) : undefined,
        content: (
          <CodeTerminal
            command={String(input.command ?? "")}
            output={String(output.stdout ?? "")}
            running={!done && part.state !== "approval-requested"}
            exitCode={done ? Number(output.exitCode) : undefined}
          />
        ),
      }
    default:
      return {
        kind: "other",
        name: getToolName(part),
        content: (
          <ToolJson
            input={part.input}
            output={done ? part.output : undefined}
            streaming={part.state === "input-streaming"}
          />
        ),
      }
  }
}

function AssistantMessage({
  message,
  streaming,
  onApproval,
}: {
  message: UIMessage
  streaming: boolean
  onApproval: (id: string, approved: boolean, reason?: string) => void
}) {
  const blocks: (React.ReactNode | ToolCallProps[])[] = []

  message.parts.forEach((part, index) => {
    if (isToolUIPart(part)) {
      const call: ToolCallProps = {
        id: part.toolCallId,
        title: part.title,
        status: getToolPartStatus(part, { stopped: !streaming }),
        approval: getToolPartApproval(part),
        error: part.state === "output-error" ? part.errorText : undefined,
        onApprove: () => part.approval && onApproval(part.approval.id, true),
        onDeny: (reason) =>
          part.approval && onApproval(part.approval.id, false, reason),
        ...describe(part),
      }
      const last = blocks.at(-1)
      if (Array.isArray(last)) last.push(call)
      else blocks.push([call])
    } else if (part.type === "text") {
      blocks.push(<p key={index}>{part.text}</p>)
    }
  })

  return (
    <div className="flex flex-col gap-4">
      {blocks.map((block, index) =>
        Array.isArray(block) ? (
          <ToolCalls key={block[0].id} calls={block} />
        ) : (
          <div key={index}>{block}</div>
        )
      )}
    </div>
  )
}

export function Chat() {
  const { messages, status, addToolApprovalResponse } = useChat({
    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
  })
  const busy = status === "submitted" || status === "streaming"

  return messages.map((message, index) =>
    message.role === "assistant" ? (
      <AssistantMessage
        key={message.id}
        message={message}
        streaming={busy && index === messages.length - 1}
        onApproval={(id, approved, reason) =>
          addToolApprovalResponse({ id, approved, reason })
        }
      />
    ) : null
  )
}

Komutları onaylama

Sunucuda bir aracı needsApproval ile işaretleyin ve addToolApprovalResponse ile yanıtlayın. Always allow programı hatırlar ve bir ret, yerine ne yapılacağını gerekçe olarak taşıyabilir. Adım odaktayken ⌘↵ ve ⌘⌫ çalışır.

"use client"

import * as React from "react"
import { useChat } from "@ai-sdk/react"
import {
  isToolUIPart,
  lastAssistantMessageIsCompleteWithApprovalResponses,
} from "ai"

import { CodeTerminal } from "../code-block/code-terminal"
import { ToolCall } from "@/components/blocks/tool-calls/tool-call"
import { getToolPartApproval, getToolPartStatus } from "@/components/blocks/tool-calls/tool-part"

function program(command: string) {
  return command.trim().split(/\s+/)[0] ?? ""
}

export function Chat() {
  const [allowed, setAllowed] = React.useState<string[]>([])
  const { messages, status, addToolApprovalResponse } = useChat({
    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
  })
  const streaming = status === "submitted" || status === "streaming"

  React.useEffect(() => {
    for (const part of messages.at(-1)?.parts ?? []) {
      if (part.type !== "tool-runCommand") continue
      if (part.state !== "approval-requested") continue
      const command = String((part.input as { command?: string }).command)
      if (allowed.includes(program(command))) {
        addToolApprovalResponse({ id: part.approval.id, approved: true })
      }
    }
  }, [messages, allowed, addToolApprovalResponse])

  return messages.flatMap((message) =>
    message.parts.map((part) => {
      if (!isToolUIPart(part) || part.type !== "tool-runCommand") return null
      const command = String((part.input as { command?: string })?.command ?? "")
      const output = part.state === "output-available"
        ? (part.output as { stdout: string; exitCode: number })
        : undefined

      return (
        <ToolCall
          key={part.toolCallId}
          id={part.toolCallId}
          kind="run"
          subject={command}
          status={getToolPartStatus(part, { stopped: !streaming })}
          exitCode={output?.exitCode}
          approval={{
            ...getToolPartApproval(part),
            alwaysLabel: `Always allow ${program(command)}`,
          }}
          onApprove={({ always }) => {
            if (!part.approval) return
            if (always) setAllowed((list) => [...list, program(command)])
            addToolApprovalResponse({ id: part.approval.id, approved: true })
          }}
          onDeny={(reason) => {
            if (!part.approval) return
            addToolApprovalResponse({
              id: part.approval.id,
              approved: false,
              reason,
            })
          }}
          content={
            <CodeTerminal
              command={command}
              output={output?.stdout}
              exitCode={output?.exitCode}
              running={part.state === "approval-responded"}
            />
          }
        />
      )
    })
  )
}

Kullanıcıya sorma

Execute fonksiyonu olmayan bir istemci aracı yanıt bekler. Seçenekleri AskUser ile gösterin ve seçimi addToolOutput ile geri gönderin.

"use client"

import { useChat } from "@ai-sdk/react"
import { lastAssistantMessageIsCompleteWithToolCalls } from "ai"

import { ToolCall } from "@/components/blocks/tool-calls/tool-call"
import { getToolPartStatus } from "@/components/blocks/tool-calls/tool-part"
import { AskUser, type AskOption } from "@/components/blocks/tool-calls/tool-views"

export function Chat() {
  const { messages, addToolOutput } = useChat({
    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
  })

  return messages.flatMap((message) =>
    message.parts.map((part) => {
      if (part.type !== "tool-askUser") return null
      const input = (part.input ?? {}) as {
        question?: string
        options?: AskOption[]
      }

      return (
        <ToolCall
          key={part.toolCallId}
          id={part.toolCallId}
          kind="ask"
          subject={input.question}
          status={getToolPartStatus(part, { waiting: true })}
          content={
            <AskUser
              question={input.question ?? ""}
              options={input.options ?? []}
              answer={
                part.state === "output-available"
                  ? String(part.output)
                  : undefined
              }
              onAnswer={(value) =>
                addToolOutput({
                  tool: "askUser",
                  toolCallId: part.toolCallId,
                  output: value,
                })
              }
            />
          }
        />
      )
    })
  )
}

Yapı

Dıştan içe doğru birleştirdiğiniz parçalar.

ParçaAçıklama
ToolCallsListe. Sessiz çalışmaları gruplar ve adımlar arasında zamanlamayı paylaştırır.
ToolCallBir adım: simge, cümle, meta, zamanlayıcı ve açıldığında içeriği.
ToolGroupBir özet satırıyla katlanmış sakin adımlar dizisi.
ToolApprovalOnay bekleyen bir adımın içinde gösterilen onayla, reddet ve yönlendir kartı.
SearchResults, FileList, AskUser, ToolJsonBir adımın içeriği olarak geçirilecek görünümler.

ToolCalls

Ayrıca children dışındaki her ol prop'unu kabul eder.

PropTürVarsayılan
callsÇalıştıkları sırayla adımlar.
ToolCallProps[]–
groupSakin adım dizilerini katlar. Her adımı göstermek için false yapın.
booleantrue
PropTürVarsayılan
idKararlı id, genellikle toolCallId. Zamanlama id başına tutulur.
string–
statusstreaming, running, waiting, approval, done, error, denied veya cancelled.
ToolStatus–
kindread, search, list, edit, write, run, web, fetch, ask veya other. Simgeyi ve fiili belirler.
ToolKind"other"
subjectÜzerinde işlem yaptığı şey: bir yol, sorgu, URL veya komut.
string–
nameAraç adı; kind değeri other olduğunda cümle için kullanılır.
string–
titleÜretilen cümleyi tamamen değiştirir.
string–
metaCümleden sonra “3 results” veya “+12 −3” gibi kısa sonuç.
ReactNode–
contentAdım açıldığında gösterilir.
ReactNode–
exitCodeÇalıştırma adımları için. 0 dışı adımı başarısız olarak işaretler ve açar.
number–
errorerror durumu için gösterilen mesaj.
string–
durationGeçmiş için saniye cinsinden saklanan süre.
number–
approvalOnaylar için gerekçe, sonuç ve her zaman izin ver etiketi.
ToolCallApproval–
defaultOpenAdımın açık başlayıp başlamayacağını geçersiz kılar.
boolean–
onApproveAllow veya Always allow ile çağrılır.
(options: { always: boolean }) => void–
onDenyDeny ile ya da “Tell it what to do instead” metniyle çağrılır.
(reason?: string) => void–
PropTürVarsayılan
reasonDüğmelerin üstünde gösterilen, onayın neden gerektiği.
string–
approvedVerildikten sonra yanıt.
boolean–
automaticBir kuralla onaylandı; bu yüzden kart gösterilmez.
boolean–
denialReasonReddedilen adımlarda gösterilen, kişinin bunun yerine istediği şey.
string–
alwaysLabelBu etiketle “Always allow …” gösterir, örneğin program adı.
string–

getToolPartStatus(part, options)

Bir AI SDK araç parçasını ToolStatus'a eşler.

PropTürVarsayılan
partmessage.parts içindeki parça.
ToolUIPart | DynamicToolUIPart–
options.stoppedSohbet durdu; bu yüzden tamamlanmamış çağrılar dönmek yerine Cancelled olarak görünür.
booleanfalse
options.waitingBir istemci aracı kişiyi bekliyor, örneğin AskUser.
booleanfalse
PropTürVarsayılan
questionSoru.
string–
optionsSeçenekler.
{ value, label, description? }[]–
answerYanıtlandıktan sonra seçilen değer.
string–
onAnsweraddToolOutput ile geri gönderin.
(value: string) => void–
PropTürVarsayılan
queryHer eşleşmenin içinde vurgulanır.
string–
matchesEşleşmeler.
{ path, line, text }[]–
limit“Show all” öncesinde gösterilir.
number6
TuşAction
EnterSpaceBir adımı ya da katlanmış bir grubu açar veya kapatır.
⌘↵Odak içindeyken onay bekleyen bir adıma izin verir.
⌘⌫Odak içindeyken onay bekleyen bir adımı reddeder.
Esc“Tell it what to do instead” alanından göndermeden çıkar.
  • Adımlar sıralı bir listedir; bu sayede ekran okuyucular kaç adım olduğunu ve her birinin nerede durduğunu duyurur.
  • Kişinin müdahalesini gerektiren adımlar, sorular ve hatalarla birlikte kibar (polite) bir canlı bölge üzerinden duyurulur; örneğin “Approval needed: Run pnpm test”. Rutin adımlar sessiz kalır, böylece ekran okuyucular gereksiz yere yüklenmez.
  • Durum asla yalnızca renkle belirtilmez: her durumun kendi simgesi ve ifadesi vardır.
  • Onay kısayolları bir alana yazarken yok sayılır ve yönlendirme alanı açıldığında kendine odaklanır, kapandığında odağı geri verir.

Şunlarla geliştirildi

Tool Calls bloğunun yapıldığı ücretsiz HextaUI bileşenleri. Her biri tek başına yüklenir.

Kod

8 dosya, components/blocks/tool-calls konumuna eklendi.