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
2 changes: 2 additions & 0 deletions agents-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@
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/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-run-outcome.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-conversation-result.php';
Expand Down
101 changes: 101 additions & 0 deletions src/Runtime/class-wp-agent-option-run-control-store.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php
/**
* Option-backed run-control store.
*
* @package AgentsAPI
*/

namespace AgentsAPI\AI;

defined( 'ABSPATH' ) || exit;

/**
* Persists run-control state in WordPress options.
*/
class WP_Agent_Option_Run_Control_Store implements WP_Agent_Run_Control_Store {

/**
* @param string $store_key Store key.
* @return array{runs:array<string,array<string,mixed>>,queues:array<string,array<int,array<string,mixed>>>}
*/
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<string,array<string,mixed>>,queues:array<string,array<int,array<string,mixed>>>} $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<string,array<string,mixed>>
*/
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<string,array<int,array<string,mixed>>>
*/
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<mixed> $value Raw array.
* @return array<string,mixed>
*/
private function assoc_array( array $value ): array {
$result = array();
foreach ( $value as $key => $item ) {
if ( is_string( $key ) ) {
$result[ $key ] = $item;
}
}
return $result;
}
}
90 changes: 20 additions & 70 deletions src/Runtime/class-wp-agent-run-control.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class WP_Agent_Run_Control {
public const STATUS_STALLED = 'stalled';
public const STATUS_INTERRUPTED = 'interrupted';

private static ?WP_Agent_Run_Control_Store $store = null;

/** @return string[] */
public static function statuses(): array {
return array(
Expand Down Expand Up @@ -221,26 +223,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<string,array<string,mixed>>,queues:array<string,array<int,array<string,mixed>>>} */
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<string,array<string,mixed>>,queues:array<string,array<int,array<string,mixed>>>} */
public static function state( string $store_key ): array {
return self::store()->get_state( $store_key );
}

/**
* @param array{runs:array<string,array<string,mixed>>,queues:array<string,array<int,array<string,mixed>>>} $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 {
Expand All @@ -255,62 +263,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<string,array<string,mixed>>
*/
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<string,array<int,array<string,mixed>>>
*/
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<mixed> $value
* @return array<string,mixed>
*/
private static function assoc_array( array $value ): array {
$result = array();
foreach ( $value as $key => $item ) {
if ( is_string( $key ) ) {
$result[ $key ] = $item;
}
}
return $result;
}
}
32 changes: 32 additions & 0 deletions src/Runtime/interface-wp-agent-run-control-store.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
/**
* Generic run-control store contract.
*
* @package AgentsAPI
*/

namespace AgentsAPI\AI;

defined( 'ABSPATH' ) || exit;

/**
* Persists addressable run-control state.
*/
interface WP_Agent_Run_Control_Store {

/**
* Read the state for a store key.
*
* @param string $store_key Store key.
* @return array{runs:array<string,array<string,mixed>>,queues:array<string,array<int,array<string,mixed>>>}
*/
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<string,array<string,mixed>>,queues:array<string,array<int,array<string,mixed>>>} $state State envelope.
*/
public function save_state( string $store_key, array $state ): void;
}
45 changes: 45 additions & 0 deletions tests/workflow-runner-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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',
Expand Down
Loading