> This page is also available as Markdown: append .md to its URL. The full index of Sikaru docs is at https://docs.sikaru.ai/llms.txt. Code examples use only operations from the public API reference at https://docs.sikaru.ai/api-reference.

# Runs and tool requests

Operations on this page (Python SDK `sikaru_api`; `client` and `project` from the Quickstart):

1. `client.runs.get(project, run_id)` reads status; `client.runs.events(project, run_id, after=cursor)` reads an event page. Pass `page.next_after` (REST: `nextAfter`) as the next `after`.
2. `client.runs.stream_events(project, run_id, after=cursor)` (TypeScript `client.runs.streamEvents`) streams SSE from `GET /v1/projects/{project_id}/runs/{run_id}/events/stream`. Reconnect with the last processed sequence in `after` or an event ID in `Last-Event-ID`.
3. `client.runs.pending_actions(project, run_id)` lists pending actions on `waiting_for_tool`.
4. `client.runs.decide_approval(project, run_id, tool_call_id, decision="approved" | "denied", idempotency_key=...)` records a user decision.
5. `client.runs.submit_tool_result(project, run_id, tool_call_id=..., tool_provider_id=..., capability_name=..., idempotency_key=..., status="completed", payload=...)` submits a product-owned tool result.
6. `client.runs.recover(project, run_id)` asks an unfinished run to continue from saved progress.
7. `client.runs.retry(project, run_id)` admits a failed, recoverable turn again as a new run; read the new ID from `retried.run.id`. Refused with `409` `not_retryable` otherwise.
8. `client.runs.cancel(project, run_id)` requests cancellation.
9. `client.runs.get_trajectory(project, run_id)` reads retained evidence; needs `runs:read`.

Statuses: `waiting_for_tool`, `paused`, `completed`, `failed`, `cancelled`. Terminal: `completed`, `failed`, `cancelled`. Failed runs carry `error` with `code`, `message`, `recoverable`. Error responses are `{"detail": ..., "code": ...}`; branch on `code`.

A **run** is one execution of work. Starting it admits the task; events and status tell you what happens next. Persist the run ID before presenting progress to a user so you can reconnect without submitting the task again.

The examples use `client` and `project` from the [Quickstart](/get-started/quickstart). Set `run_id` to the run you want to inspect.

## Read status and events

```mermaid
stateDiagram-v2
    state "in progress" as Active
    [*] --> Active: admitted
    Active --> waiting_for_tool
    waiting_for_tool --> Active: approval or result recorded
    Active --> paused
    paused --> Active: resumed
    Active --> completed
    Active --> failed
    Active --> cancelled
    completed --> [*]
    failed --> [*]
    cancelled --> [*]
```

A run is admitted and runs. It may stop at `waiting_for_tool` until you resolve a pending action, or at `paused` until it resumes. It ends as `completed`, `failed`, or `cancelled`.

