# `Planck.Agent.AgentSpec`
[🔗](https://github.com/alexdesousa/planck/blob/v0.1.0/lib/planck/agent/agent_spec.ex#L1)

Static, serializable agent definition.

An `AgentSpec` is the shape used both by team members loaded from disk (via
`Planck.Agent.Team`) and by the orchestrator's `spawn_agent` tool when it
creates workers at runtime. It contains only serializable fields — no
`execute_fn`. Tool wiring is merged in programmatically before starting the
agent.

## Member-entry JSON schema

This is the format for a single entry in a team's `members` list:

    {
      "type":          "builder",
      "name":          "Builder Joe",
      "description":   "Writes and edits code.",
      "provider":      "anthropic",
      "model_id":      "claude-sonnet-4-6",
      "system_prompt": "You are an expert builder.",
      "opts":          { "temperature": 0.7 },
      "tools":         ["read", "write", "edit", "bash"],
      "skills":        ["code_review", "refactor"]
    }

Required fields are `type`, `provider`, `model_id`. Valid providers are
derived from `Planck.AI.Model.providers/0`.

`system_prompt` is either inline text or a path to a `.md`/`.txt` file
resolved relative to a caller-provided `base_dir`. `tools` and `skills`
are lists of names resolved against caller-provided pools at start time
(see `to_start_opts/2`). When `skills` is non-empty, their descriptions
are appended to the system prompt via `Planck.Agent.Skill.system_prompt_section/1`.

## Construction

    iex> AgentSpec.from_map(%{
    ...>   "type" => "builder",
    ...>   "provider" => "ollama",
    ...>   "model_id" => "llama3.2",
    ...>   "system_prompt" => "Build things."
    ...> })
    {:ok, %AgentSpec{...}}

    iex> AgentSpec.from_list(list_of_maps, base_dir: "/path/to/team")
    [%AgentSpec{...}, ...]

# `t`

```elixir
@type t() :: %Planck.Agent.AgentSpec{
  base_url: String.t() | nil,
  compactor: String.t() | nil,
  description: String.t() | nil,
  model_id: String.t(),
  name: String.t(),
  opts: keyword(),
  provider: atom(),
  skills: [String.t()],
  system_prompt: String.t(),
  tools: [String.t()],
  type: String.t()
}
```

- `:type` — role identifier used for registry lookups and tool targeting (e.g. `"builder"`)
- `:name` — human-readable label shown to other agents via `list_team`; defaults
  to `type` when not provided or empty
- `:description` — one-line purpose shown to other agents via `list_team`
- `:provider` — LLM provider atom (e.g. `:anthropic`, `:ollama`)
- `:model_id` — model identifier within the provider (e.g. `"claude-sonnet-4-6"`)
- `:system_prompt` — system prompt text sent to the model at the start of every turn
- `:opts` — provider-specific options forwarded to the LLM call (e.g. `temperature:`)
- `:tools` — tool names to resolve from a `tool_pool:` at start time (e.g. `["read", "bash"]`)
- `:skills` — skill names to resolve from a `skill_pool:` at start time; when
  non-empty, their descriptions are appended to `system_prompt` in `to_start_opts/2`
- `:base_url` — base URL of the model server for local providers that run multiple
  instances (e.g. `"http://localhost:11434"` for a specific Ollama server). When
  `nil`, the provider's default URL is used.
- `:compactor` — fully-qualified module name of a sidecar compactor for this agent,
  e.g. `"MySidecar.Compactors.Builder"`. The module must implement `compact/2`.
  planck_headless resolves this via `Planck.Agent.Sidecar.compactor_for/1` when
  materialising the agent. `nil` means the default compactor is used.

# `from_list`

```elixir
@spec from_list(
  [map()],
  keyword()
) :: [t()]
```

Convert a list of maps (as decoded from JSON) into a list of `AgentSpec` structs.

Invalid entries are skipped with a warning; the rest are returned. Accepts
`base_dir:` for resolving relative `system_prompt` file paths. Defaults to
`File.cwd!()`.

# `from_map`

```elixir
@spec from_map(map(), Path.t()) :: {:ok, t()} | {:error, String.t()}
```

Convert a single map into an `AgentSpec` struct.

Returns `{:ok, spec}` or `{:error, reason}`. `system_prompt` values ending in
`.md` or `.txt` are treated as file paths and read from disk relative to
`base_dir`.

# `new`

```elixir
@spec new(keyword()) :: t()
```

Build an `AgentSpec` from a keyword list of validated fields.

`name` defaults to `type` when not provided or empty — every agent has a
human-readable label, and teams with multiple members of the same type are
forced to assign explicit names (via `Team.load/1`'s name-uniqueness check).

# `to_start_opts`

```elixir
@spec to_start_opts(
  t(),
  keyword()
) :: keyword()
```

Convert an `AgentSpec` to keyword options suitable for `Planck.Agent.start_link/1`.

Accepts optional overrides: `tools:`, `tool_pool:`, `skill_pool:`, `team_id:`,
`session_id:`, `available_models:`, `on_compact:`.

## Tool resolution

When `spec.tools` is non-empty, tool names are resolved against `tool_pool:` (a list
of `Tool.t()` structs). Unknown names are silently ignored. Any tools passed via
`tools:` are appended after the resolved ones. When `spec.tools` is empty, `tools:`
is used directly.

## Skill resolution

When `spec.skills` is non-empty, skill names are resolved against `skill_pool:` (a
list of `Skill.t()` structs). The resolved skills' descriptions are appended to
`spec.system_prompt` via `Planck.Agent.Skill.system_prompt_section/1`. Unknown
names are silently ignored. When `spec.skills` is empty, `system_prompt` passes
through unchanged.

## Examples

    iex> AgentSpec.to_start_opts(spec, tool_pool: [read_tool, bash_tool], team_id: "team-1")
    [id: "...", type: "builder", tools: [read_tool], ...]

---

*Consult [api-reference.md](api-reference.md) for complete listing*
