diff --git a/src/Tasks/class-wp-agent-task-run-control.php b/src/Tasks/class-wp-agent-task-run-control.php index 72a6d05..a90cd51 100644 --- a/src/Tasks/class-wp-agent-task-run-control.php +++ b/src/Tasks/class-wp-agent-task-run-control.php @@ -101,8 +101,9 @@ public static function normalize_run( array $run ): array { * @param array $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( @@ -110,14 +111,13 @@ public static function to_run_result_envelope( array $run ): WP_Agent_Run_Result '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'], @@ -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 $normalized Normalized run payload. + * @return array + */ + 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 + */ + 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. * diff --git a/tests/run-result-envelope-smoke.php b/tests/run-result-envelope-smoke.php index 7734d9d..069a451 100644 --- a/tests/run-result-envelope-smoke.php +++ b/tests/run-result-envelope-smoke.php @@ -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(