> ## Documentation Index
> Fetch the complete documentation index at: https://checkfu.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Project a session into named views

> Fold the one event stream into messages, tool calls, ActionApprovals, the subagent tree, and the context ledger.

A Session is an append-only event log, and everything you show a user — the message timeline, tool activity, pending ActionApprovals, the subagent tree, what is in context — is a *projection* of that one stream. You do not need a second transcript for each surface. You fold the same ordered events into the shape each view needs.

This guide names the five projections a Session renders and shows exactly which events each one derives from. The [TypeScript SDK](/reference/typescript-sdk) implements them as pure, replay-idempotent folds, so a live consumer keeps every view current without re-reading the log after every event.

## One stream, five views

<CardGroup cols={3}>
  <Card title="Order by seq" icon="list-ol">
    `seq` is the per-Session sequence. Feed events to a projection in `seq` order; an event at or below the high-water mark is a no-op.
  </Card>

  <Card title="Project, don't copy" icon="code-branch">
    A projection derives a view from the canonical event. It never replaces the log, never moves the cursor, and never reaches the network.
  </Card>

  <Card title="Settle on events" icon="flag-checkered">
    Read settlement from status events and their `stop_reason`, never from stream quiescence. See [Resume a stream](/guides/resume-a-stream).
  </Card>
</CardGroup>

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Checkfu, createSessionProjections } from "@checkfu/sdk"

const checkfu = new Checkfu({
  apiKey: process.env.CHECKFU_API_KEY,
})

const projections = createSessionProjections()

for await (const event of checkfu.sessions.events.stream(sessionId)) {
  projections.observe(event)
  render(projections)
}
```

`CHECKFU_API_KEY` and `CHECKFU_WORKSPACE_ID` come from [Get access](/reference/access#the-three-variables-ready); `sessionId` is the `sess_…` you are following. `createSessionProjections()` bundles all five views behind one `observe`, so a single pass over the stream keeps every view current. Each view is also available on its own — `new MessageProjector()`, `new ContextLedgerProjector()` — when a consumer wants only one.

## Messages

The readable conversation: user, system, and agent turns in log order. It folds the events whose `payload.content` is the point of the turn, and represents a screen-withheld agent message as a structural marker rather than the judged text.

| Event            | Becomes                                                             |
| ---------------- | ------------------------------------------------------------------- |
| `user.message`   | a user turn                                                         |
| `system.message` | a privileged mid-conversation system turn                           |
| `agent.thinking` | an agent reasoning turn                                             |
| `agent.message`  | an agent reply; a DeliveryScreen block arrives as a withheld marker |

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
for (const message of projections.messages.messages()) {
  // message.role: "user" | "system" | "agent"
  // message.kind: "message" | "thinking"
  // message.text: string | null   (null for multimodal blocks or a withheld message)
  // message.withheld: { reason, contentDigest } | null
}
```

Multimodal content (image and document blocks) carries no string in the projection: `text` is `null` and `multimodal` is `true`, so a renderer reads the canonical event by `seq` for the block bodies. A withheld `agent.message` has `withheld` set and `text` null — its `message.screened` verdict is a separate event you can correlate by content digest.

## Tool calls

The conversational tool-activity timeline: each `agent.tool_use` paired with its settling `agent.tool_result`.

| Event               | Becomes                                                   |
| ------------------- | --------------------------------------------------------- |
| `agent.tool_use`    | a pending call (tool identity, args, attributed subagent) |
| `agent.tool_result` | the same call, settled with a `result` or `error`         |

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
for (const call of projections.toolCalls.calls()) {
  // call.status: "pending" | "result" | "error"
}
projections.toolCalls.pending() // calls still owed a result
```

<Note>
  This is the read-side timeline, not the ToolServing loop. The SDK's
  `ToolServingProjector` (used by `tools.serve`) is the serving state machine
  that decides when to invoke your handler. Platform-witnessed `exec.tool_invocation`
  settlement is a separate governance lane — it correlates by `tool_invocation_id`, not
  `tool_use_id` — so it is documented in the [events reference](/reference/events)
  rather than folded here. See [Serve tools from your app](/guides/serve-tools-from-your-app).
</Note>

## ActionApprovals and questions

Both park a Run on a human, so they share a view. A [governed ActionApproval](/concepts/action-approvals) is a durable permission decision; a [Question](/reference/events#answer-an-agent-question) is ordinary input that grants no authority. They never collapse: an ActionApproval is answered through the ActionApprovals API, a Question through `user.question_answer`.

| Event                      | Becomes                                                       |
| -------------------------- | ------------------------------------------------------------- |
| `action_approval.pending`  | a pending ActionApproval carrying its frozen decision context |
| `action_approval.resolved` | that ActionApproval, resolved `approve` or `deny`             |
| `agent.question`           | a pending Question with its requested items                   |
| `user.question_answer`     | that Question, answered                                       |

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
projections.interactions.pendingActionApprovals()  // still awaiting a reviewer
projections.interactions.pendingQuestions()  // still awaiting an answer
```

