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

GenServer wrapper for Nous agents with PubSub integration.

This server:
- Wraps a Nous agent (standard or ReAct)
- Links to parent process (dies when parent dies)
- Subscribes to PubSub for incoming messages
- Publishes responses back via PubSub
- Maintains conversation context for multi-turn conversations

## Context-Based State

Uses `Nous.Agent.Context` to maintain conversation state:
- Messages accumulate across turns
- Tool calls are tracked
- Usage is aggregated
- Callbacks forward to PubSub

## Usage with LiveView

    defmodule MyAppWeb.ChatLive do
      use MyAppWeb, :live_view

      def mount(_params, _session, socket) do
        # Start agent linked to this LiveView
        {:ok, agent_pid} = AgentServer.start_link(
          session_id: socket.assigns.session_id,
          agent_config: %{
            model: "lmstudio:qwen3-vl-4b-thinking-mlx",
            instructions: "You are a helpful assistant",
            tools: []
          }
        )

        # Subscribe to responses. Always build the topic with
        # Nous.PubSub.agent_topic/1 — it is what this server publishes to.
        Phoenix.PubSub.subscribe(MyApp.PubSub, Nous.PubSub.agent_topic(socket.assigns.session_id))

        {:ok, assign(socket, agent_pid: agent_pid, messages: [])}
      end

      def handle_event("send_message", %{"message" => msg}, socket) do
        AgentServer.send_message(socket.assigns.agent_pid, msg)
        {:noreply, socket}
      end

      # Receive streaming deltas
      def handle_info({:agent_delta, text}, socket) do
        # Append text to current response
        {:noreply, update(socket, :current_response, &(&1 <> text))}
      end

      # Receive complete response
      def handle_info({:agent_complete, result}, socket) do
        messages = socket.assigns.messages ++ [%{role: :assistant, content: result.output}]
        {:noreply, assign(socket, messages: messages, current_response: "")}
      end

      # Receive tool calls
      def handle_info({:tool_call, call}, socket) do
        # Show tool call in UI
        {:noreply, socket}
      end
    end

## Message Interruption

Calling `send_message/2` while the agent is already processing a request
automatically cancels the in-flight execution and starts a new one. The
server uses an `:atomics`-based flag so the running task can detect
cancellation without message-passing overhead. Both the interrupted
user message and the new one are preserved in the conversation context,
so no input is lost.

## PubSub Events

Subscribers on the `Nous.PubSub.agent_topic(session_id)` topic (currently
`"nous:agent:<session_id>"`) receive the following messages:

| Message                          | Description                             |
|----------------------------------|-----------------------------------------|
| `{:agent_status, :thinking}`     | A new run is about to start             |
| `{:agent_status, :started}`      | The LLM provider acknowledged the call  |
| `{:agent_delta, text}`           | A streaming text chunk                  |
| `{:tool_call, call}`             | A tool invocation is in progress        |
| `{:tool_result, result}`         | A tool returned its result              |
| `{:agent_response, output}`      | The final text output of the run        |
| `{:agent_complete, result}`      | The full result struct (output + context + usage) |
| `{:agent_error, message}`        | An error occurred during execution      |
| `{:agent_cancelled, reason}`     | The execution was cancelled             |
| `{:session_event, %Nous.Session.Event{}}` | One committed log event — surface and bookkeeping alike |

`{:session_event, _}` is published by `Nous.Agent.Context` itself for every
event committed to the session log, which is why nothing in this server
broadcasts it: `init/1` already points the run context at this topic. It is
the message to render a transcript from, since it also carries the turn and
step boundaries (`:turn_start`, `:step_start`, `:step_end`, `:turn_end`) that
the ad-hoc callbacks above cannot express.

## Steering and injection

`send_message/2` is the interrupting path: it cancels whatever is in flight
and starts over. The inbox paths never interrupt anything — they queue a
message and let the run claim it at its next boundary:

| Call         | Queued for | Wakes an idle agent? |
|--------------|------------|----------------------|
| `followup/2` | next turn  | yes                  |
| `steer/2`    | next step  | yes                  |
| `inject/2`   | next step  | **no**               |

`steer/2` and `inject/2` differ in exactly one bit, and it is the bit people
get wrong. Worked example, on an agent that is idle after its last run:

    # Nothing happens. No model request, no tokens, no run. The note is queued
    # and will be handed to the next request the agent makes anyway.
    AgentServer.inject(pid, "FYI: the staging deploy is frozen until 14:00")

    # This starts a run. It claims the injected note *and* this message, in
    # that order, so both are in the first request's message list.
    AgentServer.steer(pid, "Which services are still pending release?")

Mid-run, the distinction is about latency rather than about starting work:

    AgentServer.send_message(pid, "Audit every service for stale configs")
    # ... the agent is three tool calls deep ...
    AgentServer.steer(pid, "Actually, skip anything under /legacy")
    # The NEXT model request of that same run sees the instruction. The tool
    # call already in flight is neither interrupted nor cancelled, and the
    # message is not claimed by the request that is already on the wire.

Both accept a binary or a `Nous.Message`, and both are safe to call whether or
not a run is in flight.

## Run state

The server keeps an explicit `run_state` of `:idle` or `:running`, and every
transition into `:idle` goes through one function. That is what makes `wakeup`
decidable: `steer/2` on an idle agent must start work and `inject/2` must not,
and both answers depend on knowing — not inferring — whether a run is in
flight.

