DeltaKitDeltaKit
OpenAI Agents

Display Tool Calls

This recipe shows how to display tool calls in your chat UI with loading states and results. You'll handle tool_call and tool_result events, then render them as rich UI components.

Handling Tool Call Events

First, set up onEvent to process tool_call and tool_result events:

import { useStreamChat } from "@deltakit/react";

const { messages } = useStreamChat({
  api: "/api/chat",
  onEvent: (event, { appendText, appendPart, setMessages }) => {
    switch (event.type) {
      case "text_delta":
        appendText(event.delta);
        break;

      case "tool_call":
        appendPart({
          type: "tool_call",
          tool_name: event.tool_name,
          argument: event.argument,
          callId: event.call_id,
        });
        break;

      case "tool_result":
        setMessages((msgs) => {
          const last = msgs[msgs.length - 1];
          if (last?.role !== "assistant") return msgs;

          const updatedParts = last.parts.map((part) => {
            if (part.type === "tool_call" && part.callId === event.call_id) {
              return { ...part, result: event.output };
            }
            return part;
          });

          return [...msgs.slice(0, -1), { ...last, parts: updatedParts }];
        });
        break;
    }
  },
});

When you provide onEvent, it replaces the default handler. You must handle text_delta yourself.

Rendering Parts

Build a switch component that renders each content part type:

import type { ContentPart } from "@deltakit/react";

function Part({ part }: { part: ContentPart }) {
  switch (part.type) {
    case "text":
      return <p>{part.text}</p>;
    case "tool_call":
      return <ToolCall part={part} />;
    case "reasoning":
      return <em>{part.text}</em>;
  }
}

Use it in your message list:

{messages.map((msg) => (
  <div key={msg.id}>
    <strong>{msg.role}:</strong>
    {msg.parts.map((part, i) => (
      <Part key={i} part={part} />
    ))}
  </div>
))}

Tool Call Component

A ToolCallPart has this shape:

interface ToolCallPart {
  type: "tool_call";
  tool_name: string;   // e.g. "get_weather"
  argument: string;    // JSON string of arguments
  callId?: string;     // Links to tool_result
  result?: string;     // Filled when the result arrives
}

Render it with a loading/result state:

import type { ToolCallPart } from "@deltakit/react";

function ToolCall({ part }: { part: ToolCallPart }) {
  // Parse the JSON argument for display
  let args: Record<string, unknown> = {};
  try {
    args = JSON.parse(part.argument);
  } catch {
    // ignore parse errors
  }

  return (
    <div style={{ background: "#f5f5f5", padding: 12, borderRadius: 8, margin: "8px 0" }}>
      <strong>{part.tool_name}</strong>
      <pre style={{ fontSize: 12 }}>{JSON.stringify(args, null, 2)}</pre>
      {part.result ? (
        <div>Result: {part.result}</div>
      ) : (
        <div style={{ color: "#888" }}>Running...</div>
      )}
    </div>
  );
}

Multiple Tool Calls

A single response can contain multiple tool calls. Each arrives as a separate tool_call/tool_result event pair, linked by callId:

data: {"type":"tool_call","tool_name":"search","argument":"...","call_id":"call_1"}
data: {"type":"tool_result","call_id":"call_1","output":"..."}
data: {"type":"tool_call","tool_name":"calculate","argument":"...","call_id":"call_2"}
data: {"type":"tool_result","call_id":"call_2","output":"..."}
data: {"type":"text_delta","delta":"Based on the results..."}
data: [DONE]

All parts end up in the same assistant message's parts array, rendered in order.

Server Setup

See Tool Calls (Protocol) for the server-side SSE format and a full FastAPI example.

On this page