# `Nous.Session.Log`
[🔗](https://github.com/nyo16/nous/blob/v0.17.1/lib/nous/session/log.ex#L1)

An append-only event log with a model-visible surface derived by folding.

The log is the source of truth. `derive_messages/1` projects it into the
`[%Nous.Message{}]` list every existing caller already reads, so the log can be
internal while `result.messages` stays byte-identical (plan constraint D2).

## Replace, don't delete

    {:ok, log} = Log.append(log, :assistant_message, %{content: "…"})
    {:ok, log} = Log.append(log, :system_message, %{
      content: "[summary of 1-40]",
      surface_op: {:replace, 1, 40}
    })

After that append, `derive_messages/1` shows the summary in place of events 1
through 40, while `events/1` still returns all of them. Compaction stops
destroying history — which is the whole point, and what makes fork, rewind and
audit possible later.

## Cost

Folding on every read would make the O(n) append this replaced look cheap. The
fold is memoized against `replace_generation` — a counter bumped only by a
replace — plus the event count, so an ordinary append extends the cached list
instead of rebuilding it, and a replace invalidates it exactly once.

## What is *not* in the log

The system prompt **rewrite** performed per request by
`Nous.AgentRunner.PromptAssembly` is assembly-time state, not history: it is
derived from agent config, plugins and skills at request time, it is replaced
wholesale on every request, and logging each rewrite would append N copies of
the same text to a durable log. System messages that are genuinely part of the
transcript — the initial one, and the summary
`Nous.Plugins.Summarization` appends — are `:system_message` events like any
other. See `Nous.Session.Event`.

# `t`

```elixir
@type t() :: %Nous.Session.Log{
  cache: {non_neg_integer(), non_neg_integer(), [Nous.Message.t()]} | nil,
  events: [Nous.Session.Event.t()],
  next_seq: non_neg_integer(),
  replace_generation: non_neg_integer()
}
```

`events` is kept newest-first so an append is O(1); `events/1` reverses.

`cache` holds `{replace_generation, event_count, messages}` — see the moduledoc
on cost.

# `append`

```elixir
@spec append(t(), Nous.Session.Event.type(), map(), DateTime.t() | nil) ::
  {:ok, t()} | {:error, term()}
```

Append an event.

`seq` is assigned here and is always contiguous — it equals the event's index,
which is what lets `{:replace, start, stop}` name a range without storing
pointers.

Returns `{:error, reason}` on an invalid event; a bad event must never take down
a live run.

`time` defaults to now. Pass it explicitly when the event records something that
already happened, which is required in two places: appending an existing
`%Nous.Message{}` must reproduce that message's own `created_at` (the fold
stamps from the event time, so re-stamping would make `add_message/2` lossy),
and rebuilding a log from persisted events must keep their original times rather
than the moment of the restore.

## Examples

    iex> log = Nous.Session.Log.new()
    iex> {:ok, log} = Nous.Session.Log.append(log, :user_message, %{content: "hi"})
    iex> {:ok, log} = Nous.Session.Log.append(log, :assistant_message, %{content: "hello"})
    iex> Enum.map(Nous.Session.Log.events(log), & &1.seq)
    [0, 1]

# `append!`

```elixir
@spec append!(t(), Nous.Session.Event.type(), map(), DateTime.t() | nil) :: t()
```

Append an event, logging and returning the log unchanged on failure.

For call sites inside the agent loop, where losing one bookkeeping event is
strictly better than failing the user's run. The failure is never silent.

# `count`

```elixir
@spec count(t()) :: non_neg_integer()
```

The number of events, shadowed ones included.

# `derive_indexed`

```elixir
@spec derive_indexed(t()) :: [{non_neg_integer(), Nous.Message.t()}]
```

Fold the surface into `{seq, message}` pairs, in the order the model sees them.

