# Agents

Define agents that run a model in a loop over their tools and skills, with every step recorded.

An **agent** is an action of kind `agent` whose handler runs a model in a loop. In each turn the model reads the turn's input, calls tools, loads skills, and replies. Everything the agent does beyond talking to the model is an execution in the ledger, with its input, output, and parent: every tool call, skill load, input read, memory search, and context retrieval. Every model request and response is recorded in the invocation's [transcript](https://www.twisp.com/docs/reference/actions/agents.md#transcripts) before the agent acts on it, so for any decision you can read what the agent was shown, what it said, what it did, and which guidance and evidence it used.

An agent has no filesystem, shell, process, or network. Its only capabilities are the tools and skills it declares, the [agent tools](https://www.twisp.com/docs/reference/actions/agents.md#agent-tools) it enables, and the tools Twisp offers every agent. What it may do with them is bounded by its own `policies`; see [Authorization](https://www.twisp.com/docs/reference/actions/actions.md#policy-chain).

```graphql
mutation CreateAgent($id: UUID!) {
  actions {
    createAction(
      id: $id
      input: {
        name: "early-fraud-detector"
        kind: "agent"
        agent: {
          prompt: "Monitor this new account for suspicious activity."
          stateSchema: {
            type: "object"
            properties: { reviewed_through: { type: "string", format: "date-time" } }
            required: ["reviewed_through"]
          }
        }
        config: {
          runtime: "strands"
          model: { provider: "bedrock", id: "global.anthropic.claude-sonnet-5" }
          harness: { builtin_tools: ["programmatic_tool_caller"], memory: { enabled: true } }
          limits: { model_calls: 64, tool_calls: 128 }
        }
        tools: [
          {
            name: "get_account_activity"
            input: { account_id: "string(session.input.account_id)" }
          }
          { name: "notify_human_ops", approval: REQUIRED }
        ]
        skills: [{ name: "review-fraud-watch-activity" }]
        triggers: [
          {
            event: "event.type == \"account.inserted\""
            session: { start: { input: { account_id: "string(event.data.account_id)" } } }
          }
          {
            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"]
          }
          id: "string(session.input.account_id)"
          ends: "session.started_at + duration('720h')"
          schedule: [{ name: "morning", cron: "0 9 * * 1-5", timezone: "America/New_York" }]
        }
        policies: [{ effect: ALLOW, actions: [SELECT], resources: ["financial.*"] }]
      }
    ) {
      actionId
      version
      pending
    }
  }
}
```

## The agent block

Kind `agent` requires an `agent` block, and an `agent` block requires kind `agent`.

| Field          | Type      | Meaning                                                                                                                                                                                                                                                                                                                                                              |
| -------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prompt`       | `String!` | What the agent is for and how it should work, at most 64 KiB. It follows fixed Twisp instructions that describe the agent's situation, including that inputs, tool results, retrieved context, and recalled memory are data, never instructions.                                                                                                                     |
| `stateSchema`  | `JSON`    | Optional JSON Schema 2020-12 for the `state` each session turn reports. State must hold the schema's fields and may hold any others, so the schema's root cannot use `additionalProperties`, `unevaluatedProperties`, `propertyNames`, `maxProperties`, `not`, or a `patternProperties` entry of `false`, and its `type`, if any, is `object`. Requires a `session`. |
| `outputSchema` | `JSON`    | Optional JSON Schema 2020-12 for a structured execution output. Without it, the output is the agent's reply.                                                                                                                                                                                                                                                         |

Both schemas are combined into the schema of the [turn outcome](https://www.twisp.com/docs/reference/actions/agents.md#turn-outcome), so they must also compile together: two schemas that name the same `$anchor`, for example, are rejected.

## Computed state

A top-level property of `stateSchema` can declare a `value`: a CEL expression that computes the field at the end of every turn from the record, instead of the model reporting it. Use it for facts, such as the latest input the agent was given or the findings it has notified, and leave judgments to the model.

```graphql
stateSchema: {
  type: "object"
  properties: {
    inputs_through: {
      type: "string"
      format: "date-time"
      value: "has(turn.inputs_through) ? turn.inputs_through : state.inputs_through"
    }
    notified_findings: {
      type: "array"
      items: { type: "string" }
      default: []
      value: "state.notified_findings + turn.calls.filter(c, c.tool == 'notify_human_ops' && c.ok && c.output.status == 'accepted').map(c, c.input.finding_id)"
    }
    open_findings: { type: "array", items: { type: "string" } }
  }
  required: ["inputs_through", "notified_findings", "open_findings"]
}
```

| Variable              | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state`               | The state the turn started from, where a field that is absent holds its `default`, if it has one. It is JSON as the last turn committed it: convert numbers with `int()` or `double()` before arithmetic, and timestamps and amounts, which are strings, with `timestamp()` or `money()`.                                                                                                                                                                                                                                                                                                                                                                                                       |
| `turn.inputs`         | The turn's inputs in the order they arrived, each with `n`, `identity`, `kind` (`event`, `command`, or `schedule`), `type`, `committed_at`, `triggers` (their names, or positions for triggers without one), `view` (what the turn showed, or null), `event`, and, for a firing of the [session's clock](https://www.twisp.com/docs/reference/actions/sessions.md#session-clock), `source` (the schedule entry's name) and `scheduled_at`, both null for other inputs.                                                                                                                                                                                                                                                       |
| `turn.inputs_through` | The latest `committed_at` among the turn's event inputs; absent when it has none. Firings do not count.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `turn.final`          | Whether the turn is the session's [final turn](https://www.twisp.com/docs/reference/actions/sessions.md#session-end).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `turn.calls`          | Every tool call the agent made in the turn, in order, including calls from programmatic tool calling and helpers, and calls refused before they ran: `tool`, `input` (the input it ran with, fixed input applied), `ok`, and `output` or `error` (`code`, `message`). Skill loads and harness tools, such as `programmatic_tool_caller` and `subagent`, are not listed; the calls they make are. A call that succeeded in a program, or under a helper, that then failed is not `ok`, since the program or helper discarded its writes: its `error` has code `rolled_back`, naming the `programmatic_tool_caller` or `subagent` call as `rolledBackByExecutionId` does, and it has no `output`. |
| `turn.now`            | The turn's time, as its first message shows it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `session`             | `id`, `input`, `started_at`, and `ends_at`, the session's [end](https://www.twisp.com/docs/reference/actions/sessions.md#session-end), or null when it has none.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |

The turn's outcome schema leaves out computed fields, so the model reports only the others, and when every field is computed the outcome needs no `state`; a computed field the model includes anyway is replaced. The turn's first message names the computed fields in the `computed` attribute of `<state>`. Values in `turn` keep their types until they are written to `state`. If an expression fails, or its result does not satisfy the property's schema, the field keeps the value it had when the turn started and the turn's transcript records the error in a `state.value` event; the turn does not fail. The state, computed fields included, is at most 16 KiB: when computed fields would take it over, each keeps its starting value, and a state still over the limit fails the turn with `agent_limit_exceeded`. A turn that waits for approval computes its values too. `value` is allowed only on top-level properties, at most 64 per schema, and each computed field's `default` must satisfy its property's schema.

## Agent configuration

An agent's `config` is a JSON object. Unknown fields are rejected, and every provider and model ID is checked against the models the deployment allows, so a definition cannot name a model the environment cannot call.

| Field                                | Default                                        | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ------------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `runtime`                            | required                                       | The agent loop. `strands`, the Strands agent harness, is the only runtime; its version is part of the Twisp release.                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `mode`                               | `live`                                         | `live`, or `preview` to run on real events and change nothing. See [Preview calls](https://www.twisp.com/docs/reference/actions/executions.md#preview-calls).                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `model.provider`                     | required                                       | `bedrock`, `anthropic`, or `fixture`. See [Models](https://www.twisp.com/docs/reference/actions/agents.md#models).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `model.id`                           | none                                           | The provider's model ID. Required except for `fixture`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `model.max_tokens`                   | `8192`                                         | Output tokens per model call, 1 to 128,000.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `model.effort`                       | `auto`                                         | Reasoning effort: `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. `off` is rejected for a model whose thinking is always on, such as Claude Opus 5.5 and Claude Fable 5.1.                                                                                                                                                                                                                                                                                                                                                                                                  |
| `model.temperature`                  | none                                           | 0 to 2, for a model that accepts one, such as Claude Haiku 4.5; rejected for models that do not. Claude models reasoning at an effort other than `auto` or `off` accept none, so it is not sent to them then.                                                                                                                                                                                                                                                                                                                                                                                     |
| `model.script`                       | none                                           | For `fixture`: the scripted responses. See [Fixture model](https://www.twisp.com/docs/reference/actions/agents.md#fixture-model).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `model.replay`                       | none                                           | For `fixture`: `{"execution_id": ..., "invocation_number": ...}` answers with the model outputs that invocation of this agent recorded.                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `model.context_window`               | `200000`                                       | For `fixture`: the model's context window, in tokens, at most 10,000,000. Other models' windows are their own.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `harness.context.manager`            | `auto`                                         | How the agent keeps its conversation within the model's context window: `auto` sets bulky tool results aside and summarizes older messages, with a model call, as the context fills; `agentic` lets the model manage its own context.                                                                                                                                                                                                                                                                                                                                                             |
| `harness.context.compact_at`         | `{"tokens": 100000}`                           | With `auto`: when older messages are summarized, as `{"percent": N}` of the model's context window, 1 to 95, or `{"tokens": N}`, below the window. A model whose window is not above the default must set it.                                                                                                                                                                                                                                                                                                                                                                                     |
| `harness.context.keep_last`          | `4`                                            | With `auto`: how many of the latest messages a summary keeps word for word.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `harness.context.summary`            | none                                           | `model` is the model that writes summaries, with purpose `compact`; without one, the agent's own model does. Its context window must be above `compact_at`. With `auto`, `prompt` replaces the summary instructions. The default keeps, in order, the operator's words and rulings, lasting effects with their IDs and failures, and open commitments; keeps per-entry detail such as amounts, dates, and IDs; and never records a question or hypothetical as fact.                                                                                                                              |
| `harness.context.offload`            | `{"over_tokens": 1500, "preview_tokens": 750}` | A tool result larger than `over_tokens` is set aside, out of the context, leaving a preview of at most `preview_tokens` that keeps the result's shape, and a reference the agent retrieves it with, using `retrieve_offloaded_content`. `preview_tokens` must be below `over_tokens`. A tool whose `tool.output.offload` is `NEVER`, and the agent's memory and history searches, are never set aside.                                                                                                                                                                                            |
| `harness.memory`                     | disabled                                       | `{"enabled": true}` gives the agent [long-term memory](https://www.twisp.com/docs/reference/actions/memory.md#agent-memory). An optional `model` is the model that extracts facts; without one, the agent's own model does. `scope: {read, write}` names the partitions of memory the agent reads and writes, each a list of `session` and `agent`, both `[session]` by default; see [Agent memory](https://www.twisp.com/docs/reference/actions/memory.md#agent-memory). `kinds`, `labels`, `pinned`, `extraction`, and `search` are described there. `tools` adds memory tools besides `search_memory`: `add_memory`, `read_memory`, and `delete_memory`. |
| `harness.history`                    | `{"index": "none"}`                            | For a session agent: `index` is what of each turn stays searchable with `search_history`: `none`, `messages`, the turn's message and the agent's replies, or `messages_and_tools`, also the results of the agent's tools. `search`, `true` by default, offers `search_history` when the history is indexed. See [Session history](https://www.twisp.com/docs/reference/actions/memory.md#session-history).                                                                                                                                                                                                                     |
| `harness.builtin_tools`              | `["programmatic_tool_caller"]`                 | [Agent tools](https://www.twisp.com/docs/reference/actions/agents.md#agent-tools) to enable: `programmatic_tool_caller` and `subagent`, as a list of names or as a mapping of each name to `true` or to its [configuration](https://www.twisp.com/docs/reference/actions/agents.md#configuring-agent-tools). A definition that leaves it unset stores the default; `[]` turns agent tools off. `shell`, `read`, `write`, `edit`, `web_fetch`, and `web_search` are rejected.                                                                                                                                                                                                                                  |
| `harness.context.prior_tool_results` | `clear`                                        | For a session agent: what each new turn does with earlier turns' tool results. `clear` replaces each result larger than a short note with a note naming the `retrieve_context` reference that returns it, and keeps smaller ones; `keep` carries them forward. The state each turn reports is what it carries forward.                                                                                                                                                                                                                                                                            |
| `harness.builtin_plugins`            | `[]`                                           | `todos`, a checklist the agent keeps, across turns in a session. `environment` is rejected.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `limits.model_calls`                 | `64`                                           | Model calls per invocation, for every purpose, at most 256.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `limits.tool_calls`                  | `128`                                          | Recorded calls per invocation, at most 256, including failed calls, skill loads, input reads, agent tool calls, and the calls programmatic tool calling makes.                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `limits.total_tokens`                | `2000000`                                      | Input plus output tokens per invocation, for every purpose, at most 100,000,000. Input tokens read from the model's prompt cache do not count.                                                                                                                                                                                                                                                                                                                                                                                                                                                    |

An invocation records at most 256 [output uses](https://www.twisp.com/docs/reference/actions/agents.md#agent-uses), and the limits must leave room for every use a turn can derive: `limits.tool_calls`, plus `limits.model_calls` when memory is enabled, plus the number of declared skills for an agent with a `session`, plus the number of preloaded skills, can be at most 256. The defaults always fit. A definition whose limits could exceed it is rejected, so a turn within its limits can always finish.

An agent's `responseTimeout` bounds one turn. It defaults to 5 minutes for agents and can be at most 10 minutes. A turn's work stops shortly before the deadline, so that it has time to commit.

## Models

| Provider    | Use                                                                                                                 |
| ----------- | ------------------------------------------------------------------------------------------------------------------- |
| `bedrock`   | Amazon Bedrock, in deployments that allow it.                                                                       |
| `anthropic` | The Anthropic API, in deployments configured with a key, such as a local server.                                    |
| `fixture`   | Deterministic scripted responses. Available everywhere; it calls no model and costs nothing. Use it to test agents. |

The models a deployment can allow are Claude Opus 5.5 (`claude-opus-5-5`), Claude Sonnet 5.5 (`claude-sonnet-5-5`), Claude Fable 5.1 (`claude-fable-5-1`), Claude Haiku 5.5 (`claude-haiku-5-5`), Claude Sonnet 5 (`claude-sonnet-5`), and Claude Haiku 4.5 (`claude-haiku-4-5`) through `anthropic`, and Claude Sonnet 5 and Claude Haiku 4.5 through `bedrock`. Every model is asked for the turn outcome the same way, without forcing a tool call, and the agent's options are checked against what its model accepts when the definition is written.

Every model call has a purpose: `turn` for the agent's loop, `outcome` for the [turn outcome](https://www.twisp.com/docs/reference/actions/agents.md#turn-outcome), `compact` for the summaries that keep a conversation within the context window, `memory` for memory extraction, or `subagent`. Purposes appear in the transcript and count against the same limits. Throttled and transient model failures, and a response the model produced malformed, are retried within the turn's deadline. A malformed response's tokens and its model call count against the limits like any other, so a model that keeps answering malformed fails with `agent_limit_exceeded` once it reaches them. A request the provider rejects fails the invocation without a retry with `model_request_rejected`, a model the deployment cannot call with `model_access_denied`, and any other model failure with `model_failed`. A deployment that cannot run the agent's runtime or provider fails it with `agent_runtime_unavailable`.

### Fixture model

A fixture replaces the model with a script. Each model call takes the next response from the first script entry whose `when` matches, and each entry keeps its own position through the invocation.

```json
{
  "provider": "fixture",
  "script": [
    {
      "when": "purpose == 'turn' && session.step == 0",
      "responses": [
        [
          {
            "toolUse": {
              "name": "skills",
              "input": { "skill_name": "review-fraud-watch-activity" }
            }
          }
        ],
        [
          {
            "toolUse": {
              "name": "get_account_activity",
              "input": {},
              "input_expr": { "through_time": "string(now)" }
            }
          }
        ],
        [{ "text": "Reviewed opening activity; nothing suspicious." }]
      ]
    },
    {
      "when": "purpose == 'outcome'",
      "responses": [
        {
          "outcome": {
            "state": { "reviewed_through": "2026-09-01T09:00:00Z" },
            "session": { "status": "OPEN" }
          }
        }
      ]
    }
  ]
}
```

- A response is one model message: a list of `text` and `toolUse` blocks. A message without a tool use ends the loop. In place of a list, a response can be `{"outcome": {...}}`, which answers an outcome request, or `{"refusal": {"category": "...", "explanation": "..."}}`, a model that declines to continue, which [holds](https://www.twisp.com/docs/reference/actions/sessions.md#held-turns) a session turn.
- `when` is CEL over `purpose`, `session` (`id`, `input`, `step`, `started_at`, `final`), and `now`. `session.step` is the number of turns before this one that committed or were [held](https://www.twisp.com/docs/reference/actions/sessions.md#held-turns), so it is `0` in the first turn, and `session.final` is true in the session's [final turn](https://www.twisp.com/docs/reference/actions/sessions.md#session-end), whose outcome must complete it. An entry without `when` always matches.
- `input_expr` computes input fields with CEL over the same values and `results`, the outputs of the invocation's earlier tool calls in order.
- A call with no response left fails the invocation with `fixture_exhausted`.

With the default context management and memory disabled, a script answers only the loop and the outcome until the context fills. With memory enabled, a script also answers each review for memories, purpose `memory`: a `text` block holding a JSON array of candidate memories, such as `[{"summary": "...", "kind": "fact"}]`, or `[]` for none. When a candidate resembles records the agent already has, a second `memory` call decides each one, answered with a JSON array such as `[{"candidate": 1, "action": "add"}]`, whose actions are `add`, `replace`, `delete`, and `skip`.

## Agent tools

The model is offered exactly:

- The tools in `tools`, by their `tool.name`, with the input fields that the reference fixes removed from their schemas. `graphql` and `cel` are offered only when declared. Declaring `graphql` gives the model the agent's full data access through arbitrary documents, so narrow tools with fixed input are usually the better choice.
- The skills in `skills`, listed by name and description, except those preloaded. Declaring a skill loads nothing; the model loads one with the `skills` tool when it needs it, and each load is an execution. A [preloaded skill](https://www.twisp.com/docs/reference/actions/skills.md#preloaded-skills) is loaded at the start of every turn, and its guidance is already in the agent's instructions.
- `read_turn_input`, on every turn of a session agent, and to an agent without a session when its input is shown through a trigger's [view](https://www.twisp.com/docs/reference/actions/agents.md#input-views). It returns one input of the turn by its position `n`, as its whole event, for an input listed by position only or shown through a view.
- The agent tools the configuration enables: `programmatic_tool_caller`, which runs a short program that calls the agent's tools and prints only its result, and `subagent`, which hands a subtask to a helper that uses the agent's model and tools, or those of the [preset](https://www.twisp.com/docs/reference/actions/agents.md#configuring-agent-tools) the model picks. The features the configuration enables add their own tools, such as `search_memory` with memory, `retrieve_offloaded_content` and `retrieve_context` to bring back tool results and content set aside from the context window, and `todo_write` with `todos`. With `harness.context.manager` set to `agentic`, the model manages its own context with `summarize_context`, `truncate_context`, and `pin_context`.
- `search_history`, on every turn of a session agent whose `harness.history` indexes its history, unless `search` is `false`. See [Session history](https://www.twisp.com/docs/reference/actions/memory.md#session-history).

A call to anything else fails, and the model is told so. The names of the agent tools, `read_turn_input`, `search_history`, `skills`, and `TurnOutcome` are reserved: no tool or skill can use them.

Each of these tools takes and returns:

| Tool                         | Input                                                                                                                                                                                                                              | Output                                                                                                                                                                            |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `read_turn_input`            | `n`, the input's position.                                                                                                                                                                                                         | The input: `n`, `identity`, `kind`, `type`, `committed_at`, the `triggers` it matched, and the event's `data` and `previous`.                                                     |
| `todo_write`                 | `todos`, the whole updated list: each item's `content`, `activeForm`, shown while it is in progress, and `status`, `pending`, `in_progress`, or `completed`. An empty list clears it.                                              | The list as the model sees it, with how many items remain.                                                                                                                        |
| `programmatic_tool_caller`   | `code`: Python, in a sandboxed subset, that calls each tool it may call as an async function with keyword arguments.                                                                                                               | The text the program printed.                                                                                                                                                     |
| `subagent`                   | `task`, self-contained. With presets, `agent_type`, the preset, and nothing else; without presets, optionally `instructions`, `tools`, a subset of the agent's, `context`, `none`, `all`, or `no_tools`, and `last_messages`.      | The helper's final reply.                                                                                                                                                         |
| `retrieve_offloaded_content` | `reference`, from a set-aside result's preview, and optionally `pattern`, a regular expression or keyword, `line_range`, `{start, end}` from 1, and `context_lines` around each match, 5 by default.                               | The matching lines, numbered, with their context; the lines in range; or, with neither, the whole result.                                                                         |
| `retrieve_context`           | As for `retrieve_offloaded_content`, with a reference from the note left in place of content the context manager set aside, such as an earlier turn's tool result.                                                                 | As for `retrieve_offloaded_content`.                                                                                                                                              |
| `summarize_context`          | Optionally `keep_recent`, the latest messages to keep word for word, 10 by default; `summary_ratio`, the share of the oldest messages to summarize, 0.1 to 0.8, 0.3 by default; and `message_type`, `all`, `messages`, or `tools`. | A line saying how many messages it summarized and how many remain, or why it summarized none. The summary is written by a model call with purpose `compact`, made under the call. |
| `truncate_context`           | Optionally `keep_recent`, 10 by default, and `message_type`.                                                                                                                                                                       | A line saying how many of the oldest messages it dropped and how many remain, or why it dropped none.                                                                             |
| `pin_context`                | `select`: `last_turn`, a number of the latest messages, or a list of message positions from 0; optionally `filter`, `user`, `assistant`, or `tools`, and `action`, `pin`, the default, or `unpin`.                                 | A line saying how many messages it pinned or unpinned. Summarizing and truncating keep pinned messages.                                                                           |

The memory tools and `search_history` are described in [Memory and History](https://www.twisp.com/docs/reference/actions/memory.md).

Each of these tools is a native tool: `actions(where: { native: true })` lists its definition, with the description the model reads and its input and output schemas. The memory tools' and `subagent`'s input schemas there are the widest any agent is offered; each agent is offered only the fields and values its configuration allows, such as its own memory kinds and labels. The output schema is what a call's execution records: the result the model was shown, and for the memory tools and `search_history`, the records they returned or wrote.

### Configuring agent tools

Give `harness.builtin_tools` as a mapping to configure an agent tool. A mapping enables exactly the tools it names with `true` or a configuration; `false` leaves a tool off.

```json
{
  "builtin_tools": {
    "programmatic_tool_caller": { "timeout": 60, "allowed_tools": ["get_activity"] },
    "subagent": {
      "max_depth": 1,
      "default_preset": "researcher",
      "presets": {
        "researcher": {
          "description": "reads account activity and summarizes it",
          "instructions": "You research account activity. Report counterparties and totals.",
          "tools": ["get_activity", "programmatic_tool_caller"],
          "model": {
            "provider": "bedrock",
            "id": "global.anthropic.claude-haiku-4-5-20251001-v1:0"
          }
        },
        "reviewer": {
          "description": "reviews a draft finding against the conversation so far",
          "instructions": "You review draft findings. Say what is unsupported.",
          "tools": [],
          "context": "no_tools"
        }
      }
    }
  }
}
```

| Field                                    | Default   | Meaning                                                                                                                                                                                                                                                                                                          |
| ---------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `programmatic_tool_caller.timeout`       | `600`     | Seconds one program may run, its calls included, more than 0 and at most 600. Each program is also bounded by the time the turn has left when it starts, so a program that runs out of time fails with an error the agent can act on before the turn's deadline, and a call it is still waiting on is cancelled. |
| `programmatic_tool_caller.allowed_tools` | all       | The tools a program may call, by name. Without it, a program may call every declared tool, `skills` when any skill is not preloaded, and the tools skill loads make available. Any other call from a program fails.                                                                                              |
| `subagent.max_depth`                     | `2`       | How many levels of helpers the agent may open, 1 to 3. At 1, the agent's helpers cannot delegate further.                                                                                                                                                                                                        |
| `subagent.presets`                       | none      | Named helper roles, at most 16. Names are lowercase letters, digits, `_` and `-`.                                                                                                                                                                                                                                |
| `subagent.default_preset`                | none      | The preset a delegation that names none uses. Required with more than one preset.                                                                                                                                                                                                                                |
| `subagent.presets.<name>.description`    | none      | When to pick the preset. The model sees it.                                                                                                                                                                                                                                                                      |
| `subagent.presets.<name>.instructions`   | required  | The helper's instructions.                                                                                                                                                                                                                                                                                       |
| `subagent.presets.<name>.tools`          | all       | The tools the helper may call, by the names the agent's model sees them: a subset of the agent's tools and agent tools. `[]` gives the helper none.                                                                                                                                                              |
| `subagent.presets.<name>.model`          | inherited | The model the helper calls, as `model` names one. Without it, the helper calls the model of the agent or helper that delegated to it.                                                                                                                                                                            |
| `subagent.presets.<name>.context`        | `none`    | How much of the delegator's conversation the helper sees: `none`, `all`, or `no_tools`, its text without tool calls and results.                                                                                                                                                                                 |
| `subagent.presets.<name>.last_messages`  | none      | Caps the shared conversation to its last messages. Only with `context` other than `none`.                                                                                                                                                                                                                        |

With presets, the model picks a preset by `agent_type` for each delegation and supplies only the task: it cannot write a helper's instructions or choose its tools. The helper can call only the tools its preset names, and a helper of a helper only the tools both presets name. A call outside them fails with `action_access_denied`. A tool a preset names that a turn does not offer is left out, and the turn's transcript warns about it. Each of a helper's model calls is recorded with purpose `subagent` and its preset, and counts against the agent's limits.

Every call is recorded as an execution in the agent's call tree: a call to a declared tool or skill is the tool's or skill's execution, and a call to an agent tool is an execution of that native tool, with the input the model supplied and the result it was shown. The calls a program or a helper makes are children of the `programmatic_tool_caller` or `subagent` call that made them, and commit with it: when that call fails, their writes roll back, and each stays recorded with `rolledBackByExecutionId` naming it. Calls within one turn run one at a time, and each sees the writes of the calls before it.

The calls one program makes commit together or not at all, as the calls of a Starlark tool do. When a program fails, for an uncaught error, its timeout, or any other reason, none of its calls' writes remain, even those of calls that succeeded; each of its calls, and each call beneath them, is still recorded, with `rolledBackByExecutionId` naming the program's call. The records of a program's calls are written when the program ends, so a query of `actionExecutions` made while it runs does not show them yet. A program also leaves nothing outside the ledger: work its calls would start when they commit starts only if the program succeeds, and an operation that acts outside the ledger or writes another tenant, such as a warehouse statement, `branch`, or an `admin` mutation, fails when called from a program, with a call error telling the agent to call the tool directly instead. An agent's tool calls in one response run one at a time, and a program ends before the next begins. A turn whose tools, skills, and inputs give a program nothing to call, or only tools that require [approval](https://www.twisp.com/docs/reference/actions/sessions.md#approvals), is not offered `programmatic_tool_caller`.

A call that fails with a [call error](https://www.twisp.com/docs/reference/actions/tools.md#calling-a-tool), such as invalid input, a business rule, or a permission error, is returned to the model with its message and hint, and leaves no writes; the model can change its input or its plan, and the turn can still commit. A write conflict, a passed deadline, or an interrupted transaction instead ends the turn, and the invocation is retried from the last committed state with a new transaction, so the model works on current data.

## Turns

A turn is the agent's whole loop over its input, every model call and tool call up to its reply or outcome, and it runs as one execution; a retry of the execution is another attempt at the same turn. An agent without a session runs one turn per execution. The model's first message is the typed triggering event, or, for a [call](https://www.twisp.com/docs/reference/actions/agents.md#callable-agents), the call's input.

A session agent runs its session's [turns](https://www.twisp.com/docs/reference/actions/sessions.md#session-turns) one at a time, and receives every pending input up to its `turns` limits in one turn: by default, every pending input up to 1000. The model's first message holds the session's ID, `step` (the number of turns the session committed before this one), `input`, the state the last turn reported, the current time, and the turn's inputs in the order they arrived. Inputs are shown inline up to 64 KiB, and the rest are listed by position for the model to read with `read_turn_input`, so every input is in view. The session keeps the agent's conversation, so each turn continues from the one before; older turns are compressed as the conversation grows, and the content they held stays retrievable.

For a session with an [end](https://www.twisp.com/docs/reference/actions/sessions.md#session-end), the message's `<turn>` element carries `ends_at`, and the final turn carries `final="true"`. The agent is told that it cannot end the session before then, and that its final turn must review what it holds and complete the session. A firing of the [session's clock](https://www.twisp.com/docs/reference/actions/sessions.md#session-clock) is listed without a body: `source` names the schedule entry, `scheduled_at` is its time, and `missed`, present when it is more than zero, counts the entry's earlier firings the turn passed over:

```xml
<input n="3" identity="schedule/morning/2026-10-02T13:00:00Z" kind="schedule" source="morning" scheduled_at="2026-10-02T13:00:00Z" missed="2"/>
```

### Input views

By default a turn shows each event whole: every field of the record, in its stored form, with enums as numbers. A trigger's `view` sets what an agent's turn shows of each event that trigger admits instead: the fields the agent needs, named as it needs them. The event itself is kept whole, and `read_turn_input` returns all of it. A `view` is an `ExpressionValue` over `event`, in one of three forms:

- An object whose string values are CEL expressions: `{ amount: "event.data.amount", direction: "event.data.direction == 1 ? 'CREDIT' : 'DEBIT'" }`. Nested objects and lists work the same way, and values that are not strings are kept as written.
- One CEL expression that builds an object: `"{'amount': event.data.amount, 'entry_id': event.data.entry_id}"`.
- One CEL expression that produces text: `"'Entry ' + string(event.data.entry_id) + ' (' + event.data.entry_type + ')'"`.

```graphql
triggers: [
  {
    event: "event.type == 'entry.inserted'"
    session: { resume: { id: "string(event.data.account_id)" } }
    view: {
      entry_id: "event.data.entry_id"
      amount: "event.data.amount"
      direction: "event.data.direction == 1 ? 'CREDIT' : 'DEBIT'"
      created: "event.data.created"
    }
  }
]
```

An object is shown as JSON and text as text. Values keep their types: identifiers stay UUIDs, amounts stay money (shown as `"9000.00 USD"`), and times stay timestamps. Each input is marked with `trigger`, the triggers it matched, and each input shown through a view with `view`, the trigger whose view it is: by name, or by position for a trigger without a name. When an input matched several triggers, the lowest-positioned one decides: its view, or the whole event when it has none.

A `view` is allowed only on an agent's triggers, and reads only `event`. It must be an object, or an expression that can produce a string or an object; the write rejects anything else, such as an expression of type `int`. A view holds up to 64 expressions. When a view fails for one event, because a field it reads is absent, it produces a number, a boolean, a list, or null, or it is larger than 4 KiB, that input is listed by position with the error as `view_error`, and the turn goes on: the agent can read the event with `read_turn_input`. The turn's [transcript](https://www.twisp.com/docs/reference/actions/agents.md#transcripts) records exactly what it showed. A view is not an access control: it sets what the agent is shown first, not what it can read.

A turn is one transaction: its tool calls' writes, its memory writes, the session's new state and conversation, its output, and the consumption of its inputs commit together or not at all.

## Turn outcome

A turn ends when the model replies without calling a tool. For an agent without a session or `outputSchema`, the reply's text is the output. A session turn, or an agent with `outputSchema`, also reports its outcome, with the `TurnOutcome` tool: usually as the last call of its turn, or, when it replies without one, in answer to an outcome request, a model call with purpose `outcome`. Such a turn replies through the outcome's `reply`: when the outcome has one, it is the turn's reply, and text the model writes between tool calls is not. In the transcript, the response that reports it has `outcome: true`, and each check of it against the agent's schema is an `outcome.check` event. The outcome:

```json
{
  "reply": "One new finding: a $15,000.00 wire to a first-time counterparty. Watching it until Sep 26.",
  "state": {
    "reviewed_through": "2026-09-23T14:05:00Z",
    "open_findings": [
      {
        "entry_id": "5c1e2f40-8a3b-4d6e-9f10-2b7c4d8e6a15",
        "amount": "15000.00 USD",
        "watch_until": "2026-09-26T09:12:00Z"
      }
    ]
  },
  "output": { "findings": 1 },
  "session": { "status": "OPEN" }
}
```

| Field            | Present when               | Meaning                                                                                                                                                                                                            |
| ---------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `reply`          | Always offered, optional   | The turn's reply: answers, findings, and anything meant for a person, at most 64 KiB. Without `outputSchema`, it is the execution's output. When it is left out, the output is the text of the model's last reply. |
| `state`          | `stateSchema` is declared  | The session state to report, at most 16 KiB. It must hold the schema's fields and may hold others. It becomes the session's `state`.                                                                               |
| `output`         | `outputSchema` is declared | The execution's output, validated against `outputSchema`.                                                                                                                                                          |
| `session.status` | Session turns              | `OPEN` to continue the session, or `COMPLETED` to end it. For a session with an [end](https://www.twisp.com/docs/reference/actions/sessions.md#session-end), only `OPEN` before its final turn, and only `COMPLETED` on it.                     |
| `session.reason` | Session turns              | Why the session ended, at most 1 KiB. Required for `COMPLETED`, and otherwise left out: the agent answers in `reply`.                                                                                              |

The statuses a turn is offered depend on the session's [end](https://www.twisp.com/docs/reference/actions/sessions.md#session-end). Without one, the outcome's schema allows `OPEN` and `COMPLETED`. With one, it allows only `OPEN` in every turn before the final turn, so the agent cannot end the session early, and only `COMPLETED` in the final turn, so the agent must end it.

An invalid outcome is returned to the model once with its errors. A turn that still cannot report a valid outcome fails with `agent_unfinished`, which is retried. A turn that exceeds one of its limits, including the transcript's 4,096 events, fails with `agent_limit_exceeded` without a retry. Neither completes the session, and the model cannot fail it. A turn the model declines to continue fails with `agent_refused`, without a retry. A turn that fails with `agent_refused`, or with `agent_unfinished` after its retries, is [held](https://www.twisp.com/docs/reference/actions/sessions.md#held-turns): the session waits for a person's answer instead of failing. Any other turn that fails for good, after its retries or with an error that is not retried, fails the session. A turn that pauses for [approval](https://www.twisp.com/docs/reference/actions/sessions.md#approvals) commits with the session `WAITING` and no output.

## Agent uses

Twisp derives an agent's [output uses](https://www.twisp.com/docs/reference/actions/skills.md#output-uses) itself, from the calls the turn made; the agent does not report them.

| Role        | Recorded for                                                                                                                                                                                                               |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TOOL`      | Each successful call, input read, and agent tool call whose result reached the model.                                                                                                                                      |
| `SKILL`     | Each skill load, a preload included. In a session, a skill loaded in an earlier turn whose content the conversation still holds is also a reused use, once per skill version, of its earliest load the conversation holds. |
| `RETRIEVED` | Each memory recall, memory search, and context retrieval whose result reached the model.                                                                                                                                   |

Failed calls are never uses. The calls a program or helper makes are not uses themselves: the `programmatic_tool_caller` or `subagent` call that made them is, and the call tree shows them beneath it.

## Callable agents

An agent with a `tool` block is callable, by other agents and by tools, like any tool: a caller declares it in `tools` and calls it by its `tool.name`. Its `tool` block has no `outputSchema`, because the agent's output is its own: the caller receives the agent's reply as a string, or its structured output when it declares `agent.outputSchema`.

- **One call, one run.** A call, whether from a caller's `tools` or from `invokeAction`, runs the agent's whole loop synchronously, in the caller's transaction, as its own execution with its own transcript, calls, and uses. The agent's limits bound its own run, and the run counts as one call against the caller's `tool_calls`.
- **Stateless.** A callable agent has no `session`. Each call is a fresh run over its input. With memory enabled it uses its own memory, never its caller's.
- **Authority.** The call is authorized by the [policy chain](https://www.twisp.com/docs/reference/actions/actions.md#policy-chain), extended by the callee's policies and then its tools', so a callable agent can never do more than its caller.
- **Failures.** A callee that ends with `agent_unfinished`, `agent_refused`, or `agent_limit_exceeded` returns a call error to its caller, whose model can decide what to do. A write conflict, a passed deadline, or an interrupted transaction fails the caller too.
- **Depth.** The callee runs within the caller's turn deadline. Agent calls nest at most 3 agents deep, counting the first; a deeper call fails with `action_call_limit_exceeded` before it runs, and is not recorded.
- **Starting.** A client can also [start](https://www.twisp.com/docs/reference/actions/tools.md#starting-a-tool) a callable agent with `startAction`, so its whole run does not hold the request open. It runs on its own, under the client's authority, with the agent's own execution settings.

A callable agent's tool references read `tool.input`, the call's input, checked against its `tool.inputSchema`. They can also read `execution.input`: for a call, the call's `action.invoked` event, whose `data` is the call's input; for a run started by a trigger, the triggering event. An agent that has both a `tool` block and triggers also runs without a call, so its references cannot read `tool.input`; read `execution.input` instead, which every run supplies. The definition write rejects a reference that reads `tool.input` in an agent with triggers.

## Transcripts

An invocation's transcript records every model request and response, with its purpose, reasoning, token usage, and the provider's exchange as sent and received, and every tool call's input and output or error. A request is recorded before it is sent, and a response before the agent acts on it; if the record cannot be written, the response is not used and the invocation is retried. Events commit on their own, so a turn that rolls back keeps the transcript of its attempt, and each invocation keeps its own.

Events are numbered from 1 without gaps within an invocation, and none can be appended after the invocation's outcome. `invocation.transcriptLastSequence` is the last event's number, recorded with the outcome, so events 1 through it are the complete transcript. An invocation records at most 4,096 events.

Read a transcript through `invocation.transcript(first, after)`, a connection in sequence order of at most 100 events per page; an earlier invocation's transcript is on that invocation in the execution's `history`. While an invocation runs, it lists the events recorded so far. The gRPC `ListActionExecutionEvents` operation reads the same events by execution and invocation number.

Transcript events and the calls they record link both ways. Each `tool.input` and `tool.output` event names its call in `callExecutionId`, and `callExecution` reads it; its `toolUseId` is the ID of the model's `toolUse` in the `model.response` that requested it. From the other side, a call the agent made, including one made by code under programmatic tool calling, lists the events that record it in `callerTranscriptEvents`: its `tool.input` and `tool.output`, or a pair for each load of a skill loaded more than once. The gRPC `ListActionExecutionEvents` reads them by `callExecutionId`. Reading them requires permission to read the agent's execution, not only the call; without it, `callerTranscriptEvents` is empty. `callExecution`, like an execution's other links, is subject to your permission to read the call. It is null for a call that was refused before it ran, rolled back with its attempt, or made by a preview turn or replay. A call refused before it was assigned an identifier, for invalid input or a pending approval, has no `callExecutionId`. Calls made by a tool are not in the transcript; reach them through `lineage` and `callPath`.

| Field                              | Type                      | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ---------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `sequence`                         | `Int!`                    | Position in the invocation's transcript, from 1.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `kind`                             | `String!`                 | `model.input`, each model request as the agent sent it; `model.request`, `model.response`, and `model.error`, the exchange with the model's provider (a request records `messages_as_input` or `system_as_input` instead of repeating messages or a system prompt its `model.input` already holds; a request records `edits` where it changed earlier messages, instead of recording them again; a response's content includes the model's reasoning, and `outcome: true` marks a response that reports the turn outcome); `usage`, tokens and latency per model call; `outcome.check`, each check of a reported outcome against the agent's schema; `state.value`, the turn's [computed state](https://www.twisp.com/docs/reference/actions/agents.md#computed-state) fields and any that failed; `skill.preload`, each [preloaded skill](https://www.twisp.com/docs/reference/actions/skills.md#preloaded-skills)'s load, before the first model call; `memory.pinned`, the [pinned memories](https://www.twisp.com/docs/reference/actions/memory.md#agent-memory) the turn's instructions listed, by ID and version; `tool.input` and `tool.output`; `context.storage`, `context.offload`, `context.summarize`, `context.drop`, `context.retrieve`, `context.clear`, and `context.strip` (the model's reasoning removed from earlier messages); and `harness.interrupt`, `harness.resume`, and `harness.log`. Transcripts recorded earlier also hold `model.output` and `reasoning`, copies of each response's message and reasoning. |
| `executionId`, `invocationNumber`  | `UUID!`, `Int!`           | The execution and invocation whose transcript the event is in.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `turn`                             | `Int!`                    | For an event of a session turn, the turn's number in its session; 0 otherwise.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `toolUseId`                        | `String`                  | For `tool.input` and `tool.output`: the model's tool-use ID for the call.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `callExecutionId`, `callExecution` | `UUID`, `ActionExecution` | For `tool.input` and `tool.output`: the call the event records.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `data`                             | `JSON!`                   | The event's content, whose shape depends on `kind`. Content too large for one event is split into consecutive events of the same kind, each `{"part": i, "parts": n, "chunk": "..."}`, whose chunks joined in order are the content as JSON text.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `created`                          | `Timestamp!`              | When the event was recorded.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |

```graphql
query Transcript($id: UUID!, $after: String) {
  actions {
    execution(id: $id) {
      invocation {
        number
        transcriptLastSequence
        transcript(first: 100, after: $after) {
          nodes {
            sequence
            kind
            data
          }
          pageInfo {
            endCursor
            hasNextPage
          }
        }
      }
    }
  }
}
```

## Operating agents

- **Approvals.** A tool reference can require a person's approval before each call; see [Approvals](https://www.twisp.com/docs/reference/actions/sessions.md#approvals).
- **Preview mode.** `config.mode: preview` runs the agent on real events and discards its writes; see [Preview calls](https://www.twisp.com/docs/reference/actions/executions.md#preview-calls).
- **Memory.** `harness.memory` gives the agent long-term memory across its sessions, which you can list and delete; see [Agent memory](https://www.twisp.com/docs/reference/actions/memory.md#agent-memory).
- **Replay.** `replayExecution` reruns a recorded turn with another version of the agent against current data, and rolls back its writes; see [Replays](https://www.twisp.com/docs/reference/actions/executions.md#replays).
- **Lineage.** `lineage` walks an agent execution's whole call tree; see [Lineage](https://www.twisp.com/docs/reference/actions/executions.md#lineage).
