# 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`.
