# `Nous.Agent.Context`
[🔗](https://github.com/nyo16/nous/blob/v0.17.1/lib/nous/agent/context.ex#L1)

Unified context for agent execution.

Accumulates state across the agent loop:
- Conversation messages
- Tool call history
- Usage tracking
- User dependencies
- Callbacks configuration

## Example

    # Create new context
    ctx = Context.new(
      system_prompt: "You are helpful",
      deps: %{database: MyDB},
      max_iterations: 15
    )

    # Add messages
    ctx = ctx
    |> Context.add_message(Message.user("Hello"))
    |> Context.add_message(Message.assistant("Hi there!"))

    # Check loop control
    if ctx.needs_response do
      # Continue execution
    end

## Callbacks

Callbacks can be configured as a map of event handlers:

    ctx = Context.new(callbacks: %{
      on_llm_new_delta: fn _event, delta -> IO.write(delta) end,
      on_tool_call: fn _event, call -> IO.inspect(call) end
    })

## Process Notification

For LiveView integration, set `notify_pid`:

    ctx = Context.new(notify_pid: self())
    # Will receive: {:agent_delta, text}, {:tool_call, call}, etc.

# `callback_fn`

```elixir
@type callback_fn() :: (atom(), any() -&gt; any())
```

# `t`

```elixir
@type t() :: %Nous.Agent.Context{
  active_skills: [Nous.Skill.t()],
  agent_name: String.t() | nil,
  approval_handler: (map() -&gt; :approve | {:edit, map()} | :reject) | nil,
  callbacks: %{optional(atom()) =&gt; callback_fn()},
  cancellation_check: (-&gt; :ok | {:error, term()}) | nil,
  deps: map(),
  hook_registry: Nous.Hook.Registry.t() | nil,
  iteration: non_neg_integer(),
  log: Nous.Session.Log.t(),
  max_iterations: non_neg_integer(),
  messages: [Nous.Message.t()],
  needs_response: boolean(),
  notify_pid: pid() | nil,
  pubsub: module() | nil,
  pubsub_topic: String.t() | nil,
  started_at: DateTime.t() | nil,
  stream: boolean(),
  system_prompt: String.t() | nil,
  system_prompt_overlay: String.t() | nil,
  tool_calls: [map()],
  tool_schema_cache: {{atom(), MapSet.t()}, [map()]} | nil,
  usage: Nous.Usage.t()
}
```

# `add_message`

```elixir
@spec add_message(t(), Nous.Message.t(), keyword()) :: t()
```

Add a message to the context.

Appends one surface event to the log and re-materializes `messages` from the
fold, so the two stay in lockstep. Automatically updates `needs_response`
based on message role and content.

## Options

  * `:source` — marks injected context (`:memory`, `:knowledge_base`) in the
    event's data, so a later reader can tell injected context from
    conversation. It does not appear in the projected message.

## Examples

    iex> ctx = Context.new()
    iex> ctx = Context.add_message(ctx, Message.user("Hello"))
    iex> length(ctx.messages)
    1

# `add_messages`

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

Add multiple messages to the context.

One event per message, materialized once at the end rather than once per
message.

## Examples

    iex> ctx = Context.new()
    iex> messages = [Message.user("Hi"), Message.assistant("Hello")]
    iex> ctx = Context.add_messages(ctx, messages)
    iex> length(ctx.messages)
    2

# `add_tool_call`

```elixir
@spec add_tool_call(t(), map()) :: t()
```

Record a tool call in the context.

## Examples

    iex> ctx = Context.new()
    iex> call = %{id: "call_123", name: "search", arguments: %{"q" => "test"}}
    iex> ctx = Context.add_tool_call(ctx, call)
    iex> length(ctx.tool_calls)
    1

# `add_usage`

```elixir
@spec add_usage(t(), Nous.Usage.t() | map()) :: t()
```

Merge usage statistics into the context.

## Examples

    iex> ctx = Context.new()
    iex> usage = %Usage{input_tokens: 100, output_tokens: 50}
    iex> ctx = Context.add_usage(ctx, usage)
    iex> ctx.usage.input_tokens
    100

# `assistant_messages`

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

Get all assistant messages from the context.

## Examples

    iex> ctx = Context.new()
    iex> ctx = ctx |> Context.add_message(Message.user("Hi"))
    iex> ctx = ctx |> Context.add_message(Message.assistant("Hello"))
    iex> length(Context.assistant_messages(ctx))
    1

# `deserialize`

```elixir
@spec deserialize(map()) :: {:ok, t()} | {:error, term()}
```

Deserialize a map back into a Context struct.

Reads both versions, which is what makes v1 → v2 a migration rather than a
break: a v2 blob rebuilds the log from its events, and a v1 blob seeds one
from its flat message list. Functions, PIDs, and callbacks are not restored
and will use defaults.

A v1 blob folds back to its original messages, but every message is stamped
with the restore time: v1 never persisted `created_at`, so the original
timestamps are not in the blob to recover.

Returns `{:ok, context}` or `{:error, reason}`.

## Examples

    iex> ctx = Context.new(system_prompt: "Be helpful")
    iex> data = Context.serialize(ctx)
    iex> {:ok, restored} = Context.deserialize(data)
    iex> restored.system_prompt
    "Be helpful"

# `from_run_context`

```elixir
@spec from_run_context(
  Nous.RunContext.t(),
  keyword()
) :: t()
```

Create context from an existing RunContext (migration helper).

## Examples

    iex> run_ctx = Nous.RunContext.new(%{key: "value"})
    iex> ctx = Context.from_run_context(run_ctx)
    iex> ctx.deps.key
    "value"

# `increment_iteration`

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

Increment the iteration counter.

## Examples

    iex> ctx = Context.new()
    iex> ctx = Context.increment_iteration(ctx)
    iex> ctx.iteration
    1

# `last_message`

```elixir
@spec last_message(t()) :: Nous.Message.t() | nil
```

Get the last message from the context.

## Examples

    iex> ctx = Context.new() |> Context.add_message(Message.user("Hello"))
    iex> Context.last_message(ctx).content
    "Hello"

    iex> ctx = Context.new()
    iex> Context.last_message(ctx)
    nil

# `log_event`

```elixir
@spec log_event(t(), Nous.Session.Event.type(), map()) :: t()
```

Append a **bookkeeping** event to the session log.

Bookkeeping events (`:turn_start`, `:turn_end`, `:step_start`, `:step_end`,
`:tool_call`, `:request_header`) project to no message, so `messages` is
unchanged and no existing reader can see them. They are what makes a run
reconstructable after the fact — which turn a tool call belonged to, which step
produced a request, where a crash interrupted things.

Refuses a surface type: appending a `:user_message` this way would bypass
`add_message/3`'s projection bookkeeping and leave `messages` disagreeing with
the log. Invalid events are dropped with a warning rather than raising, because
losing one bookkeeping event is strictly better than failing the user's run.

## Examples

    iex> ctx = Context.new() |> Context.log_event(:turn_start, %{turn: 1})
    iex> ctx.messages
    []
    iex> [event] = Nous.Session.Log.events(ctx.log)
    iex> {event.type, event.data.turn}
    {:turn_start, 1}

# `max_iterations_reached?`

```elixir
@spec max_iterations_reached?(t()) :: boolean()
```

Check if maximum iterations has been reached.

## Examples

    iex> ctx = Context.new(max_iterations: 5, iteration: 5)
    iex> Context.max_iterations_reached?(ctx)
    true

    iex> ctx = Context.new(max_iterations: 5, iteration: 3)
    iex> Context.max_iterations_reached?(ctx)
    false

# `merge_deps`

```elixir
@spec merge_deps(t(), map()) :: t()
```

Merge new dependencies into the context.

Used by tools to update context state via `__update_context__` or `ContextUpdate`.

## Examples

    iex> ctx = Context.new(deps: %{count: 0})
    iex> ctx = Context.merge_deps(ctx, %{count: 1, new_key: "value"})
    iex> ctx.deps.count
    1
    iex> ctx.deps.new_key
    "value"

# `new`

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

Create a new context with options.

## Options

  * `:messages` - Initial message list (default: [])
  * `:system_prompt` - System prompt string
  * `:deps` - User dependencies map (default: %{})
  * `:max_iterations` - Maximum loop iterations (default: 10)
  * `:callbacks` - Map of callback functions
  * `:notify_pid` - PID to receive event messages
  * `:agent_name` - Name for telemetry/logging
  * `:cancellation_check` - Function to check for cancellation
  * `:approval_handler` - Function called for tools with `requires_approval: true`
  * `:stream` - When true, the runner uses streaming + tool execution (default: false)

## Examples

    iex> ctx = Context.new(system_prompt: "Be helpful", max_iterations: 5)
    iex> ctx.max_iterations
    5

    iex> ctx = Context.new(deps: %{user_id: 123})
    iex> ctx.deps.user_id
    123

# `patch_dangling_tool_calls`

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

Patch dangling tool calls in the conversation.

Scans messages for assistant messages with tool_calls that have no
corresponding tool result message. Injects synthetic tool results
for unmatched calls with a message indicating the call was interrupted.

This is critical when resuming from a persisted context where the
session was interrupted mid-tool-execution.

## Examples

    iex> ctx = Context.new(messages: [
    ...>   Message.assistant("Let me search", tool_calls: [%{id: "call_1", name: "search"}])
    ...> ])
    iex> ctx = Context.patch_dangling_tool_calls(ctx)
    iex> length(ctx.messages)
    2

# `put_system_prompt_overlay`

```elixir
@spec put_system_prompt_overlay(t(), String.t() | nil) :: t()
```

Set the assembly-time system-prompt fragment, or clear it with `nil`.

The fragment is appended to the transcript's leading system message (or
becomes one, if there is none) every time `messages` is materialized. It is
**not** an event, deliberately: it is derived from agent config, plugins and
skills, and re-derived on every run, so logging it would append a copy of the
same text to a durable log per run. See `Nous.AgentRunner.PromptAssembly`.

