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 --input "hello" mix ex_ai.agent.run --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 --input "} @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