结构化执行计划 — Agent Planning Pattern 的运行时实体。
Plan 将一个高层目标(goal)拆成一列可执行的 Step,并通过
状态机语义 :pending → :in_progress → :completed | :failed | :skipped
记录 Agent 的进度。Plugin / Tool / 用户都可以读写计划,从而实现
"规划→执行→反馈"的闭环。
配套插件:CMDC.Plugin.Builtin.Planning(v0.3)
构造
iex> plan = CMDC.Plan.new("发布新版", ["写 CHANGELOG", "跑 preflight", "mix hex.publish"])
iex> length(plan.steps)
3
iex> {:ok, plan} = CMDC.Plan.from_markdown("发布", """
...> - [ ] 写 CHANGELOG
...> - [ ] 跑 preflight
...> - [x] 跑 mix format
...> """)
iex> Enum.count(plan.steps, &(&1.status == :completed))
1Step 状态流转
step_started/2::pending → :in_progressstep_completed/3::pending | :in_progress → :completedstep_failed/3::pending | :in_progress → :failedstep_skipped/3::pending → :skipped
非法流转(如 :completed → :in_progress)返回 {:error, {:illegal_transition, from, to}}。
查询
iex> plan = CMDC.Plan.new("task", ["a", "b", "c"])
iex> {:ok, plan} = CMDC.Plan.step_completed(plan, "step-1")
iex> CMDC.Plan.progress(plan)
%{completed: 1, total: 3, failed: 0, skipped: 0, pending: 2, percentage: 33.3}
iex> plan = CMDC.Plan.new("task", ["a", "b"])
iex> %{id: "step-1"} = CMDC.Plan.current_step(plan)渲染
to_markdown/1:渲染为 GitHub Checklist(- [x])to_prompt_section/1:渲染为注入 system prompt 的结构化段落
与 Plugin 的配合
CMDC.Plugin.Builtin.Planning 在 Plugin state 里保存 %CMDC.Plan{},
并在关键生命周期 emit :plan_generated / :plan_step_completed / :plan_completed 事件。
第三方 Tool 可通过 Plugin state 或 EventBus 读写。
Summary
Functions
在计划末尾追加一个步骤。自动分配下一个 step-N id(基于现有最大编号 + 1)。
判断是否所有 step 都成功完成。
判断所有 step 是否都进入终态(completed / failed / skipped)。
附加 notes 到某个 step,不改变其状态。可在 Step 执行前后写入上下文信息。
用户批准 Plan,记录 approved_at 时间戳(供后续审计)。
返回当前应执行的 step(第一个 :in_progress,否则第一个 :pending)。
从 markdown checklist 字符串解析 Plan。
根据 step_id 获取 Step,找不到返回 nil。
在指定 step_id 之前(before: true)或之后(默认)插入新步骤。
从目标 + 步骤描述列表构建新 Plan。
返回 Plan 进度摘要:各状态计数 + 百分比。
删除指定 id 的步骤。不存在返回 {:error, :not_found}。
完全替换 Plan 的步骤列表,重新从 step-1 开始编号。
标记某个 step 已完成,可附带结果数据(:pending | :in_progress → :completed)。
标记某个 step 已失败。
标记某个 step 已跳过(:pending → :skipped)。
标记某个 step 已开始执行(:pending → :in_progress)。
将 Plan 渲染为 Markdown Checklist。:completed 为 [x],其他状态附带 tag。
将 Plan 渲染为注入 system prompt 的结构化段落。含目标、进度摘要、全部步骤、当前步骤。
Types
@type t() :: %CMDC.Plan{ approved_at: DateTime.t() | nil, created_at: DateTime.t() | nil, goal: String.t(), metadata: map(), steps: [CMDC.Plan.Step.t()] }
Functions
在计划末尾追加一个步骤。自动分配下一个 step-N id(基于现有最大编号 + 1)。
iex> plan = CMDC.Plan.new("t", ["a"])
iex> plan = CMDC.Plan.add_step(plan, "b")
iex> Enum.map(plan.steps, & &1.id)
["step-1", "step-2"]
判断是否所有 step 都成功完成。
判断所有 step 是否都进入终态(completed / failed / skipped)。
仅在全部 step 都已推进时返回 true。
@spec annotate_step(t() | {:ok, t()} | {:error, term()}, String.t(), String.t()) :: {:ok, t()} | {:error, :not_found | term()}
附加 notes 到某个 step,不改变其状态。可在 Step 执行前后写入上下文信息。
iex> plan = CMDC.Plan.new("t", ["a"])
iex> {:ok, plan} = CMDC.Plan.annotate_step(plan, "step-1", "需要审批")
iex> CMDC.Plan.get_step(plan, "step-1").notes
"需要审批"
用户批准 Plan,记录 approved_at 时间戳(供后续审计)。
@spec current_step(t()) :: CMDC.Plan.Step.t() | nil
返回当前应执行的 step(第一个 :in_progress,否则第一个 :pending)。
全部完成/失败/跳过时返回 nil。
从 markdown checklist 字符串解析 Plan。
支持以下两种格式:
- [ ]/- [x]/- [X]— GitHub Checklist1. xxx/1) xxx— 有序列表(全部按 pending 处理)
未能识别为列表项的行被忽略,可用于 LLM 在 checklist 前后附加解释性文字。
iex> md = """
...> 这是计划:
...>
...> - [ ] 步骤一
...> - [x] 步骤二 已完成
...> - [ ] 步骤三
...>
...> 希望有用。
...> """
iex> {:ok, plan} = CMDC.Plan.from_markdown("demo", md)
iex> length(plan.steps)
3
iex> Enum.at(plan.steps, 1).status
:completed返回 {:error, :no_steps_found} 当解析结果为空。
@spec get_step(t(), String.t()) :: CMDC.Plan.Step.t() | nil
根据 step_id 获取 Step,找不到返回 nil。
在指定 step_id 之前(before: true)或之后(默认)插入新步骤。
新步骤的 id 自动分配。不存在的 step_id 返回 {:error, :not_found}。
iex> plan = CMDC.Plan.new("t", ["a", "c"])
iex> {:ok, plan} = CMDC.Plan.insert_step(plan, "step-1", "b")
iex> Enum.map(plan.steps, & &1.description)
["a", "b", "c"]
从目标 + 步骤描述列表构建新 Plan。
每个步骤自动分配 step-N 格式的 id。
iex> plan = CMDC.Plan.new("任务", ["A", "B"])
iex> Enum.map(plan.steps, & &1.id)
["step-1", "step-2"]
iex> Enum.all?(plan.steps, &(&1.status == :pending))
true
@spec progress(t()) :: %{ completed: non_neg_integer(), failed: non_neg_integer(), skipped: non_neg_integer(), pending: non_neg_integer(), total: non_neg_integer(), percentage: float() }
返回 Plan 进度摘要:各状态计数 + 百分比。
iex> plan = CMDC.Plan.new("t", ["a", "b", "c", "d"])
iex> {:ok, plan} = CMDC.Plan.step_completed(plan, "step-1")
iex> {:ok, plan} = CMDC.Plan.step_failed(plan, "step-2", "err")
iex> {:ok, plan} = CMDC.Plan.step_skipped(plan, "step-3")
iex> CMDC.Plan.progress(plan)
%{completed: 1, failed: 1, skipped: 1, pending: 1, total: 4, percentage: 75.0}
删除指定 id 的步骤。不存在返回 {:error, :not_found}。
iex> plan = CMDC.Plan.new("t", ["a", "b"])
iex> {:ok, plan} = CMDC.Plan.remove_step(plan, "step-1")
iex> Enum.map(plan.steps, & &1.description)
["b"]
完全替换 Plan 的步骤列表,重新从 step-1 开始编号。
当 LLM 根据新情况(障碍 / 新信息 / 用户反馈)重新生成整份计划时使用。
执行后 approved_at 会被清空(需要重新批准),metadata[:last_replanned_at]
记录重规划时间戳。
iex> plan = CMDC.Plan.new("旅行", ["订机票", "订酒店"])
iex> plan = CMDC.Plan.approve(plan)
iex> plan = CMDC.Plan.replace_steps(plan, ["重新选择目的地", "订机票", "订酒店"])
iex> length(plan.steps)
3
iex> plan.approved_at
nil
@spec step_completed(t() | {:ok, t()}, String.t(), keyword()) :: {:ok, t()} | {:error, :not_found | {:illegal_transition, CMDC.Plan.Step.status(), atom()}}
标记某个 step 已完成,可附带结果数据(:pending | :in_progress → :completed)。
Plan.step_completed/2 为 任务 11.16 定义的 API,允许 Tool/外部调用方
在完成某个步骤后批量推进计划。
iex> plan = CMDC.Plan.new("t", ["a", "b"])
iex> {:ok, plan} = CMDC.Plan.step_completed(plan, "step-1", result: "ok")
iex> step = CMDC.Plan.get_step(plan, "step-1")
iex> step.status
:completed
iex> step.result
"ok"
@spec step_failed(t() | {:ok, t()}, String.t(), String.t()) :: {:ok, t()} | {:error, :not_found | {:illegal_transition, CMDC.Plan.Step.status(), atom()}}
标记某个 step 已失败。
iex> plan = CMDC.Plan.new("t", ["a"])
iex> {:ok, plan} = CMDC.Plan.step_failed(plan, "step-1", "网络超时")
iex> CMDC.Plan.get_step(plan, "step-1").error
"网络超时"
@spec step_skipped(t() | {:ok, t()}, String.t(), String.t() | nil) :: {:ok, t()} | {:error, :not_found | {:illegal_transition, CMDC.Plan.Step.status(), atom()}}
标记某个 step 已跳过(:pending → :skipped)。
@spec step_started(t() | {:ok, t()}, String.t()) :: {:ok, t()} | {:error, :not_found | {:illegal_transition, CMDC.Plan.Step.status(), atom()}}
标记某个 step 已开始执行(:pending → :in_progress)。
iex> plan = CMDC.Plan.new("t", ["a"])
iex> {:ok, plan} = CMDC.Plan.step_started(plan, "step-1")
iex> CMDC.Plan.get_step(plan, "step-1").status
:in_progress
将 Plan 渲染为 Markdown Checklist。:completed 为 [x],其他状态附带 tag。
iex> plan = CMDC.Plan.new("发布", ["写 CHANGELOG", "发布"])
iex> {:ok, plan} = CMDC.Plan.step_completed(plan, "step-1")
iex> CMDC.Plan.to_markdown(plan)
"- [x] 写 CHANGELOG\n- [ ] 发布"
将 Plan 渲染为注入 system prompt 的结构化段落。含目标、进度摘要、全部步骤、当前步骤。