Skip to content

Client tools

View as Markdown

A client tool is a function your application runs and K-Agent waits for: reading the customer’s cart in the browser, checking a device’s state in your mobile app, or calling a system K-Agent can’t reach. The model decides when to call it; your code runs it and sends back the result; the agent continues its reply.

نافذة الطرفية
curl -X PATCH https://api.k-agent.kerneltics.com/v1/agents/store-assistant \
-H "Authorization: Bearer $KAGENT_API_KEY" \
-H "Content-Type: application/json" \
-H "If-Match: $ETAG" \
-d '{
"config": {
"tools": {
"client": [{
"name": "get_cart",
"description": "Returns the items in the customer'\''s cart with prices.",
"parameters": { "type": "object", "properties": {}, "additionalProperties": false },
"effect": "read",
"rules": "Call it before quoting a delivery fee, which depends on the cart total."
}]
}
}
}'

Then publish the agent. Notes:

  • name matches ^[a-z][a-z0-9_]{0,47}$ and must not clash with the agent’s other tools.
  • parameters is a JSON Schema; arguments are validated strictly before your app ever sees them.
  • effect defaults to action. Set read for tools that only look things up, so they also run for anonymous users and in one-shot calls without allow_actions.

When the model calls the tool, the run stops and waits for you:

{
"id": "run_01k6rz7d9f1h3k5n7q9s1v3x5z",
"object": "run",
"status": "requires_action",
"outcome": null,
"required_action": {
"type": "submit_tool_outputs",
"tool_calls": [{ "call_id": "call_8f2k1", "name": "get_cart", "arguments": {} }],
"expires_at": 1791272520
}
}
  • Without streaming, POST …/messages (or ask) returns this run with status 200.
  • With streaming, the stream ends with a run.requires_action event whose run carries required_action.
  • A paused run keeps the session’s turn: under queue, new messages wait behind it; under reject, they get 409 session_busy.

Answer every call in tool_calls, within 10 minutes:

const BASE = 'https://api.k-agent.kerneltics.com/v1';
const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
// Your implementations, keyed by tool name.
const clientTools = {
get_cart: async () => ({ items: [{ name: 'Bakhoor box', qty: 1, price_sar: 95 }], subtotal_sar: 95 }),
};
async function send(session, input) {
let res = await fetch(`${BASE}/sessions/${encodeURIComponent(session)}/messages`, {
method: 'POST',
headers,
body: JSON.stringify({ input, client_message_id: crypto.randomUUID() }),
});
let { run } = await res.json();
// A run can pause more than once: keep answering until it finishes.
while (run?.status === 'requires_action') {
const tool_outputs = await Promise.all(
run.required_action.tool_calls.map(async (call) => {
try {
const output = await clientTools[call.name](call.arguments);
return { call_id: call.call_id, output, is_error: false };
} catch (err) {
return { call_id: call.call_id, output: { message: String(err) }, is_error: true };
}
}),
);
res = await fetch(`${BASE}/runs/${run.id}/submit_tool_outputs`, {
method: 'POST',
headers,
body: JSON.stringify({ tool_outputs }),
});
run = await res.json(); // submit_tool_outputs returns the run
}
return run; // completed: read run.output_text and run.outcome
}
  • output is any JSON value. Set is_error: true when the tool failed, so the model can recover gracefully (for example by asking the customer or handing over).
  • submit_tool_outputs accepts stream: true and wait_seconds like any other run request, and returns the run.
  • Missing outputs return 422 tool_outputs_incomplete. Submitting to a run that isn’t waiting returns 409 run_not_requires_action.
  • If nothing arrives within 10 minutes, the run ends failed with the code tool_outputs_expired (no fallback reply, no handoff). The customer’s next message starts a fresh run.

Client tokens may call submit_tool_outputs for runs of their own sessions, so a web app can answer client tools directly in the browser — for example to read a cart that only exists there. Run steps (/steps) stay private to your server.

  • Client tools are not offered on store: false calls.
  • With the OpenAI-compatible endpoint, stateless requests expose client tools as normal tool_calls with finish_reason: "tool_calls"; the run ends with the outcome client_tool_calls and your follow-up request is a new run.
  • requires_action is a status, never an outcome: a paused run always ends later as completed, failed, cancelled or superseded.