One durable fact about a session.
Events are append-only. Nothing is ever mutated or deleted: a compaction appends a replace that shadows a range, and the shadowed events stay in the log where fork, rewind and audit can still see them.
Identity lives here, not on Nous.Message
%Nous.Message{} declares @primary_key false — messages genuinely have no
identity, and adding one would change a public struct that
Nous.run/3 hands back. The event carries seq; messages are projected fresh
from the log every time. That is what lets the log be internal while
result.messages stays byte-identical.
Surface and non-surface types
Only four types project into the model-visible surface, and only those may
carry a :surface_op:
:system_message,:user_message,:assistant_message,:tool_result
Everything else is bookkeeping the model never sees — :tool_call,
:turn_start, :turn_end, :step_start, :step_end, :request_header. They
are logged because they are what makes a run reconstructable, but
Nous.Session.Log.derive_messages/1 projects them to nothing.
Why four and not three
The plan specified exactly three surface types, on the assumption that system
content is assembly-time state rather than history. That assumption does not
survive contact with the code: Nous.Plugins.Summarization appends its
summary as a system message mid-conversation, and
build_initial_messages in Nous.AgentRunner puts one at the head. A system
message is therefore a real, ordered part of the transcript and needs a
surface type. The rewrite of the system prompt in
Nous.AgentRunner.PromptAssembly stays assembly-time and is deliberately not
an event — see that module.
Summary
Functions
Build an event, validating type, data shape and :surface_op.
Whether this event contributes to the model-visible surface.
The :surface_op this event carries. :append unless it says otherwise.
The types that project into the model-visible surface.
Every valid event type.
Types
@type surface_op() :: :append | {:replace, non_neg_integer(), non_neg_integer()}
How this event contributes to the model-visible surface.
:append adds it at the end. {:replace, start_seq, stop_seq} additionally
shadows every surface event in that inclusive seq range — the mechanism
behind non-destructive compaction.
@type t() :: %Nous.Session.Event{ data: map(), seq: non_neg_integer(), time: DateTime.t(), type: type() }
@type type() ::
:system_message
| :user_message
| :assistant_message
| :tool_result
| :tool_call
| :turn_start
| :turn_end
| :step_start
| :step_end
| :request_header
Functions
@spec new(non_neg_integer(), type(), map(), DateTime.t() | nil) :: {:ok, t()} | {:error, term()}
Build an event, validating type, data shape and :surface_op.
Validation happens here, at append time, not at persist time. An event that cannot be serialized is a bug in the caller, and discovering it hours later when a session is saved makes it someone else's bug.
Returns {:error, reason} rather than raising: an event that fails validation
must not take down a live agent run.
Whether this event contributes to the model-visible surface.
Examples
iex> Nous.Session.Event.surface?(:assistant_message)
true
iex> Nous.Session.Event.surface?(:step_start)
false
@spec surface_op(t()) :: surface_op()
The :surface_op this event carries. :append unless it says otherwise.
@spec surface_types() :: [type()]
The types that project into the model-visible surface.
Examples
iex> Nous.Session.Event.surface_types()
[:system_message, :user_message, :assistant_message, :tool_result]
@spec types() :: [type()]
Every valid event type.