# CMDC — AI Quick Reference (llm.txt)
# Elixir Agent Kernel v0.2.1 | OTP gen_statem | Hex: {:cmdc, "~> 0.2"}
# 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
  user_data: %{tenant_id: "t-1"},                 # optional — verbatim into ctx.user_data, SubAgent inherits
  messages: []                                    # optional — [%CMDC.Message{}], inject prior history at boot
)

# 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.abort(session, reason: :user_cancelled)        # v0.2 RFC B6: 事件 {:agent_abort, :user_cancelled}
CMDC.abort(session, kill_tools: :all)               # v0.2 RFC B6: brutal_kill 全部工具(含 immune)
CMDC.abort(session, kill_tools: :none)              # v0.2 RFC B6: 不杀工具,让其自然完成
CMDC.abort(session, clear_queue: false)             # v0.2 RFC B6: 保留 pending prompt 队列
# 事件保证::agent_abort 100ms 内到达订阅方;
# 默认 clear_queue: true 时,每条被丢弃的 user prompt 发 {:prompt_dropped, text};
# kill_tools 模式下,每杀一个工具发 {:tool_killed, %{name, call_id, reason}}。
CMDC.status(session)
# => %{
#   state: :idle | :running | :streaming | :executing_tools,
#   session_id: "...",
#   model: "anthropic:claude-sonnet-4-5",                      # v0.2 RFC C8 — 当前 LLM 模型
#   turns: 3, tool_calls: 7, total_tokens: 12_345,
#   cost_usd: 0.0123,                                          # v0.2 RFC B5
#   token_usage: %CMDC.TokenUsage{                              # v0.2 RFC B5 — struct
#     prompt_tokens: 8_000, completion_tokens: 4_345, total_tokens: 12_345,
#     cost_usd: 0.0123, cached_tokens: 1_200
#   },
#   uptime_ms: 4_321, timestamp_ms: 1_700_000_000_000,
#   pending_tools:     [%{name, call_id, args, started_at_ms}, ...], # v0.2 RFC C11 + v0.3 RFC 11G #A15
#   pending_approvals: [%{id, tool, args, requested_at, ...}], # v0.2 RFC C11 — 等审批工具
#   queues: %{prompt_queue: 0, steering_queue: 0}             # v0.2 RFC C11 — 内部队列长度
# }
CMDC.messages(session)                  # v0.2 — [%CMDC.Message{}] 完整对话历史

# 运行期切换 LLM 模型(v0.2 RFC C8 + v0.3 RFC 11G #A17)— 不重启 Agent,messages/tools/plugin 全保留
CMDC.switch_model(session, "openai:gpt-4o-mini")
# - idle: 立即生效,下次 prompt 用新 model
# - running/streaming/executing_tools: 当前轮跑完再切,下一轮用新 model
# - 同 model + 无 provider_opts: no-op,不发事件
# - 事件: {:model_switched, %{from: old, to: new, provider_opts_changed?: false}}
# - Plugin Action 等价: {:switch_model, "openai:gpt-4o-mini", state}
#   多个 plugin 同时 switch_model,priority 最大者(后执行)的值生效

# 同时切 provider 参数(v0.3 RFC 11G #A17 — 多 provider 容灾)
CMDC.switch_model(session, "openai:gpt-4o-mini",
  provider_opts: [base_url: "https://api.fallback.com/v1", api_key: "sk-...", timeout_ms: 90_000]
)
# - 事件: {:model_switched, %{..., provider_opts_changed?: true}}
# - state.config.provider_opts 立即被替换
# - Plugin Action 4 元组形态: {:switch_model, "openai:gpt-4o-mini", state, provider_opts: [...]}

# 运行期挂载/卸载工具(v0.2 RFC C9)— 不重启 Agent,下次 LLM 请求生效
:ok = CMDC.attach_tool(session, MyApp.Tools.GitHubMCP)
# {:error, :already_attached} | {:error, :invalid_tool}
# 事件: {:tool_attached, name}

