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
4 changes: 4 additions & 0 deletions agents-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Description: WordPress-shaped agent runtime substrate.
* Version: 0.1.0
* Requires at least: 7.0
* Tested up to: 7.0
* Requires PHP: 8.1
* Author: Automattic
* License: GPL-2.0-or-later
Expand Down Expand Up @@ -136,17 +137,20 @@
require_once AGENTS_API_PATH . 'src/Channels/class-wp-agent-channel.php';
require_once AGENTS_API_PATH . 'src/Channels/register-agents-chat-ability.php';
require_once AGENTS_API_PATH . 'src/Channels/register-frontend-chat-rest-route.php';
require_once AGENTS_API_PATH . 'src/Channels/register-agents-dispatch-message-ability.php';
require_once AGENTS_API_PATH . 'src/Workflows/class-wp-agent-workflow-bindings.php';
require_once AGENTS_API_PATH . 'src/Workflows/class-wp-agent-workflow-spec-validator.php';
require_once AGENTS_API_PATH . 'src/Workflows/class-wp-agent-workflow-spec.php';
require_once AGENTS_API_PATH . 'src/Workflows/class-wp-agent-workflow-run-result.php';
require_once AGENTS_API_PATH . 'src/Workflows/class-wp-agent-workflow-store.php';
require_once AGENTS_API_PATH . 'src/Workflows/class-wp-agent-workflow-lifecycle.php';
require_once AGENTS_API_PATH . 'src/Workflows/class-wp-agent-workflow-run-recorder.php';
require_once AGENTS_API_PATH . 'src/Workflows/class-wp-agent-workflow-runner.php';
require_once AGENTS_API_PATH . 'src/Workflows/class-wp-agent-workflow-registry.php';
require_once AGENTS_API_PATH . 'src/Workflows/class-wp-agent-workflow-action-scheduler-bridge.php';
require_once AGENTS_API_PATH . 'src/Workflows/register-workflows.php';
require_once AGENTS_API_PATH . 'src/Workflows/register-agents-workflow-abilities.php';
require_once AGENTS_API_PATH . 'src/Workflows/register-workflow-bridge-sync.php';
require_once AGENTS_API_PATH . 'src/Workflows/register-action-scheduler-listener.php';
require_once AGENTS_API_PATH . 'src/Routines/class-wp-agent-routine.php';
require_once AGENTS_API_PATH . 'src/Routines/class-wp-agent-routine-registry.php';
Expand Down
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@
"php tests/conversation-loop-budgets-smoke.php",
"php tests/channels-smoke.php",
"php tests/frontend-chat-rest-smoke.php",
"php tests/agents-dispatch-message-ability-smoke.php",
"php tests/webhook-safety-smoke.php",
"php tests/remote-bridge-smoke.php",
"php tests/context-authority-smoke.php",
"php tests/guidelines-substrate-smoke.php",
"php tests/workflow-bindings-smoke.php",
"php tests/workflow-spec-validator-smoke.php",
"php tests/workflow-runner-smoke.php",
"php tests/workflow-lifecycle-smoke.php",
"php tests/agents-workflow-ability-smoke.php",
"php tests/routine-smoke.php",
"php tests/subagents-smoke.php",
Expand Down
14 changes: 13 additions & 1 deletion src/Channels/class-wp-agent-channel.php
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ public function build_external_message( string $message_text, array $data ): WP_
$this->get_external_id_provider(),
$this->get_external_id(),
$this->extract_external_message_id( $data ),
null,
$this->extract_sender_id( $data ),
false,
$this->get_room_kind( $data ),
$this->extract_attachments( $data ),
Expand Down Expand Up @@ -431,6 +431,18 @@ protected function extract_external_message_id( array $data ): ?string {
return null;
}

/**
* Opaque external sender id. In DMs this may equal the conversation id;
* in group chats it identifies the human sender inside the room.
*
* @param array $data
* @return string|null
*/
protected function extract_sender_id( array $data ): ?string {
unset( $data );
return null;
}

