# Changelog

## v0.1.0

### execute_fn receives agent_id

- `Tool.execute_fn` type updated to `(agent_id, tool_call_id, args)` — every
  tool now receives the calling agent's id as the first argument.
- `ask_agent` drops the `own_id` closure capture — reads from `agent_id`.
- `spawn_agent` drops the `orchestrator_id` closure capture — reads from `agent_id`.
- `worker_tools/3` (was `/4`) and `orchestrator_tools/6` (was `/7`) — each lost
  one parameter as a result.
- `list_models` marks the caller's current model with `current: true` via a
  dynamic `Agent.get_state` lookup — works correctly when granted to workers.
- `AIBehaviour` — added `get_model/3` callback for base-url-aware lookups.

### Explicit agent targeting

- `ask_agent`, `delegate_task`, `destroy_agent`, `interrupt_agent` — replaced
  the three optional `type`/`name`/`id` fields with a required `identifier`
  string and a required `identifier_type` enum (`"type"`, `"name"`, `"id"`).
  The LLM can no longer omit all three and silently fail to target an agent.

### spawn_agent hardening

- `base_url` is now always required in `spawn_agent` (cloud providers may pass
  a placeholder; only ollama/llama_cpp use it).
- `spawn_agent` execute_fn refactored into focused helpers: `validate_base_url`,
  `resolve_spawn_model`, `build_spawn_start_opts`, `filter_granted`.

### Tool output truncation

- Tool results are now capped at 2 000 lines **or** 50 KB (whichever is
  reached first) before being stored in the session. Outputs that exceed either
  limit are truncated and suffixed with `\n[output truncated]`. Both limits are
  always enforced — line truncation is applied first, then byte truncation on
  the result.

### Compactor fixes

- `estimate_tokens` now counts `{:tool_call, id, name, args}` content parts
  (previously ignored, causing systematic underestimates).
- `compact_local` filters all `{:custom, :summary}` messages from `old` before
  calling `summarize/2` — only messages since the last checkpoint are
  summarised, preventing the previous checkpoint from bloating the request.
- `format_history` strips thinking blocks and truncates tool results to 2 000
  chars — keeps the summarisation input small without losing signal.

### Queued message follow-up fix

- A user message sent while the orchestrator is executing tools now correctly
  triggers a dedicated follow-up turn after all tools complete. Previously,
  `do_run_llm` called during tool continuation advanced `stream_start` past the
  queued message, so `maybe_turn_start` found no pending input.

### Runtime model switching

- `Agent.change_model/2` — replaces the model in the agent's GenServer state
  for subsequent LLM turns without affecting the current conversation history
  or status.

### AGENTS.md prepending for all agents

- `Tools.prepend_agents_md/2` is now public — walks up from `cwd` to the
  nearest `.git` root, reads `AGENTS.md` if found, and prepends its content to
  the given system prompt. Returns the prompt unchanged when no file is found or
  `cwd` is empty.
- `orchestrator_tools/7` — added `cwd` parameter (default `""`); passed into
  the `spawn_agent` closure so dynamically spawned workers inherit the same
  project context.
- `spawn_agent` tool — prepends `AGENTS.md` to the worker's system prompt before
  starting the agent process; `cwd` is stored in the new agent's state.
- `Agent.t` — added `cwd: String.t()` field (default `""`); set from start opts.

### Skills — explicit `load_skill` / `list_skills` tools

- `Skill.load_skill_tool/1` — builds a `load_skill` tool as a closure over the
  skill pool; automatically injected by `AgentSpec.to_start_opts/2` for every
  agent when `skill_pool:` is non-empty. No TEAM.json declaration needed.
- `Skill.list_skills_tool/1` — builds a `list_skills` tool returning all
  available skill names and descriptions. Opt-in: add `"list_skills"` to an
  agent's TEAM.json `"tools"` array to enable autonomous skill discovery.
- `Skill.system_prompt_section/1` updated: no longer includes file paths or
  resources dir; instructs agents to use `load_skill` instead of `read`.
- `AgentSpec.resolve_tools/2` updated: automatically appends `load_skill_tool`
  when `skill_pool:` is non-empty, regardless of `spec.skills`.

### Inter-agent tools — deadlock detection + improvements

- `ask_agent/2` — now accepts `own_id` for deadlock detection; before blocking,
  registers `{:waiting, own_id} → target_id` in `Planck.Agent.Registry` (auto-
  cleared on task exit) and checks for a circular wait chain; returns a clear
  error instead of deadlocking if a cycle is detected.
