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 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 it enables, and the tools Twisp offers every agent. What it may do with them is bounded by its own policies; see Authorization.
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, 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.
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, 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. |
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, 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. |
model.provider | required | bedrock, anthropic, or fixture. See 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. |
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. 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. 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. |
harness.builtin_tools | ["programmatic_tool_caller"] | 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. 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, 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, 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.
{
"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
textandtoolUseblocks. 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 a session turn. whenis CEL overpurpose,session(id,input,step,started_at,final), andnow.session.stepis the number of turns before this one that committed or were held, so it is0in the first turn, andsession.finalis true in the session's final turn, whose outcome must complete it. An entry withoutwhenalways matches.input_exprcomputes input fields with CEL over the same values andresults, 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 theirtool.name, with the input fields that the reference fixes removed from their schemas.graphqlandcelare offered only when declared. Declaringgraphqlgives 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 theskillstool when it needs it, and each load is an execution. A preloaded skill 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. It returns one input of the turn by its positionn, 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, andsubagent, which hands a subtask to a helper that uses the agent's model and tools, or those of the preset the model picks. The features the configuration enables add their own tools, such assearch_memorywith memory,retrieve_offloaded_contentandretrieve_contextto bring back tool results and content set aside from the context window, andtodo_writewithtodos. Withharness.context.managerset toagentic, the model manages its own context withsummarize_context,truncate_context, andpin_context. search_history, on every turn of a session agent whoseharness.historyindexes its history, unlesssearchisfalse. See 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.
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.
{
"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, is not offered programmatic_tool_caller.
A call that fails with a call error, 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, the call's input.
A session agent runs its session's 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, 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 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:
<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 + ')'".
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 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:
{
"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, 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. 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: 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 commits with the session WAITING and no output.
Agent uses
Twisp derives an agent's 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
toolsor frominvokeAction, 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'stool_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, 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, oragent_limit_exceededreturns 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_exceededbefore it runs, and is not recorded. - Starting. A client can also start 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 fields and any that failed; skill.preload, each preloaded skill's load, before the first model call; memory.pinned, the pinned memories 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. |
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.
- Preview mode.
config.mode: previewruns the agent on real events and discards its writes; see Preview calls. - Memory.
harness.memorygives the agent long-term memory across its sessions, which you can list and delete; see Agent memory. - Replay.
replayExecutionreruns a recorded turn with another version of the agent against current data, and rolls back its writes; see Replays. - Lineage.
lineagewalks an agent execution's whole call tree; see Lineage.