:ok = CMDC.detach_tool(session, "shell")
# {:error, :not_found}
# 事件: {:tool_detached, name}
# 注意: 已 in-flight 的 tool 调用继续跑完,不被取消;
#      若 LLM 在 detach 后仍调用该 tool,emit {:tool_call_unknown, name, call_id}
#      并自动注入 error tool_result 让 LLM 自我纠正

# EventBus ring buffer 与重连补帧(v0.2 RFC C10)— 默认关闭
{:ok, session} = CMDC.create_agent(model: "...", event_buffer_size: 100)
# 启动时设置 event_buffer_size > 0 开启 per-session 缓存(典型 50-200)
# 默认 0:关闭,无任何内存开销,行为同 v0.1

# 断线重连补帧
{:ok, _} = CMDC.subscribe(session, since: last_seen_index)
# replay last_seen_index < idx <= now 之间的所有事件
# 早于 buffer 起点的部分已被 FIFO 丢弃(无法 replay)

# 工具 API
last_idx = CMDC.EventBus.last_index(session_id)  # 获取当前最新 index
size = CMDC.EventBus.buffer_size(session_id)     # 当前缓存事件数

# 结构化崩溃监控(v0.2 RFC C12)— 替代 Process.monitor,拿到结构化 reason
ref = CMDC.monitor(session)   # 接受 pid | session_id string
# Agent 退出时当前进程收到:
# {:cmdc_down, ref, session_id, structured_reason}
# structured_reason:
#   :normal | :shutdown | {:shutdown, _} → 归一为 :normal
#   %RuntimeError{} | {%RuntimeError{}, stacktrace} → {:exception, exception}
#   :max_turns_exceeded | :provider_timeout | {:plugin_aborted, name, why}  (v0.2+ 预留)
#   其他 → 原样透传
CMDC.demonitor(session, ref)  # 取消监控

# PromptMode — SubAgent / 长会话 system prompt 降本(v0.2 Phase 10B)
{:ok, session} = CMDC.create_agent(model: "...", prompt_mode: :full)
# 可选::full(默认,主 Agent)| :task(SubAgent 默认,省 ~50%)
#       :minimal(仅工具名列表,省 ~90%)| :none(完全由 system_prompt 接管)
#
# SubAgent 默认 :task,Tool.Task 派发时自动透传
CMDC.SubAgent.new!(name: "router", prompt_mode: :minimal)  # 快速分类子代理
# 四模式注入矩阵(true/-)
#  mode      user_sp | BasePrompt    | Identity | tools列表           | Skills | Memory
#  :full     ✓       | 完整基座      | ✓        | name+desc           | ✓      | ✓
#  :task     ✓       | 精简(~150字节)| ✓        | name+desc           | -      | -
#  :minimal  ✓       | -             | ✓        | 仅 name (no desc)   | -      | -
#  :none     ✓       | -             | -        | -                   | -      | -

# MemoryFlush — 压缩前提取关键事实,持久化到 MEMORY.md(v0.2 Phase 10C)
{:ok, session} = CMDC.create_agent(
  model: "...",
  plugins: [
    CMDC.Plugin.Builtin.MemoryLoader,  # session_start 加载 MEMORY.md / AGENTS.md
    CMDC.Plugin.Builtin.MemoryFlush    # :before_compact 写入 MEMORY.md
  ],
  working_dir: "/project/path"
)
# 长会话 → 自动 compact → MemoryFlush 写 working_dir/MEMORY.md → 下次会话 MemoryLoader 读回
# 可覆盖 extract_fn 用真实 LLM 提取;默认启发式规则(纯离线,便于单测)
#
# RFC D14 — MemoryFlush emit :plugin_event 统一事件(便于集成方订阅入库)
CMDC.subscribe(session)
# 收到 {:cmdc_event, sid, {:plugin_event, %{kind: :memory_flush, facts, count,
#                                            session_id, occurred_at, file, v: 1}}}