- `worker_tools/4` — added `own_id` parameter (passed to `ask_agent` for cycle
  detection); callers must now supply the agent's own id.
- `orchestrator_tools/6` — added `grantable_skills` parameter so skills can be
  granted to dynamically spawned workers via `spawn_agent`.
- `spawn_agent` — spawned workers now receive a `sender` identity so the
  orchestrator knows which worker replied via `send_response`.
- `list_team/1` — added `verbose: boolean` parameter; verbose mode includes tool
  names and model for each team member.
- `list_models/1` — output now includes `base_url` for each model so the LLM
  can pass the correct base_url when calling `spawn_agent`.
- Agent `init` broadcasts `:worker_spawned` on the session PubSub topic when
  a worker with a `delegator_id` starts, enabling UIs to refresh the agent list.
- Non-blocking tool execution: `handle_continue({:execute_tools})` now spawns
  each tool as a supervised fire-and-forget task; results collected via
  `handle_info({:tool_done})`; the GenServer loop stays free for abort/prompt
  during tool execution.
- `abort/1` changed from cast to call; blocks until the agent is idle, closing
  the race condition between abort and subsequent prompt/rewind calls.
- `cost: float()` added to agent state; accumulated from model rates on each
  `:done` event; persisted to session metadata; broadcast in `:usage_delta`.
- `Message.estimate_tokens/1` — public character-based token estimator.
- `Agent.estimate_tokens/1` — public API that computes current context size.
- `running_tools` / `tool_results_acc` added to agent state for non-blocking
  tool tracking.

### Prior entries

First release.

- `Planck.Agent.Sidecar` — behaviour for distributed sidecar extensions; single
  `tools/0` callback; module-level RPC entry points: `discover/0` (auto-detects
  the entry module via `:persistent_term`-cached scan, only caches on success),
  `list_tools/0`, `list_tools/1`, `execute_tool/3`, `execute_tool/4`
- `Planck.Agent.Compactor` — redesigned: `compact/2` and `compact_timeout/0`
  callbacks; unified `build/2` accepting `sidecar_node:` and `compactor:` opts
  for remote sidecar compactors with local fallback; `compactor:` string is
  converted to `:"Elixir.<name>"` atom before RPC; `load/1` removed
- `AgentSpec.compactor` — per-agent compactor module name string; resolved via
  `Compactor.build/2` at session start
- OTP-based agent runtime with GenServer per agent
- Team lifecycle: orchestrator owns team, team dies with orchestrator
- Inter-agent tools: `ask_agent`, `delegate_task`, `send_response`, `list_team`
- Orchestrator-only tools: `spawn_agent`, `destroy_agent`, `interrupt_agent`, `list_models`
- `spawn_agent` accepts a `"tools"` JSON array; the orchestrator may grant any subset of its own `grantable_tools` to the spawned worker (no privilege escalation)
- `Planck.Agent.ExternalTool` — declarative external tool spec loaded from `<name>/TOOL.json`; `{{key}}` interpolation in commands; `erlexec`-backed execution; `load_all/1`, `from_file/1`
- `Planck.Agent.Compactor` — defines `@callback compact/1`; custom compactors implement this behaviour in a module inside a `.exs` file, allowing helper functions alongside the main callback; `load/1` compiles the file and wraps the module's `compact/1` as an `on_compact` function
- Registry-based agent discovery by type, name, or id
- Parallel tool execution via `Task.async_stream`
- Phoenix.PubSub broadcasting on `"agent:#{id}"` and `"session:#{session_id}"` topics
- Token usage tracking: `:usage_delta` events in real-time and `usage` in `:turn_end`
- `stop/1` — graceful shutdown; cancels in-flight stream via `terminate/2`
- `get_info/1` — lightweight metadata snapshot
- `Planck.Agent.BuiltinTools` — `read/0`, `write/0`, `edit/0`, `bash/0` tool factories
  - `read` streams line-by-line with optional `offset` and `limit`
  - `bash` is backed by `erlexec`; accepts `cwd` and `timeout` as runtime JSON args; stdout and stderr both captured
