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

# Events

> The append-only log: read, stream, filter by level, and resume

A session is an **append-only event log**. Read it, stream it, filter it by visibility (`level`), and resume from any `seq`. Switch on event **`type`**, not prose.

## Events in a turn

When a turn runs it appends events in order — here, the agent runs the tests, sees them fail, and replies:

```text theme={null}
turn.started
  tool.call         npm test
  exec.completed    exit 1
  agent.message     "3 tests fail"   (level: user)
turn.completed      needs_input
```

Everything between `turn.started` and `turn.completed` is that turn's work. **`turn.completed` is the done signal, so await it.** New neutral completions report `outcome`; existing built-in runtime events may report the equivalent `yield_reason`. The `body` each type carries is in the table below.

Each event is one envelope:

```json theme={null}
{ "id": "evt_…", "seq": 12, "ts": "…", "session": "ses_…",
  "actor": { "id": "agent", "type": "agent" },
  "type": "agent.message", "level": "user",
  "body": { "text": "3 tests fail in auth.spec.ts" } }
```

The two fields you act on are **`type`** (what happened — the discriminator) and **`level`** (who should see it). `seq` orders and resumes the log, `body` is the payload, and `actor` is who produced it (`{ id, display?, type: "human" | "agent" | "system" }`). The full field reference lives in the [API reference](/agent-sessions/api-reference); for messages, see how input fields map onto an event in [Messaging](/agent-sessions/messaging#message-input).

## Event types

Treat **`type`** as the stable discriminator. Unknown types should render from their `text` or `summary`, so new types do not break your UI. Match a prefix like `turn.*` or `agent.*` for coarse buckets.

| `type`                                         | `body` shape                                                     | Use it for                                                                            |
| ---------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `agent.message`                                | `{ text }`                                                       | agent output (the result is whichever one `turn.completed` points to)                 |
| `user.message`                                 | `{ text }`                                                       | a steer you sent                                                                      |
| `turn.started`                                 | `{ turn_id, input_from_seq, input_to_seq }`                      | a turn began, consuming input events in `[from,to]`                                   |
| `turn.completed`                               | `{ turn_id, outcome?, yield_reason?, result_event_id?, usage? }` | the done signal; normalize `outcome` and legacy `yield_reason` as the terminal reason |
| `tool.call`                                    | `{ tool, args_summary }`                                         | the agent invoked a tool                                                              |
| `exec.completed`                               | `{ command, exit_code, summary, content_ref?, bytes? }`          | a finished command                                                                    |
| `error.runtime` · `error.model` · `error.task` | `{ code, message, retriable? }`                                  | a failure                                                                             |

### GitHub PR events

When a session [watches](/agent-sessions/watches) a PR it opened, OpenComputer wakes it with a normalized event — level `user`, `source: "github"`, `body` a summary (author, text, url), never the raw webhook.

| `type`                       | Fires on                                                          |
| ---------------------------- | ----------------------------------------------------------------- |
| `github.pr.checks_completed` | GitHub check runs/suites for the head commit finished (coalesced) |
| `github.pr.review_submitted` | a review decision landed (approved / changes requested)           |
| `github.pr.comment`          | a comment landed on the PR — issue-style or review (diff) comment |
| `github.pr.merged`           | the PR merged                                                     |
| `github.pr.closed`           | the PR closed without merging                                     |

## Visibility levels

Every event carries a `level`; use it to choose how much detail each client receives.

| Level      | Contains                         | Use for                             |
| ---------- | -------------------------------- | ----------------------------------- |
| `user`     | Answers, results, and questions. | Chat, final results, notifications. |
| `progress` | Plain-language work updates.     | Activity feeds and status lines.    |
| `internal` | Tool calls and runtime detail.   | Debug consoles and audit trails.    |

Filtering is **cumulative** — `user` is the narrowest, `internal` is everything:

* `?level=user` → just `user`
* `?level=progress` → `progress` + `user`
* `?level=internal` → everything

Use `user` for customer-facing chat, `progress` for activity views, and `internal` for debugging. The model's private reasoning is never surfaced; webhooks default to `user`.

## Cursors and resume

* **`seq`** — a monotonic id assigned to each event.
* **`head`** — the highest `seq` in the session.
* **`input_cursor`** — how far the *runtime* has consumed (a session field). As a **reader** you ignore it and just pass `after=<seq>`.

Read or stream with `after=<seq>` to get everything since that point — this is how **resume** works: reconnect with the last `seq` you saw and you miss nothing and get no duplicates. In the browser, native `EventSource` does this for you — each event's `seq` is the SSE `id`, so it resumes via `Last-Event-ID` on reconnect (see the [quickstart](/agent-sessions/quickstart)).

## Details

<AccordionGroup>
  <Accordion title="Correlate a steer to its turn">
    [`POST /messages`](/agent-sessions/messaging) returns the input event `seq`, not a turn id. Built-in runtimes may consume several queued inputs in one turn; Flue allocates one ordered turn for each accepted input. In both cases, `turn.started` reports the consumed `input_from_seq` and `input_to_seq`. Await its matching `turn.completed`, then use `GET …/events?turn_id=` for the turn's output.
  </Accordion>

  <Accordion title="Large output and artifacts">
    A large `body` is offloaded to a `content_ref`; fetch the bytes with `GET /sessions/:id/events/:eventId/content` (e.g. full `exec.completed` output). For named files, diffs, screenshots, reports, and previews, see [Artifacts & previews](/agent-sessions/artifacts) in Labs.
  </Accordion>

  <Accordion title="id, turn_id, and idempotency keys">
    **`id`** is the event's own unique id (`evt_…`). **`turn_id`** is the turn that produced the event, when applicable — filter a turn's output with `?turn_id=`. The client idempotency key for steering is separate from both (deduped per session — see [steering](/agent-sessions/messaging)).
  </Accordion>
</AccordionGroup>
