Current section

Files

Jump to
cmdc llm.txt
Raw

llm.txt

# CMDC — AI Quick Reference (llm.txt)
# Elixir Agent Kernel v0.1.0 | OTP gen_statem | Hex: {:cmdc, "~> 0.1"}
# Full reference: llms-full.txt | HexDocs: https://hexdocs.pm/cmdc
## Facade API — CMDC module
# Create agent session (returns {:ok, pid} | {:error, term})
{:ok, session} = CMDC.create_agent(
model: "anthropic:claude-sonnet-4-5", # required — "provider:model_id"
tools: [CMDC.Tool.ReadFile, CMDC.Tool.Shell], # optional — Tool modules
plugins: [CMDC.Plugin.Builtin.SecurityGuard], # optional — Plugin modules or {Module, opts}
system_prompt: "You are a coding assistant.", # optional — prepended after BasePrompt
working_dir: "/my/project", # optional — default "."
provider_opts: [api_key: "sk-..."], # optional — passed to req_llm
max_turns: 50, # optional — default 100
skills_dirs: ["./skills"], # optional — SKILL.md discovery paths
memory: ["AGENTS.md"], # optional — memory file paths
sandbox: nil, # optional — Sandbox module, nil = local OS
subagents: [], # optional — [SubAgent.t()]
max_tokens: nil # optional — nil = provider default
)
# Also accepts %CMDC.Options{} struct or blueprint:
{:ok, session} = CMDC.create_agent(blueprint: MyApp.CodingAgent)
CMDC.prompt(session, "Help me refactor this module")
# => %{queued: false} (false = immediate, true = queued while busy)
{:ok, reply} = CMDC.collect_reply(session, timeout: 60_000)
# => {:ok, "Here's the refactored code..."} | {:error, :timeout} | {:error, reason}
CMDC.stop(session) # graceful shutdown
CMDC.abort(session) # abort running task → idle
CMDC.status(session) # => %{state: :idle, session_id: "...", turns: 3, ...}
CMDC.subscribe(session) # receive {:cmdc_event, session_id, event_tuple}
CMDC.unsubscribe(session)
CMDC.approve(session, approval_id) # HITL approve
CMDC.reject(session, approval_id) # HITL reject
## Options — CMDC.Options
opts = CMDC.Options.new!(model: "openai:gpt-4o", tools: [CMDC.Tool.Shell])
# @enforce_keys [:model]
# Fields: model, tools, system_prompt, plugins, subagents, skills_dirs,
# memory, sandbox, response_format, provider_opts, max_turns,
# max_tokens, working_dir
CMDC.Options.new(keyword) # => {:ok, t} | {:error, ValidationError}
CMDC.Options.merge(opts, overrides_keyword) # => new Options
## Events — CMDC.Event (broadcast as {:cmdc_event, session_id, event})
# Lifecycle: :agent_start | {:agent_end, messages, token_usage} | {:agent_abort, reason} | :agent_abort
# Prompt: {:prompt_received, text} | {:prompt_queued, text} | {:prompt_rejected, reason}
# Stream: :message_start | {:message_delta, %{delta: text}} | {:response_complete, %Message{}}
# :thinking_start | {:thinking_delta, %{delta: text}}
# {:status_update, text} | {:title_generated, title}
# Request: {:request_start, %{model: m, messages: n}} | {:stream_error, reason}
# {:stream_stalled, elapsed_s} | {:retry, attempt, delay_ms, reason} | {:context_overflow, r}
# Tool: {:tool_calls, count}
# {:tool_execution_start, name, call_id, args}
# {:tool_execution_end, name, call_id, {:ok, text}|{:error, text}}
# {:tool_blocked, name, call_id, reason} | {:loop_detected, %{type: atom()}}
# HITL: {:approval_required, approval_map} | {:approval_resolved, approval_map}
# approval_map: %{id, tool, args, session_id, hint, requested_at, status(resolved only)}
# Ask: {:ask_user, sid, question, options, ref} | {:user_responded, sid, ref, response}
# Plan: {:todo_change, sid, todos}
# Compact: {:compact_start, sid} | {:compact_end, sid, removed_count}
# SubAgent: {:subagent_start, sid, child_sid, desc} | {:subagent_end, sid, child_sid, result}
# {:sub_agent_event, call_id, child_sid, event}
# Plugin: {:plugin_event, name, payload}
# Other: {:intervention, prompt} | {:stop_blocked, prompt} | {:error, sid, reason}
## EventBus — CMDC.EventBus
EventBus.subscribe(session_id) # => {:ok, pid}
EventBus.subscribe_all() # all sessions
EventBus.broadcast(session_id, event)
EventBus.unsubscribe(session_id)
## Message — CMDC.Message (@derive Jason.Encoder)
Message.system("You are helpful.")
Message.user("Hello")
Message.assistant("Hi!", [%{call_id: "c1", name: "read_file", arguments: %{"path" => "a.ex"}}])
Message.tool_result("c1", "file contents...", false) # call_id, output, is_error
# Fields: id, parent_id, role (:system|:user|:assistant|:tool_result),
# content, thinking, tool_calls, call_id, name, is_error, metadata
## Context — CMDC.Context (passed to Tool.execute/2 and Plugin.handle_event/3)
# @enforce_keys [:session_id, :working_dir, :model]
# Fields: session_id, working_dir, model, sandbox, tools, subagents,
# config, todos, memory_contents, turn, total_tokens, cost_usd
## Built-in Tools (11)
# All tools implement @behaviour CMDC.Tool
# Callbacks: name/0, description/0, parameters/0, execute/2
# Result: {:ok, String.t()} | {:error, String.t()} | {:effect, term()}
CMDC.Tool.ReadFile # read_file — path (req), offset, limit
CMDC.Tool.WriteFile # write_file — path (req), content (req)
CMDC.Tool.EditFile # edit_file — path (req), old_string (req), new_string (req)
CMDC.Tool.Shell # shell — command (req), timeout
CMDC.Tool.Grep # grep — pattern (req), path, include, context_lines, max_results
CMDC.Tool.ListDir # list_dir — path
CMDC.Tool.Glob # glob — pattern (req), path
CMDC.Tool.Task # task — description (req), subagent_type
CMDC.Tool.WriteTodos # write_todos — todos (req) [%{id, content, status}]
CMDC.Tool.AskUser # ask_user — question (req), options
CMDC.Tool.CompactConversation # compact_conversation — reason
## Built-in Plugins (6)
# All plugins implement @behaviour CMDC.Plugin
# Callbacks: init/1, priority/0, handle_event/3
# Actions: {:continue, state} | {:intervene, prompt, state} | {:abort, reason, state}
# {:skip, state} | {:block_tool, reason, state}
# {:replace_tool_args, new_args, state} | {:emit, {type, data}, state}
CMDC.Plugin.Builtin.SecurityGuard # priority 10 — blocks dangerous paths/commands
CMDC.Plugin.Builtin.EventLogger # priority 50 — JSON Lines audit log
CMDC.Plugin.Builtin.HumanApproval # priority 15 — HITL before_tool approval
CMDC.Plugin.Builtin.MemoryLoader # priority 100 — loads AGENTS.md into system prompt
CMDC.Plugin.Builtin.PatchToolCalls # priority 120 — patches dangling tool_calls
CMDC.Plugin.Builtin.PromptCache # priority 130 — Anthropic prompt caching
## Plugin Events (11 hooks)
# :session_start | :session_end
# {:before_prompt, text} | {:before_request, messages} | {:after_response, message}
# {:before_tool, name, args}
# {:on_tool_error, name, call_id, error, attempt} — tool failed, before retry
# {:after_tool, name, call_id, result} | {:after_tool_batch, results}
# :before_finish | {:before_compact, messages}
## Tool Retry (via state.config)
# :tool_max_retries — max retry attempts on tool failure (default 0 = no retry)
# :tool_retry_delay_ms — delay between retries in ms (default 500)
# on_tool_error plugin hook: {:continue, state} = retry, {:abort, _, state} or {:skip, state} = give up
## Blueprint — CMDC.Blueprint
# @callback build(keyword()) :: CMDC.Options.t()
defmodule MyAgent do
use CMDC.Blueprint
@impl true
def build(opts) do
%CMDC.Options{
model: opts[:model] || "anthropic:claude-sonnet-4-5",
tools: [CMDC.Tool.ReadFile, CMDC.Tool.Shell],
system_prompt: "Expert Elixir developer."
}
end
end
{:ok, s} = CMDC.create_agent(blueprint: MyAgent, provider_opts: [api_key: "sk-..."])
## Blueprint.Base — built-in base blueprint (SecurityGuard + EventLogger + PatchToolCalls)
opts = CMDC.Blueprint.Base.build(model: "anthropic:claude-sonnet-4-5", tools: [...])
## SubAgent — CMDC.SubAgent
sa = CMDC.SubAgent.new!(name: "coder", model: "anthropic:claude-opus-4-5",
tools: [CMDC.Tool.ReadFile, CMDC.Tool.EditFile])
# @enforce_keys [:name]
# Fields: name, description, system_prompt, model, tools, plugins, skills_dirs
# nil = inherit from parent agent
## Skill — CMDC.Skill
skills = CMDC.Skill.discover(["./skills"]) # => [%Skill{}]
{:ok, loaded} = CMDC.Skill.load(skill) # reads SKILL.md content
CMDC.Skill.to_prompt_snippet(skills) # => prompt section string
## Sandbox — CMDC.Sandbox (behaviour)
# @callback read_file(path, opts) :: {:ok, String.t()} | {:error, String.t()}
# @callback write_file(path, content, opts) :: :ok | {:error, String.t()}
# @callback edit_file(path, old_str, new_str, opts) :: {:ok, count} | {:error, reason}
# @callback list_dir(path, opts) :: {:ok, [dir_entry]} | {:error, String.t()}
# @callback file_exists?(path, opts) :: boolean()
# @callback grep(pattern, path, opts) :: {:ok, [grep_match]} | {:error, String.t()}
# @callback glob(pattern, path, opts) :: {:ok, [glob_match]} | {:error, String.t()}
# @callback execute(command, opts) :: {:ok, String.t()} | {:error, String.t()}
# Default: CMDC.Sandbox.Local (direct OS execution)
## Provider — CMDC.Provider (req_llm wrapper)
Provider.stream(model, messages, tools, opts) # => {:ok, %{bridge_pid: pid}}
Provider.convert_messages(messages) # => [ReqLLM.Message.t()]
Provider.convert_tools(tool_modules) # => [map()]
## Config — CMDC.Config
config = CMDC.Config.new!(provider_opts: [api_key: "sk-..."])
# Fields: data_dir, default_model, default_tools, provider, provider_opts, providers
## Agent Internals (L2 — usually not called directly)
Agent.prompt(agent_pid, text) # direct gen_statem call
Agent.status(agent_pid) # => %{state: :idle|:running|:streaming|:executing_tools, ...}
Agent.get_state(agent_pid) # => State.t()
# States: :idle → :running → :streaming → :executing_tools → :idle (loop)
## Compactor — CMDC.Agent.Compactor
# Auto-triggers when context exceeds token threshold
# Compactor.maybe_compact(state) => {:compacted, state} | {:skip, state}
# Compactor.force_compact(state) => {:compacted, state} | {:skip, state}
# ArgTruncator: auto-truncates large write_file/edit_file/execute args
## MCP Integration
# Discover MCP servers from mcp.json:
servers = CMDC.MCP.Config.discover(working_dir)
modules = CMDC.MCP.Bridge.discover_tool_modules(servers)
# MCP tools are dynamically wrapped as CMDC.Tool modules
## Session Lifecycle
# CMDC.create_agent → SessionServer.start_link → Supervisor tree:
# SessionServer (Supervisor)
# ├── Agent (gen_statem)
# └── SubAgent.Supervisor (DynamicSupervisor)
# └── child Agent processes (isolated, :temporary)
## Key Patterns
# Streaming events:
CMDC.subscribe(session)
CMDC.prompt(session, "hello")
receive do
{:cmdc_event, sid, {:message_delta, %{delta: chunk}}} -> IO.write(chunk)
{:cmdc_event, _sid, {:agent_end, _msgs, _usage}} -> :done
end
# HITL approval flow:
receive do
{:cmdc_event, _sid, {:approval_required, %{id: ref, tool: tool, args: args}}} ->
CMDC.approve(session, ref) # or CMDC.reject(session, ref)
end