Tool Call
An assistant that calls tools does most of its work where the reader cannot see it. tool-call
is the window into one of those calls: what was called, where it stands, what it was given, and
what came back.
It composes primitives you already have — code for the name,
badge and shimmer for the state,
code-block for the arguments and
collapsible for the result. Nothing here is a new visual language.
Basic Usage
search_docs
Took 2 s
{
"query": "rate limits",
"limit": 4
}The Four States
A call is queued, in flight, finished or failed. The state is in the words as well as the color, because a reader who cannot tell the intents apart still has to be able to tell a finished call from a failed one.
fetch_invoice
Queued
get_weather
Running…
search_docs
Took 2 s
charge_card
Failed after 9 s
Two behaviors follow from the state on their own:
- A running call marks its block
aria-busyand shimmers its label, so assistive technology knows the region is still changing. - A failed call opens its disclosure. An error nobody sees is not reported; a result is reference material and stays closed.
Duration
Pass seconds and the state word gives way to how long the call took. The application owns
that number — it knows when the call started, and a stopwatch in the browser would disagree with
it the moment the page re-renders from the server.
A value that is not a number is no label at all rather than a cast: seconds="soon" would
otherwise read as "Took 0 s", which states something you never said.
Arguments
arguments takes an array and encodes it, or a string and prints it as given — so an application
that already holds the provider's raw JSON does not pay a decode and re-encode round trip to
display it.
The encoding is the readable one: slashes are left alone, so a URL argument does not arrive as
https:\/\/example.test.
Inside a Conversation
A sequence of calls reads as a sequence. Put them in a timeline when the
order carries meaning, or in a stack when it does not.
-
-
Status: Success 1.2s
list_invoicesTook 3 s{ "customer": 4711, "unpaid": true } -
4.4s
send_reminderRunning…{ "invoice": 8814 }
Streaming It From Livewire
The component is a render of server state, so a Livewire property drives it directly. Bind the status and the result; the disclosure is keyed on the status, so a call that settles while the reader has it open comes back in the state the new status implies rather than keeping the old one.
Keep the calls in a public property of the component:
<?php
namespace App\Livewire;
use Livewire\Component;
class AssistantTurn extends Component
{
/**
* One entry per tool call the turn made, in the order they happened. Update an entry
* while the call runs: its status moves from pending to running, then to done or failed.
*
* @var list<array{id: string, name: string, status: string, seconds?: int, arguments: array<string, mixed>|string, result?: string}>
*/
public array $toolCalls = [];
}
Then render one block per call:
{{-- 1. One block per call the turn made, in the order they happened. --}}
@foreach($this->toolCalls as $call)
{{-- 2. `wire:key` on the loop, so a re-render moves blocks instead of rebuilding them. --}}
<x-wirekit::tool-call
wire:key="tool-{{ $call['id'] }}"
:name="$call['name']"
:status="$call['status']"
:seconds="$call['seconds'] ?? null"
:arguments="$call['arguments']"
>
{{-- 3. The result is whatever you want a reader to see — text, a table, a link. --}}
{{ $call['result'] ?? '' }}
</x-wirekit::tool-call>
@endforeach
Give It a Column That Can Shrink
A tool call carries an arguments block, and the longest line in one is usually a URL that does
not wrap. The component takes min-w-0 so it shrinks inside a flex or grid parent instead of
insisting on its content width — but that caps the item, never the track.
A grid column written as the implicit auto track sizes itself to its widest child, and no
property on the child can change that. Measured at 390 px: four calls in a bare display: grid
came out 404 px wide, so the whole page scrolled sideways and the reader had to drag the column
to read one argument.
stack and grid already emit a track that can shrink.
Hand-written CSS needs to say so:
/* 1. `minmax(0, 1fr)`, not `1fr` — a bare `1fr` still has an `auto` minimum. */
.tool-calls { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.5rem; }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string |
null |
What was called. Shown as code and used in the arguments block's accessible name. |
status |
string |
pending |
pending, running, done or failed. Anything else is refused. |
arguments |
array|string |
null |
The arguments the model passed. An array is encoded as readable JSON; a string is printed as given. |
seconds |
int |
null |
How long the call took. Replaces the state word once it has settled. A non-numeric value is ignored. |
open |
bool |
null |
Whether the result disclosure opens. null follows the status: a failure opens, everything else stays closed. |
scope |
string |
null |
Personalization scope, as on every component. |
Accessibility
- A running call carries
aria-busy="true"on its block, so assistive technology knows the region is still changing. It is removed the moment the call settles. - The state is in the badge's text, never in its color alone (WCAG 1.4.1).
- The arguments block is a named scroll region — the name carries the tool, so ten calls on one page are ten distinguishable entries in a screen reader's rotor rather than ten called "JSON code".
- The result disclosure is
collapsibleand inherits its keyboard model and itsaria-expandedwiring unchanged.
Keyboard Interaction
| Key | Action |
|---|---|
| Enter / Space | Opens or closes the result, when the trigger has focus. |
See Also
- Assistant Message — the turn a tool call belongs to.
- Timeline — for a sequence of calls where the order carries meaning.
- Code Block — the arguments renderer.