Tools are the things an agent can do beyond generating text. Condukt ships with a small set of file and shell tools, plus a behaviour for adding your own.
Built-in tool sets
def tools, do: Condukt.Tools.coding_tools() # Read, Bash, Edit, Write, Glob, Grep
def tools, do: Condukt.Tools.read_only_tools() # Read, Bash, Glob, GrepYou can mix the helpers with extras:
def tools do
Condukt.Tools.read_only_tools() ++ [MyApp.Tools.Weather]
endBuilt-in tools
| Tool | Description |
|---|---|
Condukt.Tools.Read | Read file contents. Supports images. |
Condukt.Tools.Bash | Run a shell command via bash -c. |
Condukt.Tools.Command | Run one trusted executable without shell parsing. |
Condukt.Tools.Edit | Surgical file edits using find and replace. |
Condukt.Tools.Write | Create or overwrite files. |
Condukt.Tools.Glob | Find files by glob pattern. |
Condukt.Tools.Grep | Search file contents by regex. |
Sandboxes
Built-in tools that touch the filesystem or spawn processes route every call
through the active Condukt.Sandbox. The default sandbox,
Condukt.Sandbox.Local, talks to the host filesystem. The
Condukt.Sandbox.Virtual sandbox runs against an in-memory virtual
filesystem and a Rust-implemented bash interpreter, with no host process
spawning by default. The same agent definition works with either.
See the Sandbox guide for details, including how to pick a
sandbox at start_link/1 time and how custom sandboxes plug in.
Scoped command grants
Condukt.Tools.Command is a safer alternative to Bash when you want to
expose a single executable without giving the model a full shell. It also
lets you attach trusted environment variables that the model never sees.
Session secrets configured with :secrets are merged into that environment.
Command does not currently route through the sandbox: it runs the
configured executable directly on the host with the trusted env you provide.
That is intentional. The point of Command is the explicit allowlist on the
host side, and it is meant for cases where the host operator wants to grant a
specific tool independently of the agent's general filesystem isolation.
defmodule MyApp.ReviewAgent do
use Condukt
@impl true
def tools do
[
Condukt.Tools.Read,
{Condukt.Tools.Command, command: "git"},
{Condukt.Tools.Command,
command: "gh",
env: [GH_TOKEN: System.fetch_env!("GH_TOKEN")]}
]
end
endYou can also resolve the token through a secret provider and keep the tool definition free of plaintext values:
MyApp.ReviewAgent.start_link(
secrets: [
GH_TOKEN: {:one_password, "op://Engineering/GitHub/token"}
]
)See the Secrets guide for provider-backed configuration and redaction behavior.
Each scoped command tool accepts:
argsis an array of strings passed directly to the executablecwdoverrides the agent's working directory for this calltimeoutcaps execution time in seconds
Defining a custom tool
Implement Condukt.Tool:
defmodule MyApp.Tools.Weather do
use Condukt.Tool
@impl true
def name, do: "get_weather"
@impl true
def description, do: "Gets the current weather for a location"
@impl true
def parameters do
%{
type: "object",
properties: %{
location: %{type: "string", description: "City name"}
},
required: ["location"]
}
end
@impl true
def call(%{"location" => location}, _context) do
case WeatherAPI.get(location) do
{:ok, data} -> {:ok, "Temperature: #{data.temp}F"}
{:error, reason} -> {:error, reason}
end
end
endThe second argument to call/2 is a context map that includes:
:agentis the agent PID:agent_moduleis the agent module for the session:sandboxis the activeCondukt.Sandboxstruct:cwdis the project working directory (use:sandboxfor any file or command work;:cwdis for resolving project-relative paths that aren't themselves I/O operations):secretscontains resolved session secrets for trusted tools:optsis the keyword list from{Module, opts}
Sandbox-aware tools
If your tool reads or writes files, or runs subprocesses, route through the
Condukt.Sandbox.* facade rather than calling File.*, System.cmd/3, or
MuonTrap.cmd/3 directly. Direct calls bypass the sandbox and break the
ability to swap one in.
defmodule MyApp.Tools.LineCount do
use Condukt.Tool
alias Condukt.Sandbox
@impl true
def name, do: "line_count"
@impl true
def description, do: "Counts lines in a file"
@impl true
def parameters do
%{
type: "object",
properties: %{path: %{type: "string"}},
required: ["path"]
}
end
@impl true
def call(%{"path" => path}, %{sandbox: sandbox}) do
case Sandbox.read(sandbox, path) do
{:ok, content} -> {:ok, content |> String.split("\n") |> length()}
{:error, reason} -> {:error, "cannot read #{path}: #{inspect(reason)}"}
end
end
endTools that touch unrelated systems (HTTP APIs, databases, in-process state) have nothing to sandbox and can do their I/O directly.
Inline tools
Use Condukt.tool/1 for one-off workflows where defining a module would add
more ceremony than value. Inline tools work anywhere a module tool works,
including an agent's tools/0 callback and anonymous Condukt.run/2 calls.
weather =
Condukt.tool(
name: "weather",
description: "Returns the weather for a city",
parameters: %{
type: "object",
properties: %{city: %{type: "string"}},
required: ["city"]
},
call: fn %{"city" => city}, _context ->
{:ok, "72F in #{city}"}
end
)
{:ok, response} =
Condukt.run("What is the weather in Berlin?",
tools: [weather]
)The callback receives the same context map as module tools. If it touches the
filesystem or runs commands, use context.sandbox through Condukt.Sandbox.
Parameterized tools
Tools can be added more than once with different options. The name/1,
description/1, and parameters/1 callbacks receive those options:
defmodule MyApp.Tools.Database do
use Condukt.Tool
@impl true
def name(opts), do: "query_#{opts[:table]}"
@impl true
def description(opts), do: "Query the #{opts[:table]} table"
@impl true
def parameters(_opts) do
%{type: "object", properties: %{q: %{type: "string"}}, required: ["q"]}
end
@impl true
def call(args, context) do
table = context.opts[:table]
{:ok, MyApp.Repo.query!(table, args["q"])}
end
end
# In the agent:
def tools do
[
{MyApp.Tools.Database, table: "users"},
{MyApp.Tools.Database, table: "orders"}
]
endReturning results
call/2 should return:
{:ok, value}for success. Strings, maps, and lists are all fine. Non binary values are JSON encoded before being sent to the LLM.{:error, reason}for failures. The error is reported back to the model so it can recover.