/**
* Conversation kind: `dm`, `group`, `channel`, or null when unknown.
* Override per transport — WhatsApp can derive from the JID suffix,
Expand Down
1 change: 1 addition & 0 deletions src/Channels/class-wp-agent-external-message.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public function client_context( string $source = 'channel' ): array {
'external_provider' => $this->external_provider,
'external_conversation_id' => $this->external_conversation_id,
'external_message_id' => $this->external_message_id,
'sender_id' => $this->sender_id,
'room_kind' => $this->room_kind,
);
}
Expand Down
4 changes: 4 additions & 0 deletions src/Channels/register-agents-chat-ability.php
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ function agents_chat_input_schema(): array {
'type' => array( 'string', 'null' ),
'description' => 'Stable transport-side message id, used for reply threading / dedup / audit.',
),
'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(
'type' => array( 'string', 'null' ),
'enum' => array( 'dm', 'group', 'channel' ),
Expand Down
214 changes: 214 additions & 0 deletions src/Channels/register-agents-dispatch-message-ability.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
<?php
/**
* Canonical outbound message ability registration.
*
* Registers `agents/dispatch-message` as the stable handoff from workflows,
* agents, routines, and product plugins to an outbound channel transport.
* Agents API owns the contract and dispatch hook; consumers own the actual
* delivery runtime by filtering `wp_agent_dispatch_message_handler`.
*
* @package AgentsAPI
* @since 0.107.0
*/

namespace AgentsAPI\AI\Channels;

defined( 'ABSPATH' ) || exit;

const AGENTS_DISPATCH_MESSAGE_ABILITY = 'agents/dispatch-message';

add_action(
'wp_abilities_api_categories_init',
static function (): void {
if ( wp_has_ability_category( 'agents-api' ) ) {
return;
}

wp_register_ability_category(
'agents-api',
array(
'label' => 'Agents API',
'description' => 'Cross-cutting abilities provided by the Agents API substrate (channel dispatch, canonical chat contract, and workflow dispatch).',
)
);
}
);

add_action(
'wp_abilities_api_init',
static function (): void {
if ( wp_has_ability( AGENTS_DISPATCH_MESSAGE_ABILITY ) ) {
return;
}

wp_register_ability(
AGENTS_DISPATCH_MESSAGE_ABILITY,
array(
'label' => 'Dispatch Message',
'description' => 'Canonical entry point for sending one outbound message through a channel transport. Dispatches to whichever runtime is registered via the wp_agent_dispatch_message_handler filter.',
'category' => 'agents-api',
'input_schema' => agents_dispatch_message_input_schema(),
'output_schema' => agents_dispatch_message_output_schema(),
'execute_callback' => __NAMESPACE__ . '\\agents_dispatch_message_dispatch',
'permission_callback' => __NAMESPACE__ . '\\agents_dispatch_message_permission',
'meta' => array(
'show_in_rest' => true,
'annotations' => array(
'destructive' => true,
'idempotent' => false,
),
),
)
);
}
);

/**
* Dispatch an outbound message to the registered runtime.
*
* @since 0.107.0
*
* @param array $input Canonical dispatch-message input.
* @return array|\WP_Error Canonical output, or WP_Error if no runtime is registered.
*/
function agents_dispatch_message_dispatch( array $input ) {
/**
* Filter the outbound message runtime handler.
*
* The first hook to return a callable wins. Handlers receive the canonical
* input map and must return either the canonical output shape or WP_Error.
*
* @since 0.107.0
*
* @param callable|null $handler Currently registered handler.
* @param array $input Canonical dispatch-message input.
*/
$handler = apply_filters( 'wp_agent_dispatch_message_handler', null, $input );

if ( ! is_callable( $handler ) ) {
do_action( 'agents_dispatch_message_failed', 'no_handler', $input );

return new \WP_Error(
'agents_dispatch_message_no_handler',
'No agents/dispatch-message handler is registered. Install a channel runtime, or add a callable to the wp_agent_dispatch_message_handler filter.'
);
}

$result = call_user_func( $handler, $input );

if ( is_wp_error( $result ) ) {
do_action( 'agents_dispatch_message_failed', $result->get_error_code(), $input );
return $result;
}

if ( ! is_array( $result ) ) {
do_action( 'agents_dispatch_message_failed', 'invalid_result', $input );
return new \WP_Error(
'agents_dispatch_message_invalid_result',
'agents/dispatch-message handler returned an unexpected result type. Handlers must return an array matching the canonical output shape or a WP_Error.'
);
}

return $result;
}

