Back to feed

sledtools/pika branch #114

pika-cloud-runtime-artifacts

Collapse runtime artifact helpers

Target branch: master

Merge Commit: 316f9ef36dbff0cb2fcb999d86df1d59c9000086

branch: merged tutorial: ready ci: success
Open CI Details

Continuous Integration

CI: success

Compact status on the review page, with full logs on the CI page.

Open CI Details

Latest run #141 success

7 passed

head ed1af051e7b34ef114a537a3024eaf367ccd0672 · queued 2026-03-26 20:50:32 · 7 lane(s)

queued 15s · ran 4m 14s

check-notifications · success check-agent-contracts · success check-pikachat · success check-pikachat-typescript · success check-apple-host-sanity · success check-pikachat-openclaw-e2e · success check-fixture · success

Summary

This branch refactors the pika-cloud runtime artifact helpers by consolidating scattered free functions (encode/decode/load for status, terminal result, and event stream artifacts) into a single unit struct RuntimeArtifacts with associated methods. It also introduces a RuntimeArtifactKind enum to unify the previously duplicated error variants (DecodeStatus, DecodeTerminalResult, DecodeEventLine) into a single Decode variant with a kind field and an optional line number. On the data model side, GuestStartupArtifacts is decomposed: lifecycle-related paths are renamed from runtime_artifacts to lifecycle_artifacts, and a new ManagedGuestServiceArtifacts struct is extracted to own the service log and PID paths (previously inlined). All call sites across pika-server and pikaci are updated to use the new RuntimeArtifacts::method() syntax and the renamed struct fields.

Tutorial Steps

Introduce RuntimeArtifactKind enum and unify error type

Intent: Replace three separate error variants (DecodeStatus, DecodeTerminalResult, DecodeEventLine) with a single Decode variant parameterized by a RuntimeArtifactKind enum, reducing duplication in error definitions and Display formatting.

Affected files: crates/pika-cloud/src/lifecycle.rs

