ExAgent.Agent (ExAgent v0.3.0)

Copy Markdown View Source

GenServer that manages a single LLM agent.

Holds the provider struct in state and dispatches calls via the ExAgent.Provider behaviour. Contains the tool execution loop that automatically invokes tools when the LLM requests them.

State

The agent state includes:

  • provider - any struct implementing ExAgent.Provider
  • context - conversation history (ExAgent.Context)
  • tools - available tools for function-calling
  • skills - loadable skill definitions
  • active_skill - currently active skill (if any)

Options beyond the provider

  • :max_tool_iterations - ceiling on tool round-trips (default 10)
  • :max_history - keep at most this many messages; unbounded when unset

Summary

Functions

Sends a user message to the agent and returns the response.

Sends a user message asynchronously, returning a Task.

Sends a user message and returns a lazy Stream of the assistant's text chunks.

Returns a specification to start this module under a supervisor.

Returns the current conversation context.

Dynamically loads a skill into the agent.

Resets the agent's conversation context.

Starts an agent process linked to the current process.

Types

agent_opts()

@type agent_opts() :: [
  id: String.t(),
  provider: struct(),
  role: atom(),
  tools: [ExAgent.Tool.t()],
  skills: [ExAgent.Skill.t()],
  max_tool_iterations: pos_integer(),
  max_history: pos_integer(),
  name: GenServer.name()
]

state()

@type state() :: %{
  id: String.t(),
  provider: struct(),
  context: ExAgent.Context.t(),
  tools: [ExAgent.Tool.t()],
  skills: [ExAgent.Skill.t()],
  active_skill: ExAgent.Skill.t() | nil,
  built_in_tools: [atom()],
  max_tool_iterations: pos_integer(),
  max_history: pos_integer() | nil,
  base_system_prompt: String.t() | nil,
  status: :idle | :processing | :handed_off,
  reply_to: GenServer.from() | nil,
  task_ref: reference() | nil,
  pre_turn_context: ExAgent.Context.t() | nil
}

Functions

chat(agent, user_input, opts \\ [])

@spec chat(GenServer.server(), String.t(), keyword()) ::
  {:ok, ExAgent.Response.t()}
  | {:handoff, pid() | atom(), ExAgent.Context.t()}
  | {:error, term()}

Sends a user message to the agent and returns the response.

The agent appends the message to its context, evaluates skills, calls the LLM, and executes any requested tools in a loop until a final response or handoff is returned.

Options

  • :files - list of file attachments, each a map with :mime_type and either :data (binary) or :path (file path). Files become part of the conversation context and are sent to the LLM alongside the text message.
  • :built_in_tools - list of provider-specific built-in tools to enable for this message. Overrides the agent-level built_in_tools if provided. Examples: [:google_search], [:web_search], [:thinking].

Examples

ExAgent.Agent.chat(agent, "Describe this image",
  files: [%{path: "photo.jpg", mime_type: "image/jpeg"}])

ExAgent.Agent.chat(agent, "Search the web for Elixir news",
  built_in_tools: [:google_search])

chat_async(agent, user_input, opts \\ [])

@spec chat_async(GenServer.server(), String.t(), keyword()) :: Task.t()

Sends a user message asynchronously, returning a Task.

Accepts the same options as chat/3.

chat_stream(agent, user_input, opts \\ [])

@spec chat_stream(GenServer.server(), String.t(), keyword()) :: Enumerable.t()

Sends a user message and returns a lazy Stream of the assistant's text chunks.

Every turn is streamed, including one that requests tools: its tool calls are executed and the next turn opened, so a multi-turn answer costs one completion per turn and still ends with exactly one :done chunk.

The conversation context is committed when the stream is fully consumed, so the returned stream must be consumed (e.g. with Enum/Stream functions) for the agent to return to idle.

Never raises: a busy agent, a failed tool turn, and a mid-stream HTTP failure all arrive as a single ExAgent.Chunk with type: :done and an :error.

Examples

agent
|> ExAgent.Agent.chat_stream("Explain OTP step by step")
|> Enum.each(fn
  %ExAgent.Chunk{type: :text_delta, text: text} -> IO.write(text)
  _chunk -> :ok
end)

Or fold it back into the same response chat/3 returns:

{:ok, response} = agent |> ExAgent.Agent.chat_stream("Hi") |> ExAgent.collect()

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

get_context(agent)

@spec get_context(GenServer.server()) :: ExAgent.Context.t()

Returns the current conversation context.

load_skill(agent, skill)

@spec load_skill(GenServer.server(), ExAgent.Skill.t()) :: :ok

Dynamically loads a skill into the agent.

reset(agent)

@spec reset(GenServer.server()) :: :ok

Resets the agent's conversation context.

start_link(opts)

@spec start_link(agent_opts()) :: GenServer.on_start()

Starts an agent process linked to the current process.

Takes either :provider (a struct) or :role (an atom resolved through ExAgent.Roles), never both.