From d07843c546509300e2dc712c6469cfc392786098 Mon Sep 17 00:00:00 2001 From: Mark Ericksen Date: Wed, 4 Mar 2026 17:35:38 -0700 Subject: [PATCH 1/3] support enhanced display of interrupted tool calls - when hitting HITL interrupt then possible rejection --- lib/sagents_live_debugger/layouts.ex | 18 +++++++++ .../live/agent_list_live.ex | 40 ++++++++++++++++++- .../live/components/message_components.ex | 27 ++++++++++++- 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/lib/sagents_live_debugger/layouts.ex b/lib/sagents_live_debugger/layouts.ex index 0a558d7..e95693d 100644 --- a/lib/sagents_live_debugger/layouts.ex +++ b/lib/sagents_live_debugger/layouts.ex @@ -714,6 +714,24 @@ defmodule SagentsLiveDebugger.Layouts do color: #991b1b; } + .result-status.status-interrupted { + background: #fef3c7; + color: #92400e; + } + + .tool-result-interrupt-data { + margin-top: 0.5rem; + padding: 0.5rem; + background: #fffbeb; + border: 1px solid #fcd34d; + border-radius: 0.375rem; + } + + .tool-result-interrupt-data pre { + margin: 0; + font-size: 0.75rem; + } + /* Message Metadata */ .message-metadata { margin-top: 0.75rem; diff --git a/lib/sagents_live_debugger/live/agent_list_live.ex b/lib/sagents_live_debugger/live/agent_list_live.ex index 063f58b..e45f27d 100644 --- a/lib/sagents_live_debugger/live/agent_list_live.ex +++ b/lib/sagents_live_debugger/live/agent_list_live.ex @@ -2114,6 +2114,14 @@ defmodule SagentsLiveDebugger.AgentListLive do defp format_event_data(event) do case event do + {:status_changed, :interrupted, data} -> + %{ + type: "status_changed", + status: "interrupted", + summary: "Status: interrupted — #{format_interrupt_summary(data)}", + interrupt_data: data + } + {:status_changed, status, _data} -> %{ type: "status_changed", @@ -2129,7 +2137,15 @@ defmodule SagentsLiveDebugger.AgentListLive do Enum.map(tool_results, fn result -> name = Map.get(result, :name, "unknown") is_error = Map.get(result, :is_error, false) - status = if is_error, do: "✗", else: "✓" + is_interrupt = Map.get(result, :is_interrupt, false) + + status = + cond do + is_interrupt -> "✋" + is_error -> "✗" + true -> "✓" + end + "#{name} #{status}" end) @@ -2338,6 +2354,28 @@ defmodule SagentsLiveDebugger.AgentListLive do end end + defp format_interrupt_summary(%{type: :subagent_hitl, subagent_type: type, interrupt_data: inner}) do + tools = + inner + |> Map.get(:action_requests, []) + |> Enum.map(& &1[:tool_name] || &1["tool_name"] || "unknown") + |> Enum.join(", ") + + "sub-agent (#{type}) awaiting approval for: #{tools}" + end + + defp format_interrupt_summary(%{action_requests: action_requests}) + when is_list(action_requests) do + tools = + action_requests + |> Enum.map(& &1[:tool_name] || &1["tool_name"] || "unknown") + |> Enum.join(", ") + + "awaiting approval for: #{tools}" + end + + defp format_interrupt_summary(_), do: "awaiting human input" + defp format_action_data(action_data) when is_tuple(action_data) do case action_data do {action_name, data} when is_atom(action_name) -> diff --git a/lib/sagents_live_debugger/live/components/message_components.ex b/lib/sagents_live_debugger/live/components/message_components.ex index 69a1650..fde9904 100644 --- a/lib/sagents_live_debugger/live/components/message_components.ex +++ b/lib/sagents_live_debugger/live/components/message_components.ex @@ -193,10 +193,24 @@ defmodule SagentsLiveDebugger.Live.Components.MessageComponents do attr :tool_result, :map, required: true def tool_result_item(assigns) do + is_interrupt = Map.get(assigns.tool_result, :is_interrupt, false) + interrupt_data = Map.get(assigns.tool_result, :interrupt_data) + + assigns = + assigns + |> assign(:is_interrupt, is_interrupt) + |> assign(:interrupt_data, interrupt_data) + |> assign(:formatted_interrupt_data, format_interrupt_data(interrupt_data)) + ~H"""
- ✅ {@tool_result.name || "Result"} + <%= if @is_interrupt do %> + ✋ {@tool_result.name || "Result"} + INTERRUPTED + <% else %> + ✅ {@tool_result.name || "Result"} + <% end %> <%= if @tool_result.tool_call_id do %> {@tool_result.tool_call_id} <% end %> @@ -206,6 +220,11 @@ defmodule SagentsLiveDebugger.Live.Components.MessageComponents do <% end %>
+ <%= if @is_interrupt && @interrupt_data do %> +
+ <.highlight_code code={@formatted_interrupt_data} language="elixir" /> +
+ <% end %>
<.highlight_code code={format_tool_result(@tool_result.content)} @@ -216,6 +235,12 @@ defmodule SagentsLiveDebugger.Live.Components.MessageComponents do """ end + defp format_interrupt_data(nil), do: "" + + defp format_interrupt_data(data) do + inspect(data, pretty: true, limit: :infinity) + end + defp detect_result_language(content) when is_binary(content) do if String.match?(content, ~r/^\s*[\{\[]/) do "json" From a70cef55bb0f9d124c730b1518d6693a5e5de4fb Mon Sep 17 00:00:00 2001 From: Mark Ericksen Date: Thu, 5 Mar 2026 19:40:29 -0700 Subject: [PATCH 2/3] updated to use published sagents package --- mix.exs | 2 +- mix.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mix.exs b/mix.exs index 210ca86..0d3e8d2 100644 --- a/mix.exs +++ b/mix.exs @@ -41,7 +41,7 @@ defmodule SagentsLiveDebugger.MixProject do {:phoenix_live_view, "~> 1.0"}, {:jason, "~> 1.4"}, # {:sagents, path: "../sagents"}, - {:sagents, "~> 0.2.1"}, + {:sagents, "~> 0.3.0"}, {:horde, "~> 0.10.0", optional: true}, # markdown and code highlighting (autumn) {:mdex, "~> 0.11.0"}, diff --git a/mix.lock b/mix.lock index bff77bb..2617538 100644 --- a/mix.lock +++ b/mix.lock @@ -14,7 +14,7 @@ "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, - "langchain": {:hex, :langchain, "0.6.0", "f1245cc3d329e50e3d8eea82efa5dd80ad476b84f6c48a76527155952d5cea75", [:mix], [{:abacus, "~> 2.1.0", [hex: :abacus, repo: "hexpm", optional: true]}, {:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26.2 or ~> 1.0.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: true]}, {:nx, ">= 0.7.0", [hex: :nx, repo: "hexpm", optional: true]}, {:req, ">= 0.5.2", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "3faa3bef61cf94d35d62f187507260ad24fe19e9555b02c4c7827316bbf64bc3"}, + "langchain": {:hex, :langchain, "0.6.1", "f9d949ff8ee1608151e3038442c17feea871e78679477919222772da7bce2924", [:mix], [{:abacus, "~> 2.1.0", [hex: :abacus, repo: "hexpm", optional: true]}, {:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26.2 or ~> 1.0.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: true]}, {:nx, ">= 0.7.0", [hex: :nx, repo: "hexpm", optional: true]}, {:req, ">= 0.5.2", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "607167099c527822b5f455b0303ae1b0a5b812941b7348024893e12118de6e65"}, "libring": {:hex, :libring, "1.7.0", "4f245d2f1476cd7ed8f03740f6431acba815401e40299208c7f5c640e1883bda", [:mix], [], "hexpm", "070e3593cb572e04f2c8470dd0c119bc1817a7a0a7f88229f43cf0345268ec42"}, "lumis": {:hex, :lumis, "0.1.0", "6d3b495457d0608f8fe32fe6fa917eb17c921bd0087583a7f4c420c0009888fd", [:mix], [{:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "4de203bc5811ad21f0c6b0442612a3fc1ae9bea7fbfe7037452db9e9dfdaaab3"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, @@ -39,7 +39,7 @@ "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.4", "700a878312acfac79fb6c572bb8b57f5aae05fe1cf70d34b5974850bbf2c05bf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "3b33d99b540b15f142ba47944f7a163a25069f6d608783c321029bc1ffb09514"}, - "sagents": {:hex, :sagents, "0.2.1", "493282354029544ddc2a0c5d4fde8e3008222817e8dc6f034b87a520711a0fec", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:horde, "~> 0.10", [hex: :horde, repo: "hexpm", optional: true]}, {:langchain, "~> 0.6.0", [hex: :langchain, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "7b8c42f4505f36806418ead6fd6c0a4c0ba3410672ff48a0e5ed4dac3fe8111c"}, + "sagents": {:hex, :sagents, "0.3.0", "6b13f50636dd4aba801f2823cf2a2d56808fd97c540d0b7bf2b7a1768e676d24", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:horde, "~> 0.10", [hex: :horde, repo: "hexpm", optional: true]}, {:langchain, "~> 0.6.1", [hex: :langchain, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "ac3d277add80100956bd3d071ae51234248f7807992e4a55f9614adfb41c29be"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, From 5e7e848e2091d2d14b79dad3945cb52841771b55 Mon Sep 17 00:00:00 2001 From: Mark Ericksen Date: Thu, 5 Mar 2026 19:52:19 -0700 Subject: [PATCH 3/3] formatting --- lib/sagents_live_debugger/live/agent_list_live.ex | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/sagents_live_debugger/live/agent_list_live.ex b/lib/sagents_live_debugger/live/agent_list_live.ex index e45f27d..495098e 100644 --- a/lib/sagents_live_debugger/live/agent_list_live.ex +++ b/lib/sagents_live_debugger/live/agent_list_live.ex @@ -2354,11 +2354,15 @@ defmodule SagentsLiveDebugger.AgentListLive do end end - defp format_interrupt_summary(%{type: :subagent_hitl, subagent_type: type, interrupt_data: inner}) do + defp format_interrupt_summary(%{ + type: :subagent_hitl, + subagent_type: type, + interrupt_data: inner + }) do tools = inner |> Map.get(:action_requests, []) - |> Enum.map(& &1[:tool_name] || &1["tool_name"] || "unknown") + |> Enum.map(&(&1[:tool_name] || &1["tool_name"] || "unknown")) |> Enum.join(", ") "sub-agent (#{type}) awaiting approval for: #{tools}" @@ -2368,7 +2372,7 @@ defmodule SagentsLiveDebugger.AgentListLive do when is_list(action_requests) do tools = action_requests - |> Enum.map(& &1[:tool_name] || &1["tool_name"] || "unknown") + |> Enum.map(&(&1[:tool_name] || &1["tool_name"] || "unknown")) |> Enum.join(", ") "awaiting approval for: #{tools}"