diff --git a/CHANGELOG.md b/CHANGELOG.md index 2747d30..c922093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ being refused with `409 Conflict`. A client that drops its stream is not noticed until the next write, so its reconnect was refused, failing clients such as Cursor. +- A tool error reply (`isError: true`) from a tool with an `output_schema` is + no longer checked against the schema. The client gets the tool's error text + instead of `Invalid tool output: $: expected object`. ## 0.7.0 (2026-10-04) diff --git a/lib/phantom/router.ex b/lib/phantom/router.ex index b9590b7..0eac457 100644 --- a/lib/phantom/router.ex +++ b/lib/phantom/router.ex @@ -2198,6 +2198,8 @@ defmodule Phantom.Router do defp validate_output(:tool, %{output_schema: nil}, _formatted), do: :ok + defp validate_output(:tool, _spec, %{isError: true}), do: :ok + defp validate_output(:tool, %{output_schema: schema}, formatted) do content = formatted[:structuredContent] || formatted["structuredContent"] diff --git a/test/phantom/output_schema_test.exs b/test/phantom/output_schema_test.exs new file mode 100644 index 0000000..344b437 --- /dev/null +++ b/test/phantom/output_schema_test.exs @@ -0,0 +1,43 @@ +defmodule Phantom.OutputSchemaTest do + use ExUnit.Case, async: true + + defmodule Router do + use Phantom.Router, name: "OutputSchema", vsn: "1.0" + + @output_schema %{ + type: "object", + required: [:message], + properties: %{message: %{type: "string"}} + } + + tool :failing_tool, description: "Always fails", output_schema: @output_schema + tool :invalid_output_tool, description: "Breaks its schema", output_schema: @output_schema + + def failing_tool(_params, session) do + {:reply, Phantom.Tool.error("reason"), session} + end + + def invalid_output_tool(_params, session) do + {:reply, %{message: 1}, session} + end + end + + import Phantom.Test + + setup do + Phantom.Test.start(router: Router) + {:ok, session: build_session(Router)} + end + + test "an error reply skips output schema validation", %{session: session} do + session + |> call_tool(:failing_tool, %{}) + |> assert_tool_error("reason") + end + + test "a success reply that violates the output schema is rejected", %{session: session} do + session + |> call_tool(:invalid_output_tool, %{}) + |> assert_tool_error(~r/Invalid tool output/) + end +end