Back to Blog

AI Agent Session Architecture: Event Sourcing Explained

Tutorials and Guides1530
AI Agent Session Architecture: Event Sourcing Explained

Introduction

When developers examine the Agent loop of dsh, they will observe that a single Turn is split into multiple Steps. User messages first enter the Session component, and content generated by the model is incrementally written into the Session. Tools and corresponding Agent loop logic are recorded in the same event log whenever requests are initiated.

A core function, this.session.deriveMessages(), runs every time a model request is triggered. The Session fetches relevant historical records from the event log, assembles context messages required for model inference, and generates a complete request payload. The Session maintains two synchronized copies of data during Agent runtime: raw event records and derived messages passed into the LLM. When tool calls produce new events, both datasets must be updated synchronously. When context compression occurs, both copies are compressed in tandem. When a Session restores from a checkpoint, the system must confirm that both copies reference identical historical sequences.

This article analyzes the internal design of dsh’s Session module. It covers event log storage rules, message derivation, surface layer management, checkpoint persistence, crash recovery, replay, forking and projection. All logic is built on a unified event recording system, which enables state restoration and context compression for long-running AI agent workflows.

1. Generation of Model Context

The Session relies on event logs to maintain state. During Agent execution, every tool call generates new records. These records feed two parallel data streams. One stream retains the complete raw event history for tracing and recovery, while the other constructs message lists for LLM input via deriveMessages().

deriveMessages(): Message[] {
  // filter and transform SessionEventLog into model input messages
}

User input, model output, and tool invocation processes are all recorded as events. Step and Turn boundaries, and state shifts produced during plugin execution, are also persisted in SessionEventLog. The Session first determines which events should be included in the model context and their ordering based on the current Surface state. It then calls deriveMessages() to produce the final Message[] array. When restoring a Session, the system reloads these events and applies the same set of transformation rules to rebuild context.

In the persistent catalog definition for dsh, there are 48 defined types of persistent events. Thirteen of these are native to dsh-session, and the remaining event types are extended by individual plugins.

Not all of these 48 event types are converted into model context messages. Only three categories of events can generate messages for LLM input:

type SurfaceEventType =
  | 'user/message'
  | 'assistant/message'
  | 'tool/result'

Events such as turn/start, step/end, tool/call, assistant/chunk, retry operations, approval workflows and compression events are retained in the log. However, these records do not enter the model’s prompt context. This constraint is explicitly encoded in the type system of dsh.

append<T extends SessionEventType>(
  type: T,
  data: SessionEventMap[T],
  ...opts: T extends Textends SurfaceEventType ? [opts: SurfaceIntent] : []
): SessionEvent<T>

When invoking append() for the three Surface Event types, developers must pass SurfaceIntent parameters. Other event types cannot receive these parameters. This design enforces a hard boundary: only explicitly defined event types are eligible for inclusion in model context.

Event logs also enforce sequential ordering constraints via sequence numbers. The seq field represents the continuous position of an event inside the log. The underlying Persistence Backend responsible for physical storage can decide batch writing strategies and storage structures independently. However, when reloading a Session, events must be reconstructed in their original sequence. This explains why assistant/chunk entries remain stored inside the log.

assistant/chunk events support streaming playback, usage statistics and audit records. For model inference context, only the final aggregated assistant/message is used.

The Session Event Log stores the complete execution history of an Agent. The messages sent to the LLM are only a subset derived from this full execution history.

1.1 Surface Layer for Context Compression and Truncation

Raw event logs are retained for persistence and replay, but model context must support compression and truncation. Consider this original message sequence:

  1. Message A
  2. Message B
  3. Tool Result C
  4. Message D
  5. Message E

After compression, the model may only see the reduced sequence:

  1. Summary
  2. Message D
  3. Message E

dsh introduces the SessionSurface layer on top of the raw event log to manage this transformation.

interface SessionSurface {
  readonly nodes: readonly number[];
  readonly replaceGeneration: number;
}

Surface Events use surfaceOps to declare how they modify the current model history.

type SurfaceOp =
  | { op: 'append'; start: number; end: number }
  | { op: 'replace'; start: number; end: number }

append adds new nodes to the end of the sequence. replace substitutes a segment inside the Surface with new nodes. Events that are replaced remain preserved in the underlying SessionEventLog.

replace is mainly used by dsh’s Compression subsystem. Compression defines four dedicated persistent events:

compression/start
compression/summary
compression/end
compression/prune

