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
48 changes: 36 additions & 12 deletions inc/Engine/AI/MemoryFileRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ class MemoryFileRegistry {
*/
const MODE_ALL = 'all';

/**
* Default mode list for registered files.
*
* Files without explicit modes are registered and manageable, but are not
* injected into AI prompts. Prompt injection must be an explicit opt-in.
*/
const MODES_NONE = array();

/**
* Registered memory files.
*
Expand Down Expand Up @@ -77,10 +85,13 @@ class MemoryFileRegistry {
* A capability string (e.g. 'manage_options') = editable only
* by users with that WordPress capability. Default true.
* Forced to false when composable is true.
* @type string[] $modes Execution modes where this file should be injected.
* @type string[] $modes Execution modes where this file is available for prompt injection.
* Array of mode slugs (e.g. 'chat', 'editor', 'pipeline',
* 'system') or array( 'all' ) to inject everywhere.
* Default array( 'all' ).
* 'system') or array( 'all' ) to make available everywhere.
* Default empty array: registered but never injected.
* @type string $retrieval_policy Context injection policy. Only `always` files are injected
* by CoreMemoryFilesDirective. Defaults to `never` when modes are
* omitted, otherwise `always`.
* @type bool $composable Whether this file is auto-generated from registered sections
* via SectionRegistry. Composable files are regenerated on
* demand and are not hand-editable. Default false.
Expand Down Expand Up @@ -108,12 +119,15 @@ public static function register( string $filename, int $priority = 50, array $ar
$editable = true;
}

// Normalize modes: array of slugs, or ['all'] (default).
$modes = $args['modes'] ?? array( self::MODE_ALL );
if ( ! is_array( $modes ) || empty( $modes ) ) {
$modes = array( self::MODE_ALL );
// Normalize modes: omitted means registered but not prompt-injected.
$modes = $args['modes'] ?? self::MODES_NONE;
if ( ! is_array( $modes ) ) {
$modes = self::MODES_NONE;
}
$modes = array_values( array_unique( array_map( 'sanitize_key', $modes ) ) );
$default_retrieval_policy = empty( $modes )
? WP_Agent_Context_Injection_Policy::NEVER
: WP_Agent_Context_Injection_Policy::ALWAYS;

// Convention path: relative path from ABSPATH for an additional copy.
$convention_path = isset( $args['convention_path'] ) ? ltrim( $args['convention_path'], '/' ) : '';
Expand All @@ -129,7 +143,7 @@ public static function register( string $filename, int $priority = 50, array $ar
'modes' => $modes,
'label' => $args['label'] ?? self::filename_to_label( $filename ),
'description' => $args['description'] ?? '',
'retrieval_policy' => WP_Agent_Context_Injection_Policy::normalize( $args['retrieval_policy'] ?? WP_Agent_Context_Injection_Policy::ALWAYS ),
'retrieval_policy' => WP_Agent_Context_Injection_Policy::normalize( $args['retrieval_policy'] ?? $default_retrieval_policy ),
'authority_tier' => $args['authority_tier'] ?? self::default_authority_tier( $layer, $filename ),
'provenance' => is_array( $args['provenance'] ?? null ) ? $args['provenance'] : self::default_provenance( $filename ),
);
Expand Down Expand Up @@ -382,10 +396,11 @@ function ( $meta ) use ( $layer ) {
}

/**
* Get all files applicable to a specific agent mode.
* Get always-injected files applicable to a specific agent mode.
*
* Returns files that either list the mode in their `modes` array
* or are registered with `['all']` (the default).
* or are registered with `['all']`, excluding files
* whose retrieval policy says they should not be injected eagerly.
*
* @since 0.60.0
* @since 0.68.0 Internal key renamed from contexts to modes.
Expand All @@ -403,7 +418,16 @@ public static function get_for_mode( string $mode ): array {
return array_filter(
self::get_resolved(),
function ( $meta ) use ( $mode ) {
$modes = $meta['modes'] ?? array( self::MODE_ALL );
$retrieval_policy = $meta['retrieval_policy'] ?? WP_Agent_Context_Injection_Policy::ALWAYS;
if ( ! WP_Agent_Context_Injection_Policy::is_always_injected( $retrieval_policy ) ) {
return false;
}

$modes = $meta['modes'] ?? self::MODES_NONE;
if ( empty( $modes ) ) {
return false;
}

return in_array( self::MODE_ALL, $modes, true )
|| in_array( $mode, $modes, true );
}
Expand Down Expand Up @@ -545,7 +569,7 @@ private static function from_agents_api_sources( array $sources ): array {
'editable' => $source['editable'] ?? true,
'composable' => (bool) ( $source['composable'] ?? false ),
'convention_path' => is_string( $source['convention_path'] ?? null ) ? $source['convention_path'] : '',
'modes' => is_array( $source['modes'] ?? null ) ? $source['modes'] : array( self::MODE_ALL ),
'modes' => is_array( $source['modes'] ?? null ) ? $source['modes'] : self::MODES_NONE,
'label' => is_string( $source['label'] ?? null ) ? $source['label'] : self::filename_to_label( $filename ),
'description' => is_string( $source['description'] ?? null ) ? $source['description'] : '',
'retrieval_policy' => is_string( $source['retrieval_policy'] ?? null ) ? $source['retrieval_policy'] : WP_Agent_Context_Injection_Policy::ALWAYS,
Expand Down
3 changes: 3 additions & 0 deletions inc/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,15 @@ function () {
'layer' => MemoryFileRegistry::LAYER_SHARED,
'protected' => true,
'composable' => true,
'modes' => array( MemoryFileRegistry::MODE_ALL ),
'label' => 'Site Context',
'description' => 'Auto-generated site context. Composable — extend via SectionRegistry.',
) );
MemoryFileRegistry::register( 'RULES.md', 15, array(
'layer' => MemoryFileRegistry::LAYER_SHARED,
'protected' => true,
'editable' => 'manage_options',
'modes' => array( MemoryFileRegistry::MODE_ALL ),
'label' => 'Site Rules',
'description' => 'Behavioral constraints that apply to every agent. Admin-editable.',
) );
Expand Down Expand Up @@ -186,6 +188,7 @@ function () {
'layer' => MemoryFileRegistry::LAYER_NETWORK,
'protected' => true,
'composable' => true,
'modes' => array( MemoryFileRegistry::MODE_ALL ),
'label' => 'Network Context',
'description' => 'Auto-generated multisite network topology. Composable — extend via SectionRegistry.',
) );
Expand Down
27 changes: 27 additions & 0 deletions tests/Unit/AI/Memory/MemoryPolicyResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ public function set_up(): void {
'modes' => array( 'chat' ),
)
);
MemoryFileRegistry::register(
'EXTERNAL_RUNTIME.md',
50,
array(
'layer' => MemoryFileRegistry::LAYER_SHARED,
)
);

$this->resolver = new MemoryPolicyResolver();
}
Expand All @@ -90,6 +97,7 @@ public function test_registered_chat_context_includes_all_and_chat_files(): void
$this->assertArrayHasKey( 'MEMORY.md', $files );
$this->assertArrayHasKey( 'USER.md', $files );
$this->assertArrayHasKey( 'CHAT_ONLY.md', $files );
$this->assertArrayNotHasKey( 'EXTERNAL_RUNTIME.md', $files );
}

public function test_registered_pipeline_context_excludes_chat_only(): void {
Expand All @@ -103,6 +111,25 @@ public function test_registered_pipeline_context_excludes_chat_only(): void {
$this->assertArrayHasKey( 'MEMORY.md', $files );
$this->assertArrayHasKey( 'USER.md', $files );
$this->assertArrayNotHasKey( 'CHAT_ONLY.md', $files );
$this->assertArrayNotHasKey( 'EXTERNAL_RUNTIME.md', $files );
}

public function test_registered_file_without_modes_is_not_injected_in_any_mode(): void {
$chat_files = $this->resolver->resolveRegistered(
array(
'mode' => MemoryPolicyResolver::MODE_CHAT,
)
);
$pipeline_files = $this->resolver->resolveRegistered(
array(
'mode' => MemoryPolicyResolver::MODE_PIPELINE,
)
);

$this->assertArrayNotHasKey( 'EXTERNAL_RUNTIME.md', $chat_files );
$this->assertArrayNotHasKey( 'EXTERNAL_RUNTIME.md', $pipeline_files );
$this->assertArrayHasKey( 'EXTERNAL_RUNTIME.md', MemoryFileRegistry::get_all() );
$this->assertSame( \WP_Agent_Context_Injection_Policy::NEVER, MemoryFileRegistry::get_all()['EXTERNAL_RUNTIME.md']['retrieval_policy'] );
}

public function test_registered_preserves_metadata(): void {
Expand Down
Loading