From f31cb512294033e457d047c7de659521972e8d00 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Fri, 19 Jun 2026 01:40:17 -0400 Subject: [PATCH] Add runtime tool lifecycle abilities --- agents-api.php | 1 + composer.json | 1 + ...ister-runtime-tool-lifecycle-abilities.php | 550 ++++++++++++++++++ ...runtime-tool-lifecycle-abilities-smoke.php | 192 ++++++ 4 files changed, 744 insertions(+) create mode 100644 src/Runtime/register-runtime-tool-lifecycle-abilities.php create mode 100644 tests/runtime-tool-lifecycle-abilities-smoke.php diff --git a/agents-api.php b/agents-api.php index a88d175..b07c44e 100644 --- a/agents-api.php +++ b/agents-api.php @@ -163,6 +163,7 @@ require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-runtime-tool-request-store.php'; require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-runtime-tool-continuation.php'; require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-runtime-tool-lifecycle.php'; +require_once AGENTS_API_PATH . 'src/Runtime/register-runtime-tool-lifecycle-abilities.php'; require_once AGENTS_API_PATH . 'src/Runtime/interface-wp-agent-run-control-store.php'; require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-option-run-control-store.php'; require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-run-control.php'; diff --git a/composer.json b/composer.json index e103b52..d64a811 100644 --- a/composer.json +++ b/composer.json @@ -44,6 +44,7 @@ "php tests/tool-policy-contracts-smoke.php", "php tests/tool-source-registry-smoke.php", "php tests/runtime-tool-policy-smoke.php", + "php tests/runtime-tool-lifecycle-abilities-smoke.php", "php tests/tool-tier-resolver-smoke.php", "php tests/action-policy-resolver-smoke.php", "php tests/ability-meta-abilities-smoke.php", diff --git a/src/Runtime/register-runtime-tool-lifecycle-abilities.php b/src/Runtime/register-runtime-tool-lifecycle-abilities.php new file mode 100644 index 0000000..9b95db3 --- /dev/null +++ b/src/Runtime/register-runtime-tool-lifecycle-abilities.php @@ -0,0 +1,550 @@ + array( + 'label' => 'List Runtime Tool Requests', + 'description' => 'List recent pending runtime-tool requests from the host-provided request store.', + 'input_schema' => agents_runtime_tool_list_requests_input_schema(), + 'output_schema' => agents_runtime_tool_list_requests_output_schema(), + 'execute_callback' => __NAMESPACE__ . '\\agents_runtime_tool_list_requests', + 'permission' => __NAMESPACE__ . '\\agents_runtime_tool_read_permission', + 'annotations' => array( 'idempotent' => true ), + ), + AGENTS_GET_RUNTIME_TOOL_REQUEST_ABILITY => array( + 'label' => 'Get Runtime Tool Request', + 'description' => 'Read status and retained result data for a runtime-tool request.', + 'input_schema' => agents_runtime_tool_request_id_input_schema(), + 'output_schema' => agents_runtime_tool_request_status_output_schema(), + 'execute_callback' => __NAMESPACE__ . '\\agents_runtime_tool_get_request', + 'permission' => __NAMESPACE__ . '\\agents_runtime_tool_read_permission', + 'annotations' => array( 'idempotent' => true ), + ), + AGENTS_SUBMIT_RUNTIME_TOOL_RESULT_ABILITY => array( + 'label' => 'Submit Runtime Tool Result', + 'description' => 'Submit a runtime-tool result, complete the pending request, and optionally resume through the host continuation adapter.', + 'input_schema' => agents_runtime_tool_submit_result_input_schema(), + 'output_schema' => agents_runtime_tool_submission_output_schema(), + 'execute_callback' => __NAMESPACE__ . '\\agents_runtime_tool_submit_result', + 'permission' => __NAMESPACE__ . '\\agents_runtime_tool_write_permission', + 'annotations' => array( + 'destructive' => true, + 'idempotent' => true, + ), + ), + AGENTS_TIMEOUT_RUNTIME_TOOL_REQUEST_ABILITY => array( + 'label' => 'Timeout Runtime Tool Request', + 'description' => 'Mark a pending runtime-tool request timed out and optionally resume through the host continuation adapter.', + 'input_schema' => agents_runtime_tool_terminal_request_input_schema(), + 'output_schema' => agents_runtime_tool_terminal_output_schema(), + 'execute_callback' => __NAMESPACE__ . '\\agents_runtime_tool_timeout_request', + 'permission' => __NAMESPACE__ . '\\agents_runtime_tool_write_permission', + 'annotations' => array( + 'destructive' => true, + 'idempotent' => true, + ), + ), + AGENTS_CANCEL_RUNTIME_TOOL_REQUEST_ABILITY => array( + 'label' => 'Cancel Runtime Tool Request', + 'description' => 'Cancel a pending runtime-tool request by applying the canonical timeout terminal transition.', + 'input_schema' => agents_runtime_tool_terminal_request_input_schema(), + 'output_schema' => agents_runtime_tool_terminal_output_schema(), + 'execute_callback' => __NAMESPACE__ . '\\agents_runtime_tool_cancel_request', + 'permission' => __NAMESPACE__ . '\\agents_runtime_tool_write_permission', + 'annotations' => array( + 'destructive' => true, + 'idempotent' => true, + ), + ), + ); + + foreach ( $abilities as $ability => $args ) { + if ( wp_has_ability( $ability ) ) { + continue; + } + + wp_register_ability( + $ability, + array( + 'label' => $args['label'], + 'description' => $args['description'], + 'category' => 'agents-api', + 'input_schema' => $args['input_schema'], + 'output_schema' => $args['output_schema'], + 'execute_callback' => $args['execute_callback'], + 'permission_callback' => $args['permission'], + 'meta' => array( + 'show_in_rest' => true, + 'annotations' => $args['annotations'], + ), + ) + ); + } + } +); + +/** + * @param array $input Ability input. + * @return array|\WP_Error + */ +function agents_runtime_tool_list_requests( array $input ) { + $store = agents_runtime_tool_request_store( $input ); + if ( is_wp_error( $store ) ) { + return $store; + } + + try { + $requests = WP_Agent_Runtime_Tool_Lifecycle::recent_pending_requests( $store, agents_runtime_tool_query( $input ) ); + } catch ( \InvalidArgumentException $error ) { + return new \WP_Error( 'agents_runtime_tool_invalid_request', $error->getMessage() ); + } + + return array( + 'status' => WP_Agent_Runtime_Tool_Request::STATUS_PENDING, + 'requests' => $requests, + 'count' => count( $requests ), + ); +} + +/** + * @param array $input Ability input. + * @return array|\WP_Error + */ +function agents_runtime_tool_get_request( array $input ) { + $store = agents_runtime_tool_request_store( $input ); + if ( is_wp_error( $store ) ) { + return $store; + } + + $request_id = agents_runtime_tool_required_string( $input, 'request_id' ); + if ( is_wp_error( $request_id ) ) { + return $request_id; + } + + $request = $store->get( $request_id ); + if ( null === $request ) { + return new \WP_Error( 'agents_runtime_tool_request_not_found', 'No runtime-tool request was found for the requested request_id.' ); + } + + try { + $normalized = agents_runtime_tool_normalize_stored_request( $request ); + } catch ( \InvalidArgumentException $error ) { + return new \WP_Error( 'agents_runtime_tool_invalid_request', $error->getMessage() ); + } + + $status = is_string( $normalized['status'] ?? null ) ? $normalized['status'] : ''; + + return array( + 'status' => $status, + 'request' => $normalized, + ); +} + +/** + * @param array $input Ability input. + * @return array|\WP_Error + */ +function agents_runtime_tool_submit_result( array $input ) { + $store = agents_runtime_tool_request_store( $input ); + if ( is_wp_error( $store ) ) { + return $store; + } + + $continuation = agents_runtime_tool_continuation( $input ); + if ( is_wp_error( $continuation ) ) { + return $continuation; + } + + try { + return WP_Agent_Runtime_Tool_Lifecycle::submit_result( $store, $input, $continuation, agents_runtime_tool_context( $input ) ); + } catch ( \InvalidArgumentException $error ) { + return new \WP_Error( 'agents_runtime_tool_invalid_result', $error->getMessage() ); + } +} + +/** + * @param array $input Ability input. + * @return array|\WP_Error + */ +function agents_runtime_tool_timeout_request( array $input ) { + return agents_runtime_tool_terminal_request( $input, false ); +} + +/** + * @param array $input Ability input. + * @return array|\WP_Error + */ +function agents_runtime_tool_cancel_request( array $input ) { + return agents_runtime_tool_terminal_request( $input, true ); +} + +/** + * @param array $input Ability input. + * @param bool $cancelled Whether this terminal transition was requested as cancellation. + * @return array|\WP_Error + */ +function agents_runtime_tool_terminal_request( array $input, bool $cancelled ) { + $store = agents_runtime_tool_request_store( $input ); + if ( is_wp_error( $store ) ) { + return $store; + } + + $request_id = agents_runtime_tool_required_string( $input, 'request_id' ); + if ( is_wp_error( $request_id ) ) { + return $request_id; + } + + $continuation = agents_runtime_tool_continuation( $input ); + if ( is_wp_error( $continuation ) ) { + return $continuation; + } + + try { + $result = WP_Agent_Runtime_Tool_Lifecycle::timeout_request( $store, $request_id, $continuation, agents_runtime_tool_context( $input ) ); + } catch ( \InvalidArgumentException $error ) { + return new \WP_Error( 'agents_runtime_tool_invalid_timeout', $error->getMessage() ); + } + + if ( $cancelled ) { + $result['cancelled'] = true; + } + + return $result; +} + +/** + * Resolve the host-provided runtime-tool request store. + * + * @param array $input Ability input. + * @return WP_Agent_Runtime_Tool_Request_Store|\WP_Error + */ +function agents_runtime_tool_request_store( array $input ) { + /** + * Filters the runtime-tool request store used by lifecycle abilities. + * + * @param WP_Agent_Runtime_Tool_Request_Store|null $store Current store, or null. + * @param array $input Ability input. + */ + $store = apply_filters( 'wp_agent_runtime_tool_request_store', null, $input ); + + if ( $store instanceof WP_Agent_Runtime_Tool_Request_Store ) { + return $store; + } + + return new \WP_Error( + 'agents_runtime_tool_request_store_unavailable', + 'No runtime-tool request store is registered. Add a WP_Agent_Runtime_Tool_Request_Store through the wp_agent_runtime_tool_request_store filter.' + ); +} + +/** + * Resolve an optional host continuation adapter. + * + * @param array $input Ability input. + * @return WP_Agent_Runtime_Tool_Continuation|callable|null|\WP_Error + */ +function agents_runtime_tool_continuation( array $input ) { + if ( empty( $input['resume'] ) ) { + return null; + } + + /** + * Filters the continuation adapter used when lifecycle abilities resume a run. + * + * @param mixed $continuation Current continuation adapter, or null. + * @param array $input Ability input. + */ + $continuation = apply_filters( 'wp_agent_runtime_tool_continuation', null, $input ); + /** @var mixed $continuation */ + + if ( null === $continuation || $continuation instanceof WP_Agent_Runtime_Tool_Continuation ) { + return $continuation; + } + + if ( is_callable( $continuation ) ) { + return $continuation; + } + + return new \WP_Error( 'agents_runtime_tool_invalid_continuation', 'Runtime-tool continuation must be callable or implement WP_Agent_Runtime_Tool_Continuation.' ); +} + +/** + * @param array $input Ability input. + * @return array + */ +function agents_runtime_tool_query( array $input ): array { + $query = array(); + foreach ( array( 'run_id', 'tool_name', 'before', 'after' ) as $field ) { + if ( is_string( $input[ $field ] ?? null ) && '' !== trim( $input[ $field ] ) ) { + $query[ $field ] = trim( $input[ $field ] ); + } + } + + if ( isset( $input['limit'] ) && is_int( $input['limit'] ) && $input['limit'] > 0 ) { + $query['limit'] = $input['limit']; + } + + return $query; +} + +/** + * @param array $input Ability input. + * @return array + */ +function agents_runtime_tool_context( array $input ): array { + $context = $input['context'] ?? null; + if ( ! is_array( $context ) ) { + return array(); + } + + $normalized = array(); + foreach ( $context as $key => $value ) { + if ( is_string( $key ) ) { + $normalized[ $key ] = $value; + } + } + + return $normalized; +} + +/** + * @param array $input Ability input. + * @param string $field Field name. + * @return string|\WP_Error + */ +function agents_runtime_tool_required_string( array $input, string $field ) { + $value = $input[ $field ] ?? ''; + if ( ! is_string( $value ) || '' === trim( $value ) ) { + return new \WP_Error( 'agents_runtime_tool_invalid_request', $field . ' must be a non-empty string.' ); + } + + return trim( $value ); +} + +/** + * @param array $request Stored request. + * @return array + */ +function agents_runtime_tool_normalize_stored_request( array $request ): array { + $status = is_string( $request['status'] ?? null ) && '' !== trim( $request['status'] ) ? trim( $request['status'] ) : WP_Agent_Runtime_Tool_Request::STATUS_PENDING; + $normalized = WP_Agent_Runtime_Tool_Request::normalize( $request ); + $normalized['status'] = $status; + + if ( isset( $request['result'] ) && is_array( $request['result'] ) ) { + $stored_result = array(); + foreach ( $request['result'] as $key => $value ) { + if ( is_string( $key ) ) { + $stored_result[ $key ] = $value; + } + } + + $normalized['result'] = WP_Agent_Runtime_Tool_Result::from_request( $normalized, $stored_result ); + } + + return $normalized; +} + +/** @return array */ +function agents_runtime_tool_list_requests_input_schema(): array { + return array( + 'type' => 'object', + 'properties' => array( + 'run_id' => array( 'type' => 'string' ), + 'tool_name' => array( 'type' => 'string' ), + 'before' => array( 'type' => 'string' ), + 'after' => array( 'type' => 'string' ), + 'limit' => array( + 'type' => 'integer', + 'minimum' => 1, + ), + ), + ); +} + +/** @return array */ +function agents_runtime_tool_request_id_input_schema(): array { + return array( + 'type' => 'object', + 'required' => array( 'request_id' ), + 'properties' => array( + 'request_id' => array( 'type' => 'string' ), + ), + ); +} + +/** @return array */ +function agents_runtime_tool_submit_result_input_schema(): array { + return array( + 'type' => 'object', + 'required' => array( 'request_id' ), + 'properties' => array( + 'request_id' => array( 'type' => 'string' ), + 'success' => array( 'type' => 'boolean' ), + 'result' => array( 'type' => 'object' ), + 'error' => array( 'type' => 'string' ), + 'metadata' => array( 'type' => 'object' ), + 'resume' => array( 'type' => 'boolean' ), + 'context' => array( 'type' => 'object' ), + ), + ); +} + +/** @return array */ +function agents_runtime_tool_terminal_request_input_schema(): array { + return array( + 'type' => 'object', + 'required' => array( 'request_id' ), + 'properties' => array( + 'request_id' => array( 'type' => 'string' ), + 'reason' => array( 'type' => 'string' ), + 'resume' => array( 'type' => 'boolean' ), + 'context' => array( 'type' => 'object' ), + ), + ); +} + +/** @return array */ +function agents_runtime_tool_request_schema(): array { + return array( + 'type' => 'object', + 'required' => array( 'status', 'request_id', 'tool_name', 'tool_call_id' ), + 'properties' => array( + 'status' => array( 'type' => 'string' ), + 'request_id' => array( 'type' => 'string' ), + 'tool_name' => array( 'type' => 'string' ), + 'tool_call_id' => array( 'type' => 'string' ), + 'parameters' => array( 'type' => 'object' ), + 'run_id' => array( 'type' => 'string' ), + 'timeout_at' => array( 'type' => 'string' ), + 'runtime' => array( 'type' => 'object' ), + 'metadata' => array( 'type' => 'object' ), + 'result' => array( 'type' => 'object' ), + ), + ); +} + +/** @return array */ +function agents_runtime_tool_result_schema(): array { + return array( + 'type' => 'object', + 'required' => array( 'status', 'request_id', 'tool_name', 'success' ), + 'properties' => array( + 'status' => array( 'type' => 'string' ), + 'request_id' => array( 'type' => 'string' ), + 'tool_name' => array( 'type' => 'string' ), + 'success' => array( 'type' => 'boolean' ), + 'result' => array( 'type' => 'object' ), + 'error' => array( 'type' => 'string' ), + 'metadata' => array( 'type' => 'object' ), + ), + ); +} + +/** @return array */ +function agents_runtime_tool_list_requests_output_schema(): array { + return array( + 'type' => 'object', + 'required' => array( 'status', 'requests', 'count' ), + 'properties' => array( + 'status' => array( 'type' => 'string' ), + 'requests' => array( + 'type' => 'array', + 'items' => agents_runtime_tool_request_schema(), + ), + 'count' => array( 'type' => 'integer' ), + ), + ); +} + +/** @return array */ +function agents_runtime_tool_request_status_output_schema(): array { + return array( + 'type' => 'object', + 'required' => array( 'status', 'request' ), + 'properties' => array( + 'status' => array( 'type' => 'string' ), + 'request' => agents_runtime_tool_request_schema(), + ), + ); +} + +/** @return array */ +function agents_runtime_tool_submission_output_schema(): array { + return array( + 'type' => 'object', + 'required' => array( 'status', 'request', 'result', 'duplicate' ), + 'properties' => array( + 'status' => array( 'type' => 'string' ), + 'request' => agents_runtime_tool_request_schema(), + 'result' => agents_runtime_tool_result_schema(), + 'duplicate' => array( 'type' => 'boolean' ), + 'tool_result_message' => array( 'type' => 'object' ), + 'tool_execution_result' => array( 'type' => 'object' ), + 'continuation_result' => array( 'type' => array( 'object', 'null' ) ), + ), + ); +} + +/** @return array */ +function agents_runtime_tool_terminal_output_schema(): array { + return array( + 'type' => 'object', + 'required' => array( 'status', 'request', 'result' ), + 'properties' => array( + 'status' => array( 'type' => 'string' ), + 'request' => agents_runtime_tool_request_schema(), + 'result' => agents_runtime_tool_result_schema(), + 'tool_result_message' => array( 'type' => 'object' ), + 'tool_execution_result' => array( 'type' => 'object' ), + 'continuation_result' => array( 'type' => array( 'object', 'null' ) ), + 'cancelled' => array( 'type' => 'boolean' ), + ), + ); +} + +/** + * @param array $input Ability input. + */ +function agents_runtime_tool_read_permission( array $input ): bool { + $allowed = function_exists( 'current_user_can' ) ? current_user_can( 'manage_options' ) : false; + + /** + * Filters permission for read-only runtime-tool lifecycle abilities. + * + * @param bool $allowed Default permission result. + * @param array $input Ability input. + */ + return (bool) apply_filters( 'agents_runtime_tool_read_permission', $allowed, $input ); +} + +/** + * @param array $input Ability input. + */ +function agents_runtime_tool_write_permission( array $input ): bool { + $allowed = function_exists( 'current_user_can' ) ? current_user_can( 'manage_options' ) : false; + + /** + * Filters permission for mutating runtime-tool lifecycle abilities. + * + * @param bool $allowed Default permission result. + * @param array $input Ability input. + */ + return (bool) apply_filters( 'agents_runtime_tool_write_permission', $allowed, $input ); +} diff --git a/tests/runtime-tool-lifecycle-abilities-smoke.php b/tests/runtime-tool-lifecycle-abilities-smoke.php new file mode 100644 index 0000000..d1c1d61 --- /dev/null +++ b/tests/runtime-tool-lifecycle-abilities-smoke.php @@ -0,0 +1,192 @@ +code = $code; + $this->message = $message; + } + + public function get_error_code(): string { + return $this->code; + } + + public function get_error_message(): string { + return $this->message; + } + } +} + +if ( ! function_exists( 'wp_has_ability_category' ) ) { + function wp_has_ability_category( string $category ): bool { + return isset( $GLOBALS['__agents_api_smoke_ability_categories'][ $category ] ); + } +} + +if ( ! function_exists( 'wp_register_ability_category' ) ) { + function wp_register_ability_category( string $category, array $args ): void { + $GLOBALS['__agents_api_smoke_ability_categories'][ $category ] = $args; + } +} + +if ( ! function_exists( 'wp_has_ability' ) ) { + function wp_has_ability( string $ability ): bool { + return isset( $GLOBALS['__agents_api_smoke_abilities'][ $ability ] ); + } +} + +if ( ! function_exists( 'wp_register_ability' ) ) { + function wp_register_ability( string $ability, array $args ): void { + $GLOBALS['__agents_api_smoke_abilities'][ $ability ] = $args; + } +} + +function agents_api_smoke_execute_runtime_tool_ability( string $ability, array $input ) { + if ( ! isset( $GLOBALS['__agents_api_smoke_abilities'][ $ability ]['execute_callback'] ) ) { + return new WP_Error( 'missing_ability', 'Ability is not registered.' ); + } + + return call_user_func( $GLOBALS['__agents_api_smoke_abilities'][ $ability ]['execute_callback'], $input ); +} + +agents_api_smoke_require_module(); +do_action( 'wp_abilities_api_categories_init' ); +do_action( 'wp_abilities_api_init' ); + +agents_api_smoke_assert_equals( true, wp_has_ability( AgentsAPI\AI\AGENTS_LIST_RUNTIME_TOOL_REQUESTS_ABILITY ), 'list runtime-tool requests ability registers', $failures, $passes ); +agents_api_smoke_assert_equals( true, wp_has_ability( AgentsAPI\AI\AGENTS_GET_RUNTIME_TOOL_REQUEST_ABILITY ), 'get runtime-tool request ability registers', $failures, $passes ); +agents_api_smoke_assert_equals( true, wp_has_ability( AgentsAPI\AI\AGENTS_SUBMIT_RUNTIME_TOOL_RESULT_ABILITY ), 'submit runtime-tool result ability registers', $failures, $passes ); +agents_api_smoke_assert_equals( true, wp_has_ability( AgentsAPI\AI\AGENTS_TIMEOUT_RUNTIME_TOOL_REQUEST_ABILITY ), 'timeout runtime-tool request ability registers', $failures, $passes ); +agents_api_smoke_assert_equals( true, wp_has_ability( AgentsAPI\AI\AGENTS_CANCEL_RUNTIME_TOOL_REQUEST_ABILITY ), 'cancel runtime-tool request ability registers', $failures, $passes ); + +$submit_schema = $GLOBALS['__agents_api_smoke_abilities'][ AgentsAPI\AI\AGENTS_SUBMIT_RUNTIME_TOOL_RESULT_ABILITY ]['input_schema'] ?? array(); +agents_api_smoke_assert_equals( array( 'request_id' ), $submit_schema['required'] ?? array(), 'submit result schema requires request_id', $failures, $passes ); +agents_api_smoke_assert_equals( 'boolean', $submit_schema['properties']['resume']['type'] ?? '', 'submit result schema exposes generic resume flag', $failures, $passes ); + +$list_schema = $GLOBALS['__agents_api_smoke_abilities'][ AgentsAPI\AI\AGENTS_LIST_RUNTIME_TOOL_REQUESTS_ABILITY ]['input_schema'] ?? array(); +agents_api_smoke_assert_equals( 'integer', $list_schema['properties']['limit']['type'] ?? '', 'list schema exposes generic limit query hint', $failures, $passes ); + +$runtime_tool_store = new class() implements AgentsAPI\AI\WP_Agent_Runtime_Tool_Request_Store { + public array $requests = array(); + + public function create( array $request ): void { + $this->requests[ $request['request_id'] ] = $request; + } + + public function get( string $request_id ): ?array { + return $this->requests[ $request_id ] ?? null; + } + + public function complete( string $request_id, array $result ): void { + $this->requests[ $request_id ]['status'] = AgentsAPI\AI\WP_Agent_Runtime_Tool_Request::STATUS_COMPLETED; + $this->requests[ $request_id ]['result'] = $result; + } + + public function timeout( string $request_id ): void { + $this->requests[ $request_id ]['status'] = AgentsAPI\AI\WP_Agent_Runtime_Tool_Request::STATUS_TIMEOUT; + } + + public function recent_pending( array $query = array() ): array { + $limit = isset( $query['limit'] ) && is_int( $query['limit'] ) ? $query['limit'] : 100; + $pending = array_filter( + $this->requests, + static fn( array $request ): bool => AgentsAPI\AI\WP_Agent_Runtime_Tool_Request::STATUS_PENDING === ( $request['status'] ?? '' ) + ); + + return array_slice( array_values( $pending ), 0, $limit ); + } +}; + +add_filter( + 'wp_agent_runtime_tool_request_store', + static function () use ( $runtime_tool_store ) { + return $runtime_tool_store; + } +); + +$resume_calls = array(); +add_filter( + 'wp_agent_runtime_tool_continuation', + static function () use ( &$resume_calls ) { + return static function ( array $request, array $result, array $context ) use ( &$resume_calls ): array { + $resume_calls[] = compact( 'request', 'result', 'context' ); + return array( 'resumed' => true ); + }; + } +); + +$pending_request = AgentsAPI\AI\WP_Agent_Runtime_Tool_Request::from_tool_call( + 'client/choose_post', + 'call_lifecycle_ability', + array( 'post_id' => 123 ), + array( 'run_id' => 'run-lifecycle-ability' ) +); +AgentsAPI\AI\WP_Agent_Runtime_Tool_Lifecycle::create_pending_request( $runtime_tool_store, $pending_request ); + +$list = agents_api_smoke_execute_runtime_tool_ability( + AgentsAPI\AI\AGENTS_LIST_RUNTIME_TOOL_REQUESTS_ABILITY, + array( 'limit' => 1 ) +); +agents_api_smoke_assert_equals( 1, $list['count'] ?? 0, 'list ability returns pending request count', $failures, $passes ); +agents_api_smoke_assert_equals( $pending_request['request_id'], $list['requests'][0]['request_id'] ?? '', 'list ability returns pending request payload', $failures, $passes ); + +$submission = agents_api_smoke_execute_runtime_tool_ability( + AgentsAPI\AI\AGENTS_SUBMIT_RUNTIME_TOOL_RESULT_ABILITY, + array( + 'request_id' => $pending_request['request_id'], + 'success' => true, + 'result' => array( 'post_id' => 456 ), + 'resume' => true, + 'context' => array( 'source' => 'ability-smoke' ), + ) +); +agents_api_smoke_assert_equals( AgentsAPI\AI\WP_Agent_Runtime_Tool_Result::STATUS_SUBMITTED, $submission['status'] ?? '', 'submit ability returns submitted status', $failures, $passes ); +agents_api_smoke_assert_equals( true, $submission['continuation_result']['resumed'] ?? false, 'submit ability resumes through continuation adapter', $failures, $passes ); +agents_api_smoke_assert_equals( 'ability-smoke', $resume_calls[0]['context']['source'] ?? '', 'submit ability passes context to continuation', $failures, $passes ); + +$get = agents_api_smoke_execute_runtime_tool_ability( + AgentsAPI\AI\AGENTS_GET_RUNTIME_TOOL_REQUEST_ABILITY, + array( 'request_id' => $pending_request['request_id'] ) +); +agents_api_smoke_assert_equals( AgentsAPI\AI\WP_Agent_Runtime_Tool_Request::STATUS_COMPLETED, $get['status'] ?? '', 'get ability preserves completed request status', $failures, $passes ); +agents_api_smoke_assert_equals( 456, $get['request']['result']['result']['post_id'] ?? null, 'get ability exposes retained result payload', $failures, $passes ); + +$timeout_request = AgentsAPI\AI\WP_Agent_Runtime_Tool_Request::from_tool_call( 'client/choose_post', 'call_cancel_ability', array(), array( 'run_id' => 'run-cancel' ) ); +AgentsAPI\AI\WP_Agent_Runtime_Tool_Lifecycle::create_pending_request( $runtime_tool_store, $timeout_request ); + +$cancel = agents_api_smoke_execute_runtime_tool_ability( + AgentsAPI\AI\AGENTS_CANCEL_RUNTIME_TOOL_REQUEST_ABILITY, + array( + 'request_id' => $timeout_request['request_id'], + 'resume' => false, + ) +); +agents_api_smoke_assert_equals( AgentsAPI\AI\WP_Agent_Runtime_Tool_Request::STATUS_TIMEOUT, $cancel['status'] ?? '', 'cancel ability applies canonical timeout transition', $failures, $passes ); +agents_api_smoke_assert_equals( true, $cancel['cancelled'] ?? false, 'cancel ability marks cancellation envelope', $failures, $passes ); +agents_api_smoke_assert_equals( AgentsAPI\AI\WP_Agent_Runtime_Tool_Request::STATUS_TIMEOUT, $runtime_tool_store->requests[ $timeout_request['request_id'] ]['status'] ?? '', 'cancel ability delegates terminal transition to store', $failures, $passes ); + +agents_api_smoke_finish( 'Agents API runtime-tool lifecycle abilities', $failures, $passes );