Because it is applied *during* materialization rather than written over
`messages`, the view still equals the fold plus this one pure, idempotent
overlay — setting it twice replaces it instead of compounding.

# `replace_message`

```elixir
@spec replace_message(t(), non_neg_integer(), Nous.Message.t()) :: t()
```

Replace the message at `index` with `message`, in place.

Appends a `{:replace, seq, seq}` event instead of rewriting the list, so the
original stays in the log. This is how compaction prunes an oversized tool
result without destroying what it pruned.

An `index` outside the transcript is a no-op with a warning: a caller working
from a stale view must not silently rewrite the wrong message.

# `replace_message_range`

```elixir
@spec replace_message_range(
  t(),
  non_neg_integer(),
  non_neg_integer(),
  Nous.Message.t()
) :: t()
```

Replace the inclusive message range `first..last` with a single `message`.

Non-destructive compaction: the replacement takes the *position* of the range
it shadows, so a summary lands where the conversation it summarizes was, and
`Nous.Session.Log.events/1` still returns every shadowed event.

The replacement inherits the `created_at` of the first message it shadows — it
stands in for that range, and the transcript's timestamps stay non-decreasing.

Every message whose position falls inside the range is shadowed, including one
the caller did not enumerate (an injected system message sitting between two
conversation turns, say). That is deliberate: the range is a position range,
and a hole in it would reorder the transcript.