Evidence
@@ -77,28 +77,38 @@ impl LifecycleTerminalStatus {
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum RuntimeArtifactKind {
+    Status,
+    TerminalResult,
+    EventStream,
+}
@@ -77,28 +77,38 @@
 #[derive(Debug)]
-pub enum RuntimeLifecycleLoadError {
+pub enum RuntimeArtifactLoadError {
     Read {
         path: PathBuf,
         source: io::Error,
     },
-    DecodeStatus {
-        source_name: String,
-        source: serde_json::Error,
-    },
-    DecodeTerminalResult {
+    Decode {
+        kind: RuntimeArtifactKind,
         source_name: String,
-        source: serde_json::Error,
-    },
-    DecodeEventLine {
-        path: PathBuf,
-        line: usize,
+        line: Option<usize>,
         source: serde_json::Error,
     },

The old RuntimeLifecycleLoadError had three decode variants that each carried slightly different context but performed the same role. This step introduces:

  1. RuntimeArtifactKind — a Copy enum with variants Status, TerminalResult, and EventStream, each providing a human-readable label() used in error messages.
  2. RuntimeArtifactLoadError (renamed from RuntimeLifecycleLoadError) — collapses the three decode variants into a single Decode { kind, source_name, line: Option<usize>, source }. The optional line field is Some only for event-stream decode errors, eliminating the need for a separate DecodeEventLine variant.

The Display implementation now pattern-matches on line: None vs line: Some(line) to choose the appropriate format string, and interpolates kind.label() for the artifact type name. The Error::source() impl likewise simplifies to two arms.

Consolidate free functions into RuntimeArtifacts unit struct

Intent: Move all encode, decode, and load helper functions from module-level free functions into associated methods on a new RuntimeArtifacts unit struct, providing a single namespace for all artifact I/O operations.

Affected files: crates/pika-cloud/src/lifecycle.rs

Evidence
@@ -138,127 +147,145 @@
+pub struct RuntimeArtifacts;
+
+impl RuntimeArtifacts {
+    pub fn encode_terminal_result_pretty(
+        result: &RuntimeTerminalResult,
+    ) -> serde_json::Result<Vec<u8>> {
@@ -138,127 +147,145 @@
+    pub fn decode_event_stream(
+        source_name: impl Into<String>,
+        contents: &str,
+    ) -> Result<Vec<LifecycleEvent>, RuntimeArtifactLoadError> {
@@ -138,127 +147,145 @@
+fn read_artifact_bytes(path: &Path) -> Result<Vec<u8>, RuntimeArtifactLoadError> {
+fn read_artifact_string(path: &Path) -> Result<String, RuntimeArtifactLoadError> {

Previously the module exposed twelve free functions with a runtime_/encode_runtime_/decode_runtime_/load_runtime_ naming convention. This step:

  1. Creates pub struct RuntimeArtifacts; — a zero-sized unit struct serving purely as a method namespace.
  2. Moves all encode/decode/load functions into impl RuntimeArtifacts, dropping the runtime_ prefix since the struct name already conveys context (e.g., decode_runtime_statusRuntimeArtifacts::decode_status).
  3. Renames load_runtime_events to RuntimeArtifacts::load_events and refactors its body to call the new decode_event_stream method (renamed from inline logic), passing the RuntimeArtifactKind::EventStream kind.
  4. Extracts two private helpers — read_artifact_bytes and read_artifact_string — to DRY up the fs::read / fs::read_to_string calls with consistent RuntimeArtifactLoadError::Read mapping.

The sole surviving free function is runtime_terminal_result_for_exit_code, which constructs a result value and doesn't perform I/O, so it remains at module level.

Update public re-exports in pika-cloud lib.rs

Intent: Adjust the crate's public API surface to export the new types (RuntimeArtifacts, RuntimeArtifactKind, RuntimeArtifactLoadError) and remove the old free-function exports.

Affected files: crates/pika-cloud/src/lib.rs

Evidence
@@ -10,12 +10,9 @@ pub use lifecycle::{
-    LIFECYCLE_SCHEMA_VERSION, LifecycleEvent, LifecycleState, RuntimeLifecycleLoadError,
-    RuntimeResultStatus, RuntimeStatusSnapshot, RuntimeTerminalResult, decode_runtime_event_line,
-    decode_runtime_status, decode_runtime_status_artifact, decode_runtime_terminal_result,
-    decode_runtime_terminal_result_artifact, encode_runtime_event_line,
-    encode_runtime_status_pretty, encode_runtime_terminal_result_pretty, load_runtime_events,
-    load_runtime_status, load_runtime_terminal_result, runtime_terminal_result_for_exit_code,
+    LIFECYCLE_SCHEMA_VERSION, LifecycleEvent, LifecycleState, RuntimeArtifactKind,
+    RuntimeArtifactLoadError, RuntimeArtifacts, RuntimeResultStatus, RuntimeStatusSnapshot,
+    RuntimeTerminalResult, runtime_terminal_result_for_exit_code,

The pub use lifecycle::{...} block in lib.rs is trimmed from twelve free-function re-exports plus the old error type down to just the types and the single remaining free function. Downstream crates now import RuntimeArtifacts and call methods on it rather than importing individual functions. RuntimeLifecycleLoadError is replaced by RuntimeArtifactLoadError, and the new RuntimeArtifactKind is also exported for callers that need to inspect error details.

Extract ManagedGuestServiceArtifacts and rename runtime_artifacts to lifecycle_artifacts

Intent: Separate lifecycle artifact paths from service-management artifact paths (log, PID) by introducing a dedicated ManagedGuestServiceArtifacts struct and renaming the existing field for clarity.

Affected files: crates/pika-cloud/src/lib.rs

Evidence
@@ -315,19 +312,59 @@
-    pub runtime_artifacts: RuntimeArtifactPaths,
-    pub log_path: String,
+    pub lifecycle_artifacts: RuntimeArtifactPaths,
+    #[serde(flatten)]
+    pub service_artifacts: ManagedGuestServiceArtifacts,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct ManagedGuestServiceArtifacts {
+    #[serde(rename = "log_path")]
+    pub service_log_path: String,
     pub pid_path: String,
 }
@@ -346,16 +383,6 @@
-            (
-                "log_path",
-                self.log_path.as_str(),
-                canonical.log_path.as_str(),
-            ),
-            (
-                "pid_path",
-                self.pid_path.as_str(),
-                canonical.pid_path.as_str(),
-            ),

The GuestStartupArtifacts struct previously mixed lifecycle artifact paths (status, events, result) with service-management paths (log, PID) in a flat layout. This step:

  1. Renames the runtime_artifacts field to lifecycle_artifacts to clarify its purpose.
  2. Extracts log_path and pid_path into a new ManagedGuestServiceArtifacts struct, added as a #[serde(flatten)] field called service_artifacts. The log path field is renamed to service_log_path internally with #[serde(rename = "log_path")] to preserve JSON wire compatibility.
  3. Moves the path validation for log_path and pid_path out of GuestStartupArtifacts::validate_canonical_paths and into ManagedGuestServiceArtifacts::validate_canonical_paths, following the same pattern as RuntimeArtifactPaths. The parent struct now delegates to both sub-structs.
  4. Both Default impls produce the canonical guest paths from the existing constants.

Update pika-server call sites to use RuntimeArtifacts methods

Intent: Migrate the agent API and managed guest modules from free-function imports to the new RuntimeArtifacts associated method syntax.

Affected files: crates/pika-server/src/agent_api.rs, crates/pika-server/src/managed_openclaw_guest.rs

Evidence
@@ -36,12 +36,12 @@
-use pika_cloud::{
-    decode_runtime_status_artifact, incus_mount_device_config, incus_runtime_config,
+use pika_cloud::{
+    incus_mount_device_config, incus_runtime_config, ... RuntimeArtifacts,
@@ -1896,7 +1896,7 @@
-        match decode_runtime_status_artifact(&status_path, &status_bytes) {
+        match RuntimeArtifacts::decode_status_artifact(&status_path, &status_bytes) {
@@ -704,9 +704,9 @@
-        assert_eq!(plan.artifacts.runtime_artifacts.status_path, STATUS_PATH);
+        assert_eq!(plan.artifacts.lifecycle_artifacts.status_path, STATUS_PATH);

In agent_api.rs, the import of decode_runtime_status_artifact is replaced by RuntimeArtifacts, and the single call site in IncusManagedRuntimeProvider switches to RuntimeArtifacts::decode_status_artifact(...). The associated test decode_runtime_status_artifact_rejects_malformed_snapshot is updated identically.

In managed_openclaw_guest.rs, test assertions that referenced plan.artifacts.runtime_artifacts.* are updated to plan.artifacts.lifecycle_artifacts.* to match the renamed field.

Update pikaci executor to use RuntimeArtifacts methods

Intent: Migrate the CI executor from free-function imports to RuntimeArtifacts method calls for encoding and loading terminal results.

Affected files: crates/pikaci/src/executor.rs

Evidence
@@ -12,9 +12,8 @@
-    encode_runtime_terminal_result_pretty, load_runtime_terminal_result,
+    RuntimeArtifacts,
@@ -424,7 +423,8 @@
-        encode_runtime_terminal_result_pretty(&result).context("encode host-local result")?,
+        RuntimeArtifacts::encode_terminal_result_pretty(&result)
+            .context("encode host-local result")?,
@@ -1013,8 +1013,9 @@
-        let guest_result = load_runtime_terminal_result(ctx.job_dir.join("artifacts/result.json"))
+        let guest_result =
+            RuntimeArtifacts::load_terminal_result(ctx.job_dir.join("artifacts/result.json"))
@@ -1250,7 +1251,7 @@
-    let guest_result = load_runtime_terminal_result(result_path)
+    let guest_result = RuntimeArtifacts::load_terminal_result(result_path)

The pikaci executor had three call sites using the old free functions:

  1. run_host_local_jobencode_runtime_terminal_result_pretty(&result) becomes RuntimeArtifacts::encode_terminal_result_pretty(&result).
  2. run_remote_linux_vm_jobload_runtime_terminal_result(...) becomes RuntimeArtifacts::load_terminal_result(...).
  3. run_tart_job — same load_runtime_terminal_resultRuntimeArtifacts::load_terminal_result migration.

The import block drops the two free-function names and adds RuntimeArtifacts instead. No behavioral changes — purely a namespace migration.

Update all lifecycle module tests to use new API

Intent: Ensure all unit tests in the lifecycle module exercise the new RuntimeArtifacts methods and RuntimeArtifactLoadError type, validating that the refactoring preserves correctness.

Affected files: crates/pika-cloud/src/lifecycle.rs, crates/pika-cloud/src/lib.rs

Evidence
@@ -308,8 +335,8 @@
-        let encoded = encode_runtime_event_line(&event).expect("encode");
-        let decoded = decode_runtime_event_line(&encoded).expect("decode");
+        let encoded = RuntimeArtifacts::encode_event_line(&event).expect("encode");
+        let decoded = RuntimeArtifacts::decode_event_line(&encoded).expect("decode");
@@ -406,7 +433,7 @@
-        let err = load_runtime_status(&path).expect_err("missing file should fail");
+        let err = RuntimeArtifacts::load_status(&path).expect_err("missing file should fail");
@@ -937,11 +966,11 @@
-        assert_eq!(artifacts.runtime_artifacts.status_path, STATUS_PATH);
+        assert_eq!(artifacts.lifecycle_artifacts.status_path, STATUS_PATH);
@@ -1068,7 +1097,7 @@
-                runtime_artifacts: RuntimeArtifactPaths {
+                lifecycle_artifacts: RuntimeArtifactPaths {

Every test in lifecycle.rs and lib.rs that previously called a free function is updated to use RuntimeArtifacts::method() syntax:

  • Event round-trip testsencode_runtime_event_line / decode_runtime_event_lineRuntimeArtifacts::encode_event_line / decode_event_line.
  • Status encode/decode testsencode_runtime_status_pretty / decode_runtime_statusRuntimeArtifacts::encode_status_pretty / decode_status.
  • Terminal result tests — all encode/decode/load calls updated.
  • Error-path testsload_runtime_status, load_runtime_terminal_result, load_runtime_eventsRuntimeArtifacts::load_status, load_terminal_result, load_events.
  • Struct field tests in lib.rs — assertions updated from runtime_artifacts to lifecycle_artifacts and from log_path/pid_path to service_artifacts.service_log_path/service_artifacts.pid_path.
  • Validation rejection test — the GuestStartupArtifacts construction in the non-canonical path test uses lifecycle_artifacts as the field name.

Diff