# ModelRouter — 按业务规则在 before_request 切换模型(v0.2 Phase 10D)
{:ok, session} = CMDC.create_agent(
  model: "anthropic:claude-sonnet-4-5",
  user_data: %{user_tier: :free, task_complexity: :complex, token_budget: 100_000},
  plugins: [
    {CMDC.Plugin.Builtin.ModelRouter,
      default_model: "anthropic:claude-sonnet-4-5",
      rules: [
        # v0.1 三条(运行时)
        %{condition: {:turn_gt, 20},             model: "openai:gpt-4.1-mini"},
        %{condition: {:cost_gt, 0.5},            model: "openai:gpt-4.1-mini"},
        %{condition: {:tokens_gt, 50_000},       model: "openai:gpt-4.1-mini"},

        # v0.2 四条新条件(业务层面)
        %{condition: {:token_budget_lt, 5_000},  model: "openai:gpt-4.1-mini"},
        %{condition: {:task_complexity, :complex},        model: "anthropic:claude-opus-4"},
        %{condition: {:task_complexity_in, [:simple]},    model: "openai:gpt-4.1-mini"},
        %{condition: {:time_of_day_in, [22..23, 0..6]},   model: "openai:gpt-4.1-mini"},
        %{condition: {:user_tier, :free},                 model: "openai:gpt-4.1-mini"},
        %{condition: {:user_tier_in, [:enterprise]},      model: "anthropic:claude-opus-4"},
        %{condition: {:user_data, :region, "eu-west"},    model: "mistral:mistral-large"},
        %{condition: {:user_data, :priority, :gt, 5},     model: "anthropic:claude-opus-4"}
      ]
    }
  ]
)
# 命中首条规则 → emit {:switch_model, new_model, state} action,下一次 LLM
# 请求用新模型,EventBus 广播 :model_switched(from/to/reason=:model_router)

CMDC.subscribe(session)                 # receive {:cmdc_event, session_id, event_tuple}
CMDC.unsubscribe(session)
CMDC.approve(session, approval_id)               # HITL approve, auto_resume: true (default)
CMDC.approve(session, approval_id, auto_resume: false)  # 旧版:只放行不续 turn
CMDC.reject(session, approval_id)                # HITL reject, auto_resume: false (default)
CMDC.reject(session, approval_id, auto_resume: true)    # 拒绝后立即续 turn 让 LLM 改方案
CMDC.user_respond(session, ref, response)        # 应答 AskUser 工具(与 approve/reject 对称)

# session 入参类型 — v0.2 起所有 API 同时接受 pid 与 session_id 字符串
# @type session :: pid() | String.t()
# 字符串通过 CMDC.SessionRegistry(:via 注册)反查 pid;不存在 raise ArgumentError
CMDC.prompt("my-sid", "hello")          # ✓ string 入参直接工作
CMDC.status("my-sid")                   # ✓ 反查 pid 后调用
CMDC.subscribe("not-yet-started")       # ✓ 提前订阅合理场景,不 raise
CMDC.user_respond("my-sid", ref, "ans") # ✓ 直接 broadcast,不查注册表
SessionServer.whereis("my-sid")         # => pid | nil(不抛异常)

# Steering — mid-execution soft interrupt (do NOT abort + re-prompt; use this)
{:ok, ref} = CMDC.steer(session, "改方向:只看官方文档")
# => {:ok, reference()}                 # queued; effect signaled by :steering_applied event
# => {:error, :queue_full}               # steering_queue 满(默认 max=3)
# => {:error, :rejected}                 # plugin abort before_steering
# => {:error, :invalid_text}             # 入参非合法字符串
# 状态行为:
#  idle             → 等同 prompt/2,立刻进入新 turn(仅 emit :steering_applied)
#  running/streaming → 入 queue,下个 turn 间隙合并注入
#  executing_tools  → 入 queue,下次 collect_result 时杀掉非 immune 工具,全清空后注入