# `serialize`

```elixir
@spec serialize(t()) :: map()
```

Serialize context to a JSON-encodable map.

Version 2 persists the **event log**. `messages` is still emitted, because it
is what a v1 reader (or a human) consumes, but it is a projection of the
events, not the source of truth.

Persists messages, usage, metadata. Never persists functions, PIDs, or
modules. The assembled system-prompt overlay is runtime state and is not
persisted either; it is re-derived on the next run.

## Examples

    iex> ctx = Context.new(system_prompt: "Be helpful", max_iterations: 5)
    iex> data = Context.serialize(ctx)
    iex> data.version
    2
    iex> data.system_prompt
    "Be helpful"

# `set_needs_response`

```elixir
@spec set_needs_response(t(), boolean()) :: t()
```

Set needs_response flag explicitly.

## Examples

    iex> ctx = Context.new()
    iex> ctx = Context.set_needs_response(ctx, false)
    iex> ctx.needs_response
    false

# `to_run_context`

```elixir
@spec to_run_context(
  t(),
  keyword()
) :: Nous.RunContext.t()
```

Convert to RunContext for tool execution (backwards compatibility).

This allows tools to continue using the existing RunContext interface.

## Options

  * `:sandbox` - `Nous.Sandbox.Policy` to carry onto the run context as the
    session-level sandbox override. The agent runner passes `agent.sandbox`
    here; `nil` leaves resolution to application config.

## Examples

    iex> ctx = Context.new(deps: %{db: :postgres})
    iex> run_ctx = Context.to_run_context(ctx)
    iex> run_ctx.deps.db
    :postgres

    iex> ctx = Context.new(deps: %{})
    iex> run_ctx = Context.to_run_context(ctx, sandbox: Nous.Sandbox.Policy.new(:read_only))
    iex> run_ctx.sandbox.mode
    :read_only

---

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