Client tools

Client tools

Client tools run on your side. When the model calls one, the task moves to interrupted; you send back the result and processing continues.

Unlike assistant tools (which the server calls), client tools are defined by you right in the request. They’re handy when the data or the action is only available on your side: the user’s location, access to your database, an interactive confirmation.

1. Declare the tools

Add client_tools to the POST /api/tasks body:

{
  "assistant_id": 1,
  "input": [{ "type": "text", "text": "What's near me?" }],
  "client_tools": [
    {
      "name": "get_user_location",
      "description": "Returns the user's city",
      "input_schema": {
        "type": "object",
        "properties": { "precision": { "type": "string" } }
      }
    }
  ]
}

name and description are required, input_schema (JSON Schema) is optional.

2. Wait for the interrupt

If the model called a client tool, polling GET /api/tasks/{uuid} returns the interrupted status and the list of pending calls in pending_tool_calls:

{
  "uuid": "550e8400-…",
  "status": "interrupted",
  "pending_tool_calls": [
    { "id": "call_abc", "name": "get_user_location", "arguments": { "precision": "city" } }
  ]
}

3. Send back the results

POST /api/tasks/{uuid}/tool-results

Run the tools on your side and send the results. You must cover every call from pending_tool_calls.

{
  "tool_results": [
    { "id": "call_abc", "content": "Berlin" }
  ]
}

Report a tool error like this:

{ "id": "call_abc", "error": "Location unavailable", "reason": "permission_denied" }

Each item carries an id (from pending_tool_calls) and either content or error (plus an optional reason).

4. Continuation

Once the results are in, the task goes back to processing. Keep polling: the interrupted → tool-results → processing cycle may repeat several times until the task reaches completed or failed.

POST /api/tasks         → pending
GET  /api/tasks/{uuid}  → interrupted   # the model called a client tool
POST …/tool-results     → processing    # you sent the result
GET  /api/tasks/{uuid}  → completed      # done (or interrupted again)
POST …/cancel           → canceled       # or you declined to answer
POST /api/tasks/cancel-interrupted       # cancel all interrupted tasks at once

5. Give up on an interrupted task

POST /api/tasks/{uuid}/cancel

If you have nothing to answer with — the user left, the tool is unavailable, the scenario is no longer needed — don’t leave the task hanging in interrupted: it stays active and keeps occupying a slot in the concurrent task limit. Cancel it:

{ "reason": "The user closed the app" }   // optional, up to 1000 characters

The task moves to the terminal canceled status, reason is stored in error (default: Task canceled by the client.), and the slot in the limit is freed immediately. If callback_url is set, the cancellation is delivered by webhook too.

{ "uuid": "550e8400-…", "status": "canceled" }

You can cancel only a task in the interrupted status — otherwise 409. A task in pending or processing can’t be canceled: Aist Core doesn’t know about the cancellation, will finish the turn anyway and send the result, which has already been charged.

Cancel all interrupted tasks at once

POST /api/tasks/cancel-interrupted

Got a 429 and don’t know which tasks are holding the limit? You can see them in the task list (GET /api/tasks?status=interrupted) and clear them all with a single request — typical cleanup after a process crash. The body is the same (reason is optional and applies to all of them):

{ "canceled": 2, "uuids": ["550e8400-…", "660f9511-…"] }

Only your tasks in the interrupted status are affected — pending and processing keep running. The request is idempotent: if there’s nothing to cancel, you get { "canceled": 0, "uuids": [] }, never a 409.

The final tool_call in output

Once the task is complete, the client tool call shows up in output as a regular tool_call (without a prefix):

{
  "type": "tool_call",
  "tool_call": {
    "name": "get_user_location",
    "input": { "precision": "city" },
    "output": { "type": "content", "content": [{ "type": "text", "text": "Berlin" }] }
  }
}
Important
  • tool_results must cover every pending call, otherwise 422.
  • POST /tool-results for a task that isn’t interrupted returns 409.
  • An interrupted task counts as active and towards the concurrent task limit — until you answer the call or cancel it.
  • POST /cancel works only for a task in the interrupted status, otherwise 409.
  • POST /cancel-interrupted never returns 409: it cancels whatever there is, even zero tasks.
The SDK does this for you
runTask walks through the whole interrupt cycle on its own: it calls your handler, sends the result and continues until the task is done. Both kinds of cancellation are in the SDK too: cancelTask(uuid, { reason }) and cancelInterruptedTasks().