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
34 changes: 32 additions & 2 deletions src/Runtime/class-wp-agent-run-result-envelope.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ final class WP_Agent_Run_Result_Envelope {
* @param array<string,mixed> $error Stable error envelope.
* @param array<string,mixed> $cancellation Cancellation request/result metadata.
* @param array<string,mixed> $metadata Host/runtime metadata.
* @param array<int,array<string,mixed>> $logs Canonical log entries.
*/
public function __construct(
private string $run_id,
Expand All @@ -55,12 +56,14 @@ public function __construct(
private array $timestamps = array(),
private array $error = array(),
private array $cancellation = array(),
private array $metadata = array()
private array $metadata = array(),
private array $logs = array()
) {
$this->status = self::normalize_status( $this->status );
$this->outputs = self::map_value( $this->outputs );
$this->artifact_refs = self::normalize_refs( $this->artifact_refs );
$this->evidence_refs = self::normalize_refs( $this->evidence_refs );
$this->logs = self::normalize_entries( $this->logs );
$this->replay = self::map_value( $this->replay );
$this->provenance = self::map_value( $this->provenance );
$this->timestamps = self::timestamps_value( $this->timestamps );
Expand Down Expand Up @@ -112,7 +115,8 @@ public static function from_array( array $value ): self {
$timestamps,
self::map_value( $value['error'] ?? array() ),
self::map_value( $value['cancellation'] ?? array() ),
self::map_value( $value['metadata'] ?? array() )
self::map_value( $value['metadata'] ?? array() ),
self::normalize_entries( $value['logs'] ?? array() )
);
}

Expand Down Expand Up @@ -179,6 +183,11 @@ public function get_evidence_refs(): array {
return $this->evidence_refs;
}

/** @return array<int,array<string,mixed>> */
public function get_logs(): array {
return $this->logs;
}

/** @return array<string,mixed> */
public function get_replay(): array {
return $this->replay;
Expand Down Expand Up @@ -219,6 +228,7 @@ public function to_array(): array {
'outputs' => $this->outputs,
'artifact_refs' => $this->artifact_refs,
'evidence_refs' => $this->evidence_refs,
'logs' => $this->logs,
'replay' => $this->replay,
'provenance' => $this->provenance,
'timestamps' => $this->timestamps,
Expand Down Expand Up @@ -248,6 +258,26 @@ private static function map_value( mixed $value ): array {
return $map;
}

/**
* @param mixed $value Raw entries.
* @return array<int,array<string,mixed>>
*/
private static function normalize_entries( mixed $value ): array {
if ( ! is_array( $value ) ) {
return array();
}

$entries = array();
foreach ( $value as $entry ) {
$entry = self::map_value( $entry );
if ( array() !== $entry ) {
$entries[] = $entry;
}
}

return $entries;
}

/**
* @param array<string,mixed> $value Raw timestamps.
* @return array<string,mixed>
Expand Down
61 changes: 58 additions & 3 deletions src/Workflows/class-wp-agent-workflow-run-result.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ final class WP_Agent_Workflow_Run_Result {
* @param array<mixed> $metadata Free-form metadata for recorders / tracers (Langfuse trace ids, etc.).
* @param array<mixed> $evidence_refs Neutral JSON-serializable artifact/log references owned by the host.
* @param array<mixed> $replay_metadata Deterministic metadata needed to replay or audit this run.
* @param array<mixed> $artifacts Normalized artifact descriptors produced by this run.
* @param array<mixed> $logs Normalized log entries produced by this run.
*/
public function __construct(
private string $run_id,
Expand All @@ -71,8 +73,13 @@ public function __construct(
private int $ended_at,
private array $metadata,
private array $evidence_refs = array(),
private array $replay_metadata = array()
) {}
private array $replay_metadata = array(),
private array $artifacts = array(),
private array $logs = array()
) {
$this->artifacts = self::normalized_list( $this->artifacts );
$this->logs = self::normalized_list( $this->logs );
}

/**
* @param array<mixed> $inputs
Expand Down Expand Up @@ -102,7 +109,9 @@ public static function from_array( array $value ): self {
self::int_value( $value['ended_at'] ?? 0 ),
self::array_value( $value['metadata'] ?? array() ),
self::array_value( $value['evidence_refs'] ?? array() ),
self::array_value( $value['replay'] ?? array() )
self::array_value( $value['replay'] ?? array() ),
self::array_value( $value['artifacts'] ?? array() ),
self::array_value( $value['logs'] ?? array() )
);
}

Expand Down Expand Up @@ -175,6 +184,20 @@ public function get_replay_metadata(): array {
return $this->replay_metadata;
}

/**
* @return array<mixed>
*/
public function get_artifacts(): array {
return $this->artifacts;
}

/**
* @return array<mixed>
*/
public function get_logs(): array {
return $this->logs;
}

public function is_succeeded(): bool {
return self::STATUS_SUCCEEDED === $this->status;
}
Expand Down Expand Up @@ -225,7 +248,9 @@ public function to_run_result_envelope(): WP_Agent_Run_Result_Envelope {
'run_id' => $this->run_id,
'status' => $this->status,
'outputs' => $this->output,
'artifact_refs' => $this->artifacts,
'evidence_refs' => $this->evidence_refs,
'logs' => $this->logs,
'replay' => $this->replay_metadata,
'provenance' => array( 'workflow_id' => $this->workflow_id ),
'timestamps' => array(
Expand Down Expand Up @@ -268,6 +293,8 @@ public function with( array $patch ): self {
self::array_patch_value( $patch, 'metadata', $this->metadata ),
self::array_patch_value( $patch, 'evidence_refs', $this->evidence_refs ),
self::array_patch_value( $patch, 'replay', $this->replay_metadata ),
self::array_patch_value( $patch, 'artifacts', $this->artifacts ),
self::array_patch_value( $patch, 'logs', $this->logs ),
);
}

Expand Down Expand Up @@ -309,6 +336,32 @@ private static function array_value( mixed $value ): array {
return is_array( $value ) ? $value : array();
}

/**
* @param array<mixed> $value
* @return array<int,array<string,mixed>>
*/
private static function normalized_list( array $value ): array {
$normalized = array();
foreach ( $value as $entry ) {
if ( ! is_array( $entry ) ) {
continue;
}

$item = array();
foreach ( $entry as $key => $entry_value ) {
if ( is_string( $key ) ) {
$item[ $key ] = $entry_value;
}
}

if ( array() !== $item ) {
$normalized[] = $item;
}
}

return $normalized;
}

/**
* Render to a plain array. Useful for recorders that want to serialise
* the run record verbatim (CPT meta, JSON column, REST response).
Expand All @@ -331,6 +384,8 @@ public function to_array(): array {
'metadata' => $this->metadata,
'evidence_refs' => $this->evidence_refs,
'replay' => $this->replay_metadata,
'artifacts' => $this->artifacts,
'logs' => $this->logs,
);
}
}
8 changes: 7 additions & 1 deletion src/Workflows/class-wp-agent-workflow-runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,17 @@ public function __construct(
* - `continue_on_error` (bool): keep running after a failed step. Default false.
* - `metadata` (array): forwarded to the run result.
* - `evidence_refs` (array): neutral artifact/log references forwarded to the run result.
* - `artifacts` (array): artifact descriptors forwarded to the run result.
* - `logs` (array): log entries forwarded to the run result.
* @return WP_Agent_Workflow_Run_Result
*/
public function run( WP_Agent_Workflow_Spec $spec, array $inputs = array(), array $options = array() ): WP_Agent_Workflow_Run_Result {
$started_at = time();
$run_id = self::string_value( $options['run_id'] ?? self::generate_run_id() );
$metadata = (array) ( $options['metadata'] ?? array() );
$evidence_refs = (array) ( $options['evidence_refs'] ?? array() );
$artifacts = (array) ( $options['artifacts'] ?? array() );
$logs = (array) ( $options['logs'] ?? array() );
$replay = self::build_replay_metadata( $spec );

// Build the initial RUNNING result and persist via recorder->start()
Expand All @@ -135,7 +139,9 @@ public function run( WP_Agent_Workflow_Spec $spec, array $inputs = array(), arra
0,
$metadata,
$evidence_refs,
$replay
$replay,
$artifacts,
$logs
);

if ( $this->recorder ) {
Expand Down
10 changes: 9 additions & 1 deletion src/Workflows/register-agents-workflow-abilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ function agents_run_workflow_input_schema(): array {
),
'options' => array(
'type' => 'object',
'description' => 'Runtime options forwarded to the runner. Recognized keys: run_id, continue_on_error, metadata, evidence_refs.',
'description' => 'Runtime options forwarded to the runner. Recognized keys: run_id, continue_on_error, metadata, evidence_refs, artifacts, logs.',
'default' => array(),
),
),
Expand Down Expand Up @@ -498,6 +498,14 @@ function agents_run_workflow_output_schema(): array {
'type' => 'array',
'description' => 'Neutral JSON-serializable artifact/log references owned by the host runtime.',
),
'artifacts' => array(
'type' => 'array',
'description' => 'Normalized artifact descriptors produced by the workflow run.',
),
'logs' => array(
'type' => 'array',
'description' => 'Normalized log entries produced by the workflow run.',
),
'replay' => array(
'type' => 'object',
'properties' => array(
Expand Down
4 changes: 4 additions & 0 deletions tests/agents-workflow-ability-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ function smoke_assert( $expected, $actual, string $name, array &$failures, int &
$input_schema = agents_run_workflow_input_schema();
$output_schema = agents_run_workflow_output_schema();
smoke_assert( true, str_contains( $input_schema['properties']['options']['description'] ?? '', 'evidence_refs' ), 'input schema documents evidence_refs runtime option', $failures, $passes );
smoke_assert( true, str_contains( $input_schema['properties']['options']['description'] ?? '', 'artifacts' ), 'input schema documents artifacts runtime option', $failures, $passes );
smoke_assert( true, str_contains( $input_schema['properties']['options']['description'] ?? '', 'logs' ), 'input schema documents logs runtime option', $failures, $passes );
smoke_assert( 'array', $output_schema['properties']['evidence_refs']['type'] ?? '', 'output schema exposes evidence_refs', $failures, $passes );
smoke_assert( 'array', $output_schema['properties']['artifacts']['type'] ?? '', 'output schema exposes artifacts', $failures, $passes );
smoke_assert( 'array', $output_schema['properties']['logs']['type'] ?? '', 'output schema exposes logs', $failures, $passes );
smoke_assert( 'object', $output_schema['properties']['replay']['type'] ?? '', 'output schema exposes replay metadata', $failures, $passes );
smoke_assert( 'integer', $output_schema['properties']['replay']['properties']['run_record_schema_version']['type'] ?? '', 'replay schema includes schema version', $failures, $passes );
smoke_assert( 'string', $output_schema['properties']['replay']['properties']['workflow_spec_hash']['type'] ?? '', 'replay schema includes spec hash', $failures, $passes );
Expand Down
8 changes: 7 additions & 1 deletion tests/run-result-envelope-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
'ignored',
),
'evidence_refs' => array( array( 'type' => 'log', 'label' => 'runner' ) ),
'logs' => array( array( 'level' => 'info', 'message' => 'started' ), 'ignored' ),
'replay' => array( 'seed' => 42 ),
'provenance' => array( 'producer' => 'smoke' ),
'started_at' => '2026-06-19T00:00:00Z',
Expand All @@ -56,6 +57,7 @@
agents_api_smoke_assert_equals( 1, count( $envelope->get_artifact_refs() ), 'artifact refs drop non-array items', $failures, $passes );
agents_api_smoke_assert_equals( 'transcript', $envelope->get_artifact_refs()[0]['label'] ?? '', 'artifact ref string fields trim', $failures, $passes );
agents_api_smoke_assert_equals( 'runner', $envelope->get_evidence_refs()[0]['label'] ?? '', 'evidence refs normalize with same vocabulary', $failures, $passes );
agents_api_smoke_assert_equals( 'started', $envelope->get_logs()[0]['message'] ?? '', 'logs drop non-array items and preserve string-keyed entries', $failures, $passes );
agents_api_smoke_assert_equals( '2026-06-19T00:00:00Z', $envelope->get_timestamps()['started_at'] ?? '', 'top-level started_at folds into timestamps', $failures, $passes );

echo "\n[2] Runtime package results convert without changing legacy arrays:\n";
Expand Down Expand Up @@ -90,13 +92,17 @@
110,
array( 'trace_id' => 'trace-1' ),
array( array( 'type' => 'log', 'label' => 'workflow log' ) ),
array( 'workflow_version' => 2 )
array( 'workflow_version' => 2 ),
array( array( 'type' => 'json', 'label' => 'workflow artifact' ) ),
array( array( 'level' => 'error', 'message' => 'workflow failed' ) )
);
$workflow_envelope = $workflow->to_run_result_envelope();
agents_api_smoke_assert_equals( WP_Agent_Run_Result_Envelope::STATUS_FAILED, $workflow_envelope->get_status(), 'workflow status maps to canonical failed', $failures, $passes );
agents_api_smoke_assert_equals( 'build-site', $workflow_envelope->get_provenance()['workflow_id'] ?? '', 'workflow id maps to provenance', $failures, $passes );
agents_api_smoke_assert_equals( 2, $workflow_envelope->get_replay()['workflow_version'] ?? 0, 'workflow replay metadata is preserved', $failures, $passes );
agents_api_smoke_assert_equals( 'workflow log', $workflow_envelope->get_evidence_refs()[0]['label'] ?? '', 'workflow evidence refs are preserved', $failures, $passes );
agents_api_smoke_assert_equals( 'workflow artifact', $workflow_envelope->get_artifact_refs()[0]['label'] ?? '', 'workflow artifacts map to canonical artifact refs', $failures, $passes );
agents_api_smoke_assert_equals( 'workflow failed', $workflow_envelope->get_logs()[0]['message'] ?? '', 'workflow logs map to canonical logs', $failures, $passes );

echo "\n[4] Task run-control arrays convert to the canonical envelope:\n";
$task_envelope = WP_Agent_Task_Run_Control::to_run_result_envelope(
Expand Down
54 changes: 54 additions & 0 deletions tests/workflow-runner-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,60 @@ static function ( array $input ): \WP_Error {
smoke_assert( $spec->to_array(), $last_write['result']['replay']['workflow_spec_snapshot'] ?? array(), 'recorder update preserves replay metadata', $failures, $passes );
smoke_assert( true, is_string( json_encode( $evidence_result->get_evidence_refs() ) ), 'evidence refs are JSON-serializable', $failures, $passes );

// ─── Artifacts and logs stay first-class through results and recorders ─

$artifacts = array(
array(
'type' => 'json',
'id' => 'artifact:123',
'label' => 'Workflow artifact',
),
'ignored',
);
$logs = array(
array(
'level' => 'info',
'message' => 'Workflow run completed.',
),
'ignored',
);
$artifact_metadata = array(
'artifacts' => array( 'legacy metadata artifact stays untouched' ),
'logs' => array( 'legacy metadata log stays untouched' ),
);
$artifact_recorder = new Capture_Recorder();
$artifact_result = ( new WP_Agent_Workflow_Runner( $artifact_recorder ) )->run(
$spec,
array( 'text' => 'artifact' ),
array(
'run_id' => 'artifact-run-1',
'metadata' => $artifact_metadata,
'artifacts' => $artifacts,
'logs' => $logs,
)
);
$artifact_roundtrip = WP_Agent_Workflow_Run_Result::from_array( $artifact_result->to_array() );
$artifact_last = end( $artifact_recorder->writes );
$expected_artifacts = array( $artifacts[0] );
$expected_logs = array( $logs[0] );

smoke_assert( $expected_artifacts, $artifact_result->get_artifacts(), 'result exposes first-class artifacts', $failures, $passes );
smoke_assert( $expected_logs, $artifact_result->get_logs(), 'result exposes first-class logs', $failures, $passes );
smoke_assert( $artifact_metadata, $artifact_result->get_metadata(), 'metadata remains untouched when artifacts/logs are first-class', $failures, $passes );
smoke_assert( $artifact_result->to_array(), $artifact_roundtrip->to_array(), 'artifacts/logs round-trip through to_array/from_array', $failures, $passes );
smoke_assert( $expected_artifacts, $artifact_last['result']['artifacts'] ?? array(), 'recorder update preserves first-class artifacts', $failures, $passes );
smoke_assert( $expected_logs, $artifact_last['result']['logs'] ?? array(), 'recorder update preserves first-class logs', $failures, $passes );

$legacy_result = WP_Agent_Workflow_Run_Result::from_array(
array(
'run_id' => 'legacy-run',
'workflow_id' => 'demo/legacy',
'status' => WP_Agent_Workflow_Run_Result::STATUS_SUCCEEDED,
)
);
smoke_assert( array(), $legacy_result->get_artifacts(), 'legacy result defaults artifacts to empty array', $failures, $passes );
smoke_assert( array(), $legacy_result->get_logs(), 'legacy result defaults logs to empty array', $failures, $passes );

// ─── Failed step short-circuits ───────────────────────────────────────

$bad_spec = WP_Agent_Workflow_Spec::from_array(
Expand Down