Current section

Files

Jump to
condukt lib condukt.ex
Raw

lib/condukt.ex

defmodule Condukt do
@moduledoc """
A framework for building AI agents in Elixir.
Condukt treats AI agents as first-class OTP processes that can
reason, use tools, and orchestrate complex workflows.
## Defining an Agent
defmodule MyApp.ResearchAgent do
use Condukt
@impl true
def tools do
[
Condukt.Tools.Read,
Condukt.Tools.Bash
]
end
end
## Running an Agent
{:ok, agent} = MyApp.ResearchAgent.start_link(
api_key: "sk-...",
system_prompt: \"\"\"
You are a research assistant that helps users find information.
Be thorough and cite your sources.
\"\"\"
)
{:ok, response} = Condukt.run(agent, "What's new in Elixir 1.18?")
## Streaming Responses
Condukt.stream(agent, "Explain OTP")
|> Stream.each(fn
{:text, chunk} -> IO.write(chunk)
{:tool_call, name, _id, _args} -> IO.puts("\\nCalling: \#{name}")
{:tool_result, _id, result} -> IO.puts("Result: \#{inspect(result)}")
:done -> IO.puts("\\nDone!")
end)
|> Stream.run()
## Core Concepts
- **Session** - A GenServer managing conversation state and the agent loop
- **Message** - User, assistant, or tool result messages in the conversation
- **Tool** - A capability the agent can invoke (read files, run commands, etc.)
- **Provider** - An LLM backend (Anthropic, OpenAI, Ollama, etc.)
- **Event** - Notifications during agent execution for streaming/UI
"""
# ============================================================================
# Behaviour Definition
# ============================================================================
@doc """
Returns the default system prompt for this agent.
This can be overridden at `start_link/1` via the `:system_prompt` option.
If neither is provided, the agent will have no system prompt.
"""
@callback system_prompt() :: String.t() | nil
@doc """
Returns the list of tools this agent can use.
"""
@callback tools() :: [module() | {module(), keyword()}]
@doc """
Returns the model identifier.
Uses ReqLLM format: "provider:model", e.g., "anthropic:claude-sonnet-4-20250514"
"""
@callback model() :: String.t()
@doc """
Returns the default thinking level.
"""
@callback thinking_level() :: :off | :minimal | :low | :medium | :high
@doc """
Initializes agent state from options.
"""
@callback init(keyword()) :: {:ok, term()} | {:stop, term()}
@doc """
Handles events during execution.
"""
@callback handle_event(term(), term()) :: {:noreply, term()} | {:stop, term(), term()}
@optional_callbacks [system_prompt: 0, tools: 0, model: 0, thinking_level: 0, init: 1, handle_event: 2]
# ============================================================================
# __using__ Macro
# ============================================================================
defmacro __using__(_opts) do
quote location: :keep do
@behaviour Condukt
import Condukt.Operation, only: [operation: 2]
Module.register_attribute(__MODULE__, :condukt_operations, accumulate: true)
@before_compile Condukt.Operation
# Default implementations
@impl Condukt
def system_prompt, do: nil
@impl Condukt
def tools, do: []
@impl Condukt
def model, do: "anthropic:claude-sonnet-4-20250514"
@impl Condukt
def thinking_level, do: :medium
@impl Condukt
def init(opts), do: {:ok, opts}
@impl Condukt
def handle_event(_event, state), do: {:noreply, state}
defoverridable system_prompt: 0, tools: 0, model: 0, thinking_level: 0, init: 1, handle_event: 2
@doc """
Starts the agent process.
## Options
- `:api_key` - API key for the LLM provider
- `:model` - Override the default model (format: "provider:model")
- `:base_url` - Override the provider's default base URL
- `:system_prompt` - System prompt for the agent
- `:load_project_instructions` - Auto-load `AGENTS.md`, `CLAUDE.md`, and local skills from the project root (default: `true`)
- `:thinking_level` - Override the thinking level
- `:cwd` - Working directory for tools (default: File.cwd!())
- `:session_store` - Session store module or `{module, opts}` tuple
- `:compactor` - Compactor module or `{module, opts}` tuple
(see `Condukt.Compactor`)
- `:name` - GenServer registration name
Plus all standard GenServer options.
"""
def start_link(opts \\ []) do
Condukt.Session.start_link(__MODULE__, opts)
end
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
type: :worker,
restart: :permanent
}
end
end
end
# ============================================================================
# Public API
# ============================================================================
@doc """
Runs a prompt and returns the final response.
## Options
- `:timeout` - Max time in ms (default: 300_000)
- `:max_turns` - Max tool use cycles (default: 50)
- `:images` - List of images to include (see Image module)
## Examples
{:ok, response} = Condukt.run(agent, "Hello!")
"""
defdelegate run(agent, prompt, opts \\ []), to: Condukt.Session
@doc """
Streams a prompt, yielding events as they occur.
## Events
- `{:text, chunk}` - Text chunk from LLM
- `{:thinking, chunk}` - Thinking/reasoning chunk
- `{:tool_call, name, id, args}` - Tool being called
- `{:tool_result, id, result}` - Tool result
- `{:error, reason}` - Error occurred
- `:agent_start` - Agent started processing
- `:agent_end` - Agent finished
- `:turn_start` - New LLM turn starting
- `:turn_end` - Turn completed
- `:done` - Stream complete
"""
defdelegate stream(agent, prompt, opts \\ []), to: Condukt.Session
@doc """
Returns the conversation history.
"""
defdelegate history(agent), to: Condukt.Session
@doc """
Clears conversation history.
"""
defdelegate clear(agent), to: Condukt.Session
@doc """
Aborts current operation.
"""
defdelegate abort(agent), to: Condukt.Session
@doc """
Runs the configured compactor against the conversation history.
See `Condukt.Compactor` for details and built-in strategies.
"""
defdelegate compact(agent), to: Condukt.Session
@doc """
Injects a message mid-execution (steering).
This message will be delivered after the current tool completes,
and remaining tool calls will be skipped.
"""
defdelegate steer(agent, message), to: Condukt.Session
@doc """
Queues a follow-up message.
This message will be delivered when the agent finishes its current work.
"""
defdelegate follow_up(agent, message), to: Condukt.Session
end