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
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,44 @@ wp_register_agent(
- `AgentsAPI\AI\Tools\ToolExecutorInterface`
- `AgentsAPI\AI\Tools\ToolExecutionCore`
- `AgentsAPI\AI\Tools\ToolExecutionResult`
- `AgentsAPI\Core\Workspace\AgentWorkspaceScope`
- `AgentsAPI\Core\Database\Chat\ConversationTranscriptStoreInterface`
- `AgentsAPI\Core\FilesRepository\AgentMemoryStoreInterface` and memory value objects

## Workspace Scope

`AgentsAPI\Core\Workspace\AgentWorkspaceScope` is the generic workspace identity shared by memory, transcript, persistence, and audit adapters. It is deliberately broader than a WordPress site ID:

```php
$workspace = AgentsAPI\Core\Workspace\AgentWorkspaceScope::from_parts(
'code_workspace',
'Automattic/intelligence@contexta8c-read-coverage'
);

$workspace->to_array();
// array(
// 'workspace_type' => 'code_workspace',
// 'workspace_id' => 'Automattic/intelligence@contexta8c-read-coverage',
// )
```

Consumers may map WordPress sites, networks, headless runtimes, Studio sites, code workspaces, pull requests, or ephemeral execution environments into that pair. Agents API keeps those mappings in consumer adapters; the generic contracts only depend on `workspace_type` + `workspace_id`.

Memory scope uses `(layer, workspace_type, workspace_id, user_id, agent_id, filename)` as its identity model:

```php
$scope = new AgentsAPI\Core\FilesRepository\AgentMemoryScope(
'user',
$workspace->workspace_type,
$workspace->workspace_id,
123,
456,
'MEMORY.md'
);
```

Transcript sessions are also workspace-stamped. `ConversationTranscriptStoreInterface::create_session()` and `::get_recent_pending_session()` both receive an `AgentWorkspaceScope`, and `AgentConversationRequest` can carry a workspace so runtime persisters can stamp the session they materialize.

## Execution Principals

`AgentsAPI\AI\AgentExecutionPrincipal` represents the actor and agent context for one runtime request. It records the acting WordPress user ID, effective agent ID/slug, auth source, request context, optional token ID, and JSON-friendly request metadata.
Expand Down
1 change: 1 addition & 0 deletions agents-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
require_once AGENTS_API_PATH . 'src/Registry/class-wp-agents-registry.php';
require_once AGENTS_API_PATH . 'src/Registry/register-agents.php';
require_once AGENTS_API_PATH . 'src/Packages/register-agent-package-artifacts.php';
require_once AGENTS_API_PATH . 'src/Workspace/AgentWorkspaceScope.php';
require_once AGENTS_API_PATH . 'src/Identity/AgentIdentityScope.php';
require_once AGENTS_API_PATH . 'src/Identity/MaterializedAgentIdentity.php';
require_once AGENTS_API_PATH . 'src/Identity/MaterializedAgentIdentityStoreInterface.php';
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"php tests/approval-resolver-contract-smoke.php",
"php tests/identity-smoke.php",
"php tests/approval-action-value-shape-smoke.php",
"php tests/workspace-scope-smoke.php",
"php tests/compaction-item-smoke.php",
"php tests/conversation-runner-contracts-smoke.php",
"php tests/conversation-compaction-smoke.php",
Expand Down
60 changes: 48 additions & 12 deletions src/Memory/AgentMemoryScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Agent Memory Scope
*
* Immutable value object that uniquely identifies an agent memory record
* across the four-tuple primary key (layer, user_id, agent_id, filename).
* across (layer, workspace_type, workspace_id, user_id, agent_id, filename).
*
* Same identity model as the on-disk path encoding, just decoupled from
* the filesystem so an alternate store (e.g. database-backed, guideline-backed,
Expand All @@ -15,30 +15,66 @@

namespace AgentsAPI\Core\FilesRepository;

use AgentsAPI\Core\Workspace\AgentWorkspaceScope;

defined( 'ABSPATH' ) || exit;

final class AgentMemoryScope {

public readonly string $layer;

public readonly string $workspace_type;

public readonly string $workspace_id;

public readonly int $user_id;

public readonly int $agent_id;

public readonly string $filename;

/**
* @param string $layer Memory layer identifier (for example shared, agent, user, or network).
* @param int $user_id Effective WordPress user ID. 0 = legacy shared / no user.
* @param int $agent_id Agent ID for direct resolution. 0 = resolve from user_id.
* @param string $filename Filename or relative path within the layer
* (e.g. 'MEMORY.md', 'contexts/chat.md', 'daily/2026/04/17.md').
* @param string $layer Memory layer identifier (for example shared, agent, user, or network).
* @param string $workspace_type Generic workspace kind.
* @param string $workspace_id Stable workspace identifier within the workspace type.
* @param int $user_id Effective WordPress user ID. 0 = shared / no user.
* @param int $agent_id Agent ID for direct resolution. 0 = resolve from user_id.
* @param string $filename Filename or relative path within the layer
* (e.g. 'MEMORY.md', 'contexts/chat.md', 'daily/2026/04/17.md').
*/
public function __construct(
public readonly string $layer,
public readonly int $user_id,
public readonly int $agent_id,
public readonly string $filename,
) {}
string $layer,
string $workspace_type,
string $workspace_id,
int $user_id,
int $agent_id,
string $filename,
) {
$workspace = AgentWorkspaceScope::from_parts( $workspace_type, $workspace_id );

$this->layer = trim( $layer );
$this->workspace_type = $workspace->workspace_type;
$this->workspace_id = $workspace->workspace_id;
$this->user_id = $user_id;
$this->agent_id = $agent_id;
$this->filename = trim( $filename );
}

/**
* Return the normalized workspace identity.
*
* @return AgentWorkspaceScope
*/
public function workspace(): AgentWorkspaceScope {
return AgentWorkspaceScope::from_parts( $this->workspace_type, $this->workspace_id );
}

/**
* Stable string key for caching / map lookups.
*
* @return string
*/
public function key(): string {
return sprintf( '%s:%d:%d:%s', $this->layer, $this->user_id, $this->agent_id, $this->filename );
return sprintf( '%s:%s:%s:%d:%d:%s', $this->layer, $this->workspace_type, $this->workspace_id, $this->user_id, $this->agent_id, $this->filename );
}
}
6 changes: 3 additions & 3 deletions src/Memory/AgentMemoryStoreInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
* - translating an {@see AgentMemoryScope} to a physical key (path, row, URL);
* - returning a stable content hash so callers can implement
* compare-and-swap concurrency via the `if_match` write parameter;
* - honoring the layer + user_id + agent_id + filename four-tuple as the
* identity model.
* - honoring the layer + workspace_type + workspace_id + user_id + agent_id
* + filename identity model.
*
* Section parsing, scaffold/default-file creation, editability gating,
* ability permissions, prompt-injection policy, and registry-driven
Expand Down Expand Up @@ -74,7 +74,7 @@ public function delete( AgentMemoryScope $scope ): AgentMemoryWriteResult;
* List all top-level files in a single layer for the given identity.
*
* The $scope_query's `filename` field is ignored — list operations
* return all files matching `(layer, user_id, agent_id)`. The
* return all files matching `(layer, workspace_type, workspace_id, user_id, agent_id)`. The
* `layer` field is required.
*
* Top-level only: subdirectories under the layer (e.g. `daily/`,
Expand Down
15 changes: 14 additions & 1 deletion src/Runtime/AgentConversationRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace AgentsAPI\AI;

use AgentsAPI\AI\Tools\RuntimeToolDeclaration;
use AgentsAPI\Core\Workspace\AgentWorkspaceScope;

defined( 'ABSPATH' ) || exit;

Expand Down Expand Up @@ -40,6 +41,9 @@ class AgentConversationRequest {
/** @var bool Whether to stop after one orchestration turn. */
private bool $single_turn;

/** @var AgentWorkspaceScope|null Workspace scope for persistence/audit adapters. */
private ?AgentWorkspaceScope $workspace;

/**
* @param array $messages Initial conversation messages.
* @param array $tools Runtime tool declarations available to the run.
Expand All @@ -48,6 +52,7 @@ class AgentConversationRequest {
* @param array<string, mixed> $metadata Caller-owned metadata.
* @param int $max_turns Maximum conversation turns.
* @param bool $single_turn Single-turn orchestration flag.
* @param AgentWorkspaceScope|null $workspace Workspace scope for persistence/audit adapters.
*/
public function __construct(
array $messages,
Expand All @@ -56,7 +61,8 @@ public function __construct(
array $runtime_context = array(),
array $metadata = array(),
int $max_turns = self::DEFAULT_MAX_TURNS,
bool $single_turn = false
bool $single_turn = false,
?AgentWorkspaceScope $workspace = null
) {
$this->messages = AgentMessageEnvelope::normalize_many( $messages );
$this->tools = self::normalize_tools( $tools );
Expand All @@ -65,6 +71,7 @@ public function __construct(
$this->metadata = self::normalize_json_array( $metadata, 'metadata' );
$this->max_turns = max( 1, $max_turns );
$this->single_turn = $single_turn;
$this->workspace = $workspace;
}

/** @return array<int, array<string, mixed>> Initial conversation messages. */
Expand Down Expand Up @@ -102,6 +109,11 @@ public function singleTurn(): bool {
return $this->single_turn;
}

/** @return AgentWorkspaceScope|null Workspace scope for persistence/audit adapters. */
public function workspace(): ?AgentWorkspaceScope {
return $this->workspace;
}

/**
* Return a normalized array representation.
*
Expand All @@ -116,6 +128,7 @@ public function to_array(): array {
'metadata' => $this->metadata,
'max_turns' => $this->max_turns,
'single_turn' => $this->single_turn,
'workspace' => $this->workspace ? $this->workspace->to_array() : null,
);
}

Expand Down
34 changes: 19 additions & 15 deletions src/Transcripts/ConversationTranscriptStoreInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,29 @@

namespace AgentsAPI\Core\Database\Chat;

use AgentsAPI\Core\Workspace\AgentWorkspaceScope;

defined( 'ABSPATH' ) || exit;

interface ConversationTranscriptStoreInterface {

/**
* Create a new conversation transcript session and return its ID.
*
* @param int $user_id WordPress user ID owning the session.
* @param int $agent_id Agent ID (0 = legacy agent-less session).
* @param array $metadata Arbitrary session metadata (JSON-serializable).
* @param string $context Execution mode ('chat', 'pipeline', 'system').
* @param AgentWorkspaceScope $workspace Workspace owning the session.
* @param int $user_id WordPress user ID owning the session.
* @param int $agent_id Agent ID (0 = agent-less session).
* @param array $metadata Arbitrary session metadata (JSON-serializable).
* @param string $context Execution mode ('chat', 'pipeline', 'system').
* @return string Session ID (UUIDv4), or empty string on failure.
*/
public function create_session( int $user_id, int $agent_id = 0, array $metadata = array(), string $context = 'chat' ): string;
public function create_session( AgentWorkspaceScope $workspace, int $user_id, int $agent_id = 0, array $metadata = array(), string $context = 'chat' ): string;

/**
* Retrieve a transcript session by ID.
*
* Returns the session as an associative array with keys:
* session_id, user_id, agent_id, title, messages (decoded array),
* session_id, workspace_type, workspace_id, user_id, agent_id, title, messages (decoded array),
* metadata (decoded array), provider, model, context/mode, created_at,
* updated_at, last_read_at, expires_at.
*
Expand Down Expand Up @@ -64,18 +67,19 @@ public function delete_session( string $session_id ): bool;
/**
* Find a recent pending session for deduplication after request timeouts.
*
* Returns the most recent session that belongs to $user_id, was created
* within $seconds, and is either empty or actively processing. Used by
* the orchestrator to avoid duplicate sessions when a timeout triggers a
* client retry while PHP keeps executing.
* Returns the most recent session that belongs to $workspace and $user_id,
* was created within $seconds, and is either empty or actively processing.
* Used by the orchestrator to avoid duplicate sessions when a timeout
* triggers a client retry while PHP keeps executing.
*
* @param int $user_id WordPress user ID.
* @param int $seconds Lookback window (default 600 = 10 minutes).
* @param string $context Context filter.
* @param int|null $token_id Optional token ID for login-scoped dedup.
* @param AgentWorkspaceScope $workspace Workspace owning the session.
* @param int $user_id WordPress user ID.
* @param int $seconds Lookback window (default 600 = 10 minutes).
* @param string $context Context filter.
* @param int|null $token_id Optional token ID for login-scoped dedup.
* @return array|null Session data or null if none.
*/
public function get_recent_pending_session( int $user_id, int $seconds = 600, string $context = 'chat', ?int $token_id = null ): ?array;
public function get_recent_pending_session( AgentWorkspaceScope $workspace, int $user_id, int $seconds = 600, string $context = 'chat', ?int $token_id = null ): ?array;

/**
* Set a transcript session's stored display title.
Expand Down
Loading
Loading