From 35147e4b540ad402b2db22bfa0927a9ff21036d0 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Wed, 13 May 2026 21:17:15 -0400 Subject: [PATCH 1/2] feat: expose persisted agents to Agents API --- inc/Core/Auth/AgentAccessStoreAdapter.php | 131 ++++++++++++++- inc/Engine/Agents/PersistedAgentProjector.php | 152 ++++++++++++++++++ .../Agents/datamachine-register-agents.php | 11 ++ .../agents-api-access-store-adapter-smoke.php | 66 +++++++- ...nts-api-persisted-agent-registry-smoke.php | 120 ++++++++++++++ 5 files changed, 466 insertions(+), 14 deletions(-) create mode 100644 inc/Engine/Agents/PersistedAgentProjector.php create mode 100644 tests/agents-api-persisted-agent-registry-smoke.php diff --git a/inc/Core/Auth/AgentAccessStoreAdapter.php b/inc/Core/Auth/AgentAccessStoreAdapter.php index 2ae71d896..f21baa381 100644 --- a/inc/Core/Auth/AgentAccessStoreAdapter.php +++ b/inc/Core/Auth/AgentAccessStoreAdapter.php @@ -9,6 +9,7 @@ namespace DataMachine\Core\Auth; use DataMachine\Core\Database\Agents\AgentAccess; +use DataMachine\Core\Database\Agents\Agents; defined( 'ABSPATH' ) || exit; @@ -24,11 +25,20 @@ class AgentAccessStoreAdapter implements \WP_Agent_Access_Store { */ private AgentAccess $access_repository; + /** + * Agent identity repository used to map Data Machine IDs to Agents API slugs. + * + * @var Agents + */ + private Agents $agents_repository; + /** * @param AgentAccess|null $access_repository Optional repository for tests. + * @param Agents|null $agents_repository Optional repository for tests. */ - public function __construct( ?AgentAccess $access_repository = null ) { + public function __construct( ?AgentAccess $access_repository = null, ?Agents $agents_repository = null ) { $this->access_repository = $access_repository ?? new AgentAccess(); + $this->agents_repository = $agents_repository ?? new Agents(); } /** @@ -62,21 +72,23 @@ public static function filter_access_store( $store ) { * Create or update an access grant. */ public function grant_access( \WP_Agent_Access_Grant $grant ): \WP_Agent_Access_Grant { - return $this->access_repository->grant_access( $grant ); + $resolved = $this->access_repository->grant_access( $this->grant_for_storage( $grant ) ); + return $this->grant_for_contract( $resolved, $grant->agent_id ); } /** * Revoke a user's access grant for an agent. */ public function revoke_access( string $agent_id, int $user_id, ?string $workspace_id = null ): bool { - return $this->access_repository->revoke_access( $agent_id, $user_id, $workspace_id ); + return $this->access_repository->revoke_access( $this->storage_agent_id( $agent_id ), $user_id, $workspace_id ); } /** * Fetch a user's access grant for an agent. */ public function get_access( string $agent_id, int $user_id, ?string $workspace_id = null ): ?\WP_Agent_Access_Grant { - return $this->access_repository->get_access( $agent_id, $user_id, $workspace_id ); + $grant = $this->access_repository->get_access( $this->storage_agent_id( $agent_id ), $user_id, $workspace_id ); + return $grant ? $this->grant_for_contract( $grant, $agent_id ) : null; } /** @@ -85,7 +97,30 @@ public function get_access( string $agent_id, int $user_id, ?string $workspace_i * @return string[] */ public function get_agent_ids_for_user( int $user_id, ?string $minimum_role = null, ?string $workspace_id = null ): array { - return array_map( 'strval', $this->access_repository->get_agent_ids_for_user( $user_id, $minimum_role, $workspace_id ) ); + $agent_ids = $this->access_repository->get_agent_ids_for_user( $user_id, $minimum_role, $workspace_id ); + if ( empty( $agent_ids ) ) { + return array(); + } + + $rows = $this->agents_repository->get_agents_by_ids( array_map( 'intval', $agent_ids ) ); + $slugs_by_id = array(); + foreach ( $rows as $row ) { + $agent_id = (int) ( $row['agent_id'] ?? 0 ); + $slug = sanitize_title( (string) ( $row['agent_slug'] ?? '' ) ); + if ( $agent_id > 0 && '' !== $slug ) { + $slugs_by_id[ $agent_id ] = $slug; + } + } + + $slugs = array(); + foreach ( $agent_ids as $agent_id ) { + $agent_id = (int) $agent_id; + if ( isset( $slugs_by_id[ $agent_id ] ) ) { + $slugs[] = $slugs_by_id[ $agent_id ]; + } + } + + return $slugs; } /** @@ -94,6 +129,90 @@ public function get_agent_ids_for_user( int $user_id, ?string $minimum_role = nu * @return \WP_Agent_Access_Grant[] */ public function get_users_for_agent( string $agent_id, ?string $workspace_id = null ): array { - return $this->access_repository->get_users_for_agent( $agent_id, $workspace_id ); + return array_map( + fn( \WP_Agent_Access_Grant $grant ): \WP_Agent_Access_Grant => $this->grant_for_contract( $grant, $agent_id ), + $this->access_repository->get_users_for_agent( $this->storage_agent_id( $agent_id ), $workspace_id ) + ); + } + + /** + * Convert an Agents API slug to the Data Machine numeric storage ID. + */ + private function storage_agent_id( string $agent_id ): string { + if ( is_numeric( $agent_id ) ) { + return $agent_id; + } + + $row = $this->agents_repository->get_by_slug( $agent_id ); + if ( $row && ! empty( $row['agent_id'] ) ) { + return (string) (int) $row['agent_id']; + } + + return $agent_id; + } + + /** + * Convert a contract grant into Data Machine's numeric storage shape. + */ + private function grant_for_storage( \WP_Agent_Access_Grant $grant ): \WP_Agent_Access_Grant { + $storage_agent_id = $this->storage_agent_id( $grant->agent_id ); + if ( $storage_agent_id === $grant->agent_id ) { + return $grant; + } + + return new \WP_Agent_Access_Grant( + $storage_agent_id, + $grant->user_id, + $grant->role, + $grant->workspace_id, + $grant->grant_id, + $grant->granted_by_user_id, + $grant->granted_at, + $grant->metadata + ); + } + + /** + * Convert a Data Machine numeric grant into the Agents API slug shape. + */ + private function grant_for_contract( \WP_Agent_Access_Grant $grant, string $requested_agent_id = '' ): \WP_Agent_Access_Grant { + $agent_slug = $this->contract_agent_id( $grant->agent_id ); + if ( ! is_numeric( $requested_agent_id ) ) { + $requested_slug = sanitize_title( $requested_agent_id ); + if ( '' !== $requested_slug ) { + $agent_slug = $requested_slug; + } + } + + if ( $agent_slug === $grant->agent_id ) { + return $grant; + } + + return new \WP_Agent_Access_Grant( + $agent_slug, + $grant->user_id, + $grant->role, + $grant->workspace_id, + $grant->grant_id, + $grant->granted_by_user_id, + $grant->granted_at, + $grant->metadata + ); + } + + /** + * Convert a Data Machine numeric storage ID to an Agents API slug. + */ + private function contract_agent_id( string $agent_id ): string { + if ( ! is_numeric( $agent_id ) ) { + return sanitize_title( $agent_id ); + } + + $row = $this->agents_repository->get_agent( (int) $agent_id ); + if ( $row && ! empty( $row['agent_slug'] ) ) { + return sanitize_title( (string) $row['agent_slug'] ); + } + + return $agent_id; } } diff --git a/inc/Engine/Agents/PersistedAgentProjector.php b/inc/Engine/Agents/PersistedAgentProjector.php new file mode 100644 index 000000000..ee22c85d6 --- /dev/null +++ b/inc/Engine/Agents/PersistedAgentProjector.php @@ -0,0 +1,152 @@ +get_all() as $row ) { + $slug = sanitize_title( (string) ( $row['agent_slug'] ?? '' ) ); + if ( '' === $slug || wp_has_agent( $slug ) ) { + continue; + } + + $agent = wp_register_agent( $slug, self::definition_from_row( $row ) ); + if ( $agent instanceof \WP_Agent ) { + $registered[] = $agent->get_slug(); + } + } + + return $registered; + } + + /** + * Build WP_Agent registration args from a persisted agent row. + * + * @param array $row Data Machine agent row. + * @return array + */ + public static function definition_from_row( array $row ): array { + $config = is_array( $row['agent_config'] ?? null ) ? $row['agent_config'] : array(); + $owner_id = (int) ( $row['owner_id'] ?? 0 ); + + return array( + 'label' => self::label_from_row( $row ), + 'description' => self::description_from_row( $row, $config ), + 'owner_resolver' => static fn(): int => $owner_id, + 'default_config' => $config, + 'meta' => self::meta_from_row( $row, $config ), + ); + } + + /** + * Resolve display label from durable row fields. + * + * @param array $row Data Machine agent row. + */ + private static function label_from_row( array $row ): string { + $label = trim( (string) ( $row['agent_name'] ?? '' ) ); + if ( '' !== $label ) { + return $label; + } + + return sanitize_title( (string) ( $row['agent_slug'] ?? 'agent' ) ); + } + + /** + * Resolve description from known durable config shapes. + * + * @param array $row Data Machine agent row. + * @param array $config Agent config. + */ + private static function description_from_row( array $row, array $config ): string { + foreach ( array( $config, $config['intelligence_wiki_brain'] ?? null ) as $source ) { + if ( ! is_array( $source ) ) { + continue; + } + + foreach ( array( 'description', 'agent_description', 'summary' ) as $key ) { + $description = trim( (string) ( $source[ $key ] ?? '' ) ); + if ( '' !== $description ) { + return $description; + } + } + } + + $label = self::label_from_row( $row ); + return '' !== $label ? sprintf( 'Data Machine agent: %s.', $label ) : __( 'Data Machine persisted agent.', 'data-machine' ); + } + + /** + * Build diagnostics/provenance metadata for the runtime definition. + * + * @param array $row Data Machine agent row. + * @param array $config Agent config. + * @return array + */ + private static function meta_from_row( array $row, array $config ): array { + $bundle = is_array( $config['datamachine_bundle'] ?? null ) ? $config['datamachine_bundle'] : array(); + $brain = is_array( $config['intelligence_wiki_brain'] ?? null ) ? $config['intelligence_wiki_brain'] : array(); + + $meta = array( + 'source_plugin' => 'data-machine', + 'source_type' => 'persisted-agent', + 'datamachine_agent_id' => (int) ( $row['agent_id'] ?? 0 ), + 'datamachine_owner_id' => (int) ( $row['owner_id'] ?? 0 ), + ); + + foreach ( array( 'bundle_slug', 'bundle_version', 'source_ref', 'source_revision' ) as $key ) { + $value = trim( (string) ( $bundle[ $key ] ?? '' ) ); + if ( '' !== $value ) { + $meta[ 'datamachine_' . $key ] = $value; + } + } + + if ( ! empty( $bundle['bundle_slug'] ) ) { + $meta['source_package'] = (string) $bundle['bundle_slug']; + } + + foreach ( array( 'domain', 'wiki_slug', 'source_slug', 'brain_slug', 'bundle_slug' ) as $key ) { + $value = trim( (string) ( $brain[ $key ] ?? '' ) ); + if ( '' !== $value ) { + $meta[ 'intelligence_wiki_brain_' . $key ] = $value; + } + } + + return $meta; + } +} diff --git a/inc/Engine/Agents/datamachine-register-agents.php b/inc/Engine/Agents/datamachine-register-agents.php index 209a9ba37..8c628b534 100644 --- a/inc/Engine/Agents/datamachine-register-agents.php +++ b/inc/Engine/Agents/datamachine-register-agents.php @@ -8,6 +8,7 @@ use DataMachine\Core\FilesRepository\DirectoryManager; use DataMachine\Engine\Agents\AgentRegistry; +use DataMachine\Engine\Agents\PersistedAgentProjector; defined( 'ABSPATH' ) || exit; @@ -143,3 +144,13 @@ function datamachine_register_default_admin_agent(): void { ); } add_action( 'wp_agents_api_init', 'datamachine_register_default_admin_agent', 10 ); + +/** + * Project durable Data Machine agents into the Agents API runtime registry. + * + * @since 0.110.3 + */ +function datamachine_register_persisted_agents(): void { + PersistedAgentProjector::register_persisted_agents(); +} +add_action( 'wp_agents_api_init', 'datamachine_register_persisted_agents', 20 ); diff --git a/tests/agents-api-access-store-adapter-smoke.php b/tests/agents-api-access-store-adapter-smoke.php index ddf4cb34f..7604bda00 100644 --- a/tests/agents-api-access-store-adapter-smoke.php +++ b/tests/agents-api-access-store-adapter-smoke.php @@ -12,10 +12,12 @@ require_once dirname( __DIR__ ) . '/inc/Core/Database/BaseRepository.php'; require_once dirname( __DIR__ ) . '/inc/Core/Database/Agents/AgentAccess.php'; +require_once dirname( __DIR__ ) . '/inc/Core/Database/Agents/Agents.php'; require_once dirname( __DIR__ ) . '/inc/Core/Auth/AgentAccessStoreAdapter.php'; use DataMachine\Core\Auth\AgentAccessStoreAdapter; use DataMachine\Core\Database\Agents\AgentAccess; +use DataMachine\Core\Database\Agents\Agents; $GLOBALS['wpdb'] = (object) array( 'base_prefix' => 'wp_', @@ -81,6 +83,43 @@ private function key( string $agent_id, int $user_id ): string { } } +class DataMachineAccessStoreAdapterFakeAgentsRepository extends Agents { + /** @var array> */ + private array $rows; + + /** + * @param array> $rows Agent rows keyed by ID. + */ + public function __construct( array $rows ) { + $this->rows = $rows; + } + + public function get_agent( int $agent_id ): ?array { + return $this->rows[ $agent_id ] ?? null; + } + + public function get_by_slug( string $agent_slug ): ?array { + foreach ( $this->rows as $row ) { + if ( $row['agent_slug'] === $agent_slug ) { + return $row; + } + } + + return null; + } + + public function get_agents_by_ids( array $agent_ids ): array { + $rows = array(); + foreach ( $agent_ids as $agent_id ) { + if ( isset( $this->rows[ (int) $agent_id ] ) ) { + $rows[] = $this->rows[ (int) $agent_id ]; + } + } + + return $rows; + } +} + class DataMachineAccessStoreAdapterExistingStore implements \WP_Agent_Access_Store { public function grant_access( \WP_Agent_Access_Grant $grant ): \WP_Agent_Access_Grant { return $grant; } public function revoke_access( string $agent_id, int $user_id, ?string $workspace_id = null ): bool { unset( $agent_id, $user_id, $workspace_id ); return true; } @@ -92,7 +131,15 @@ public function get_users_for_agent( string $agent_id, ?string $workspace_id = n echo "agents-api-access-store-adapter-smoke\n"; $repository = new DataMachineAccessStoreAdapterFakeRepository(); -$adapter = new AgentAccessStoreAdapter( $repository ); +$agents = new DataMachineAccessStoreAdapterFakeAgentsRepository( + array( + 42 => array( + 'agent_id' => 42, + 'agent_slug' => 'wiki-brain', + ), + ) +); +$adapter = new AgentAccessStoreAdapter( $repository, $agents ); agents_api_smoke_assert_equals( true, $adapter instanceof \WP_Agent_Access_Store, 'adapter implements Agents API store contract', $failures, $passes ); @@ -101,12 +148,15 @@ public function get_users_for_agent( string $agent_id, ?string $workspace_id = n agents_api_smoke_assert_equals( true, AgentAccessStoreAdapter::filter_access_store( null ) instanceof AgentAccessStoreAdapter, 'filter supplies Data Machine adapter when empty', $failures, $passes ); agents_api_smoke_assert_equals( AgentAccessStoreAdapter::filter_access_store( null ), AgentAccessStoreAdapter::filter_access_store( null ), 'filter reuses default adapter instance', $failures, $passes ); -$grant = new \WP_Agent_Access_Grant( '42', 7, \WP_Agent_Access_Grant::ROLE_OPERATOR ); -agents_api_smoke_assert_equals( $grant, $adapter->grant_access( $grant ), 'grant delegates to repository', $failures, $passes ); -agents_api_smoke_assert_equals( $grant, $adapter->get_access( '42', 7 ), 'get_access delegates to repository', $failures, $passes ); -agents_api_smoke_assert_equals( array( '42' ), $adapter->get_agent_ids_for_user( 7, \WP_Agent_Access_Grant::ROLE_VIEWER ), 'agent ID list is contract string shape', $failures, $passes ); -agents_api_smoke_assert_equals( array( $grant ), $adapter->get_users_for_agent( '42' ), 'get_users_for_agent delegates to repository', $failures, $passes ); -agents_api_smoke_assert_equals( true, $adapter->revoke_access( '42', 7 ), 'revoke delegates to repository', $failures, $passes ); -agents_api_smoke_assert_equals( null, $adapter->get_access( '42', 7 ), 'revoke removes repository grant', $failures, $passes ); +$grant = new \WP_Agent_Access_Grant( 'wiki-brain', 7, \WP_Agent_Access_Grant::ROLE_OPERATOR ); +$stored_grant = new \WP_Agent_Access_Grant( '42', 7, \WP_Agent_Access_Grant::ROLE_OPERATOR ); +$returned_grant = $adapter->grant_access( $grant ); +agents_api_smoke_assert_equals( $grant->to_array(), $returned_grant->to_array(), 'grant maps slug to storage ID and returns slug contract', $failures, $passes ); +agents_api_smoke_assert_equals( $stored_grant->to_array(), $repository->get_access( '42', 7 )->to_array(), 'grant delegates numeric ID to repository', $failures, $passes ); +agents_api_smoke_assert_equals( $grant->to_array(), $adapter->get_access( 'wiki-brain', 7 )->to_array(), 'get_access maps slug to storage ID and returns slug contract', $failures, $passes ); +agents_api_smoke_assert_equals( array( 'wiki-brain' ), $adapter->get_agent_ids_for_user( 7, \WP_Agent_Access_Grant::ROLE_VIEWER ), 'agent ID list is Agents API slug shape', $failures, $passes ); +agents_api_smoke_assert_equals( array( $grant->to_array() ), array_map( static fn( \WP_Agent_Access_Grant $value ): array => $value->to_array(), $adapter->get_users_for_agent( 'wiki-brain' ) ), 'get_users_for_agent returns slug contract', $failures, $passes ); +agents_api_smoke_assert_equals( true, $adapter->revoke_access( 'wiki-brain', 7 ), 'revoke maps slug to storage ID', $failures, $passes ); +agents_api_smoke_assert_equals( null, $adapter->get_access( 'wiki-brain', 7 ), 'revoke removes repository grant', $failures, $passes ); agents_api_smoke_finish( 'access-store adapter smoke', $failures, $passes ); diff --git a/tests/agents-api-persisted-agent-registry-smoke.php b/tests/agents-api-persisted-agent-registry-smoke.php new file mode 100644 index 000000000..1a52b14c3 --- /dev/null +++ b/tests/agents-api-persisted-agent-registry-smoke.php @@ -0,0 +1,120 @@ +> */ + private array $rows; + + /** + * @param array> $rows Agent rows. + */ + public function __construct( array $rows ) { + $this->rows = $rows; + } + + public function get_all( array $args = array() ): array { + unset( $args ); + return $this->rows; + } +} + +function datamachine_persisted_agent_registry_reset(): void { + WP_Agents_Registry::reset_for_tests(); + $GLOBALS['__agents_api_smoke_actions'] = array(); + $GLOBALS['__agents_api_smoke_wrong'] = array(); + $GLOBALS['__agents_api_smoke_current'] = array(); + $GLOBALS['__agents_api_smoke_done'] = array(); + add_action( 'init', array( 'WP_Agents_Registry', 'init' ), 10 ); +} + +echo "\n[1] persisted Data Machine rows register as WP_Agent definitions:\n"; +datamachine_persisted_agent_registry_reset(); +$repository = new DataMachinePersistedAgentProjectorFakeRepository( + array( + array( + 'agent_id' => 101, + 'agent_slug' => 'wordpress-com-wiki', + 'agent_name' => 'WordPress.com Wiki', + 'owner_id' => 7, + 'agent_config' => array( + 'default_model' => 'gpt-5.5', + 'intelligence_wiki_brain' => array( + 'description' => 'Maintains the WordPress.com wiki brain.', + 'wiki_slug' => 'wordpress-com', + ), + 'datamachine_bundle' => array( + 'bundle_slug' => 'wordpress-com-wiki', + 'bundle_version' => '1.2.3', + 'source_revision' => 'abc123', + ), + ), + ), + array( + 'agent_id' => 102, + 'agent_slug' => 'woocommerce-wiki', + 'agent_name' => 'WooCommerce Wiki', + 'owner_id' => 8, + 'agent_config' => array( + 'description' => 'Maintains WooCommerce knowledge.', + ), + ), + ) +); + +add_action( + 'wp_agents_api_init', + static function () use ( $repository ): void { + PersistedAgentProjector::register_persisted_agents( $repository ); + } +); +do_action( 'init' ); + +$agents = wp_get_agents(); +$agent = wp_get_agent( 'wordpress-com-wiki' ); +agents_api_smoke_assert_equals( array( 'wordpress-com-wiki', 'woocommerce-wiki' ), array_keys( $agents ), 'persisted rows are visible through wp_get_agents()', $failures, $passes ); +agents_api_smoke_assert_equals( true, $agent instanceof WP_Agent, 'persisted row resolves through wp_get_agent()', $failures, $passes ); +agents_api_smoke_assert_equals( 'WordPress.com Wiki', $agent ? $agent->get_label() : '', 'row agent_name becomes label', $failures, $passes ); +agents_api_smoke_assert_equals( 'Maintains the WordPress.com wiki brain.', $agent ? $agent->get_description() : '', 'wiki brain description is surfaced', $failures, $passes ); +agents_api_smoke_assert_equals( 7, $agent && is_callable( $agent->get_owner_resolver() ) ? call_user_func( $agent->get_owner_resolver() ) : 0, 'owner_resolver returns persisted owner_id', $failures, $passes ); +agents_api_smoke_assert_equals( 'gpt-5.5', $agent ? $agent->get_default_config()['default_model'] ?? '' : '', 'agent_config becomes default_config', $failures, $passes ); +agents_api_smoke_assert_equals( 'wordpress-com-wiki', $agent ? $agent->get_meta()['source_package'] ?? '' : '', 'bundle slug is exposed as source package', $failures, $passes ); +agents_api_smoke_assert_equals( 'wordpress-com', $agent ? $agent->get_meta()['intelligence_wiki_brain_wiki_slug'] ?? '' : '', 'wiki brain hint is exposed in metadata', $failures, $passes ); + +echo "\n[2] persisted projection preserves earlier declarative registrations:\n"; +datamachine_persisted_agent_registry_reset(); +add_action( + 'wp_agents_api_init', + static function () use ( $repository ): void { + wp_register_agent( 'wordpress-com-wiki', array( 'label' => 'Declarative Definition' ) ); + PersistedAgentProjector::register_persisted_agents( $repository ); + } +); +do_action( 'init' ); + +$agents = wp_get_agents(); +agents_api_smoke_assert_equals( array( 'wordpress-com-wiki', 'woocommerce-wiki' ), array_keys( $agents ), 'projection skips duplicate slugs without dropping other rows', $failures, $passes ); +agents_api_smoke_assert_equals( 'Declarative Definition', $agents['wordpress-com-wiki']->get_label(), 'first registered definition wins', $failures, $passes ); +agents_api_smoke_assert_equals( array(), $GLOBALS['__agents_api_smoke_wrong'], 'duplicate skip avoids doing-it-wrong notices', $failures, $passes ); + +agents_api_smoke_finish( 'persisted agent registry smoke', $failures, $passes ); From 52c6ba3c4f46d6a2b0d2fb64cd4242c89c171873 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Wed, 13 May 2026 21:54:54 -0400 Subject: [PATCH 2/2] chore: align access adapter assignments --- inc/Core/Auth/AgentAccessStoreAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/Core/Auth/AgentAccessStoreAdapter.php b/inc/Core/Auth/AgentAccessStoreAdapter.php index f21baa381..c4ec68299 100644 --- a/inc/Core/Auth/AgentAccessStoreAdapter.php +++ b/inc/Core/Auth/AgentAccessStoreAdapter.php @@ -102,7 +102,7 @@ public function get_agent_ids_for_user( int $user_id, ?string $minimum_role = nu return array(); } - $rows = $this->agents_repository->get_agents_by_ids( array_map( 'intval', $agent_ids ) ); + $rows = $this->agents_repository->get_agents_by_ids( array_map( 'intval', $agent_ids ) ); $slugs_by_id = array(); foreach ( $rows as $row ) { $agent_id = (int) ( $row['agent_id'] ?? 0 );