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 @@ -167,6 +167,8 @@
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-result-envelope.php';
require_once AGENTS_API_PATH . 'src/Runtime/interface-wp-agent-run-control-adapter.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-filter-run-control-adapter.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-run-outcome.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-runtime-package-run-request.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-runtime-package-run-result.php';
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"php tests/iteration-budget-smoke.php",
"php tests/conversation-loop-budgets-smoke.php",
"php tests/runtime-package-run-contract-smoke.php",
"php tests/run-control-normalization-smoke.php",
"php tests/channels-smoke.php",
"php tests/chat-run-control-smoke.php",
"php tests/task-execution-smoke.php",
Expand Down
50 changes: 33 additions & 17 deletions src/Channels/register-agents-chat-run-control-abilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
namespace AgentsAPI\AI\Channels;

use AgentsAPI\AI\WP_Agent_Chat_Run_Control;
use AgentsAPI\AI\WP_Agent_Filter_Run_Control_Adapter;
use AgentsAPI\AI\WP_Agent_Run_Control;

defined( 'ABSPATH' ) || exit;

Expand Down Expand Up @@ -94,9 +96,9 @@ static function (): void {
* @return array<string, mixed>|\WP_Error
*/
function agents_get_chat_run( array $input ) {
$handler = apply_filters( 'wp_agent_chat_run_status_handler', null, $input );
if ( is_callable( $handler ) ) {
return agents_chat_run_control_normalize_result( call_user_func( $handler, $input ), 'agents_chat_run_invalid_status' );
$result = agents_chat_run_control_adapter()->get_run( $input );
if ( null !== $result ) {
return is_wp_error( $result ) ? $result : agents_chat_run_control_normalize_result( $result, 'agents_chat_run_invalid_status' );
}

$run = WP_Agent_Chat_Run_Control::get_run( agents_chat_run_control_string( $input['run_id'] ?? '' ) );
Expand All @@ -116,23 +118,17 @@ function agents_get_chat_run( array $input ) {
* @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 agents_chat_run_events_normalize_result( agents_chat_run_control_adapter()->list_events( $input ) );
}

/**
* @param array<string, mixed> $input Ability input.
* @return array<string, mixed>|\WP_Error
*/
function agents_cancel_chat_run( array $input ) {
$handler = apply_filters( 'wp_agent_chat_run_cancel_handler', null, $input );
if ( is_callable( $handler ) ) {
$result = agents_chat_run_control_normalize_result( call_user_func( $handler, $input ), 'agents_chat_run_invalid_cancel_result' );
$result = agents_chat_run_control_adapter()->cancel_run( $input );
if ( null !== $result ) {
$result = is_wp_error( $result ) ? $result : agents_chat_run_control_normalize_result( $result, 'agents_chat_run_invalid_cancel_result' );
} else {
$run = WP_Agent_Chat_Run_Control::get_run( agents_chat_run_control_string( $input['run_id'] ?? '' ) );
$requested_session_id = agents_chat_run_control_string( $input['session_id'] ?? '' );
Expand Down Expand Up @@ -284,8 +280,9 @@ function agents_chat_run_control_normalize_result( $result, string $error_code )
return $result;
}

if ( ! is_array( $result ) ) {
return new \WP_Error( $error_code, 'Chat run-control handlers must return an array or WP_Error.' );
$result = WP_Agent_Run_Control::normalize_run_result( $result, $error_code );
if ( is_wp_error( $result ) ) {
return $result;
}

try {
Expand All @@ -304,8 +301,9 @@ function agents_chat_run_events_normalize_result( $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 = WP_Agent_Run_Control::normalize_events_result( $result, 'agents_chat_run_invalid_events_result' );
if ( is_wp_error( $result ) ) {
return $result;
}

$result = agents_chat_run_control_string_keyed_array( $result );
Expand All @@ -319,6 +317,24 @@ function agents_chat_run_events_normalize_result( $result ) {
return $result;
}

function agents_chat_run_control_adapter(): WP_Agent_Filter_Run_Control_Adapter {
static $adapter = null;
if ( ! $adapter instanceof WP_Agent_Filter_Run_Control_Adapter ) {
$adapter = new WP_Agent_Filter_Run_Control_Adapter(
'wp_agent_chat_run_status_handler',
'wp_agent_chat_run_events_handler',
'wp_agent_chat_run_cancel_handler',
'agents_chat_run_invalid_status',
'agents_chat_run_invalid_events_result',
'agents_chat_run_invalid_cancel_result',
'agents_chat_run_events_no_handler',
'No chat run events handler is registered.'
);
}

return $adapter;
}

function agents_chat_run_control_string( mixed $value ): string {
if ( is_scalar( $value ) ) {
return (string) $value;
Expand Down
73 changes: 73 additions & 0 deletions src/Runtime/class-wp-agent-filter-run-control-adapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
/**
* Filter-backed generic run-control adapter.
*
* @package AgentsAPI
*/

namespace AgentsAPI\AI;

defined( 'ABSPATH' ) || exit;

class WP_Agent_Filter_Run_Control_Adapter implements WP_Agent_Run_Control_Adapter {

/**
* @param non-empty-string $status_filter Filter that returns a get-run handler.
* @param non-empty-string $events_filter Filter that returns a list-events handler.
* @param non-empty-string $cancel_filter Filter that returns a cancel handler.
* @param non-empty-string $invalid_status_code Error code for invalid get-run results.
* @param non-empty-string $invalid_events_code Error code for invalid event results.
* @param non-empty-string $invalid_cancel_code Error code for invalid cancel results.
* @param non-empty-string $events_no_handler_code Error code when no event handler is registered.
* @param non-empty-string $events_no_handler_message Error message when no event handler is registered.
*/
public function __construct(
private string $status_filter,
private string $events_filter,
private string $cancel_filter,
private string $invalid_status_code,
private string $invalid_events_code,
private string $invalid_cancel_code,
private string $events_no_handler_code,
private string $events_no_handler_message
) {}

/**
* @param array<string,mixed> $input Run-control request input.
* @return array<string,mixed>|\WP_Error|null
*/
public function get_run( array $input ) {
$handler = apply_filters( $this->status_filter, null, $input );
if ( ! is_callable( $handler ) ) {
return null;
}

return WP_Agent_Run_Control::normalize_run_result( call_user_func( $handler, $input ), $this->invalid_status_code );
}

/**
* @param array<string,mixed> $input Run-control request input.
* @return array<string,mixed>|\WP_Error
*/
public function list_events( array $input ) {
$handler = apply_filters( $this->events_filter, null, $input );
if ( ! is_callable( $handler ) ) {
return new \WP_Error( $this->events_no_handler_code, $this->events_no_handler_message );
}

return WP_Agent_Run_Control::normalize_events_result( call_user_func( $handler, $input ), $this->invalid_events_code );
}

/**
* @param array<string,mixed> $input Run-control request input.
* @return array<string,mixed>|\WP_Error|null
*/
public function cancel_run( array $input ) {
$handler = apply_filters( $this->cancel_filter, null, $input );
if ( ! is_callable( $handler ) ) {
return null;
}

return WP_Agent_Run_Control::normalize_cancel_result( call_user_func( $handler, $input ), $this->invalid_cancel_code );
}
}
122 changes: 121 additions & 1 deletion src/Runtime/class-wp-agent-run-control.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,111 @@ public static function normalize_run( array $run ): array {
return $normalized;
}

/**
* Normalize a handler result into the generic run envelope.
*
* @param mixed $result Handler result.
* @param string $error_code Error code for invalid results.
* @return array<string,mixed>|\WP_Error
*/
public static function normalize_run_result( mixed $result, string $error_code ) {
if ( is_wp_error( $result ) ) {
return $result;
}

if ( ! is_array( $result ) ) {
return new \WP_Error( $error_code, 'Run-control handlers must return an array or WP_Error.' );
}

try {
return self::normalize_run( self::string_keyed_array( $result ) );
} catch ( \InvalidArgumentException $error ) {
return new \WP_Error( $error_code, $error->getMessage() );
}
}

/**
* Normalize a cancellation result and infer whether the request was accepted.
*
* @param mixed $result Handler result.
* @param string $error_code Error code for invalid results.
* @return array<string,mixed>|\WP_Error
*/
public static function normalize_cancel_result( mixed $result, string $error_code ) {
$result = self::normalize_run_result( $result, $error_code );
if ( is_wp_error( $result ) ) {
return $result;
}

$status = self::normalize_status( $result['status'] ?? self::STATUS_RUNNING );
$result['status'] = $status;
$result['cancelled'] = (bool) ( $result['cancelled'] ?? in_array(
$status,
array(
self::STATUS_CANCELLING,
self::STATUS_CANCELLED,
),
true
) );

return $result;
}

/**
* Normalize an event page for an addressable run.
*
* @param mixed $result Handler result.
* @param string $error_code Error code for invalid results.
* @return array<string,mixed>|\WP_Error
*/
public static function normalize_events_result( mixed $result, string $error_code ) {
if ( is_wp_error( $result ) ) {
return $result;
}

if ( ! is_array( $result ) ) {
return new \WP_Error( $error_code, 'Run event handlers must return an array or WP_Error.' );
}

try {
$run = self::normalize_run( self::string_keyed_array( $result ) );
} catch ( \InvalidArgumentException $error ) {
return new \WP_Error( $error_code, $error->getMessage() );
}

$events = array();
foreach ( is_array( $result['events'] ?? null ) ? array_values( $result['events'] ) : array() as $event ) {
if ( is_array( $event ) ) {
$events[] = self::normalize_event( self::string_keyed_array( $event ) );
}
}

$run['events'] = $events;
$run['cursor'] = self::string_value( $result['cursor'] ?? '' );
$run['has_more'] = (bool) ( $result['has_more'] ?? false );

return $run;
}

/**
* @param array<string,mixed> $event Raw event.
* @return array<string,mixed>
*/
public static function normalize_event( array $event ): array {
$normalized = array(
'id' => self::string_value( $event['id'] ?? '' ),
'type' => self::string_value( $event['type'] ?? '' ),
'created_at' => self::string_value( $event['created_at'] ?? '' ),
'metadata' => isset( $event['metadata'] ) && is_array( $event['metadata'] ) ? self::string_keyed_array( $event['metadata'] ) : array(),
);

if ( isset( $event['message'] ) ) {
$normalized['message'] = self::string_value( $event['message'] );
}

return $normalized;
}

/**
* Start or update an addressable run in the selected store.
*
Expand Down Expand Up @@ -255,10 +360,25 @@ public static function now(): string {
return gmdate( 'c' );
}

private static function string_value( mixed $value ): string {
public static function string_value( mixed $value ): string {
return is_int( $value ) || is_float( $value ) || is_string( $value ) || is_bool( $value ) ? (string) $value : '';
}

/**
* @param array<array-key,mixed> $value Raw array.
* @return array<string,mixed>
*/
public static function string_keyed_array( array $value ): array {
$result = array();
foreach ( $value as $key => $item ) {
if ( is_string( $key ) ) {
$result[ $key ] = $item;
}
}

return $result;
}

private static function int_value( mixed $value ): int {
return is_int( $value ) || is_float( $value ) || is_string( $value ) || is_bool( $value ) ? (int) $value : 0;
}
Expand Down
31 changes: 31 additions & 0 deletions src/Runtime/interface-wp-agent-run-control-adapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
/**
* Generic run-control adapter contract.
*
* @package AgentsAPI
*/

namespace AgentsAPI\AI;

defined( 'ABSPATH' ) || exit;

interface WP_Agent_Run_Control_Adapter {

/**
* @param array<string,mixed> $input Run-control request input.
* @return array<string,mixed>|\WP_Error|null
*/
public function get_run( array $input );

/**
* @param array<string,mixed> $input Run-control request input.
* @return array<string,mixed>|\WP_Error
*/
public function list_events( array $input );

/**
* @param array<string,mixed> $input Run-control request input.
* @return array<string,mixed>|\WP_Error|null
*/
public function cancel_run( array $input );
}
Loading