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
94 changes: 88 additions & 6 deletions src/Channels/register-agents-chat-run-control-abilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,32 @@

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(),
'output_schema' => agents_chat_run_output_schema(),
'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(),
Expand All @@ -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(),
Expand Down Expand Up @@ -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<string,mixed>|\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<string,mixed>|\WP_Error */
function agents_cancel_chat_run( array $input ) {
$handler = apply_filters( 'wp_agent_chat_run_cancel_handler', null, $input );
Expand Down Expand Up @@ -208,6 +228,26 @@ function agents_chat_run_control_normalize_result( $result, string $error_code )
}
}

/** @return array<string,mixed>|\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 );
}
Expand All @@ -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',
Expand All @@ -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';
Expand Down
44 changes: 44 additions & 0 deletions tests/chat-run-control-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 );
Loading