Skip to content
kitn AI/UI

Tool calls & reasoning

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.

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.

import '@kitn.ai/ui/elements';
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:

stateRendered asWhen to set it
input-streamingSpinning loader, “Processing” badgeTool call chunk received; input is still arriving
input-availableSettings icon, “Ready” badgeInput complete; tool is executing
output-availableCheck icon, “Completed” badgeTool returned successfully
output-errorX icon, “Error” badge + errorTextTool 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):

<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>