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
43 changes: 43 additions & 0 deletions docs/runtime-and-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,55 @@ $tool = AgentsAPI\AI\Tools\WP_Agent_Tool_Declaration::normalize(
),
'executor' => 'client',
'scope' => 'run',
'runtime' => array(
'duplicate_policy' => 'repeatable',
'completion_signal' => 'progress',
),
)
);
```

Invalid declarations produce machine-readable invalid field names through `validate()` or an `InvalidArgumentException` from `normalize()`.

### Tool runtime metadata

Tool declarations and tool results may include optional `runtime` metadata. This metadata is product-neutral execution policy data for the agent loop and host runtime; it is not a concrete tool implementation detail and should not encode product-specific tool names.

Canonical keys:

| Key | Value | Meaning |
| --- | --- | --- |
| `duplicate_policy` | `repeatable` | The same tool may be called repeatedly with the same parameters when the host considers that safe. |
| `completion_signal` | `progress` | A successful tool result is progress toward completion and may be used by caller-owned completion policy. |

The substrate treats `runtime` as a JSON-friendly associative array. It preserves scalar and nested array values with string keys, drops unsupported values, and leaves product-specific interpretation to callers.

When the conversation loop mediates tool calls, declaration runtime metadata is propagated into the normalized tool result and exposed on the corresponding `tool_execution_results[]` entry:

```php
array(
'tool_name' => 'client/search_docs',
'tool_call_id' => 'call_123',
'parameters' => array( 'query' => 'runtime metadata' ),
'result' => array(
'success' => true,
'tool_name' => 'client/search_docs',
'result' => array( 'matches' => array() ),
'runtime' => array(
'duplicate_policy' => 'repeatable',
'completion_signal' => 'progress',
),
),
'runtime' => array(
'duplicate_policy' => 'repeatable',
'completion_signal' => 'progress',
),
'turn_count' => 1,
)
```

If a concrete executor returns its own `runtime` metadata, the normalized result merges it over declaration metadata for that execution result. This lets the declaration advertise generic defaults while an executor refines result-scoped signals without changing the declaration.

## Tool execution core

`WP_Agent_Tool_Execution_Core` mediates calls without owning any concrete tool implementation.
Expand Down
9 changes: 8 additions & 1 deletion src/Runtime/class-wp-agent-conversation-loop.php
Original file line number Diff line number Diff line change
Expand Up @@ -391,14 +391,21 @@ private static function mediate_tool_calls(
) );

// Build the tool_execution_results entry.
$tool_execution_results[] = array(
$execution_result = array(
'tool_name' => $tool_name,
'tool_call_id' => $tool_call_id,
'result' => $exec_result,
'parameters' => is_array( $parameters ) ? $parameters : array(),
'turn_count' => $turn,
);

$runtime = isset( $exec_result['runtime'] ) && is_array( $exec_result['runtime'] ) ? $exec_result['runtime'] : array();
if ( ! empty( $runtime ) ) {
$execution_result['runtime'] = $runtime;
}

$tool_execution_results[] = $execution_result;

$tool_audit_events[] = self::tool_audit_event(
$tool_name,
is_array( $parameters ) ? $parameters : array(),
Expand Down
4 changes: 4 additions & 0 deletions src/Runtime/class-wp-agent-conversation-result.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ public static function normalize( array $result ): array {
throw self::invalid( $path . '.is_handler_tool', 'must be a boolean when present' );
}

if ( array_key_exists( 'runtime', $tool_result ) && ! is_array( $tool_result['runtime'] ) ) {
throw self::invalid( $path . '.runtime', 'must be an array when present' );
}

if ( ! array_key_exists( 'turn_count', $tool_result ) || ! is_int( $tool_result['turn_count'] ) ) {
throw self::invalid( $path . '.turn_count', 'must be an integer' );
}
Expand Down
88 changes: 87 additions & 1 deletion src/Tools/class-wp-agent-tool-declaration.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ class WP_Agent_Tool_Declaration {
public const EXECUTOR_CLIENT = 'client';
public const SCOPE_RUN = 'run';

/**
* Generic runtime metadata key for duplicate-call behavior.
*/
public const RUNTIME_DUPLICATE_POLICY = 'duplicate_policy';

/**
* Generic runtime metadata key for progress/completion signaling.
*/
public const RUNTIME_COMPLETION_SIGNAL = 'completion_signal';

/**
* Normalize a runtime tool declaration or throw a field-scoped error.
*
Expand All @@ -45,14 +55,21 @@ public static function normalize( array $declaration ): array {
$name = (string) $declaration['name'];
$source = self::sourceFromName( $name );

return array(
$normalized = array(
'name' => $name,
'source' => $source,
'description' => trim( (string) $declaration['description'] ),
'parameters' => $declaration['parameters'] ?? array(),
'executor' => self::EXECUTOR_CLIENT,
'scope' => self::SCOPE_RUN,
);

$runtime = self::normalizeRuntimeMetadata( $declaration['runtime'] ?? array() );
if ( ! empty( $runtime ) ) {
$normalized['runtime'] = $runtime;
}

return $normalized;
}

/**
Expand Down Expand Up @@ -105,9 +122,78 @@ public static function validate( array $declaration ): array {
$errors[] = 'scope';
}

if ( isset( $declaration['runtime'] ) && ! is_array( $declaration['runtime'] ) ) {
$errors[] = 'runtime';
}

return array_values( array_unique( $errors ) );
}

/**
* Normalize optional product-neutral runtime metadata.
*
* Runtime metadata is a JSON-friendly object used by agent loops and hosts to
* make generic execution decisions without hardcoding product tool names. The
* canonical keys are `duplicate_policy` and `completion_signal`, but callers
* may include additional product-neutral scalar/list values for future policy.
*
* @param mixed $runtime Raw runtime metadata.
* @return array<string, mixed> Normalized runtime metadata.
*/
public static function normalizeRuntimeMetadata( $runtime ): array {
if ( ! is_array( $runtime ) ) {
return array();
}

$normalized = array();
foreach ( $runtime as $key => $value ) {
if ( ! is_string( $key ) || '' === $key ) {
continue;
}

$normalized_value = self::normalizeRuntimeMetadataValue( $value );
if ( null === $normalized_value ) {
continue;
}

$normalized[ $key ] = $normalized_value;
}

return $normalized;
}

/**
* Normalize one JSON-friendly runtime metadata value.
*
* @param mixed $value Raw metadata value.
* @return mixed|null Normalized value, or null when unsupported.
*/
private static function normalizeRuntimeMetadataValue( $value ) {
if ( is_string( $value ) || is_int( $value ) || is_float( $value ) || is_bool( $value ) ) {
return $value;
}

if ( ! is_array( $value ) ) {
return null;
}

$normalized = array();
foreach ( $value as $key => $item ) {
$normalized_item = self::normalizeRuntimeMetadataValue( $item );
if ( null === $normalized_item ) {
continue;
}

if ( is_string( $key ) ) {
$normalized[ $key ] = $normalized_item;
} else {
$normalized[] = $normalized_item;
}
}

return $normalized;
}

/**
* Build a namespaced runtime tool name.
*
Expand Down
20 changes: 18 additions & 2 deletions src/Tools/class-wp-agent-tool-execution-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public function prepareWP_Agent_Tool_Call( string $tool_name, array $tool_parame
);
}

$runtime = WP_Agent_Tool_Declaration::normalizeRuntimeMetadata( $tool_definition['runtime'] ?? array() );

$parameters = WP_Agent_Tool_Parameters::buildParameters( $tool_parameters, $context, $tool_definition );
$validation = WP_Agent_Tool_Parameters::validateRequiredParameters( $parameters, $tool_definition );
if ( ! $validation['valid'] ) {
Expand All @@ -45,7 +47,8 @@ public function prepareWP_Agent_Tool_Call( string $tool_name, array $tool_parame
array(
'error_type' => 'missing_required_parameters',
'missing_parameters' => $validation['missing'],
)
),
$runtime
)
);
}
Expand Down Expand Up @@ -84,9 +87,19 @@ public function executePreparedTool( array $tool_call, array $tool_definition, W
try {
$result = $executor->executeWP_Agent_Tool_Call( $tool_call, $tool_definition, $context );
} catch ( \Throwable $throwable ) {
return WP_Agent_Tool_Result::error( $tool_call['tool_name'], $throwable->getMessage(), array( 'error_type' => 'executor_exception' ) );
return WP_Agent_Tool_Result::error(
$tool_call['tool_name'],
$throwable->getMessage(),
array( 'error_type' => 'executor_exception' ),
WP_Agent_Tool_Declaration::normalizeRuntimeMetadata( $tool_definition['runtime'] ?? array() )
);
}

$runtime = array_merge(
WP_Agent_Tool_Declaration::normalizeRuntimeMetadata( $tool_definition['runtime'] ?? array() ),
WP_Agent_Tool_Declaration::normalizeRuntimeMetadata( $result['runtime'] ?? array() )
);

if ( ! array_key_exists( 'success', $result ) ) {
$result = array(
'success' => true,
Expand All @@ -96,6 +109,9 @@ public function executePreparedTool( array $tool_call, array $tool_definition, W
}

$result['tool_name'] = is_string( $result['tool_name'] ?? null ) ? $result['tool_name'] : $tool_call['tool_name'];
if ( ! empty( $runtime ) ) {
$result['runtime'] = $runtime;
}

return WP_Agent_Tool_Result::normalize( $result );
}
Expand Down
12 changes: 10 additions & 2 deletions src/Tools/class-wp-agent-tool-result.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ class WP_Agent_Tool_Result {
* @param array $metadata Optional result metadata.
* @return array<string, mixed>
*/
public static function success( string $tool_name, $result, array $metadata = array() ): array {
public static function success( string $tool_name, $result, array $metadata = array(), array $runtime = array() ): array {
return self::normalize(
array(
'success' => true,
'tool_name' => $tool_name,
'result' => $result,
'metadata' => $metadata,
'runtime' => $runtime,
)
);
}
Expand All @@ -41,13 +42,14 @@ public static function success( string $tool_name, $result, array $metadata = ar
* @param array $metadata Optional result metadata.
* @return array<string, mixed>
*/
public static function error( string $tool_name, string $error, array $metadata = array() ): array {
public static function error( string $tool_name, string $error, array $metadata = array(), array $runtime = array() ): array {
return self::normalize(
array(
'success' => false,
'tool_name' => $tool_name,
'error' => $error,
'metadata' => $metadata,
'runtime' => $runtime,
)
);
}
Expand All @@ -70,12 +72,18 @@ public static function normalize( array $result ): array {
$metadata = array();
}

$runtime = WP_Agent_Tool_Declaration::normalizeRuntimeMetadata( $result['runtime'] ?? array() );

$normalized = array(
'success' => $success,
'tool_name' => $tool_name,
'metadata' => $metadata,
);

if ( ! empty( $runtime ) ) {
$normalized['runtime'] = $runtime;
}

if ( $success ) {
$normalized['result'] = $result['result'] ?? array();
return $normalized;
Expand Down
6 changes: 6 additions & 0 deletions tests/conversation-loop-tool-execution-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public function executeWP_Agent_Tool_Call( array $tool_call, array $tool_definit
),
'executor' => 'client',
'scope' => 'run',
'runtime' => array(
'duplicate_policy' => 'repeatable',
'completion_signal' => 'progress',
),
),
);

Expand Down Expand Up @@ -88,6 +92,8 @@ static function ( array $messages, array $context ): array {
agents_api_smoke_assert_equals( 'client/summarize', $result['tool_execution_results'][0]['tool_name'], 'tool execution result has correct tool name', $failures, $passes );
agents_api_smoke_assert_equals( 'call_123', $result['tool_execution_results'][0]['tool_call_id'] ?? '', 'tool execution result preserves provider tool call id', $failures, $passes );
agents_api_smoke_assert_equals( 'HELLO WORLD', $result['tool_execution_results'][0]['result']['result']['summary'], 'tool execution result carries executor payload', $failures, $passes );
agents_api_smoke_assert_equals( 'repeatable', $result['tool_execution_results'][0]['runtime']['duplicate_policy'] ?? '', 'tool execution result exposes duplicate policy runtime metadata', $failures, $passes );
agents_api_smoke_assert_equals( 'progress', $result['tool_execution_results'][0]['result']['runtime']['completion_signal'] ?? '', 'tool result preserves completion signal runtime metadata', $failures, $passes );
agents_api_smoke_assert_equals( 1, count( $result['tool_audit_events'] ), 'result contains one tool audit event', $failures, $passes );
agents_api_smoke_assert_equals( 'tool_call', $result['tool_audit_events'][0]['type'], 'tool audit event has stable type', $failures, $passes );
agents_api_smoke_assert_equals( 'client/summarize', $result['tool_audit_events'][0]['tool_name'], 'tool audit event has correct tool name', $failures, $passes );
Expand Down
39 changes: 39 additions & 0 deletions tests/tool-runtime-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,19 @@
),
'executor' => 'client',
'scope' => 'run',
'runtime' => array(
'duplicate_policy' => 'repeatable',
'completion_signal' => 'progress',
'unsupported' => new stdClass(),
),
)
);
agents_api_smoke_assert_equals( 'client/choose_post', $declaration['name'], 'runtime declaration keeps namespaced name', $failures, $passes );
agents_api_smoke_assert_equals( 'client', $declaration['source'], 'runtime declaration records source', $failures, $passes );
agents_api_smoke_assert_equals( 'run', $declaration['scope'], 'runtime declaration records run scope', $failures, $passes );
agents_api_smoke_assert_equals( 'repeatable', $declaration['runtime']['duplicate_policy'] ?? '', 'runtime declaration preserves duplicate policy metadata', $failures, $passes );
agents_api_smoke_assert_equals( 'progress', $declaration['runtime']['completion_signal'] ?? '', 'runtime declaration preserves completion signal metadata', $failures, $passes );
agents_api_smoke_assert_equals( false, array_key_exists( 'unsupported', $declaration['runtime'] ?? array() ), 'runtime declaration drops unsupported metadata values', $failures, $passes );

$registry = new AgentsAPI\AI\Tools\WP_Agent_Tool_Source_Registry();
$registry->registerSource(
Expand All @@ -57,6 +65,10 @@ static function () {
// this declaration, a `text` key in context never satisfies the
// required parameter — keeps required-arg sourcing auditable.
'client_context_bindings' => array( 'text' ),
'runtime' => array(
'duplicate_policy' => 'repeatable',
'completion_signal' => 'progress',
),
),
);
}
Expand Down Expand Up @@ -199,5 +211,32 @@ public function executeWP_Agent_Tool_Call( array $tool_call, array $tool_definit
agents_api_smoke_assert_equals( 'local/summarize', $result['tool_name'], 'mediated result records tool name', $failures, $passes );
agents_api_smoke_assert_equals( 'HELLO', $result['result']['summary'], 'mediated result carries adapter payload', $failures, $passes );
agents_api_smoke_assert_equals( 'req-123', $result['result']['request_id'], 'adapter receives merged parameters', $failures, $passes );
agents_api_smoke_assert_equals( 'repeatable', $result['runtime']['duplicate_policy'] ?? '', 'mediated result preserves declaration duplicate policy runtime metadata', $failures, $passes );
agents_api_smoke_assert_equals( 'progress', $result['runtime']['completion_signal'] ?? '', 'mediated result preserves declaration completion signal runtime metadata', $failures, $passes );

$result_override_adapter = new class() implements AgentsAPI\AI\Tools\WP_Agent_Tool_Executor {
public function executeWP_Agent_Tool_Call( array $tool_call, array $tool_definition, array $context = array() ): array {
unset( $tool_definition, $context );

return array(
'success' => true,
'tool_name' => $tool_call['tool_name'],
'result' => array( 'summary' => 'OK' ),
'runtime' => array(
'completion_signal' => 'complete',
),
);
}
};

$result_override = $executor->executeTool(
'local/summarize',
array( 'text' => 'hello' ),
$tools,
$result_override_adapter,
array()
);
agents_api_smoke_assert_equals( 'repeatable', $result_override['runtime']['duplicate_policy'] ?? '', 'result runtime keeps declaration metadata when executor adds runtime', $failures, $passes );
agents_api_smoke_assert_equals( 'complete', $result_override['runtime']['completion_signal'] ?? '', 'executor runtime metadata can refine result runtime metadata', $failures, $passes );

agents_api_smoke_finish( 'Agents API tool runtime', $failures, $passes );
Loading