From 1d7dbcedbbec0f32981947421d5cf7baa2f7e99c Mon Sep 17 00:00:00 2001 From: Miguel Lezama Date: Thu, 6 Aug 2026 17:37:13 -0300 Subject: [PATCH 1/2] Fix: map task run failure into the run-result envelope error field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The task converter was the only run-result envelope producer whose `error` and `cancellation` fields were permanently empty. `to_run_result_envelope()` read `$normalized['error']`, `$normalized['cancellation']`, and `$normalized['evidence_refs']`, but `normalize_run()` whitelists keys and never emits any of those three — so they always fell through to empty defaults. A `STATUS_FAILED` task run stores its failure under `diagnostics` (see `agents_run_task`), which was never mapped to `envelope.error`, so failed task runs carried no error at all. Make the contract honest, mirroring the Runtime/Workflow producers that carry their own error map: - Map a failed run's `diagnostics` (error_code/error_message + raw diagnostics) into the canonical `envelope.error`, guaranteeing failed runs always carry a non-empty error. - Populate `cancellation` from the run status for cancelling/cancelled runs. - Drop the dead `evidence_refs` read (normalize_run has no such source). - Keep successful runs with empty error and empty cancellation. Adds smoke coverage asserting a failed run produces a non-empty error carrying the diagnostics, a cancelled run populates cancellation, and a successful run stays empty. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Tasks/class-wp-agent-task-run-control.php | 67 +++++++++++++++++-- tests/run-result-envelope-smoke.php | 33 +++++++++ 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/src/Tasks/class-wp-agent-task-run-control.php b/src/Tasks/class-wp-agent-task-run-control.php index 72a6d05..7a08ada 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, $normalized ), 'metadata' => self::map_value( $normalized['metadata'] ?? array() ) + array( 'session_id' => $normalized['session_id'], 'executor_id' => $normalized['executor_id'], @@ -152,6 +152,63 @@ 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. + * @param array $normalized Normalized run payload. + * @return array + */ + private static function cancellation_envelope( string $task_status, array $normalized ): array { + if ( ! in_array( $task_status, array( self::STATUS_CANCELLING, self::STATUS_CANCELLED ), true ) ) { + return array(); + } + + $cancellation = array( + 'requested' => true, + 'status' => $task_status, + ); + + $requested_at = self::non_empty_string_value( $normalized['updated_at'] ?? null ); + if ( null !== $requested_at ) { + $cancellation['requested_at'] = $requested_at; + } + + return $cancellation; + } + /** * 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..84d736a 100644 --- a/tests/run-result-envelope-smoke.php +++ b/tests/run-result-envelope-smoke.php @@ -121,6 +121,39 @@ 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( 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 ); + +$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( From 60c70050d6c4172e3b3a87f93cb324e0e57d9204 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Wed, 26 Aug 2026 21:51:11 +0000 Subject: [PATCH 2/2] fix: omit unsupported task cancellation timestamp --- src/Tasks/class-wp-agent-task-run-control.php | 16 ++++------------ tests/run-result-envelope-smoke.php | 12 ++++++++++++ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Tasks/class-wp-agent-task-run-control.php b/src/Tasks/class-wp-agent-task-run-control.php index 7a08ada..a90cd51 100644 --- a/src/Tasks/class-wp-agent-task-run-control.php +++ b/src/Tasks/class-wp-agent-task-run-control.php @@ -117,7 +117,7 @@ public static function to_run_result_envelope( array $run ): WP_Agent_Run_Result 'updated_at' => $normalized['updated_at'] ?? '', ), 'error' => self::error_envelope( $task_status, $normalized ), - 'cancellation' => self::cancellation_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'], @@ -187,26 +187,18 @@ private static function error_envelope( string $task_status, array $normalized ) /** * Build the cancellation envelope for a run whose cancellation was requested. * - * @param string $task_status Normalized task status. - * @param array $normalized Normalized run payload. + * @param string $task_status Normalized task status. * @return array */ - private static function cancellation_envelope( string $task_status, array $normalized ): 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(); } - $cancellation = array( + return array( 'requested' => true, 'status' => $task_status, ); - - $requested_at = self::non_empty_string_value( $normalized['updated_at'] ?? null ); - if ( null !== $requested_at ) { - $cancellation['requested_at'] = $requested_at; - } - - return $cancellation; } /** diff --git a/tests/run-result-envelope-smoke.php b/tests/run-result-envelope-smoke.php index 84d736a..069a451 100644 --- a/tests/run-result-envelope-smoke.php +++ b/tests/run-result-envelope-smoke.php @@ -123,6 +123,7 @@ 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( @@ -143,6 +144,17 @@ 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',