Packages

Secure BEAM sandbox runtime for LLM code mode and MCP aggregation. Run concurrent LLM/tool clients safely while agents orchestrate approved tools, call upstream MCP/OpenAPI servers, and transform data.

Current section

Files

Jump to
ptc_runner lib ptc_runner meta_planner.ex
Raw

lib/ptc_runner/meta_planner.ex

defmodule PtcRunner.MetaPlanner do
@moduledoc """
Generate and repair execution plans for multi-agent workflows.
The MetaPlanner is responsible for:
- Generating initial plans from mission descriptions
- Repairing plans when tasks fail with `:replan` strategy
## Initial Planning
Generate a plan from a natural language mission:
{:ok, plan} = MetaPlanner.plan("Research AAPL stock price and write a summary",
llm: my_llm,
available_tools: %{
"search" => "Search the web for information",
"fetch_price" => "Fetch current stock price for a symbol. Returns {price, currency}",
"summarize" => "Summarize a list of documents into key points"
}
)
## Replanning (Tail Repair)
When a task fails verification with `on_verification_failure: :replan`,
the MetaPlanner generates a "repair plan" that:
1. Preserves completed task IDs (so PlanRunner skips them)
2. Redesigns the failed task based on the diagnosis
3. May restructure downstream tasks
Example:
# Original execution failed at task "fetch_prices"
failure_context = %{
task_id: "fetch_prices",
task_output: %{"prices" => []},
diagnosis: "Expected at least 5 price entries, got 0"
}
{:ok, repair_plan} = MetaPlanner.replan(
"Compare stock prices for AAPL, GOOGL, MSFT",
%{"fetch_symbols" => ["AAPL", "GOOGL", "MSFT"]},
failure_context,
llm: my_llm
)
# Execute repair plan with initial_results to skip completed tasks
PlanRunner.execute(repair_plan,
llm: my_llm,
initial_results: %{"fetch_symbols" => ["AAPL", "GOOGL", "MSFT"]}
)
"""
alias PtcRunner.Plan
alias PtcRunner.Prompts
alias PtcRunner.SubAgent
require Logger
@type tool_descriptions :: %{String.t() => String.t()}
@type failure_context :: %{
task_id: String.t(),
task_output: term(),
diagnosis: String.t()
}
# ============================================================================
# Initial Planning
# ============================================================================
@doc """
Generate an execution plan from a natural language mission.
Takes a mission description and available tools, then generates a structured
plan that can be executed by PlanRunner.
## Parameters
- `mission` - Natural language description of what to accomplish
- `opts` - Options including `:llm` callback (required)
## Options
- `llm` - Required. LLM callback function
- `available_tools` - Map of tool_name => description (recommended)
- `timeout` - Timeout for plan generation (default: 30_000)
- `constraints` - Optional string with additional constraints or guidelines
- `validation_errors` - Optional. List of validation issues from a previous attempt (for self-correction)
## Returns
- `{:ok, plan}` - Parsed execution plan
- `{:error, reason}` - Generation or parsing failed
## Example
{:ok, plan} = MetaPlanner.plan("Research AAPL stock and summarize findings",
llm: my_llm,
available_tools: %{
"search" => "Search the web. Input: query string. Output: list of results",
"fetch_price" => "Get stock price. Input: symbol. Output: {price, change}",
"summarize" => "Summarize text. Input: text. Output: summary string"
}
)
"""
@spec plan(String.t(), keyword()) :: {:ok, Plan.t()} | {:error, term()}
def plan(mission, opts) do
llm = Keyword.fetch!(opts, :llm)
timeout = Keyword.get(opts, :timeout, 30_000)
available_tools = Keyword.get(opts, :available_tools, %{})
constraints = Keyword.get(opts, :constraints)
validation_errors = Keyword.get(opts, :validation_errors)
prompt = build_plan_prompt(mission, available_tools, constraints, validation_errors)
agent =
SubAgent.new(
prompt: prompt,
signature: ":map",
output: :json,
schema: plan_schema(),
max_turns: 1,
retry_turns: 2,
timeout: timeout,
system_prompt: %{
# Disable default PTC-Lisp reference - we're generating JSON, not Lisp programs.
# The verification predicate syntax is included in the suffix.
language_spec: "",
output_format: "",
prefix:
"You are a workflow architect. Design execution plans for multi-agent workflows.\n\n",
suffix:
"\n\n#{Prompts.signature_guide()}\n\n#{Prompts.verification_guide()}\n\n#{Prompts.planning_examples()}"
}
)
Logger.info("MetaPlanner: Generating plan for mission: #{String.slice(mission, 0, 100)}")
case SubAgent.run(agent, llm: llm) do
{:ok, step} ->
raw_plan = step.return
Logger.debug("MetaPlanner: Raw plan: #{inspect(raw_plan, limit: 200)}")
case Plan.parse(raw_plan) do
{:ok, plan} ->
Logger.info("MetaPlanner: Plan generated with #{length(plan.tasks)} task(s)")
{:ok, plan}
{:error, reason} ->
Logger.error("MetaPlanner: Failed to parse plan: #{inspect(reason)}")
{:error, {:parse_error, reason}}
end
{:error, step} ->
Logger.error("MetaPlanner: Failed to generate plan: #{inspect(step.fail)}")
{:error, {:generation_error, step.fail}}
end
end
defp build_plan_prompt(mission, available_tools, constraints, validation_errors) do
tools_section = format_available_tools(available_tools)
constraints_section = format_constraints(constraints)
validation_section = format_validation_errors(validation_errors)
"""
## Mission
#{mission}
#{tools_section}#{constraints_section}#{validation_section}
## Plan Design Guidelines
1. **Decompose first** - before creating tasks, work backwards from the desired outcome:
- What intermediate results must be produced to accomplish this mission?
- Does the mission require any computations, comparisons, or derived values?
- For each intermediate result, what specific inputs are needed?
Then create tasks that produce each required intermediate result.
2. **Use appropriate tools** - only reference tools from the available list
3. **Define dependencies** - tasks that need results from other tasks should declare depends_on
4. **Add verification** - include predicates to validate task outputs (see system prompt for syntax)
5. **Keep it simple** - prefer fewer, well-defined tasks over many small ones
6. **Create computation agents when needed** - if the mission requires calculations, derived metrics,
or data transformations, create a dedicated agent with no tools and a precise signature for that step.
Don't bury computation inside synthesis — make it an explicit task with typed inputs and outputs.
IMPORTANT: Set `"output": "ptc_lisp"` on computation tasks. This makes the agent write executable
PTC-Lisp programs with verified arithmetic instead of computing values mentally. The agent's prompt
must reference PTC-Lisp (not JSON) — instruct it to extract values into `let` bindings and use
arithmetic expressions (`/`, `*`, `+`, `-`).
PTC-Lisp also supports conditionals, string operations, and collection functions (`map`, `filter`,
`reduce`, `sort-by`) for data transformations.
Computation task signatures should contain only numeric and boolean fields — no `:string` fields
for analysis or interpretation. Prose belongs in the downstream synthesis gate, which can read
the computed numbers and write accurate narrative. String fields in computation signatures tempt
the LLM to hardcode numbers in text before the interpreter verifies them.
7. **Quality gates** - Set `"quality_gate": true` on computation/analysis tasks that need exact upstream
values (e.g., financial calculations, ratio computations). Skip on synthesis tasks that just
narrate upstream results — they don't need precise data validation.
8. **Extract and type-check between tasks** - When a tool-using task returns a flexible schema
(e.g., generic findings with `value: any`), you MUST insert a `direct` task to extract each specific
typed value needed downstream. Without this step, computation tasks receive raw findings arrays and
fail to locate the correct values. Each direct-extract task should filter findings by label, extract
the value, and declare a typed signature (e.g., `:float`). This costs zero LLM calls, gives
computation tasks clean typed inputs, and catches missing data early via signature validation.
REQUIRED pattern: search → direct extract (one per value) → compute.
## Task Types
- **Regular tasks**: Use an agent with tools to accomplish work
- **Direct tasks**: When you already know the exact tool call or computation, use `"agent": "direct"` and
write PTC-Lisp code in the `input` field. This executes instantly without an LLM call. All base tools
are available. Upstream results are accessible via `data/results`. Use this whenever the plan can
determine the precise tool arguments upfront — it eliminates unnecessary LLM round-trips.
Example: `{"id": "get_data", "agent": "direct", "input": "(tool/search {:query \"average temperature\"})"}`
With upstream data: `{"id": "transform", "agent": "direct", "input": "(map :name (get data/results \"lookup\"))", "depends_on": ["lookup"]}`
**Data extraction pattern** — use `direct` tasks to extract typed values from flexible tool output.
Always set `"on_failure": "replan"` on direct extraction tasks so data-shape mismatches trigger
replanning instead of halting:
```
{"id": "search_population", "agent": "researcher", "input": "Find city population"},
{"id": "extract_population", "agent": "direct",
"input": "(let [findings (get-in data/results [\"search_population\" \"findings\"])] (let [entry (first (filter (fn [f] (str/includes? (get f \"label\") \"population\")) findings))] (if entry (get entry \"value\") nil)))",
"depends_on": ["search_population"], "signature": ":float", "on_failure": "replan"}
```
This gives downstream tasks a clean `:float` instead of a generic findings array.
**CRITICAL threading rule**: `filter`, `map`, and `reduce` take the predicate/function FIRST and
collection LAST: `(filter pred coll)`. Do NOT use `(-> coll (filter pred))` — that puts the
collection in predicate position. Use nested calls instead: `(first (filter pred coll))`.
**PTC-Lisp restrictions**: No `try`/`catch`/`throw` — use `(if cond value nil)` for safe access.
No `read-string`, `resolve`, `eval` — only static expressions.
- **Synthesis gates**: Compress/summarize results from multiple upstream tasks (type: "synthesis_gate")
When designing a synthesis_gate, you MUST specify a `signature` that defines the exact output structure
(e.g., `"{stocks [{symbol :string, price :float, currency :string}]}"`). This ensures machine-readable results.
## Output Format
Return a JSON plan:
```json
{
"tasks": [
{"id": "search_data", "agent": "researcher", "input": "Find the population of Berlin"},
{"id": "extract_population", "agent": "direct",
"input": "(let [findings (get-in data/results [\"search_data\" \"findings\"])] (let [entry (first (filter (fn [f] (str/includes? (get f \"label\") \"population\")) findings))] (if entry (get entry \"value\") nil)))",
"depends_on": ["search_data"], "signature": ":float", "on_failure": "replan"},
{"id": "compute_density", "agent": "calculator",
"input": "Compute population density from upstream values",
"depends_on": ["extract_population"], "output": "ptc_lisp",
"signature": "{density :float}", "quality_gate": true},
{"id": "final_answer", "agent": "default",
"input": "Summarize the result",
"depends_on": ["compute_density"], "type": "synthesis_gate",
"signature": "{answer :string, density :float}"}
],
"agents": {
"researcher": {"prompt": "You search for data.", "tools": ["search"]},
"calculator": {"prompt": "You compute values using PTC-Lisp let bindings and arithmetic.", "tools": []}
}
}
```
Note the `direct` extract task between search and compute — this is REQUIRED when tools return flexible schemas.
## Important Rules
- Task IDs must be unique and descriptive (e.g., "fetch_prices", "compute_ratio", "summarize_research")
- Only reference tools that are in the available tools list
- Set `"output": "ptc_lisp"` on computation tasks (arithmetic, ratios, aggregations) so the interpreter
verifies the math. Omit `output` for tasks with tools (auto-detects to ptc_lisp) or pure Q&A/synthesis
(auto-detects to json).
- Computation agents receive upstream task results as context and produce PTC-Lisp programs.
Always give these agents a precise numeric signature (e.g., `"{ratio :float, growth_pct :float}"`).
Do NOT include `:string` fields for analysis — leave interpretation to the synthesis gate.
Their prompt should instruct them to use `let` bindings and arithmetic expressions for calculations
- Use `on_verification_failure: "replan"` for critical tasks that may need strategy changes
- Set `"on_failure": "replan"` on tasks where the agent examines fetched data and might
determine it's insufficient. Include this instruction in the agent's prompt:
"If the data you received does not contain the specific information needed for your task,
call (fail \"reason\") explaining what is missing and where it might be found instead."
Generate the execution plan now:
"""
end
defp format_available_tools(tools) when map_size(tools) == 0 do
"""
## Available Tools
No external tools available. Tasks will process data and produce results via LLM.
"""
end
defp format_available_tools(tools) do
tool_list =
Enum.map_join(tools, "\n", fn {name, description} ->
"- **#{name}**: #{description}"
end)
"""
## Available Tools
#{tool_list}
"""
end
defp format_constraints(nil), do: ""
defp format_constraints(constraints) do
"""
## Additional Constraints
#{constraints}
"""
end
# JSON Schema for plan structure - helps LLMs generate valid plans
defp plan_schema do
%{
"type" => "object",
"properties" => %{
"tasks" => %{
"type" => "array",
"items" => %{
"type" => "object",
"properties" => %{
"id" => %{"type" => "string", "description" => "Unique task identifier"},
"agent" => %{"type" => "string", "description" => "Agent to execute the task"},
"input" => %{"type" => "string", "description" => "Task description/instructions"},
"depends_on" => %{
"type" => "array",
"items" => %{"type" => "string"},
"description" => "IDs of tasks this depends on"
},
"verification" => %{
"type" => "string",
"description" => "PTC-Lisp predicate to verify output"
},
"on_verification_failure" => %{
"type" => "string",
"enum" => ["retry", "replan", "fail"],
"description" => "Action on verification failure"
},
"on_failure" => %{
"type" => "string",
"enum" => ["stop", "skip", "replan"],
"description" =>
"Action when the agent itself reports failure via (fail reason). Use replan for tasks where the agent examines data and might determine it is insufficient."
},
"type" => %{
"type" => "string",
"description" => "Task type (e.g., synthesis_gate)"
},
"output" => %{
"type" => "string",
"enum" => ["ptc_lisp", "json"],
"description" =>
"Execution mode: ptc_lisp for computation tasks (verified arithmetic), json for Q&A/synthesis. Default: auto-detect (tools → ptc_lisp, no tools → json)"
},
"signature" => %{
"type" => "string",
"description" => "Output signature for JSON mode (REQUIRED for synthesis_gate)"
},
"quality_gate" => %{
"type" => "boolean",
"description" =>
"Enable data sufficiency check before this task. Use on computation/analysis tasks needing exact upstream values."
}
},
"required" => ["id", "input"]
},
"description" => "List of tasks to execute"
},
"agents" => %{
"type" => "object",
"additionalProperties" => %{
"type" => "object",
"properties" => %{
"prompt" => %{"type" => "string", "description" => "System prompt for the agent"},
"tools" => %{
"type" => "array",
"items" => %{"type" => "string"},
"description" => "Tool names available to this agent"
}
}
},
"description" => "Agent definitions keyed by agent name"
}
},
"required" => ["tasks"]
}
end
# ============================================================================
# Replanning (Tail Repair)
# ============================================================================
@doc """
Generate a repair plan after a task fails verification.
The repair plan includes all task IDs (including completed ones) so that
PlanRunner can use the `initial_results` option to skip already-completed tasks.
## Parameters
- `mission` - Original mission description
- `completed_results` - Map of task_id => result for successful tasks
- `failure_context` - Details about the failed task
- `opts` - Options including `:llm` callback (required)
## Options
- `llm` - Required. LLM callback function
- `timeout` - Timeout for plan generation (default: 30_000)
- `original_plan` - Optional. The original plan that failed (for context)
- `constraints` - Optional. Planning constraints to preserve during replanning
- `validation_errors` - Optional. List of validation issues from a previous repair attempt
## Returns
- `{:ok, plan}` - Parsed repair plan
- `{:error, reason}` - Generation or parsing failed
"""
@spec replan(String.t(), map(), failure_context(), keyword()) ::
{:ok, Plan.t()} | {:error, term()}
def replan(mission, completed_results, failure_context, opts) do
llm = Keyword.fetch!(opts, :llm)
timeout = Keyword.get(opts, :timeout, 30_000)
original_plan = Keyword.get(opts, :original_plan)
constraints = Keyword.get(opts, :constraints)
validation_errors = Keyword.get(opts, :validation_errors)
trial_history = Keyword.get(opts, :trial_history, [])
# Build the healer prompt
prompt =
build_replan_prompt(
mission,
completed_results,
failure_context,
original_plan,
constraints,
validation_errors,
trial_history
)
# Create a SubAgent to generate the repair plan
# Use condensed reminder since LLM likely saw full guide in initial plan
agent =
SubAgent.new(
prompt: prompt,
signature: ":map",
output: :json,
schema: plan_schema(),
max_turns: 1,
retry_turns: 2,
timeout: timeout,
system_prompt: %{
# Disable default PTC-Lisp reference - we're generating JSON, not Lisp programs
language_spec: "",
output_format: "",
prefix: "You are a workflow repair specialist. Fix failed multi-agent plans.\n\n",
suffix: "\n\n#{Prompts.signature_guide()}\n\n#{Prompts.verification_reminder()}"
}
)
Logger.info(
"MetaPlanner: Generating repair plan for failed task '#{failure_context.task_id}'"
)
case SubAgent.run(agent, llm: llm) do
{:ok, step} ->
raw_plan = step.return
Logger.debug("MetaPlanner: Raw repair plan: #{inspect(raw_plan, limit: 200)}")
case Plan.parse(raw_plan) do
{:ok, plan} ->
Logger.info("MetaPlanner: Repair plan generated with #{length(plan.tasks)} task(s)")
{:ok, plan}
{:error, reason} ->
Logger.error("MetaPlanner: Failed to parse repair plan: #{inspect(reason)}")
{:error, {:parse_error, reason}}
end
{:error, step} ->
Logger.error("MetaPlanner: Failed to generate repair plan: #{inspect(step.fail)}")
{:error, {:generation_error, step.fail}}
end
end
defp build_replan_prompt(
mission,
completed_results,
failure_context,
original_plan,
constraints,
validation_errors,
trial_history
) do
completed_summary = format_completed_results(completed_results)
original_plan_section = format_original_plan(original_plan)
constraints_section = format_constraints(constraints)
validation_section = format_validation_errors(validation_errors)
trial_history_section = format_trial_history(trial_history)
"""
You are a workflow repair specialist. A multi-agent plan has failed and needs to be fixed.
## Original Mission
#{mission}
#{constraints_section}
## What Has Already Succeeded
The following tasks completed successfully. Keep their exact IDs in your repair plan
so they can be reused (the executor will skip them automatically):
#{completed_summary}
## What Failed
Task "#{failure_context.task_id}" reports: "#{failure_context.diagnosis}"
Task Output: #{format_value(failure_context.task_output)}
Adjust the plan to address this specific blocker.
#{original_plan_section}#{trial_history_section}#{validation_section}
## Your Task
Generate a REPAIR PLAN that:
1. **Preserves successful work**: Include tasks with the same IDs as the completed tasks above.
These will be automatically skipped during execution.
2. **Fixes the failed task**: The task "#{failure_context.task_id}" failed because:
"#{failure_context.diagnosis}"
Redesign this task or replace it with a different approach.
If a task reports it could not find data in a specific section, do NOT simply assign
the same section to a different agent. Look for alternative sources mentioned in the
diagnosis (e.g., "See Note 12" hints to fetch that note instead).
3. **Completes the mission**: Ensure downstream tasks can still accomplish the original goal.
## Important Guidelines
- DO NOT change the IDs of successful tasks (they need to match for skip-if-present)
- DO change the failed task's approach based on the diagnosis
- You MAY add new tasks if a different strategy is needed
- You MAY remove or restructure downstream tasks if they depend on the failed approach
- Include verification predicates to prevent the same failure (see system prompt for syntax)
- Set `"quality_gate": true` on computation/analysis tasks that need exact upstream values
(e.g., financial calculations). Skip on synthesis tasks that just narrate upstream results.
- You MUST use `"agent": "direct"` tasks to extract and type-check individual values from
flexible tool output (e.g., findings with `value: any`) before passing to computation tasks.
Pattern: search → direct extract (one per value, with typed signature like `:float`) → compute.
Always set `"on_failure": "replan"` on direct extraction tasks.
This costs zero LLM calls and catches missing data early.
- **CRITICAL**: `"agent": "direct"` task inputs MUST be valid PTC-Lisp code, NOT natural language.
Upstream results are accessible via `data/results`. Example:
```
{"id": "extract_value", "agent": "direct",
"input": "(let [findings (get-in data/results [\"search_task\" \"findings\"])] (let [entry (first (filter (fn [f] (str/includes? (get f \"label\") \"revenue\")) findings))] (if entry (get entry \"value\") nil)))",
"depends_on": ["search_task"], "signature": ":float", "on_failure": "replan"}
```
If the upstream data shape is unknown, inspect the actual completed results above and
write Lisp that matches the real structure.
- **PTC-Lisp restrictions for direct tasks**:
- No `try`/`catch`/`throw` — use `(if cond value nil)` for safe access
- No `read-string`, `resolve`, `eval` — only static expressions
- `filter`/`map`/`reduce` take function FIRST, collection LAST: `(filter pred coll)`.
Do NOT use `(-> coll (filter pred))` — use nested calls: `(first (filter pred coll))`
## Impossible Missions
If the mission objective is impossible or the failure diagnosis indicates a non-recoverable
error (e.g., requested resource doesn't exist, invalid credentials, impossible constraints),
return a plan with a single task:
```json
{
"tasks": [{"id": "mission_impossible", "input": "Explain why the mission cannot be completed"}],
"agents": {}
}
```
## Output Format
Return a JSON plan:
```json
{
"tasks": [
{
"id": "task_id",
"agent": "agent_type",
"input": "task description",
"depends_on": ["dependency_ids"],
"output": "ptc_lisp or json (optional, for computation tasks use ptc_lisp)",
"signature": "{field :type}",
"quality_gate": true,
"verification": "(optional Lisp predicate)",
"on_verification_failure": "retry"
}
],
"agents": {
"agent_type": {
"prompt": "agent system prompt",
"tools": ["tool_names"]
}
}
}
```
Generate the repair plan now:
"""
end
defp format_validation_errors(nil), do: ""
defp format_validation_errors([]), do: ""
defp format_validation_errors(errors) do
error_list =
Enum.map_join(errors, "\n", fn issue ->
"- [#{issue.category}] #{issue.message}"
end)
"""
## CRITICAL: Previous Plan Had Validation Errors
Your previous plan was rejected because it had structural problems.
You MUST fix these issues in your new plan:
#{error_list}
Pay close attention to:
- Circular dependencies (task A depends on B, B depends on A)
- Missing dependencies (referencing task IDs that don't exist)
- Missing agents (using agent names not defined in the agents section)
- Duplicate task IDs
- PTC-Lisp syntax in `direct` task inputs and verification predicates
(use `data/result` not `result`, `data/results` not `results`; ensure balanced parens)
"""
end
defp format_completed_results(results) when map_size(results) == 0 do
"(No tasks completed yet)"
end
defp format_completed_results(results) do
Enum.map_join(results, "\n", fn {task_id, value} ->
formatted_value = format_value(value)
"- Task '#{task_id}': #{String.slice(formatted_value, 0, 200)}"
end)
end
defp format_original_plan(nil), do: ""
defp format_original_plan(%Plan{} = plan) do
task_summaries =
Enum.map_join(plan.tasks, "\n", fn task ->
deps =
if task.depends_on == [],
do: "",
else: " (depends: #{Enum.join(task.depends_on, ", ")})"
input = task.input |> to_string() |> String.slice(0, 200) |> escape_placeholders()
" - #{task.id}: #{input}#{deps}"
end)
"""
## Original Plan Structure (for reference)
#{task_summaries}
"""
end
defp format_original_plan(_), do: ""
# Escape {{placeholder}} syntax so it doesn't trigger SubAgent prompt validation
defp escape_placeholders(text), do: String.replace(text, "{{", "{ {")
defp format_value(value) when is_binary(value), do: value
defp format_value(value) do
case Jason.encode(value) do
{:ok, json} -> json
{:error, _} -> inspect(value)
end
end
# ============================================================================
# Trial History Formatting
# ============================================================================
@doc """
Format trial history for inclusion in the replan prompt.
Returns an empty string for empty history, otherwise formats each attempt
with approach, output, and diagnosis details plus a self-reflection section.
"""
@spec format_trial_history([map()]) :: String.t()
def format_trial_history([]), do: ""
def format_trial_history(history) when is_list(history) do
attempts = Enum.map_join(history, "\n---\n\n", &format_attempt/1)
"""
## Trial & Error History
The following attempts have already been made to complete this mission.
DO NOT repeat these failed approaches.
#{attempts}
## Self-Reflection Required
Before generating your repair plan, analyze the trial history above:
1. **Pattern Recognition**: Are the same errors recurring?
2. **Root Cause Analysis**: Is it a tool limitation, bad input, or impossible task?
3. **Strategy Shift**: How should the approach fundamentally change?
Your repair plan MUST address the root cause, not just retry with minor changes.
"""
end
defp format_attempt(%{attempt: attempt, task_id: task_id} = record) do
approach = Map.get(record, :approach, "(not recorded)")
output = Map.get(record, :output, "(not recorded)")
diagnosis = Map.get(record, :diagnosis, "(not recorded)")
"""
### Attempt #{attempt} - Task "#{task_id}"
**Approach:**
```
#{approach}
```
**Output:**
```
#{output}
```
**Failure Diagnosis:** "#{diagnosis}"
"""
end
end