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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,23 @@ $visible_tools = ( new WP_Agent_Tool_Policy() )->resolve(
);
```

Consumers that gather tools from multiple product-owned sources can use `WP_Agent_Tool_Source_Registry` before the policy pass. Sources are named callbacks that receive `(array $context, WP_Agent_Tool_Source_Registry $registry)` and return declarations keyed by tool name. Lower source priorities run earlier, `agents_api_tool_source_order` can reorder sources for runtime context such as modes, and earlier sources win duplicate tool names.

```php
$registry = new WP_Agent_Tool_Source_Registry();
$registry->registerSource( 'runtime', $runtime_source, 10 );
$registry->registerSource( 'static', $static_source, 20 );

$all_tools = $registry->gather(
array(
'agent_id' => 'writer',
'modes' => array( 'chat' ),
)
);
```

The registry exposes three composition hooks: `agents_api_tool_sources` for injecting or replacing named sources, `agents_api_tool_source_order` for source precedence, and `agents_api_tool_source_tools` for context-aware adjustment of one source's gathered declarations.

Action policy is resolved by `WP_Agent_Action_Policy_Resolver` and always returns one of the canonical values from `AgentsAPI\AI\Tools\WP_Agent_Action_Policy`: `direct`, `preview`, or `forbidden`.

Resolution order is:
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"php tests/action-policy-values-smoke.php",
"php tests/consent-policy-smoke.php",
"php tests/tool-policy-contracts-smoke.php",
"php tests/tool-source-registry-smoke.php",
"php tests/tool-tier-resolver-smoke.php",
"php tests/action-policy-resolver-smoke.php",
"php tests/ability-meta-abilities-smoke.php",
Expand Down
94 changes: 81 additions & 13 deletions src/Tools/class-wp-agent-tool-source-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@
class WP_Agent_Tool_Source_Registry {

/**
* @var array<string, callable>
* @var array<string, array{callback: callable, priority: int, index: int}>
*/
private array $sources = array();

/**
* Monotonic registration index used to keep same-priority ordering stable.
*
* @var int
*/
private int $registration_index = 0;

/**
* Register a source callback.
*
Expand All @@ -27,14 +34,19 @@ class WP_Agent_Tool_Source_Registry {
*
* @param string $source_slug Source slug.
* @param callable $source Source callback.
* @param int $priority Source priority. Lower numbers run earlier.
* @return void
*/
public function registerSource( string $source_slug, callable $source ): void {
public function registerSource( string $source_slug, callable $source, int $priority = 10 ): void {
if ( '' === $source_slug ) {
throw new \InvalidArgumentException( 'invalid_tool_source: source_slug must be a non-empty string' );
}

$this->sources[ $source_slug ] = $source;
$this->sources[ $source_slug ] = array(
'callback' => $source,
'priority' => $priority,
'index' => $this->registration_index++,
);
}

/**
Expand All @@ -54,7 +66,7 @@ public function unregisterSource( string $source_slug ): void {
* @return array<string, callable>
*/
public function getSources( array $context = array() ): array {
$sources = $this->sources;
$sources = $this->getRegisteredSources();
if ( function_exists( 'apply_filters' ) ) {
$sources = apply_filters( 'agents_api_tool_sources', $sources, $context, $this );
}
Expand All @@ -73,22 +85,24 @@ public function getSources( array $context = array() ): array {
public function gather( array $context = array() ): array {
$tools = array();
$sources = $this->getSources( $context );
$order = array_keys( $sources );

if ( function_exists( 'apply_filters' ) ) {
$order = apply_filters( 'agents_api_tool_source_order', $order, $context, $this );
}

if ( ! is_array( $order ) ) {
return array();
}
$order = $this->getSourceOrder( $sources, $context );

foreach ( $order as $source_slug ) {
if ( ! is_string( $source_slug ) || ! isset( $sources[ $source_slug ] ) ) {
continue;
}

$source_tools = call_user_func( $sources[ $source_slug ], $context, $this );
if ( function_exists( 'apply_filters' ) ) {
$source_tools = apply_filters(
'agents_api_tool_source_tools',
$source_tools,
$source_slug,
$context,
$this
);
}

if ( ! is_array( $source_tools ) ) {
continue;
}
Expand All @@ -105,6 +119,60 @@ public function gather( array $context = array() ): array {
return $tools;
}

/**
* Return directly registered sources sorted by priority and registration order.
*
* @return array<string, callable>
*/
private function getRegisteredSources(): array {
$sources = $this->sources;
uasort(
$sources,
static function ( array $a, array $b ): int {
return ( $a['priority'] <=> $b['priority'] ) ?: ( $a['index'] <=> $b['index'] );
}
);

$callbacks = array();
foreach ( $sources as $source_slug => $source ) {
$callbacks[ $source_slug ] = $source['callback'];
}

return $callbacks;
}

/**
* Return source slugs in final precedence order.
*
* @param array<string, callable> $sources Registered sources.
* @param array $context Runtime context.
* @return array<int, string>
*/
private function getSourceOrder( array $sources, array $context ): array {
$order = array_keys( $sources );

if ( function_exists( 'apply_filters' ) ) {
$order = apply_filters(
'agents_api_tool_source_order',
$order,
$context,
$this,
$sources
);
}

if ( ! is_array( $order ) ) {
return array();
}

return array_values(
array_filter(
$order,
static fn( $source_slug ): bool => is_string( $source_slug ) && isset( $sources[ $source_slug ] )
)
);
}

/**
* Normalize source metadata on a gathered declaration.
*
Expand Down
Loading
Loading