defmodule AI.Tools.Tasks.ResolveTask do @moduledoc """ Tool to resolve a task as success or failure in a Services.Task list. """ @behaviour AI.Tools @impl AI.Tools def async?, do: false @impl AI.Tools def is_available?, do: true @impl AI.Tools def read_args(args) when is_map(args) do with {:ok, raw_id} <- AI.Tools.get_arg(args, "list_id"), {:ok, list_id} <- (case raw_id do id when is_integer(id) -> {:ok, Integer.to_string(id)} id when is_binary(id) -> {:ok, id} _ -> {:error, :invalid_argument, "list_id"} end), {:ok, task_id} <- AI.Tools.get_arg(args, "task_id"), true <- is_binary(task_id) or {:error, :invalid_argument, "task_id must be a string"}, task_id <- String.trim(task_id), true <- task_id != "" or {:error, :invalid_argument, "task_id cannot be empty"}, {:ok, disposition} <- AI.Tools.get_arg(args, "disposition"), true <- disposition in ["success", "failure"] or {:error, :invalid_argument, "disposition must be 'success' or 'failure'"}, {:ok, result} <- AI.Tools.get_arg(args, "result"), true <- is_binary(result) or {:error, :invalid_argument, "result must be a string"} do {:ok, %{ "list_id" => list_id, "task_id" => task_id, "disposition" => disposition, "result" => result }} end end @impl AI.Tools def ui_note_on_request(%{ "list_id" => list_id, "task_id" => task_id, "disposition" => disposition }) do glyph = if disposition == "success" do "✓" else "✗" end case get_task_counts(list_id, disposition) do {total, resolved} -> {"Task resolved", Util.truncate_chars("(#{resolved}/#{total}) #{glyph} #{task_id}")} nil -> {"Task resolved", Util.truncate_chars("#{glyph} #{task_id}")} end end @impl AI.Tools def ui_note_on_result(_args, _result), do: nil @impl AI.Tools def tool_call_failure_message(_args, _reason), do: :default @impl AI.Tools def spec do %{ type: "function", function: %{ name: "tasks_resolve_task", description: """ Resolve a task in an existing task list as either success or failure. The disposition and result are visible to the user, so ensure that the result is clear and informative. Include as much detail as possible in the event of a problem. This will help guide the user in appropriate next steps to resolve the issue. """, parameters: %{ type: "object", additionalProperties: false, required: ["list_id", "task_id", "disposition", "result"], properties: %{ "list_id" => %{ type: "string", description: "The ID of the task list (string; integers accepted)." }, "task_id" => %{ type: "string", description: "Identifier of the task to resolve." }, "disposition" => %{ type: "string", enum: ["success", "failure"], description: "Outcome of the task" }, "result" => %{ type: "string", description: "Outcome message used for either success or failure" } } } } } end @impl AI.Tools def call(%{ "list_id" => list_id, "task_id" => task_id, "disposition" => disp, "result" => result }) do case disp do "success" -> Services.Task.complete_task(list_id, task_id, result) "failure" -> Services.Task.fail_task(list_id, task_id, result) end {:ok, Services.Task.as_string(list_id)} end # ---------------------------------------------------------------------------- # Private Functions # ---------------------------------------------------------------------------- # Returns {total, resolved} task counts if the Services.Task GenServer is # running, or nil if it's not available (e.g., during replay). defp get_task_counts(list_id, disposition) do case Process.whereis(Services.Task) do nil -> nil _pid -> list_id |> Services.Task.get_list() |> case do {:error, _reason} -> [] tasks -> tasks end |> Enum.reduce({0, 0}, fn %{outcome: :todo}, {t, r} -> {t + 1, r} _, {t, r} -> {t + 1, r + 1} end) |> then(fn {t, r} -> if disposition == "success" do {t, r + 1} else {t, r} end end) end end end