- `Planck.Agent.Skill` — filesystem-based skill loader; `load_all/1`, `from_file/1`, `system_prompt_section/1`; skills are `<name>/SKILL.md` directories with YAML-style frontmatter
- `Planck.Agent.Session` — SQLite-backed session store with checkpoint-based pagination; caller-supplied `:dir` (no default)
- `Planck.Agent.Compactor` — default LLM-based context compaction anchored on `model.context_window`
- `Planck.Agent.Team` — directory-based team loader (`TEAM.json` + `members/<name>.md`); `%Team{source: :filesystem | :dynamic}`; `Team.load/1` and `Team.dynamic/1`
- `Planck.Agent.AgentSpec` — explicit constructor `new/1`; JSON parsers `from_map/2` and `from_list/2` for member entries; `description`, `tools: [String.t()]`, and `skills: [String.t()]` fields; `to_start_opts/2` accepts `tool_pool:` and `skill_pool:` overrides — tool names resolve from `tool_pool:` (falling back to the `tools:` override when `spec.tools` is empty); skill names resolve from `skill_pool:` and their descriptions are appended to `system_prompt` via `Skill.system_prompt_section/1` when `spec.skills` is non-empty
- Member `name` defaults to `type` when not provided; `Team.load/1` rejects duplicate names so multiple same-type members must be explicitly named
- `spawn_agent` tool accepts a `"skills"` parameter and a `grantable_skills` closure arg, symmetric with `grantable_tools`
- `Planck.AI.Model.providers/0` — valid provider atoms
- Pluggable `on_compact` hook — `Compactor.build/2` returns a ready-to-use function
- `@type agent` and `@type t` now have full `@typedoc` documentation with all fields typed

### Session API additions

- `Session.append/3` changed from fire-and-forget cast to synchronous call —
  returns `pos_integer() | nil` (the SQLite autoincrement row id, or `nil` when
  the session is not found); enables the agent to set `Message.id = db_id`
  immediately after each persist
- `Session.truncate_after/2` — deletes all messages with `id >= db_id` across all
  agents in a session; used by the edit-message feature
- `Session.messages/1` rows now include `db_id: pos_integer()` — the SQLite row id
- `Message.id` is now the SQLite row id after persistence (previously a random UUID);
  this unifies the two identifiers so callers never need to track both
- `Message.id` is **not** stored in the serialised blob — the field is stripped
  before writing and set from the DB `id` column on every read; the row id is
  therefore authoritative for all rows, including legacy ones that stored a UUID
- `Agent.rewind_to_message/2` — truncates both the session and in-memory history to
  strictly before the given db_id, then reloads from the DB to restore canonical
  order and rebuild `turn_checkpoints`; replaces the old `rewind/2` (removed)
- `Agent.rewind/2` removed — replaced by `rewind_to_message/2`

### Message persistence ordering

- Queued messages (received while the agent is streaming) are no longer persisted
  immediately; they retain a UUID id in memory and are flushed to the session at
  the start of the next LLM turn via `flush_unpersisted_messages`. This guarantees
  that the queued message's db_id is always greater than the current turn's
  assistant response, preserving correct insertion order in the DB
- `flush_unpersisted_messages` and `reload_messages_from_session` are internal
  helpers that keep in-memory message order consistent with DB order after queuing
  or rewind; `turn_checkpoints` is rebuilt from the reloaded list

### Agent API

- `Agent.prompt/3` is now a synchronous `call` (was a `cast`) — returns `:ok` once the agent
  has set its status to `:streaming`; if the agent is already busy the message is queued
  (appended to history) and re-triggered automatically after the current turn ends via
  `maybe_turn_start/1`
- `send_response` tool now carries sender attribution: orchestrator receives
  `{:agent_response, response, %{id, name}}` and stores `sender_id`/`sender_name` in the
  message metadata
- `to_ai_messages/1` converts `{:custom, :agent_response}` messages to `:user` role, prefixed
  with `"Response from <name>: "` when `sender_name` metadata is present
- `ask_agent` no longer accepts a `timeout_ms` parameter — blocks indefinitely; monitors the
  target process and returns `{:error, "Agent terminated: ..."}` if it crashes; subscribes
  before prompting to close the race condition
- `delegate_task` tool result now includes guidance to end the turn

### Notes

- `planck_agent` is a pure library with no runtime config module; filesystem-path configuration (sessions, skills, tools, compactor) lives in `Planck.Headless.Config`. Callers using `planck_agent` directly pass paths as explicit arguments.
- `Planck.Agent.TeamTemplate` iterated out during development — superseded by `Planck.Agent.Team` and `AgentSpec.from_map/2`/`from_list/2`.
