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
19 changes: 10 additions & 9 deletions includes/class-openclawp-agenttic-bridge.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
* JSON-RPC bridge for `@automattic/agenttic-client`.
*
* Translates the agenttic protocol (JSON-RPC 2.0 over HTTP, A2A-shaped Task
* envelopes) into canonical `agents/chat` ability calls. The motivation is
* envelopes) into local `openclawp/chat` ability calls. The motivation is
* code reuse: the agenttic React hooks (`useAgentChat`, `useAgentSession`,
* etc.) are the right UI layer for openclaWP, and they speak this wire
* format — so we expose the canonical chat dispatcher under that wire
* format rather than reimplementing the React side against the openclaWP
* REST shape.
* format — so we expose the local openclaWP chat runtime under that wire
* format rather than reimplementing the React side against another REST
* shape.
*
* Wire shape, abridged (full schema in @automattic/agenttic-client v0.1.x):
*
Expand Down Expand Up @@ -54,7 +54,7 @@
* - `message/stream` (real SSE; one frame per loop event plus a final
* Task envelope). We subscribe to canonical's `agents_api_loop_event`
* action and openclaWP's `openclawp_chat_turn_completed` telemetry
* event for the duration of the synchronous `agents/chat` call, write
* event for the duration of the synchronous `openclawp/chat` call, write
* each one as a `data: {…}\n\n` SSE frame, and close with the
* completed Task envelope. Progress frames carry `result.kind =
* "status-update"` so clients that only want the final result can
Expand Down Expand Up @@ -170,8 +170,9 @@ public static function handle( WP_REST_Request $request ): WP_REST_Response {
$task_id = isset( $params['id'] ) && is_string( $params['id'] ) ? $params['id'] : self::generate_task_id();

// Agenttic sends dynamic client context as a data part on the message.
// Preserve it as canonical agents/chat client_context so runtimes can
// expose it to model prompts and tool policies.
// Preserve it as openclaWP runtime client_context so product-owned keys
// can reach model prompts and tool policies without being filtered by
// the generic agents/chat transport schema.
$client_context = self::client_context_from_message( $message );

// When the request carries agents-api caller-chain headers, this is an
Expand All @@ -187,9 +188,9 @@ public static function handle( WP_REST_Request $request ): WP_REST_Response {
if ( ! function_exists( 'wp_get_ability' ) ) {
return self::error_response( $rpc_id, self::INTERNAL_ERROR, 'Abilities API is not loaded.' );
}
$chat = wp_get_ability( 'agents/chat' );
$chat = wp_get_ability( 'openclawp/chat' );
if ( null === $chat ) {
return self::error_response( $rpc_id, self::INTERNAL_ERROR, 'agents/chat ability is not registered.' );
return self::error_response( $rpc_id, self::INTERNAL_ERROR, 'openclawp/chat ability is not registered.' );
}

// Streaming opens the SSE response BEFORE invoking the chat ability
Expand Down
231 changes: 171 additions & 60 deletions tests/unit/AgentticClientContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,80 +7,191 @@

declare( strict_types=1 );

namespace OpenclaWP\Tests\Unit;
namespace {
if ( ! class_exists( 'WP_REST_Request' ) ) {
class WP_REST_Request {
/** @param array<string,mixed> $params */
public function __construct(
private array $params = array(),
private array $json = array(),
private array $headers = array()
) {}
public function get_param( string $name ) {
return $this->params[ $name ] ?? null;
}
public function get_json_params(): array {
return $this->json;
}
public function get_header( string $name ) {
return $this->headers[ strtolower( $name ) ] ?? null;
}
}
}

if ( ! class_exists( 'WP_REST_Response' ) ) {
class WP_REST_Response {
public function __construct( private $data = null, private int $status = 200 ) {}
public function get_data() {
return $this->data;
}
public function get_status(): int {
return $this->status;
}
}
}

use OpenclaWP_Agenttic_Bridge;
use OpenclaWP_Runner;
use PHPUnit\Framework\TestCase;
if ( ! function_exists( 'wp_get_ability' ) ) {
function wp_get_ability( string $name ) {
return $GLOBALS['openclawp_test_abilities_registry'][ $name ] ?? null;
}
}
}

namespace OpenclaWP\Tests\Unit {

use OpenclaWP_Agenttic_Bridge;
use OpenclaWP_Runner;
use PHPUnit\Framework\TestCase;
use WP_REST_Request;

/**
* @covers OpenclaWP_Agenttic_Bridge
* @covers OpenclaWP_Runner
*/
final class AgentticClientContextTest extends TestCase {
final class AgentticClientContextTest extends TestCase {

public function test_bridge_extracts_agenttic_client_context_data_part(): void {
$message = array(
'role' => 'user',
'parts' => array(
protected function tearDown(): void {
unset( $GLOBALS['openclawp_test_abilities_registry'] );
parent::tearDown();
}

public function test_bridge_extracts_agenttic_client_context_data_part(): void {
$message = array(
'role' => 'user',
'parts' => array(
array(
'type' => 'text',
'text' => 'Sí, borrar',
),
array(
'type' => 'data',
'data' => array(
'clientContext' => array(
'carpeta_pending_confirmation' => array(
'type' => 'delete-potreros',
'ids' => array( 1312, 1313 ),
),
),
),
),
),
);

$context = OpenclaWP_Agenttic_Bridge::client_context_from_message( $message );

$this->assertSame( 'agenttic', $context['source'] );
$this->assertSame( 'agenttic-client', $context['client_name'] );
$this->assertSame(
array(
'type' => 'text',
'text' => 'Sí, borrar',
'type' => 'delete-potreros',
'ids' => array( 1312, 1313 ),
),
$context['carpeta_pending_confirmation']
);
}

public function test_runner_injects_client_context_into_model_messages_only(): void {
$messages = array(
array(
'type' => 'data',
'data' => array(
'clientContext' => array(
'carpeta_pending_confirmation' => array(
'type' => 'delete-potreros',
'ids' => array( 1312, 1313 ),
),
'role' => 'assistant',
'content' => '¿Borro estos 2 potreros?',
),
array(
'role' => 'user',
'content' => 'Sí, borrar',
),
);

$for_model = OpenclaWP_Runner::messages_with_client_context(
$messages,
array(
'client_context' => array(
'carpeta_pending_confirmation' => array(
'type' => 'delete-potreros',
'ids' => array( 1312, 1313 ),
),
),
),
),
);

$context = OpenclaWP_Agenttic_Bridge::client_context_from_message( $message );

$this->assertSame( 'agenttic', $context['source'] );
$this->assertSame( 'agenttic-client', $context['client_name'] );
$this->assertSame(
array(
'type' => 'delete-potreros',
'ids' => array( 1312, 1313 ),
),
$context['carpeta_pending_confirmation']
);
}
)
);

$this->assertSame( 'Sí, borrar', $messages[1]['content'] );
$this->assertStringContainsString( 'Sí, borrar', $for_model[1]['content'] );
$this->assertStringContainsString( '[Client context for this turn]', $for_model[1]['content'] );
$this->assertStringContainsString( '"carpeta_pending_confirmation":{"type":"delete-potreros","ids":[1312,1313]}', $for_model[1]['content'] );
}

public function test_runner_injects_client_context_into_model_messages_only(): void {
$messages = array(
array(
'role' => 'assistant',
'content' => '¿Borro estos 2 potreros?',
),
array(
'role' => 'user',
'content' => 'Sí, borrar',
),
);

$for_model = OpenclaWP_Runner::messages_with_client_context(
$messages,
array(
'client_context' => array(
'carpeta_pending_confirmation' => array(
'type' => 'delete-potreros',
'ids' => array( 1312, 1313 ),
public function test_agenttic_bridge_invokes_openclawp_chat_with_product_client_context(): void {
$captured = null;
$GLOBALS['openclawp_test_abilities_registry'] = array(
'openclawp/chat' => new class( $captured ) {
public function __construct( private &$captured ) {}
public function execute( array $args ): array {
$this->captured = $args;
return array(
'session_id' => 'session-1',
'reply' => 'ok',
'completed' => true,
);
}
},
);

$request = new WP_REST_Request(
array( 'agent' => 'carpeta-bot' ),
array(
'jsonrpc' => '2.0',
'id' => 'req-1',
'method' => 'message/send',
'params' => array(
'id' => 'task-1',
'message' => array(
'role' => 'user',
'parts' => array(
array(
'type' => 'text',
'text' => 'Sí, borrar',
),
array(
'type' => 'data',
'data' => array(
'clientContext' => array(
'carpeta_pending_confirmation' => array(
'type' => 'delete-potreros',
'ids' => array( 1312, 1313 ),
),
),
),
),
),
),
),
),
)
);
)
);

$response = OpenclaWP_Agenttic_Bridge::handle( $request );

$this->assertSame( 'Sí, borrar', $messages[1]['content'] );
$this->assertStringContainsString( 'Sí, borrar', $for_model[1]['content'] );
$this->assertStringContainsString( '[Client context for this turn]', $for_model[1]['content'] );
$this->assertStringContainsString( '"carpeta_pending_confirmation":{"type":"delete-potreros","ids":[1312,1313]}', $for_model[1]['content'] );
$this->assertSame( 200, $response->get_status() );
$this->assertSame( 'ok', $response->get_data()['result']['status']['message']['parts'][0]['text'] );
$this->assertSame( 'carpeta-bot', $captured['agent'] );
$this->assertSame( 'Sí, borrar', $captured['message'] );
$this->assertSame(
array(
'type' => 'delete-potreros',
'ids' => array( 1312, 1313 ),
),
$captured['client_context']['carpeta_pending_confirmation']
);
$this->assertArrayNotHasKey( 'agents/chat', $GLOBALS['openclawp_test_abilities_registry'] );
}
}
}
Loading