Skip to content

Toolnexus.Agents.Home.from_dir

Elixir · package toolnexus · SPEC §7E · elixir/lib/toolnexus/agents/home.ex

@spec from_dir(String.t(), keyword() | map()) :: Toolnexus.Agents.AgentDef.t()
def from_dir(dir, opts \\ [])
# opts: :does (routing description, default "persona agent from <dir>"),
# :name (default: dir's basename), :model (default "inherit"),
# :tools (extra tools alongside the memory builtin),
# :memory (set false to omit the memory tool — a read-only persona)

The directory IS the agent. from_dir/2 runs compose_soul/1 over dir for the frozen system-prompt snapshot, wires the memory tool over the same directory (unless :memory is false), and returns a Toolnexus.Agents.AgentDef — runnable with Toolnexus.Agents.run/3 or droppable into a parent’s :team like any other agent definition.

  • You have a persona’s files on diskAGENTS.md/SOUL.md/IDENTITY.md/… under one directory — and want a runnable agent without hand-assembling the soul string and the memory tool yourself.
  • You want the persona to remember things across sessions — the memory tool comes wired by default; pass memory: false for a read-only persona (a fixed identity that never writes).
  • You’re building several personas the same way — one directory convention, one call, per persona; naming and routing description both have sane defaults.

1. The smallest useful call — a directory with one file becomes a runnable agent

Section titled “1. The smallest useful call — a directory with one file becomes a runnable agent”
alias Toolnexus.Agents.Home
dir = Path.join(System.tmp_dir!(), "toolnexus_doc_from_dir_1_#{System.unique_integer([:positive])}")
File.rm_rf!(dir)
File.mkdir_p!(dir)
File.write!(Path.join(dir, "SOUL.md"), "You are Kestrel, a calm research agent.")
agent = Home.from_dir(dir)
true = agent.name == Path.basename(dir)
true = agent.spec[:does] == "persona agent from #{dir}"
true = agent.spec[:model] == "inherit"
true = agent.spec[:soul] == "## SOUL.md\n\nYou are Kestrel, a calm research agent."
# the memory tool rides along by default
true = Enum.any?(agent.spec[:uses][:tools], &(&1.name == "memory"))
IO.puts("ok: #{agent.name} composed with #{length(agent.spec[:uses][:tools])} tool(s)")

2. The realistic case — explicit options, and a read-only persona with memory: false

Section titled “2. The realistic case — explicit options, and a read-only persona with memory: false”
alias Toolnexus.Agents.Home
dir = Path.join(System.tmp_dir!(), "toolnexus_doc_from_dir_2_#{System.unique_integer([:positive])}")
File.rm_rf!(dir)
File.mkdir_p!(dir)
File.write!(Path.join(dir, "AGENTS.md"), "Answer tersely.")
named = Home.from_dir(dir, does: "terse Q&A", name: "terse-bot", model: "gpt-4o-mini")
true = named.name == "terse-bot"
true = named.spec[:does] == "terse Q&A"
true = named.spec[:model] == "gpt-4o-mini"
read_only = Home.from_dir(dir, memory: false)
false = Enum.any?(read_only.spec[:uses][:tools], &(&1.name == "memory"))
IO.puts("ok: #{named.name} (writable) vs #{read_only.name} (#{length(read_only.spec[:uses][:tools])} tools, read-only)")

3. The full surface — running the persona end to end, memory tool included

Section titled “3. The full surface — running the persona end to end, memory tool included”
alias Toolnexus.{Agents}
alias Toolnexus.Agents.Home
dir = Path.join(System.tmp_dir!(), "toolnexus_doc_from_dir_3_#{System.unique_integer([:positive])}")
File.rm_rf!(dir)
File.mkdir_p!(dir)
File.write!(Path.join(dir, "IDENTITY.md"), "You are Kestrel.")
extra_tool =
Toolnexus.define_tool(%{
name: "ping",
description: "health check",
execute: fn _args -> "pong" end
})
agent = Home.from_dir(dir, does: "demo persona", tools: [extra_tool])
true = Enum.map(agent.spec[:uses][:tools], & &1.name) |> Enum.sort() == ["memory", "ping"]
mock_transport = fn %{body: body} ->
msgs = body["messages"] || []
tool_msgs = Enum.filter(msgs, &(&1["role"] == "tool"))
resp =
if tool_msgs == [] do
%{
"choices" => [%{"message" => %{"role" => "assistant", "content" => nil, "tool_calls" => [
%{"id" => "c1", "type" => "function", "function" => %{"name" => "memory", "arguments" => ~s({"action":"add","text":"user likes concise replies"})}}
]}}],
"usage" => %{"prompt_tokens" => 4, "completion_tokens" => 2, "total_tokens" => 6}
}
else
%{"choices" => [%{"message" => %{"role" => "assistant", "content" => hd(tool_msgs)["content"]}}], "usage" => %{"prompt_tokens" => 2, "completion_tokens" => 1, "total_tokens" => 3}}
end
{:ok, %{status: 200, headers: %{}, body: resp}}
end
r = Agents.run(agent, %{transport: mock_transport}, "remember that I like concise replies")
true = r.status == "done"
true = String.contains?(r.text, "MEMORY.md")
true = File.read!(Path.join(dir, "MEMORY.md")) =~ "user likes concise replies"
IO.puts("ok: persona from #{Path.basename(dir)} wrote to its own MEMORY.md via the wired memory tool")
Field Type Default What it is
:does String.t() "persona agent from <dir>" Routing description.
:name String.t() the directory’s basename Agent name.
:model String.t() "inherit" LLM model.
:tools [Tool.t()] [] Extra tools alongside the memory builtin.
:memory boolean() true Set false to omit the memory tool (read-only persona).