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
69 changes: 69 additions & 0 deletions inc/Core/Database/Chat/Chat.php
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,75 @@ public function get_session( string $session_id ): ?array {
return $session;
}

/**
* List transcript sessions for a workspace/user pair.
*
* @param WP_Agent_Workspace_Scope $workspace Workspace owning the sessions.
* @param int $user_id WordPress user ID owning the sessions.
* @param array $args Optional filters/pagination.
* @return array<int,array<string,mixed>> Session rows.
*/
public function list_sessions( WP_Agent_Workspace_Scope $workspace, int $user_id, array $args = array() ): array {
global $wpdb;

$table_name = self::get_prefixed_table_name();
$include_messages = (bool) ( $args['include_messages'] ?? true );
$limit = max( 1, min( 100, (int) ( $args['limit'] ?? 20 ) ) );
$offset = max( 0, (int) ( $args['offset'] ?? 0 ) );
$where = array(
'workspace_type = %s',
'workspace_id = %s',
'user_id = %d',
);
$query_args = array(
$table_name,
$workspace->workspace_type,
$workspace->workspace_id,
$user_id,
);

if ( is_string( $args['context'] ?? null ) && '' !== $args['context'] ) {
$where[] = 'mode = %s';
$query_args[] = $args['context'];
}

if ( is_string( $args['agent_slug'] ?? null ) && '' !== $args['agent_slug'] ) {
try {
$identity = self::resolve_agent_identity_for_session( $args['agent_slug'] );
} catch ( \InvalidArgumentException $e ) {
unset( $e );
return array();
}

$where[] = 'agent_id = %d';
$query_args[] = $identity['agent_id'];
}

$select = $include_messages ? '*' : 'session_id, workspace_type, workspace_id, user_id, agent_id, title, metadata, provider, model, provider_response_id, mode, created_at, updated_at, last_read_at, expires_at';
$sql = 'SELECT ' . $select . ' FROM %i WHERE ' . implode( ' AND ', $where ) . ' ORDER BY updated_at DESC LIMIT %d OFFSET %d';

$query_args[] = $limit;
$query_args[] = $offset;

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
$sessions = $wpdb->get_results( $wpdb->prepare( $sql, ...$query_args ), ARRAY_A );

if ( ! $sessions ) {
return array();
}

foreach ( $sessions as &$session ) {
if ( $include_messages ) {
$session['messages'] = self::normalize_messages( json_decode( $session['messages'] ?? '[]', true ) ?? array() );
}
$session['metadata'] = json_decode( $session['metadata'] ?? '[]', true ) ?? array();
$session['agent_slug'] = self::resolve_agent_slug_from_session_row( $session );
}
unset( $session );

return $sessions;
}

