44import shutil
55import tempfile
66import time
7+ from collections .abc import Callable
78from dataclasses import dataclass
89from pathlib import Path
9- from typing import Callable , Literal
10+ from typing import Any , Literal
1011
1112
1213@dataclass
@@ -93,7 +94,20 @@ def _prune(self) -> None:
9394 pass
9495 self ._frames = keep
9596
96- def persist (self , * , reason : str | None , status : Literal ["failure" , "success" ]) -> Path | None :
97+ def _write_json_atomic (self , path : Path , data : Any ) -> None :
98+ tmp_path = path .with_suffix (path .suffix + ".tmp" )
99+ tmp_path .write_text (json .dumps (data , indent = 2 ))
100+ tmp_path .replace (path )
101+
102+ def persist (
103+ self ,
104+ * ,
105+ reason : str | None ,
106+ status : Literal ["failure" , "success" ],
107+ snapshot : Any | None = None ,
108+ diagnostics : Any | None = None ,
109+ metadata : dict [str , Any ] | None = None ,
110+ ) -> Path | None :
97111 if self ._persisted :
98112 return None
99113
@@ -107,8 +121,23 @@ def persist(self, *, reason: str | None, status: Literal["failure", "success"])
107121 for frame in self ._frames :
108122 shutil .copy2 (frame .path , frames_out / frame .file_name )
109123
110- steps_path = run_dir / "steps.json"
111- steps_path .write_text (json .dumps (self ._steps , indent = 2 ))
124+ self ._write_json_atomic (run_dir / "steps.json" , self ._steps )
125+
126+ snapshot_payload = None
127+ if snapshot is not None :
128+ if hasattr (snapshot , "model_dump" ):
129+ snapshot_payload = snapshot .model_dump ()
130+ else :
131+ snapshot_payload = snapshot
132+ self ._write_json_atomic (run_dir / "snapshot.json" , snapshot_payload )
133+
134+ diagnostics_payload = None
135+ if diagnostics is not None :
136+ if hasattr (diagnostics , "model_dump" ):
137+ diagnostics_payload = diagnostics .model_dump ()
138+ else :
139+ diagnostics_payload = diagnostics
140+ self ._write_json_atomic (run_dir / "diagnostics.json" , diagnostics_payload )
112141
113142 manifest = {
114143 "run_id" : self .run_id ,
@@ -117,12 +146,12 @@ def persist(self, *, reason: str | None, status: Literal["failure", "success"])
117146 "reason" : reason ,
118147 "buffer_seconds" : self .options .buffer_seconds ,
119148 "frame_count" : len (self ._frames ),
120- "frames" : [
121- {"file" : frame .file_name , "ts" : frame .ts } for frame in self ._frames
122- ],
149+ "frames" : [{"file" : frame .file_name , "ts" : frame .ts } for frame in self ._frames ],
150+ "snapshot" : "snapshot.json" if snapshot_payload is not None else None ,
151+ "diagnostics" : "diagnostics.json" if diagnostics_payload is not None else None ,
152+ "metadata" : metadata or {},
123153 }
124- manifest_path = run_dir / "manifest.json"
125- manifest_path .write_text (json .dumps (manifest , indent = 2 ))
154+ self ._write_json_atomic (run_dir / "manifest.json" , manifest )
126155
127156 self ._persisted = True
128157 return run_dir
0 commit comments