Powered by AppSignal & Oban Pro

Session 17: Phoenix - Up and Running

17_phoenix_up_and_running.livemd

Session 17: Phoenix - Up and Running

Mix.install([])

Introduction

In Phase 3, you built a solid OTP foundation: AgentServer (GenServer), AgentSupervisor (DynamicSupervisor), and an Application module. Your agents can now process tasks, maintain memory, and automatically recover from crashes.

But there’s a problem: How do EXTERNAL clients communicate with your agents?

Your agents speak Erlang messages. The outside world speaks HTTP.

This is where Phoenix comes in.

Sources for This Session

This session synthesizes concepts from:

Learning Goals

By the end of this session, you’ll be able to:

  • Explain what Phoenix provides (HTTP layer for OTP)
  • Understand the Phoenix project structure
  • See how Phoenix fits into OTP supervision trees
  • Create an API-only Phoenix project

Section 1: Why Phoenix?

πŸ€” Opening Reflection

Before we dive in, think about these questions:

reflection = %{
  current_situation: """
    Your agent_framework can:
    - Start agents with AgentSupervisor.start_agent("Worker-1")
    - Send tasks with AgentServer.send_task(agent, :search, %{query: "OTP"})
    - Process tasks with AgentServer.process_next(agent)

    But WHO can call these functions?
    Answer: Only Elixir code running in the same BEAM VM.
  """,

  the_gap: """
    What if you want:
    - A Python script to send tasks?
    - A web dashboard to monitor agents?
    - Another server to discover your agent's capabilities?

    What would you need to build?
  """,

  without_phoenix: """
    Without Phoenix, you'd need to build manually:
    - [ ] HTTP server (Cowboy? Bandit?)
    - [ ] Request parsing (URL, headers, body)
    - [ ] Response encoding (JSON, status codes)
    - [ ] Routing (which URL maps to which function?)
    - [ ] Error handling (what if request is malformed?)
    - [ ] Connection management (timeouts, keep-alive)

    This is a LOT of boilerplate for something so common!
  """
}

The Pattern You’ve Seen Before

Think about the progression:

Phase 2: Manual process management β†’ Phase 3: OTP GenServer/Supervisor
         (100s of lines of code)         (10s of lines of code)

Phase 3: Manual HTTP server ???   β†’ Phase 4: Phoenix
         (100s of lines of code)         (10s of lines of code)

Phoenix is to HTTP what OTP is to processes.

It takes common patterns that everyone implements and provides a battle-tested framework.

What Phoenix Provides

phoenix_features = %{
  endpoint: "Entry point for all HTTP requests",
  router: "Maps URLs to controller actions",
  controllers: "Handle requests and produce responses",
  pipelines: "Middleware chains (authentication, JSON parsing, etc.)",
  channels: "WebSocket support for real-time features",
  pubsub: "Distributed message broadcasting",
  telemetry: "Built-in metrics and observability"
}

# Key insight: Phoenix is an OTP application!
# It runs as part of your supervision tree.

Section 2: Phoenix Architecture

Where Phoenix Fits

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Your Application                             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚
β”‚  β”‚   Phoenix Layer  β”‚ ←HTTPβ†’  β”‚   External       β”‚                  β”‚
β”‚  β”‚   (agent_api)    β”‚         β”‚   Clients        β”‚                  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚
β”‚           β”‚                                                          β”‚
β”‚           β”‚ Elixir function calls                                    β”‚
β”‚           β–Ό                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                               β”‚
β”‚  β”‚   OTP Layer      β”‚                                               β”‚
β”‚  β”‚ (agent_framework)β”‚                                               β”‚
β”‚  β”‚                  β”‚                                               β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚                                               β”‚
β”‚  β”‚  β”‚AgentServer  β”‚ β”‚                                               β”‚
β”‚  β”‚  β”‚AgentServer  β”‚ β”‚                                               β”‚
β”‚  β”‚  β”‚AgentServer  β”‚ β”‚                                               β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚                                               β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                               β”‚
β”‚                                                                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ€” Understanding the Layers

# Consider this request flow:

# 1. External client makes HTTP request:
#    POST /a2a
#    {"method": "SendMessage", "params": {"agent": "Worker-1", "action": "search"}}

# 2. Phoenix receives the request in the Endpoint

# 3. Router matches URL to controller

# 4. Controller parses JSON, calls AgentFramework functions

# 5. AgentServer receives Elixir message, processes task

# 6. Controller formats response as JSON

# 7. Phoenix sends HTTP response

# Question: At which layer does the "translation" happen?
translation_layer = """
Your answer: ???

Hint: Which layer converts HTTP concepts to Elixir concepts?
"""

# Answer: The Controller layer
# Controllers are the bridge between HTTP (external) and OTP (internal)

