v0.1.0
API keys now stored under :req_llm app
anthropic_api_key,openai_api_key,google_api_keySkogsra entries now write intoApplication.env(:req_llm, ...)instead of:planck, so req_llm resolves them directly from its own config source without extra wiring.
Dynamic worker session history preserved on resume
- On session resume, dynamic worker agents are reconstructed with their original
agent ids extracted from the
spawn_agenttool-result messages in session history. Worker message history is fully visible after restart. - Failed
spawn_agentcalls (error results) are skipped during reconstruction. The most recent successful spawn wins when the orchestrator retried. save_metadatanow runs afterreconstruct_dynamic_workersso reconstructed worker ids are captured for subsequent resumes.
Worker duplication fix on resume
reconstruct_dynamic_workersdeduplicates spawn calls by{type, name}— a worker spawned multiple times (e.g. after a recovery nudge) is only reconstructed once.
API key loading from .planck/.env
- New
Planck.Headless.Config.EnvBinding— Skogsra binding that reads API keys from./.planck/.env(project-local) and~/.planck/.env(global). Priority: system env → project .env → global .env → Elixir config. Standard dotenv format; skipped in tests viaskip_env_config: true. Config.env_filesapp_env — configurable list of env files; defaults to["~/.planck/.env", "./.planck/.env"].
Runtime model configuration
Headless.configure_model/1— writes a model configuration to disk and reloads resources. Options:provider:,model_id:,scope:(:localor:global),api_key:,base_url:,model_name:,context_window:,supports_thinking:,advanced_opts:(map fordefault_opts),default:(set asdefault_provider/default_model). Writes to JSON config file (merging with existing content, appending tomodelsarray for local providers) and to the.envfile for API keys. Acceptsconfig_file:andenv_file:overrides for test isolation.reload_resources/0now clears all Skogsra key caches (Config.reload_*) before callingResourceStore.reload/0, ensuring config file changes are immediately visible without stale persistent-term values.
Session metadata
team_descriptionadded to session metadata — populated fromteam.descriptionatstart_sessionand preserved onresume_session. Used by the Web UI to render a welcome card in the empty chat state.
AGENTS.md prepended to all agents
- Static workers now receive
AGENTS.mdprepended to their system prompt, on par with the orchestrator.start_workerscallsTools.prepend_agents_md/2(the now-public function fromplanck_agent) and passescwdto each worker's start opts so the field is populated in agent state. prepend_agents_md/2andfind_agents_md/1removed fromplanck_headless— replaced byPlanck.Agent.Tools.prepend_agents_md/2, which is the single implementation used by both static worker/orchestrator startup and dynamicspawn_agentcalls.
Inter-agent tools — orchestrator improvements
orchestrator_tools/6— addedgrantable_skillsparameter; orchestrators can now grant skills to dynamically spawned workers viaspawn_agent.start_orchestratorpassesstore.skillsasgrantable_skillsso all available skills are grantable by default.start_workersandstart_dynamic_workerpass the worker's own id asown_idtoworker_tools/4for deadlock detection inask_agent.list_modelstool now includesbase_urlin its output so the LLM can pass the correct base_url when callingspawn_agentfor non-default servers.
Session — agent usage persistence
start_orchestratorandstart_workersreadagent_usage:#{id}from session metadata and pass:usageand:costinit options to each agent so token counts and cost are restored on session resume.
Skills — list_skills opt-in tool
list_skillstool added to the agent tool pool when skills are available. Agents that need autonomous skill discovery declare"list_skills"in their TEAM.json"tools"array.load_skillis injected automatically byAgentSpec.to_start_opts/2and does not need to be declared.
Prior entries
First release.
Planck.Headless.SidecarManager— manages the optional sidecar OTP application: builds (mix deps.get+mix compile), spawns via erlexec (elixir --sname planck_sidecar --cookie <cookie> -S mix run --no-halt), monitors node connections, auto-discovers the entry module viaPlanck.Agent.Sidecar.discover/0RPC on nodeup, wraps tools with RPCexecute_fnclosures, stores inResourceStore; clears on nodedown; forwardsPATH,MIX_ENV,PLANCK_LOCALfrom the parent environment; PubSub events on"planck:sidecar"topic;subscribe/0/unsubscribe/0APIResourceStore.put_tools/1andclear_tools/0— called bySidecarManagerto sync sidecar toolsConfig.sidecar(PLANCK_SIDECAR) — path to the sidecar Mix project directory- Removed
Config.tools_dirs,Config.compactor,ResourceStore.on_compact; per-agent compactors viaAgentSpec.compactorandCompactor.build/2 Config.JsonBinding.init/1returns:error(not{:ok, %{}}) whenskip_json_config: true— Skogsra skips the binding without emitting warnings
Edit-message support
Headless.rewind_to_message/3— truncates the session to strictly before the given DB row id (Session.truncate_after/2), rewinds the orchestrator's in-memory history to before that same id (Agent.rewind_to_message/2, sinceMessage.id == db_idfor persisted messages), then re-prompts withnew_text; powers the edit-message UI feature
Session lifecycle
Planck.Headless.start_session/1— resolves team (alias, path, or nil for the default dynamic team), generates a<adjective>-<noun>session name, startsPlanck.Agent.Session, materialises agents with built-in + external tools and resolved skills, saves metadata (team_id,team_alias,cwd,session_name) to SQLite.Planck.Headless.resume_session/1— accepts session id or name, reopens the SQLite session, reconstructs the base team from metadata, replays completedspawn_agentcalls from the previous orchestrator's history to restore dynamically-added workers (deduped by{type, name}against the base team, so two builders "Bob" and "Charlie" are both correctly reconstructed), detects in-flightask_agentand unfinished workers, injects a recovery context message under the new orchestrator if anything was in-flight.Planck.Headless.close_session/1— stops all agents byteam_id, stops the Session GenServer; SQLite file retained.Planck.Headless.prompt/2— dispatches to the orchestrator via the agent registry (team_idis read from session metadata; no separate tracker).Planck.Headless.list_sessions/0— globs sessions dir for<id>_<name>.dbfiles; checksSession.whereis/1for active status.Planck.Headless.list_teams/0,get_team/1— wrapResourceStore.Planck.Headless.available_models/0,reload_resources/0.
Team materialization
- Orchestrators receive all four
BuiltinTools(read, write, edit, bash) in theirtool_poolso spec.tools names like"read"resolve correctly. orchestrator_tools+worker_toolsinjected on top of resolved spec tools; workers getworker_toolsonly (no spawn_agent etc.).- Default dynamic team: orchestrator's
base_urlpulled fromResourceStore.available_modelsso local servers use the correct URL.
Config
Planck.Headless.Config.JsonBinding— SkogsraBindingthat reads~/.planck/config.jsonand.planck/config.jsonat resolution time; results cached in persistent_term;invalidate/0for cache busting before reload.config_filesapp_env (PLANCK_CONFIG_FILES) — controls which JSON files are read;config :planck_headless, :skip_json_config, truefor tests.modelsapp_env —Planck.AI.Config-format model declarations parsed to[Planck.AI.Model.t()]; replaceslocal_servers; no network at boot.- Provider atoms pre-loaded at boot via
Planck.AI.Model.providers()to avoidString.to_existing_atomfailures on lazy module load. PathListinline asPlanck.Headless.Config.PathListsubmodule.
ResourceStore
- Cloud models: static LLMDB catalog filtered by API key presence.
- Local/custom models: from
Config.models!()— already parsed, zero network. AppSupervisorownsResourceStore; noSessionRegistry— dropped in favour of readingteam_iddirectly from session SQLite metadata.
Session naming
Planck.Headless.SessionName— generates<adjective>-<noun>names;generate/1retries on collision;sanitize/1normalises to[a-z0-9-]+.- Session files stored as
<sessions_dir>/<id>_<name>.db;Session.find_by_id/2andfind_by_name/2use glob for O(1) lookup.
Other
Planck.Headless.DefaultPrompt— default system prompt for dynamic-team orchestrator.Moxin test deps;Planck.Agent.MockAIwired in test.exs.start_session(template: alias)exercised via ResourceStore in tests.- Fixed in-flight detection and completed spawn_agent matching to use
MapSet.member?/2instead ofis_map_key/2guard (MapSet is a struct, not a plain map; the guard silently never matched).
Session resume improvements
- Stable agent IDs across session resumes:
save_metadatanow persists anagent_idsmap (name→id JSON) andresume_sessionloads it, passing previous IDs tomaterialize_team,start_workers, andstart_dynamic_workerso processes restart with the same IDs they had in the original session maybe_inject_recoverysimplified: no longer needsfind_previous_orchestratorsince IDs are stable across resumes
Worker lifecycle
unfinished_workersrewrite: usesworker_unfinished?/1— a worker is considered unfinished when their most recent:usermessage (last assigned task) has nosend_responsein any assistant message that follows itsend_responsesender attribution threaded through:start_workersandstart_dynamic_workernow build asender = %{id, name}map and pass it toworker_tools/3, so every response reaches the orchestrator with full sender metadata