# Run control API

> Resolve waits, steer sessions, and apply idempotent safe-point or abort controls to exact Runs.

Every route below begins `/v1/namespaces/:namespace/agents/:agent/sessions/:session`. Controls are durable authenticated events. Supply the exact Run id and caller-stable `eventId`; do not infer current state from analytics before mutating it.

| Method | Suffix | Required body or purpose |
| --- | --- | --- |
| `POST` | `/waits/:promiseId/resolve` | `{ eventId, value }`; value must satisfy the pinned wait schema |
| `POST` | `/steers` | `{ eventId, text, data? }`; append fresh authenticated guidance |
| `GET` | `/events/:eventId` | Requires `ledger:read`; the Session's decision on an event: the Run it started, its place while it waits, or its refusal with that status |
| `POST` | `/runs/:run/pause` | `{ eventId }` or idempotency header; stop at a safe point |
| `POST` | `/runs/:run/resume` | `{ eventId }`; resume and drive dispatchable work |
| `POST` | `/runs/:run/cancel` | `{ eventId? }`; terminally stop future work |
| `POST` | `/runs/:run/interrupt` | `{ eventId, payload, mode? }`; `mode` is `safe-point` or `abort` |
| `POST` | `/runs/:run/policy` | `{ eventId, policyHash }`, `{ eventId, maxTurns }`, or `{ eventId, maxMicroUsd }` |
| `POST` | `/runs/:run/rebind` | `{ eventId, addBindings }` to add exact pins, `{ eventId, bindings }` to replace the fixed map, or `{ eventId }` to refresh scoped assignments |
| `POST` | `/runs/:run/truncate` | `{ eventId, to, text, data? }`; move the same history head and steer |
| `POST` | `/runs/:run/branch` | `{ eventId, at, text, data? }`; create and drive a forked Run |

Abort interruption additionally requires `run:abort`. A fixed rebind supplies the complete map of exact Resource CRN/hash pins; it cannot change the Agent's logical model identity or leave a Tool need unsatisfied. Selected revisions must be available, but do not have to be the latest Resource head. A scoped refresh resolves current assignments for the Run's already accepted principal/customer authority.

## Add a Resource during a Run {#add-bindings}

Install the Resource first, then pass its returned CRN and hash to `addBindings`.
For example, an MCP Gateway installed as `tickets` can be attached without
replacing the Run's model, GitHub connection, or other bindings:

```json
{
  "eventId": "attach-tickets",
  "addBindings": {
    "tickets": {
      "crn": "crn:constal:production:acme:default:mcp/tickets",
      "hash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
  }
}
```

Use the actual installation hash, not the example value. The same request is
available to Agents as the `run.rebind` operation through the Constal API
Resource, with this body in `input.request` and the exact namespace, Agent,
Session, and Run in `input`.

`addBindings` preserves existing pins and resolves scoped credentials only for
the additions. Repeating an existing CRN/hash binding leaves it unchanged;
changing that binding requires an explicit replacement. Do not supply both
`bindings` and `addBindings`.

The response's `targetResourcesHash` identifies the requested snapshot. An
accepted receipt does not mean the current Agent frame already sees it: the
change applies at a safe point. Compare Run detail's `resourcesHash` with the
receipt, or observe the applied bindings through the Agent context after that
safe point. Rebinding a manually paused Run does not resume it.

`StaleResourceSnapshot` means another change won before this request could be
accepted. Read the current state and retry the intended change.
`RunControlPending` means an accepted control is still awaiting its safe point;
the new request has not replaced it.

## Run queues {#run-queues}

