Powered by AppSignal & Oban Pro

Introduction to Jido AI Agents

01-jido-agents-intro.livemd

Introduction to Jido AI Agents

Learning Objectives

By the end of this checkpoint, you will:

  • Understand agent-based architecture
  • Learn the plan → act → observe lifecycle
  • Build your first Jido agent
  • Integrate agents with Elixir systems
  • Understand when to use agents vs other patterns

Setup

Mix.install([
  {:jido, "~> 1.0"},
  {:jason, "~> 1.4"},
  {:kino, "~> 0.12"}
])

Concept: What are AI Agents?

AI Agents are autonomous entities that can:

  • Perceive their environment (observe)
  • Decide what actions to take (plan)
  • Act on those decisions (execute)
  • Learn from outcomes (observe feedback)

In the Jido framework, agents follow a structured lifecycle: Plan → Act → Observe

The Agent Lifecycle

Kino.Markdown.new("""
### Plan → Act → Observe

graph LR

A[Directive] --> B[Plan]
B --> C[Act]
C --> D[Observe]
D --> E[Signals]

**1. Plan** - Analyze the directive and create an execution plan
**2. Act** - Execute the plan and generate results
**3. Observe** - Analyze results and emit signals
""")

Your First Agent: Hello World

Let’s build a simple agent that greets users:

defmodule HelloAgent do
  use Jido.Agent,
    name: "hello_agent",
    description: "Greets users with personalized messages",
    schema: [
      name: [type: :string, required: true, doc: "User's name"],
      language: [type: :string, default: "en", doc: "Language code"]
    ]

  alias Jido.Agent.{Directive, Signal}

  @impl Jido.Agent
  def plan(agent, directive) do
    name = Directive.get_param(directive, :name)
    language = Directive.get_param(directive, :language, "en")

    # Create the greeting plan
    plan = %{
      name: name,
      language: language,
      greeting_template: get_template(language)
    }

    {:ok, Agent.put_plan(agent, plan)}
  end

  @impl Jido.Agent
  def act(agent) do
    plan = Agent.get_plan(agent)

    # Generate the greeting
    greeting = String.replace(plan.greeting_template, "{name}", plan.name)

    result = %{
      greeting: greeting,
      language: plan.language,
      timestamp: DateTime.utc_now()
    }

    {:ok, Agent.put_result(agent, result)}
  end

  @impl Jido.Agent
  def observe(agent) do
    result = Agent.get_result(agent)

    # Create observation
    observations = %{
      greeting_length: String.length(result.greeting),
      language_used: result.language
    }

    signal = Signal.new(:greeting_sent, observations)
    {:ok, agent, [signal]}
  end

  ## Private functions

  defp get_template("en"), do: "Hello, {name}! Welcome to Jido agents."
  defp get_template("es"), do: "¡Hola, {name}! Bienvenido a los agentes Jido."
  defp get_template("fr"), do: "Bonjour, {name}! Bienvenue aux agents Jido."
  defp get_template(_), do: "Hello, {name}!"
end

:ok

Running the Agent

Now let’s run our agent:

# Create a directive
directive = Jido.Agent.Directive.new(:greet, params: %{name: "Alice", language: "en"})

# Run the agent
{:ok, agent_result} = Jido.Agent.run(HelloAgent, directive)

# Get the greeting
result = Jido.Agent.get_result(agent_result)
IO.puts(result.greeting)

Try changing the language to “es” or “fr” and re-evaluate!

Interactive Exercise 1.1: Modify the Agent

Add support for more languages:

defmodule HelloAgentExtended do
  use Jido.Agent,
    name: "hello_agent_extended",
    description: "Greets users in multiple languages",
    schema: [
      name: [type: :string, required: true],
      language: [type: :string, default: "en"]
    ]

  alias Jido.Agent.{Directive, Signal}

  @impl Jido.Agent
  def plan(agent, directive) do
    name = Directive.get_param(directive, :name)
    language = Directive.get_param(directive, :language, "en")

    plan = %{
      name: name,
      language: language,
      greeting_template: get_template(language)
    }

    {:ok, Agent.put_plan(agent, plan)}
  end

  @impl Jido.Agent
  def act(agent) do
    plan = Agent.get_plan(agent)
    greeting = String.replace(plan.greeting_template, "{name}", plan.name)

    result = %{
      greeting: greeting,
      language: plan.language
    }

    {:ok, Agent.put_result(agent, result)}
  end

  @impl Jido.Agent
  def observe(agent) do
    result = Agent.get_result(agent)

    observations = %{
      greeting_length: String.length(result.greeting)
    }

    signal = Signal.new(:greeting_sent, observations)
    {:ok, agent, [signal]}
  end

  ## TODO: Add more language templates!
  defp get_template("en"), do: "Hello, {name}!"
  defp get_template("es"), do: "¡Hola, {name}!"
  defp get_template("fr"), do: "Bonjour, {name}!"
  defp get_template("de"), do: "Guten Tag, {name}!"
  defp get_template("ja"), do: "こんにちは、{name}さん!"
  defp get_template(_), do: "Hello, {name}!"
end

# Test it
directive = Jido.Agent.Directive.new(:greet, params: %{name: "Bob", language: "de"})
{:ok, agent} = Jido.Agent.run(HelloAgentExtended, directive)
result = Jido.Agent.get_result(agent)
IO.puts(result.greeting)

Real-World Example: Code Review Agent

Let’s look at a practical agent from labs_jido_agent:

# This demonstrates the Code Review Agent concept
# (Simplified version - see labs_jido_agent for full implementation)

defmodule SimpleCodeReviewAgent do
  use Jido.Agent,
    name: "code_reviewer",
    description: "Reviews Elixir code for basic issues",
    schema: [
      code: [type: :string, required: true]
    ]

  alias Jido.Agent.{Directive, Signal}

  @impl Jido.Agent
  def plan(agent, directive) do
    code = Directive.get_param(directive, :code)

    plan = %{
      code: code,
      checks: [:documentation, :pattern_matching, :tail_recursion]
    }

    {:ok, Agent.put_plan(agent, plan)}
  end

  @impl Jido.Agent
  def act(agent) do
    plan = Agent.get_plan(agent)

    # Simple code analysis
    issues = []

    issues =
      if not String.contains?(plan.code, "@doc") do
        [%{type: :quality, message: "Missing documentation"} | issues]
      else
        issues
      end

    issues =
      if String.contains?(plan.code, "+ sum(") do
        [%{type: :performance, message: "Non-tail-recursive function"} | issues]
      else
        issues
      end

    result = %{
      issues: issues,
      score: calculate_score(issues)
    }

    {:ok, Agent.put_result(agent, result)}
  end

  @impl Jido.Agent
  def observe(agent) do
    result = Agent.get_result(agent)

    observations = %{
      issues_found: length(result.issues),
      passing: result.score >= 80
    }

    signal = Signal.new(:review_complete, observations)
    {:ok, agent, [signal]}
  end

  defp calculate_score(issues) do
    max(0, 100 - length(issues) * 10)
  end
end

# Test it
code = """
defmodule BadExample do
  def sum([]), do: 0
  def sum([h | t]), do: h + sum(t)
end
"""

directive = Jido.Agent.Directive.new(:review, params: %{code: code})
{:ok, agent} = Jido.Agent.run(SimpleCodeReviewAgent, directive)
result = Jido.Agent.get_result(agent)

IO.inspect(result, label: "Review Result")

Exercise 1.2: Build a Calculator Agent

Create an agent that performs calculations and tracks statistics:

