Packages

Graph-first runtime for building agent systems on the BEAM in Elixir

Current section

Files

Jump to
ex_ai lib mix tasks ex_ai.agent.run.ex
Raw

lib/mix/tasks/ex_ai.agent.run.ex

defmodule Mix.Tasks.ExAi.Agent.Run do
@shortdoc "Run a configured ExAI agent"
@moduledoc """
Runs a configured ExAI agent and persists run metadata, messages, and events.
mix ex_ai.agent.run <agent_name> --input "hello"
mix ex_ai.agent.run <agent_name> --input "hello" --json
"""
use Mix.Task
alias ExAI.CLI.AgentCatalog
alias ExAI.CLI.CommandTelemetry
alias ExAI.CLI.JSON
alias ExAI.CLI.Output
alias ExAI.CLI.Runtime
alias ExAI.Error
@switches [input: :string, run_id: :string, dir: :string, json: :boolean]
@impl true
def run(argv) do
{opts, args, invalid} = OptionParser.parse(argv, strict: @switches)
opts = Map.new(opts)
started_ms = CommandTelemetry.start("ex_ai.agent.run")
with :ok <- validate_invalid_options(invalid),
{:ok, agent_name} <- parse_agent_name(args),
{:ok, input} <- parse_input(opts),
:ok <- Runtime.boot(opts),
{:ok, agent} <- AgentCatalog.fetch(agent_name),
{:ok, context, events} <- execute(agent, agent_name, input, opts),
{:ok, persisted} <- Runtime.persist_run_bundle(context, events, opts) do
response =
JSON.success(
response_payload(context,
agent: agent_name,
events: persisted.events_persisted,
messages: persisted.messages_persisted
)
)
CommandTelemetry.stop("ex_ai.agent.run", started_ms, %{run_id: context.run_id})
emit_success(response, opts)
else
{:error, %Error{} = error} ->
CommandTelemetry.error("ex_ai.agent.run", started_ms, error)
emit_error(error, opts, %{})
{:error, reason} ->
error = Error.new(:validation_error, reason)
CommandTelemetry.error("ex_ai.agent.run", started_ms, error)
emit_error(error, opts, %{})
end
end
@spec execute(ExAI.Agent.t(), String.t(), String.t(), map()) ::
{:ok, ExAI.Graph.Context.t(), [ExAI.Graph.Event.t()]}
| {:error, ExAI.Error.t()}
defp execute(agent, agent_name, input, opts) do
metadata = %{
agent_name: agent_name,
cli_task: "ex_ai.agent.run"
}
runtime_opts =
[]
|> put_opt(:run_id, Map.get(opts, :run_id))
|> put_opt(:metadata, metadata)
case Runtime.run_agent(agent, input, runtime_opts) do
{:ok, context, events} ->
{:ok, context, events}
{:error, %Error{} = error, _context, _events} ->
{:error, error}
end
end
@spec parse_agent_name([String.t()]) :: {:ok, String.t()} | {:error, String.t()}
defp parse_agent_name([agent_name | _]) when is_binary(agent_name), do: {:ok, agent_name}
defp parse_agent_name(_), do: {:error, "usage: mix ex_ai.agent.run <agent_name> --input <text>"}
@spec parse_input(map()) :: {:ok, String.t()} | {:error, String.t()}
defp parse_input(%{input: input}) when is_binary(input) and input != "", do: {:ok, input}
defp parse_input(_), do: {:error, "--input is required"}
@spec validate_invalid_options([{String.t(), String.t() | nil}]) :: :ok | {:error, String.t()}
defp validate_invalid_options([]), do: :ok
defp validate_invalid_options(invalid) do
{:error, "invalid options: #{inspect(invalid)}"}
end
@spec response_payload(ExAI.Graph.Context.t(), keyword()) :: map()
defp response_payload(context, opts) do
%{
run_id: context.run_id,
status: context.status,
result: context.result,
usage: usage_from_result(context.result),
metadata: context.metadata,
agent: Keyword.get(opts, :agent),
persisted: %{
events: Keyword.get(opts, :events, 0),
messages: Keyword.get(opts, :messages, 0)
}
}
end
@spec usage_from_result(term()) :: term()
defp usage_from_result(%{} = result), do: Map.get(result, :usage)
defp usage_from_result(_), do: nil
@spec emit_success(map(), map()) :: :ok
defp emit_success(payload, %{json: true}) do
Output.emit_json(payload)
end
defp emit_success(payload, _opts) do
Output.emit_info("run #{payload.run_id} completed with status #{payload.status}")
:ok
end
@spec emit_error(ExAI.Error.t(), map(), map()) :: no_return()
defp emit_error(%Error{} = error, opts, payload_overrides) do
Output.halt_error(error, opts, payload_overrides)
end
@spec put_opt(keyword(), atom(), term()) :: keyword()
defp put_opt(opts, _key, nil), do: opts
defp put_opt(opts, key, value), do: Keyword.put(opts, key, value)
end