During summary compression, the compression workflow records its progress through compression/* events. The final summary that enters model context is written as a user/message, which uses replace to overwrite the original Surface range.

The Tool Result Pruner uses this identical mechanism. It adds a compression/prune event to record the pruned Surface nodes and token count, and appends a replacement tool/result entry synchronously.

The original tool result is kept in the log, while the pruned version enters the current model context. Token consumption metrics can be calculated based on compression/prune records. The mapping rules convert three categories of events into messages:

  1. user/message → User Message
  2. assistant/message → Assistant Message
  3. tool/result → tool_result block wrapped inside User Message

Obsolete assistant/message entries are removed from the Surface view after replacement. The message generation logic leverages caching. When replace operations modify the Surface, only affected message segments are regenerated. The rest of the conversation history remains cached and unchanged.

As a result, deriveMessages() does not need to traverse and rebuild the entire conversation history on every model request. It only refreshes the segments modified by replace.

2. Request State and Persistence Boundaries

A single model request carries the complete set of request configuration: System Prompt, tool definitions, sampling parameters and adapter defaults. dsh defines request/header to store a complete EpochHeader.

interface EpochHeader {
  config: LlmCallConfig;
  adapterDefaults: LlmCallConfigAdapterDefaults;
  tools?: ToolSpec[];
  system?: string;
}

The first request writes an initial header. A new Agent loop instance resumes using the existing header when a Session is reloaded. If the model version, System Prompt or tool definitions change, a new header is recorded. Unchanged Steps reuse the previous header.

A model request can be fully restored from two state sources: the Session Event Log and the Request Header stored inside the Surface.

adapterDefaults records default configuration values injected by the Adapter. Before entering the next Agent turn, the Agent loop clears Adapter-supplied default parameters and re-evaluates defaults based on current routing logic. Explicitly defined parameters persist and override adapter defaults.

The request/context object records the model’s context window capacity. It does not participate in Request Header change detection or restoration. dsh also provides an deepseek-ai/dsh-agent-log plugin. This plugin regenerates request payloads from Session Event Log and Request Header, then compares them against the actual requests emitted by the Agent loop. This verification ensures that requests sent by the Agent loop can be reproduced exactly from the persisted event log.

Once SessionEventLog becomes the source of truth, a critical question emerges: at which point must events be written to external persistent storage before operations execute? dsh splits persistence logic into two separate concepts: Persistence Backend and Checkpoint Policy.

Persistence Backend handles write and flush operations. Events can be written into memory first and flushed later via session/flush. dsh-session-checkpoint-policy defines three key moments to trigger persistence:

  1. Before a model request is sent
  2. Before tool invocation
  3. Before the next Agent Step

When a checkpoint is triggered, related events must finish persistence before downstream streams start. For tool execution, checkpoints run after pre-flight validation and before actual tool invocation. If checkpoint saving fails, the model request and subsequent tool calls will not execute. If a SIGINT signal arrives during persistence, the tool wrapper performs automatic flushing.

Nested tool invocations can reuse checkpoints from outer tool calls, eliminating the requirement to run separate flushes for every nested call. This mechanism reduces the scope of non-deterministic states introduced by tool calls, though it cannot fully guarantee idempotent execution for external tools.

3. Crash Recovery and State Restoration

When a crash occurs while writing events into Session and executing external systems, recovery logic differs based on crash timing. dsh categorizes incomplete execution states for recovery processing.

When recovery cannot automatically determine the operation state, external metadata or user confirmation is required. The Agent then resumes processing according to tool semantics.

Checkpoints only guarantee that event records are fully persisted before operations begin. Actual execution results from external systems remain outside full control of Session. After a crash, normal Turn state reconstruction will proceed.

A complete normal Turn sequence:

turn/start
step/start
...
step/end
turn/end

If a process exits before a Turn completes, the persisted log may be incomplete. The incomplete sequence lacks turn/end.

The Repair module handles incomplete Sessions. Repair only operates on non-live Sessions. A live Session still in memory will not be modified by Repair. When a Session is loaded, Repair supplements missing Tool, Step and Turn records for interrupted workflows. It adds an interrupted marker for incomplete Turns. The interrupted marker is only generated during Session recovery and does not appear in normal Agent loop execution.

Repair reads historical events and current pending events. It supplements missing step/end and turn/end markers and injects session/ended-seq to mark the boundary of completed history.

4. Replay, Fork and Projection

Replay loads historical event logs and reconstructs identical Surface and Message sequences. It can reproduce the entire Agent history. Streamed assistant/chunk records can also be replayed.

Fork creates a new independent Session branch from a specified boundary point. The original parent Session remains unchanged. Fork supports branching at arbitrary Turn boundaries.

Projection enables registration of custom state update functions. When specified events match projection rules, state information such as task progress can be updated following the order of SessionEvent. Projection does not alter raw event logs. Multiple projections can run concurrently on the same Session log without modifying underlying persistence storage. Adding new projections does not require rewriting historical records.

5. Long-running Session Cost and Performance

For long-running agent sessions, the volume of event logs continuously grows. Every event is persisted to durable storage. The design of dsh’s Session query API supports inspecting running and historical Sessions. The built-in persistence implementation uses SQLite for storage. This storage engine handles event serialization, flushing and querying. When loading a historical Session, the system reads the event log and rebuilds Surface and message state.

Event persistence brings storage overhead. Once events are written into durable storage, subsequent reads must fetch these historical records. Current dsh-session has built-in limits on log volume. When the log exceeds defined thresholds, dsh adopts a defensive fallback strategy for unrecognized historical events.

Conclusion

The core design philosophy of dsh Session is to separate raw event history from the derived model context view. Every Turn and Step in the Agent loop appends records to SessionEventLog. The Surface layer applies transformation rules such as append and replace, to generate the message sequence sent to the model. Checkpoint policies define when event data must be flushed into persistent storage. Crash recovery, replay, forking and projection all operate on the same underlying SessionEvent record stream. This unified event architecture enables state restoration, context compression and audit tracing for long-lived AI agent workflows.

When building production agent systems with multiple LLM endpoints and complex routing rules, developers can use 4sapi, an API gateway, to manage unified model access and traffic governance.

International access: https://4sapi.com
Domestic access: https://4sapi.cn

Tags:AI Agent Sessionevent sourcingAgent architectureSessionEventLogLLM engineering

Recommended reading

Explore more frontier insights and industry know-how.