> For the complete documentation index, see [llms.txt](https://stair-ai.gitbook.io/stair-ai-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://stair-ai.gitbook.io/stair-ai-docs/sdk/concepts.md).

# Record, Session, Trace

Three concepts, distinct scopes:

| Concept         | Definition                                                                                           |
| --------------- | ---------------------------------------------------------------------------------------------------- |
| **TraceRecord** | Atomic unit of submission. One behavior step, independently verifiable.                              |
| **Session**     | Group key. Records sharing a `session_id` belong to one decision cycle. Not an entity, no lifecycle. |
| **Trace**       | Agent's long-term reasoning ledger. Append-only history of records under one `agent_id`.             |

```
Agent X's Trace
├─ session "cycle-001"
│  ├─ Observing
│  ├─ ToolCalling × N
│  ├─ Thinking
│  └─ Acting           (terminal record of the cycle)
│
├─ session "cycle-002"
│  ├─ Observing
│  └─ Thinking         (no Acting — incomplete; not scored)
│
└─ session "cycle-001" (post-outcome)
   └─ Reflecting       (attached via session_id reuse)
```

There is no session lifecycle — no `open`, `finalize`, or `incomplete` states. A session is whatever set of records share a `session_id`.

Multi-pass reasoning is native (multiple `Thinking` per session is fine). The terminal `Acting` record marks the cycle complete for scoring purposes; sessions without one are stored but not scored.

## Identifiers

| ID           | Format  | Assigned by               | Scope                                                                  |
| ------------ | ------- | ------------------------- | ---------------------------------------------------------------------- |
| `agent_id`   | UUID v4 | Server (at registration)  | Globally unique; the canonical attribution key on every record         |
| `agent.name` | string  | Partner                   | Unique within owner; metadata; mutable; **never appears on records**   |
| `record_id`  | UUID v4 | SDK (before network call) | Globally unique; doubles as idempotency key on `(agent_id, record_id)` |
| `session_id` | string  | Agent                     | Unique within `agent_id`; partner-chosen                               |

`agent_id` is what the SDK and records use at runtime. The agent's human-readable `name` is for display, search, and resolution only — renaming an agent does not change which records belong to it.

## Trace DAG

Records reference each other through two fields with different semantics:

| Field                | Cardinality | Use for                                                                    |
| -------------------- | ----------- | -------------------------------------------------------------------------- |
| `upstream_record_id` | List        | Plain DAG dependency: "this record builds on those"                        |
| `parent_record_id`   | Single      | Sub-thread containment: "this record lives inside that record's execution" |

The two are independent — a record may set neither, either, or both.

### `upstream_record_id` — DAG edges

A `Thinking` step that consumed both an `Observing` trigger and a prior `Reflecting` lists both:

```mermaid
graph LR
  O[Observing<br/>obs-001] --> T[Thinking<br/>thk-009]
  R[Reflecting<br/>rfl-014] --> T
  T --> A[Acting<br/>act-002]
```

```typescript
await session.submit({
  behavior: "Thinking",
  upstream_record_id: ["obs-001-uuid", "rfl-014-uuid"],
  prompt: "...",
  inputs: [],
  output_payload: "...",
});
```

### `parent_record_id` — sub-thread containment

A `Thinking` step that opens a private sub-thread of `ToolCalling` and `Thinking` records uses `parent_record_id` to scope them. Records outside the sub-thread do not see the internal records as upstream — the sub-thread is a closed scope whose external surface is the parent's eventual output.

```mermaid
graph TD
  P[Thinking<br/>parent-001] -.parent.-> SC[ToolCalling<br/>sub-002]
  P -.parent.-> ST[Thinking<br/>sub-003]
  ST --> SC
  P --> A[Acting<br/>act-004]
```

The downstream `Acting` lists `parent-001` as upstream, not `sub-002` or `sub-003`.

## Idempotency

Every record's `record_id` is a UUID v4 generated by the SDK before the network call. On retry, the SDK reuses the same `record_id`; the server matches `(agent_id, record_id)` and returns the original ack with `is_duplicate: true`. There is no "client" vs "server" record ID distinction.

Submitting a different body under a previously-used `record_id` raises `IdempotencyConflictError` rather than overwriting.

## Post-hoc Reflecting

Reflection runs after the outcome is known. It attaches to the original decision cycle by reusing the same `session_id`:

```mermaid
sequenceDiagram
  participant Agent
  participant Server
  Note over Agent,Server: T0 — decision cycle
  Agent->>Server: Observing (session=cycle-001)
  Agent->>Server: Thinking (session=cycle-001)
  Agent->>Server: Acting (session=cycle-001, terminal)
  Note over Agent,Server: T1 — outcome resolves
  Agent->>Server: Reflecting (session=cycle-001)
```

Reflecting can also start a new session (e.g., a daily review reflecting over many prior sessions) with `upstream_record_id` pointing at the records under review.

## Cross-cutting fields on every record

`BaseRecord` carries fields that apply regardless of behavior:

| Field              | Purpose                                                                                                                                               |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model_invocation` | The LLM call that produced this record (provider, model\_name, tokens, cost). LLM is cross-cutting — set on `BaseRecord`, not as a separate behavior. |
| `tags`             | Faceted labels (max 32 entries, 64 chars each).                                                                                                       |
| `notes`            | Free-text annotation (max 2 KB).                                                                                                                      |
| `client_ts_utc`    | Client-reported epoch ms. SDK auto-fills if omitted.                                                                                                  |
| `schema_version`   | Bundled constant. SDK auto-fills.                                                                                                                     |

**LLM invocations are not `ToolCalling`.** When a record is *produced via* an LLM, set `model_invocation` on the record itself. Reserve `ToolCalling` for calls to external APIs, KBs, on-chain reads, local functions, and sub-agents.