## 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, max_steering_queue, interrupt_immune_tools,
#         user_data, messages
# user_data: arbitrary map() (or keyword, normalized to map) → ctx.user_data
#            SubAgent inherits unless its own :user_data is non-nil (then full replace).
# messages:  [%CMDC.Message{}] — prior conversation history, injected directly into
#            state.messages at Agent boot. Skips one full LLM "re-read" round-trip
#            for idle-resume / cross-process restore. Default []. system_prompt is
#            still composed by SystemPrompt independently — do NOT put :system in here.

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, %CMDC.TokenUsage{}} | {:agent_abort, reason} | :agent_abort
#            ↑ v0.2 RFC B5: token_usage is now a struct with prompt/completion/total_tokens + cost_usd + cached_tokens
#              v0.1.x compat: still a map; pattern-match on Map.get/2 if you need to support both
# 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_execution_metrics, name, call_id, %{started_at_ms[, ended_at_ms, duration_ms]}}
#                                                          # v0.3 RFC 11G #A15
#                                                          # start 时只含 started_at_ms,
#                                                          # end 时含完整 3 字段
#            {: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)}
#            {:agent_resumed, %{trigger: :tool_approved|:tool_rejected|:tool_approval_timeout, approval_id}}
#                ↑ broadcast when approve/reject auto_resume kicks in (Agent was idle → re-enters running)
# 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}
# Steering:  {:steering_received, %{ref, text, queued_at, status: :queued|:rejected_full|:rejected_by_plugin}}
#            {:steering_applied, %{refs: [ref], count: pos_integer()}}
#            {:tool_skipped_for_steering, %{name, call_id, reason: :killed_by_steering | :pending_dispatch}}
# 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)

## TokenUsage — CMDC.TokenUsage (v0.2 RFC B5, @derive Jason.Encoder)

# %CMDC.TokenUsage{
#   prompt_tokens:    non_neg_integer(),  # OpenAI input  / Anthropic input_tokens
#   completion_tokens: non_neg_integer(), # OpenAI output / Anthropic output_tokens
#   total_tokens:     non_neg_integer(),  # 多数 Provider 自带,缺失时 prompt+completion 补齐
#   cost_usd:         float() | nil,       # Provider 配定价时计算;否则 nil
#   cached_tokens:    non_neg_integer() | nil  # Anthropic cache_read_input_tokens
# }
#
TokenUsage.new(prompt_tokens: 100, completion_tokens: 50)
# %CMDC.TokenUsage{prompt_tokens: 100, completion_tokens: 50, total_tokens: 150, ...}

TokenUsage.from_raw(%{input_tokens: 80, output_tokens: 40})  # Anthropic 风格
TokenUsage.from_raw(%{prompt_token_count: 200, candidates_token_count: 100})  # Google
TokenUsage.add(usage_a, usage_b)                              # 多 Provider 累加

# 内部 State.token_usage 仍是扩展 map(含 :context_window / :current_context_tokens 等
# 上下文窗口元数据,不进 struct);事件 / status 边界用 from_state_token_map/3 转 struct。

## 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, user_data, turn, total_tokens, cost_usd,
#         last_assistant_reply  # v0.3 — 最后一条 assistant message 的文本,Plugin/Tool 直接读取无需翻 messages
#
# user_data — arbitrary business context map injected from Options.user_data;
#             use directly in pattern match: execute(args, %Context{user_data: %{tenant_id: t}})

## Plan — CMDC.Plan (v0.3 ADP Ch6, @derive Jason.Encoder)

