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
59 changes: 54 additions & 5 deletions src/Tasks/class-wp-agent-task-run-control.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,23 +101,23 @@ public static function normalize_run( array $run ): array {
* @param array<string,mixed> $run Raw or normalized run payload.
*/
public static function to_run_result_envelope( array $run ): WP_Agent_Run_Result_Envelope {
$normalized = self::normalize_run( $run );
$status = self::envelope_status( self::string_value( $normalized['status'] ) );
$normalized = self::normalize_run( $run );
$task_status = self::string_value( $normalized['status'] );
$status = self::envelope_status( $task_status );

return WP_Agent_Run_Result_Envelope::from_array(
array(
'run_id' => self::string_value( $normalized['run_id'] ),
'status' => $status,
'outputs' => self::map_value( $normalized['output'] ?? array() ),
'artifact_refs' => WP_Agent_Run_Result_Envelope::normalize_refs( $normalized['artifact_refs'] ?? array() ),
'evidence_refs' => WP_Agent_Run_Result_Envelope::normalize_refs( $normalized['evidence_refs'] ?? array() ),
'provenance' => self::map_value( $normalized['provenance'] ?? array() ),
'timestamps' => array(
'started_at' => $normalized['started_at'] ?? '',
'updated_at' => $normalized['updated_at'] ?? '',
),
'error' => self::map_value( $normalized['error'] ?? array() ),
'cancellation' => self::map_value( $normalized['cancellation'] ?? array() ),
'error' => self::error_envelope( $task_status, $normalized ),
'cancellation' => self::cancellation_envelope( $task_status ),
'metadata' => self::map_value( $normalized['metadata'] ?? array() ) + array(
'session_id' => $normalized['session_id'],
'executor_id' => $normalized['executor_id'],
Expand Down Expand Up @@ -152,6 +152,55 @@ private static function envelope_status( string $status ): string {
return WP_Agent_Run_Result_Envelope::normalize_status( $status );
}

/**
* Build the canonical error envelope for a failed run.
*
* Executors record failure details under `diagnostics` (see agents_run_task),
* so failed runs must surface that as the envelope's stable `error` field —
* mirroring how the workflow/runtime producers carry their own error map.
* Non-failed runs carry an empty error.
*
* @param string $task_status Normalized task status.
* @param array<string,mixed> $normalized Normalized run payload.
* @return array<string,mixed>
*/
private static function error_envelope( string $task_status, array $normalized ): array {
if ( self::STATUS_FAILED !== $task_status ) {
return array();
}

$diagnostics = self::map_value( $normalized['diagnostics'] ?? array() );
$code = self::non_empty_string_value( $diagnostics['error_code'] ?? null );
$message = self::non_empty_string_value( $diagnostics['error_message'] ?? null );

$error = array(
'code' => $code ?? 'agents_task_run_failed',
'message' => $message ?? 'The task run failed.',
);
if ( array() !== $diagnostics ) {
$error['data'] = array( 'diagnostics' => $diagnostics );
}

return $error;
}

/**
* Build the cancellation envelope for a run whose cancellation was requested.
*
* @param string $task_status Normalized task status.
* @return array<string,mixed>
*/
private static function cancellation_envelope( string $task_status ): array {
if ( ! in_array( $task_status, array( self::STATUS_CANCELLING, self::STATUS_CANCELLED ), true ) ) {
return array();
}

return array(
'requested' => true,
'status' => $task_status,
);
}

/**
* Start an addressable task run in the default store.
*
Expand Down
45 changes: 45 additions & 0 deletions tests/run-result-envelope-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,51 @@
agents_api_smoke_assert_equals( WP_Agent_Run_Result_Envelope::STATUS_CANCELLING, $task_envelope->get_status(), 'task cancelling status is shared', $failures, $passes );
agents_api_smoke_assert_equals( 'queued cancel', $task_envelope->get_outputs()['summary'] ?? '', 'task output maps to outputs', $failures, $passes );
agents_api_smoke_assert_equals( 'executor-1', $task_envelope->get_metadata()['executor_id'] ?? '', 'task executor id maps to metadata', $failures, $passes );
agents_api_smoke_assert_equals( true, $task_envelope->get_cancellation()['requested'] ?? false, 'cancelling task run populates cancellation', $failures, $passes );
agents_api_smoke_assert_equals( 'cancelling', $task_envelope->get_cancellation()['status'] ?? '', 'cancellation carries the task status', $failures, $passes );
agents_api_smoke_assert_equals( false, isset( $task_envelope->get_cancellation()['requested_at'] ), 'task updated_at is not treated as the cancellation request time', $failures, $passes );
agents_api_smoke_assert_equals( array(), $task_envelope->get_error(), 'non-failed task run carries empty error', $failures, $passes );

$failed_task_envelope = WP_Agent_Task_Run_Control::to_run_result_envelope(
array(
'run_id' => 'task-run-failed',
'session_id' => 'session-1',
'executor_id' => 'executor-1',
'status' => 'failed',
'diagnostics' => array(
'error_code' => 'executor_boom',
'error_message' => 'Executor exploded.',
),
)
);
agents_api_smoke_assert_equals( WP_Agent_Run_Result_Envelope::STATUS_FAILED, $failed_task_envelope->get_status(), 'failed task status is shared', $failures, $passes );
agents_api_smoke_assert_equals( 'executor_boom', $failed_task_envelope->get_error()['code'] ?? '', 'failed task run maps diagnostics code into envelope error', $failures, $passes );
agents_api_smoke_assert_equals( 'Executor exploded.', $failed_task_envelope->get_error()['message'] ?? '', 'failed task run maps diagnostics message into envelope error', $failures, $passes );
agents_api_smoke_assert_equals( 'executor_boom', $failed_task_envelope->get_error()['data']['diagnostics']['error_code'] ?? '', 'failed task run preserves raw diagnostics under error data', $failures, $passes );
agents_api_smoke_assert_equals( false, array() === $failed_task_envelope->get_error(), 'failed task run never carries an empty error', $failures, $passes );

$failed_task_without_diagnostics = WP_Agent_Task_Run_Control::to_run_result_envelope(
array(
'run_id' => 'task-run-failed-without-diagnostics',
'session_id' => 'session-1',
'executor_id' => 'executor-1',
'status' => 'failed',
)
);
agents_api_smoke_assert_equals( 'agents_task_run_failed', $failed_task_without_diagnostics->get_error()['code'] ?? '', 'failed task run uses a deterministic fallback code without diagnostics', $failures, $passes );
agents_api_smoke_assert_equals( 'The task run failed.', $failed_task_without_diagnostics->get_error()['message'] ?? '', 'failed task run uses a deterministic fallback message without diagnostics', $failures, $passes );

$succeeded_task_envelope = WP_Agent_Task_Run_Control::to_run_result_envelope(
array(
'run_id' => 'task-run-succeeded',
'session_id' => 'session-1',
'executor_id' => 'executor-1',
'status' => 'succeeded',
'output' => array( 'summary' => 'done' ),
)
);
agents_api_smoke_assert_equals( array(), $succeeded_task_envelope->get_error(), 'successful task run carries empty error', $failures, $passes );
agents_api_smoke_assert_equals( array(), $succeeded_task_envelope->get_cancellation(), 'successful task run carries empty cancellation', $failures, $passes );

echo "\n[5] Package adoption results expose canonical envelope refs:\n";
$recorded = new WP_Agent_Package_Installed_Artifact(
Expand Down