| Observation              | What your application should do                                                                                                             |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Work admitted or running | Display progress and continue observing.                                                                                                    |
| `waiting_for_tool`       | Read pending actions and resolve the required approval or result.                                                                           |
| `paused`                 | Inspect the retained state and pending actions before deciding how to resume.                                                               |
| `completed`              | Drain remaining events and present the final output.                                                                                        |
| `failed`                 | Read `error` on the run. Retry when `error.recoverable` is true; otherwise follow `error.message`. See [Handle failures](#handle-failures). |
| `cancelled`              | Stop presenting the run as active; reconcile already dispatched external actions.                                                           |

```python
current = client.runs.get(project, run_id)
page = client.runs.events(project, run_id, after="0")
for event in page.events:
    print(event.event_type, event.payload)
cursor = str(page.next_after)
```

* Pass `cursor` as `after` on the next page.
* Save the cursor after processing the page. Deduplicate events by their ID if a page is received again.
* In REST responses the cursor is named `nextAfter`; the Python client exposes `next_after`.

Use a bounded polling interval and application deadline, as shown in the quickstart.

> **Note**
>
> An empty page means there are no newer events in that response. It does not mean the run has finished.

## Stream events

Current SDKs expose a dedicated SSE operation.

#### Python

Python returns an event iterator:

```python
cursor = "0"  # Restore the last processed sequence when reconnecting.
for event in client.runs.stream_events(project, run_id, after=cursor):
    print(event.event_type, event.payload)
    cursor = str(event.sequence)
```

#### TypeScript

TypeScript exposes an asynchronous stream:

```typescript
const events = await client.runs.streamEvents(project, runId, { after: "0" });
for await (const event of events) {
  console.log(event.eventType, event.payload);
}
```

The REST endpoint is `GET /v1/projects/{project_id}/runs/{run_id}/events/stream`. Reconnect using the last processed sequence in `after`, or an event ID in `Last-Event-ID`.

A terminal run event ends the stream, but idle streams may also close. Read status before interpreting a closed stream as completion.

Keep one cursor per run. If your connection drops after displaying an event but before saving its cursor, replay may include that event again.

> **Warning**
>
> Make event handling safe to repeat. Updating a UI twice is recoverable; executing an external tool twice may not be.

## Resolve pending actions

```python
actions = client.runs.pending_actions(project, run_id)
```

Inspect the returned actions and approval states. Your application enforces the user's grants and chooses how to handle product-owned actions. Reading an action or approving it does not execute your backend's tool.

```mermaid
sequenceDiagram
    participant Sikaru
    participant App as Your backend
    actor User
    participant Tool as Your tool
    Sikaru-->>App: Pending action and identities
    App->>User: Request approval when required
    User-->>App: Decision
    App->>Sikaru: Record decision
    opt Approved product-owned action
        App->>Tool: Execute admitted action
        Tool-->>App: Outcome
        App->>Sikaru: Submit retained result receipt
        Sikaru-->>App: Further run events
    end
```

### Record an approval decision

When the user has approved the specific action, record that decision:

```python
approval = client.runs.decide_approval(
    project, run_id, tool_call_id,
    decision="approved",
    idempotency_key=approval_request_id,
)
```

* `tool_call_id` comes from the pending action.
* `approval_request_id` is a stable ID retained for that decision.
* A denied decision uses `decision="denied"`.

Do not approve every request automatically simply to unblock the run.

### Submit a tool result

After your backend executes an admitted product-owned action, submit its actual result:

```python
receipt = client.runs.submit_tool_result(
    project, run_id,
    tool_call_id=tool_call_id,
    tool_provider_id=tool_provider_id,
    capability_name=capability_name,
    idempotency_key=result_request_id,
    status="completed",
    payload=tool_result,
)
```

* The identities above come from the pending action.
* `tool_result` is the actual result dictionary.
* `result_request_id` is the stable key for its submission.

Record failure as failure rather than fabricating a completed payload. Managed connection tools execute through their managed path; do not also execute them in your backend.

## Recover without duplicating effects

Retain the immutable action receipt and idempotency key across uncertain acknowledgements.

For example, if a tool sent an email but the result submission timed out, reconcile the email operation and resubmit the retained result when appropriate. Do not send the email again just because Sikaru has not yet observed its receipt.

`client.runs.recover(project, run_id)` is an explicit recovery request. Inspect the run, actions, and retained receipts first. Recovery is separate from status reads, approval decisions, and result submission. It cannot reverse an external effect.

## Handle failures

A failed run carries `error` with three fields:

| Field         | Meaning                                                    |
| ------------- | ---------------------------------------------------------- |
| `code`        | A stable value to branch on.                               |
| `message`     | An explanation you can show to a user, with the next step. |
| `recoverable` | Whether `retry` can run the failed turn again.             |

`error` is null for any other status. The same object is the `error` field of the run's `run.failed` event.

```python
current = client.runs.get(project, run_id)
if current.status == "failed" and current.error.recoverable:
    retried = client.runs.retry(project, run_id)
    run_id = retried.run.id
```

| `code`                 | `recoverable` | Meaning                                                                                                  |
| ---------------------- | ------------- | -------------------------------------------------------------------------------------------------------- |
| `model_timeout`        | yes           | The model did not respond in time.                                                                       |
| `model_unavailable`    | yes           | The model was temporarily unavailable.                                                                   |
| `evidence_unavailable` | yes           | The turn's record could not be saved, so the turn did not finish.                                        |
| `interrupted`          | yes           | Execution stopped before it finished.                                                                    |
| `capacity_unavailable` | yes           | No execution capacity became available to start the turn.                                                |
| `execution_failed`     | yes           | The agent stopped because of an execution problem. Contact support if retries keep failing.              |
| `insufficient_credits` | no            | Credits or the agent's spending cap ran out. Add credits or raise the cap, then send the message again.  |
| `setup_failed`         | no            | The agent could not finish setup. Review its instructions and connected tools, then start a new session. |
| `tool_not_allowed`     | no            | The agent requested a tool it is not allowed to use. Review its tools, then start a new session.         |

New codes may be added. Treat a code you do not recognize like `execution_failed`.

### Retry a failed turn

`client.runs.retry(project, run_id)` admits the failed turn again as a new turn with its own run ID.

* The failed run stays failed.
* The retry starts from the session's last completed turn.
* External actions the failed turn already completed are not repeated.
* Repeating the request returns the same retry.

A run that did not fail, whose failure is not recoverable, or that failed too long ago is refused with `409` and code `not_retryable`. In that case send the message again as a new turn.

> **Note**
>
> `retry` and `recover` are different. `recover` asks an unfinished run to continue from its saved progress. It does nothing for a run that has already completed, failed, or been cancelled.

### Failure details in events

* **`run.tool_call.failed`** may carry `error` with `code` and `message`. In each case the action was not performed again:
  * `approval_denied`: a reviewer denied the action, so it did not run.
  * `access_changed`: access to the tool changed during the turn, so the action was not performed.
  * `effect_unconfirmed`: an earlier attempt started but its result was never confirmed. Check whether it took effect before trying again.
* **`run.paused`** carries `reason`:
  * `credits_required`: credits or the agent's spending cap ran out. The run resumes once it is funded.
  * `admission_deferred`: the run is waiting to be admitted and resumes on its own.

## Refused requests

Every error response is `{"detail": ..., "code": ...}`. Branch on `code`; `detail` is for people.

* The codes below are specific. Any other error carries its status default, such as `unauthenticated` (401), `forbidden` (403), `not_found` (404), `conflict` (409), `rate_limited` (429) or `service_unavailable` (503).
* A `429` or `503` refusal with a specific code includes `Retry-After` in seconds.
* A refused submission starts no new work.
* Request validation errors (`422`) list each invalid field in `detail` instead.

| Status | `code`                           | What to do                                                                                                                                                                                  |
| ------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `402`  | `insufficient_credits`           | Billing does not allow the request. Check your plan, credits and the agent's spending cap in Billing, then send it again.                                                                   |
| `409`  | `idempotency_conflict`           | The idempotency key was already used for a different request, or for a file that was deleted. Use a new key for new content.                                                                |
| `409`  | `concurrent_request`             | Another request is changing the same resource. Send the same request again.                                                                                                                 |
| `409`  | `agent_disabled`                 | The agent is turned off. Turn it on, then send the request again.                                                                                                                           |
| `409`  | `release_unavailable`            | The agent has no release that can run. Ask an agent administrator to publish or upgrade it.                                                                                                 |
| `409`  | `action_not_pending`             | The tool call or approval is not waiting for this input. It already has an outcome, its run finished, it is handled by Sikaru, or it doesn't match a pending request. Read pending actions. |
| `409`  | `not_retryable`                  | The run cannot be retried. Read its `error` and send a new turn instead.                                                                                                                    |
| `409`  | `branch_unavailable`             | The session cannot be branched at that run yet. Wait until the turn has finished and its workspace is saved.                                                                                |
| `409`  | `record_invalid`                 | The retained record cannot be read. Contact support if this continues.                                                                                                                      |
| `409`  | `state_conflict`                 | The request conflicts with the resource's current state. Read it before trying again.                                                                                                       |
| `503`  | `storage_unavailable`            | The storage behind this resource is unavailable right now. Try again after `Retry-After`.                                                                                                   |
| `429`  | `agent_capacity_full`            | The agent's turn capacity is full. Try again after `Retry-After`.                                                                                                                           |
| `503`  | `agent_unavailable`              | Execution is switched off for this agent. Try again after `Retry-After`; contact support if it persists.                                                                                    |
| `503`  | `turn_delivery_pending`          | The message was recorded but has not started. Send it again with the same idempotency key.                                                                                                  |
| `503`  | `execution_storage_unavailable`  | Session, file, or workspace storage is unavailable right now. Try again after `Retry-After`.                                                                                                |
| `503`  | `trajectory_storage_unavailable` | Retained records cannot be read right now. Read again after `Retry-After`.                                                                                                                  |

`agent_disabled` means someone turned the agent off, so it stays off until it is turned on. `agent_unavailable` is temporary, so waiting is enough. New codes may be added; handle an unrecognized code by its status.

> **Tip**
>
> Keep the same idempotency key when you resend a refused submission.

## Cancel work

```python
cancelled = client.runs.cancel(project, run_id)
```

Then observe status until cancellation has been processed.

Closing the CLI, ending an HTTP wait, or disconnecting from events does not cancel hosted work. Your application must also stop or reconcile any product-owned tool operation it has already dispatched.

For machine execution, the native executor observes hosted termination and stops its local process groups. See [Local compute](/run/local-compute) for interruption and crash limits.

## Inspect retained evidence

Call `client.runs.get_trajectory(project, run_id)` with `runs:read` or inspect managed history in the platform. The permitted trajectory includes retained execution structure and measured usage. Private messages, reasoning, tool payloads, and serving metadata are omitted.

A completed status does not guarantee a complete retained trajectory. Missing measurements remain unknown. See [Trace ingestion](/improve/trace-ingestion) for the response shape and distinction from traces you submit.

## Next steps

#### [Sessions and steering](/run/sessions-and-steering)

Continue a conversation and steer active work.

#### [Local compute](/run/local-compute)

Run work on your own machine and handle interruptions.

#### [Trace ingestion](/improve/trace-ingestion)

Read retained evidence and submit your own traces.

#### [Quickstart](/get-started/quickstart)

Start a run and poll it to a terminal status.