# Plan struct 字段:
#   goal :: String.t()
#   steps :: [%CMDC.Plan.Step{}]
#   status :: :draft | :approved | :in_progress | :completed
#   created_at, approved_at, metadata :: map()
#
# Step struct 字段:
#   id :: String.t()                # "step-1", "step-2" ...
#   description :: String.t()
#   status :: :pending | :in_progress | :completed | :failed | :skipped
#   started_at, finished_at, result, notes
#
# 创建/解析
CMDC.Plan.new(goal, step_descriptions)           # => %Plan{status: :draft}
CMDC.Plan.from_markdown(markdown, goal)          # 解析 "- [ ] ..." checklist
# 状态迁移(railway:{:ok, plan} | {:error, :not_found})
CMDC.Plan.step_started(plan, step_id)
CMDC.Plan.step_completed(plan, step_id, result)
CMDC.Plan.step_failed(plan, step_id, reason)
CMDC.Plan.step_skipped(plan, step_id, reason)
CMDC.Plan.annotate_step(plan, step_id, notes)
CMDC.Plan.approve(plan)                          # status → :approved
# Replan(ADP Ch6 动态调整)
CMDC.Plan.replace_steps(plan, ["new1", "new2"])  # 全量重规划,重新编号 step-1…
CMDC.Plan.add_step(plan, "追加的步骤")            # 末尾 append,自动 step-N+1
CMDC.Plan.insert_step(plan, "step-2", "新步骤")   # 默认在 step-2 之后;opts before: true 在之前
CMDC.Plan.remove_step(plan, "step-3")
# 查询/渲染
CMDC.Plan.get_step(plan, step_id)
CMDC.Plan.current_step(plan)                     # 第一个 :pending / :in_progress 步骤
CMDC.Plan.progress(plan)                         # %{total, completed, failed, skipped, pending, in_progress, pct}
CMDC.Plan.all_finished?(plan) / all_completed?(plan)
CMDC.Plan.to_markdown(plan)                      # "## Goal\n...\n- [x] step-1 ..."
CMDC.Plan.to_prompt_section(plan)                # "# Plan\n...\n# Progress\n..."  注入 system prompt

## 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 (8)

# All plugins implement @behaviour CMDC.Plugin
# Callbacks: init/1, priority/0, handle_event/3
# Actions (8):  {:continue, state} | {:intervene, prompt, state} | {:abort, reason, state}
#               {:skip, state} | {:block_tool, reason, state}
#               {:replace_tool_args, new_args, state} | {:emit, {type, data}, state}
#               {:switch_model, model_string, state}    # v0.2 RFC C8
#               {:switch_model, model_string, state, provider_opts: [...]}  # v0.3 RFC 11G #A17

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
CMDC.Plugin.Builtin.Planning        # priority 200 — v0.3 ADP Ch6: plan_first + dynamic plan injection
CMDC.Plugin.Builtin.Reflection      # priority 400 — v0.3 ADP Ch4+Ch7: Producer-Reviewer 自评/SubAgent

# --- v0.3 Planning plugin ---
# {CMDC.Plugin.Builtin.Planning,
#   plan_first: true,                      # default true — 在长 prompt 上先出 plan
#   min_prompt_length: 20,                 # 默认 20 字符以下不触发规划
#   max_plan_attempts: 2                   # 解析失败兜底重试次数
# }
# Emits: {:plugin_event, :plan_generated, %CMDC.Plan{}}
# Side-effect: emit {:update_system_context, :plan, Plan.to_prompt_section(plan)}
#   → Agent.state.dynamic_context_sections[:plan] 更新,下一轮 system prompt 自动携带
#
# --- v0.3 Reflection plugin(修 iteration-reset bug + Producer-Reviewer)---
# {CMDC.Plugin.Builtin.Reflection,
#   reviewer_prompt: "You are a strict quality reviewer...",  # 评审员人设
#   reviewer_model: "qwen3-max",                              # 可选,SubAgent 模式下用不同模型
#   reviewer_subagent: true,                                  # true = 启独立 SubAgent (ADP 推荐)
#   criteria: ["准确性", "完整性", "可读性"],
#   max_reviews: 3,                                           # 最大 review 轮数(max_iterations 别名仍兼容)
#   pass_signal: "APPROVED"                                   # 字符串或 %Regex{},通过信号
# }
# 状态机::idle ─before_finish→ :reviewing ─after_response(pass)→ :done
#                                  │
#                                  └─max_reviews 达到──→ :done(强制通过)
# Emits: {:plugin_event, :reflection_approved, %{critique, iteration}}

