diff --git a/src/Runtime/register-runtime-package-run-ability.php b/src/Runtime/register-runtime-package-run-ability.php index 1756098..576be59 100644 --- a/src/Runtime/register-runtime-package-run-ability.php +++ b/src/Runtime/register-runtime-package-run-ability.php @@ -19,27 +19,59 @@ const AGENTS_LIST_RUNTIME_PACKAGE_RUN_EVENTS_ABILITY = 'agents/list-runtime-package-run-events'; const AGENTS_RUNTIME_PACKAGE_RUN_CONTROL_STORE = 'agents_api_runtime_package_run_control'; -add_action( - 'wp_abilities_api_categories_init', - static function (): void { - if ( wp_has_ability_category( 'agents-api' ) ) { - return; - } +add_action( 'wp_abilities_api_categories_init', __NAMESPACE__ . '\agents_register_runtime_package_ability_category' ); +add_action( 'wp_abilities_api_init', __NAMESPACE__ . '\agents_register_runtime_package_run_abilities' ); - wp_register_ability_category( - 'agents-api', - array( - 'label' => 'Agents API', - 'description' => 'Cross-cutting abilities provided by the Agents API substrate.', - ) - ); +if ( function_exists( 'did_action' ) && did_action( 'wp_abilities_api_init' ) ) { + agents_register_runtime_package_run_abilities(); +} + +/** + * Register the Agents API runtime package ability category. + */ +function agents_register_runtime_package_ability_category(): void { + if ( ! function_exists( 'wp_has_ability_category' ) || ! function_exists( 'wp_register_ability_category' ) ) { + return; + } + + if ( wp_has_ability_category( 'agents-api' ) ) { + return; + } + + /** @var array $args */ + $args = array( + 'label' => 'Agents API', + 'description' => 'Cross-cutting abilities provided by the Agents API substrate.', + ); + + if ( doing_action( 'wp_abilities_api_categories_init' ) ) { + wp_register_ability_category( 'agents-api', $args ); + return; + } + + if ( ! did_action( 'init' ) || ! class_exists( '\WP_Ability_Categories_Registry' ) ) { + return; + } + + $registry = \WP_Ability_Categories_Registry::get_instance(); + if ( null === $registry ) { + return; + } + + $registry->register( 'agents-api', $args ); +} + +/** + * Register canonical runtime package execution abilities. + */ +function agents_register_runtime_package_run_abilities(): void { + if ( ! function_exists( 'wp_has_ability' ) || ! function_exists( 'wp_register_ability' ) ) { + return; } -); -add_action( - 'wp_abilities_api_init', - static function (): void { - $abilities = array( + agents_register_runtime_package_ability_category(); + + $abilities = array( AGENTS_RUN_RUNTIME_PACKAGE_ABILITY => array( 'label' => 'Run Runtime Package', 'description' => 'Canonical entry point for running a portable agent package workflow. Dispatches to a consumer-provided runtime handler.', @@ -84,30 +116,53 @@ static function (): void { ), ); - 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'], - ), - ) - ); + foreach ( $abilities as $ability => $args ) { + if ( wp_has_ability( $ability ) ) { + continue; } + + agents_register_runtime_package_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'], + ), + ) + ); } -); +} + +/** + * Register a runtime package ability across normal and late-loaded runtimes. + * + * @param string $ability Ability name. + * @param array $args Ability args. + */ +function agents_register_runtime_package_ability( string $ability, array $args ): void { + if ( wp_has_ability( $ability ) ) { + return; + } + + if ( doing_action( 'wp_abilities_api_init' ) ) { + wp_register_ability( $ability, $args ); + return; + } + + if ( ! did_action( 'init' ) || ! class_exists( '\WP_Abilities_Registry' ) ) { + return; + } + + $registry = \WP_Abilities_Registry::get_instance(); + $registry->register( $ability, $args ); +} /** * Dispatch a runtime package workflow run to a registered consumer handler. diff --git a/tests/runtime-package-run-contract-smoke.php b/tests/runtime-package-run-contract-smoke.php index e52077b..108815c 100644 --- a/tests/runtime-package-run-contract-smoke.php +++ b/tests/runtime-package-run-contract-smoke.php @@ -30,6 +30,167 @@ function is_wp_error( $value ): bool { } require_once __DIR__ . '/agents-api-smoke-helpers.php'; + +if ( ! class_exists( 'WP_Ability' ) ) { + class WP_Ability { + /** @param array $args Ability registration arguments. */ + public function __construct( private string $name, private array $args ) {} + + /** @param array $input Ability input. */ + public function execute( array $input ) { + $callback = $this->args['execute_callback'] ?? null; + return is_callable( $callback ) ? call_user_func( $callback, $input ) : null; + } + + public function get_name(): string { + return $this->name; + } + } +} + +if ( ! class_exists( 'WP_Ability_Category' ) ) { + class WP_Ability_Category { + /** @param array $args Category registration arguments. */ + public function __construct( private string $slug, private array $args ) {} + + public function get_slug(): string { + return $this->slug; + } + } +} + +if ( ! class_exists( 'WP_Ability_Categories_Registry' ) ) { + class WP_Ability_Categories_Registry { + private static ?self $instance = null; + + /** @var array */ + private array $categories = array(); + + public static function get_instance(): ?self { + if ( ! did_action( 'init' ) ) { + _doing_it_wrong( __METHOD__, 'Ability API should not be initialized before init.', '6.9.0' ); + return null; + } + + if ( null === self::$instance ) { + self::$instance = new self(); + do_action( 'wp_abilities_api_categories_init', self::$instance ); + } + + return self::$instance; + } + + /** @param array $args Category registration arguments. */ + public function register( string $category, array $args ): ?WP_Ability_Category { + if ( $this->is_registered( $category ) ) { + return null; + } + + $this->categories[ $category ] = new WP_Ability_Category( $category, $args ); + return $this->categories[ $category ]; + } + + public function is_registered( string $category ): bool { + return isset( $this->categories[ $category ] ); + } + + public static function reset_for_smoke(): void { + self::$instance = null; + } + } +} + +if ( ! class_exists( 'WP_Abilities_Registry' ) ) { + class WP_Abilities_Registry { + private static ?self $instance = null; + + /** @var array */ + private array $abilities = array(); + + public static function get_instance(): ?self { + if ( ! did_action( 'init' ) ) { + _doing_it_wrong( __METHOD__, 'Ability API should not be initialized before init.', '6.9.0' ); + return null; + } + + if ( null === self::$instance ) { + self::$instance = new self(); + WP_Ability_Categories_Registry::get_instance(); + do_action( 'wp_abilities_api_init', self::$instance ); + } + + return self::$instance; + } + + /** @param array $args Ability registration arguments. */ + public function register( string $ability, array $args ): ?WP_Ability { + if ( $this->is_registered( $ability ) || ! wp_has_ability_category( (string) ( $args['category'] ?? '' ) ) ) { + return null; + } + + $this->abilities[ $ability ] = new WP_Ability( $ability, $args ); + return $this->abilities[ $ability ]; + } + + public function is_registered( string $ability ): bool { + return isset( $this->abilities[ $ability ] ); + } + + public function get_registered( string $ability ): ?WP_Ability { + return $this->abilities[ $ability ] ?? null; + } + + public function reset_registered_for_smoke(): void { + $this->abilities = array(); + } + } +} + +if ( ! function_exists( 'wp_has_ability_category' ) ) { + function wp_has_ability_category( string $category ): bool { + $registry = WP_Ability_Categories_Registry::get_instance(); + return null !== $registry && $registry->is_registered( $category ); + } +} + +if ( ! function_exists( 'wp_register_ability_category' ) ) { + function wp_register_ability_category( string $category, array $args ): ?WP_Ability_Category { + if ( ! doing_action( 'wp_abilities_api_categories_init' ) ) { + _doing_it_wrong( __FUNCTION__, 'Ability categories must be registered on wp_abilities_api_categories_init.', '6.9.0' ); + return null; + } + + $registry = WP_Ability_Categories_Registry::get_instance(); + return null === $registry ? null : $registry->register( $category, $args ); + } +} + +if ( ! function_exists( 'wp_has_ability' ) ) { + function wp_has_ability( string $ability ): bool { + $registry = WP_Abilities_Registry::get_instance(); + return null !== $registry && $registry->is_registered( $ability ); + } +} + +if ( ! function_exists( 'wp_register_ability' ) ) { + function wp_register_ability( string $ability, array $args ): ?WP_Ability { + if ( ! doing_action( 'wp_abilities_api_init' ) ) { + _doing_it_wrong( __FUNCTION__, 'Abilities must be registered on wp_abilities_api_init.', '6.9.0' ); + return null; + } + + $registry = WP_Abilities_Registry::get_instance(); + return null === $registry ? null : $registry->register( $ability, $args ); + } +} + +if ( ! function_exists( 'wp_get_ability' ) ) { + function wp_get_ability( string $ability ): ?WP_Ability { + $registry = WP_Abilities_Registry::get_instance(); + return null === $registry ? null : $registry->get_registered( $ability ); + } +} + require_once __DIR__ . '/../src/Runtime/class-wp-agent-runtime-package-run-request.php'; require_once __DIR__ . '/../src/Runtime/class-wp-agent-runtime-package-run-result.php'; require_once __DIR__ . '/../src/Runtime/register-runtime-package-run-ability.php'; @@ -38,6 +199,45 @@ function is_wp_error( $value ): bool { use AgentsAPI\AI\WP_Agent_Runtime_Package_Run_Request; use AgentsAPI\AI\WP_Agent_Runtime_Package_Run_Result; +echo "\n[0] Runtime package ability resolves in normal and late Abilities API lifecycles:\n"; +do_action( 'init' ); +$registry = WP_Abilities_Registry::get_instance(); +agents_api_smoke_assert_equals( true, wp_get_ability( AgentsAPI\AI\AGENTS_RUN_RUNTIME_PACKAGE_ABILITY ) instanceof WP_Ability, 'runtime package ability registers through wp_abilities_api_init', $failures, $passes ); +agents_api_smoke_assert_equals( 0, count( $GLOBALS['__agents_api_smoke_wrong'] ), 'normal registration path does not call public helpers outside their actions', $failures, $passes ); + +if ( $registry instanceof WP_Abilities_Registry ) { + $registry->reset_registered_for_smoke(); +} + +AgentsAPI\AI\agents_register_runtime_package_run_abilities(); +$late_ability = wp_get_ability( AgentsAPI\AI\AGENTS_RUN_RUNTIME_PACKAGE_ABILITY ); +agents_api_smoke_assert_equals( true, $late_ability instanceof WP_Ability, 'runtime package ability resolves through wp_get_ability after abilities init already fired', $failures, $passes ); + +add_filter( + 'wp_agent_runtime_package_run_handler', + static function ( $handler, WP_Agent_Runtime_Package_Run_Request $handler_request ) { + unset( $handler ); + return static function () use ( $handler_request ): array { + return array( + 'status' => 'succeeded', + 'result' => array( 'workflow_id' => $handler_request->get_workflow()['id'] ?? '' ), + ); + }; + }, + 10, + 2 +); + +$late_dispatch = $late_ability instanceof WP_Ability ? $late_ability->execute( + array( + 'package' => array( 'slug' => 'site-builder' ), + 'workflow' => array( 'id' => 'late-load' ), + ) +) : null; +agents_api_smoke_assert_equals( false, is_wp_error( $late_dispatch ), 'late-resolved ability executes through the canonical ability object', $failures, $passes ); +agents_api_smoke_assert_equals( 'late-load', is_array( $late_dispatch ) ? $late_dispatch['result']['workflow_id'] ?? '' : '', 'late-resolved ability uses the runtime package handler filter', $failures, $passes ); +$GLOBALS['__agents_api_smoke_actions']['wp_agent_runtime_package_run_handler'] = array(); + echo "\n[1] Request validates package and workflow selectors:\n"; $request = WP_Agent_Runtime_Package_Run_Request::from_array( array(