/**
* Permission gate for `agents/dispatch-message`.
*
* @since 0.107.0
*
* @param array $input Canonical dispatch-message input.
*/
function agents_dispatch_message_permission( array $input ): bool {
return (bool) apply_filters(
'agents_dispatch_message_permission',
current_user_can( 'manage_options' ),
$input
);
}

/**
* Canonical input schema.
*
* @since 0.107.0
*/
function agents_dispatch_message_input_schema(): array {
return array(
'type' => 'object',
'required' => array( 'channel', 'recipient', 'message' ),
'properties' => array(
'channel' => array(
'type' => 'string',
'description' => 'Outbound channel identifier, e.g. whatsapp, wacli, slack, sms, or a product-defined channel id.',
),
'recipient' => array(
'type' => 'string',
'description' => 'Transport-specific destination id, e.g. phone number, WhatsApp JID, channel id, or email address.',
),
'message' => array(
'type' => 'string',
'description' => 'Text body to send.',
),
'conversation_id' => array(
'type' => array( 'string', 'null' ),
'description' => 'Optional transport conversation/thread id.',
),
'attachments' => array(
'type' => 'array',
'default' => array(),
'items' => array( 'type' => 'object' ),
'description' => 'Optional transport-defined attachments.',
),
'client_context' => array(
'type' => 'object',
'description' => 'Optional caller/runtime context.',
),
'metadata' => array(
'type' => 'object',
'description' => 'Opaque caller metadata for transport runtimes and audit logs.',
),
),
);
}

/**
* Canonical output schema.
*
* @since 0.107.0
*/
function agents_dispatch_message_output_schema(): array {
return array(
'type' => 'object',
'required' => array( 'sent', 'channel', 'recipient' ),
'properties' => array(
'sent' => array( 'type' => 'boolean' ),
'channel' => array( 'type' => 'string' ),
'recipient' => array( 'type' => 'string' ),
'message_id' => array( 'type' => array( 'string', 'null' ) ),
'metadata' => array( 'type' => 'object' ),
),
);
}

/**
* Convenience helper for consumers.
*
* @since 0.107.0
*
* @param callable $handler Receives canonical input, returns canonical output or WP_Error.
* @param int $priority Filter priority.
*/
function register_dispatch_message_handler( callable $handler, int $priority = 10 ): void {
add_filter(
'wp_agent_dispatch_message_handler',
static function ( $existing, array $input ) use ( $handler ) {
unset( $input );
if ( null !== $existing ) {
return $existing;
}
return $handler;
},
$priority,
2
);
}
5 changes: 3 additions & 2 deletions src/Tools/class-wp-agent-tool-execution-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public function prepareWP_Agent_Tool_Call( string $tool_name, array $tool_parame
);
}

$validation = WP_Agent_Tool_Parameters::validateRequiredParameters( $tool_parameters, $tool_definition );
$parameters = WP_Agent_Tool_Parameters::buildParameters( $tool_parameters, $context, $tool_definition );
$validation = WP_Agent_Tool_Parameters::validateRequiredParameters( $parameters, $tool_definition );
if ( ! $validation['valid'] ) {
return array_merge(
array( 'ready' => false ),
Expand All @@ -51,7 +52,7 @@ public function prepareWP_Agent_Tool_Call( string $tool_name, array $tool_parame
'tool_call' => WP_Agent_Tool_Call::normalize(
array(
'tool_name' => $tool_name,
'parameters' => WP_Agent_Tool_Parameters::buildParameters( $tool_parameters, $context, $tool_definition ),
'parameters' => $parameters,
'metadata' => array(
'source' => $tool_definition['source'] ?? WP_Agent_Tool_Declaration::sourceFromName( $tool_name ),
),
Expand Down
Loading
Loading