Client tools
View as Markdown# Client tools
> Let the agent use functions that run in your own app — declare them, handle requires_action, and submit tool outputs to finish the reply.
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.
## 1. Declare the tool on the agent
```bash
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`.
## 2. The run pauses with `requires_action`
When the model calls the tool, the run stops and waits for you:
```json
{
"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`.
## 3. Run the tool and submit the outputs
Answer **every** call in `tool_calls`, within 10 minutes:
**JavaScript**
```js
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
}
```
**Python**
```python
import uuid, requests
BASE = "https://api.k-agent.kerneltics.com/v1"
HEADERS = {"Authorization": f"Bearer {token}"}
# Your implementations, keyed by tool name.
CLIENT_TOOLS = {
"get_cart": lambda args: {"items": [{"name": "Bakhoor box", "qty": 1, "price_sar": 95}], "subtotal_sar": 95},
}
def send(session: str, text: str) -> dict:
r = requests.post(f"{BASE}/sessions/{session}/messages", headers=HEADERS,
json={"input": text, "client_message_id": str(uuid.uuid4())}, timeout=120)
run = r.json()["run"]
# A run can pause more than once: keep answering until it finishes.
while run and run["status"] == "requires_action":
outputs = []
for call in run["required_action"]["tool_calls"]:
try:
outputs.append({"call_id": call["call_id"],
"output": CLIENT_TOOLS[call["name"]](call["arguments"]),
"is_error": False})
except Exception as exc:
outputs.append({"call_id": call["call_id"], "output": {"message": str(exc)}, "is_error": True})
r = requests.post(f"{BASE}/runs/{run['id']}/submit_tool_outputs", headers=HEADERS,
json={"tool_outputs": outputs}, timeout=120)
run = r.json() # submit_tool_outputs returns the run
return run # completed: read run["output_text"] and run["outcome"]
```
**curl**
```bash
curl https://api.k-agent.kerneltics.com/v1/runs/run_01k6rz7d9f1h3k5n7q9s1v3x5z/submit_tool_outputs \
-H "Authorization: Bearer $KAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tool_outputs": [
{ "call_id": "call_8f2k1", "output": { "items": [{ "name": "Bakhoor box", "qty": 1, "price_sar": 95 }], "subtotal_sar": 95 }, "is_error": false }
]
}'
```
- `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.
## From the browser
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.
## Things to know
- Client tools are not offered on `store: false` calls.
- With the [OpenAI-compatible endpoint](/docs/en/guides/openai-sdk/#tools), 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`.
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.
1. Declare the tool on the agent
Section titled “1. Declare the tool on the agent”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:
namematches^[a-z][a-z0-9_]{0,47}$and must not clash with the agent’s other tools.parametersis a JSON Schema; arguments are validated strictly before your app ever sees them.effectdefaults toaction. Setreadfor tools that only look things up, so they also run for anonymous users and in one-shot calls withoutallow_actions.
2. The run pauses with requires_action
Section titled “2. The run pauses with requires_action”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(orask) returns this run with status200. - With streaming, the stream ends with a
run.requires_actionevent whoseruncarriesrequired_action. - A paused run keeps the session’s turn: under
queue, new messages wait behind it; underreject, they get409 session_busy.
3. Run the tool and submit the outputs
Section titled “3. Run the tool and submit the outputs”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}import uuid, requests
BASE = "https://api.k-agent.kerneltics.com/v1"HEADERS = {"Authorization": f"Bearer {token}"}
# Your implementations, keyed by tool name.CLIENT_TOOLS = { "get_cart": lambda args: {"items": [{"name": "Bakhoor box", "qty": 1, "price_sar": 95}], "subtotal_sar": 95},}
def send(session: str, text: str) -> dict: r = requests.post(f"{BASE}/sessions/{session}/messages", headers=HEADERS, json={"input": text, "client_message_id": str(uuid.uuid4())}, timeout=120) run = r.json()["run"] # A run can pause more than once: keep answering until it finishes. while run and run["status"] == "requires_action": outputs = [] for call in run["required_action"]["tool_calls"]: try: outputs.append({"call_id": call["call_id"], "output": CLIENT_TOOLS[call["name"]](call["arguments"]), "is_error": False}) except Exception as exc: outputs.append({"call_id": call["call_id"], "output": {"message": str(exc)}, "is_error": True}) r = requests.post(f"{BASE}/runs/{run['id']}/submit_tool_outputs", headers=HEADERS, json={"tool_outputs": outputs}, timeout=120) run = r.json() # submit_tool_outputs returns the run return run # completed: read run["output_text"] and run["outcome"]curl https://api.k-agent.kerneltics.com/v1/runs/run_01k6rz7d9f1h3k5n7q9s1v3x5z/submit_tool_outputs \ -H "Authorization: Bearer $KAGENT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "tool_outputs": [ { "call_id": "call_8f2k1", "output": { "items": [{ "name": "Bakhoor box", "qty": 1, "price_sar": 95 }], "subtotal_sar": 95 }, "is_error": false } ] }'outputis any JSON value. Setis_error: truewhen the tool failed, so the model can recover gracefully (for example by asking the customer or handing over).submit_tool_outputsacceptsstream: trueandwait_secondslike any other run request, and returns the run.- Missing outputs return
422 tool_outputs_incomplete. Submitting to a run that isn’t waiting returns409 run_not_requires_action. - If nothing arrives within 10 minutes, the run ends
failedwith the codetool_outputs_expired(no fallback reply, no handoff). The customer’s next message starts a fresh run.
From the browser
Section titled “From the browser”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.
Things to know
Section titled “Things to know”- Client tools are not offered on
store: falsecalls. - With the OpenAI-compatible endpoint, stateless requests expose client tools as normal
tool_callswithfinish_reason: "tool_calls"; the run ends with the outcomeclient_tool_callsand your follow-up request is a new run. requires_actionis a status, never an outcome: a paused run always ends later ascompleted,failed,cancelledorsuperseded.