## Plugin Events (13 hooks)

# :session_start | :session_end | {:after_turn, payload}     # v0.3 RFC 11G #A16
# {: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}
# {:before_steering, text}                          — CMDC.steer/2 触发;continue/intervene/abort/emit

# {:after_turn, payload} payload 字段(v0.3 RFC 11G #A16):
#   outcome :: :finished | :aborted
#   abort_reason :: term() | nil
#   messages_diff :: [%CMDC.Message{}]      — 本轮 prompt cycle 新增的消息(按时间序)
#   token_usage_diff :: %CMDC.TokenUsage{}  — 本轮增量
#   started_at_ms / ended_at_ms / duration_ms
# 与 :session_end 的区别::session_end 无 payload,保持向后兼容;新 Plugin 建议用 :after_turn

## Dynamic System Prompt Injection (v0.3)

# Plugin 可以通过 emit 事件把文本动态注入下一轮 system prompt:
#   {:emit, {:update_system_context, key, text}, state}    # 注入 text
#   {:emit, {:update_system_context, key, nil}, state}     # 删除 key
# 对应 state field:CMDC.Agent.State.dynamic_context_sections :: %{atom() => String.t()}
# SystemPrompt 在 :full / :task / :minimal 三种模式下都会把这些段落拼进去
# (按 key 字典序;排在 BasePrompt + 用户 system_prompt + Blueprint + Skills + Memory 之后)
#
# 典型使用者:CMDC.Plugin.Builtin.Planning 每次生成/更新 plan 都 emit {:update_system_context, :plan, ...}

## Plugin Hook × Action 完整矩阵 + 5 个 Cookbook(v0.2 RFC B7)
# 详见 docs/dev/plugin-cookbook.md
# Hook \ Action          | continue | intervene | abort | skip | block_tool | replace_tool_args | emit | switch_model
# :session_start         |    ✓     |     -     |   ✓   |   -  |     -      |         -         |  ✓  |      -
# :session_end           |    ✓     |     -     |   -   |   -  |     -      |         -         |  ✓  |      -
# {:after_turn, p}       |    ✓     |     -     |   -   |   -  |     -      |         -         |  ✓  |      -
# :before_prompt         |    ✓     |     ✓     |   ✓   |   ✓  |     -      |         -         |  ✓  |      -
# :before_request        |    ✓     |     ✓     |   ✓   |   ✓  |     -      |         -         |  ✓  |      ✓
# :after_response        |    ✓     |     ✓     |   ✓   |   ✓  |     -      |         -         |  ✓  |      ✓
# :before_tool           |    ✓     |     -     |   ✓   |   -  |     ✓      |         ✓         |  ✓  |      ✓
# :on_tool_error         |    ✓     |     -     |   ✓   |   ✓  |     -      |         -         |  ✓  |      -*
# :after_tool            |    ✓     |     ✓     |   ✓   |   -  |     -      |         -         |  ✓  |      ✓
# :after_tool_batch      |    ✓     |     ✓     |   ✓   |   -  |     -      |         -         |  ✓  |      ✓
# :before_finish         |    ✓     |     ✓     |   ✓   |   -  |     -      |         -         |  ✓  |      -
# :before_compact        |    ✓     |     -     |   -   |   ✓  |     -      |         -         |  ✓  |      -
# :before_steering       |    ✓     |     ✓     |   ✓   |   -  |     -      |         -         |  ✓  |      -
# *: :on_tool_error 在 Task 内 retry 循环触发,无 Agent state 上下文,switch_model 被忽略;
#    若需基于工具失败切 model,请在 :after_tool 钩子内基于 result 是 {:error, _} 决定。
#
# 13 hooks × 8 actions 完整矩阵 + emit 形态详解 + 累积/覆盖语义见
# `CMDC.Plugin` 模块 @moduledoc(v0.3 RFC 11G #D27),及 docs/dev/plugin-cookbook.md。
#
# 5 Cookbook 场景:
#  1. DangerousCommandGuard  — :before_tool + block_tool 拦截 rm -rf
#  2. PromptAuditLog          — :before_prompt + emit 审计所有 user prompt
#  3. BudgetGuard             — :after_response + abort 控制成本上限
#  4. ToolFallbackGuard       — :on_tool_error + emit 工具降级触发
#  5. SteeringContentGuard    — :before_steering + abort 拦截 prompt injection

