diff --git a/src/Channels/register-agents-chat-run-control-abilities.php b/src/Channels/register-agents-chat-run-control-abilities.php index 7162424..b394ebb 100644 --- a/src/Channels/register-agents-chat-run-control-abilities.php +++ b/src/Channels/register-agents-chat-run-control-abilities.php @@ -11,15 +11,24 @@ defined( 'ABSPATH' ) || exit; -const AGENTS_GET_CHAT_RUN_ABILITY = 'agents/get-chat-run'; -const AGENTS_CANCEL_CHAT_RUN_ABILITY = 'agents/cancel-chat-run'; -const AGENTS_QUEUE_CHAT_MESSAGE_ABILITY = 'agents/queue-chat-message'; +const AGENTS_GET_CHAT_RUN_ABILITY = 'agents/get-chat-run'; +const AGENTS_CANCEL_CHAT_RUN_ABILITY = 'agents/cancel-chat-run'; +const AGENTS_QUEUE_CHAT_MESSAGE_ABILITY = 'agents/queue-chat-message'; +const AGENTS_LIST_CHAT_RUN_EVENTS_ABILITY = 'agents/list-chat-run-events'; add_action( 'wp_abilities_api_init', static function (): void { $abilities = array( - AGENTS_GET_CHAT_RUN_ABILITY => array( + AGENTS_LIST_CHAT_RUN_EVENTS_ABILITY => array( + 'label' => 'List Chat Run Events', + 'description' => 'List canonical lifecycle events for an addressable chat run.', + 'input_schema' => agents_chat_run_events_input_schema(), + 'output_schema' => agents_chat_run_events_output_schema(), + 'execute_callback' => __NAMESPACE__ . '\\agents_list_chat_run_events', + 'annotations' => array( 'idempotent' => true ), + ), + AGENTS_GET_CHAT_RUN_ABILITY => array( 'label' => 'Get Chat Run', 'description' => 'Read the canonical status for an addressable chat run.', 'input_schema' => agents_chat_run_id_input_schema(), @@ -27,7 +36,7 @@ static function (): void { 'execute_callback' => __NAMESPACE__ . '\\agents_get_chat_run', 'annotations' => array( 'idempotent' => true ), ), - AGENTS_CANCEL_CHAT_RUN_ABILITY => array( + AGENTS_CANCEL_CHAT_RUN_ABILITY => array( 'label' => 'Cancel Chat Run', 'description' => 'Request best-effort cancellation for an addressable chat run.', 'input_schema' => agents_chat_run_id_input_schema(), @@ -38,7 +47,7 @@ static function (): void { 'idempotent' => true, ), ), - AGENTS_QUEUE_CHAT_MESSAGE_ABILITY => array( + AGENTS_QUEUE_CHAT_MESSAGE_ABILITY => array( 'label' => 'Queue Chat Message', 'description' => 'Queue a user message for a conversation while another chat run is active.', 'input_schema' => agents_queue_chat_message_input_schema(), @@ -95,6 +104,17 @@ function agents_get_chat_run( array $input ) { return agents_chat_run_control_no_handler( 'agents_chat_run_not_found', 'No chat run was found for the requested run_id.' ); } +/** @return array|\WP_Error */ +function agents_list_chat_run_events( array $input ) { + $handler = apply_filters( 'wp_agent_chat_run_events_handler', null, $input ); + if ( is_callable( $handler ) ) { + $result = call_user_func( $handler, $input ); + return agents_chat_run_events_normalize_result( $result ); + } + + return agents_chat_run_control_no_handler( 'agents_chat_run_events_no_handler', 'No chat run events handler is registered.' ); +} + /** @return array|\WP_Error */ function agents_cancel_chat_run( array $input ) { $handler = apply_filters( 'wp_agent_chat_run_cancel_handler', null, $input ); @@ -208,6 +228,26 @@ function agents_chat_run_control_normalize_result( $result, string $error_code ) } } +/** @return array|\WP_Error */ +function agents_chat_run_events_normalize_result( $result ) { + if ( is_wp_error( $result ) ) { + return $result; + } + + if ( ! is_array( $result ) ) { + return new \WP_Error( 'agents_chat_run_invalid_events_result', 'Chat run event handlers must return an array or WP_Error.' ); + } + + $result['run_id'] = (string) ( $result['run_id'] ?? '' ); + $result['session_id'] = (string) ( $result['session_id'] ?? '' ); + $result['status'] = WP_Agent_Chat_Run_Control::normalize_status( $result['status'] ?? WP_Agent_Chat_Run_Control::STATUS_RUNNING ); + $result['events'] = is_array( $result['events'] ?? null ) ? array_values( $result['events'] ) : array(); + $result['cursor'] = (string) ( $result['cursor'] ?? '' ); + $result['has_more'] = (bool) ( $result['has_more'] ?? false ); + + return $result; +} + function agents_chat_run_control_no_handler( string $code, string $message ): \WP_Error { return new \WP_Error( $code, $message ); } @@ -224,6 +264,17 @@ function agents_chat_run_id_input_schema(): array { ); } +function agents_chat_run_events_input_schema(): array { + $schema = agents_chat_run_id_input_schema(); + $schema['properties']['cursor'] = array( 'type' => 'string' ); + $schema['properties']['limit'] = array( + 'type' => 'integer', + 'minimum' => 1, + 'maximum' => 1000, + ); + return $schema; +} + function agents_chat_run_output_schema(): array { return array( 'type' => 'object', @@ -242,6 +293,37 @@ function agents_chat_run_output_schema(): array { ); } +function agents_chat_run_events_output_schema(): array { + return array( + 'type' => 'object', + 'required' => array( 'run_id', 'session_id', 'status', 'events', 'cursor' ), + 'properties' => array( + 'run_id' => array( 'type' => 'string' ), + 'session_id' => array( 'type' => 'string' ), + 'status' => array( + 'type' => 'string', + 'enum' => WP_Agent_Chat_Run_Control::statuses(), + ), + 'events' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'required' => array( 'id', 'type', 'created_at', 'metadata' ), + 'properties' => array( + 'id' => array( 'type' => 'string' ), + 'type' => array( 'type' => 'string' ), + 'message' => array( 'type' => 'string' ), + 'created_at' => array( 'type' => 'string' ), + 'metadata' => array( 'type' => 'object' ), + ), + ), + ), + 'cursor' => array( 'type' => 'string' ), + 'has_more' => array( 'type' => 'boolean' ), + ), + ); +} + function agents_cancel_chat_run_output_schema(): array { $schema = agents_chat_run_output_schema(); $schema['required'][] = 'cancelled'; diff --git a/tests/chat-run-control-smoke.php b/tests/chat-run-control-smoke.php index 8923b2d..0a175a6 100644 --- a/tests/chat-run-control-smoke.php +++ b/tests/chat-run-control-smoke.php @@ -68,6 +68,7 @@ function update_option( string $option, $value, $autoload = null ): bool { agents_api_smoke_assert_equals( true, isset( $GLOBALS['__agents_api_smoke_abilities'][ AgentsAPI\AI\Channels\AGENTS_GET_CHAT_RUN_ABILITY ] ), 'get-run ability registers', $failures, $passes ); agents_api_smoke_assert_equals( true, isset( $GLOBALS['__agents_api_smoke_abilities'][ AgentsAPI\AI\Channels\AGENTS_CANCEL_CHAT_RUN_ABILITY ] ), 'cancel-run ability registers', $failures, $passes ); agents_api_smoke_assert_equals( true, isset( $GLOBALS['__agents_api_smoke_abilities'][ AgentsAPI\AI\Channels\AGENTS_QUEUE_CHAT_MESSAGE_ABILITY ] ), 'queue-message ability registers', $failures, $passes ); +agents_api_smoke_assert_equals( true, isset( $GLOBALS['__agents_api_smoke_abilities'][ AgentsAPI\AI\Channels\AGENTS_LIST_CHAT_RUN_EVENTS_ABILITY ] ), 'list-run-events ability registers', $failures, $passes ); agents_api_smoke_assert_equals( true, in_array( 'run_id', AgentsAPI\AI\Channels\agents_chat_output_schema()['required'] ?? array(), true ) || isset( AgentsAPI\AI\Channels\agents_chat_output_schema()['properties']['run_id'] ), 'chat output schema exposes run_id', $failures, $passes ); $captured_chat_input = array(); @@ -174,4 +175,47 @@ static function ( $handler, array $input ) use ( &$captured_chat_input ) { agents_api_smoke_assert_equals( 'cancel', $interrupt['metadata']['interrupt_action'] ?? null, 'cancellation helper maps to loop interrupt action', $failures, $passes ); agents_api_smoke_assert_equals( 'run-1', $interrupt['metadata']['run_id'] ?? null, 'cancellation helper carries run id', $failures, $passes ); +$no_events_handler = AgentsAPI\AI\Channels\agents_list_chat_run_events( array( 'session_id' => 'session-events-1', 'run_id' => 'run-events-1' ) ); +agents_api_smoke_assert_equals( true, $no_events_handler instanceof WP_Error, 'run events require host handler', $failures, $passes ); +agents_api_smoke_assert_equals( 'agents_chat_run_events_no_handler', $no_events_handler->get_error_code(), 'run events no-handler error is explicit', $failures, $passes ); + +add_filter( + 'wp_agent_chat_run_events_handler', + static fn() => static fn( array $input ): array => array( + 'run_id' => $input['run_id'], + 'session_id' => $input['session_id'], + 'status' => 'running', + 'events' => array( + array( + 'id' => 'evt_1', + 'type' => 'tool_call', + 'message' => 'Calling client/tool...', + 'created_at' => '2026-01-01T00:00:00Z', + 'metadata' => array( + 'turn' => 1, + 'tool_name' => 'client/tool', + 'tool_call_id' => 'call-1', + ), + ), + ), + 'cursor' => 'evt_1', + 'has_more' => false, + ), + 10, + 2 +); + +$event_page = AgentsAPI\AI\Channels\agents_list_chat_run_events( + array( + 'session_id' => 'session-events-1', + 'run_id' => 'run-events-1', + 'cursor' => 'evt_0', + ) +); +agents_api_smoke_assert_equals( 'run-events-1', $event_page['run_id'] ?? null, 'run events handler preserves run id', $failures, $passes ); +agents_api_smoke_assert_equals( 'session-events-1', $event_page['session_id'] ?? null, 'run events handler preserves session id', $failures, $passes ); +agents_api_smoke_assert_equals( 'running', $event_page['status'] ?? null, 'run events handler normalizes status', $failures, $passes ); +agents_api_smoke_assert_equals( 'evt_1', $event_page['cursor'] ?? null, 'run events handler returns cursor', $failures, $passes ); +agents_api_smoke_assert_equals( 'client/tool', $event_page['events'][0]['metadata']['tool_name'] ?? null, 'run events handler returns safe metadata', $failures, $passes ); + agents_api_smoke_finish( 'chat run-control', $failures, $passes );