Tool call
The row a reply shows when the model calls one of your tools — its name, what it was asked, what it returned, and whether it is running, waiting for a person or done.
-
type: 'tool_call'
<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant"></aparte-chat-bubble>
<script>
// A reply is a list of segments: here a finished tool call, then the answer it led to.
// Open the row to see what the model sent and what the tool returned.
document.querySelector('aparte-chat-bubble').setSegments([
{
id: 's1',
type: 'tool_call',
status: 'resolved',
toolCall: { id: 't1', name: 'get_weather', input: { city: 'Lille' } },
result: 'Cloudy, 14 °C, wind from the west at 20 km/h.',
},
{ id: 's2', type: 'text', content: 'It is **14 °C and cloudy** in Lille, with a westerly wind. Bring a jacket.' },
]);
</script>When the model calls a tool, the reply gets a tool_call segment and the bubble draws it as a
quiet row: the tool’s name and its status. Opening the row shows the input the model chose and
the result your handler returned. The row is drawn for you; a tool registered with
aparteGlobalConfig.registerTool() produces it during a turn without any code of yours.
Variants
Section titled “Variants”Every status
Section titled “Every status”<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant"></aparte-chat-bubble>
<script>
const call = (id, name, input) => ({ id, name, input });
document.querySelector('aparte-chat-bubble').setSegments([
// Running: the model asked, the handler has not answered yet.
{ id: 's1', type: 'tool_call', status: 'pending', toolCall: call('t1', 'search_docs', { query: 'release checklist' }) },
// Paused for a person (a tool marked needsApproval): the decision is asked at the composer.
{ id: 's2', type: 'tool_call', status: 'awaiting-approval', toolCall: call('t2', 'run_command', { cmd: 'pnpm publish' }) },
{ id: 's3', type: 'tool_call', status: 'resolved', toolCall: call('t3', 'read_file', { path: 'CHANGELOG.md' }), result: '## 0.19.2' },
{ id: 's4', type: 'tool_call', status: 'rejected', toolCall: call('t4', 'delete_file', { path: 'src/legacy.ts' }), result: 'The user rejected this tool call.' },
{ id: 's5', type: 'tool_call', status: 'aborted', toolCall: call('t5', 'fetch_page', { url: 'https://example.com' }) },
{ id: 's6', type: 'tool_call', status: 'failed', toolCall: call('t6', 'write_file', { path: '/etc/hosts' }), result: 'EACCES: permission denied' },
]);
</script>A call is pending while your handler runs, awaiting-approval when the tool is marked
needsApproval and a person has to decide (see Tools and approval), then
resolved, rejected, aborted or failed. Only the first two are still open.
A summary of what it ran on
Section titled “A summary of what it ran on”<!-- In your app: import { aparteGlobalConfig } from '@aparte/core'. -->
<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant"></aparte-chat-bubble>
<script>
const { aparteGlobalConfig } = window.aparte;
// One line after the tool's name: what it targeted. Plain text, never HTML; the row,
// its states and its disclosure stay the library's.
aparteGlobalConfig.registerToolSummary('read_file', (segment) => String(segment.toolCall.input.path ?? ''));
aparteGlobalConfig.registerToolSummary('search_docs', (segment) => `“${segment.toolCall.input.query}”`);
document.querySelector('aparte-chat-bubble').setSegments([
{ id: 's1', type: 'tool_call', status: 'resolved', toolCall: { id: 't1', name: 'search_docs', input: { query: 'release checklist' } }, result: '3 pages' },
{ id: 's2', type: 'tool_call', status: 'resolved', toolCall: { id: 't2', name: 'read_file', input: { path: 'docs/release.md' } }, result: '## Release' },
]);
</script>registerToolSummary(name, summary) adds one line after the tool’s name, so read_file reads
read_file docs/release.md without opening the row. It returns plain text; the row, its states
and its disclosure stay the library’s.
Your own rendering
Section titled “Your own rendering”<!-- In your app: import { aparteGlobalConfig } from '@aparte/core'. -->
<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant"></aparte-chat-bubble>
<script>
const { aparteGlobalConfig } = window.aparte;
// Every get_weather call is drawn by this instead of the default row. Returning an
// element (not a string) means the model's data never goes through innerHTML.
aparteGlobalConfig.registerToolRenderer('get_weather', {
render(segment) {
const card = document.createElement('div');
card.className = 'aparte-card';
const body = document.createElement('div');
body.className = 'aparte-card__body';
body.textContent = segment.status === 'resolved'
? `${segment.toolCall.input.city}: ${segment.result}`
: `Checking the weather in ${segment.toolCall.input.city}…`;
card.append(body);
return card;
},
});
document.querySelector('aparte-chat-bubble').setSegments([
{ id: 's1', type: 'tool_call', status: 'resolved', toolCall: { id: 't1', name: 'get_weather', input: { city: 'Lille' } }, result: 'cloudy, 14 °C' },
]);
</script>registerToolRenderer(name, renderer) replaces the row for one tool. Return an element rather
than an HTML string: the input and the result come from the model and your tool, so a string
would have to escape every value it interpolates. Add an update() to patch the element when
the result lands, instead of rebuilding it.
import { aparteGlobalConfig } from '@aparte/core';
aparteGlobalConfig.registerTool(
{
name: 'get_weather',
description: 'Current weather for a city.',
inputSchema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
},
async (call) => ({ toolCallId: call.id, content: `Cloudy, 14 °C in ${call.input.city}.` }),
);The client offers the tool to the model, runs the handler when it is called and draws the row for each call. Building a segment by hand, as these demos do, is for a reply your own code assembles.
Fields
Section titled “Fields”AparteToolCallSegment, discriminated by type: 'tool_call'.
| Field | Type | Required | Description |
|---|---|---|---|
toolCall | AparteToolCall | yes | |
status | 'pending' | 'resolved' | 'aborted' | 'awaiting-approval' | 'rejected' | 'failed' | yes | awaiting-approval — paused for a human decision; rejected — the human (or a policy) declined to run it; aborted — stopped, timed out, or nothing could run it; failed — the handler threw, and result carries the crash’s one line. More |
result | string | — | |
structuredResult | unknown | — | The handler’s structuredContent, when it returned one — the value behind result’s prose. |
deferred | { reason: 'approval' | 'external' | 'queued'; ticket?: string } | — | Set when a server loop (createAparteAgentHandler) suspended its run on this call: why it waits — a person’s decision (approval), a tool the page runs (external), or nothing but its turn (`queued More |
status — awaiting-approval — paused for a human decision; rejected — the human (or a
policy) declined to run it; aborted — stopped, timed out, or nothing could run
it; failed — the handler threw, and result carries the crash’s one line.
deferred — Set when a server loop (createAparteAgentHandler) suspended its run on this call:
why it waits — a person’s decision (approval), a tool the page runs
(external), or nothing but its turn (queued) — and the server’s signed
continuation. Saved with the conversation, so a reload can still answer it
(AparteClient.resumeDeferred).
Accessibility
Section titled “Accessibility”- When there is an input or a result to show, the row is a native
<details>: its summary opens with Enter or Space and announces whether it is expanded. With nothing behind it, it is a plain row and takes no tab stop. - The status is written in words (locale keys such as
toolRunning,toolCompleted,toolFailed), not only shown as a colour or an icon.
Related
Section titled “Related”- Tools and approval — registering tools, what the row shows, and asking a person first.