From 4876d667b26d9cbf3caf7c6f92557f9c5dc3442a Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 23 Aug 2026 00:28:38 +0000 Subject: [PATCH] fix: preserve normalized pending mediation requests --- .../class-wp-agent-conversation-loop.php | 26 ++++++-- ...conversation-loop-tool-execution-smoke.php | 64 +++++++++++++++++++ 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/src/Runtime/class-wp-agent-conversation-loop.php b/src/Runtime/class-wp-agent-conversation-loop.php index 64e63d6..788bc04 100644 --- a/src/Runtime/class-wp-agent-conversation-loop.php +++ b/src/Runtime/class-wp-agent-conversation-loop.php @@ -1364,12 +1364,24 @@ private static function normalize_pre_tool_mediation_decision( $decision, array $runtime ); } elseif ( 'pending' === $action ) { - $runtime = self::associative_array_or_null( $decision['runtime'] ?? null ) ?? array(); - $metadata = self::associative_array_or_null( $decision['metadata'] ?? null ) ?? array(); - $tool_call_id = is_string( $context['tool_call_id'] ?? null ) ? $context['tool_call_id'] : ''; - $parameters = is_array( $context['parameters'] ?? null ) ? $context['parameters'] : array(); - $turn_context = self::normalize_assoc_array( $context['turn_context'] ?? array() ); - $request = self::associative_array_or_null( $decision['runtime_tool_request'] ?? ( $decision['request'] ?? ( $decision['result'] ?? null ) ) ) ?? array(); + $normalized_result = self::associative_array_or_null( $decision['result'] ?? null ); + $is_normalized = isset( $normalized_result['runtime_tool_request'] ) && is_array( $normalized_result['runtime_tool_request'] ); + $runtime = self::associative_array_or_null( $decision['runtime'] ?? ( $is_normalized ? ( $normalized_result['runtime'] ?? null ) : null ) ) ?? array(); + $metadata = self::associative_array_or_null( $decision['metadata'] ?? ( $is_normalized ? ( $normalized_result['metadata'] ?? null ) : null ) ) ?? array(); + $pending_error = $decision['error'] ?? ( $is_normalized ? ( $normalized_result['error'] ?? null ) : null ); + $tool_call_id = is_string( $context['tool_call_id'] ?? null ) ? $context['tool_call_id'] : ''; + $parameters = is_array( $context['parameters'] ?? null ) ? $context['parameters'] : array(); + $turn_context = self::normalize_assoc_array( $context['turn_context'] ?? array() ); + if ( isset( $decision['runtime_tool_request'] ) ) { + $request = $decision['runtime_tool_request']; + } elseif ( isset( $decision['request'] ) ) { + $request = $decision['request']; + } elseif ( $is_normalized ) { + $request = $normalized_result['runtime_tool_request']; + } else { + $request = $decision['result'] ?? null; + } + $request = self::associative_array_or_null( $request ) ?? array(); try { $request = WP_Agent_Runtime_Tool_Request::normalize( array_merge( @@ -1394,7 +1406,7 @@ private static function normalize_pre_tool_mediation_decision( $decision, array 'success' => false, 'tool_name' => $tool_name, 'status' => WP_Agent_Runtime_Tool_Request::STATUS_PENDING, - 'error' => is_string( $decision['error'] ?? null ) && '' !== trim( $decision['error'] ) ? trim( $decision['error'] ) : 'Waiting for external runtime tool result.', + 'error' => is_string( $pending_error ) && '' !== trim( $pending_error ) ? trim( $pending_error ) : 'Waiting for external runtime tool result.', 'metadata' => $metadata, 'runtime' => $runtime, 'runtime_tool_request' => $request, diff --git a/tests/conversation-loop-tool-execution-smoke.php b/tests/conversation-loop-tool-execution-smoke.php index 345c421..162c67e 100644 --- a/tests/conversation-loop-tool-execution-smoke.php +++ b/tests/conversation-loop-tool-execution-smoke.php @@ -768,6 +768,70 @@ static function ( array $messages ): array { agents_api_smoke_assert_equals( 1, did_action( 'agents_api_runtime_tool_request_created' ), 'loop persistence emits generic runtime tool created event', $failures, $passes ); agents_api_smoke_assert_equals( 1, $loop_runtime_tool_store->requests[ $stored_request_id ]['metadata']['turn_count'] ?? 0, 'stored pending request records generic turn metadata for replay', $failures, $passes ); +echo "\n[Option-provided pending decisions remain stable through a no-op filter (#521):\n"; + +$pending_noop_filter_called = false; +add_filter( + 'agents_api_pre_tool_call_decision', + static function ( array $decision, array $context ) use ( &$pending_noop_filter_called ): array { + if ( 'call_option_pending' === ( $context['tool_call_id'] ?? '' ) ) { + $pending_noop_filter_called = true; + } + + return $decision; + }, + 10, + 2 +); + +$executor->executed = array(); +$option_pending_result = AgentsAPI\AI\WP_Agent_Conversation_Loop::run( + array( array( 'role' => 'user', 'content' => 'pending via option' ) ), + static function ( array $messages ): array { + return array( + 'messages' => $messages, + 'tool_calls' => array( + array( + 'id' => 'call_option_pending', + 'name' => 'client/summarize', + 'parameters' => array( 'text' => 'needs host runtime' ), + ), + ), + ); + }, + array( + 'max_turns' => 3, + 'tool_executor' => $executor, + 'tool_declarations' => $tools, + 'runtime_tool_request_store' => $loop_runtime_tool_store, + 'pre_tool_mediator' => static function (): array { + return array( + 'action' => 'pending', + 'runtime_tool_request' => array( + 'request_id' => 'runtime_tool_521', + 'tool_name' => 'client/summarize', + 'tool_call_id' => 'call_option_pending', + 'parameters' => array( 'text' => 'needs host runtime' ), + 'run_id' => 'run-option-pending', + 'timeout_at' => '2026-08-23T12:00:00Z', + 'runtime' => array( 'completion_signal' => 'external_result' ), + 'metadata' => array( 'transport' => 'host' ), + ), + ); + }, + ) +); + +$option_pending_request = $option_pending_result['runtime_tool_pending'] ?? array(); +agents_api_smoke_assert_equals( true, $pending_noop_filter_called, 'option-provided pending decision passes through the active no-op filter', $failures, $passes ); +agents_api_smoke_assert_equals( 0, count( $executor->executed ), 'option-provided pending decision prevents executor call', $failures, $passes ); +agents_api_smoke_assert_equals( 'runtime_tool_521', $option_pending_request['request_id'] ?? '', 'returned pending request preserves the host-provided request ID', $failures, $passes ); +agents_api_smoke_assert_equals( 'runtime_tool_521', $loop_runtime_tool_store->requests['runtime_tool_521']['request_id'] ?? '', 'persisted pending request preserves the host-provided request ID', $failures, $passes ); +agents_api_smoke_assert_equals( $option_pending_request, $loop_runtime_tool_store->requests['runtime_tool_521'] ?? array(), 'returned and persisted pending requests retain the same canonical request', $failures, $passes ); +agents_api_smoke_assert_equals( '2026-08-23T12:00:00Z', $option_pending_request['timeout_at'] ?? '', 'pending normalization preserves the canonical timeout', $failures, $passes ); +agents_api_smoke_assert_equals( 'external_result', $option_pending_request['runtime']['completion_signal'] ?? '', 'pending normalization preserves canonical runtime metadata', $failures, $passes ); +agents_api_smoke_assert_equals( 'host', $option_pending_request['metadata']['transport'] ?? '', 'pending normalization preserves canonical host metadata', $failures, $passes ); + echo "\n[Pre-tool filter hook allows, rejects, or pauses external runtime tools (#259):\n"; $filter_contexts = array();