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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Agents API sits between tool/action discovery and product-specific automation. I
- Multi-turn orchestration contracts.
- Opt-in mediated tool result truncation for oversized transcript payloads.
- Opt-in between-turn interrupt sources for cancel, redirect, or additional instruction messages.
- Canonical chat run-control contracts for run IDs, run status, best-effort cancellation, and queued messages.
- Canonical ability discovery and dispatch meta-abilities for large tool surfaces.
- Agent package and package-artifact contracts.
- Shared `wp_guideline` / `wp_guideline_type` storage substrate polyfill when Core/Gutenberg do not provide it.
Expand Down Expand Up @@ -177,6 +178,7 @@ wp_register_agent(
- `AgentsAPI\AI\WP_Agent_Tool_Result_Truncator`
- `AgentsAPI\AI\WP_Agent_Byte_Limit_Tool_Result_Truncator`
- `AgentsAPI\AI\WP_Agent_Conversation_Result`
- `AgentsAPI\AI\WP_Agent_Chat_Run_Control`
- `AgentsAPI\AI\WP_Agent_Conversation_Loop`
- `WP_Agent_Consent_Policy`
- `WP_Agent_Default_Consent_Policy`
Expand All @@ -203,6 +205,7 @@ wp_register_agent(
- `AgentsAPI\AI\Tools\WP_Agent_Tool_Execution_Core`
- `AgentsAPI\AI\Tools\WP_Agent_Tool_Result`
- `agents/ability-search` / `agents/ability-call`
- `agents/chat` / `agents/get-chat-run` / `agents/cancel-chat-run` / `agents/queue-chat-message`
- `AgentsAPI\AI\Approvals\WP_Agent_Approval_Decision`
- `AgentsAPI\AI\Approvals\WP_Agent_Pending_Action`
- `AgentsAPI\AI\Approvals\WP_Agent_Pending_Action_Status`
Expand Down
2 changes: 2 additions & 0 deletions agents-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-tool-result-truncator.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-byte-limit-tool-result-truncator.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-conversation-result.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-chat-run-control.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-conversation-loop.php';
require_once AGENTS_API_PATH . 'src/Tools/class-wp-agent-tool-call.php';
require_once AGENTS_API_PATH . 'src/Tools/class-wp-agent-tool-parameters.php';
Expand Down Expand Up @@ -170,6 +171,7 @@
require_once AGENTS_API_PATH . 'src/Channels/class-wp-agent-bridge.php';
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-agents-chat-run-control-abilities.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';
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"php tests/iteration-budget-smoke.php",
"php tests/conversation-loop-budgets-smoke.php",
"php tests/channels-smoke.php",
"php tests/chat-run-control-smoke.php",
"php tests/frontend-chat-rest-smoke.php",
"php tests/agents-dispatch-message-ability-smoke.php",
"php tests/webhook-safety-smoke.php",
Expand Down
29 changes: 28 additions & 1 deletion docs/channels-workflows-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,34 @@ array(
)
```

The channel can consume either a single `reply` or assistant `messages` from the ability result. It stores a returned `session_id` before sending replies so delivery failures do not lose session continuity.
The channel can consume either a single `reply` or assistant `messages` from the ability result. It stores a returned `session_id` before sending replies so delivery failures do not lose session continuity. `agents/chat` also exposes a canonical `run_id`; runtimes receive a generated `run_id` in the input when callers omit one, and responses include that same ID unless the runtime returns its own.

## Chat run control

Agents API owns the generic run-control ability contracts while runtimes own concrete storage, workers, provider aborts, and queue draining.

| Ability | Purpose | Runtime hook |
| --- | --- | --- |
| `agents/get-chat-run` | Return status for a known chat run. | `wp_agent_chat_run_status_handler` |
| `agents/cancel-chat-run` | Request best-effort cancellation for a known chat run. | `wp_agent_chat_run_cancel_handler` |
| `agents/queue-chat-message` | Accept a next user message while a session has an active run. | `wp_agent_chat_message_queue_handler` |

Run status vocabulary is bounded to `queued`, `running`, `cancelling`, `cancelled`, `completed`, and `failed`. The canonical run payload is:

```php
array(
'run_id' => 'run_opaque',
'session_id' => 'session_opaque',
'status' => 'running',
'started_at' => '2026-01-01T00:00:00Z',
'updated_at' => '2026-01-01T00:00:01Z',
'metadata' => array(),
)
```

Cancellation is best-effort. A runtime that can abort provider work immediately may do so; a runtime that cannot should mark the run `cancelling` and let its conversation loop stop at the next interrupt check. `WP_Agent_Chat_Run_Control::cancellation_interrupt_message()` builds the message shape expected by `WP_Agent_Conversation_Loop` `interrupt_source` callbacks.

Queued messages return the same run payload plus `queued_message_id` and `position`. Async runtimes can drain queued messages through their worker, cron, or Action Scheduler integration. Synchronous runtimes can expose queued state and require polling or an explicit continue operation in the consuming product; the substrate does not force a background runner.

## Session, webhook, and idempotency helpers

Expand Down
18 changes: 18 additions & 0 deletions src/Channels/register-agents-chat-ability.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@

namespace AgentsAPI\AI\Channels;

use AgentsAPI\AI\WP_Agent_Chat_Run_Control;

defined( 'ABSPATH' ) || exit;

/**
Expand Down Expand Up @@ -104,6 +106,10 @@ static function (): void {
* @return array|\WP_Error Canonical output, or WP_Error if no runtime is registered.
*/
function agents_chat_dispatch( array $input ) {
if ( ! isset( $input['run_id'] ) || '' === trim( (string) $input['run_id'] ) ) {
$input['run_id'] = WP_Agent_Chat_Run_Control::generate_run_id();
}

/**
* Filter the chat runtime handler.
*
Expand Down Expand Up @@ -156,6 +162,10 @@ function agents_chat_dispatch( array $input ) {
);
}

if ( ! isset( $result['run_id'] ) || '' === trim( (string) $result['run_id'] ) ) {
$result['run_id'] = $input['run_id'];
}

return $result;
}

Expand Down Expand Up @@ -207,6 +217,10 @@ function agents_chat_input_schema(): array {
'type' => array( 'string', 'null' ),
'description' => 'Existing session ID to continue, or null to start a new session.',
),
'run_id' => array(
'type' => array( 'string', 'null' ),
'description' => 'Optional client-supplied idempotency/run key. If omitted, the dispatcher provides an opaque run ID to the runtime and response.',
),
'session_owner' => agents_chat_session_owner_schema(),
'attachments' => array(
'type' => 'array',
Expand Down Expand Up @@ -323,6 +337,10 @@ function agents_chat_output_schema(): array {
'type' => 'string',
'description' => 'Primary assistant text. Must be set even when the runtime supplies multi-message output via `messages`.',
),
'run_id' => array(
'type' => 'string',
'description' => 'Opaque ID for this accepted chat turn. Use with agents/get-chat-run, agents/cancel-chat-run, and agents/queue-chat-message when the runtime supports run control.',
),
'messages' => array(
'type' => 'array',
'description' => 'Optional multi-message expansion (e.g. assistant emitted multiple turns or split a long answer). When present, each entry is `{ role, content }`. The single-string `reply` is still required for clients that don\'t parse `messages`.',
Expand Down
210 changes: 210 additions & 0 deletions src/Channels/register-agents-chat-run-control-abilities.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
<?php
/**
* Canonical chat run-control ability registration.
*
* @package AgentsAPI
*/

namespace AgentsAPI\AI\Channels;

use AgentsAPI\AI\WP_Agent_Chat_Run_Control;

defined( 'ABSPATH' ) || exit;

const AGENTS_GET_CHAT_RUN_ABILITY = 'agents/get-chat-run';
const AGENTS_CANCEL_CHAT_RUN_ABILITY = 'agents/cancel-chat-run';
const AGENTS_QUEUE_CHAT_MESSAGE_ABILITY = 'agents/queue-chat-message';

add_action(
'wp_abilities_api_init',
static function (): void {
$abilities = array(
AGENTS_GET_CHAT_RUN_ABILITY => array(
'label' => 'Get Chat Run',
'description' => 'Read the canonical status for an addressable chat run.',
'input_schema' => agents_chat_run_id_input_schema(),
'output_schema' => agents_chat_run_output_schema(),
'execute_callback' => __NAMESPACE__ . '\\agents_get_chat_run',
'annotations' => array( 'idempotent' => true ),
),
AGENTS_CANCEL_CHAT_RUN_ABILITY => array(
'label' => 'Cancel Chat Run',
'description' => 'Request best-effort cancellation for an addressable chat run.',
'input_schema' => agents_chat_run_id_input_schema(),
'output_schema' => agents_cancel_chat_run_output_schema(),
'execute_callback' => __NAMESPACE__ . '\\agents_cancel_chat_run',
'annotations' => array(
'destructive' => true,
'idempotent' => true,
),
),
AGENTS_QUEUE_CHAT_MESSAGE_ABILITY => array(
'label' => 'Queue Chat Message',
'description' => 'Queue a user message for a conversation while another chat run is active.',
'input_schema' => agents_queue_chat_message_input_schema(),
'output_schema' => agents_queue_chat_message_output_schema(),
'execute_callback' => __NAMESPACE__ . '\\agents_queue_chat_message',
'annotations' => array(
'destructive' => true,
'idempotent' => false,
),
),
);

foreach ( $abilities as $ability => $args ) {
if ( wp_has_ability( $ability ) ) {
continue;
}

wp_register_ability(
$ability,
array(
'label' => $args['label'],
'description' => $args['description'],
'category' => 'agents-api',
'input_schema' => $args['input_schema'],
'output_schema' => $args['output_schema'],
'execute_callback' => $args['execute_callback'],
'permission_callback' => __NAMESPACE__ . '\\agents_chat_run_control_permission',
'meta' => array(
'show_in_rest' => true,
'annotations' => $args['annotations'],
),
)
);
}
}
);

/** @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_no_handler( 'agents_chat_run_status_unsupported', 'No chat run status handler is registered.' );
}

return agents_chat_run_control_normalize_result( call_user_func( $handler, $input ), 'agents_chat_run_invalid_status' );
}

/** @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 ) ) {
return agents_chat_run_control_no_handler( 'agents_chat_run_cancel_unsupported', 'No chat run cancellation handler is registered.' );
}

$result = agents_chat_run_control_normalize_result( call_user_func( $handler, $input ), 'agents_chat_run_invalid_cancel_result' );
if ( is_wp_error( $result ) ) {
return $result;
}

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

return $result;
}

/** @return array<string,mixed>|\WP_Error */
function agents_queue_chat_message( array $input ) {
$handler = apply_filters( 'wp_agent_chat_message_queue_handler', null, $input );
if ( ! is_callable( $handler ) ) {
return agents_chat_run_control_no_handler( 'agents_chat_message_queue_unsupported', 'No chat message queue handler is registered.' );
}

$result = agents_chat_run_control_normalize_result( call_user_func( $handler, $input ), 'agents_chat_message_queue_invalid_result' );
if ( is_wp_error( $result ) ) {
return $result;
}

if ( empty( $result['queued_message_id'] ) ) {
return new \WP_Error( 'agents_chat_message_queue_invalid_result', 'Queued message results must include queued_message_id.' );
}

$result['queued_message_id'] = (string) $result['queued_message_id'];
$result['position'] = max( 0, (int) ( $result['position'] ?? 0 ) );

return $result;
}

function agents_chat_run_control_permission( array $input ): bool {
$allowed = function_exists( 'current_user_can' ) ? current_user_can( 'read' ) : false;
return (bool) apply_filters( 'agents_chat_run_control_permission', $allowed, $input );
}

/** @return array<string,mixed>|\WP_Error */
function agents_chat_run_control_normalize_result( $result, string $error_code ) {
if ( is_wp_error( $result ) ) {
return $result;
}

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

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

function agents_chat_run_control_no_handler( string $code, string $message ): \WP_Error {
return new \WP_Error( $code, $message );
}

function agents_chat_run_id_input_schema(): array {
return array(
'type' => 'object',
'required' => array( 'session_id', 'run_id' ),
'properties' => array(
'session_id' => array( 'type' => 'string' ),
'run_id' => array( 'type' => 'string' ),
'session_owner' => agents_chat_session_owner_schema(),
),
);
}

function agents_chat_run_output_schema(): array {
return array(
'type' => 'object',
'required' => array( 'run_id', 'session_id', 'status' ),
'properties' => array(
'run_id' => array( 'type' => 'string' ),
'session_id' => array( 'type' => 'string' ),
'status' => array(
'type' => 'string',
'enum' => WP_Agent_Chat_Run_Control::statuses(),
),
'started_at' => array( 'type' => 'string' ),
'updated_at' => array( 'type' => 'string' ),
'metadata' => array( 'type' => 'object' ),
),
);
}

function agents_cancel_chat_run_output_schema(): array {
$schema = agents_chat_run_output_schema();
$schema['required'][] = 'cancelled';
$schema['properties']['cancelled'] = array( 'type' => 'boolean' );
return $schema;
}

function agents_queue_chat_message_input_schema(): array {
$schema = agents_chat_input_schema();
$schema['required'][] = 'session_id';
return $schema;
}

function agents_queue_chat_message_output_schema(): array {
$schema = agents_chat_run_output_schema();
$schema['required'][] = 'queued_message_id';
$schema['properties']['queued_message_id'] = array( 'type' => 'string' );
$schema['properties']['position'] = array( 'type' => 'integer' );
return $schema;
}
Loading
Loading