defmodule CalculatorAgent do
  use Jido.Agent,
    name: "calculator",
    description: "Performs calculations and tracks usage",
    schema: [
      operation: [type: {:in, [:add, :subtract, :multiply, :divide]}, required: true],
      a: [type: :number, required: true],
      b: [type: :number, required: true]
    ]

  alias Jido.Agent.{Directive, Signal}

  @impl Jido.Agent
  def plan(agent, directive) do
    # TODO: Extract parameters and create plan
    operation = Directive.get_param(directive, :operation)
    a = Directive.get_param(directive, :a)
    b = Directive.get_param(directive, :b)

    plan = %{
      operation: operation,
      a: a,
      b: b
    }

    {:ok, Agent.put_plan(agent, plan)}
  end

  @impl Jido.Agent
  def act(agent) do
    # TODO: Perform the calculation
    plan = Agent.get_plan(agent)

    result_value =
      case plan.operation do
        :add -> plan.a + plan.b
        :subtract -> plan.a - plan.b
        :multiply -> plan.a * plan.b
        :divide when plan.b != 0 -> plan.a / plan.b
        :divide -> :error
      end

    result = %{
      result: result_value,
      operation: plan.operation
    }

    {:ok, Agent.put_result(agent, result)}
  end

  @impl Jido.Agent
  def observe(agent) do
    # TODO: Create observations
    result = Agent.get_result(agent)

    observations = %{
      operation: result.operation,
      success: result.result != :error
    }

    signal = Signal.new(:calculation_complete, observations)
    {:ok, agent, [signal]}
  end
end

# Test your calculator
directive = Jido.Agent.Directive.new(:calculate, params: %{operation: :add, a: 5, b: 3})
{:ok, agent} = Jido.Agent.run(CalculatorAgent, directive)
result = Jido.Agent.get_result(agent)
IO.puts("Result: #{result.result}")

Key Concepts Summary

Kino.Markdown.new("""
### Important Concepts

**1. Directive**
- Input to the agent
- Contains action type and parameters
- Created with `Directive.new(:action, params: %{...})`

**2. Agent State**
- Plan: What the agent will do
- Result: What the agent produced
- Accessed via `Agent.get_plan/1` and `Agent.get_result/1`

**3. Signals**
- Output events from the agent
- Used for monitoring and coordination
- Created with `Signal.new(:event_name, data)`

**4. Lifecycle**
- `plan/2` - Analyze input, create plan
- `act/1` - Execute plan, produce result
- `observe/1` - Analyze result, emit signals
""")

Self-Assessment

form = Kino.Control.form(
  [
    lifecycle: {:checkbox, "I understand the plan → act → observe lifecycle"},
    directives: {:checkbox, "I can create and use directives"},
    state: {:checkbox, "I can manage agent state (plan and result)"},
    signals: {:checkbox, "I understand how signals work"},
    build_agent: {:checkbox, "I can build a basic Jido agent"}
  ],
  submit: "Check Progress"
)

Kino.render(form)

Kino.listen(form, fn event ->
  completed = event.data |> Map.values() |> Enum.count(& &1)
  total = map_size(event.data)

  progress_message =
    if completed == total do
      "🎉 Excellent! You've mastered Jido Agent basics!"
    else
      "Keep going! #{completed}/#{total} objectives complete"
    end

  Kino.Markdown.new("### Progress: #{progress_message}") |> Kino.render()
end)

Key Takeaways

  • Agents follow a structured lifecycle: plan → act → observe
  • Directives provide input and parameters to agents
  • State management happens through plan and result
  • Signals allow agents to communicate outcomes
  • Agents are autonomous - they make decisions based on their logic
  • Use agents when you need structured decision-making and observable behavior

Next Steps

Ready to dive deeper? Continue to the next checkpoint:

Continue to Checkpoint 2: Multi-Agent Systems →

Or explore the full agent implementations:

  • Code Review Agent - apps/labs_jido_agent/lib/labs_jido_agent/code_review_agent.ex
  • Study Buddy Agent - apps/labs_jido_agent/lib/labs_jido_agent/study_buddy_agent.ex
  • Progress Coach Agent - apps/labs_jido_agent/lib/labs_jido_agent/progress_coach_agent.ex

Try building agents for your own use cases! What problems could benefit from agent-based solutions? 🤖