Nous.AgentRunner (nous v0.17.1)

Copy Markdown View Source

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)

Summary

Functions

Run agent to completion.

Run agent with streaming.

Run agent with an existing context.

Functions

run(agent, prompt, opts \\ [])

@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(agent, prompt, opts \\ [])

@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(agent, ctx, opts \\ [])

@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.