From 2b719500e178f9018078174fdc7a4585a168a68e Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 18 Jun 2026 10:19:06 -0400 Subject: [PATCH] Extract run control store interface --- agents-api.php | 2 + ...lass-wp-agent-option-run-control-store.php | 101 ++++++++++++++++++ src/Runtime/class-wp-agent-run-control.php | 90 ++++------------ .../interface-wp-agent-run-control-store.php | 32 ++++++ tests/workflow-runner-smoke.php | 45 ++++++++ 5 files changed, 200 insertions(+), 70 deletions(-) create mode 100644 src/Runtime/class-wp-agent-option-run-control-store.php create mode 100644 src/Runtime/interface-wp-agent-run-control-store.php diff --git a/agents-api.php b/agents-api.php index b11df97..91f4bb5 100644 --- a/agents-api.php +++ b/agents-api.php @@ -162,6 +162,8 @@ 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/class-wp-agent-conversation-result.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'; require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-chat-run-control.php'; require_once AGENTS_API_PATH . 'src/Tasks/class-wp-agent-task-run-control.php'; diff --git a/src/Runtime/class-wp-agent-option-run-control-store.php b/src/Runtime/class-wp-agent-option-run-control-store.php new file mode 100644 index 0000000..7715dcb --- /dev/null +++ b/src/Runtime/class-wp-agent-option-run-control-store.php @@ -0,0 +1,101 @@ +>,queues:array>>} + */ + public function get_state( string $store_key ): array { + $state = function_exists( 'get_option' ) ? get_option( $store_key, array() ) : array(); + if ( ! is_array( $state ) ) { + $state = array(); + } + + return array( + 'runs' => $this->stored_runs( $state['runs'] ?? array() ), + 'queues' => $this->stored_queues( $state['queues'] ?? array() ), + ); + } + + /** + * @param string $store_key Store key. + * @param array{runs:array>,queues:array>>} $state State envelope. + */ + public function save_state( string $store_key, array $state ): void { + if ( function_exists( 'update_option' ) ) { + update_option( $store_key, $state, false ); + } + } + + /** + * @param mixed $runs Raw stored runs. + * @return array> + */ + private function stored_runs( mixed $runs ): array { + if ( ! is_array( $runs ) ) { + return array(); + } + + $stored = array(); + foreach ( $runs as $run_id => $run ) { + if ( is_string( $run_id ) && is_array( $run ) ) { + $stored[ $run_id ] = $this->assoc_array( $run ); + } + } + + return $stored; + } + + /** + * @param mixed $queues Raw stored queues. + * @return array>> + */ + private function stored_queues( mixed $queues ): array { + if ( ! is_array( $queues ) ) { + return array(); + } + + $stored = array(); + foreach ( $queues as $scope => $items ) { + if ( ! is_string( $scope ) || ! is_array( $items ) ) { + continue; + } + + $stored[ $scope ] = array(); + foreach ( $items as $item ) { + if ( is_array( $item ) ) { + $stored[ $scope ][] = $this->assoc_array( $item ); + } + } + } + + return $stored; + } + + /** + * @param array $value Raw array. + * @return array + */ + private function assoc_array( array $value ): array { + $result = array(); + foreach ( $value as $key => $item ) { + if ( is_string( $key ) ) { + $result[ $key ] = $item; + } + } + return $result; + } +} diff --git a/src/Runtime/class-wp-agent-run-control.php b/src/Runtime/class-wp-agent-run-control.php index cea29d6..d0951e7 100644 --- a/src/Runtime/class-wp-agent-run-control.php +++ b/src/Runtime/class-wp-agent-run-control.php @@ -24,6 +24,8 @@ class WP_Agent_Run_Control { public const STATUS_RUNTIME_TOOL_PENDING = 'runtime_tool_pending'; public const STATUS_APPROVAL_REQUIRED = 'approval_required'; + private static ?WP_Agent_Run_Control_Store $store = null; + /** @return string[] */ public static function statuses(): array { return array( @@ -215,26 +217,32 @@ public static function cancel_requested( string $store_key, string $run_id ): bo return null !== $run && self::STATUS_CANCELLING === ( $run['status'] ?? '' ); } - /** @return array{runs:array>,queues:array>>} */ - public static function state( string $store_key ): array { - $state = function_exists( 'get_option' ) ? get_option( $store_key, array() ) : array(); - if ( ! is_array( $state ) ) { - $state = array(); + public static function store(): WP_Agent_Run_Control_Store { + if ( null === self::$store ) { + self::$store = new WP_Agent_Option_Run_Control_Store(); } - return array( - 'runs' => self::stored_runs( $state['runs'] ?? array() ), - 'queues' => self::stored_queues( $state['queues'] ?? array() ), - ); + return self::$store; + } + + public static function set_store( WP_Agent_Run_Control_Store $store ): void { + self::$store = $store; + } + + public static function reset_store(): void { + self::$store = null; + } + + /** @return array{runs:array>,queues:array>>} */ + public static function state( string $store_key ): array { + return self::store()->get_state( $store_key ); } /** * @param array{runs:array>,queues:array>>} $state */ public static function save_state( string $store_key, array $state ): void { - if ( function_exists( 'update_option' ) ) { - update_option( $store_key, $state, false ); - } + self::store()->save_state( $store_key, $state ); } public static function now(): string { @@ -249,62 +257,4 @@ private static function int_value( mixed $value ): int { return is_int( $value ) || is_float( $value ) || is_string( $value ) || is_bool( $value ) ? (int) $value : 0; } - /** - * @param mixed $runs Raw stored runs. - * @return array> - */ - private static function stored_runs( mixed $runs ): array { - if ( ! is_array( $runs ) ) { - return array(); - } - - $stored = array(); - foreach ( $runs as $run_id => $run ) { - if ( is_string( $run_id ) && is_array( $run ) ) { - $stored[ $run_id ] = self::assoc_array( $run ); - } - } - - return $stored; - } - - /** - * @param mixed $queues Raw stored queues. - * @return array>> - */ - private static function stored_queues( mixed $queues ): array { - if ( ! is_array( $queues ) ) { - return array(); - } - - $stored = array(); - foreach ( $queues as $scope => $items ) { - if ( ! is_string( $scope ) || ! is_array( $items ) ) { - continue; - } - - $stored[ $scope ] = array(); - foreach ( $items as $item ) { - if ( is_array( $item ) ) { - $stored[ $scope ][] = self::assoc_array( $item ); - } - } - } - - return $stored; - } - - /** - * @param array $value - * @return array - */ - private static function assoc_array( array $value ): array { - $result = array(); - foreach ( $value as $key => $item ) { - if ( is_string( $key ) ) { - $result[ $key ] = $item; - } - } - return $result; - } } diff --git a/src/Runtime/interface-wp-agent-run-control-store.php b/src/Runtime/interface-wp-agent-run-control-store.php new file mode 100644 index 0000000..f67c43b --- /dev/null +++ b/src/Runtime/interface-wp-agent-run-control-store.php @@ -0,0 +1,32 @@ +>,queues:array>>} + */ + public function get_state( string $store_key ): array; + + /** + * Save the state for a store key. + * + * @param string $store_key Store key. + * @param array{runs:array>,queues:array>>} $state State envelope. + */ + public function save_state( string $store_key, array $state ): void; +} diff --git a/tests/workflow-runner-smoke.php b/tests/workflow-runner-smoke.php index c754d34..8bac321 100644 --- a/tests/workflow-runner-smoke.php +++ b/tests/workflow-runner-smoke.php @@ -183,12 +183,15 @@ function smoke_assert( $expected, $actual, string $name, array &$failures, int & require_once __DIR__ . '/../src/Workflows/class-wp-agent-workflow-store.php'; require_once __DIR__ . '/../src/Workflows/class-wp-agent-workflow-run-recorder.php'; require_once __DIR__ . '/../src/Abilities/class-wp-agent-ability-dispatcher.php'; +require_once __DIR__ . '/../src/Runtime/interface-wp-agent-run-control-store.php'; +require_once __DIR__ . '/../src/Runtime/class-wp-agent-option-run-control-store.php'; require_once __DIR__ . '/../src/Runtime/class-wp-agent-run-control.php'; require_once __DIR__ . '/../src/Workflows/class-wp-agent-workflow-run-context.php'; require_once __DIR__ . '/../src/Workflows/class-wp-agent-workflow-step-executor.php'; require_once __DIR__ . '/../src/Workflows/class-wp-agent-workflow-runner.php'; use AgentsAPI\AI\WP_Agent_Run_Control; +use AgentsAPI\AI\WP_Agent_Run_Control_Store; use AgentsAPI\AI\Workflows\WP_Agent_Workflow_Run_Recorder; use AgentsAPI\AI\Workflows\WP_Agent_Workflow_Run_Result; use AgentsAPI\AI\Workflows\WP_Agent_Workflow_Runner; @@ -218,6 +221,26 @@ public function find( string $run_id ): ?WP_Agent_Workflow_Run_Result { return n public function recent( array $args = array() ): array { return array(); } } +class Memory_Run_Control_Store implements WP_Agent_Run_Control_Store { + public array $states = array(); + public int $reads = 0; + public int $writes = 0; + + public function get_state( string $store_key ): array { + ++$this->reads; + $state = $this->states[ $store_key ] ?? array(); + return array( + 'runs' => isset( $state['runs'] ) && is_array( $state['runs'] ) ? $state['runs'] : array(), + 'queues' => isset( $state['queues'] ) && is_array( $state['queues'] ) ? $state['queues'] : array(), + ); + } + + public function save_state( string $store_key, array $state ): void { + ++$this->writes; + $this->states[ $store_key ] = $state; + } +} + // ─── Happy path: 2 sequential ability steps with bindings between them ─── workflow_runner_smoke_register_ability( @@ -267,6 +290,28 @@ static function ( array $input ): \WP_Error { do_action( 'wp_abilities_api_categories_init' ); do_action( 'wp_abilities_api_init' ); +// ─── Generic run-control facade can use an injected store ────────────── + +$memory_store = new Memory_Run_Control_Store(); +WP_Agent_Run_Control::set_store( $memory_store ); + +$stored_run = WP_Agent_Run_Control::start_run( + 'injected_run_control_store', + 'injected-run-1', + array( + 'workflow_id' => 'demo/injected-store', + 'metadata' => array( 'source' => 'smoke' ), + ) +); +$cancelled = WP_Agent_Run_Control::request_cancel( 'injected_run_control_store', 'injected-run-1' ); + +smoke_assert( 'injected-run-1', $stored_run['run_id'] ?? '', 'injected run-control store writes through facade', $failures, $passes ); +smoke_assert( WP_Agent_Run_Control::STATUS_CANCELLING, $cancelled['status'] ?? '', 'injected run-control store updates cancellation status', $failures, $passes ); +smoke_assert( true, $memory_store->writes >= 2, 'injected run-control store receives writes', $failures, $passes ); +smoke_assert( array(), $GLOBALS['__options']['injected_run_control_store'] ?? array(), 'injected run-control store does not write default options', $failures, $passes ); + +WP_Agent_Run_Control::reset_store(); + $spec = WP_Agent_Workflow_Spec::from_array( array( 'id' => 'demo/transform',