Skip to content

Proposal: Task-level tools and dynamic agents #107

Description

@mlund01

Problem

Squadron's unit of work assignment is the predefined agent: tools are declared on agent blocks, and tasks/missions list which agents may be used. The commander coordinates; agents do all the work.

In practice this paradigm is breaking down:

  • Agents aren't reusable. An agent is a fixed bundle of model + personality + tools + skills. That exact bundle is rarely right for more than one or two tasks, so configs accumulate near-duplicate agents that exist only to carry a particular tool list to a particular task.
  • The indirection adds friction. To give a task a capability, you must invent an agent for it, name it, give it a personality, and wire it into the mission's agents list — even when there's nothing agent-like about the need ("this task needs the playwright tools").
  • Tool intent lives in the wrong place. Which tools a task needs is a property of the task, not of some persona. Today that information is smeared across agent definitions.

We're simplifying the relationship between tasks and tools: tools get assigned at the task level (directly or via named groups), and an implicit dynamic agent per task carries those tools and does most of the work. Predefined agents remain fully supported for specialized cases — the commander decides when to delegate to one instead.

Proposal

1. tools on tasks

Tasks accept a tools = [...] attribute using the same reference syntax agents use today (builtins.*, plugins.*.all, mcp.*.*, tools.*):

mission "research" {
  commander { model = models.anthropic.claude_sonnet_4 }

  task "gather" {
    objective = "Collect pricing data from the target sites"
    tools     = [plugins.playwright.all, builtins.http.get]
  }

  task "summarize" {
    depends_on = [tasks.gather]
    objective  = "Summarize findings into a report"
    tools      = [mcp.filesystem.write_file]
  }
}

No agent block required anywhere in this mission.

2. toolset blocks — named tool groups

A top-level block that names a reusable group of tool refs:

toolset "browsing" {
  description = "Navigate and scrape the web"
  tools       = [plugins.playwright.all, builtins.http.get]
}

toolset "data_io" {
  tools = [mcp.filesystem.read_file, mcp.filesystem.write_file]
}

task "gather" {
  objective = "Collect pricing data"
  tools     = [toolsets.browsing, toolsets.data_io, builtins.utils.current_time]
}

Toolsets expand at config-load time (like .all does today). They are pure grouping — no model, no personality, no behavior. toolsets.* refs are also valid in agent tools lists, so predefined agents benefit too.

3. Dynamic agents

When a task declares tools, the runner materializes one implicit dynamic agent for that task, holding all of the task's tools. The commander sees it alongside any explicitly assigned agents and delegates to it via the existing call_agent/ask_agent flow.

  • Naming: a reserved, deterministic name (e.g. worker), consistent across events, debug logs (agent_<task>_worker.md), and persistence.
  • System prompt: derived from the task objective and mission context rather than a configured personality.
  • Commander awareness: the commander's system prompt lists the dynamic agent with its tool list, plus guidance: prefer the dynamic agent for general work; use specialized agents when their personality/skills fit the subtask.
  • Lifecycle: identical to today's agents — instantiated on first call_agent, session persisted, resumable, queryable via ask_agent.

4. Model selection

A mission-level default with per-task override:

mission "research" {
  commander { model = models.anthropic.claude_sonnet_4 }

  agent_defaults {
    model     = models.anthropic.claude_sonnet_4   # dynamic agents use this...
    reasoning = "low"                              # (optional)
  }

  task "gather" {
    objective = "Collect pricing data"
    tools     = [toolsets.browsing]
    model     = models.anthropic.claude_haiku      # ...unless the task overrides
  }
}

If no agent_defaults block exists, dynamic agents inherit the commander's model.

Coexistence with predefined agents

Nothing about agent blocks changes. A task may have tools, agents, or both:

task "gather" {
  objective = "Collect pricing data; consult the domain expert for ambiguous SKUs"
  tools     = [toolsets.browsing]
  agents    = [agents.pricing_expert]
}

The commander decides at runtime which to use for each piece of work.

Validation rules

  • Task tools refs validated the same way agent tools are (reuse ValidateToolReferences).
  • A task must have at least one work source: tools, task-level agents, or mission-level agents (relaxes today's "agents required" rule).
  • The dynamic agent's reserved name must not collide with any global or mission-scoped agent name.
  • toolset labels live in their own namespace; refs to unknown toolsets are config errors; toolsets cannot reference other toolsets (v1 — keeps expansion non-recursive).
  • agent_defaults is optional; at most one per mission; task-level model is only meaningful when the task has tools.

Implementation sketch

Config (config/)

  • New toolset block, parsed in Stage 3 alongside custom tool blocks (needs plugins + mcp context); expose a toolsets.* HCL namespace; expansion happens in BuildToolsMap or just before it.
  • Task gains Tools []string and Model string (config/mission.go); Mission gains an AgentDefaults block.
  • Validation per the rules above.

Runtime (agent/, mission/)

  • BuildToolsMap (config/tools_builder.go) is already parametric — dynamic agents reuse it unchanged.
  • AgentManager (agent/agent_manager.go) learns to construct the dynamic agent: synthesize an agent config (reserved name, resolved model, task tools, objective-derived prompt) and feed it through the existing agent.New() path.
  • Commander prompt (agent/commander.go, agent/internal/prompts/prompts.go): include the dynamic agent with its tool list and selection guidance.

Persistence/resume (store/)

  • Dynamic agent sessions persist under the deterministic name; resume works through the existing session-restore path. Likely no schema change; if one is needed, it goes through a numbered migration.

Docs

  • README + extracted docs (squadron docs) + CLAUDE.md sections on agents, missions, and the HCL reference.

Out of scope (follow-ups)

  • Commander-chosen tool subsets per spawn (multiple dynamic agents per task with different tool slices).
  • Nested/composed toolsets.
  • Per-toolset settings or instructions (that's what skills are; relationship revisited later).
  • Any deprecation of agent-level tools.

Open questions

  1. Reserved name for the dynamic agent — worker? agents.dynamic? Shows up in events, debug filenames, and the command center UI.
  2. agent_defaults block name and placement — top of mission vs. inside commander?
  3. Should the dynamic agent get the mission's memory/scratchpad file tools automatically (matching current agent behavior)? Presumably yes.
  4. Should task-level tools also be grantable to predefined agents assigned to that task (tool merging), or do predefined agents keep exactly their own tools? Proposal assumes the latter for v1.
  5. Iterator interaction: parallel iterations each get their own dynamic agent instance (mirrors current per-iteration agent instancing) — confirm.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions