Powered by AppSignal & Oban Pro

Prerequisites

reasoning-strategies-compared.livemd

Prerequisites

Complete Your first LLM agent before starting. This notebook is advanced, but it is still meant to run end to end in Livebook.

Setup

Mix.install([
  {{mix_dep:jido}},
  {{mix_dep:jido_ai}},
  {{mix_dep:req_llm}}
])

Logger.configure(level: :warning)

Configure credentials

Set your OpenAI API key. In Livebook, add OPENAI_API_KEY as a Livebook Secret prefixed with LB_.

openai_key = System.get_env("LB_OPENAI_API_KEY") || System.get_env("OPENAI_API_KEY")

configured? =
  if is_binary(openai_key) do
    ReqLLM.put_key(:openai_api_key, openai_key)
    true
  else
    IO.puts("Set OPENAI_API_KEY as a Livebook Secret or environment variable to run this notebook.")
    false
  end

Start the runtime

case Jido.start() do
  {:ok, _} -> :ok
  {:error, {:already_started, _}} -> :ok
end

runtime = Jido.default_instance()

comparison_prompt = """
You are advising a four-person developer-tools team.

Question:
Should the team ship its new command palette this Friday, or delay two weeks?

Known facts:
- The feature is implemented and internally usable.
- Keyboard navigation has one known bug with nested menus.
- Docs are 70% complete.
- Three design partners want early access this month.
- The team can afford one patch release next week.

Return:
1. key tradeoffs
2. your recommendation
3. concrete next steps
"""

tot_prompt = """
Generate three materially different rollout strategies for the new command palette.

Context:
- The team can ship this Friday or delay two weeks.
- Keyboard navigation has one known bug with nested menus.
- Docs are 70% complete.
- Three design partners want early access this month.

Return options that compare speed, product quality, and partner expectations.
"""

Define the strategy agents

Each agent uses the same model and prompt domain. Only the reasoning strategy changes.

defmodule MyApp.ReleaseDecisionCoTAgent do
  use Jido.AI.CoTAgent,
    name: "release_decision_cot_agent",
    description: "Explains a release decision step by step",
    model: "openai:gpt-4o-mini",
    system_prompt: """
    You are a release advisor.
    Reason step by step.
    Separate known facts, assumptions, and recommendations.
    """
end

defmodule MyApp.ReleaseDecisionToTAgent do
  use Jido.AI.ToTAgent,
    name: "release_decision_tot_agent",
    description: "Explores multiple release options before choosing",
    model: "openai:gpt-4o-mini",
    branching_factor: 3,
    max_depth: 3,
    top_k: 3,
    min_depth: 2,
    max_nodes: 30,
    max_duration_ms: 20_000,
    max_parse_retries: 2,
    generation_prompt: """
    You generate multiple distinct rollout strategies.

    Return strict JSON only with this exact shape:
    {"thoughts":[{"id":"t1","content":"..."},{"id":"t2","content":"..."},{"id":"t3","content":"..."}]}

    Rules:
    - do not echo the user prompt
    - do not include markdown
    - each thought must be a distinct rollout strategy
    - keep each thought concise and decision-oriented
    """,
    evaluation_prompt: """
    You evaluate rollout strategies.

    Return strict JSON only with this exact shape:
    {"scores":{"t1":0.82,"t2":0.61,"t3":0.74}}

    Rules:
    - do not include markdown
    - keys must match the provided thought IDs
    - higher scores should favor clearer tradeoffs and stronger recommendations
    """

  def format_candidates(result, limit \\ 3) do
    result
    |> Jido.AI.Reasoning.TreeOfThoughts.Result.top_candidates(limit)
    |> Enum.with_index(1)
    |> Enum.map_join("\n\n", fn {candidate, idx} ->
      score = Map.get(candidate, :score)
      content = Map.get(candidate, :content, "(no content)")

      """
      Option #{idx} (score: #{score})
      #{content}
      """
    end)
  end
end

defmodule MyApp.ReleaseDecisionAdaptiveAgent do
  use Jido.AI.AdaptiveAgent,
    name: "release_decision_adaptive_agent",
    description: "Selects the best reasoning strategy for a product decision",
    model: "openai:gpt-4o-mini",
    default_strategy: :cot,
    available_strategies: [:cot, :tot, :trm]
end

Start one agent per strategy

agent_suffix = System.unique_integer([:positive])

{cot_pid, tot_pid, adaptive_pid} =
  if configured? do
    {:ok, cot_pid} =
      Jido.start_agent(
        runtime,
        MyApp.ReleaseDecisionCoTAgent,
        id: "release-cot-#{agent_suffix}"
      )

    {:ok, tot_pid} =
      Jido.start_agent(
        runtime,
        MyApp.ReleaseDecisionToTAgent,
        id: "release-tot-#{agent_suffix}"
      )

    {:ok, adaptive_pid} =
      Jido.start_agent(
        runtime,
        MyApp.ReleaseDecisionAdaptiveAgent,
        id: "release-adaptive-#{agent_suffix}"
      )

    {cot_pid, tot_pid, adaptive_pid}
  else
    {nil, nil, nil}
  end

Chain-of-Thought

CoT is the linear baseline. It works best when you want one explicit chain of reasoning.

cot_result =
  if configured? do
    {:ok, result} =
      MyApp.ReleaseDecisionCoTAgent.think_sync(
        cot_pid,
        comparison_prompt,
        timeout: 60_000
      )

    result
  else
    "Configure OPENAI_API_KEY to run the comparison cells."
  end

IO.puts(cot_result)

Tree-of-Thoughts

ToT explores multiple branches, scores them, and returns ranked candidates instead of one linear answer. Because it relies on strict structured intermediate output, this section surfaces parse diagnostics instead of crashing if the model fails the JSON contract.

tot_result =
  if configured? do
    MyApp.ReleaseDecisionToTAgent.explore_sync(
      tot_pid,
      tot_prompt,
      timeout: 60_000
    )
  else
    :not_configured
  end
if configured? do
  case tot_result do
    {:ok, result} ->
      IO.puts("Best answer:\n")
      IO.puts(Jido.AI.Reasoning.TreeOfThoughts.Result.best_answer(result))
      IO.puts("\nTop candidates:\n")
      IO.puts(MyApp.ReleaseDecisionToTAgent.format_candidates(result))

    {:error, reason} ->
      IO.puts("Tree-of-Thoughts returned an error instead of ranked candidates.")
      IO.inspect(reason, label: "ToT diagnostic")
  end
else
  :ok
end

Adaptive

Adaptive chooses the reasoning strategy at runtime. Use it when callers should not have to pick CoT versus ToT themselves.

adaptive_result =
  if configured? do
    {:ok, result} =
      MyApp.ReleaseDecisionAdaptiveAgent.ask_sync(
        adaptive_pid,
        comparison_prompt,
        timeout: 60_000
      )

    result
  else
    "Configure OPENAI_API_KEY to run the comparison cells."
  end

IO.puts(adaptive_result)

Inspect which strategy the adaptive agent selected:

selected_strategy =
  if configured? do
    {:ok, server_state} = Jido.AgentServer.state(adaptive_pid)
    server_state.agent.state.selected_strategy
  else
    nil
  end

selected_strategy

Choosing the right strategy

Strategy Best for Tradeoff
CoT Linear decisions, explanation-heavy prompts, debugging One path only
ToT Comparing plans, option generation, scenario analysis Slower and more expensive
Adaptive Mixed workloads where prompt shape changes often Adds strategy selection overhead

Use CoT when you want a direct reasoning trace. Use ToT when you want alternatives compared. Use Adaptive when you want one public interface and do not want callers choosing strategy modules themselves.

Outside Livebook: CLI usage

Run any strategy agent from the terminal with mix jido_ai:

mix jido_ai --agent MyApp.ReleaseDecisionCoTAgent "Should we ship this Friday?"
mix jido_ai --agent MyApp.ReleaseDecisionToTAgent "Compare three rollout options for this feature."
mix jido_ai --agent MyApp.ReleaseDecisionAdaptiveAgent "Should we ship or delay?"

Next steps