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_responseis 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 appendedTo 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)
Summary
Functions
@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. Understream: 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'soutput_typefor this run:structured_output- Override the agent'sstructured_outputoptions for this run:sandbox- Override the agent'ssandboxpolicy for this run. Accepts the same shapes asNous.Agent.new/2's:sandboxoption (a mode atom, a keyword list, or aNous.Sandbox.Policy), e.g.Nous.run(agent, prompt, sandbox: :read_only):stream- Whentrue, the LLM call streams chunks while still running the tool-call loop (default:false). Fires:on_llm_new_deltaper text chunk and:on_llm_new_thinking_deltaper reasoning chunk.:on_llm_new_messagestill 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 withoutput_type(the synthetic-tool path is honored under streaming).
@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
@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.