Section 3: Project Structure

API-Only Phoenix Project

For A2A protocol support, we don’t need HTML views, JavaScript assets, or a database. We create an API-only project:

# This is what we'd run to create a new project:
# mix phx.new agent_api --no-html --no-assets --no-ecto

# Flags explained:
# --no-html   β†’ No HTML templates or view helpers
# --no-assets β†’ No JavaScript bundling (esbuild, tailwind)
# --no-ecto   β†’ No database layer (we use agent_framework's state)

Directory Structure

agent_api/
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ agent_api/               # Business logic (domain)
β”‚   β”‚   β”œβ”€β”€ application.ex       # OTP Application
β”‚   β”‚   └── a2a/                 # A2A protocol modules
β”‚   β”‚       β”œβ”€β”€ agent_card.ex    # Agent Card data
β”‚   β”‚       β”œβ”€β”€ json_rpc.ex      # JSON-RPC parsing
β”‚   β”‚       └── task_manager.ex  # Bridge to AgentFramework
β”‚   β”‚
β”‚   └── agent_api_web/           # Web layer
β”‚       β”œβ”€β”€ endpoint.ex          # HTTP entry point
β”‚       β”œβ”€β”€ router.ex            # URL β†’ Controller mapping
β”‚       └── controllers/         # Request handlers
β”‚           β”œβ”€β”€ agent_card_controller.ex
β”‚           └── a2a_controller.ex
β”‚
β”œβ”€β”€ config/                      # Configuration files
β”‚   β”œβ”€β”€ config.exs              # Base config
β”‚   β”œβ”€β”€ dev.exs                 # Development overrides
β”‚   β”œβ”€β”€ prod.exs                # Production overrides
β”‚   └── test.exs                # Test overrides
β”‚
β”œβ”€β”€ test/                        # Tests
└── mix.exs                      # Project definition

πŸ€” Why Two Directories?

# Notice: lib/agent_api/ and lib/agent_api_web/

# Question: Why separate these?

separation_question = """
lib/agent_api/     β†’ Contains: ???
lib/agent_api_web/ β†’ Contains: ???

Why not put everything in one directory?
"""

# Answer:
# lib/agent_api/     β†’ Business logic that could work without HTTP
#                      (Agent Card data, JSON-RPC parsing, task management)
#
# lib/agent_api_web/ β†’ Web-specific code (HTTP handling)
#                      (Endpoint, Router, Controllers)
#
# This separation means:
# - You could test business logic without starting a web server
# - You could reuse business logic in a CLI tool
# - Clear boundaries make code easier to navigate

Section 4: The Endpoint

The Endpoint is the entry point for ALL HTTP requests.

What the Endpoint Does

# lib/agent_api_web/endpoint.ex

defmodule AgentApiWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :agent_api

  # Each plug processes the request in order:

  plug Plug.RequestId          # Add unique request ID
  plug Plug.Telemetry          # Emit telemetry events
  plug Plug.Parsers,           # Parse request body
    parsers: [:urlencoded, :json],
    json_decoder: Jason
  plug Plug.MethodOverride     # Support _method param
  plug Plug.Head               # Convert HEAD to GET
  plug Plug.Session            # Session management
  plug AgentApiWeb.Router      # Route to controllers
end

πŸ€” Endpoint as Supervisor

# Here's something interesting:
# The Endpoint is itself a Supervisor!

# When you start Phoenix, the supervision tree looks like:

# YourApp.Supervisor
#      β”‚
#      β”œβ”€β”€ Phoenix.PubSub
#      β”‚
#      └── AgentApiWeb.Endpoint  ← This is a Supervisor!
#                β”‚
#                β”œβ”€β”€ HTTP Server (Cowboy/Bandit)
#                β”œβ”€β”€ Code Reloader (dev only)
#                └── Long-poll transport (for channels)

# Question: Why would the Endpoint need to supervise children?

endpoint_children = """
Your answer: ???

Hint: What might need to run independently of request handling?
"""

# Answer:
# - HTTP server needs to accept connections continuously
# - Code reloader watches files and triggers recompilation
# - WebSocket processes need supervision for fault tolerance

Section 5: The Application Module

Starting Phoenix

# lib/agent_api/application.ex