A [concurrent Run limit](/docs/policies/governance.md#run-limits) with `overLimit: "queue"` keeps an ordered queue of Run requests waiting for a slot. A queue is addressed by its Agent, its limit id as `:queue`, and its scope. These routes begin `/v1/namespaces/:namespace/agents/:agent/run-queues/:queue` and are not Session routes:

| Method | Suffix | Action | Body | Response |
| --- | --- | --- | --- | --- |
| `GET` | `?scope=:scope` | `run:read` | none; optional `limit` (default 100) and `cursor` | `{ data: RunQueueItem[], next }` |
| `POST` | `/items/:item/move?scope=:scope` | `run:reorder` | exactly one of `{ before: item }`, `{ after: item }`, or `{ position }` | `{ data: RunQueueItem }` |
| `POST` | `/items/:item/drop?scope=:scope` | `run:cancel` | `{ reason? }` | `{ data: { id, state: "dropped" } }` |

`scope` is the limit's scope: `customer`, `subject`, `session`, `tenant`, or `resource`. Add `&session=:session` for a `session` queue. The scope value comes from the caller, never from the request: the caller's Customer for `customer`, its principal for `subject`, its own Customer's Session named by the `session` query value for `session`, its tenant for `tenant`, and the Agent for `resource`. A caller with no Customer that addresses a `customer` queue gets 400. Policies see the action on the `run` Resource path `:agent/run-queues/:queue`.

A caller sees and controls only the items of its own Customer; a parent-tenant caller sees parent-tenant items only. `position` counts from 1 among those items, and a move reorders only them, so other Customers' items keep their positions. Pass the previous page's `next` as `cursor`.

```json
{
  "id": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "position": 1,
  "session": "session-42",
  "eventId": "evt-7",
  "runId": null,
  "receivedAt": 1790000000000,
  "enqueuedAt": 1790000000150,
  "summary": {
    "kind": "message.received",
    "title": "Summarize the open incidents",
    "actor": { "kind": "user", "id": "user-9" },
    "source": { "kind": "channel", "channel": "crn:constal:production:acme:default:channel/support" }
  }
}
```

The summary never includes the request's input. `source` is `{ kind: "agent", agent, session }` for an Agent send, `{ kind: "channel", channel }` for a Channel event, or `null`.

`before` and `after` name another waiting item in the same view. `position` is an integer from 1 and is clamped to the queue length. A drop refuses the waiting request with 409 `RunQueueItemDropped` and the dropping actor: an Agent send's Handle is cancelled and a Channel receives `channel.failed`. Dropping the same item again returns the same response. `reason` is 1 to 4096 characters.

| Status | Error | Meaning |
| --- | --- | --- |
| 400 | `invalid run queue request` | Not exactly one move target, or an invalid position, reason, scope, or query |
| 400 | `customer-scoped run queues are addressed by a Customer caller` | A caller with no Customer addressed a `customer` queue |
| 403 | `authorization_error` | The caller lacks `run:read`, `run:reorder`, or `run:cancel` |
| 404 | `agent not found` | The Agent does not exist |
| 404 | `RunQueueItemNotFound` | The item or anchor is not in the caller's view |
| 409 | `RunQueueItemNotWaiting` | The item or anchor was already granted a slot, dropped, or withdrawn; the body carries its `state` |

Agents reach the same controls through the Constal API Resource. `apply` accepts `run-queue.move` (requires `run:reorder`) with input `{ namespace, agent, queue, scope, session?, item, before? | after? | position? }` and `run-queue.drop` (requires `run:cancel`) with `{ namespace, agent, queue, scope, session?, item, reason? }`. Both support dry run and an idempotency key, and apply to one item per call. A successful call is `succeeded`.

A `run.start` that must wait has no Run yet. Its result is `{ outcome: "queued", eventId, admission: { state: "waiting", queue, scope, position } }`, or carries `activeRunId` when it waits behind the Session's live Run, and its receipt stays `pending` with that result. Each `observe` re-reads the event's decision at `/events/:eventId`, which updates `position`. Once the Session admits the event, the result carries the Run's `runId` and the receipt follows that Run to its end, as it does for a start admitted at once. A dropped start fails with `RunQueueItemDropped`. A start refused by a limit with `overLimit: "reject"`, at once or when its turn comes, fails with `AdmissionLimitExceeded`.

Query kind `run-queue-item` lists waiting items. `query` requires exact equality filters on `agent`, `queue`, and `scope`, and on `session` for a `session` queue. Each result's id is `agent/queue/scope/session/item`, with `-` for no Session, and its fields carry `position`, `eventId`, `runId`, `receivedAt`, `enqueuedAt`, and `summary`. `get` accepts the same id. An Agent calling through its `api` binding acts with its Run's authority, including the Customer, so it sees only that Customer's items.

After every operation, persist the receipt and refresh [Run detail](/docs/api/agents-and-runs.md#read). An uncertain external effect is reconciled according to the pinned Resource operation recovery contract; a control does not make blind repetition safe.
