Current section
Files
Jump to
Current section
Files
lib/mix/tasks/ex_ai.graph.cancel.ex
defmodule Mix.Tasks.ExAi.Graph.Cancel do
@shortdoc "Mark a persisted run as cancelled"
@moduledoc """
Cancels a persisted run by updating its durable status.
mix ex_ai.graph.cancel <run_id>
mix ex_ai.graph.cancel <run_id> --reason "user requested" --json
"""
use Mix.Task
alias ExAI.CLI.CommandTelemetry
alias ExAI.CLI.JSON
alias ExAI.CLI.Output
alias ExAI.CLI.Runtime
alias ExAI.Error
alias ExAI.Persistence.History
alias ExAI.Persistence.Store
@switches [reason: :string, dir: :string, json: :boolean]
@impl true
def run(argv) do
{opts, args, invalid} = OptionParser.parse(argv, strict: @switches)
opts = Map.new(opts)
persistence_opts = Runtime.persistence_opts(opts)
reason = Map.get(opts, :reason, "requested")
started_ms = CommandTelemetry.start("ex_ai.graph.cancel")
with :ok <- validate_invalid_options(invalid),
{:ok, run_id} <- parse_run_id(args),
:ok <- Runtime.boot(opts),
{:ok, run} <-
Store.update_run_status(
run_id,
:cancelled,
Keyword.merge(
persistence_opts,
metadata: %{
cancel_reason: reason,
cancelled_at: DateTime.utc_now(),
cancelled_via: "ex_ai.graph.cancel"
}
)
),
{:ok, _count} <- persist_cancel_event(run_id, reason, persistence_opts) do
payload =
%{
run_id: run.run_id,
status: run.status,
result: run.result,
metadata: run.metadata
}
|> JSON.success()
CommandTelemetry.stop("ex_ai.graph.cancel", started_ms, %{run_id: run.run_id})
emit_success(payload, opts)
else
{:error, %Error{} = error} ->
CommandTelemetry.error("ex_ai.graph.cancel", started_ms, error)
emit_error(error, opts, %{})
{:error, reason} ->
error = Error.new(:validation_error, reason)
CommandTelemetry.error("ex_ai.graph.cancel", started_ms, error)
emit_error(error, opts, %{})
end
end
@spec persist_cancel_event(String.t(), String.t(), keyword()) ::
{:ok, non_neg_integer()} | {:error, ExAI.Error.t()}
defp persist_cancel_event(run_id, reason, persistence_opts) do
event = %{
role: :event,
content: %{type: :cancel, reason: reason},
metadata: %{kind: :event, event_type: :cancel, at: DateTime.utc_now()},
inserted_at: DateTime.utc_now()
}
History.persist_messages(run_id, [event], persistence_opts)
end
@spec parse_run_id([String.t()]) :: {:ok, String.t()} | {:error, String.t()}
defp parse_run_id([run_id | _]) when is_binary(run_id), do: {:ok, run_id}
defp parse_run_id(_), do: {:error, "usage: mix ex_ai.graph.cancel <run_id>"}
@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 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} status updated to #{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
end