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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Agents API sits between tool/action discovery and product-specific automation. I
- Session and persistence contracts where they are provider-neutral.
- Retrieved context authority vocabulary, context item shape, and conflict resolution contracts.
- Workflow spec value object, structural validator, in-memory registry, abstract runner with `ability` and `agent` step types, `Store` and `Run_Recorder` interfaces, optional Action Scheduler bridge, and three canonical abilities (`agents/run-workflow`, `agents/validate-workflow`, `agents/describe-workflow`).
- Runtime package workflow execution request/result contracts and the canonical dispatcher ability (`agents/run-runtime-package`) for consumer-owned package materialization and execution adapters.

## What Agents API Does Not Own

Expand All @@ -58,6 +59,7 @@ Agents API sits between tool/action discovery and product-specific automation. I
- Product CLI commands beyond generic substrate needs.
- Public REST controllers in v1 unless they are separately designed.
- Product runner adapters that assemble prompts, choose concrete tools, materialize storage, or decide product policy.
- Concrete runtime package materialization, package source checkout, sandbox provisioning, provider mapping, run polling, or evidence artifact upload. The package run contract only defines the request/result envelope and dispatcher seam.
- Concrete tool execution adapters, prompt assembly policy, or product storage/materialization policy.
- Product-specific consent UX, support routing, escalation targets, or transcript-sharing policy.
- Concrete memory retrieval, file projection, convention-path writing, or filesystem layout adapters.
Expand Down Expand Up @@ -188,6 +190,8 @@ wp_register_agent(
- `AgentsAPI\AI\WP_Agent_Byte_Limit_Tool_Result_Truncator`
- `AgentsAPI\AI\WP_Agent_Conversation_Result`
- `AgentsAPI\AI\WP_Agent_Chat_Run_Control`
- `AgentsAPI\AI\WP_Agent_Runtime_Package_Run_Request`
- `AgentsAPI\AI\WP_Agent_Runtime_Package_Run_Result`
- `AgentsAPI\AI\WP_Agent_Conversation_Loop`
- `WP_Agent_Consent_Policy`
- `WP_Agent_Default_Consent_Policy`
Expand All @@ -214,6 +218,7 @@ wp_register_agent(
- `AgentsAPI\AI\Tools\WP_Agent_Tool_Execution_Core`
- `AgentsAPI\AI\Tools\WP_Agent_Tool_Result`
- `agents/ability-search` / `agents/ability-call`
- `agents/run-runtime-package`
- `agents/chat` / `agents/get-chat-run` / `agents/cancel-chat-run` / `agents/queue-chat-message`
- `AgentsAPI\AI\Approvals\WP_Agent_Approval_Decision`
- `AgentsAPI\AI\Approvals\WP_Agent_Pending_Action`
Expand Down
3 changes: 3 additions & 0 deletions agents-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-option-run-control-store.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-run-control.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-run-outcome.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-runtime-package-run-request.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-runtime-package-run-result.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-conversation-result.php';
require_once AGENTS_API_PATH . 'src/Runtime/class-wp-agent-chat-run-control.php';
require_once AGENTS_API_PATH . 'src/Tasks/class-wp-agent-task-run-control.php';
Expand Down Expand Up @@ -214,6 +216,7 @@
require_once AGENTS_API_PATH . 'src/Channels/class-wp-agent-channel.php';
require_once AGENTS_API_PATH . 'src/Channels/register-agents-chat-ability.php';
require_once AGENTS_API_PATH . 'src/Channels/register-agents-chat-run-control-abilities.php';
require_once AGENTS_API_PATH . 'src/Runtime/register-runtime-package-run-ability.php';
require_once AGENTS_API_PATH . 'src/Tasks/register-agents-task-abilities.php';
require_once AGENTS_API_PATH . 'src/Channels/register-frontend-chat-rest-route.php';
require_once AGENTS_API_PATH . 'src/Channels/register-agents-chat-jsonrpc-route.php';
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"php tests/run-outcome-status-smoke.php",
"php tests/iteration-budget-smoke.php",
"php tests/conversation-loop-budgets-smoke.php",
"php tests/runtime-package-run-contract-smoke.php",
"php tests/channels-smoke.php",
"php tests/chat-run-control-smoke.php",
"php tests/task-execution-smoke.php",
Expand Down
137 changes: 137 additions & 0 deletions src/Runtime/class-wp-agent-runtime-package-run-request.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php
/**
* Runtime package run request value object.
*
* @package AgentsAPI
*/

namespace AgentsAPI\AI;

defined( 'ABSPATH' ) || exit;

/**
* Immutable, JSON-friendly request for running a portable agent package.
*
* Agents API defines the neutral contract; consumers provide package loading,
* workflow materialization, storage, and execution adapters.
*
* @since 0.3.0
*/
final class WP_Agent_Runtime_Package_Run_Request {

/**
* @param array<string,mixed> $package Portable package descriptor.
* @param array<string,mixed> $workflow Workflow selector/spec.
* @param array<string,mixed> $input Runtime input supplied to the workflow.
* @param array<string,mixed> $options Execution options such as budgets.
* @param array<string,mixed> $metadata Caller metadata for observability.
* @param array<string,mixed> $replay Replay/materialization hints.
*/
public function __construct(
private array $package,
private array $workflow,
private array $input = array(),
private array $options = array(),
private array $metadata = array(),
private array $replay = array()
) {}

/**
* Build from the canonical ability/filter input.
*
* @param array<mixed> $input Raw input.
* @return self|\WP_Error
*/
public static function from_array( array $input ) {
$package = self::array_value( $input['package'] ?? array() );
$workflow = self::array_value( $input['workflow'] ?? array() );

$package_source = self::string_value( $package['source'] ?? '' );
$package_slug = self::string_value( $package['slug'] ?? $package['id'] ?? '' );
if ( '' === $package_source && '' === $package_slug ) {
return new \WP_Error(
'agents_runtime_package_run_missing_package',
'Runtime package run requests require package.source or package.slug.'
);
}

$workflow_id = self::string_value( $workflow['id'] ?? '' );
$workflow_spec = self::array_value( $workflow['spec'] ?? array() );
if ( '' === $workflow_id && empty( $workflow_spec ) ) {
return new \WP_Error(
'agents_runtime_package_run_missing_workflow',
'Runtime package run requests require workflow.id or workflow.spec.'
);
}

return new self(
$package,
$workflow,
self::array_value( $input['input'] ?? array() ),
self::array_value( $input['options'] ?? array() ),
self::array_value( $input['metadata'] ?? array() ),
self::array_value( $input['replay'] ?? array() )
);
}

/** @return array<string,mixed> */
public function get_package(): array {
return $this->package;
}

/** @return array<string,mixed> */
public function get_workflow(): array {
return $this->workflow;
}

/** @return array<string,mixed> */
public function get_input(): array {
return $this->input;
}

/** @return array<string,mixed> */
public function get_options(): array {
return $this->options;
}

/** @return array<string,mixed> */
public function get_metadata(): array {
return $this->metadata;
}

/** @return array<string,mixed> */
public function get_replay(): array {
return $this->replay;
}

/** @return array<string,mixed> */
public function to_array(): array {
return array(
'package' => $this->package,
'workflow' => $this->workflow,
'input' => $this->input,
'options' => $this->options,
'metadata' => $this->metadata,
'replay' => $this->replay,
);
}

private static function string_value( mixed $value ): string {
return is_scalar( $value ) ? trim( (string) $value ) : '';
}

/** @return array<string,mixed> */
private static function array_value( mixed $value ): array {
if ( ! is_array( $value ) ) {
return array();
}

$normalized = array();
foreach ( $value as $key => $item ) {
if ( is_string( $key ) ) {
$normalized[ $key ] = $item;
}
}
return $normalized;
}
}
154 changes: 154 additions & 0 deletions src/Runtime/class-wp-agent-runtime-package-run-result.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php
/**
* Runtime package run result value object.
*
* @package AgentsAPI
*/

namespace AgentsAPI\AI;

defined( 'ABSPATH' ) || exit;

/**
* Normalized status/result/evidence envelope for portable package runs.
*
* @since 0.3.0
*/
final class WP_Agent_Runtime_Package_Run_Result {

public const STATUS_PENDING = 'pending';
public const STATUS_RUNNING = 'running';
public const STATUS_SUCCEEDED = 'succeeded';
public const STATUS_FAILED = 'failed';
public const STATUS_CANCELLED = 'cancelled';
public const STATUS_SKIPPED = 'skipped';

/**
* @param array<string,mixed> $result Consumer-defined output.
* @param array<string,mixed> $error Stable error envelope.
* @param array<int,array<string,mixed>> $evidence_refs Neutral artifact/log refs.
* @param array<string,mixed> $metadata Host/runtime metadata.
* @param array<string,mixed> $replay Replay/materialization metadata.
*/
public function __construct(
private string $status,
private string $run_id = '',
private array $result = array(),
private array $error = array(),
private array $evidence_refs = array(),
private array $metadata = array(),
private array $replay = array()
) {}

/**
* @param array<mixed> $value Raw handler result.
*/
public static function from_array( array $value ): self {
$status = self::string_value( $value['status'] ?? '' );
if ( ! in_array( $status, self::statuses(), true ) ) {
$status = self::STATUS_SUCCEEDED;
}

return new self(
$status,
self::string_value( $value['run_id'] ?? '' ),
self::array_value( $value['result'] ?? array() ),
self::array_value( $value['error'] ?? array() ),
self::list_of_arrays( $value['evidence_refs'] ?? array() ),
self::array_value( $value['metadata'] ?? array() ),
self::array_value( $value['replay'] ?? array() )
);
}

/** @return array<int,string> */
public static function statuses(): array {
return array(
self::STATUS_PENDING,
self::STATUS_RUNNING,
self::STATUS_SUCCEEDED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
self::STATUS_SKIPPED,
);
}

public function get_status(): string {
return $this->status;
}

public function get_run_id(): string {
return $this->run_id;
}

/** @return array<string,mixed> */
public function get_result(): array {
return $this->result;
}

/** @return array<string,mixed> */
public function get_error(): array {
return $this->error;
}

/** @return array<int,array<string,mixed>> */
public function get_evidence_refs(): array {
return $this->evidence_refs;
}

/** @return array<string,mixed> */
public function get_metadata(): array {
return $this->metadata;
}

/** @return array<string,mixed> */
public function get_replay(): array {
return $this->replay;
}

/** @return array<string,mixed> */
public function to_array(): array {
return array(
'status' => $this->status,
'run_id' => $this->run_id,
'result' => $this->result,
'error' => $this->error,
'evidence_refs' => $this->evidence_refs,
'metadata' => $this->metadata,
'replay' => $this->replay,
);
}

private static function string_value( mixed $value ): string {
return is_scalar( $value ) ? trim( (string) $value ) : '';
}

/** @return array<string,mixed> */
private static function array_value( mixed $value ): array {
if ( ! is_array( $value ) ) {
return array();
}

$normalized = array();
foreach ( $value as $key => $item ) {
if ( is_string( $key ) ) {
$normalized[ $key ] = $item;
}
}
return $normalized;
}

/** @return array<int,array<string,mixed>> */
private static function list_of_arrays( mixed $value ): array {
if ( ! is_array( $value ) ) {
return array();
}

$items = array();
foreach ( $value as $item ) {
if ( is_array( $item ) ) {
$items[] = self::array_value( $item );
}
}
return $items;
}
}
Loading