Skip to content
Open
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
7 changes: 4 additions & 3 deletions inc/Core/AbilityResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ public static function normalize( $result ): array {
/**
* Convert a WP_Ability::execute() result into Data Machine's tool result shape.
*
* AI tools use the Agents API execution envelope. The payload key is `result`;
* `data` remains an ability/REST presentation concern, not a mirrored tool
* result field.
*
* @param mixed $result Ability execution result.
* @param string $tool_name Tool name.
* @param string $ability_slug Ability slug.
Expand All @@ -67,8 +71,6 @@ public static function normalize_tool_result( $result, string $tool_name, string
if ( is_array( $result ) ) {
if ( array_key_exists( 'data', $result ) && ! array_key_exists( 'result', $result ) ) {
$result['result'] = $result['data'];
} elseif ( array_key_exists( 'result', $result ) && ! array_key_exists( 'data', $result ) ) {
$result['data'] = $result['result'];
}

return $result;
Expand All @@ -78,7 +80,6 @@ public static function normalize_tool_result( $result, string $tool_name, string
'success' => true,
'tool_name' => $tool_name,
'ability' => $ability_slug,
'data' => $result,
'result' => $result,
);
}
Expand Down
30 changes: 25 additions & 5 deletions inc/Core/Steps/Step.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use DataMachine\Core\DataPacket;
use DataMachine\Core\EngineData;
use DataMachine\Core\StepExecutionResult;

if ( ! defined('ABSPATH') ) {
exit;
Expand Down Expand Up @@ -81,7 +82,7 @@ public function __construct( string $step_type ) {
* Execute step with unified payload handling.
*
* @param array $payload Unified step payload (job_id, flow_step_id, data, flow_step_config)
* @return array Updated data packet array
* @return array Explicit step execution result.
*/
public function execute( array $payload ): array {
try {
Expand All @@ -90,18 +91,18 @@ public function execute( array $payload ): array {

// Validate common configuration
if ( ! $this->validateCommonConfiguration() ) {
return $this->dataPackets;
return $this->failedStepResult( 'invalid_step_configuration' );
}

// Validate step-specific configuration
if ( ! $this->validateStepConfiguration() ) {
return $this->dataPackets;
return $this->failedStepResult( 'invalid_step_configuration' );
}

// Execute step-specific logic
return $this->executeStep();
return StepExecutionResult::fromStepOutput( $this->executeStep(), $this->step_type );
} catch ( \Exception $e ) {
return $this->handleException($e);
return StepExecutionResult::fromStepOutput( $this->handleException( $e ), $this->step_type );
}
}

Expand All @@ -113,6 +114,25 @@ public function execute( array $payload ): array {
*/
abstract protected function executeStep(): array;

/**
* Build an explicit failed execution result for validation failures.
*
* @param string $reason Machine-readable failure reason.
* @return array Explicit step execution result.
*/
protected function failedStepResult( string $reason ): array {
$status = in_array( $this->step_type, array( 'fetch', 'event_import' ), true ) && empty( $this->dataPackets ) ? 'completed_no_items' : 'failed';

return StepExecutionResult::fromStepOutput(
array(
'status' => $status,
'reason' => $reason,
'packets' => $this->dataPackets,
),
$this->step_type
);
}

/**
* Validate step-specific configuration requirements.
* Default implementation checks for a configured handler slug. Override for custom validation.
Expand Down
11 changes: 6 additions & 5 deletions inc/Engine/AI/ConversationManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ConversationManager {
*
* @param string $role Role identifier (user, assistant, system).
* @param string|array $content Message content — string for text, array for multi-modal content blocks.
* @param array $metadata Optional metadata for the message (e.g., type, tool_data, attachments).
* @param array $metadata Optional metadata for the message (e.g., type, tool_result, attachments).
* @return array Message envelope.
*/
public static function buildConversationMessage( string $role, $content, array $metadata = array() ): array {
Expand Down Expand Up @@ -160,12 +160,13 @@ public static function formatToolResultMessage( string $tool_name, array $tool_r
'turn' => $turn_count,
);

if ( ! empty( $tool_result['data'] ) ) {
$payload['tool_data'] = $tool_result['data'];
$result_payload = $tool_result['result'] ?? null;
if ( ! empty( $result_payload ) ) {
$payload['tool_result'] = $result_payload;

// Still append to content for AI context, but frontend can use metadata to hide it
if ( ! $is_handler_tool ) {
$content .= "\n\n" . wp_json_encode( $tool_result['data'] );
$content .= "\n\n" . wp_json_encode( $result_payload );
}
}

Expand Down Expand Up @@ -194,7 +195,7 @@ public static function formatToolResultMessage( string $tool_name, array $tool_r
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Kept for callers that pass original tool params alongside result data.
public static function generateSuccessMessage( string $tool_name, array $tool_result, array $tool_parameters ): string {
$success = $tool_result['success'] ?? false;
$data = $tool_result['data'] ?? array();
$data = is_array( $tool_result['result'] ?? null ) ? $tool_result['result'] : array();

if ( ! $success ) {
$error = $tool_result['error'] ?? 'Unknown error occurred';
Expand Down
2 changes: 1 addition & 1 deletion inc/Engine/AI/DataMachineCompletionAssertions.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ private function resultsHaveOutputField( array $results, string $field ): bool {
return true;
}

if ( is_array( $result['data'] ?? null ) && $this->hasNonEmptyPath( $result['data'], $field ) ) {
if ( is_array( $result['result'] ?? null ) && $this->hasNonEmptyPath( $result['result'], $field ) ) {
return true;
}
}
Expand Down
10 changes: 3 additions & 7 deletions inc/Engine/AI/Tools/Execution/ToolExecutionCore.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,9 @@ private function normalizeDataMachineResult( array $result, string $tool_name, a
}

if ( ! array_key_exists( 'result', $result ) ) {
if ( array_key_exists( 'data', $result ) ) {
$result['result'] = $result['data'];
} else {
$payload = $result;
unset( $payload['success'], $payload['tool_name'], $payload['metadata'], $payload['runtime'] );
$result['result'] = $payload;
}
$payload = $result;
unset( $payload['success'], $payload['tool_name'], $payload['metadata'], $payload['runtime'] );
$result['result'] = $payload;
}

return $result;
Expand Down
10 changes: 5 additions & 5 deletions inc/Engine/AI/Tools/ToolResultFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ public static function projectResultData( array $entry ): array {
* @return array
*/
public static function projectEnvelopeData( array $envelope ): array {
if ( is_array( $envelope['data'] ?? null ) ) {
return $envelope['data'];
}

if ( is_array( $envelope['result'] ?? null ) ) {
return $envelope['result'];
}

if ( is_array( $envelope['data'] ?? null ) ) {
return $envelope['data'];
}

$payload = $envelope;
foreach ( array( 'success', 'error', 'message', 'code', 'tool_name', 'ability', 'executor', 'wp_error_code', 'wp_error_data' ) as $semantic_key ) {
unset( $payload[ $semantic_key ] );
Expand Down Expand Up @@ -89,7 +89,7 @@ public static function projectResultEnvelope( array $entry ): array {
if ( is_array( $metadata['tool_result_data'] ?? null ) ) {
return array(
'success' => (bool) ( $metadata['tool_success'] ?? false ),
'data' => $metadata['tool_result_data'],
'result' => $metadata['tool_result_data'],
);
}

Expand Down
3 changes: 0 additions & 3 deletions inc/Engine/AI/conversation-loop.php
Original file line number Diff line number Diff line change
Expand Up @@ -1369,8 +1369,6 @@ function datamachine_normalize_runtime_tool_result( string $tool_name, $result )
$result['executor'] = $result['executor'] ?? 'client';
if ( array_key_exists( 'data', $result ) && ! array_key_exists( 'result', $result ) ) {
$result['result'] = $result['data'];
} elseif ( array_key_exists( 'result', $result ) && ! array_key_exists( 'data', $result ) ) {
$result['data'] = $result['result'];
}
return $result;
}
Expand All @@ -1379,7 +1377,6 @@ function datamachine_normalize_runtime_tool_result( string $tool_name, $result )
'success' => true,
'tool_name' => $tool_name,
'executor' => 'client',
'data' => $result,
'result' => $result,
);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Core/Steps/AI/AIStepTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ public function test_process_loop_results_does_not_include_input_packets(): void
'tool_execution_results' => array(
array(
'tool_name' => 'upsert_event',
'result' => array( 'success' => true, 'data' => array( 'post_id' => 123 ) ),
'result' => array( 'success' => true, 'result' => array( 'post_id' => 123 ) ),
'parameters' => array( 'title' => 'Test Event' ),
'is_handler_tool' => true,
'turn_count' => 1,
Expand Down
27 changes: 21 additions & 6 deletions tests/Unit/Core/Steps/Upsert/UpsertStepTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ private function buildPayload( array $flow_step_config, array $data_packets = ar
);
}

/**
* Return packets from the explicit Step::execute contract.
*
* @param array $result Step execution result.
* @return array
*/
private function resultPackets( array $result ): array {
return is_array( $result['packets'] ?? null ) ? $result['packets'] : array();
}

public function test_missing_required_handler_tool_sets_explicit_failure_reason(): void {
$step = new UpsertStep();

Expand All @@ -63,8 +73,10 @@ public function test_missing_required_handler_tool_sets_explicit_failure_reason(
)
);

$this->assertNotEmpty( $result );
$last = $result[ array_key_last( $result ) ];
$packets = $this->resultPackets( $result );

$this->assertNotEmpty( $packets );
$last = $packets[ array_key_last( $packets ) ];

$this->assertSame( 'upsert', $last['type'] ?? '' );
$this->assertSame( 'required_handler_tool_not_called', $last['metadata']['failure_reason'] ?? '' );
Expand Down Expand Up @@ -100,11 +112,12 @@ public function test_required_handler_slugs_allows_non_first_handler_when_config
)
);

$this->assertNotEmpty( $result );
$packets = $this->resultPackets( $result );
$this->assertNotEmpty( $packets );

// Find the update packet — it's added alongside the original tool_result.
$update_packet = null;
foreach ( $result as $packet ) {
foreach ( $packets as $packet ) {
if ( ( $packet['type'] ?? '' ) === 'upsert' ) {
$update_packet = $packet;
break;
Expand Down Expand Up @@ -158,8 +171,10 @@ public function test_batch_child_with_missing_handler_produces_real_failure(): v
)
);

$this->assertNotEmpty( $result );
$last = $result[ array_key_last( $result ) ];
$packets = $this->resultPackets( $result );

$this->assertNotEmpty( $packets );
$last = $packets[ array_key_last( $packets ) ];

// Must be a real failure, NOT a fan-out skip packet.
$this->assertSame( 'upsert', $last['type'] ?? '' );
Expand Down
2 changes: 1 addition & 1 deletion tests/ability-result-wp-error-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ function rest_ensure_response( $response ) {
$tool_scalar_result = AbilityResult::normalize_tool_result( 'ok', 'demo_tool', 'datamachine/demo' );
$assert( 'tool scalar result is wrapped as success', true === $tool_scalar_result['success'] );
$assert( 'tool scalar result payload uses tool result key', 'ok' === $tool_scalar_result['result'] );
$assert( 'tool scalar result payload also uses data key', 'ok' === $tool_scalar_result['data'] );
$assert( 'tool scalar result does not mirror payload into data key', ! array_key_exists( 'data', $tool_scalar_result ) );

$legacy_error = AbilityResult::legacy_failure_to_wp_error(
array(
Expand Down
117 changes: 117 additions & 0 deletions tests/step-base-explicit-result-smoke.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php
/**
* Pure-PHP smoke test for the first-party Step::execute result contract.
*
* Run with: php tests/step-base-explicit-result-smoke.php
*
* @package DataMachine\Tests
*/

if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', __DIR__ . '/' );
}

if ( ! function_exists( 'do_action' ) ) {
function do_action( ...$args ): void {
unset( $args );
}
}

if ( ! function_exists( 'sanitize_key' ) ) {
function sanitize_key( string $key ): string {
$key = strtolower( $key );
return preg_replace( '/[^a-z0-9_\-]/', '', $key );
}
}

if ( ! function_exists( 'datamachine_get_engine_data' ) ) {
function datamachine_get_engine_data( int $job_id ): array {
unset( $job_id );
return array();
}
}

require_once __DIR__ . '/../inc/Core/DataPacket.php';
require_once __DIR__ . '/../inc/Core/EngineData.php';
require_once __DIR__ . '/../inc/Core/JobStatus.php';
require_once __DIR__ . '/../inc/Core/StepExecutionResult.php';
require_once __DIR__ . '/../inc/Core/Steps/FlowStepConfig.php';
require_once __DIR__ . '/../inc/Core/Steps/Step.php';

use DataMachine\Core\DataPacket;
use DataMachine\Core\EngineData;
use DataMachine\Core\Steps\Step;

class Step_Base_Explicit_Result_Smoke_Step extends Step {
private array $packets;

public function __construct( string $step_type, array $packets ) {
parent::__construct( $step_type );
$this->packets = $packets;
}

protected function validateStepConfiguration(): bool {
return true;
}

protected function executeStep(): array {
return $this->packets;
}
}

$failures = 0;
$total = 0;

$assert = function ( string $label, bool $condition ) use ( &$failures, &$total ): void {
++$total;
if ( $condition ) {
echo " [PASS] {$label}\n";
return;
}

++$failures;
echo " [FAIL] {$label}\n";
};

$payload = function ( string $step_type ): array {
return array(
'job_id' => 1,
'flow_step_id' => 'step_1',
'data' => array(),
'engine' => new EngineData(
array(
'flow_config' => array(
'step_1' => array(
'step_type' => $step_type,
),
),
),
1
),
);
};

echo "=== step-base-explicit-result-smoke ===\n";

echo "\n[1] first-party step execution returns explicit result shape\n";
$packet = new DataPacket( array( 'body' => 'ok' ), array(), 'source_item' );
$step = new Step_Base_Explicit_Result_Smoke_Step( 'custom', $packet->addTo( array() ) );
$result = $step->execute( $payload( 'custom' ) );

$assert( 'Step::execute returns status', 'succeeded' === ( $result['status'] ?? null ) );
$assert( 'Step::execute returns packets key', isset( $result['packets'] ) && 1 === count( $result['packets'] ) );
$assert( 'Step::execute exposes success boolean', true === ( $result['success'] ?? null ) );

echo "\n[2] fetch empty output is explicit completed_no_items\n";
$step = new Step_Base_Explicit_Result_Smoke_Step( 'fetch', array() );
$result = $step->execute( $payload( 'fetch' ) );

$assert( 'empty fetch output is explicit completed_no_items', 'completed_no_items' === ( $result['status'] ?? null ) );
$assert( 'empty fetch output is not success for downstream routing', false === ( $result['success'] ?? null ) );

if ( $failures > 0 ) {
echo "\n=== step-base-explicit-result-smoke: {$failures} FAILURE(S) / {$total} assertions ===\n";
exit( 1 );
}

echo "\n=== step-base-explicit-result-smoke: ALL PASS ({$total} assertions) ===\n";
Loading
Loading