@@ -257,82 +257,84 @@ async def main() -> None:
257257 destination = "Lunar South Pole"
258258
259259 # SQLite checkpointer in JSON mode (the migration-eligible
260- # serialization). A real app would point at a persistent path; for
261- # the demo a temp file keeps the workspace clean.
262- db_dir = tempfile .mkdtemp (prefix = "oa-checkpoint-demo-" )
263- db_path = Path (db_dir ) / "checkpoints.sqlite"
264- checkpointer = SQLiteCheckpointer (path = db_path , serialization = "json" )
265-
266- print ("=" * 72 )
267- print ("Phase 1 — invoke v1 graph; checkpoints save after every node" )
268- print ("=" * 72 )
269- print ()
270- print (f" destination: { destination } " )
271- print (f" checkpoint db: { db_path } " )
272- print ()
273-
274- # Pass a deterministic correlation_id so phase 2 can find the
275- # invocation's saved records via the checkpoint filter. Without a
276- # caller-supplied correlation_id, invoke() generates a UUIDv4.
277- run_id = "demo-mission-plan-1"
278-
279- graph_v1 = build_graph_v1 (checkpointer )
280- initial_v1 = MissionPlanStateV1 (destination = destination )
281- final_v1 = await graph_v1 .invoke (initial_v1 , correlation_id = run_id )
282- await graph_v1 .drain ()
283-
284- # Look up the saved record's invocation_id by correlation_id. The
285- # invocation_id is generated by invoke() and isn't exposed on the
286- # returned state; finding it through the checkpointer's list API
287- # is the canonical lookup path.
288- summaries = list (await checkpointer .list (CheckpointFilter (correlation_id = run_id )))
289- assert summaries , "expected at least one saved checkpoint"
290- invocation_id = summaries [- 1 ].invocation_id
291-
292- print ("v1 result:" )
293- print (f" objective: { final_v1 .objective } " )
294- print (f" crew_size: { final_v1 .crew_size } " )
295- print (f" timeline: { final_v1 .timeline } " )
296- print ()
297- print (f" v1 invocation_id: { invocation_id } " )
298- print ()
299-
300- print ("=" * 72 )
301- print ("Phase 2 — invoke v2 graph with resume; v1->v2 migration runs" )
302- print ("=" * 72 )
303- print ()
304- print (" v2 adds: risk_assessment field + assess_risks node" )
305- print (" migration: backfills risk_assessment='' for v1 records" )
306- print ()
307-
308- graph_v2 = build_graph (checkpointer )
309- # Resume from the v1 invocation. The engine reads the saved record,
310- # applies migrate_v1_to_v2, re-deserializes against
311- # MissionPlanStateV2, and continues at the first uncompleted node
312- # (assess_risks — the v1 pipeline's three nodes are all in
313- # completed_positions, the new v2 node is not).
314- final_v2 = await graph_v2 .invoke (
315- MissionPlanStateV2 (destination = destination ),
316- resume_invocation = invocation_id ,
317- )
318- await graph_v2 .drain ()
319-
320- print ("v2 result after resume:" )
321- print (f" objective: { final_v2 .objective } " )
322- print (f" crew_size: { final_v2 .crew_size } " )
323- print (f" timeline: { final_v2 .timeline } " )
324- print (f" risk_assessment: { final_v2 .risk_assessment } " )
325- print ()
326- print (f" trace: { final_v2 .trace } " )
327- print ()
328- print (
329- "The v1 nodes appear once each in v1's trace and NOT in v2's "
330- "trace — they were skipped on resume because completed_positions "
331- "already covered them. Only assess_risks ran in phase 2."
332- )
333-
334- if _provider_instance is not None :
335- await _provider_instance .aclose ()
260+ # serialization). A real app would point at a persistent path; the
261+ # demo uses TemporaryDirectory so the DB file + folder get cleaned
262+ # up on exit (happy path or exception) without leaving cruft in /tmp.
263+ with tempfile .TemporaryDirectory (prefix = "oa-checkpoint-demo-" ) as db_dir :
264+ db_path = Path (db_dir ) / "checkpoints.sqlite"
265+ checkpointer = SQLiteCheckpointer (path = db_path , serialization = "json" )
266+
267+ try :
268+ print ("=" * 72 )
269+ print ("Phase 1 — invoke v1 graph; checkpoints save after every node" )
270+ print ("=" * 72 )
271+ print ()
272+ print (f" destination: { destination } " )
273+ print (f" checkpoint db: { db_path } " )
274+ print ()
275+
276+ # Pass a deterministic correlation_id so phase 2 can find the
277+ # invocation's saved records via the checkpoint filter. Without a
278+ # caller-supplied correlation_id, invoke() generates a UUIDv4.
279+ run_id = "demo-mission-plan-1"
280+
281+ graph_v1 = build_graph_v1 (checkpointer )
282+ initial_v1 = MissionPlanStateV1 (destination = destination )
283+ final_v1 = await graph_v1 .invoke (initial_v1 , correlation_id = run_id )
284+ await graph_v1 .drain ()
285+
286+ # Look up the saved record's invocation_id by correlation_id. The
287+ # invocation_id is generated by invoke() and isn't exposed on the
288+ # returned state; finding it through the checkpointer's list API
289+ # is the canonical lookup path.
290+ summaries = list (await checkpointer .list (CheckpointFilter (correlation_id = run_id )))
291+ assert summaries , "expected at least one saved checkpoint"
292+ invocation_id = summaries [- 1 ].invocation_id
293+
294+ print ("v1 result:" )
295+ print (f" objective: { final_v1 .objective } " )
296+ print (f" crew_size: { final_v1 .crew_size } " )
297+ print (f" timeline: { final_v1 .timeline } " )
298+ print ()
299+ print (f" v1 invocation_id: { invocation_id } " )
300+ print ()
301+
302+ print ("=" * 72 )
303+ print ("Phase 2 — invoke v2 graph with resume; v1->v2 migration runs" )
304+ print ("=" * 72 )
305+ print ()
306+ print (" v2 adds: risk_assessment field + assess_risks node" )
307+ print (" migration: backfills risk_assessment='' for v1 records" )
308+ print ()
309+
310+ graph_v2 = build_graph (checkpointer )
311+ # Resume from the v1 invocation. The engine reads the saved record,
312+ # applies migrate_v1_to_v2, re-deserializes against
313+ # MissionPlanStateV2, and continues at the first uncompleted node
314+ # (assess_risks — the v1 pipeline's three nodes are all in
315+ # completed_positions, the new v2 node is not).
316+ final_v2 = await graph_v2 .invoke (
317+ MissionPlanStateV2 (destination = destination ),
318+ resume_invocation = invocation_id ,
319+ )
320+ await graph_v2 .drain ()
321+
322+ print ("v2 result after resume:" )
323+ print (f" objective: { final_v2 .objective } " )
324+ print (f" crew_size: { final_v2 .crew_size } " )
325+ print (f" timeline: { final_v2 .timeline } " )
326+ print (f" risk_assessment: { final_v2 .risk_assessment } " )
327+ print ()
328+ print (f" trace: { final_v2 .trace } " )
329+ print ()
330+ print (
331+ "The v1 nodes appear once each in v1's trace and NOT in v2's "
332+ "trace — they were skipped on resume because completed_positions "
333+ "already covered them. Only assess_risks ran in phase 2."
334+ )
335+ finally :
336+ if _provider_instance is not None :
337+ await _provider_instance .aclose ()
336338
337339
338340if __name__ == "__main__" :
0 commit comments