# 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