# Steering

> The Steering operations of the Kabu API: human instructions to a running agent: steer, claim, ack.

Human instructions to a running agent: steer, claim, ack.

**Preview.** Steering a running agent is not yet part of Kabu: these operations are in the contract ahead of it.

Generated from the contract, version `2.0.0-draft.2` ([OpenAPI](/openapi.yaml)). Base URL `https://usekabu.com/api`. Errors are problem details, each [code](/docs/errors) with its own page.

## `POST /v2/messages/{message_id}/ack` · Report what the agent did with a steer

received (started; no redelivery after this) then applied or rejected, once. Stale lease token: 409 lease_lost. Backwards: 409 invalid_ack_transition.

- **Auth:** an API key.
- **Scopes:** `claims:write`.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `message_id` | path | string | yes | A `msg_…` id. |
| `Idempotency-Key` | header | string, at most 255 characters | no | Retry-safe key. For messages, sessions and responses it binds to the created object for its life; elsewhere for 24 h. |

**Request body**: [AckInput](#schema-AckInput).

| Status | Means | Body |
|---|---|---|
| `200` | OK | [Message](/docs/api/messages#schema-Message) |
| `4XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |
| `5XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |

```sh
curl -X POST "https://usekabu.com/api/v2/messages/$MESSAGE_ID/ack" \
  -H "Authorization: Bearer $KABU_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"lease_token":"…","status":"received"}'
```

## `PUT /v2/sessions/{session_id}/continuation` · Bind the runtime's own session id (claim holder only)

Only the consumer holding the session's current claim or durable attempt may set the continuation: the lease_token is checked atomically against the session, agent, mode and current claim generation. A superseded token is 409 lease_lost, so a stale worker can never redirect future steering into the wrong runtime.

- **Auth:** an API key.
- **Scopes:** `claims:write`.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `session_id` | path | string | yes | A `ses_…` id. |

**Request body**: [ContinuationSet](#schema-ContinuationSet).

| Status | Means | Body |
|---|---|---|
| `200` | OK | [Session](/docs/api/sessions#schema-Session) |
| `4XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |
| `5XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |

```sh
curl -X PUT "https://usekabu.com/api/v2/sessions/$SESSION_ID/continuation" \
  -H "Authorization: Bearer $KABU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"lease_token":"…","continuation":{"kind":"…","id":"…"}}'
```

## `POST /v2/sessions/{session_id}/steer` · A human instructs a running agent

Creates a kind=steer message: instructions are FIFO within the session; signals (pause, resume, stop) use their own lane and are never blocked by an instruction. Human-voice writes have their own budget, never send or month, so a runaway agent can never prevent its own stop. Only the session audience may steer: a user token outside `to`, or a key whose as_recipient is outside `to`, gets a non-disclosing 404. Keys need steer:write and as_recipient, and a key holding messages:write gets 403 self_response_forbidden.

- **Auth:** an API key, or a signed-in human's token (the Kabu apps).
- **Scopes:** `steer:write`.
- **Refused to:** a key holding `messages:write`.
- **Signed-in humans:** `audience`.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `session_id` | path | string | yes | A `ses_…` id. |
| `Idempotency-Key` | header | string, at most 255 characters | no | Retry-safe key. For messages, sessions and responses it binds to the created object for its life; elsewhere for 24 h. |
| `Kabu-Org` | header | string | no | Select a live membership (default personal org); non-member is 404. Human tokens are live-only. Caller user id is distinct from billing owner. |

**Request body**: [SteerInput](/docs/api/sessions#schema-SteerInput).

| Status | Means | Body |
|---|---|---|
| `201` | OK | [Message](/docs/api/messages#schema-Message) |
| `429` | A rate, concurrency or quota limit was reached | [Problem](/docs/api/discovery#schema-Problem) |
| `4XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |
| `5XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |

```sh
curl -X POST "https://usekabu.com/api/v2/sessions/$SESSION_ID/steer" \
  -H "Authorization: Bearer $KABU_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"control":"instruction","body":"…"}'
```

## `POST /v2/claims` · Take queued steers with a lease (the only consumption path)

Holds up to `wait` seconds for steers on one session, or across an agent's sessions and kickoffs. Only the head of each session's instruction queue is leasable; signals (pause, resume, stop) have their own lane and are leasable even while an instruction holds the head. A newer claim from the same consumer supersedes its own parked wait; it never takes another consumer's lease. Retrying with the same Idempotency-Key while the leases are live returns the same claim and items, so a lost response loses nothing; once the leases lapse the items return to their queues.

- **Auth:** an API key.
- **Scopes:** `claims:write`.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `Idempotency-Key` | header | string, at most 255 characters | no | Retry-safe key. For messages, sessions and responses it binds to the created object for its life; elsewhere for 24 h. |
| `wait` | query | integer, from 0 to 50 | no | Hold up to N seconds for the state to settle. 0 or absent returns at once. The kabu CLI and the MCP tools wait 25. |

**Request body**: [ClaimCreate](#schema-ClaimCreate).

| Status | Means | Body |
|---|---|---|
| `200` | OK | [Claim](#schema-Claim) |
| `429` | A rate, concurrency or quota limit was reached | [Problem](/docs/api/discovery#schema-Problem) |
| `4XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |
| `5XX` | Error | [Problem](/docs/api/discovery#schema-Problem) |

```sh
curl -X POST "https://usekabu.com/api/v2/claims" \
  -H "Authorization: Bearer $KABU_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"target":{"type":"session","session":"…"},"consumer":"…"}'
```

## Schemas

### AckInput

Send `received` only after the consumer has durably accepted the instruction, or immediately before a non-idempotent injection. That turns the lease into a durable attempt: Kabu never redelivers it, and the same lease_token stays valid for the final applied/rejected until outcome_deadline (24 h), even after lease_expires_at. While it is received, the session's instruction head stays with the same consumer id, so a runtime that restarts under that id continues in order; other consumers wait. If no outcome arrives within the claim's attempt_timeout the attempt becomes uncertain and the head is released; a late outcome still settles it. A lease that lapses before `received` returns the steer to the head.

| Property | Type | Required | Description |
|---|---|---|---|
| `lease_token` | string | yes |  |
| `status` | string, one of `"received"`, `"applied"`, `"rejected"` | yes |  |
| `note` | string, at most 500 characters | no |  |

Unknown properties are refused.

### ContinuationSet

| Property | Type | Required | Description |
|---|---|---|---|
| `lease_token` | string | yes | The token from the claim item (or the durable attempt after received) that currently holds this session. |
| `continuation` | [Continuation](#schema-Continuation) | yes | The runtime's own session id (stored encrypted, never shown to recipients). |

Unknown properties are refused.

### Continuation

The runtime's own session id (stored encrypted, never shown to recipients).

| Property | Type | Required | Description |
|---|---|---|---|
| `kind` | string, at most 40 characters | yes |  |
| `id` | string, at most 300 characters | yes |  |

Unknown properties are refused.

### ClaimCreate

| Property | Type | Required | Description |
|---|---|---|---|
| `target` | [ClaimTarget](#schema-ClaimTarget) | yes |  |
| `max` | integer, from 1 to 20 | no |  |
| `consumer` | string, at most 120 characters | yes | Stable id of this consumer, shown to the human. |
| `lease_seconds` | integer, from 10 to 600 | no |  |
| `attempt_timeout` | integer, from 60 to 86400 | no | How long a received instruction holds its session's head for this consumer before it becomes uncertain and the head is released (default 600). |

Unknown properties are refused.

### ClaimTarget

Type: [SessionTarget](#schema-SessionTarget) or [AgentTarget](#schema-AgentTarget).

Chosen by `type`: `"session"`: [SessionTarget](#schema-SessionTarget), `"agent"`: [AgentTarget](#schema-AgentTarget).

### SessionTarget

Claim one session's steering queue.

| Property | Type | Required | Description |
|---|---|---|---|
| `type` | `"session"` | yes |  |
| `session` | string | yes | A `ses_…` id. |

Unknown properties are refused.

### AgentTarget

Claim across an agent's sessions and kickoffs (a daemon), optionally within one project.

| Property | Type | Required | Description |
|---|---|---|---|
| `type` | `"agent"` | yes |  |
| `agent` | string | yes |  |
| `project` | [ProjectRef](/docs/api/messages#schema-ProjectRef) | no | A project slug (auto-created on first use) or `prj_…` id. Omit for `default`. A slug that normalises (lowercase, letters and digits only) to an existing project's key names that project, so `My App`, `my-app` and `my_app` are one project. Automatic creation stops at 1,000 live projects per org and mode with `429 quota_exceeded`; existing projects stay usable. Nothing remembers a deleted project (projects.delete): the next message that names its slug creates a fresh one. |

Unknown properties are refused.

### Claim

| Property | Type | Required | Description |
|---|---|---|---|
| `object` | `"claim"` | yes |  |
| `id` | string | yes |  |
| `items` | array of [ClaimItem](#schema-ClaimItem) | yes |  |

### ClaimItem

`continuation` is returned only to the claiming runtime, never in Message or events.

| Property | Type | Required | Description |
|---|---|---|---|
| `message` | [Message](/docs/api/messages#schema-Message) | yes | One object for notify, ask and steer. Never contains answer links. On human routes (user token, answer link, delegate) Kabu returns the recipient projection: `to` lists only the caller, metadata is `{}`, and correlation, extensions and quiz answers before the reveal point are null. For a recipient who did not respond, `resolution` is null (status and settled_at still show the ask is settled), so no recipient learns who else was asked or what they answered; the responding human, the creator and integrators see the full resolution and receipt. |
| `lease_token` | string | yes |  |
| `lease_expires_at` | [Timestamp](/docs/api/messages#schema-Timestamp) | yes |  |
| `outcome_deadline` | [Timestamp](/docs/api/messages#schema-Timestamp) or null | yes |  |
| `continuation` | [ContinuationView](#schema-ContinuationView) or null | yes |  |

### ContinuationView

The runtime's own session id (stored encrypted, never shown to recipients).

| Property | Type | Required | Description |
|---|---|---|---|
| `kind` | string, at most 40 characters | yes |  |
| `id` | string, at most 300 characters | yes |  |
