Agents + subagents (v0.4)

Copy Markdown View Source

A subagent is a synchronous nested Loop.run/2 — a model delegating focused work (exploration, planning, verification) to a fresh conversation with its own message history. The parent only pays for the subagent's final text, not its intermediate steps.

v0.4 turns subagents from "spawn another loop" into "isolated, observable, configurable sub-runtimes" via three additions:

  1. Custom agent definitions in markdown + YAML frontmatter.
  2. Optional git-worktree isolation with safety checks.
  3. Sidechain transcripts persisted to disk.

Spawning

The model invokes the SpawnAgent builtin tool:

spawn_agent(
  prompt: "explore the auth module and list public functions",
  agent: "explore",
  max_iterations: 8
)

agent is optional — if set, ex_athena resolves it against the agent definition catalog and applies its frontmatter (tools, permissions, mode, model, provider, isolation) to the sub-loop opts. Explicit tools / system_prompt / max_iterations args still override.

Agent definitions

Files at <cwd>/.exathena/agents/<name>.md, ~/.config/ex_athena/agents/<name>.md, or shipped builtins in priv/agents/. Resolution: builtin → user → project, later sources overriding earlier ones.

Frontmatter

---
name: explore
description: Read-only fast investigation
model: claude-haiku-4-5
provider: anthropic
tools: [read, glob, grep, web_fetch]
permissions: plan
mode: react
isolation: in_process
---

You are a read-only research assistant. Walk the codebase and report
findings concisely. Do not modify any files.
FieldMeaning
nameRequired. The string the model passes as agent: "...".
descriptionRequired. One sentence; surfaces in tool docs.
modelOptional. Overrides the parent's :model.
providerOptional. Overrides the parent's :provider.
toolsOptional. List of tool names. Overrides parent's tools.
permissionsOptional. One of :plan / :default / :accept_edits / :trusted / :bypass_permissions.
modeOptional. :react / :plan_and_solve / :reflexion. Default :react.
isolationOptional. :in_process (default) or :worktree.

The body becomes a system-prompt addendum appended to whatever the parent's system prompt was.

Builtin definitions

Three ship in priv/agents/:

  • general — full-tool default. Matches the prior SpawnAgent behaviour. Use when you need a sub-loop with the parent's full kit.
  • explore — read-only investigation. Tools: read, glob, grep, web_fetch. Permissions: :plan. The right pick for "summarise this codebase" or "find the bug" sub-tasks.
  • plan — analysis-only with writes restricted to .exathena/plans/*.md. Mode: plan_and_solve. The right pick for "produce a written plan I'll review before letting an agent execute".

Project + user definitions override builtins by name; e.g. dropping a <cwd>/.exathena/agents/explore.md lets a project teach explore how its codebase is organised.

Worktree isolation

Setting isolation: :worktree in an agent definition asks the runtime to create a git worktree for the subagent. Three safety checks run before creation:

  1. git is on PATH.
  2. cwd is inside a git work tree (git rev-parse --is-inside-work-tree).
  3. The work tree is clean (git status --porcelain returns empty).

If any check fails, the subagent transparently falls back to :in_process and the fallback reason (:no_git, :not_a_repo, :dirty_tree, :create_failed) flows through the SubagentStart hook payload.

When all checks pass:

  • A new branch is created: ex_athena/<parent_session_id>-<agent-name>-<n>
  • The worktree lives at ~/.cache/ex_athena/worktrees/<parent_session_id>/<agent-name>-<n>
  • The sub-loop's :cwd becomes the worktree path.

After the subagent finishes:

StateAction
Worktree has uncommitted changesKept. Path + branch surface in spawn result's ui_payload.
Worktree cleangit worktree remove --force cleans up.

ExAthena.Agents.WorktreeSweeper is a one-shot at boot under the application supervisor that runs git worktree prune and removes cache entries older than 7 days.

Permission reentrancy

The runtime invokes git directly via System.cmd/3not through Tools.Bash — so worktree creation/teardown bypasses the parent's permission gate. Without this, a parent in :plan mode could never spawn a worktree-isolated subagent (the bash calls would be denied), defeating the point.

Sidechain transcripts

Every subagent run writes its full transcript to:

<cwd>/.exathena/sessions/<parent_session_id>/sidechains/<subagent_id>.jsonl

The file contains one JSON object per line: prompt, opts (best-effort inspect-serialised — closures and PIDs render as strings), and the final result with text, finish_reason, iterations, tool_calls_made, duration_ms, cost_usd.

The parent only sees the subagent's text. The full conversation lives here for review, replay, and debugging.

Hooks

SubagentStart and SubagentStop (see hooks reference):

ExAthena.run("...",
  tools: :all,
  hooks: %{
    SubagentStart: [fn p, _ ->
      Logger.info("subagent #{p.subagent_id} started: agent=#{p.agent} isolation=#{inspect(p.isolation)}")
      :ok
    end],
    SubagentStop: [fn p, _ ->
      Logger.info("subagent #{p.subagent_id} stopped: outcome=#{p.outcome}")
      :ok
    end]
  })

SubagentStart payload includes the resolved agent name and isolation decision. SubagentStop includes the finalized isolation state — :worktree_kept, :worktree_removed, :worktree_error, or {:in_process, reason}.

Spawn result UI payload

SpawnAgent returns the PR3b 3-tuple {:ok, text, ui} where ui is:

%{
  kind: :subagent,
  payload: %{
    subagent_id: "subagent_2KLm9P",
    iterations: 5,
    tool_calls_made: 12,
    cost_usd: 0.014,
    duration_ms: 8230,
    isolation: {:worktree_kept, %{path: "...", branch: "ex_athena/...", parent_cwd: "..."}}
  }
}

Hosts watching the loop's :tool_ui event get this on every spawn completion — useful for rendering "subagent ran for 8.2s, kept worktree at /path" cards in a TUI or LiveView UI.

Worked examples

Investigate before changing

# Parent agent uses `explore` to gather context, then `general` to act.
ExAthena.run("refactor the auth flow",
  tools: :all,
  cwd: project_root,
  assigns: %{
    spawn_agent_opts: [
      provider: :ollama,
      model: "qwen2.5-coder",
      memory: false  # subagents don't re-load AGENTS.md
    ]
  })

The model's first move is typically:

spawn_agent(prompt: "List every file that imports MyApp.Auth", agent: "explore")

Plan-then-execute pipeline

# Define a custom workflow agent that uses `plan` then `general`.
File.write!(".exathena/agents/refactor.md", """
---
name: refactor
description: Two-phase refactor with planning + execution
mode: plan_and_solve
permissions: accept_edits
isolation: worktree
---

You are a refactoring specialist. Phase 1: read the relevant files
and produce a plan. Phase 2: implement the plan. Verify with tests.
""")

ExAthena.run("refactor the auth flow",
  tools: :all,
  assigns: %{spawn_agent_opts: [provider: :anthropic, model: "claude-sonnet-4-6"]})

The model spawns:

spawn_agent(prompt: "Refactor MyApp.Auth into MyApp.Identity", agent: "refactor")

The refactor agent runs in an isolated worktree (clean tree assumed). On completion, the parent gets the final summary text and a UI payload with the worktree path/branch — the human reviews and merges.

See also