# Sessions

Give an action a session: durable identity, ordered turns, an end, schedules, approvals, and held turns.

A session gives an action continuity across turns: an immutable input, handler-owned state, input history, and a lifecycle. The `session` block declares how sessions are identified, when each one ends, and the clock it keeps, and each trigger declares whether a matching event starts a session or resumes one. The same action and session ID always address the same session.

A matching start event creates the session, or is delivered to it when it is open with the same input; a start with different input for an open session is a conflict, and a closed session ID never starts again. The session's `origin` is the earliest-committed start event delivered to it, and `startedAt` is that event's commit time, even when a start that committed earlier is delivered after the session was created; the session's [end](https://www.twisp.com/docs/reference/actions/sessions.md#session-end) moves with it. A matching resume event is delivered to each open session it names. An event that fails to start or resume a session, such as a start with conflicting input, is recorded as an [intake record](https://www.twisp.com/docs/reference/actions/executions.md#intake-records). See [Session turns](https://www.twisp.com/docs/reference/actions/sessions.md#session-turns). Calls to a [tool with a session](https://www.twisp.com/docs/reference/actions/sessions.md#tool-sessions) record their history.

| Field         | Type                            | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inputSchema` | `JSON!`                         | JSON Schema 2020-12 for the session's initialization input, `session.input`. It must have `type: "object"`. External references are not supported; references into `$defs` are.                                                                                                                                                                                                                                                        |
| `id`          | `String!`                       | CEL expression over `session.input` that evaluates to the session ID, a nonempty string.                                                                                                                                                                                                                                                                                                                                               |
| `ends`        | `String`                        | CEL expression over `session.input` and `session.started_at` that evaluates to a timestamp: when each session ends, such as `session.started_at + duration('720h')`. Before it, no turn may complete the session; at it, the session gets a final turn, or completes with the reason `ended` when nothing is pending. Omitted: a session ends only when a turn completes it or an operator closes it. See [Session end](https://www.twisp.com/docs/reference/actions/sessions.md#session-end). |
| `schedule`    | `[ActionSessionScheduleInput!]` | Each session's own clock: up to 8 named entries whose firings are inputs to that session alone. See [Session clock](https://www.twisp.com/docs/reference/actions/sessions.md#session-clock).                                                                                                                                                                                                                                                                                                   |
| `turns`       | `ActionSessionTurnsInput`       | When a session works: which inputs start a turn, and how many one turn takes. Omitted uses the handler kind's defaults. See [Session turns](https://www.twisp.com/docs/reference/actions/sessions.md#session-turns).                                                                                                                                                                                                                                                                           |

| Schedule field | Type       | Meaning                                                                                                                                                                                                         |
| -------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`         | `String!`  | Names the entry: lowercase letters, digits, `_`, and `-`, starting with a letter, at most 64 bytes, and unique among the action's triggers and schedule entries. `turns.on` names entries as it names triggers. |
| `cron`         | `String`   | A standard 5-field cron expression, as for a [schedule trigger](https://www.twisp.com/docs/reference/actions/actions.md#schedule-triggers). It fires at each matching time after the session started.                                        |
| `timezone`     | `String`   | The IANA timezone `cron` is evaluated in. Omitted is `UTC`. Only with `cron`.                                                                                                                                   |
| `every`        | `Duration` | In place of `cron`: fires every period from the session's start, such as `"24h"`. Whole minutes, at least 1 minute.                                                                                             |

Each entry has exactly one of `cron` and `every`.

| Turns field | Type        | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ----------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `on`        | `[String!]` | Names of the triggers, or `schedule` entries, whose inputs start a turn. An input from another trigger or entry is delivered to the session as usual and waits for its next turn. Omitted or empty: every input starts a turn. Approval responses always do. It must name a `schedule` entry, or at least one trigger that can deliver to every open session: a start trigger, a resume trigger by `id`, or a resume trigger by a `session.input` field the input schema requires. |
| `window`    | `Duration`  | After the session has been idle, how long the first input that starts a turn waits for more to arrive, so a burst is one turn. At most 24 hours. Firings of the session's clock and the session's end never wait for it.                                                                                                                                                                                                                                                           |
| `maxInputs` | `Int`       | Most inputs one turn takes, at most 1000; `1` is one input per turn. Omitted or `0` uses the handler kind's default: every pending input up to 1000 for agents, one for Starlark actions.                                                                                                                                                                                                                                                                                          |
| `maxBytes`  | `Int`       | Most event bytes one turn takes, at most 1 MiB, except that a turn always takes at least one input. Omitted or `0` uses that ceiling.                                                                                                                                                                                                                                                                                                                                              |

## Session roles

With a `session` block, every trigger has a `session` role with exactly one of `start` or `resume`. Without one, no trigger or tool may declare a role and no expression may read `session`.

| Role              | Meaning                                                                                                                                                                                                                                                                                                              |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `start: {input}`  | Finds the session, or creates it. `input` is a JSON object mapping each initialization field to a CEL expression over `event`; the resulting object is validated against `inputSchema` and becomes `session.input`. A literal string needs a CEL string literal, such as `"'monitoring'"`.                           |
| `resume: {id}`    | Delivers the event to the open session whose ID is `id`, a CEL expression over `event` that evaluates to a string. Never creates a session.                                                                                                                                                                          |
| `resume: {input}` | Delivers the event to every open session whose `session.input` field equals a value. `input` is a one-entry JSON object mapping a top-level property of `inputSchema` to a CEL expression over `event`. The value must be a string, integer, boolean, or null; convert other values, such as UUIDs, with `string()`. |

A resume trigger's `event` predicate may also read `session.id` and `session.input`; those terms are checked against each session the key names. It must also constrain the events it selects through `event` fields, such as `event.type`; a predicate that would select every event is rejected. A start predicate and its mapping cannot read `session`, which may not exist yet. `session.state` and other session fields are not available to expressions. Paths into `session.input` must exist in `inputSchema` when it disallows additional properties. A start mapping must cover every property `inputSchema` requires, since defaults are not inserted.

An action with a session needs a start trigger or a starting [tool session](https://www.twisp.com/docs/reference/actions/sessions.md#tool-sessions). Each session keeps the action version it started with, so later definition updates do not change the triggers, schemas, tools, end, or clock of existing sessions.

```graphql
mutation CreateMonitor($id: UUID!) {
  actions {
    createAction(
      id: $id
      input: {
        name: "early-fraud-detector"
        kind: "agent.echo"
        config: { agent_id: "monitor", sleep_ms: 0 }
        triggers: [
          {
            name: "opened"
            event: "event.type == \"account.inserted\""
            session: { start: { input: { account_id: "string(event.data.account_id)" } } }
          }
          {
            name: "entry"
            event: "event.type == \"entry.inserted\""
            session: { resume: { id: "string(event.data.account_id)" } }
          }
        ]
        session: {
          inputSchema: {
            type: "object"
            properties: { account_id: { type: "string", format: "uuid" } }
            required: ["account_id"]
            additionalProperties: false
          }
          id: "string(session.input.account_id)"
          turns: { on: ["entry"], maxInputs: 100 }
        }
      }
    ) {
      actionId
      definition {
        session {
          id
          turns {
            on
            maxInputs
          }
        }
      }
    }
  }
}
```

## Session turns

A session advances in **turns**. A turn is one pass of the action's handler over a fixed set of the session's pending inputs; for an [agent](https://www.twisp.com/docs/reference/actions/agents.md), that is its whole loop of model calls and tool calls, up to its outcome. Turns are numbered from 1 and run one at a time.

Each event is delivered to a session once, however many of its triggers match, and waits in the session until a turn takes it. Events that arrive while a turn runs wait, and the next turn takes every pending input up to the `turns` limits, in the order they were delivered. With `turns.on`, only an input of a trigger it names starts a turn; any other input waits for that turn, which takes it in the order it was delivered. In the example above, opening an account starts the session, and its first entry starts the first turn, which sees both. `window` delays only the first turn after the session has been idle, and never a turn that a firing of the session's clock or the session's end starts.

The handler receives the session's ID, `input`, current `state`, and the turn's inputs, and returns the session's next `state` and status: `OPEN`, `WAITING`, `COMPLETED`, or `FAILED`; a session with an [end](https://www.twisp.com/docs/reference/actions/sessions.md#session-end) limits when it may report `OPEN` and `COMPLETED`. The new state, the status, the turn's output, and the consumption of its inputs commit together, or not at all, so turn N+1 always builds on what turn N committed. A turn runs as one execution, and every retry of that execution receives the same inputs and state. If the execution fails after its retries, or with an error that is not retried, the turn is `FAILED` and so is the session: its inputs remain, unconsumed, and the reason names the execution. An agent's turn that the model refused, or that did not report an outcome, is [held](https://www.twisp.com/docs/reference/actions/sessions.md#held-turns) instead: the session waits for a person's answer. `COMPLETED`, `FAILED`, and `CANCELLED` are final; later events are not delivered.

A turn reads as an `ActionSessionTurn`:

| Field                             | Type                         | Meaning                                                                                                                                                                                                                                                                                                       |
| --------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                              | `ID!`                        | Global identifier for the `node(id)` query.                                                                                                                                                                                                                                                                   |
| `actionId`, `sessionId`, `number` | `UUID!`, `String!`, `Int!`   | The session, and the turn's number within it, from 1.                                                                                                                                                                                                                                                         |
| `status`                          | `ActionSessionTurnStatus!`   | `RUNNING` while it runs or waits to retry, `COMMITTED` once it succeeded, `FAILED` if it failed without committing, `HELD` if it failed without committing and its session waited for a person's answer (see [Held turns](https://www.twisp.com/docs/reference/actions/sessions.md#held-turns)), and `CANCELLED` if the session was cancelled before it could commit. |
| `inputs`                          | `[ActionSessionTurnInput!]!` | The turn's inputs, in the order they arrived. Each carries its `event`, unless the inputs together exceed the size delivered inline; read the rest through the session's `inputs`.                                                                                                                            |
| `checkpoint`                      | `ActionSession`              | The session as the turn left it: the version it committed, with the `state`, `status`, and `reason` it wrote. Null unless `COMMITTED`.                                                                                                                                                                        |
| `final`                           | `Boolean!`                   | Whether this is the session's [final turn](https://www.twisp.com/docs/reference/actions/sessions.md#session-end): it started at or after the session's end and takes every input then pending. Its outcome must complete the session.                                                                                                                                 |
| `endsAt`                          | `Timestamp`                  | The session's end when the turn started, or null if it has none.                                                                                                                                                                                                                                              |
| `executionId`, `execution`        | `UUID!`, `ActionExecution`   | The execution that runs the turn: its attempts, [transcript](https://www.twisp.com/docs/reference/actions/agents.md#transcripts), tool calls, and output.                                                                                                                                                                                  |
| `created`                         | `Timestamp!`                 | When the turn started.                                                                                                                                                                                                                                                                                        |

A turn's execution has no `event`. Its `turn` field reads the turn it runs, and its `session` field the session. A [replay](https://www.twisp.com/docs/reference/actions/executions.md#replays) of a turn's execution also reads that turn through `turn`, and receives its inputs, firings included, and its `final` and `endsAt` as the original turn had them.

While its turn runs, a session action's credential can read its own session, that session's turns, history, and inputs. It cannot read other sessions, list sessions, or change any session.

## Session end

`session.ends` gives each session an end. It is evaluated when the session starts, over the session's `input` and `started_at`, and the result is the session's `endsAt`. When a start event that committed earlier moves the session's `startedAt` earlier, `endsAt` is evaluated again and moves with it. An `ends` that fails to evaluate for a session's input, or does not produce a timestamp, fails the start: no session is created, and the event is recorded as an [intake record](https://www.twisp.com/docs/reference/actions/executions.md#intake-records).

- **Before its end**, no turn may complete the session. A turn may keep it `OPEN`, or leave it `WAITING` for an [approval](https://www.twisp.com/docs/reference/actions/sessions.md#approvals); a turn of any handler that reports `COMPLETED` fails with `action_output_invalid`, and an [agent](https://www.twisp.com/docs/reference/actions/agents.md#turn-outcome) is offered only `OPEN`. An operator can still close the session with `closeSession`.
- **At its end**, a session with pending inputs gets a **final turn** at once, marked `final`. The final turn takes everything pending, whatever `turns.on` names, and waits for no `window`. Its outcome must be `COMPLETED`, with a reason; `OPEN` fails as invalid output.
- **Nothing delivered before the end goes unreviewed.** A turn is final only if it takes every input pending when it starts. When `maxInputs` or `maxBytes` leave some, it is an ordinary turn, which may only keep the session open, and the next turn continues. A turn that is running when the end passes finishes as an ordinary turn, and then the end applies.
- **With nothing pending at its end**, the session completes with no turn: status `COMPLETED`, reason `ended`.
- A session `WAITING` for an approval response at its end stays `WAITING` until the response arrives. The turn that takes the response, with everything pending with it, is final.
- Inputs delivered while the final turn runs stay `PENDING` when the session completes, kept as evidence, as for any closed session.

Without `ends`, a session completes only when a turn reports `COMPLETED` or an operator closes it; a session that receives no inputs stays `OPEN`.

## Session clock

`session.schedule` gives each session its own clock: up to 8 named entries, each a `cron` expression evaluated in its `timezone`, or an `every` period. A `cron` entry fires at each time its expression matches after the session's `startedAt`, and an `every` entry at `startedAt + every`, `startedAt + 2 × every`, and so on. No entry fires at or after the session's end. Expressions, timezones, and daylight-saving changes follow the rules of [schedule triggers](https://www.twisp.com/docs/reference/actions/actions.md#schedule-triggers), and a firing comes within about a minute of its time.

Each firing is an input of kind `SCHEDULE` to that session alone. Its event is a `schedule.fired` event whose `data` is `{schedule, scheduled_at, missed}`: the entry's name, the scheduled time, and how many earlier firings of the entry the turn passed over. Its identity is `schedule/<name>/<scheduled_at>`.

- A firing starts a turn when `turns.on` is omitted or names its entry. Otherwise it waits for the next turn, as an input of a trigger that `on` does not name does.
- A firing never waits for a `window`: one that starts a turn starts it at once, with every input pending with it.
- A turn takes at most one firing of each entry, the latest that is due. `missed` counts the entry's firings it passed over since the last one a turn took, for example while the session was `WAITING` or a turn was running.
- Firings are placed among a turn's inputs by their scheduled time, against the commit times of its events.
- A firing is recorded among the session's `inputs` when a turn takes it.

```graphql
session: {
  inputSchema: {
    type: "object"
    properties: { account_id: { type: "string", format: "uuid" } }
    required: ["account_id"]
  }
  id: "string(session.input.account_id)"
  ends: "session.started_at + duration('720h')"
  schedule: [
    { name: "morning", cron: "0 9 * * 1-5", timezone: "America/New_York" }
    { name: "hourly", every: "1h" }
  ]
  turns: { on: ["entry", "morning"] }
}
```

With the triggers of the [example above](https://www.twisp.com/docs/reference/actions/sessions.md#session-roles), each session ends 30 days after it starts. An `entry` input or the weekday `morning` firing starts a turn; an `hourly` firing waits for the next turn, which receives only the latest one, with `missed` counting those it passed over.

## Inspecting and closing sessions

| Operation                                                     | Behavior                                                                                                                                                                                                                                                                                                                                                                             |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `session(actionId, id, version)`                              | The session, or null. Pass `version` for an exact historical version. `Action.session(id, version)` reads the same session from its action.                                                                                                                                                                                                                                          |
| `sessions(where, sort, first, after)`                         | Sessions of every action ordered by creation time, newest first unless `sort: ASC`. `where` takes `actionId`, `sessionId` (with `actionId`), `status`, `createdAfter`, and `createdBefore`. `Action.sessions` lists one action's.                                                                                                                                                    |
| `closeSession(actionId: UUID!, id: String!, reason: String!)` | Mutation. Cancels the session with a retained `reason`. It accepts no further input, a running turn cannot commit and becomes `CANCELLED`, and its inputs and turns remain readable. Effects already performed are not undone. Closing a session that is already closed, whether cancelled, completed, or failed, returns it unchanged, with the status and `reason` it closed with. |

A session reaches everything that happened in it:

| Field                       | Type                                  | Meaning                                                                                                                                                                                                                                                      |
| --------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`                        | `ID!`                                 | Global identifier for the `node(id)` query.                                                                                                                                                                                                                  |
| `actionId`, `sessionId`     | `UUID!`, `String!`                    | The owning action and the session ID, unique within it.                                                                                                                                                                                                      |
| `action`                    | `Action!`                             | The action version the session is pinned to.                                                                                                                                                                                                                 |
| `status`, `reason`, `state` |                                       | Lifecycle, and the handler's state as the latest committed turn left it.                                                                                                                                                                                     |
| `startedAt`                 | `Timestamp!`                          | Commit time of the session's `origin`.                                                                                                                                                                                                                       |
| `endsAt`                    | `Timestamp`                           | When the session ends, from the definition's `session.ends`; null when it declares none. It moves with `startedAt`. See [Session end](https://www.twisp.com/docs/reference/actions/sessions.md#session-end).                                                                                                         |
| `turn(number)`              | `ActionSessionTurn`                   | One turn by its number, or null.                                                                                                                                                                                                                             |
| `turns(sort, first, after)` | `ActionSessionTurnConnection!`        | The session's turns, newest first unless `sort: ASC`. The first is the latest turn: its `number` is the session's turn count, and it is `RUNNING` while the session is busy.                                                                                 |
| `history(first, after)`     | `ActionSessionConnection!`            | Every version of the session, newest first: each turn's start and commit, and each status change.                                                                                                                                                            |
| `inputs(first, after)`      | `ActionSessionInputRecordConnection!` | Inputs delivered to turns, in delivery order, then `PENDING` inputs in the order they arrived. Each carries its `event` and the `execution` of the turn it was delivered to. A `PENDING` input has no record yet, so its `version` and `_recordId` are null. |

Each input has a `kind`: `EVENT`, an event a trigger matched; `COMMAND`, such as an [approval](https://www.twisp.com/docs/reference/actions/sessions.md#approvals) response; `CALL`, a call to a [tool with a session](https://www.twisp.com/docs/reference/actions/sessions.md#tool-sessions); `SCHEDULE`, a firing of the [session's clock](https://www.twisp.com/docs/reference/actions/sessions.md#session-clock), whose event is a `schedule.fired` event; or `REQUEST`, a request to an [MCP server](https://www.twisp.com/docs/reference/actions/mcp.md#sessions), with its execution and the number of tool calls it made in `calls`.

An input's event reads the ledger record it refers to through `record`. This query follows one session turn by turn, newest first: what each turn received, the session as it left it, and, from its execution, what it replied and did:

```graphql
query Session($actionId: UUID!, $id: String!) {
  actions {
    action(id: $actionId) {
      session(id: $id) {
        status
        state
        turns(first: 50) {
          nodes {
            number
            status
            inputs {
              event {
                eventType
                record {
                  ... on KVValue {
                    namespace
                    key
                    value
                  }
                }
              }
            }
            checkpoint {
              state
              status
            }
            execution {
              invocation {
                output
                transcript(first: 100) {
                  nodes {
                    kind
                    data
                  }
                }
              }
              children(first: 50) {
                nodes {
                  action {
                    definition {
                      name
                    }
                  }
                  status
                }
              }
            }
          }
        }
      }
    }
  }
}
```

## Approvals

A tool reference of an agent can require approval with `approval: REQUIRED`. Before each call to that tool, the agent's turn ends and commits with the session `WAITING`: everything it did before the call commits with it, and its execution's `invocation.approvals` lists the call it paused before, with the tool, the tool version the call would run, the call's effective input, and the `executionId` of the execution that first paused before it; `toolAction` and `execution` read the tool version and that execution. A request the session still waits on is listed again, under the same `interruptId` and `executionId`, by the execution of each later turn that ends with the session `WAITING`. Other inputs that arrive while the session waits stay `PENDING`, in the order they arrived, and are delivered once it is `OPEN` again. A gated tool cannot be called from programmatic tool calling; there the call fails with an error the code can catch, telling the agent to call the tool directly.

Answer a request with `respondToSession`. The response is delivered to the session as a `COMMAND` input, in a turn of its own, and the agent continues from where it paused: an approved call runs, with the replacement `input` if the response supplies one and the reference's fixed input still applied, and a denied call is reported to the agent as denied, with the `reason`. Each request is answered once. The caller needs the `RESPOND` policy action on the action's record (`financial.Action.document`), and the input's event, of type `action_session.responded`, records the decision, the replacement input, the reason, and the responder.

| Mutation           | Arguments                                                                                         | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------ | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `respondToSession` | `actionId: UUID!`, `id: String!`, `interruptId: String!`, `response: ActionSessionResponseInput!` | Answers the approval request `interruptId` of a `WAITING` session with `response.approval`: `decision: APPROVE` or `DENY`, an optional replacement `input` for an approval, and an optional `reason` of at most 1 KiB. Answers a [held turn](https://www.twisp.com/docs/reference/actions/sessions.md#held-turns) with `response.hold` instead. Returns the admitted input, `PENDING`. Fails if the session is not `WAITING`, the request is not one it waits on, or the request was already answered. |

```graphql
mutation Approve($actionId: UUID!, $interruptId: String!) {
  actions {
    respondToSession(
      actionId: $actionId
      id: "3f0c9f8e-account"
      interruptId: $interruptId
      response: { approval: { decision: APPROVE, reason: "Confirmed with the customer." } }
    ) {
      identity
      kind
      status
    }
  }
}
```

## Held turns

A session turn that the model declines to continue fails with `agent_refused`. A turn that does not report an outcome is retried, and fails with `agent_unfinished` once its retries are spent. Either way, nothing the turn did commits: its writes, its calls' writes, and what it said are discarded, and its execution keeps its transcript. Instead of failing the session, the turn is held. Its inputs are received, the session's state stays as the turn before left it, and the session is `WAITING` with a reason naming the turn. The turn is listed as `HELD`, and its execution's `invocation.hold` has the `interruptId` to answer, the `cause`, `REFUSED` or `UNFINISHED`, and, for a refusal, the `category` and `explanation` the model provider reports, if any. Other inputs that arrive while the session holds stay `PENDING`, and are delivered once it is `OPEN` again.

Answer a held turn with `respondToSession` and `response.hold`:

- `RETRY` runs the held turn's inputs again, in a turn of their own, followed by the answer as a `COMMAND` input. The retry turn's `inputs` lists only that `COMMAND`; the inputs it runs again stay with the held turn, and its transcript shows the agent receiving them. The answer's optional `note`, at most 1 KiB, is shown to the agent with the inputs. A retry of a turn that resumed from approval responses runs the approved calls again, and the agent sees the note with their results. The turn runs at the session's pinned version, so a change to the agent applies only to sessions started after it.
- `SKIP` goes on without the held turn's inputs, with no model call, and the session is `OPEN` again. A turn that resumed from approval responses cannot be skipped, because the responses it consumed cannot be given again: retry it, or close the session.

Each hold is answered once, by a caller with the `RESPOND` policy action, and the answer's `action_session.responded` event records the decision, the note, and the responder. Closing the session ends it instead. A turn that is held again after a retry is answered the same way, and a retry of it runs the inputs of the turn it retried too.

```graphql
mutation Retry($actionId: UUID!, $interruptId: String!) {
  actions {
    respondToSession(
      actionId: $actionId
      id: "3f0c9f8e-account"
      interruptId: $interruptId
      response: {
        hold: { decision: RETRY, note: "The transfer was reviewed; continue monitoring." }
      }
    ) {
      identity
      status
    }
  }
}
```

## Tool sessions

A tool with a `session` records its calls per session ID. Its `tool.session` has a `start` with the same shape as a start trigger's, with expressions over `tool.input` instead of `event`. Every call starts its session; a tool session has no `resume`. A tool with a session has no triggers, `turns`, `ends`, or `schedule`.

A tool session is call history only: it has no state, inbox, or turns, and calls to it never wait for one another. A call finds its session, or creates it before the call runs, so concurrent first calls for one session all succeed; a session whose first call rolls back exists with no history. A call whose session already exists with a different input fails. A call whose session is closed fails with `session_closed` and runs and records nothing. A call checks the session's status when it starts, so a call that is already running when an operator closes the session still records when its transaction commits: a closed session can gain calls from transactions that were in flight at the close. Each committed call adds one `CALL` input to its session, with the effective input, the output, and the calling agent execution, if any; the input's identity is the call's `executionId`. The record commits or rolls back with the caller, so the history lists committed calls only. Calls are listed in the order their transactions began, and calls made in one transaction keep the order they were made in. A transaction lists its own uncommitted calls in that order too. A call made by a transaction that is still running when you read a page can commit later with a position before that page's cursor: continuing from the cursor does not return it, and a new listing does.

A Starlark tool reads its session as `event["session"]`, with `id` and `input`. It can read the calls before it through its session's `inputs`, which include the calls made earlier in the same transaction. `session(actionId, id)` finds the session a call records in even when the call's transaction began before the session was created. The session stays `OPEN` until an operator closes it.

```graphql
tool: {
  name: "notify_human_ops"
  description: "Notify human operations about a finding."
  inputSchema: {
    type: "object"
    properties: { account_id: { type: "string" }, note: { type: "string" } }
    required: ["account_id"]
  }
  session: { start: { input: { account_id: "tool.input.account_id" } } }
}
```