## 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 (auto-resumes by default — no sleep + re-prompt hack needed):
receive do
  {:cmdc_event, _sid, {:approval_required, %{id: ref, tool: tool, args: args}}} ->
    :ok = CMDC.approve(session, ref)
end
# 上层若需等待 Agent 重新跑起来再做下一步:
receive do
  {:cmdc_event, _sid, {:agent_resumed, %{approval_id: ^ref}}} -> :resumed
end

# 关闭自动续 turn(保留旧 v0.1 行为,需要自己 prompt 续接):
:ok = CMDC.approve(session, ref, auto_resume: false)
:ok = CMDC.prompt(session, "继续刚才被拦截的操作")

# Steering flow (mid-execution direction change):
CMDC.subscribe(session)
CMDC.prompt(session, "搜索 Elixir gen_statem 教程")

# 一段时间后改变主意,无需 abort,直接追加新指引
{:ok, _ref} = CMDC.steer(session, "改成只看官方 hexdocs,不要第三方教程")
receive do
  {:cmdc_event, _sid, {:steering_applied, %{count: n}}} ->
    IO.puts("已注入 #{n} 条 steering 指引,agent 重新规划中")
end

# Business context passthrough (multi-tenant / operator id / data scope):
{:ok, session} = CMDC.create_agent(
  model: "anthropic:claude-sonnet-4-5",
  tools: [MyApp.Tools.FetchOrders],
  user_data: %{tenant_id: "acme-1", operator_id: "u-9", data_scope: :restricted}
)

defmodule MyApp.Tools.FetchOrders do
  @behaviour CMDC.Tool
  def execute(_args, %CMDC.Context{user_data: %{tenant_id: t, data_scope: scope}}) do
    rows = Repo.all(from o in Order, where: o.tenant_id == ^t and o.scope == ^scope)
    {:ok, Jason.encode!(rows)}
  end
end
# SubAgent inherits parent user_data; SubAgent.new!(name: "x", user_data: %{tenant_id: "other"})
# fully replaces (does NOT merge).

# Restore prior conversation history (idle resume / cross-process restore):
history = [
  CMDC.Message.user("帮我审核这段代码"),
  CMDC.Message.assistant("好的,请贴上来", [], []),
  CMDC.Message.user("def hello, do: :world")
]

{:ok, session} = CMDC.create_agent(
  model: "anthropic:claude-sonnet-4-5",
  system_prompt: "你是一名严谨的 Elixir 审计员。",
  messages: history          # ← 直接进 state.messages,下次 prompt 自然续接
)

CMDC.prompt(session, "继续:这段代码的复杂度如何?")
# LLM 看到的 messages: [system, user, assistant, user, user] —— 不需要让 LLM 重读 history
# system_prompt 由 SystemPrompt.build/1 拼接,messages 列表里**不要**再放 :system 消息

## Steering Options (CMDC.Options)

# :max_steering_queue       — Steering queue 最大长度(默认 3,溢出 → :queue_full)
# :interrupt_immune_tools   — Steering 触发时不会被 brutal_kill 的 tool 名白名单
#                             默认 ~w(write_file edit_file shell git_commit notebook_edit ask_user)