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
endCallbacks
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.
Summary
Functions
Add a message to the context.
Add multiple messages to the context.
Record a tool call in the context.
Merge usage statistics into the context.
Get all assistant messages from the context.
Deserialize a map back into a Context struct.
Create context from an existing RunContext (migration helper).
Increment the iteration counter.
Get the last message from the context.
Append a bookkeeping event to the session log.
Check if maximum iterations has been reached.
Merge new dependencies into the context.
Create a new context with options.
Patch dangling tool calls in the conversation.
Set the assembly-time system-prompt fragment, or clear it with nil.
Replace the message at index with message, in place.
Replace the inclusive message range first..last with a single message.
Serialize context to a JSON-encodable map.
Set needs_response flag explicitly.
Convert to RunContext for tool execution (backwards compatibility).
Types
@type t() :: %Nous.Agent.Context{ active_skills: [Nous.Skill.t()], agent_name: String.t() | nil, approval_handler: (map() -> :approve | {:edit, map()} | :reject) | nil, callbacks: %{optional(atom()) => callback_fn()}, cancellation_check: (-> :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() }
Functions
@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
@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
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
@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
@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 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"
@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 the iteration counter.
Examples
iex> ctx = Context.new()
iex> ctx = Context.increment_iteration(ctx)
iex> ctx.iteration
1
@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
@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}
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 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"
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 withrequires_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 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
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.
@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.
@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 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 flag explicitly.
Examples
iex> ctx = Context.new()
iex> ctx = Context.set_needs_response(ctx, false)
iex> ctx.needs_response
false
@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.Policyto carry onto the run context as the session-level sandbox override. The agent runner passesagent.sandboxhere;nilleaves 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