defmodule AgentApi.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Start PubSub for message broadcasting
      {Phoenix.PubSub, name: AgentApi.PubSub},

      # Start the Endpoint (HTTP server)
      AgentApiWeb.Endpoint

      # Note: AgentFramework's supervisor starts automatically
      # because agent_framework is a dependency
    ]

    opts = [strategy: :one_for_one, name: AgentApi.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

πŸ€” Two Applications Working Together

# When you run `iex -S mix phx.server`, two OTP applications start:

# 1. agent_framework (dependency)
#    └── AgentFramework.Supervisor
#        β”œβ”€β”€ Registry
#        └── AgentSupervisor (DynamicSupervisor)

# 2. agent_api (main app)
#    └── AgentApi.Supervisor
#        β”œβ”€β”€ Phoenix.PubSub
#        └── AgentApiWeb.Endpoint
#            └── HTTP Server

# Question: How can AgentApi.A2AController call AgentFramework functions?

cross_app_calls = """
Your answer: ???

Hint: Are they in the same BEAM VM?
"""

# Answer:
# They're in the SAME BEAM VM!
# OTP applications are just organizational units.
# Any module can call any other module's public functions.
#
# AgentApi.A2AController can simply:
#   AgentFramework.AgentSupervisor.start_agent("Worker-1")

Section 6: Interactive Exploration

Exercise 1: Examine the Supervision Tree

When Phoenix starts, explore what’s running:

# In IEx with your Phoenix app running:

# See all running applications
Application.started_applications()

# See the agent_api supervision tree
Supervisor.which_children(AgentApi.Supervisor)

# See the agent_framework supervision tree
Supervisor.which_children(AgentFramework.Supervisor)

# Check the Endpoint's children
# (The Endpoint is itself a supervisor)

Exercise 2: Trace the Request Path

# Think through this request:
# curl http://localhost:4000/.well-known/agent.json

request_path = """
1. HTTP request arrives at: ???
2. Parsed by plugs in: ???
3. Routed to: ???
4. Controller function: ???
5. Returns: ???
"""

# Answer:
# 1. AgentApiWeb.Endpoint (HTTP server)
# 2. AgentApiWeb.Endpoint (plug pipeline)
# 3. AgentApiWeb.Router (matches GET /.well-known/agent.json)
# 4. AgentCardController.show/2
# 5. JSON with Agent Card data

Exercise 3: Understand the Connection

# In Phoenix, the `conn` struct carries all request/response data.

# Question: What's in a conn?

conn_contents = %{
  request_data: [
    # ???
  ],
  response_data: [
    # ???
  ],
  metadata: [
    # ???
  ]
}

# Answer:
conn_contents = %{
  request_data: [
    :method,      # "GET", "POST", etc.
    :path_info,   # ["a2a"]
    :query_params,
    :body_params,
    :headers
  ],
  response_data: [
    :status,      # 200, 404, etc.
    :resp_body,
    :resp_headers
  ],
  metadata: [
    :assigns,     # Your custom data
    :private,     # Phoenix internal data
    :host,
    :port,
    :scheme
  ]
}

Section 7: Configuration

Environment-Specific Config

# config/config.exs - Base configuration
config :agent_api, AgentApiWeb.Endpoint,
  url: [host: "localhost"],
  render_errors: [formats: [json: AgentApiWeb.ErrorJSON]]

# config/dev.exs - Development overrides
config :agent_api, AgentApiWeb.Endpoint,
  http: [port: 4000],
  debug_errors: true,
  code_reloader: true

# config/prod.exs - Production settings
config :agent_api, AgentApiWeb.Endpoint,
  url: [host: "example.com", port: 443]

# config/runtime.exs - Runtime configuration (uses env vars)
config :agent_api, AgentApiWeb.Endpoint,
  secret_key_base: System.get_env("SECRET_KEY_BASE")

πŸ€” Configuration Hierarchy

# Question: If you set a value in config.exs and a different value
#           in dev.exs, which one "wins" in development?

config_question = """
# config/config.exs
config :agent_api, :port, 4000

# config/dev.exs
config :agent_api, :port, 4001

# In development, Application.get_env(:agent_api, :port) returns: ???
"""

# Answer: 4001
# Later config files override earlier ones.
# Order: config.exs β†’ dev.exs/prod.exs/test.exs β†’ runtime.exs

Key Takeaways

  1. Phoenix is to HTTP what OTP is to processes - It wraps common patterns in a battle-tested framework

  2. Phoenix is an OTP application - It runs as part of your supervision tree, not outside it

  3. Two-directory structure separates concerns - lib/app/ for business logic, lib/app_web/ for HTTP

  4. Endpoint is the entry point - All requests flow through the plug pipeline

  5. Cross-application calls work - Multiple OTP apps in one BEAM can call each other’s functions

  6. Configuration is layered - Base config + environment overrides + runtime config


What’s Next?

In the next session, we’ll explore Request Lifecycle and Routing:

  • How a request flows from Endpoint β†’ Router β†’ Controller
  • Defining routes for A2A endpoints
  • Understanding pipelines and plugs
  • Path parameters and pattern matching

You’ll see how Phoenix routing is similar to GenServer message pattern matching!


Navigation

← Previous: Session 16 - Checkpoint OTP Agents

β†’ Next: Session 18 - Request Lifecycle and Routing