# `Nous.AgentRunner`
[🔗](https://github.com/nyo16/nous/blob/v0.17.1/lib/nous/agent_runner.ex#L1)

Executes agent runs with tool calling loop.

The AgentRunner is responsible for:
- Building messages with system prompts and instructions
- Calling the model via the provider
- Detecting and executing tool calls
- Looping until `needs_response` is false
- Extracting and validating output
- Executing callbacks and sending process notifications

## Context-Based Execution

The runner uses a `Context` struct to manage all state during execution:

    ctx = Context.new(
      deps: %{database: MyDB},
      callbacks: %{on_llm_new_delta: fn _, d -> IO.write(d) end},
      notify_pid: self()
    )

## Behaviour Integration

Different agent types can customize behavior by implementing
`Nous.Agent.Behaviour` and setting `behaviour_module` on the agent.

## Examples

Most callers reach the runner through `Nous.run/3`, but calling it directly
is the same thing minus the convenience wrapper. `run/3` drives the whole
tool-calling loop and returns a plain map:

    agent =
      Nous.Agent.new("openai:gpt-4o-mini",
        tools: [&Nous.Tools.DateTimeTools.current_date/2]
      )

    {:ok, result} = Nous.AgentRunner.run(agent, "What day is it in Athens?")

    result.output          #=> "It's Tuesday, 12 August 2026 in Athens."
    result.iterations      #=> 2
    result.usage.total_tokens
    result.new_messages    #=> the assistant/tool messages this run appended

To continue a conversation, hand the previous run's messages back in — or
reuse `result.context` directly with `run_with_context/3`, which also
preserves any `deps` a tool mutated:

    {:ok, first} = Nous.AgentRunner.run(agent, "My name is Ada.")
    {:ok, second} = Nous.AgentRunner.run(agent, "What is my name?",
      message_history: first.all_messages
    )

    {:ok, third} = Nous.AgentRunner.run_with_context(agent, second.context)

Streaming is a flag on the same loop, not a separate code path. With
`stream: true` the tool loop still runs; deltas arrive through callbacks:

    {:ok, result} =
      Nous.AgentRunner.run(agent, "Write a haiku about BEAM schedulers",
        stream: true,
        callbacks: %{on_llm_new_delta: fn _event, delta -> IO.write(delta) end}
      )

`run_stream/3` instead returns an enumerable of `Nous.Types.stream_event()`
tuples, for when you want to own the consumption:

    {:ok, stream} = Nous.AgentRunner.run_stream(agent, "Explain OTP in one line")

    Enum.each(stream, fn
      {:text_delta, text} -> IO.write(text)
      {:complete, _result} -> IO.puts("")
      _other -> :ok
    end)

# `run`

```elixir
@spec run(Nous.Agent.t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
```

Run agent to completion.

## Options
  * `:deps` - Dependencies for tools
  * `:message_history` - Previous messages
  * `:usage_limits` - Usage limits (not implemented yet)
  * `:model_settings` - Override model settings
  * `:max_iterations` - Maximum iterations (default: 10)
  * `:cancellation_check` - Function to check if execution should be cancelled.
    Under `stream: true`, also invoked between every streamed chunk; on
    cancellation the consumer aborts cleanly without partial tool execution.
  * `:callbacks` - Map of callback functions
  * `:notify_pid` - PID to receive event messages
  * `:context` - Existing context to continue from
  * `:output_type` - Override the agent's `output_type` for this run
  * `:structured_output` - Override the agent's `structured_output` options for this run
  * `:sandbox` - Override the agent's `sandbox` policy for this run. Accepts
    the same shapes as `Nous.Agent.new/2`'s `:sandbox` option (a mode atom, a
    keyword list, or a `Nous.Sandbox.Policy`), e.g.
    `Nous.run(agent, prompt, sandbox: :read_only)`
  * `:stream` - When `true`, the LLM call streams chunks while still running
    the tool-call loop (default: `false`). Fires `:on_llm_new_delta` per
    text chunk and `:on_llm_new_thinking_delta` per reasoning chunk.
    `:on_llm_new_message` still fires once per iteration with the assembled
    message, identical in shape to the non-streaming path. Works across all
    providers (OpenAI-compatible, Anthropic, Gemini) and is compatible with
    `output_type` (the synthetic-tool path is honored under streaming).

# `run_stream`

```elixir
@spec run_stream(Nous.Agent.t(), String.t(), keyword()) ::
  {:ok, Enumerable.t()} | {:error, term()}
```

Run agent with streaming.

Returns a stream that yields events as they occur.

## Events
  * `{:text_delta, text}` - Incremental text update
  * `{:thinking_delta, text}` - Thinking content (reasoning models)
  * `{:tool_call, call}` - Tool is being called
  * `{:tool_result, result}` - Tool execution completed
  * `{:finish, reason}` - Stream finished
  * `{:complete, result}` - Final result

# `run_with_context`

```elixir
@spec run_with_context(Nous.Agent.t(), Nous.Agent.Context.t(), keyword()) ::
  {:ok, map()} | {:error, term()}
```

Run agent with an existing context.

Useful for continuing from a previous run or with pre-built context.

---

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