Skip to content
Open
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
45 changes: 34 additions & 11 deletions src/Channels/register-agents-chat-ability.php
Original file line number Diff line number Diff line change
Expand Up @@ -383,52 +383,75 @@ function agents_chat_input_schema(): array {
'type' => 'object',
'description' => 'Optional transport-level context describing where this turn originated. Hosts may include opaque, product-owned metadata; Agents API preserves it but does not define product semantics.',
'properties' => array(
'source' => array(
'source' => array(
'type' => 'string',
'enum' => array( 'channel', 'bridge', 'rest', 'block', 'peer-agent', 'jsonrpc' ),
'description' => 'How the request reached this dispatcher.',
),
'client_name' => array(
'client_name' => array(
'type' => 'string',
'description' => 'Specific client identifier within the source (e.g. "cli-relay" or "messaging-bot").',
),
'connector_id' => array(
'connector_id' => array(
'type' => 'string',
'description' => 'Stable connector or channel instance id used for settings, attribution, and external conversation session mapping.',
),
'external_provider' => array(
'external_provider' => array(
'type' => array( 'string', 'null' ),
'description' => 'External network identifier (e.g. "whatsapp", "slack", "email"). Null if not applicable.',
),
'external_conversation_id' => array(
'external_conversation_id' => array(
'type' => array( 'string', 'null' ),
'description' => 'Opaque external conversation id (chat JID, channel id, thread root). Null if the source has no per-conversation isolation.',
),
'external_message_id' => array(
'external_message_id' => array(
'type' => array( 'string', 'null' ),
'description' => 'Stable transport-side message id, used for reply threading / dedup / audit.',
),
'sender_id' => array(
'sender_id' => array(
'type' => array( 'string', 'null' ),
'description' => 'Opaque external sender id. In group rooms this identifies the human sender inside the conversation.',
),
'room_kind' => array(
'room_kind' => array(
'type' => array( 'string', 'null' ),
'enum' => array( 'dm', 'group', 'channel' ),
'description' => 'Conversation kind: direct message, multi-participant group, broadcast channel. Null when the source has no notion of room kind.',
),
'caller_agent' => array(
'caller_agent' => array(
'type' => array( 'string', 'null' ),
'description' => 'Agent slug that initiated this turn when source is peer-agent. Null when the source is not another agent.',
),
'caller_session_id' => array(
'caller_session_id' => array(
'type' => array( 'string', 'null' ),
'description' => 'Originating agent session id when source is peer-agent. Null when unavailable or not applicable.',
),
'peer_agent_call' => array(
'peer_agent_call' => array(
'type' => 'boolean',
'description' => 'Whether this turn is an explicit agent-to-agent delegation call.',
),
'runtime_tools' => array(
'type' => 'object',
'description' => 'Explicit runtime-local tool declarations supplied by a trusted caller for this turn.',
'additionalProperties' => array( 'type' => 'object' ),
),
'runtime_tool_declarations' => array(
'type' => 'object',
'description' => 'Alias for explicit runtime-local tool declarations supplied by a trusted caller for this turn.',
'additionalProperties' => array( 'type' => 'object' ),
),
'tool_declarations' => array(
'type' => 'object',
'description' => 'Transport-level tool declarations supplied by a trusted caller for this turn.',
'additionalProperties' => array( 'type' => 'object' ),
),
'runtime_tool_callback' => array(
'type' => 'string',
'description' => 'Runtime-local callback identifier for executing runtime tool calls in trusted in-process callers.',
),
'runtime_tool_timeout' => array(
'type' => 'integer',
'description' => 'Runtime-local tool timeout in seconds.',
),
),
),
),
Expand Down
222 changes: 142 additions & 80 deletions tests/ability-meta-abilities-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,38 @@
define( 'ABSPATH', __DIR__ . '/' );
}

class WP_Error {
public function __construct( private string $code = '', private string $message = '', private $data = null ) {}
public function get_error_code(): string { return $this->code; }
public function get_error_message(): string { return $this->message; }
public function get_error_data() { return $this->data; }
if ( ! class_exists( 'WP_Error' ) ) {
class WP_Error {
public function __construct( private string $code = '', private string $message = '', private $data = null ) {}
public function get_error_code(): string { return $this->code; }
public function get_error_message(): string { return $this->message; }
public function get_error_data() { return $this->data; }
}
}

class WP_Ability_Category {}

class WP_Ability {
public function __construct( private string $name, private array $args ) {}
public function get_name(): string { return $this->name; }
public function get_label(): string { return (string) ( $this->args['label'] ?? '' ); }
public function get_description(): string { return (string) ( $this->args['description'] ?? '' ); }
public function get_category(): string { return (string) ( $this->args['category'] ?? '' ); }
public function get_input_schema(): array { return isset( $this->args['input_schema'] ) && is_array( $this->args['input_schema'] ) ? $this->args['input_schema'] : array(); }
public function get_output_schema(): array { return isset( $this->args['output_schema'] ) && is_array( $this->args['output_schema'] ) ? $this->args['output_schema'] : array(); }
public function get_meta_item( string $key, $default = null ) { return $this->args['meta'][ $key ] ?? $default; }
public function execute( $input = null ) {
$permission = $this->args['permission_callback'] ?? null;
if ( is_callable( $permission ) && true !== call_user_func( $permission, is_array( $input ) ? $input : array() ) ) {
return new WP_Error( 'ability_invalid_permissions', 'Permission denied.' );
}
if ( ! class_exists( 'WP_Ability_Category' ) ) {
class WP_Ability_Category {}
}

$callback = $this->args['execute_callback'] ?? null;
return is_callable( $callback ) ? call_user_func( $callback, is_array( $input ) ? $input : array() ) : null;
if ( ! class_exists( 'WP_Ability' ) ) {
class WP_Ability {
public function __construct( private string $name, private array $args ) {}
public function get_name(): string { return $this->name; }
public function get_label(): string { return (string) ( $this->args['label'] ?? '' ); }
public function get_description(): string { return (string) ( $this->args['description'] ?? '' ); }
public function get_category(): string { return (string) ( $this->args['category'] ?? '' ); }
public function get_input_schema(): array { return isset( $this->args['input_schema'] ) && is_array( $this->args['input_schema'] ) ? $this->args['input_schema'] : array(); }
public function get_output_schema(): array { return isset( $this->args['output_schema'] ) && is_array( $this->args['output_schema'] ) ? $this->args['output_schema'] : array(); }
public function get_meta_item( string $key, $default = null ) { return $this->args['meta'][ $key ] ?? $default; }
public function execute( $input = null ) {
$permission = $this->args['permission_callback'] ?? null;
if ( is_callable( $permission ) && true !== call_user_func( $permission, is_array( $input ) ? $input : array() ) ) {
return new WP_Error( 'ability_invalid_permissions', 'Permission denied.' );
}

$callback = $this->args['execute_callback'] ?? null;
return is_callable( $callback ) ? call_user_func( $callback, is_array( $input ) ? $input : array() ) : null;
}
}
}

Expand All @@ -51,83 +57,139 @@ public function execute( $input = null ) {
$GLOBALS['__agents_api_smoke_ability_categories'] = array();
$GLOBALS['__agents_api_smoke_can'] = true;

function current_user_can( string $capability ): bool {
unset( $capability );
return (bool) $GLOBALS['__agents_api_smoke_can'];
if ( function_exists( 'current_user_can' ) ) {
add_filter(
'user_has_cap',
static function ( array $allcaps ): array {
$allcaps['manage_options'] = (bool) $GLOBALS['__agents_api_smoke_can'];
return $allcaps;
}
);
} else {
function current_user_can( string $capability ): bool {
unset( $capability );
return (bool) $GLOBALS['__agents_api_smoke_can'];
}
}

function wp_has_ability_category( string $slug ): bool {
return isset( $GLOBALS['__agents_api_smoke_ability_categories'][ $slug ] );
if ( ! function_exists( 'wp_has_ability_category' ) ) {
function wp_has_ability_category( string $slug ): bool {
return isset( $GLOBALS['__agents_api_smoke_ability_categories'][ $slug ] );
}
}

function wp_register_ability_category( string $slug, array $args ): ?WP_Ability_Category {
$GLOBALS['__agents_api_smoke_ability_categories'][ $slug ] = $args;
return null;
if ( ! function_exists( 'wp_register_ability_category' ) ) {
function wp_register_ability_category( string $slug, array $args ): ?WP_Ability_Category {
$GLOBALS['__agents_api_smoke_ability_categories'][ $slug ] = $args;
return null;
}
}

function wp_has_ability( string $name ): bool {
return isset( $GLOBALS['__agents_api_smoke_abilities'][ $name ] );
if ( ! function_exists( 'wp_has_ability' ) ) {
function wp_has_ability( string $name ): bool {
return isset( $GLOBALS['__agents_api_smoke_abilities'][ $name ] );
}
}

function wp_register_ability( string $name, array $args ): ?WP_Ability {
$ability = new WP_Ability( $name, $args );
$GLOBALS['__agents_api_smoke_abilities'][ $name ] = $ability;
return $ability;
if ( ! function_exists( 'wp_register_ability' ) ) {
function wp_register_ability( string $name, array $args ): ?WP_Ability {
$ability = new WP_Ability( $name, $args );
$GLOBALS['__agents_api_smoke_abilities'][ $name ] = $ability;
return $ability;
}
}

function wp_get_ability( string $name ): ?WP_Ability {
return $GLOBALS['__agents_api_smoke_abilities'][ $name ] ?? null;
if ( ! function_exists( 'wp_get_ability' ) ) {
function wp_get_ability( string $name ): ?WP_Ability {
return $GLOBALS['__agents_api_smoke_abilities'][ $name ] ?? null;
}
}

function wp_get_abilities(): array {
return array_values( $GLOBALS['__agents_api_smoke_abilities'] );
if ( ! function_exists( 'wp_get_abilities' ) ) {
function wp_get_abilities(): array {
return array_values( $GLOBALS['__agents_api_smoke_abilities'] );
}
}

agents_api_smoke_require_module();
do_action( 'wp_abilities_api_categories_init' );
do_action( 'wp_abilities_api_init' );
add_action(
'wp_abilities_api_categories_init',
static function (): void {
if ( ! wp_has_ability_category( 'demo-tools' ) ) {
wp_register_ability_category(
'demo-tools',
array(
'label' => 'Demo Tools',
'description' => 'Demo tool abilities for smoke coverage.',
)
);
}

wp_register_ability(
'demo/weather-forecast',
array(
'label' => 'Weather Forecast',
'description' => 'Fetch a local weather forecast for a city.',
'category' => 'demo-tools',
'input_schema' => array(
'type' => 'object',
'required' => array( 'city' ),
'properties' => array(
'city' => array( 'type' => 'string' ),
),
),
'execute_callback' => static function ( array $input ): array {
return array( 'forecast' => 'sunny in ' . ( $input['city'] ?? '' ) );
},
'permission_callback' => static function (): bool {
return true;
},
)
if ( ! wp_has_ability_category( 'content' ) ) {
wp_register_ability_category(
'content',
array(
'label' => 'Content',
'description' => 'Demo content abilities for smoke coverage.',
)
);
}
}
);

wp_register_ability(
'demo/publish-post',
array(
'label' => 'Publish Post',
'description' => 'Publish a draft post by ID.',
'category' => 'content',
'input_schema' => array(
'type' => 'object',
'required' => array( 'post_id' ),
),
'execute_callback' => static function ( array $input ): array {
return array( 'published' => (int) ( $input['post_id'] ?? 0 ) );
},
'permission_callback' => static function (): bool {
return true;
},
)
add_action(
'wp_abilities_api_init',
static function (): void {
if ( ! wp_has_ability( 'demo/weather-forecast' ) ) {
wp_register_ability(
'demo/weather-forecast',
array(
'label' => 'Weather Forecast',
'description' => 'Fetch a local weather forecast for a city.',
'category' => 'demo-tools',
'input_schema' => array(
'type' => 'object',
'required' => array( 'city' ),
'properties' => array(
'city' => array( 'type' => 'string' ),
),
),
'execute_callback' => static function ( array $input ): array {
return array( 'forecast' => 'sunny in ' . ( $input['city'] ?? '' ) );
},
'permission_callback' => static function (): bool {
return true;
},
)
);
}

if ( ! wp_has_ability( 'demo/publish-post' ) ) {
wp_register_ability(
'demo/publish-post',
array(
'label' => 'Publish Post',
'description' => 'Publish a draft post by ID.',
'category' => 'content',
'input_schema' => array(
'type' => 'object',
'required' => array( 'post_id' ),
),
'execute_callback' => static function ( array $input ): array {
return array( 'published' => (int) ( $input['post_id'] ?? 0 ) );
},
'permission_callback' => static function (): bool {
return true;
},
)
);
}
}
);

do_action( 'wp_abilities_api_categories_init' );
do_action( 'wp_abilities_api_init' );

echo "\n[1] Meta-abilities register in canonical namespace:\n";
agents_api_smoke_assert_equals( true, wp_has_ability( 'agents/ability-search' ), 'ability-search is registered', $failures, $passes );
agents_api_smoke_assert_equals( true, wp_has_ability( 'agents/ability-call' ), 'ability-call is registered', $failures, $passes );
Expand Down
Loading
Loading