This is the primitive; `derive_messages/1` drops the seqs. Compaction needs the
pairs: to replace "these messages" it has to name a seq range, and the mapping
from message position to originating event is exactly what the projection rules
decide. Re-deriving that mapping outside this module means re-implementing the
drop rule below, which is how the two would silently diverge.

Projection rules:

  * `:system_message`, `:user_message`, `:assistant_message` → a message of that
    role, reproduced field for field
  * `:tool_result` → a `:tool` message carrying `tool_call_id` and `name`
  * everything else → nothing

Nothing else is filtered. An assistant event with neither content nor tool calls
still projects: it is what happened, and dropping it made
`Nous.Agent.Context.last_message/1` and output extraction disagree with the log.

Memoized; see the moduledoc on cost.

# `derive_messages`

```elixir
@spec derive_messages(t()) :: [Nous.Message.t()]
```

The message list every caller sees: `derive_indexed/1` without the seqs.

# `events`

```elixir
@spec events(t()) :: [Nous.Session.Event.t()]
```

Every event ever appended, oldest first, including shadowed ones.

# `materialize`

```elixir
@spec materialize(t()) :: {[Nous.Message.t()], t()}
```

`derive_messages/1`, returning the log with the fold memoized.

Callers that hold the log (`Nous.Agent.Context`) should use this so a later read
is free. The cache holds the indexed pairs, so a later `derive_indexed/1` is
free too.

# `new`

```elixir
@spec new() :: t()
```

An empty log.

# `seed`

```elixir
@spec seed([Nous.Message.t()]) :: t()
```

Seed a log from a flat message list, one surface event per message, in order.

This is the v1→v2 persistence bridge and what `Nous.run(agent, messages: [...])`
uses. Messages with an unrecognised role are dropped with a warning rather than
guessed at.

Each event takes its message's own `created_at` as its time, so seeding is
lossless: `seed/1 |> derive_messages/1` returns the input list, timestamps
included. Re-stamping here would make `Nous.Agent.Context.new(messages: …)`
quietly rewrite the history it was handed.

# `since`

```elixir
@spec since(t(), non_neg_integer()) :: [Nous.Session.Event.t()]
```

Events from `seq` onward, oldest first. Costs O(number returned), not O(log).

`events/1` reverses the whole list, which is fine for a one-off read and wrong
for anything on the append path — publishing newly committed events runs on
every append, and so does a consumer catching up from a known seq. Because
events are stored newest-first, taking the tail walks only what is new.

`seq` at or beyond the end returns `[]`.

## Examples

    iex> log = Nous.Session.Log.new()
    iex> {:ok, log} = Nous.Session.Log.append(log, :user_message, %{content: "a"})
    iex> {:ok, log} = Nous.Session.Log.append(log, :user_message, %{content: "b"})
    iex> Nous.Session.Log.since(log, 1) |> Enum.map(& &1.data.content)
    ["b"]
    iex> Nous.Session.Log.since(log, 5)
    []

# `surface`

```elixir
@spec surface(t()) :: [Nous.Session.Event.t()]
```

The surface events, in the order the model sees them: surface-typed events that
no later replace has shadowed.

A replacing event takes the position of the range it replaces, **not** its own
append position. A summary of events 0–40 belongs where those events were; put
at the end it would read as "here is the conversation, and now a summary of the
part that came before it", and it would leave a `:tool` message at the head with
no assistant prelude, which every provider rejects.

## Examples

    iex> log = Nous.Session.Log.new()
    iex> {:ok, log} = Nous.Session.Log.append(log, :user_message, %{content: "one"})
    iex> {:ok, log} = Nous.Session.Log.append(log, :step_start, %{})
    iex> {:ok, log} = Nous.Session.Log.append(log, :assistant_message, %{content: "two"})
    iex> Enum.map(Nous.Session.Log.surface(log), & &1.type)
    [:user_message, :assistant_message]

---

*Consult [api-reference.md](api-reference.md) for complete listing*