The upstream implementation this was ported from needs a `wakeRequested` latch
to cover the window where a message lands after the last claim of a run that
is about to end. There is no latch here. The wake intent rides on the queued
message itself (see `Nous.Session.Inbox`), so the `:running -> :idle`
transition just asks the inbox whether a run is still owed. Enqueueing and
finishing are both serialized through this process's mailbox, so exactly one
of the two starts the run.

## Lifecycle

Each server starts an inactivity timer (default 5 minutes, configurable
via `:inactivity_timeout`). The timer resets on every `send_message/2`
call. When the timer fires, the server terminates with `:normal`.

If a `:persistence` backend is configured, the conversation context is
automatically saved after each successful agent run and restored on
`start_link/1`.

# `agent_config`

```elixir
@type agent_config() :: %{
  model: String.t(),
  instructions: String.t(),
  tools: list(),
  type: :standard | :react,
  model_settings: map()
}
```

# `state`

```elixir
@type state() :: %{
  session_id: String.t(),
  agent: Nous.Agent.t(),
  context: Nous.Agent.Context.t(),
  pubsub: module() | nil,
  topic: String.t(),
  agent_type: :standard | :react,
  current_task: Task.t() | nil,
  task_generation: non_neg_integer(),
  cancelled_ref: :atomics.atomics_ref(),
  inactivity_timeout: timeout(),
  inactivity_timer_ref: reference() | nil,
  persistence: module() | nil,
  run_state: :idle | :running,
  inbox: Nous.Session.Inbox.t()
}
```

# `cancel_execution`

```elixir
@spec cancel_execution(pid()) :: {:ok, :cancelled} | {:ok, :no_execution}
```

Cancel the current agent execution.

Returns `{:ok, :cancelled}` when an execution was running and has been
stopped, or `{:ok, :no_execution}` when there was nothing to cancel.

The server will:
- Set the atomics cancellation flag so the task exits at the next check
- Shut down the running task gracefully (5 s timeout)
- Broadcast `{:agent_cancelled, reason}` to PubSub subscribers
- Reset the flag for future executions

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `clear_history`

```elixir
@spec clear_history(pid()) :: :ok
```

Clear conversation context and start fresh.

Resets messages and tool-call history but preserves the configured
dependencies (`:deps`) and system prompt.

# `followup`

```elixir
@spec followup(GenServer.server(), Nous.Message.t() | String.t()) :: :ok
```

Queue a message for the agent's next **turn**, waking it if it is idle.

"Answer this once you have finished what you are doing." Unlike
`send_message/2` it never cancels the run in flight, and unlike `steer/2` it
does not join that run: it is claimed when the next turn opens.

`message` may be a binary or a `Nous.Message`. Returns immediately.

# `get_context`

```elixir
@spec get_context(pid()) :: Nous.Agent.Context.t()
```

Get conversation context.

# `get_history`

```elixir
@spec get_history(pid()) :: list()
```

Get conversation history (messages only).

# `inject`

```elixir
@spec inject(GenServer.server(), Nous.Message.t() | String.t()) :: :ok
```

Queue context for the agent's next **step** without waking it.

Injected context waits for the next admitted request rather than starting one:
on an idle agent this is inert until something else makes the agent run, and
on a running agent it behaves exactly like `steer/2`.

Use it for material the agent should have *if* it asks another question —
retrieved documents, a changed permission, a note from another process — where
spending a model request purely to deliver it would be wrong.

`message` may be a binary or a `Nous.Message`. Returns immediately.

# `load_context`

```elixir
@spec load_context(pid(), String.t()) :: :ok | {:error, term()}
```

Load a previously saved context from the persistence backend.

Replaces the current context with the loaded one. Patches any dangling tool
calls that may have been interrupted mid-execution.

Returns `:ok` on success, `{:error, :no_persistence}` if no backend is configured,
or `{:error, reason}` on failure.

# `save_context`

```elixir
@spec save_context(pid()) :: :ok | {:error, term()}
```

Manually save the current context to the persistence backend.

Returns `:ok` on success, `{:error, :no_persistence}` if no backend is configured,
or `{:error, reason}` on failure.

# `send_message`

```elixir
@spec send_message(pid(), String.t()) :: :ok
```

Send a message to the agent.

If a previous execution is still running, it is automatically cancelled
before the new message is processed. The interrupted message and the new
one both remain in the conversation context. Returns immediately — the
agent run happens asynchronously and results are broadcast via PubSub.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Start an AgentServer linked to the calling process.

## Options

- `:session_id` - Unique session identifier (required)
- `:agent_config` - Agent configuration map (required)
- `:pubsub` - PubSub module (default: MyApp.PubSub)
- `:name` - Optional GenServer name (e.g., a Registry via tuple)
- `:inactivity_timeout` - Inactivity timeout in ms (default: 5 minutes). Set to `:infinity` to disable.
- `:persistence` - Persistence backend module (e.g., `Nous.Persistence.ETS`). When set, context is auto-saved after each response and restored on init.

## Agent Config

- `:model` - Model string (e.g., "openai:gpt-4")
- `:instructions` - System instructions
- `:tools` - List of tool functions
- `:type` - `:standard` or `:react` (default: :standard)
- `:model_settings` - Model settings map
- `:deps` - Initial dependencies for tools

# `steer`

```elixir
@spec steer(GenServer.server(), Nous.Message.t() | String.t()) :: :ok
```

Queue a message for the agent's next **step**, waking it if it is idle.

Mid-run steering. The message is claimed by the next model request of the run
already in flight — not by the request currently on the wire — and nothing is
cancelled or discarded. On an idle agent it starts a run.

`message` may be a binary or a `Nous.Message`. Returns immediately.

Contrast `inject/2`, which queues to the same place and never starts a run.
The moduledoc has a worked example of the difference.

---

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