From 05d29c374185fafe05a5ac7e7178878d01fc3b6f Mon Sep 17 00:00:00 2001 From: Miguel Lezama Date: Wed, 3 Jun 2026 22:27:12 -0300 Subject: [PATCH] Replay empty tool inputs as objects --- includes/class-openclawp-message-adapter.php | 20 +++++++++++++- tests/unit/MessageAdapterTest.php | 28 +++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/includes/class-openclawp-message-adapter.php b/includes/class-openclawp-message-adapter.php index 02086c5..23b0eee 100644 --- a/includes/class-openclawp-message-adapter.php +++ b/includes/class-openclawp-message-adapter.php @@ -96,12 +96,30 @@ private static function tool_call_part( array $message ) { return null; } $id = (string) ( $meta['tool_call_id'] ?? '' ); - $args = is_array( $payload['parameters'] ?? null ) ? $payload['parameters'] : array(); + $args = self::function_call_args( $payload['parameters'] ?? array() ); $call = new \WordPress\AiClient\Tools\DTO\FunctionCall( '' !== $id ? $id : null, $name, $args ); return new \WordPress\AiClient\Messages\DTO\MessagePart( $call ); } + /** + * Return provider-safe function-call args for transcript replay. + * + * Anthropic requires `tool_use.input` to be a JSON object. PHP encodes an + * empty array as `[]`, so a no-argument tool call must be carried as an + * empty object when replayed through WP AI Client. + * + * @param mixed $parameters Stored tool parameters. + * @return mixed + */ + private static function function_call_args( $parameters ) { + if ( array() === $parameters ) { + return new \stdClass(); + } + + return is_array( $parameters ) ? $parameters : array(); + } + /** * Build a `MessagePart` wrapping the `FunctionResponse` for a stored * `tool_result` transcript message, matched to its call by `tool_call_id`. diff --git a/tests/unit/MessageAdapterTest.php b/tests/unit/MessageAdapterTest.php index 4536c75..07adce2 100644 --- a/tests/unit/MessageAdapterTest.php +++ b/tests/unit/MessageAdapterTest.php @@ -69,6 +69,27 @@ public function test_to_ai_client_messages_round_trips_tool_mediation_parts(): v $this->assertSame( array( 'ok' => true ), $response->getResponse() ); } + public function test_to_ai_client_messages_replays_empty_tool_parameters_as_object(): void { + self::install_ai_client_stubs(); + + $output = OpenclaWP_Message_Adapter::to_ai_client_messages( + array( + array( + 'type' => 'tool_call', + 'payload' => array( + 'tool_name' => 'client/openclawp__current-context', + 'parameters' => array(), + ), + 'metadata' => array( 'tool_call_id' => 'call-empty' ), + ), + ) + ); + + $call = $output[0]->getParts()[0]->getContent(); + $this->assertInstanceOf( \stdClass::class, $call->getArgs() ); + $this->assertSame( '{}', json_encode( $call->getArgs() ) ); + } + public function test_last_assistant_text_returns_most_recent_assistant_message(): void { $messages = array( array( 'role' => 'user', 'content' => 'q1' ), @@ -200,9 +221,10 @@ public function getContent() { final class FunctionCall { private ?string $id; private string $name; - private array $args; + /** @var mixed */ + private $args; - public function __construct( ?string $id, string $name, array $args ) { + public function __construct( ?string $id, string $name, $args ) { $this->id = $id; $this->name = $name; $this->args = $args; @@ -216,7 +238,7 @@ public function getName(): string { return $this->name; } - public function getArgs(): array { + public function getArgs() { return $this->args; } }