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
20 changes: 2 additions & 18 deletions data-machine.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,7 @@
define( 'DATAMACHINE_URL', plugin_dir_url( __FILE__ ) );

require_once __DIR__ . '/vendor/autoload.php';

if ( ! defined( 'AGENTS_API_LOADED' ) ) {
require_once __DIR__ . '/vendor/wordpress/agents-api/agents-api.php';
}

// Guard against agents-api dual-load version skew. When an older standalone copy of the
// substrate won the load race, its coarse AGENTS_API_LOADED guard prevents our newer
// bundled copy from loading its newer classes — self-heal where safe, fail loud otherwise.
require_once __DIR__ . '/inc/agents-api-guardrail.php';
datamachine_agents_api_run_guardrail( __DIR__ . '/vendor/wordpress/agents-api' );
require_once __DIR__ . '/vendor/wordpress/agents-api/agents-api.php';

// WP-CLI integration
// @phpstan-ignore-next-line Runtime constant may be defined false outside PHPStan's configured CLI context.
Expand Down Expand Up @@ -334,14 +325,7 @@ function () {
// ActionPolicy + unified pending-action resolver. Content abilities register
// themselves on `datamachine_pending_action_handlers` via
// inc/Abilities/Content/ContentActionHandlers.php (required above).
if (
\DataMachine\Core\Bootstrap\DependencyChecker::has(
\DataMachine\Core\Bootstrap\DependencyChecker::CHECK_PENDING_ACTION_OBSERVER
)
) {
// @phpstan-ignore-next-line Scoped analysis sees the observer implementation before the conditional interface load.
\DataMachine\Engine\AI\Actions\PendingActionObservers::register( new \DataMachine\Engine\AI\Actions\WordPressActionDispatchObserver() );
}
\DataMachine\Engine\AI\Actions\PendingActionObservers::register( new \DataMachine\Engine\AI\Actions\WordPressActionDispatchObserver() );
new \DataMachine\Engine\AI\Actions\PendingActionInspectionAbility();
new \DataMachine\Engine\AI\Actions\SignPendingActionResolutionAbility();
new \DataMachine\Engine\AI\Actions\ResolvePendingActionAbility();
Expand Down
4 changes: 2 additions & 2 deletions inc/Abilities/Chat/AgentsChatHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function checkPermission( bool $allowed, array $input ): bool {
}

$identity = $this->resolveAgentIdentity( $agent );
if ( $identity instanceof WP_Error || ! class_exists( '\WP_Agent_Access' ) || ! class_exists( '\WP_Agent_Access_Grant' ) ) {
if ( $identity instanceof WP_Error ) {
return $allowed;
}

Expand Down Expand Up @@ -210,7 +210,7 @@ private function resolveRuntimeUserId( ?AgentIdentity $identity, string $agent,
return $user_id;
}

if ( ! $identity || ! class_exists( '\WP_Agent_Access' ) || ! class_exists( '\WP_Agent_Access_Grant' ) ) {
if ( ! $identity ) {
return 0;
}

Expand Down
55 changes: 11 additions & 44 deletions inc/Core/Bootstrap/DependencyChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,11 @@
*/
class DependencyChecker {

public const CHECK_AGENTS_API_ACCESS_STORE = 'agents_api_access_store';
public const CHECK_AGENTS_API_IDENTITY_STORE = 'agents_api_identity_store';
public const CHECK_ACTION_SCHEDULER = 'action_scheduler';
public const CHECK_FILESYSTEM_WRITES = 'filesystem_writes';
public const CHECK_IMAP = 'imap';
public const CHECK_PENDING_ACTION_OBSERVER = 'pending_action_observer';
public const CHECK_WORDPRESS_ABILITIES = 'wordpress_abilities';
public const CHECK_ZIP_ARCHIVE = 'zip_archive';
public const CHECK_ACTION_SCHEDULER = 'action_scheduler';
public const CHECK_FILESYSTEM_WRITES = 'filesystem_writes';
public const CHECK_IMAP = 'imap';
public const CHECK_WORDPRESS_ABILITIES = 'wordpress_abilities';
public const CHECK_ZIP_ARCHIVE = 'zip_archive';

/**
* Run a named dependency/capability check.
Expand All @@ -32,36 +29,15 @@ class DependencyChecker {
*/
public static function has( string $check ): bool {
return match ( $check ) {
self::CHECK_AGENTS_API_ACCESS_STORE => self::has_agents_api_access_store_contracts(),
self::CHECK_AGENTS_API_IDENTITY_STORE => self::has_agents_api_identity_store_contracts(),
self::CHECK_ACTION_SCHEDULER => self::has_action_scheduler(),
self::CHECK_FILESYSTEM_WRITES => self::has_filesystem_writes(),
self::CHECK_IMAP => self::has_imap(),
self::CHECK_PENDING_ACTION_OBSERVER => self::has_pending_action_observer_contract(),
self::CHECK_WORDPRESS_ABILITIES => self::has_wordpress_abilities(),
self::CHECK_ZIP_ARCHIVE => self::has_zip_archive(),
default => false,
self::CHECK_ACTION_SCHEDULER => self::has_action_scheduler(),
self::CHECK_FILESYSTEM_WRITES => self::has_filesystem_writes(),
self::CHECK_IMAP => self::has_imap(),
self::CHECK_WORDPRESS_ABILITIES => self::has_wordpress_abilities(),
self::CHECK_ZIP_ARCHIVE => self::has_zip_archive(),
default => false,
};
}

/**
* Determine whether the Agents API access-store contracts are available.
*
* @return bool True when the adapter can safely register.
*/
public static function has_agents_api_access_store_contracts(): bool {
return interface_exists( 'WP_Agent_Access_Store' ) && interface_exists( 'WP_Agent_Principal_Access_Store' );
}

/**
* Determine whether the Agents API identity-store contracts are available.
*
* @return bool True when the adapter can safely register.
*/
public static function has_agents_api_identity_store_contracts(): bool {
return interface_exists( '\AgentsAPI\Core\Identity\WP_Agent_Identity_Store' ) && class_exists( '\AgentsAPI\Core\Identity\WP_Agent_Materialized_Identity' );
}

/**
* Determine whether Action Scheduler is available.
*
Expand Down Expand Up @@ -96,15 +72,6 @@ public static function has_imap(): bool {
return function_exists( 'imap_open' );
}

/**
* Determine whether the Agents API pending-action observer contract is available.
*
* @return bool True when observer implementations can safely register.
*/
public static function has_pending_action_observer_contract(): bool {
return interface_exists( '\AgentsAPI\AI\Approvals\WP_Agent_Pending_Action_Observer' );
}

/**
* Determine whether the WordPress Abilities API is available.
*
Expand Down
10 changes: 1 addition & 9 deletions inc/Core/Database/Chat/ConversationStoreFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,10 @@ public static function get(): ConversationStoreInterface {
/**
* Create the default MySQL-backed store.
*
* The principal-owner transcript interface is an optional Agents API contract.
* Use the principal-aware subclass only when that interface is loaded, so
* activation remains safe with older dependency checkouts.
*
* @return ConversationStoreInterface
*/
private static function default_store(): ConversationStoreInterface {
if ( interface_exists( 'AgentsAPI\\Core\\Database\\Chat\\WP_Agent_Principal_Conversation_Session_Reader' ) ) {
return new PrincipalChat();
}

return new Chat();
return new PrincipalChat();
}

/**
Expand Down
91 changes: 7 additions & 84 deletions inc/Engine/AI/MemoryFileRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,7 @@ public static function register( string $filename, int $priority = 50, array $ar

self::$files[ $filename ] = $metadata;

// Mirror into the Agents API source registry. When the substrate is
// missing (Homeboy playground bootstrap), DM runs degraded with the
// local self::$files map only — see agents_api_loaded() docblock.
if ( ! self::agents_api_loaded() ) {
return;
}

// Mirror into the Agents API source registry.
WP_Agent_Memory_Registry::register(
self::source_id_for_filename( $filename ),
array(
Expand Down Expand Up @@ -194,16 +188,10 @@ public static function register( string $filename, int $priority = 50, array $ar
/**
* Resolve the default retrieval policy for a registration.
*
* Mirrors `WP_Agent_Context_Injection_Policy::NEVER` / `::ALWAYS` while
* tolerating the substrate being absent at bootstrap.
*
* @param bool $modes_empty Whether the registration declared no modes.
* @return string Canonical policy slug.
*/
private static function default_retrieval_policy( bool $modes_empty ): string {
if ( ! self::agents_api_loaded() ) {
return $modes_empty ? 'never' : 'always';
}
return $modes_empty
? WP_Agent_Context_Injection_Policy::NEVER
: WP_Agent_Context_Injection_Policy::ALWAYS;
Expand All @@ -212,17 +200,10 @@ private static function default_retrieval_policy( bool $modes_empty ): string {
/**
* Normalize a retrieval policy string.
*
* Delegates to the substrate when present; otherwise falls back to a
* local allowlist matching `WP_Agent_Context_Injection_Policy::values()`.
*
* @param string $policy Raw policy value.
* @return string Canonical policy slug.
*/
private static function normalize_retrieval_policy( string $policy ): string {
if ( ! self::agents_api_loaded() ) {
$valid = array( 'always', 'on_intent', 'on_tool_need', 'manual', 'never' );
return in_array( $policy, $valid, true ) ? $policy : 'always';
}
return WP_Agent_Context_Injection_Policy::normalize( $policy );
}

Expand Down Expand Up @@ -265,9 +246,7 @@ public static function normalize_injection_contexts( mixed $contexts ): array {
public static function deregister( string $filename ): void {
$filename = sanitize_file_name( $filename );
unset( self::$files[ $filename ] );
if ( self::agents_api_loaded() ) {
WP_Agent_Memory_Registry::unregister( self::source_id_for_filename( $filename ) );
}
WP_Agent_Memory_Registry::unregister( self::source_id_for_filename( $filename ) );
}

/**
Expand Down Expand Up @@ -501,16 +480,12 @@ public static function get_for_modes( array $modes, array $injection_contexts =
return self::get_resolved();
}

$agents_api_loaded = self::agents_api_loaded();

return array_filter(
self::get_resolved(),
function ( $meta ) use ( $modes, $injection_contexts, $agents_api_loaded ) {
$default_policy = $agents_api_loaded ? WP_Agent_Context_Injection_Policy::ALWAYS : 'always';
function ( $meta ) use ( $modes, $injection_contexts ) {
$default_policy = WP_Agent_Context_Injection_Policy::ALWAYS;
$retrieval_policy = $meta['retrieval_policy'] ?? $default_policy;
$is_always = $agents_api_loaded
? WP_Agent_Context_Injection_Policy::is_always_injected( $retrieval_policy )
: ( 'always' === $retrieval_policy );
$is_always = WP_Agent_Context_Injection_Policy::is_always_injected( $retrieval_policy );
if ( ! $is_always ) {
return false;
}
Expand Down Expand Up @@ -604,9 +579,7 @@ public static function get_layer_filenames( string $layer ): array {
public static function reset(): void {
self::$files = array();
self::$filter_applied = false;
if ( self::agents_api_loaded() ) {
WP_Agent_Memory_Registry::reset();
}
WP_Agent_Memory_Registry::reset();
}

/**
Expand Down Expand Up @@ -635,11 +608,7 @@ private static function get_resolved(): array {
self::$filter_applied = true;
}

// When the Agents API substrate is missing, the local self::$files map
// is the single source of truth. See agents_api_loaded() docblock.
$files = self::agents_api_loaded()
? array_replace( self::from_agents_api_sources( WP_Agent_Memory_Registry::get_all() ), self::$files )
: self::$files;
$files = array_replace( self::from_agents_api_sources( WP_Agent_Memory_Registry::get_all() ), self::$files );
uasort(
$files,
function ( $a, $b ) {
Expand Down Expand Up @@ -693,65 +662,19 @@ public static function context_slug_for_filename( string $filename ): string {
return sanitize_key( str_replace( '.', '-', strtolower( $filename ) ) );
}

/**
* Whether the Agents API runtime classes are loaded.
*
* DM core declares `agents-api` as a `Requires Plugins` dependency, so under
* normal WordPress activation flows the substrate is always present. The
* Homeboy playground bootstrap (used by `homeboy test` / `homeboy release`)
* loads plugins via `require_once` and bypasses that gate, which means DM
* core can boot in an environment where these classes do not exist.
*
* When that happens, every site that touches `MemoryFileRegistry::register()`
* during `muplugins_loaded` fatals before plugin code runs. To keep the
* registry usable in that degraded mode we mirror the data locally in
* `self::$files` and skip the Agents API mirror calls. Production behavior
* (where the substrate is loaded) is unchanged.
*
* The result is cached per request — class loading state is immutable
* within a single PHP process for our purposes.
*
* @return bool
*/
private static function agents_api_loaded(): bool {
static $loaded = null;
if ( null === $loaded ) {
$loaded = class_exists( WP_Agent_Memory_Layer::class )
&& class_exists( WP_Agent_Memory_Registry::class )
&& class_exists( WP_Agent_Context_Injection_Policy::class )
&& class_exists( '\\AgentsAPI\\AI\\Context\\WP_Agent_Context_Authority_Tier' );
}
return $loaded;
}

/**
* Normalize a raw layer string.
*
* Delegates to {@see WP_Agent_Memory_Layer::normalize()} when the Agents API
* substrate is loaded. When the substrate is missing (Homeboy playground
* bootstrap, isolated tests), falls back to a local allowlist of the four
* DM-canonical layers and defaults to {@see self::LAYER_AGENT}.
*
* @param string $layer Raw layer identifier.
* @return string Normalized layer slug.
*/
private static function normalize_layer( string $layer ): string {
if ( ! self::agents_api_loaded() ) {
$valid = array( self::LAYER_SHARED, self::LAYER_AGENT, self::LAYER_USER, self::LAYER_NETWORK );
return in_array( $layer, $valid, true ) ? $layer : self::LAYER_AGENT;
}
return WP_Agent_Memory_Layer::normalize( $layer, self::LAYER_AGENT );
}

/**
* Default authority tier for a layer/filename pair.
*
* When the Agents API substrate is missing, returns the canonical string
* literals that `WP_Agent_Context_Authority_Tier` would otherwise resolve
* to ({@see WP_Agent_Context_Authority_Tier::ordered()}). Keeping the
* fallbacks in sync with the substrate's vocabulary is intentional —
* downstream consumers compare these as plain strings.
*
* @param string $layer Normalized layer slug.
* @param string $filename Filename being registered.
* @return string Authority tier slug.
Expand Down
16 changes: 4 additions & 12 deletions inc/Engine/AI/System/Tasks/DispatchMessageTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ public static function getTaskMeta(): array {
* Execute dispatch message task.
*
* Resolves the canonical `agents/dispatch-message` ability and forwards
* the configured `params`. Fails the job cleanly when the substrate is
* missing (agents-api not installed or too old) or when the ability
* returns a WP_Error. On success, stores the canonical output
* the configured `params`. Fails the job cleanly when the ability is not
* registered or when the ability returns a WP_Error. On success, stores
* the canonical output
* (`sent`, `channel`, `recipient`, `message_id`, `metadata`) in the
* job result envelope alongside a `completed_at` timestamp — mirroring
* AgentCallTask's success shape.
Expand All @@ -86,19 +86,11 @@ public static function getTaskMeta(): array {
* @param array $params Task params with `channel`, `recipient`, `message` and optional passthrough fields.
*/
public function executeTask( int $jobId, array $params ): void {
if ( ! function_exists( 'wp_get_ability' ) ) {
$this->failJob(
$jobId,
sprintf( 'Ability %s not registered — agents-api missing or too old.', self::ABILITY_SLUG )
);
return;
}

$ability = wp_get_ability( self::ABILITY_SLUG );
if ( ! $ability ) {
$this->failJob(
$jobId,
sprintf( 'Ability %s not registered — agents-api missing or too old.', self::ABILITY_SLUG )
sprintf( 'Ability %s not registered.', self::ABILITY_SLUG )
);
return;
}
Expand Down
Loading
Loading