/**
* Resolve the generic transcript agent slug from a stored session row.
*
Expand Down
128 changes: 68 additions & 60 deletions inc/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,67 +136,75 @@ function () {
0
);

// Shared layer — site-wide context, visible to all agents.
// Composable: content assembled from sections registered against SectionRegistry
// (see inc/migrations/site-md.php). `editable` is forced to false by composable=true.
MemoryFileRegistry::register( 'SITE.md', 10, array(
'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.',
) );

// Agent layer — identity and knowledge, scoped to a single agent.
// Injected in interactive modes only (chat, pipeline). Excluded from
// system mode so autonomous maintenance tasks (e.g. daily memory
// compaction) are not primed with the agent's identity while operating
// on these files.
MemoryFileRegistry::register( 'SOUL.md', 20, array(
'layer' => MemoryFileRegistry::LAYER_AGENT,
'protected' => true,
'modes' => array( 'chat', 'pipeline' ),
'label' => 'Agent Identity',
'description' => 'Agent identity, voice, rules. Injected in interactive modes only.',
) );
MemoryFileRegistry::register( 'MEMORY.md', 30, array(
'layer' => MemoryFileRegistry::LAYER_AGENT,
'protected' => true,
'modes' => array( 'chat', 'pipeline' ),
'label' => 'Agent Memory',
'description' => 'Accumulated knowledge. Injected in interactive modes only.',
) );

// User layer — human preferences, network-scoped on multisite.
// Only injected in interactive modes where a human is present.
// Pipelines can still opt in via pipeline memory file selection.
MemoryFileRegistry::register( 'USER.md', 25, array(
'layer' => MemoryFileRegistry::LAYER_USER,
'protected' => true,
'modes' => array( 'chat', 'editor' ),
'label' => 'User Profile',
'description' => 'Information about the human the agent works with. Injected in chat and editor modes only.',
) );
function datamachine_register_default_memory_files(): void {
// Shared layer — site-wide context, visible to all agents.
// Composable: content assembled from sections registered against SectionRegistry
// (see inc/migrations/site-md.php). `editable` is forced to false by composable=true.
MemoryFileRegistry::register( 'SITE.md', 10, array(
'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.',
) );

// Agent layer — identity and knowledge, scoped to a single agent.
// Injected in interactive modes only (chat, pipeline). Excluded from
// system mode so autonomous maintenance tasks (e.g. daily memory
// compaction) are not primed with the agent's identity while operating
// on these files.
MemoryFileRegistry::register( 'SOUL.md', 20, array(
'layer' => MemoryFileRegistry::LAYER_AGENT,
'protected' => true,
'modes' => array( 'chat', 'pipeline' ),
'label' => 'Agent Identity',
'description' => 'Agent identity, voice, rules. Injected in interactive modes only.',
) );
MemoryFileRegistry::register( 'MEMORY.md', 30, array(
'layer' => MemoryFileRegistry::LAYER_AGENT,
'protected' => true,
'modes' => array( 'chat', 'pipeline' ),
'label' => 'Agent Memory',
'description' => 'Accumulated knowledge. Injected in interactive modes only.',
) );

// User layer — human preferences, network-scoped on multisite.
// Only injected in interactive modes where a human is present.
// Pipelines can still opt in via pipeline memory file selection.
MemoryFileRegistry::register( 'USER.md', 25, array(
'layer' => MemoryFileRegistry::LAYER_USER,
'protected' => true,
'modes' => array( 'chat', 'editor' ),
'label' => 'User Profile',
'description' => 'Information about the human the agent works with. Injected in chat and editor modes only.',
) );

// Network layer — multisite topology, only meaningful on multisite installs.
// Composable: content assembled from sections registered against SectionRegistry.
MemoryFileRegistry::register( 'NETWORK.md', 5, array(
'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.',
) );
}

// Network layer — multisite topology, only meaningful on multisite installs.
// Composable: content assembled from sections registered against SectionRegistry.
MemoryFileRegistry::register( 'NETWORK.md', 5, array(
'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.',
) );
if ( did_action( 'plugins_loaded' ) ) {
datamachine_register_default_memory_files();
} else {
add_action( 'plugins_loaded', 'datamachine_register_default_memory_files', 0 );
}

// Composable file auto-regeneration — rebuilds AGENTS.md, SITE.md, NETWORK.md, and any other
// composable files on plugin (de)activation plus any hooks plugins register via
Expand Down
29 changes: 29 additions & 0 deletions tests/Unit/Core/Database/Chat/InMemoryConversationStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,35 @@ public function get_session( string $session_id ): ?array {
return $session;
}

public function list_sessions( WP_Agent_Workspace_Scope $workspace, int $user_id, array $args = array() ): array {
$include_messages = (bool) ( $args['include_messages'] ?? true );
$limit = max( 1, min( 100, (int) ( $args['limit'] ?? 20 ) ) );
$offset = max( 0, (int) ( $args['offset'] ?? 0 ) );
$rows = array();

foreach ( $this->sessions as $session ) {
if ( $session['workspace_type'] !== $workspace->workspace_type || $session['workspace_id'] !== $workspace->workspace_id || (int) $session['user_id'] !== $user_id ) {
continue;
}
if ( is_string( $args['context'] ?? null ) && '' !== $args['context'] && $session['context'] !== $args['context'] ) {
continue;
}
if ( is_string( $args['agent_slug'] ?? null ) && '' !== $args['agent_slug'] && ( $session['agent_slug'] ?? '' ) !== sanitize_title( $args['agent_slug'] ) ) {
continue;
}

$row = $session;
if ( ! $include_messages ) {
unset( $row['messages'] );
}
$rows[] = $row;
}

usort( $rows, static fn( $a, $b ) => strcmp( $b['updated_at'], $a['updated_at'] ) );

return array_slice( $rows, $offset, $limit );
}

public function update_session( string $session_id, array $messages, array $metadata = array(), string $provider = '', string $model = '', ?string $provider_response_id = null ): bool {
if ( ! isset( $this->sessions[ $session_id ] ) ) {
return false;
Expand Down
1 change: 1 addition & 0 deletions tests/ai-request-metadata-guardrails-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ public function create_session( ...$args ): string {
}

public function get_session( string $session_id ): ?array { return null; }
public function list_sessions( WP_Agent_Workspace_Scope $workspace, int $user_id, array $args = array() ): array { return array(); }
public function update_session( string $session_id, array $messages, array $metadata = array(), string $provider = '', string $model = '', ?string $provider_response_id = null ): bool {
unset( $provider_response_id );
$this->updated[ $session_id ] = compact( 'messages', 'metadata', 'provider', 'model' );
Expand Down
Loading