Skip to content
kitn AI/UI

Tool calls & reasoning

Agents think before they act and invoke tools to get things done. The tools and reasoning fields on ChatMessage bring both inline — one object per tool call, updated in place as the agent streams its output.

Both tools and reasoning live directly on the ChatMessage object you pass to <kai-chat> or <kai-message>. Update the message in place 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';
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', content: prompt },
{
id: aId,
role: 'assistant',
content: '',
reasoning: { text: '', label: 'Agent reasoning' },
tools: [
{
type: 'search_web',
state: 'input-streaming', // tool call incoming — inputs not yet complete
toolCallId: callId,
},
],
},
];
chat.loading = true;
// As the stream arrives, mutate the message array with new array references
// to drive re-renders. Each state transition is a new array assigned to chat.messages.
// Input fully received → update state to 'input-available'
chat.messages = chat.messages.map((m) =>
m.id === aId
? {
...m,
tools: [{ ...m.tools[0], state: 'input-available', input: { query: 'kitn-chat docs' } }],
}
: m,
);
// Tool executed successfully → state becomes 'output-available'
chat.messages = chat.messages.map((m) =>
m.id === aId
? {
...m,
tools: [{ ...m.tools[0], state: 'output-available', output: { results: [''] } }],
}
: m,
);
// Tool failed → state becomes 'output-error'
// chat.messages = chat.messages.map((m) =>
// m.id === aId
// ? { ...m, tools: [{ ...m.tools[0], state: 'output-error', errorText: 'Rate limit exceeded' }] }
// : m
// );
// Stream the final reply into content
for await (const token of streamFromYourModel(prompt)) {
chat.messages = chat.messages.map((m) =>
m.id === aId ? { ...m, content: m.content + token } : m,
);
}
chat.loading = false;

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 single { text, label? } object — 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>