Parsed execution plan for multi-agent workflows.
Plans are generated by PtcRunner.MetaPlanner as JSON and parsed into
a normalized structure. This module handles parsing, validation, and
sanitization of LLM-generated plans.
LLM-Generated JSON Structure
The MetaPlanner produces JSON with this structure:
%{
"tasks" => [
%{
"id" => "fetch_data",
"agent" => "researcher",
"input" => "Find quarterly revenue",
"depends_on" => ["prior_task_id"],
"output" => "ptc_lisp",
"signature" => "{revenue :float}",
"verification" => "(number? (get data/result "revenue"))",
"on_verification_failure" => "retry",
"on_failure" => "stop",
"max_retries" => 1,
"critical" => true,
"type" => "task"
}
],
"agents" => %{
"researcher" => %{
"prompt" => "You are a researcher...",
"tools" => ["search"]
}
}
}Task Fields
| Field | Type | Default | Description |
|---|---|---|---|
id | string | "task_N" | Unique task identifier |
agent | string | "default" | Agent to execute the task |
input | string | "" | Task description or PTC-Lisp code (for "direct" agent) |
depends_on | list | [] | IDs of upstream tasks (also: requires, after) |
output | string | nil | "ptc_lisp", "json", or nil (auto-detect) |
signature | string | nil | Output signature (e.g., "{name :string}") |
verification | string | nil | PTC-Lisp predicate to validate output |
on_verification_failure | string | "replan" | "stop", "skip", "retry", or "replan" |
on_failure | string | "stop" | "stop", "skip", "retry", or "replan" |
max_retries | integer | 1 | Max retry attempts |
critical | boolean | true | Whether failure stops the plan |
type | string | "task" | "task", "synthesis_gate", or "human_review" |
Parsing
parse/1 handles common LLM variations:
- Task keys:
tasks,steps,workflow,plan.steps - Agent keys:
agents,workers - Dependency keys:
depends_on,requires,after
Sanitization
sanitize/1 validates verification predicates using PtcRunner.Lisp.validate/1.
Invalid predicates (undefined variables, parse errors, arity mistakes) are
stripped with a warning rather than failing the plan.
Summary
Functions
Group tasks into parallel execution phases by dependency level.
Parse an LLM-generated plan into a normalized structure.
Sanitize a plan by removing invalid verification predicates.
Sort tasks by dependencies (topological sort).
Validate a parsed plan for structural integrity.
Types
@type on_failure() :: :stop | :skip | :retry | :replan
@type on_verification_failure() :: :stop | :skip | :retry | :replan
@type output_mode() :: :ptc_lisp | :json | nil
@type t() :: %PtcRunner.Plan{ agents: %{required(String.t()) => agent_spec()}, tasks: [task()] }
@type task() :: %{ id: String.t(), agent: String.t(), input: term(), depends_on: [String.t()], on_failure: on_failure(), max_retries: non_neg_integer(), critical: boolean(), type: task_type(), output: output_mode(), signature: String.t() | nil, verification: String.t() | nil, on_verification_failure: on_verification_failure(), quality_gate: boolean() | nil }
@type task_type() :: :task | :synthesis_gate | :human_review
Functions
Group tasks into parallel execution phases by dependency level.
Tasks with no dependencies are level 0 (can run first in parallel). Tasks whose dependencies are all level 0 are level 1, etc.
Returns a list of phases, where each phase is a list of tasks that can execute in parallel.
Examples
iex> tasks = [
...> %{id: "a", depends_on: [], agent: "x", input: "", on_failure: :stop, max_retries: 1, critical: true},
...> %{id: "b", depends_on: [], agent: "x", input: "", on_failure: :stop, max_retries: 1, critical: true},
...> %{id: "c", depends_on: ["a", "b"], agent: "x", input: "", on_failure: :stop, max_retries: 1, critical: true}
...> ]
iex> phases = PtcRunner.Plan.group_by_level(tasks)
iex> length(phases)
2
iex> phases |> hd() |> Enum.map(& &1.id) |> Enum.sort()
["a", "b"]
iex> phases |> Enum.at(1) |> Enum.map(& &1.id)
["c"]
Parse an LLM-generated plan into a normalized structure.
Handles common variations in how LLMs structure plans:
- Tasks can be under
tasks,steps,workflow, orplan.steps - Agents can be under
agentsorworkers - Dependencies can be
depends_on,requires, orafter
Examples
iex> raw = %{"tasks" => [%{"id" => "t1", "agent" => "researcher", "input" => "test"}]}
iex> {:ok, plan} = PtcRunner.Plan.parse(raw)
iex> length(plan.tasks)
1
iex> raw = %{"steps" => [%{"id" => "s1", "action" => "search"}]}
iex> {:ok, plan} = PtcRunner.Plan.parse(raw)
iex> hd(plan.tasks).id
"s1"
@spec sanitize(t()) :: {t(), [validation_issue()]}
Sanitize a plan by removing invalid verification predicates.
Checks each task's verification predicate and removes any that:
- Fail to parse
- Use undefined variables (e.g.,
resultinstead ofdata/result)
This is a pragmatic approach: rather than failing on LLM-generated invalid predicates, we strip them so the plan can execute without verification.
Returns {plan, warnings} where warnings list any predicates that were removed.
Sort tasks by dependencies (topological sort).
Returns tasks in an order where all dependencies come before dependents. Raises if there's a cycle.
Examples
iex> tasks = [
...> %{id: "t2", depends_on: ["t1"], agent: "a", input: ""},
...> %{id: "t1", depends_on: [], agent: "a", input: ""}
...> ]
iex> sorted = PtcRunner.Plan.topological_sort(tasks)
iex> Enum.map(sorted, & &1.id)
["t1", "t2"]
@spec validate(t()) :: :ok | {:error, [validation_issue()]}
Validate a parsed plan for structural integrity.
Checks for:
- Circular dependencies (cycles in task graph)
- Missing dependency references (depends_on non-existent task)
- Missing agent references (task references non-existent agent)
- Duplicate task IDs
Examples
iex> plan = %PtcRunner.Plan{tasks: [%{id: "a", depends_on: ["b"], agent: "x", input: ""}]}
iex> {:error, issues} = PtcRunner.Plan.validate(plan)
iex> hd(issues).category
:missing_dependency
iex> plan = %PtcRunner.Plan{tasks: [
...> %{id: "a", depends_on: ["b"], agent: "x", input: ""},
...> %{id: "b", depends_on: ["a"], agent: "x", input: ""}
...> ]}
iex> {:error, issues} = PtcRunner.Plan.validate(plan)
iex> hd(issues).category
:cycle_detected