PtcRunner.Plan (PtcRunner v0.7.0)

Copy Markdown View Source

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

FieldTypeDefaultDescription
idstring"task_N"Unique task identifier
agentstring"default"Agent to execute the task
inputstring""Task description or PTC-Lisp code (for "direct" agent)
depends_onlist[]IDs of upstream tasks (also: requires, after)
outputstringnil"ptc_lisp", "json", or nil (auto-detect)
signaturestringnilOutput signature (e.g., "{name :string}")
verificationstringnilPTC-Lisp predicate to validate output
on_verification_failurestring"replan""stop", "skip", "retry", or "replan"
on_failurestring"stop""stop", "skip", "retry", or "replan"
max_retriesinteger1Max retry attempts
criticalbooleantrueWhether failure stops the plan
typestring"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

agent_spec()

@type agent_spec() :: %{prompt: String.t(), tools: [String.t()]}

on_failure()

@type on_failure() :: :stop | :skip | :retry | :replan

on_verification_failure()

@type on_verification_failure() :: :stop | :skip | :retry | :replan

output_mode()

@type output_mode() :: :ptc_lisp | :json | nil

t()

@type t() :: %PtcRunner.Plan{
  agents: %{required(String.t()) => agent_spec()},
  tasks: [task()]
}

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
}

task_type()

@type task_type() :: :task | :synthesis_gate | :human_review

validation_issue()

@type validation_issue() :: %{
  severity: :error | :warning,
  category: atom(),
  message: String.t(),
  task_id: String.t() | nil
}

Functions

group_by_level(tasks)

@spec group_by_level([task()]) :: [[task()]]

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(raw_plan)

@spec parse(map()) :: {:ok, t()} | {:error, term()}

Parse an LLM-generated plan into a normalized structure.

Handles common variations in how LLMs structure plans:

  • Tasks can be under tasks, steps, workflow, or plan.steps
  • Agents can be under agents or workers
  • Dependencies can be depends_on, requires, or after

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"

sanitize(plan)

@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., result instead of data/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.

topological_sort(tasks)

@spec topological_sort([task()]) :: [task()]

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"]

validate(plan)

@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