Skip to main content

Generations

Generation records track individual LLM generation runs started by agents, including their lifecycle status and any failure details.

Overview

Every agent generation (POST /agents/:id/generate, session generation, sub-agent calls) creates a generation record before the model is called. The record tracks the run through its lifecycle and — when the run fails — stores a structured error payload so failed generations are distinguishable from pending ones and can be debugged post-mortem.

Generations can be listed via GET /generations (filter by agent_id, trace_id, or status), and each record can be retrieved via GET /generations/:generation_id.

See the Permissions Reference for the IAM action strings for this module.

Data Model

FieldTypeDescription
idstringPublic identifier for the generation
project_idstringProject the generation belongs to
agent_idstringAgent that ran the generation
trace_idstringTrace this generation belongs to
initiator_generation_idstring | nullGeneration that triggered this one. Set only for sub-agent invocations; null for top-level generations
chain_idstring | nullContinuation chain this generation belongs to — set on every member including the root; null when it is not part of one
session_idstring | nullSession this generation was dispatched through; null when it was started outside one
actor_idstring | nullEnd-user actor the generation is attributed to, derived from the session; null when there is none
started_by_principal_typestring | nullPrincipal kind that started the generation — user or api_key (see Starting principal)
started_by_principal_idstring | nullPublic id of that principal — the key's own key_… when a key was used, else user_…
statusstringLifecycle status: in_progress, requires_action, completed, or failed
started_atstringWhen the generation started
completed_atstring | nullWhen the generation reached a terminal state
last_activity_atstring | nullLast activity timestamp
stop_reasonstring | nullWhy the generation stopped — see Stop Reason
errorobject | nullStructured error payload recorded when the generation failed (see Error Recording)
metadataobject | nullCaller-owned key/value annotations, returned verbatim (see Metadata)
action_idstring | nullLogical action label supplied on the generate request
trigger_idstring | nullTrigger that initiated the generation
orchestration_run_idstring | nullOrchestration run that dispatched the generation
node_idstring | nullNode within that run
node_attemptnumber | nullThe node's 1-based retry attempt, so a retried node's generations are told apart (see Finding an orchestration run's generations)
agent_versionnumber | nullAgent config version that served the generation
sourcestring | nulleval when an eval run produced this generation; null for ordinary traffic
routingobject | nullWhat the model route did for this generation
extractionobject | nullMemory-extraction summary for this turn (see extraction)
content_redacted_atstring | nullWhen the generation's content was purged; null while content is intact
content_redacted_by_principal_typestring | nullPrincipal kind that purged the content (user or api_key)
content_redacted_by_principal_idstring | nullPublic ID of that principal — the key's own id for API-key auth
created_atstringISO 8601 creation timestamp
updated_atstringISO 8601 last-update timestamp

Key Concepts

Starting principal

Every generation records who started it in started_by_principal_type / started_by_principal_id. When the request was authenticated with an API key the principal is the key itself (key_…), so a generation names which key acted rather than only the user that owns it; a JWT-authenticated request records the user (user_…).

The pair is durable identity, not a log line: work that resumes after the original request is gone re-mints a short-lived credential from it. That is what lets an approval continuation — possibly days later — authenticate its builtin tools as the principal that started the chain, and it is why a generation started by a request-less drive (a workflow dispatch, an orchestration node) records the drive's principal rather than nothing.

Both fields are null when the chain has no re-mintable principal — a generation started by a trigger or an OAuth token. Each of those carries its authority in the token (the trigger's attached policy, the consented scope) rather than in the principal, so recording one would let a later re-mint drop that boundary and act with the whole of the owning user's access.

Lifecycle

A generation starts as in_progress. It transitions to:

  • requires_action when a client tool call pauses the run and the caller must submit tool outputs.
  • completed when the model finishes (the stop_reason carries the finish reason).
  • failed when the run errors — for example when the upstream AI provider returns an error or is unreachable. stop_reason is set to error and the error field carries the failure details.

Error Recording

When a generation fails, the failure is persisted on both the generation record and its trace: status becomes failed, stop_reason is error, and error carries { code, message }.

The error object always contains message. code is set for mapped errors — most notably AI_PROVIDER_ERROR, which is used when the upstream AI provider returns an error (e.g. exhausted credits, rate limit) or is unreachable.

Provider Error Surfacing (AI_PROVIDER_ERROR)

Generation endpoints return HTTP 502 with the AI_PROVIDER_ERROR code when the upstream AI provider fails:

{
"error": {
"code": "AI_PROVIDER_ERROR",
"message": "Provider returned 402: insufficient credits",
"meta": {
"provider_status_code": 402,
"generation_id": "gen_abc123",
"trace_id": "trace_xyz789"
}
}
}

The meta field includes the generation_id and trace_id of the failed run so the failure can be inspected post-mortem via GET /generations/:generation_id and GET /traces/:trace_id.

Metadata

The metadata field is a caller-owned bag: it holds only what the caller put there, and it is returned verbatim. It is a place to attach per-run audit attribution — for example, which knowledge-corpus version produced an AI action.

Callers can write metadata two ways:

Both paths require the generations:UpdateGeneration action for PATCH and agents:CreateAgentGeneration for the create path.

No key is reserved. Every piece of state the server owns (action_id, trigger_id, orchestration_run_id, node_id, agent_version, routing, extraction) is a field of its own on the generation, so nothing written into metadata can reach it. A caller key that happens to be spelled action_id is just an annotation; it does not affect the action_id field.

Internal recovery state (used to resume a requires_action generation after a server restart) is stored in its own column and is never exposed through the API under any name.

extraction — memory-extraction summary

When an agent is configured with knowledge_config.extraction and write_memory_id, a completed generation writes an extraction summary — { "candidates": 3, "created": 2, "updated": 1, "skipped": 0 } — describing what the auto-extraction pass did with the turn. See Memories — Automatic Extraction for how it is configured.

Recorded input

A generation also stores the messages it was asked to answer, resolved (file and document references already inlined) but without the agent's own instructions or knowledge injections — those are config, recoverable from agent_version.

The record is not part of the generation response; it is served by the transcript and it exists so a real turn can be promoted into an evaluation fixture with create-dataset-item-from-generation. It is content, not skeleton, so it follows the same rules as everything below: never written under zero-retention, cleared by a purge, and swept by retention. A generation whose input is gone can no longer be curated, and says so with 409 GENERATION_CONTENT_UNAVAILABLE.

Transcript

GET /generations/{generation_id}/transcript reads one turn back step by step: what it was asked, each model step with its tool calls and results, and how it ended.

soat get-generation-transcript --generation_id gen_abc

The transcript is assembled at read time from the generation record and the trace's steps object. There is no transcript table and no extra write on the generation path, so it always reflects the current records and can never outlive the content it projects.

Requires traces:GetTrace in addition to generations:GetGeneration: the response merges content from both resources, so a single generations action would silently widen to cover trace content.

Each entry in steps carries index, text, finish_reason, tool_calls, tool_results and usage. args on a call and result on a result are tool-owned payloads, returned as values — their keys are passed through exactly as recorded and are never inspected or rewritten.

The stored steps are projected, never forwarded: their on-disk shape belongs to the ai package and changes with it, so putting it on the wire would freeze an internal detail of a dependency as a public contract.

Two states return 200 with a skeleton rather than an error, so a caller never has to distinguish "no content" from "no such generation":

Statestatusinput / outputstepscontent_redacted_at
Still runningin_progressnull[]null
Never stored (zero-retention)terminalnull[]set, principal zero_retention
Erased by a purge or sweepterminalnull[]set, purging principal

step_count survives all three, because it is a counter rather than content. It counts this turn's steps: when a trace_id groups several generations, the trace's own step_count covers every one of them, while each transcript reports and projects only its own slice — see Traces → Grouping Generations Under One Trace.

A purged generation returns the skeleton even though the trace's steps object may still exist — see the warning under Content Purge. The redaction marker governs the whole transcript, so an erased turn is never reconstituted from an adjacent record.

Content Purge

DELETE /generations/{generation_id}/content clears the generation's content — metadata, error, extraction, the recorded input messages, and the internal recovery state of a paused run — and stamps content_redacted_at. It requires the generations:PurgeGenerationContent action.

The usage and audit skeleton is preserved on purpose: ids, timestamps, status, stop reason, and every attribution field (action_id, trigger_id, orchestration_run_id, node_id, node_attempt, agent_version, routing). A billing ledger has to outlive a tenant's erasure of the content, so a purged generation reads back as that skeleton rather than as a 404.

The operation is idempotent: a second purge succeeds and leaves the original content_redacted_at untouched.

warning

A generation purge does not delete the parent trace's steps object, which holds this generation's content alongside its siblings'. To erase a run's content completely, purge the trace — DELETE /traces/{trace_id}/content deletes the steps bytes from storage and cascades the content purge to every generation in the tree. See Traces.

Automatic content lifecycle

Two project settings turn the manual purge into a policy:

  • Retentiontrace_content_retention_days on the project runs a daily sweep that purges content past the window, through this same purge path.
  • Zero-retentiontrace_content_mode: "none" on the project or the agent means the content columns above are never written at all. The generation is still created and still metered; it simply reads back as a skeleton stamped content_redacted_by_principal_id: "zero_retention" from the moment it exists.

Sub-agent invocations

initiator_generation_id is populated only when an agent calls another agent via a builtin tool: the child generation records the calling generation's ID, while top-level generations leave it null. This is the sole case in which the field is set.

Multi-step reasoning is composed by the calling application, so intermediate steps appear as ordinary generations of their own rather than as metadata on, or child generations of, the calling generation.

Finding an orchestration run's generations

An orchestration run's node_executions record what each node received and produced, but they carry no generation id. The pointer runs the other way: a generation dispatched by an agent node stores orchestration_run_id, node_id and node_attempt as attribution columns of its own, next to action_id and trigger_id.

So a run is traced to what its agents actually did by filtering this module's list endpoint:

# every generation the run produced
soat list-generations --orchestration-run-id run_abc123

# just one node's — one row per attempt if the node was retried
soat list-generations --orchestration-run-id run_abc123 --node-id summarize

node_attempt is what distinguishes the generations of a retried node. A node with a retry policy produces one node execution record per attempt and one generation per attempt; matching them on node_attempt is exact, where matching on timestamps is a guess.

From a generation reached this way, the rest of the graph is already reachable: trace_id opens the trace for that turn, initiator_generation_id walks down into any sub-agent invocations it made, chain_id opens the continuation chain it belongs to — filtering generations by that id returns every member of the chain — and session_id / actor_id name the session and end user it ran for, the same pair its usage event is attributed to.

session_id and actor_id also filter the listing, so the turns behind a conversation's or an end user's cost are one call away from the figure:

soat list-generations --session-id sess_abc123
soat list-generations --actor-id actor_abc123

An id naming nothing in scope yields an empty page, never an unfiltered one.

Tool context

The generation-creation endpoints (POST /agents/{agent_id}/generate, and the session and conversation generate endpoints) accept an optional tool_context object. Its entries are forwarded as X-Soat-Context-* request headers on every http, mcp and builtin tool call the generation makes, and an invalid key is rejected with 400 INVALID_TOOL_CONTEXT_KEY before the provider is called. It is not persisted on the Generation record. See the Tool Context reference.

Examples

List generations

Filter by agent_id, trace_id, initiator_generation_id, chain_id, orchestration_run_id, node_id, or status.

soat list-generations --trace-id trace_abc123 --status failed

Get a generation

soat get-generation --generation-id gen_abc123

Attach audit metadata

Merge caller-supplied metadata onto a generation for per-run audit attribution.

soat update-generation --generation-id gen_abc123 \
--metadata '{"team":"payments","ticket_id":"OPS-4821"}'