Key the wait loop off `run.requires_action` to learn *what* a Run is parked on (see [Answering a parked run](/reference/events#answering-a-parked-run)); this projection is the read-side summary of those waits.

## Multiagent state

The multi-agent view keeps two distinct mechanisms. A projection presents them as separate lists so observed harness-internal activity is never conflated with governed coordinator threads.

| Event                               | Becomes                                              |
| ----------------------------------- | ---------------------------------------------------- |
| `agent.subagent_started`            | an observed harness-internal subagent (running)      |
| `agent.subagent_completed`          | that subagent, settled                               |
| `session.thread_created`            | a governed coordinator-roster Agent thread (running) |
| `session.thread_status_running`     | that thread is actively running                      |
| `session.thread_status_idle`        | that thread reached an end turn or requires action   |
| `session.thread_status_rescheduled` | that thread is waiting for a bounded retry           |
| `session.thread_status_terminated`  | that thread is archived or otherwise terminal        |

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
projections.multiagent.observedSubagents() // nested work inside one harness
projections.multiagent.sessionThreads()    // governed Session threads
```

<Warning>
  Observed subagents are a harness accounting for its own nested work; they are
  never governed platform resources. Session threads are governed coordinator-roster Agent
  lifecycles inside one Session and expose only stable `sthr_*` identities.
  Each thread has its own filtered event view while the Session event log stays
  the durable source of truth. See [Multiagent threads](/concepts/multiagent-threads).
</Warning>

## Context ledger

The inspectable accounting of what is in context: token usage, model routing, compaction and condensation boundaries, and definition-version changes. It is the read-side fold of the same events the `GET /v1/sessions/{id}/ledger` endpoint summarizes, so a live consumer keeps a current ledger without re-reading the endpoint after every event.

| Event                       | Becomes                                               |
| --------------------------- | ----------------------------------------------------- |
| `model.usage_recorded`      | one provider-observed usage entry                     |
| `model.routed`              | the binding, model, and tier a Run routed through     |
| `session.compacted`         | a compaction boundary (strategy, tokens before/after) |
| `session.context_condensed` | a context-window condensation boundary                |
| `session.updated`           | a definition-version or loaded-capability change      |

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
projections.ledger.usageTotals()              // token totals across the Session
projections.ledger.compactions()              // compaction + condensation history
projections.ledger.routes()                   // admitted model routes
projections.ledger.definitionVersionChanges() // version moves at turn boundaries
projections.ledger.loadedCapabilities()       // latest loaded-capability set
```

## Feeding projections durably

A live tail is enough for a transient view. When the projection must survive a process restart — a Slack thread, a ticket, a dashboard backed by its own store — feed it from [`createSessionObserver`](/reference/typescript-sdk), which owns lossless cursor resume, live-event custody, and terminal cleanup:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createSessionObserver } from "@checkfu/sdk"

const observer = createSessionObserver({
  client: checkfu,
  registry,                // your durable store: list() / put() / remove()
  bindingId: (binding) => binding.id,
  sessionId: (binding) => binding.sessionId,
  loadCursor: (binding) => receipts.cursorFor(binding.id),
  reconcile: async (binding, eventDelta, throughSequence) => {
    // One provider transaction applies this retryable delta to its cumulative
    // state and commits exactly the cursor that state now covers.
    return receipts.projectAndCommit(binding.id, eventDelta, throughSequence)
  },
})

await observer.resume() // re-attach every stored binding after a restart
```

Events stay in the observer until the cursor you return covers them, so a crash mid-projection replays rather than skips. `eventDelta` is only the recognized, unsettled delta and can be redelivered; it is not the complete Session prefix. Your provider receipt transaction must update cumulative provider state and its cursor together. Unknown future events do not appear in the typed delta but still contribute to `throughSequence`, so read the canonical log when their exact envelopes matter. The SDK projectors have no durable snapshot format: use them for a process-lifetime view, or replay the authoritative log under an explicit retention/work policy before publishing a complete view. The full observer contract — coalescing, backoff, and terminal cleanup — is in the [SDK reference](/reference/typescript-sdk).

## Next steps

<CardGroup cols={2}>
  <Card title="Events reference" icon="list-timeline" href="/reference/events">
    The envelope, the full event catalog, and the drive events these projections fold.
  </Card>

  <Card title="Resume a stream reliably" icon="arrows-rotate" href="/guides/resume-a-stream">
    The cursor and replay rules every projection inherits.
  </Card>
</CardGroup>
