Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions lib/phantom/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down
43 changes: 43 additions & 0 deletions test/phantom/output_schema_test.exs
Original file line number Diff line number Diff line change
@@ -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
Loading