Nous.Plugins.SubAgent (nous v0.17.2)

Copy Markdown View Source

Plugin that enables agents to delegate tasks to specialized sub-agents.

Provides two tools:

  • delegate_task — run a single sub-agent for one task (sequential)
  • spawn_agents — run multiple sub-agents in parallel via Task.Supervisor

Usage

agent = Agent.new("openai:gpt-4",
  plugins: [Nous.Plugins.SubAgent],
  deps: %{
    sub_agent_templates: %{
      "researcher" => Agent.new("openai:gpt-4o-mini",
        instructions: "You are a research specialist. Find accurate information."
      ),
      "coder" => Agent.new("openai:gpt-4",
        instructions: "You are a coding specialist. Write clean Elixir code."
      )
    }
  }
)

Templates

Pre-configured agent templates can be provided via deps[:sub_agent_templates]. Templates can be either %Nous.Agent{} structs or config maps:

# Using Agent structs (recommended)
"researcher" => Agent.new("openai:gpt-4o-mini", instructions: "Research specialist")

# Using config maps (legacy)
"researcher" => %{model: "openai:gpt-4o-mini", instructions: "Research specialist"}

Both delegate_task and spawn_agents can reference templates by name or provide inline model/instructions for ad-hoc sub-agents.

Parallel Configuration

Configure concurrency and timeout for spawn_agents via deps:

  • :parallel_max_concurrency — max concurrent sub-agents (default: 5)
  • :parallel_timeout — per-task timeout in ms (default: 120_000)

Bounds on what the model may spawn

The tool arguments are LLM-controlled, so three limits bound them. All three are read from deps, inherit across delegation like confinement (a nested sub-agent cannot loosen them), and refuse rather than silently adjust:

  • :sub_agent_allowed_models — model strings an inline model argument may name (templates are developer-configured and are not checked). Default: the parent agent's own model only. A model outside the list is rejected with a tool error, never substituted.
  • :sub_agent_max_tasks — most tasks one spawn_agents call may carry (default: 10). An over-cap call is rejected whole, not truncated.
  • :sub_agent_max_depth — how deep delegation may nest (default: 2: the root agent may delegate, its sub-agent may delegate once more, and that sub-sub-agent may not). The current depth travels as :sub_agent_depth.

Deps Propagation

Two different things travel from parent to sub-agent, with opposite defaults:

Data deps (database handles, API keys, signed URLs, ...) are forwarded ONLY on explicit opt-in. Sub-agent prompts are LLM-controlled and any tool the sub-agent can call has access to its ctx.deps — so secrets placed in the parent's deps would otherwise be one prompt-injected sub-agent task away from exfiltration. Opt in by listing the keys to share, or request "everything except plugin internals" via the :all shortcut:

deps: %{
  sub_agent_templates: %{...},
  database: MyApp.Repo,
  api_key: "sk-...",
  # Pick one:
  sub_agent_shared_deps: [:database]   # share only :database
  # sub_agent_shared_deps: :all        # share everything except internals
}

Confinement and execution policy ALWAYS inherit, regardless of :sub_agent_shared_deps: the parent's :workspace_root and :session_id deps, its effective sandbox policy, its permission policy, and its approval handler all travel to every sub-agent. Withholding them would not protect a secret — it would silently widen what a delegated agent may do (e.g. re-rooting PathGuard at File.cwd!() or dropping the approval gate). A sub-agent template may narrow the inherited sandbox/permissions, never widen them.

Two boundaries of that inheritance to know about: hooks do not inherit — a sub-agent runs only the hooks its own template declares, so an operator's :pre_tool_use guard on the parent does not gate delegated tool calls (deny tools via :permissions, which does inherit, when that matters). And a parent with no policy at all (sandbox/permissions unset, no approval handler) delegates the same unbounded capability set it has itself — inheritance bounds sub-agents by the parent's policy, it does not invent one.

Security note: prior to v0.14.4 the default was the equivalent of :all. Upgrading is a behaviour change: if your existing sub-agent flows relied on inheriting parent deps, set sub_agent_shared_deps: :all to restore the old behaviour.