# Tool calls & reasoning

Render an agent's thinking trace and tool invocations inline in a message, progressing through the four-state tool lifecycle.

Agents think before they act and invoke tools to get things done. `reasoning` and `tool` parts in a message's `parts` array bring both inline — one part per tool call, updated in place as the agent streams its output.

<ChatDemo client:only="solid" chatTitle="Research Agent" placeholder="Ask the agent anything…" messages={[
  { id: 'u1', role: 'user', parts: [{ type: 'text', text: 'What is the current price of NVDA stock and its 52-week range?' }] },
  {
    id: 'a1',
    role: 'assistant',
    parts: [
      {
        type: 'reasoning',
        text: "The user wants current market data for NVDA. I should use the stock_quote tool to fetch the price and the market_data tool to get the 52-week range. I'll call stock_quote first since I need the current price, then market_data for the range.",
        label: 'Agent reasoning',
      },
      {
        type: 'tool',
        tool: {
          type: 'stock_quote',
          state: 'output-available',
          toolCallId: 'call_abc123',
          input: { ticker: 'NVDA', exchange: 'NASDAQ' },
          output: { price: 875.4, currency: 'USD', timestamp: '2026-06-17T14:30:00Z' },
        },
      },
      {
        type: 'tool',
        tool: {
          type: 'market_data',
          state: 'output-available',
          toolCallId: 'call_def456',
          input: { ticker: 'NVDA', fields: ['52w_low', '52w_high'] },
          output: { '52w_low': 402.11, '52w_high': 974.0 },
        },
      },
      {
        type: 'text',
        text: "Here's what I found for NVIDIA (NVDA):\n\n- **Current price:** $875.40\n- **52-week low:** $402.11\n- **52-week high:** $974.00\n\nThe stock has nearly doubled from its 52-week low, driven by sustained demand for AI training hardware.",
      },
    ],
    actions: ['copy', 'like', 'dislike'],
  },
  { id: 'u2', role: 'user', parts: [{ type: 'text', text: 'Now look up the weather in San Francisco.' }] },
  {
    id: 'a2',
    role: 'assistant',
    parts: [
      {
        type: 'tool',
        tool: {
          type: 'get_weather',
          state: 'input-available',
          toolCallId: 'call_ghi789',
          input: { city: 'San Francisco', units: 'imperial' },
        },
      },
    ],
  },
]} reply={(prompt) => `I'll look that up for you. Running the necessary tool calls now — results will appear in a moment.`} />

## How it works

Both `tool` and `reasoning` are parts inside the `parts` array of the `ChatMessage` object you pass to `<kai-chat>` or `<kai-message>`. Update the matching part in place (immutably) as the agent streams. Since the message carries arrays, you assign it in JavaScript rather than as an HTML attribute.

```js

await customElements.whenDefined('kai-chat');

const chat = document.getElementById('chat');
const aId = crypto.randomUUID();
const callId = crypto.randomUUID();

// Append an empty assistant message the moment the agent starts
chat.messages = [
  ...chat.messages,
  { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: prompt }] },
  {
    id: aId,
    role: 'assistant',
    parts: [
      { type: 'reasoning', text: '', label: 'Agent reasoning' },
      {
        type: 'tool',
        tool: {
          type: 'search_web',
          state: 'input-streaming',   // tool call incoming — inputs not yet complete
          toolCallId: callId,
        },
      },
    ],
  },
];
chat.loading = true;

// As the stream arrives, replace the matching part with a new array reference
// to drive re-renders. Each state transition reassigns chat.messages.

// Input fully received → update state to 'input-available'
chat.messages = chat.messages.map((m) =>
  m.id === aId
    ? {
        ...m,
        parts: m.parts.map((p) =>
          p.type === 'tool'
            ? { ...p, tool: { ...p.tool, state: 'input-available', input: { query: 'AI/UI docs' } } }
            : p,
        ),
      }
    : m,
);

// Tool executed successfully → state becomes 'output-available'
chat.messages = chat.messages.map((m) =>
  m.id === aId
    ? {
        ...m,
        parts: m.parts.map((p) =>
          p.type === 'tool'
            ? { ...p, tool: { ...p.tool, state: 'output-available', output: { results: ['…'] } } }
            : p,
        ),
      }
    : m,
);

// Tool failed → state becomes 'output-error'
// chat.messages = chat.messages.map((m) =>
//   m.id === aId
//     ? { ...m, parts: m.parts.map((p) => p.type === 'tool' ? { ...p, tool: { ...p.tool, state: 'output-error', errorText: 'Rate limit exceeded' } } : p) }
//     : m
// );

// Stream the final reply into a text part, appended after reasoning + tool
let answer = '';
for await (const token of streamFromYourModel(prompt)) {
  answer += token;
  chat.messages = chat.messages.map((m) =>
    m.id === aId
      ? { ...m, parts: [...m.parts.filter((p) => p.type !== 'text'), { type: 'text', text: answer }] }
      : m,
  );
}
chat.loading = false;
```

The ergonomic path is `createAssistantStream` from `@kitn.ai/ui/state`: its `upsertTool(toolCallId, patch)` and `appendReasoning(delta)` handle the part-lookup and new-reference plumbing above for you.

**Tool lifecycle — four states:**

| `state` | Rendered as | When to set it |
|---|---|---|
| `input-streaming` | Spinning loader, "Processing" badge | Tool call chunk received; input is still arriving |
| `input-available` | Settings icon, "Ready" badge | Input complete; tool is executing |
| `output-available` | Check icon, "Completed" badge | Tool returned successfully |
| `output-error` | X icon, "Error" badge + `errorText` | Tool threw or returned an error |

**`reasoning`** is a `{ type: 'reasoning', text, label? }` part — append tokens to `text` as they stream in. The block auto-expands while reasoning is in progress if you use `<kai-reasoning streaming>` directly; via `<kai-chat>` or `<kai-message>` the block is collapsible once complete.

**`<kai-thinking-bar>`** is the pre-reasoning status bar — show it before the first reasoning token arrives. It fires `kai-stop` when the user clicks "Answer now" (requires `stoppable`):

```html
<kai-thinking-bar text="Thinking…" stoppable stop-label="Answer now"></kai-thinking-bar>

<script type="module">
  document.querySelector('kai-thinking-bar').addEventListener('kai-stop', () => {
    abortController.abort();
  });
</script>
```

## Next steps

- **[Drop-in chat](/examples/drop-in-chat/)** — the full `kai-submit` streaming loop.
- **[`kai-message` reference](/components/message/)** — complete `ChatMessage` shape, `actionsReveal`, `proseSize`.
- **[`kai-tool` reference](/components/tool/)** — `tool` property shape, `open` flag, state tokens.
- **[`kai-reasoning` reference](/components/reasoning/)** — `streaming` auto-expand, `kai-open-change`.
- **[`kai-thinking-bar` reference](/components/thinking-bar/)** — `text`, `stoppable`, `kai-stop`.
- **[Compose your own shell](/patterns/compose-your-own/)** — lay out the thread and composer without `<kai-chat>`.
