LangGraph 1.2.15 ย้อน Checkpoint อย่างไรไม่ให้ State คนละ Branch ปนกัน

วิศวกรเลือก Checkpoint C1 บนแผงกระจก โดยเส้นทางเดิม C0 ถึง C2 เป็นสีไซแอน และเส้นทางทดลอง C1 ถึง F2 แยกเป็นสีอำพัน
วิศวกรเลือก Checkpoint C1 บนแผงกระจก โดยเส้นทางเดิม C0 ถึง C2 เป็นสีไซแอน และเส้นทางทดลอง C1 ถึง F2 แยกเป็นสีอำพัน
ภาพปกอธิบายแนวคิดการเลือก checkpoint เก่าแล้วสร้างสาขาทดลอง โดยเก็บสาขาเดิมแยกไว้ เป็นภาพประกอบ ไม่ใช่ภาพระบบที่รันจริง ต้นทาง · ภาพเต็ม

บทเรียน Agent Engineering · LangGraph Python 1.2.15 · ตรวจต้นทาง 14 ตุลาคม 2026

Agent ทำรายงานฉบับใหม่จาก checkpoint เก่าได้ แต่คุณตรวจฉบับเดิมกลับมาหรือยัง? ผลทางเลือกที่ดูถูกต้องยังไม่พิสูจน์ว่า state ของเส้นทางเดิมไม่ปนกับเส้นทางใหม่

Quick Answer: ข่าวนี้เปลี่ยนอะไร และเราจะฝึกอะไร

LangGraph 1.2.15 แก้กรณีการรันงาน (run) จาก checkpoint เก่าที่อาจทำให้ State ของ branch เดิมปนกับค่าที่เขียน (writes) จาก run ใหม่ ขอบเขตของบั๊กนี้คือ DeltaChannel ร่วมกับ durability="exit" และการระบุ checkpoint ที่ thread เดินเลยไปแล้ว ไม่ใช่ข้อสรุปว่าการใช้ checkpoint ทุกแบบเสียหาย รุ่นนี้เผยแพร่ 12 ตุลาคม 2026 เวลา 22:38:26 UTC ตามข้อมูล GitHub Release API [1] [2]

ทักษะเดียวในบทเรียนนี้คือ เลือก checkpoint ในอดีต สร้าง fork แล้วอ่าน checkpoint ปลายเส้นทางเดิมเพื่อพิสูจน์ว่าไม่ได้เปลี่ยน ตัวอย่างใช้ workflow Python กับข้อมูลจำลองในเครื่อง ไม่มีการเรียก LLM หรือ API จริง โค้ดและ tests ยัง NOT RUN ส่วน Lab เป็น simulation ผลที่คาดหมายจึงต้องแยกจากหลักฐานการทดสอบที่ผู้อ่านรันเอง

หลังอ่าน คุณต้องส่งมอบอะไรได้

จัดทำ manifest หรือรายการหลักฐานที่มี thread ID, checkpoint ที่เลือก, ปลายเส้นทางเดิม, จุดเริ่มและปลาย fork พร้อมรายงาน A/B และ values เดิมก่อน/หลัง เกณฑ์ตรวจรับคือ fork ใช้ B แต่การอ่านด้วย config ปลาย A ยังคงได้ A และค่าทั้งชุดเท่าเดิม ถ้าหาไม่พบหรือเจอหลายจุดต้องหยุด ไม่เลือกตัวแรกเพื่อให้โค้ดเดินต่อ

จาก LLM chat สู่ระบบที่มีประวัติให้ตรวจ

ใน LLM chat คุณพิมพ์ว่า “เปลี่ยนแหล่งข้อมูลแล้วสรุปใหม่” จากนั้นอ่านสองคำตอบเปรียบเทียบ แต่ Agent มีข้อมูลจากเครื่องมือและงานหลายขั้น จึงต้องระบุว่ากำลังใช้ข้อมูลฉบับใดและเดินต่อจากตรงไหน ป้าย “ล่าสุด” ใช้แทน ID ของฉบับที่ต้องการตรวจไม่ได้

ตัวอย่างนี้เป็น workflow ที่กำหนดขั้นแน่นอน ไม่มีโมเดลเลือกเครื่องมือเอง เราจำกัดโจทย์เพื่อเห็น state ชัดก่อนเพิ่มความไม่แน่นอน อ่านภาพรวมได้ใน สถาปัตยกรรม Agent: Workflow, Single-Agent และ Multi-Agent

คำที่ต้องเข้าใจก่อนเลือกจุดย้อน

State คือข้อมูลที่ส่งต่อ เช่นโจทย์ ผลเครื่องมือและรายงาน Thread คือหน่วยติดตามประวัติงาน Checkpoint คือสถานะพร้อมตำแหน่งการดำเนินงานที่บันทึกไว้ Branch คือเส้นทางที่แยกจากจุดก่อนหน้า

Checkpointer เก็บ graph state ใน thread ส่วน Store เก็บข้อมูลระดับแอปพลิเคชันข้าม thread ตัวอย่างใช้ InMemorySaver ที่เก็บใน RAM และหายเมื่อ process ปิด จึงไม่ใช่ตัวอย่างกู้หลังเครื่องดับ [4]

เมื่ออ่านสถานะ values บอกข้อมูล next บอกขั้นที่จะทำต่อ และ config ระบุ checkpoint ตัวอย่างเลือก next == ("format_report",) เพื่อเปลี่ยนข้อมูลหลัง mock tool แต่ก่อนรายงาน [5]

ผัง checkpoint สมมติ C0 ไป C1 ไป C2 แสดงสาขาเดิม และสาขาทดลองสีอำพันที่แยกจาก C1 ไป F1 และ F2 โดยไม่มีลูกศรย้อนเข้าสู่ C2
C0–C2 และ F1–F2 เป็น ID สมมติสำหรับสอน การ fork เก็บเส้นทาง state ใหม่แยกจากสาขาเดิม และ checkpoint ไม่ย้อนผลที่เกิดในระบบภายนอก ต้นทาง · ภาพเต็ม

Replay, Fork และ Resume ให้ตอบคนละคำถาม

การทำงานคำถามที่ใช้เลือก
Replayจะเดินต่อจากข้อมูลเดิมอีกครั้งหรือไม่
Forkจะเปลี่ยน state ในอดีตเพื่อสำรวจทางเลือกหรือไม่
Resume งานที่หยุดจะทำงานที่ค้างภายใต้ประวัติที่บันทึกไว้ต่อหรือไม่

Time travel ใช้ update_state กับ historical config แล้ว invoke(None, fork_config) เพื่อเดินต่อ มันสร้าง checkpoint ใหม่โดยไม่ลบประวัติเดิม แต่ขั้นหลังจุดย้อน (downstream nodes) รวม LLM/API/interrupt อาจทำงานใหม่ [6]

ข้อวิเคราะห์สำคัญคือ checkpoint ไม่ undo การกระทำภายนอก: อีเมลที่ส่งหรือโพสต์ที่เผยแพร่ไปแล้วไม่ได้ถูกเรียกคืนจากการ fork ตัวอย่างจึงใช้ formatter ที่คืนข้อความ ไม่มีเครื่องมือส่งจริง

Worked example: รายงานข้อมูล A กับทางเลือก B

สถานการณ์สมมติ: รายงานภายในอ่าน fixture A “เอกสารขั้นตอนเดิม” ส่วนทางเลือกใช้ B “เอกสารขั้นตอนทางเลือก” Fixture คือข้อมูลทดสอบที่เราตั้งเอง ไม่ใช่ฐานข้อมูลบริษัท

คำสั่งคือ “ทำรายงาน A แล้วลอง B ที่จุดก่อนรายงาน พร้อมยืนยันว่าฉบับ A เหมือนเดิม” แผนคืออ่านข้อมูล → checkpoint ก่อนรายงาน → รายงาน A → fork จากจุดที่เลือก → รายงาน B → อ่าน checkpoint ปลาย A → ตรวจรับ

Mock tool read_fixture เลือกข้อมูลในเครื่องและคืนสำเนา ไม่เรียก API ส่วน format_report ตรวจ field path และ text ก่อนประกอบรายงาน เราเก็บ config ปลาย A และสำเนา values ก่อน แล้ว patch fixture B ที่จุดก่อน formatter ด้วย as_node="read_fixture" เพราะแก้ผลที่ node นี้เคยส่งออก ไม่ได้เรียก mock tool ซ้ำ

Expected output คือ “เส้นทาง A | เอกสารขั้นตอนเดิม” กับ “เส้นทาง B | เอกสารขั้นตอนทางเลือก” ยังไม่ใช่ผลรันจริง หลักฐานตรวจรับต้องรวมผลอ่านกลับ A/B ด้วย config ปลายแต่ละฉบับ ไม่ใช้รายงานใหม่เพียงบรรทัดเดียว

ขั้นตอนเก็บ original_config เลือก checkpoint เก่า สร้าง fork_config ด้วย update_state เดินต่อด้วย invoke แล้วเก็บ fork_final_config เพื่ออ่านสถานะสองสาขาแยกกัน
เก็บ config ของปลายสาขาเดิมก่อนสร้าง fork และเก็บ config ปลายสาขาทดลองหลังเดินต่อ จึงอ่าน checkpoint ที่ตั้งใจตรวจได้ ไม่สรุปจาก latest เพียงค่าเดียว ภาพเป็นแผนการทำงาน ไม่ใช่ log จริง ต้นทาง · ภาพเต็ม

โค้ด Python: เก็บหลักฐานก่อน แล้วจึงแยกเส้นทาง

ใช้ Python 3.12+ และตรึง langgraph==1.2.15 Package รองรับ Python>=3.10; dependency ตาม tag มี langchain-core, checkpoint, SDK และ prebuilt [8] ไม่ต้อง API key/environment variable ไม่มี model/API billing ในตัวอย่าง แต่ใช้ทรัพยากรเครื่องและการดาวน์โหลด package หากติดตั้งเอง

โครงสร้างคือ START → read_fixture → format_report → END imports ตรวจจาก Graph exports ที่ tag [11] ชุดท้ายบทความมีไฟล์เต็ม ช่วงหลักคือ:

original_final = graph.get_state(thread_config)
original_config = deepcopy(original_final.config)
original_values = deepcopy(original_final.values)

selected = select_before_report(graph, thread_config)
fork_start_config = graph.update_state(
    selected.config,
    {"scenario": "alternate", "fixture": deepcopy(FIXTURES["alternate"])},
    as_node="read_fixture",
)
fork_output = graph.invoke(None, fork_start_config)
original_readback = deepcopy(graph.get_state(original_config).values)
assert original_readback == original_values

deepcopy แยกค่าที่เก็บไว้ตรวจจากข้อมูลที่อาจแก้ภายหลัง ไม่สร้าง transaction ให้ฐานข้อมูล ตัวช่วยเลือกด้วย next ยอมรับหนึ่งจุด; หากมีหลายรอบ/branch ต้องระบุ checkpoint ID จาก history ไม่เดา list index นี่เป็นกติกาตรวจรับที่เราออกแบบ

fork_start_config ชี้จุดก่อน formatter ไม่ใช่ปลาย fork โค้ดเต็มจึงเก็บ fork_final_config เพิ่ม ตรวจว่า terminal และ parent chain กลับถึง fork start แล้วอ่าน B ผ่าน config นั้น การจับ latest หลัง invoke ใช้ได้ใน ตัวอย่างหนึ่ง worker ที่ไม่มีใครเขียน thread ระหว่างนั้น ระบบ concurrent ต้องมี run ownership/correlation ไม่ถือว่าหัวล่าสุดเป็นงานของเราเสมอ ส่วนการตรวจ A ใช้ original final config ที่เก็บไว้เท่านั้น

วิธีติดตั้งและทดสอบ: ให้ผู้อ่านทำในเครื่องที่มีสิทธิ์

ผู้เขียน NOT RUN ไม่ได้ติดตั้ง รัน import หรือ compile ตัวอย่าง คำสั่งนี้เปลี่ยน virtual environment ของผู้อ่านและเข้าถึง package registry ไม่ใช่การทำงานของ Lab บนเว็บ:

py -3.12 -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt
.venv\Scripts\python.exe code\checkpoint_fork.py
.venv\Scripts\python.exe -m unittest discover -s code -p "test_checkpoint_fork.py" -v

Input คือ scenario="original" แล้ว controlled fork ใช้ "alternate" ผลที่คาดหมายมีรายงาน A/B, original_unchanged: true และ runtime-generated IDs อย่าอ้าง expected output เป็น log จริง

Tests ใช้ standard-library unittest: ตรวจ branch A/B, next/ID, terminal, ID ที่ไม่พบ, ประวัติคลุมเครือ และ fork ที่ fixture ผิด schema แต่ A ต้องเหมือนเดิม ยังไม่รันทุกข้อ มี assertion ไม่เท่ากับ PASS และอย่าใช้ Python -O ซึ่งปิด assert กับ demo นี้

ทำไมเราไม่ใช้ DeltaChannel เป็นพื้นฐานทั้งหมด

ข่าวเกี่ยวกับ DeltaChannel แต่ตัวอย่างหลักใช้ state ธรรมดาเพื่อฝึก fork ก่อนรายละเอียด storage อ่าน source [9] และ regression test ที่ tag [10] สำหรับบั๊กเฉพาะ

DeltaChannel ยัง beta; reducer รับ current state กับ batch ของ writes ต้อง deterministic และ batching-invariant จึงแทนด้วย ordinary binary operator.add โดยไม่ตรวจสัญญาไม่ได้ เราไม่ได้รัน reproduction รายงาน tests ของผู้พัฒนาและ community ใน PR9269 [3] เป็นผลของผู้อื่น ไม่ใช่ Hybrid Mind runtime PASS และไม่พิสูจน์ migration/storage ที่ไม่ได้ตรวจ

จุดที่งานอาจพัง และเงื่อนไขหยุด

  • เลือก list index: ตำแหน่งเปลี่ยนเมื่อมี checkpoint เพิ่ม
  • เลือกปลายงาน: next ว่างอาจได้ no-op แทนทดลอง formatter
  • ตรวจ latest: ไม่ได้ตอบว่า checkpoint ปลาย A เปลี่ยนหรือไม่
  • patch field ที่มีกฎรวมค่า (reducer): ค่าอาจสะสมแทนการแทนที่
  • เจอหลายจุด: หยุดระบุ checkpoint ID ให้ตรงช่วง ไม่เดา
  • มี downstream external write: หยุดก่อนทดลองจนมีอำนาจและแผนกันซ้ำ

รายงานหยุดให้ระบุสิ่งที่ขาด เช่น “มีสอง candidates” หรือ “fixture ไม่มี text” อย่าเติมรายงานสมบูรณ์กลบ error ตัวอย่างไม่มี concurrent workers; หากหลาย Agent เขียนร่วมกัน อ่าน Blackboard Architecture และสถานะร่วมแบบมีเวอร์ชัน เพิ่มก่อนออกแบบสิทธิ์เขียน

ผังขอบเขตแพตช์ LangGraph 1.2.15 แสดง DeltaChannel ร่วมกับ durability exit และ checkpoint เก่า แพตช์แยก writes ด้วย snapshot แต่ไม่ซ่อม branch ที่ข้อมูลปนไปแล้ว
ขอบเขตบั๊กที่แก้ใน 1.2.15 ต้องมีเงื่อนไขร่วมกันตาม PR9269: DeltaChannel, exit durability และ checkpoint เก่าที่ thread ผ่านไปแล้ว การอัปเกรดไม่ซ่อม branch ที่เสียไปก่อนหน้า และตัวอย่างของบทเรียนยัง NOT RUN ต้นทาง · ภาพเต็ม

Persistence, ความปลอดภัย และต้นทุนที่ต้องรายงานตรง ๆ

Durability exit บันทึกตอน run ออก; async บันทึกระหว่างขั้นถัดไป; sync บันทึกก่อนขั้นถัดไป โหมด exit จึงไม่เก็บสถานะระหว่างทางเพื่อกู้เมื่อ process พังกลาง run [5] ตัวอย่าง RAM ไม่วัด latency/storage และไม่สร้างเปอร์เซ็นต์ความเร็วให้แต่ละโหมด

ในระบบจริง ให้บันทึก data revision กับ tool result ที่ใช้ ไม่ใช่เพียง checkpoint ID และเลือกโหมดจากงานที่ยอมเสียได้กับจุดที่คนตรวจ นี่เป็นข้อเสนอออกแบบ ไม่ใช่ผลทดสอบ production

State/log อาจมีข้อมูลละเอียด ต้องกำหนดผู้เข้าถึง อายุการเก็บ และ secret handling ตัวอย่างนี้มีเพียงข้อความสมมติ ไม่มี tool เปลี่ยนข้อมูลภายนอก การไม่ฝัง secret ไม่แทน security review ของระบบบริษัท ส่วนต้นทุนจริงต้องมาจากจำนวน run/model calls/tool/storage ที่บันทึก ไม่ถือว่าเป็นศูนย์เพราะหน้านี้ไม่มีราคา

Task brief หกส่วนสำหรับนำไปใช้

ข้อความต่อไปนี้เป็น brief สมมติสำหรับฝึกกับไฟล์ตัวอย่าง อ่านหรือคัดลอกได้โดยไม่ต้องใช้ปุ่ม มันมอบหมายงานตรวจ branch เท่านั้น ไม่เพิ่มอำนาจเรียก Agent จริง

1. เป้าหมาย: ทำรายงาน fixture A แล้ว fork จากจุดก่อนรายงาน
   เพื่อเปรียบเทียบ fixture B โดยรักษาหลักฐานของ A
2. ข้อมูล/เครื่องมือ: Python 3.12+, LangGraph 1.2.15,
   local fixtures และ InMemorySaver; ไม่มี LLM/API/secret
3. ขอบเขตสิทธิ์: อ่านข้อมูลจำลองและทำงานใน RAM เท่านั้น
   ห้ามส่งข้อความ เผยแพร่ หรือเปลี่ยนข้อมูลภายนอก
4. สิ่งส่งมอบ: manifest พร้อม thread/checkpoint IDs,
   report A/B และ values ของ original ก่อน/หลัง fork
5. เกณฑ์ตรวจรับ: fork ใช้ B, original final config ยังได้ A,
   values เดิมเท่าเดิม และระบุจุดที่เลือกจาก next ได้
6. จุดหยุด: checkpoint ไม่พบ/มีหลายจุด/fixture ผิด schema
   ให้รายงานสาเหตุ; ถ้าต้องใช้ tool เขียนจริงให้ส่งคนตัดสิน

แบบฝึกหนึ่งข้อ: ทางเลือกถูกต้อง แต่จุดตรวจผิดได้ไหม

ทำ A ให้เสร็จ เก็บ original final config แล้ว fork B ตามตัวอย่าง เพิ่มคอลัมน์ใน manifest ว่าแต่ละการอ่านใช้ config ใด เปรียบเทียบการอ่านด้วย thread ID อย่างเดียวกับการอ่านด้วย original final config โดยไม่แก้โค้ดบังคับผล

ส่ง manifest หนึ่งชุดพร้อมเหตุผลว่าแถวใดตรวจ A และแถวใดบอกหัวประวัติปัจจุบัน Rubric ที่ออกแบบคือเลือกจุดก่อน formatter ได้, B ตรง fixture, A เท่าค่าเดิม และอธิบาย config ได้ หากคลุมเครือ ส่งรายงานหยุดพร้อม candidates แทน ยังแสดงทักษะการไม่เดาจุดย้อน

ห้ามให้คะแนนหรือ PASS จนรันและเก็บหลักฐานจริง แบบฝึกไม่ตรวจโมเดล เครื่องดับ ฐานข้อมูล หรือทุก branch ในระบบอื่น

Lab จำลอง: เห็นเส้นทางแล้วตรวจหลักฐาน

ฝึกเลือกจุดย้อนและตรวจ A/B ใน simulation ด้านล่าง ปุ่มไม่รัน Python/API หรือสร้าง LangGraph checkpoint จริง ต้องทดสอบโค้ดแยก

TOPIC 3D · SIMULATION · ไม่รัน LangGraph

อ่าน branch เดิมผ่าน config เดิม

เลือก C1 ก่อนจัดรูปแบบรายงาน แล้วดูสาขาทดลอง F1 แยกจาก C2 ของเดิม C0, C1, C2 และ F1 เป็นชื่อกับค่าที่ผู้เขียนตั้ง ไม่ใช่ checkpoint ID หรือ log ที่ได้จากการรันจริง ภาพนี้ลดขั้นตอนภายในให้เหลือความสัมพันธ์ที่ต้องตรวจ ไม่ใช่ผังของทุก checkpoint ที่ LangGraph สร้าง

จุดเริ่มที่ระบุด้วย checkpoint_id

C1: next=[format_report] · C2: next=[] ในข้อมูลสมมตินี้

การเลือก “ค่าปน” แสดงผลผิดที่สร้างขึ้นเพื่อฝึกตรวจรับ ไม่ได้จำลอง implementation ของ DeltaChannel หรือพิสูจน์บั๊กของเครื่องคุณ บั๊กในข่าวจำกัดที่ DeltaChannel + durability=”exit” + run ที่อ้าง checkpoint เก่าตาม PR9269 การอัปเกรดไม่ซ่อมค่าที่ปนไปแล้วโดยอัตโนมัติ

ต้นฉบับ C0 ไป C1 ไป C2; สาขาทดลองจาก C1 ไป F1

C0

รับคำขอ

รายงานจาก fixture A

C1 · จุดในอดีต

อ่าน fixture A แล้ว

ยังเหลือ format_report

C2 · ปลายเดิม

report=A · ไม่มี FORK_ONLY

เก็บ config ของจุดนี้ไว้ก่อน

F1 · สาขาทดลอง

report=B · FORK_ONLY

latest ในแบบฝึกชี้ F1

C1 → F1 เป็นภาพย่อของการอัปเดต state แล้วรัน downstream ต่อ

ผลตามกฎจำลอง: ค่า C2 เดิมคงอยู่

SIMULATION / ผลที่ตั้งไว้ ไม่ใช่ log จริง
selected_config = {thread_id: lesson, checkpoint_id: C1}
saved_original_config = {thread_id: lesson, checkpoint_id: C2}
original_before = {report: A, fork_only: false}
fork_result = {report: B, fork_only: true}
get_state(saved_original_config) = {report: A, fork_only: false}
comparison = unchanged ตามข้อมูลสมมตินี้
latest = F1; อ่าน latest อย่างเดียวไม่ใช่การตรวจ C2

หากปุ่ม ภาพสามมิติ หรือ JavaScript ไม่พร้อม ให้อ่านตารางด้านล่างเป็นผลตามกฎของแบบฝึก การตอบสนองของปุ่มและหน้าจอจริงยัง UNKNOWN การเปลี่ยนมุมภาพไม่เปลี่ยน state ของระบบภายนอก

ผลที่คาดหมายของข้อมูลสมมติ — ใช้เป็นทางอ่านเมื่อภาพไม่พร้อม
เลือกขั้นที่จะรันต่ออ่าน config C2 เดิมข้อสรุป
C1 / แยกสาขาสาขาทดลองได้ report Breport A, ไม่มี FORK_ONLYค่าเดิมไม่เปลี่ยนใน fixture นี้
C1 / ตัวอย่างค่าปนสาขาทดลองได้ report Breport B, มี FORK_ONLYพบความต่าง ต้องหยุดตรวจ ไม่รับรอง isolation
C2 / replaynext=[] จึงไม่มี downstream nodereport ANo-op ไม่ใช่การรัน formatter ใหม่

แหล่งกลไก: Checkpointers, Time travel และ PR9269 ค่าของ Lab เป็น SIMULATION; runtime, integration และการตรวจ browser ยังไม่ได้รัน

VISUAL INTERACTIVE แยกจากภาพ 3D · PLAN ONLY

เลือกวิธีกลับมาทำงาน ก่อนสั่งให้รันซ้ำ

Inspect อ่าน snapshot ส่วน Replay รัน downstream ใหม่ และ Fork เพิ่ม state ที่แก้แล้วเดินต่อ แบบฝึกนี้เพิ่มสถานการณ์สมมติของ node อ่าน fixture หรือ node ส่งงานภายนอก เพื่อเห็นว่าจุดเริ่มเดียวกันอาจต้องมีแผนต่างกัน ปุ่มสร้างเพียงคำอธิบาย ไม่เรียก API ไม่ส่งโพสต์ และไม่สร้าง checkpoint จริง

PLAN_ONLY · Fork กับ fixture

  1. เก็บ config และ values ของ C2 เดิม
  2. อ่าน C1 ที่ระบุชัด ไม่ใช้ latest แทน
  3. เตรียม update_state ที่ producer node ตรงกับ state
  4. รัน downstream กับ fixture ที่แก้ในแผนสมมติ
  5. อ่าน C2 ผ่าน config เดิมแล้วเทียบค่า

หากดำเนินการตามแผนจริง: downstream จะรันใหม่ ปุ่มนี้ยังไม่รันอะไร

ข้อมูลสมมติอยู่ในหน่วยความจำ ไม่มี network, LLM, credential หรือการส่งงาน

ถ้า interactive ไม่พร้อม ใช้ตารางกฎนี้ได้ทันที คำว่าแผนครบเงื่อนไขไม่ได้ยืนยันสิทธิ์จริง ปลายทางรองรับ idempotency หรือความปลอดภัยใน Production ผลทดสอบ code/runtime NOT RUN; browser UI UNKNOWN

เกณฑ์เลือกแผนของแบบฝึก — ไม่ใช่ security policy สำเร็จรูป
กรณีผลตามกฎเหตุผล
Inspect ทุก checkpointREAD_ONLY_PLANอ่าน values/config/next ไม่เรียก downstream
Replay C2 ที่ next=[]NO_OPไม่มี downstream ให้รัน ไม่ปลอม as_node ว่ามีงานค้าง
Fork C2 ในแบบฝึกนี้HOLD_EXAMPLE_SCOPEแบบฝึกกำหนด fork ก่อน formatter ที่ C1; ไม่ใช่ข้อห้ามของ LangGraph ทุกกรณี
Replay/Fork C1 กับ fixturePLAN_ONLYdownstream จะรันใหม่หากนำไป execute; ตอนนี้แค่แสดงแผน
External write ยังไม่อนุมัติSTOP_PERMISSIONความสามารถ replay ไม่ได้เพิ่มสิทธิ์ส่งงาน
External write ผลเดิมคลุมเครือSTOP_RECONCILEอ่านผลเดิมก่อน ห้ามสร้าง operation ID ใหม่เพื่อกลบความไม่รู้
External write พบผลเดิมสำเร็จSTOP_DUPLICATEใช้หลักฐานผลเดิม ไม่ส่งซ้ำเพื่อทดสอบ replay
External write อนุมัติ + มีแผน reconcile/idempotencyREADY_FOR_HUMAN_REVIEWแผนตามกฎสมมติครบ ยังไม่สั่งส่งหรือรับรอง exactly-once

อ่านต่อ: Replay และ Fork กับ Determinism / Idempotency การอ่านคืน task ที่บันทึกเสร็จระหว่าง resume ต่างจาก deliberate historical replay อย่าเหมารวมว่าการกลับมาทำงานทุกแบบจะใช้ผลเดิมหรือเรียกซ้ำเหมือนกัน

Video Agent 3D Lab: เปรียบเทียบ brief สองฉบับ

Lab วิดีโอที่ติดตั้งใช้เป็นฉากเปรียบเทียบ brief A/B ชี้จุด “หลังข้อมูล ก่อนทำบท” และจด manifest แต่ละฉบับแยก มันไม่ได้เพิ่ม checkpoint/fork ให้ปลั๊กอิน และไม่ทดสอบ persistence ของ LangGraph

Brief หกส่วน: เป้าหมาย:วิดีโออธิบาย A แล้วเสนอ B; ข้อมูล/เครื่องมือ:fixture สองชุดกับภาพ workflow; สิทธิ์:จำลองและร่างเท่านั้น; สิ่งส่งมอบ:brief A/B กับรายการขั้นที่ใช้แต่ละฉบับ; เกณฑ์:A ยังอ้าง A หลังร่าง B; จุดหยุด:ข้อมูลขาดหรือเสนอเผยแพร่จริงให้ส่งคนตรวจ อย่าอ้าง generator เติม brief ครบเอง

Fallback:เขียน A/B คนละคอลัมน์บนกระดาษ จดจุดร่วมและจุดแยก แล้วตรวจ A หลังแก้ B ตัวเลขเวลา/เงินใน Lab เป็นค่าตั้ง simulation ไม่ใช่ผลวัดหรือราคาปัจจุบัน ปุ่มและหน้าจอจริงยัง UNKNOWN

H✦HYBRID MINDINTERACTIVE EDITION LIVE EXPERIENCE
HYBRID MIND ORIGINAL / AI AGENT WORKFLOW / 2026

จากคลิปดิบ
สู่จักรวาลคอนเทนต์

ChatGPT ตัดต่อวิดีโอแทนคนได้จริงแค่ไหน? สำรวจระบบ AI Video Editing Agent ผ่านฉากสามมิติที่โต้ตอบได้ พร้อมแยกข้อเท็จจริง สร้างพรอมป์ต์ และจำลองต้นทุนก่อนลงมือทำจริง

06 EDITING STAGES03 CONTENT FORMATS01 INTERACTIVE LAB

ออกแบบระบบตัดต่อในมุมมอง 3D

ลากบนฉากเพื่อหมุนมุมมอง แตะโหนดเพื่อดูหน้าที่ของ AI ในแต่ละขั้นตอน

แผนภาพ 6 ขั้นตอน: Footage, Story, Edit, Captions, Graphics, Render
AGENT PIPELINE / VIEWPORT ORBIT 3D
DRAG / SWIPE TO ROTATE
SELECT NODE TO INSPECT
01 / 06นำเข้าคลิปดิบ

จัดระเบียบไฟล์ ตรวจคุณภาพภาพและเสียง และกำหนดสิทธิ์เข้าถึงให้ AI Agent

● SOURCE
จุดสำคัญ: AI Agent ทำหน้าที่วางแผนและควบคุมเครื่องมือ ส่วนการตัดต่อจริงต้องมีซอฟต์แวร์หรือระบบเรนเดอร์รองรับ ฉากนี้เป็น แบบจำลองเชิงอธิบาย ไม่ใช่ระบบอัปโหลดหรือตัดต่อวิดีโอ

แยกความสามารถจริงจากคำโฆษณา

กดแต่ละประเด็นเพื่ออ่านรายละเอียดและแหล่งข้อมูล

EVIDENCE-FIRST
✓ChatGPT Work เข้าถึงไฟล์ในเครื่องได้ตรวจสอบแล้ว / มีเงื่อนไข+
รองรับการทำงานกับโฟลเดอร์หรือแอปบนคอมพิวเตอร์เดสก์ท็อป เมื่อแผนและการตั้งค่าอนุญาตและผู้ใช้ให้สิทธิ์ ส่วน Work บนมือถือหรือเว็บไม่ได้เข้าถึงโฟลเดอร์ในคอมพิวเตอร์ของคุณโดยตรง ที่มา: OpenAI Help ↗
✓Astra ใช้โปรแกรมคอมพิวเตอร์ทำงานได้ตรวจสอบแล้ว / ไม่ใช่โปรแกรมตัดต่อในตัว+
OpenAI อธิบายความสามารถด้าน computer use ของ Astra รวมถึงงานสื่อที่ใช้เครื่องมือ แต่ต้องแยกบทบาท AI ผู้ควบคุมออกจากซอฟต์แวร์ประมวลผลวิดีโอและการเรนเดอร์ไฟล์ ที่มา: OpenAI ↗
?ตัดเสร็จใน 5–10 นาทีทุกกรณียังพิสูจน์เป็นเกณฑ์ทั่วไปไม่ได้+
เวลาขึ้นกับความยาวคลิป จำนวนทรานซิชัน คุณภาพซับ เครื่องมือ และประสิทธิภาพคอมพิวเตอร์ ควรใช้การจับเวลางานจริงแบบต้นทางเดียวกัน ไม่ใช้ตัวเลขจากโพสต์เป็นเกณฑ์อ้างอิง
?ถูกกว่าจ้างคน 10 เท่าต้องกำหนดต้นทุนให้เทียบกันได้+
ต้องรวมเวลาตรวจแก้ ลิขสิทธิ์เสียง เครื่องมือที่จ่ายเพิ่ม และต้นทุนโมเดล ยังไม่มีหลักฐานว่าประหยัด 10 เท่าสำหรับทุกงาน ทดลองสมมติฐานส่วนตัวได้ใน Cost Simulator ด้านล่าง

หนึ่งไอเดีย สามรูปแบบคอนเทนต์

ปรับรูปแบบวิดีโอ แล้วดูกรอบภาพตัวอย่างและ Prompt เปลี่ยนตามแบบเรียลไทม์

WORKING INTERACTIVE
15 SEC180 SEC
LIVE CREATIVE PREVIEW9:16 · 45 SEC
HYBRID MIND / AI
AI EDITOR / EXPLAINEDAI Agent
ช่วยตัดต่อ?
ใส่ซับตามประโยคได้อัตโนมัติ
▶◉↗
CONCEPT PREVIEW · NOT RENDERED VIDEO
VERTICAL · SHORT FORMVisual Mockup

พร้อมนำไปใช้กับ AI Agent

ตัวสร้าง Prompt นี้ไม่อัปโหลดคลิป ไม่เรียก API เสียเงิน และไม่ตัดต่อวิดีโอจริง ต้องใช้กับเครื่องมือที่ผู้ใช้อนุญาตภายหลัง

จำลองเวลาและต้นทุนของคุณเอง

เลื่อนตัวเลขเพื่อเปรียบเทียบสถานการณ์สมมติ ไม่ใช่ผล benchmark ที่ยืนยันแล้ว

SCENARIO · NOT BENCHMARK
ESTIMATED TIME SAVED11.0 ชม./เดือน
18.0 ชม.
7.0 ชม.
ส่วนต่างต้นทุนเวลา + เครื่องมือ (สมมติ)฿3,920

ตัวเลขเป็นตัวอย่างเพื่อช่วยตัดสินใจ ไม่ใช่ผลประหยัดที่รับประกัน

AI ที่ตัดคลิปเป็น ไม่ได้ทำงานลำพัง

สิ่งที่น่าสนใจของ GPT-6 Astra และ GPT-6.1 Sol ไม่ได้อยู่ที่ว่าตัวโมเดลมีปุ่ม Render MP4 ซ่อนอยู่ แต่คือความสามารถในการจัดการงานหลายขั้นตอนร่วมกับเครื่องมืออื่น: อ่านไฟล์ วางลำดับเรื่อง ใช้แอป ตรวจผลลัพธ์ และแก้งานตามข้อเสนอแนะของมนุษย์

สำหรับคนทำคอนเทนต์ ความต่างสำคัญคือการเปลี่ยนจาก “ส่งคลิปให้ AI แล้วหวังว่าจะได้ผลงาน” ไปเป็น Workflow ที่ตรวจรับได้ทุกขั้น ตั้งแต่การเลือกช่วงเด็ด การสร้างซับ การตรวจคำผิด จนถึงการเรนเดอร์ไฟล์ตามสัดส่วนของแต่ละแพลตฟอร์ม

ข้อควรระวังคือระบบ Work ที่รันบนเว็บหรือมือถือไม่ได้หมายความว่า AI เข้าถึงคลิปบนคอมพิวเตอร์บ้านได้ทันที ต้องทำงานในสภาพแวดล้อมที่เปิดให้เข้าถึงไฟล์จริง มีเครื่องมือตัดต่อ และมีสิทธิ์ใช้งานอย่างชัดเจน

01จัดคลิปให้พร้อม

รวบรวมไฟล์ในโฟลเดอร์ที่เข้าถึงได้ ระบุสิทธิ์และข้อจำกัด

02ตรวจสคริปต์และไทม์ไลน์

ให้ Agent เสนอ Hook, ช่วงตัด และโครงเรื่องก่อนตัดต่อ

03สร้างซับ + กราฟิก

ตรวจชื่อเฉพาะ ตัวเลข ภาษาไทย และความถูกต้องของคำกล่าวอ้าง

04ตรวจไฟล์ MP4 จริง

ตรวจความละเอียด เสียง ระยะเวลา safe zone และคำบรรยาย

Checklist ก่อนเพิ่มระบบจริง

  • เลือก replay/fork ให้ตรงคำถาม
  • เก็บ config และ values เดิมก่อนทำทางเลือก
  • เลือก checkpoint ด้วย next และ ID
  • ตรวจ node ของ patch และ parent lineage
  • อ่าน A/B ด้วย config ปลายแต่ละฉบับ
  • แยก expected กับผลทดสอบจริง

Checkpoint อาจเก็บข้อมูลที่ยังอ่านไม่ครบ ตรวจขอบเขตต้นทางเพิ่มใน Pagination: API ตอบ 200 แต่ Agent อ่านครบหรือยัง ก่อนใช้ข้อมูลเป็น state

FAQ

1. รุ่นนี้แก้ checkpoint ทุกกรณีหรือไม่

ยืนยันเฉพาะ change ที่ตรวจต้นทางได้ ไม่รับรองทุก storage/workload

2. Fork ต่างจากเริ่มงานใหม่อย่างไร

ระบุ state และจุดในอดีตที่ใช้ตั้งต้นได้ ตัวอย่างรักษา A แล้วเปลี่ยนข้อมูลก่อน formatter

3. ทำไมอ่าน latest ไม่พอ

มันตอบตำแหน่งปัจจุบัน ไม่ยืนยันค่า checkpoint ปลาย A ที่ตั้งใจตรวจ

4. อัปเกรดแล้วประวัติผิดจะหายเองไหม

PR9269ระบุว่าข้อมูล branch ที่ปนไปแล้วคงอยู่ ต้องตรวจและออกแบบการกู้แยก

5. เพิ่มเครื่องมือส่งจริงได้เลยไหม

ต้องกำหนดสิทธิ์และการตรวจผลก่อน checkpoint ไม่ undo ผลภายนอก

6. โค้ดและ Lab ผ่านหรือยัง

Code/tests NOT RUN; Lab เป็น simulation; UI UNKNOWN ไม่มีผล live/production ของผู้เขียน

Source code และคำแนะนำทดสอบ

ไฟล์เขียนสำหรับบทเรียนนี้ตาม public API เป็น local synthetic NOT RUN อ่าน README ก่อนติดตั้งเอง

code/checkpoint_fork.py · NOT RUN
"""Synthetic checkpoint/fork lesson. No model, network or external writes.

Editorial test status: NOT RUN. Assertions are acceptance checks for the reader
to execute; their presence is not evidence that this program has been tested.
"""
from __future__ import annotations

from copy import deepcopy
import json
from typing import Any, TypedDict
from uuid import uuid4

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph


FIXTURES = {
    "original": {"path": "A", "text": "เอกสารขั้นตอนเดิม"},
    "alternate": {"path": "B", "text": "เอกสารขั้นตอนทางเลือก"},
}


class LessonState(TypedDict, total=False):
    scenario: str
    fixture: dict[str, str]
    report: str


def read_fixture(state: LessonState) -> dict[str, Any]:
    """Mock tool node: select local synthetic data; no API call."""
    scenario = state.get("scenario")
    if scenario not in FIXTURES:
        raise ValueError("Unknown synthetic scenario.")
    return {"fixture": deepcopy(FIXTURES[scenario])}


def format_report(state: LessonState) -> dict[str, str]:
    """Pure formatter. Reject incomplete mock tool data."""
    fixture = state.get("fixture")
    if not isinstance(fixture, dict) or set(fixture) != {"path", "text"}:
        raise ValueError("Fixture must contain only path and text.")
    if any(not isinstance(value, str) or not value.strip()
           for value in fixture.values()):
        raise ValueError("Fixture values must be nonempty strings.")
    return {"report": f"เส้นทาง {fixture['path']} | {fixture['text']}"}


def build_graph() -> Any:
    builder = StateGraph(LessonState)
    builder.add_node("read_fixture", read_fixture)
    builder.add_node("format_report", format_report)
    builder.add_edge(START, "read_fixture")
    builder.add_edge("read_fixture", "format_report")
    builder.add_edge("format_report", END)
    return builder.compile(checkpointer=InMemorySaver())


def select_before_report(
    graph: Any,
    thread_config: dict[str, Any],
    checkpoint_id: str | None = None,
) -> Any:
    """Select by execution boundary, never by a history-list index.

    This lesson accepts exactly one matching checkpoint. With multiple turns or
    branches, the caller must specify an ID from the displayed history.
    """
    candidates = [
        snapshot
        for snapshot in graph.get_state_history(thread_config)
        if snapshot.next == ("format_report",)
        and (
            checkpoint_id is None
            or snapshot.config["configurable"]["checkpoint_id"] == checkpoint_id
        )
    ]
    if len(candidates) != 1:
        raise ValueError(
            "Expected one checkpoint before format_report; "
            "inspect history and supply its exact checkpoint_id."
        )
    return candidates[0]


def checkpoint_lineage(
    graph: Any, snapshot: Any, ancestor_config: dict[str, Any], limit: int = 12
) -> list[str]:
    """Read a bounded parent chain; stop if the expected ancestor is absent."""
    ancestor_id = ancestor_config["configurable"]["checkpoint_id"]
    ids: list[str] = []
    current = snapshot
    for _ in range(limit):
        current_id = current.config["configurable"]["checkpoint_id"]
        if current_id in ids:
            raise RuntimeError("Unexpected cycle in checkpoint lineage.")
        ids.append(current_id)
        if current_id == ancestor_id:
            return ids
        if current.parent_config is None:
            break
        current = graph.get_state(current.parent_config)
    raise RuntimeError("Fork start not found in the bounded parent chain.")


def run_demo() -> dict[str, Any]:
    graph = build_graph()
    thread_config = {"configurable": {"thread_id": str(uuid4())}}
    graph.invoke({"scenario": "original"}, thread_config)

    original_final = graph.get_state(thread_config)
    if original_final.next:
        raise RuntimeError("Original branch has not finished.")
    original_config = deepcopy(original_final.config)
    original_values = deepcopy(original_final.values)

    selected = select_before_report(graph, thread_config)
    fork_start_config = graph.update_state(
        selected.config,
        {
            "scenario": "alternate",
            "fixture": deepcopy(FIXTURES["alternate"]),
        },
        as_node="read_fixture",
    )
    if graph.get_state(fork_start_config).next != ("format_report",):
        raise RuntimeError("Fork is not at the expected boundary.")

    fork_output = graph.invoke(None, fork_start_config)

    # ONE WORKER demo: no other invocation can advance this thread between
    # invoke and this capture. A concurrent production thread needs its own
    # run ownership/correlation; its latest head is not necessarily our fork.
    fork_final = graph.get_state(thread_config)
    if fork_final.next:
        raise RuntimeError("Captured fork is not terminal.")
    fork_final_config = deepcopy(fork_final.config)
    lineage = checkpoint_lineage(graph, fork_final, fork_start_config)
    fork_readback = deepcopy(graph.get_state(fork_final_config).values)
    assert fork_readback == fork_output

    original_readback = deepcopy(graph.get_state(original_config).values)

    assert original_readback == original_values
    assert original_readback["report"] == (
        "เส้นทาง A | เอกสารขั้นตอนเดิม"
    )
    assert fork_output["report"] == (
        "เส้นทาง B | เอกสารขั้นตอนทางเลือก"
    )
    assert original_readback["fixture"]["path"] == "A"

    return {
        "mode": "synthetic_no_model_no_network",
        "thread_id": thread_config["configurable"]["thread_id"],
        "selected_checkpoint_id": selected.config["configurable"]["checkpoint_id"],
        "original_final_checkpoint_id": (
            original_config["configurable"]["checkpoint_id"]
        ),
        "fork_start_checkpoint_id": (
            fork_start_config["configurable"]["checkpoint_id"]
        ),
        "fork_final_checkpoint_id": (
            fork_final_config["configurable"]["checkpoint_id"]
        ),
        "fork_final_parent_checkpoint_id": (
            fork_final.parent_config["configurable"]["checkpoint_id"]
            if fork_final.parent_config else None
        ),
        "fork_lineage_checkpoint_ids": lineage,
        "original_report": original_readback["report"],
        "fork_report": fork_readback["report"],
        "original_unchanged": original_readback == original_values,
    }


if __name__ == "__main__":
    print(json.dumps(run_demo(), ensure_ascii=False, indent=2))

code/test_checkpoint_fork.py · NOT RUN
"""Synthetic acceptance tests. Editorial status: NOT RUN."""
from copy import deepcopy
import unittest

from checkpoint_fork import (
    FIXTURES,
    build_graph,
    run_demo,
    select_before_report,
)


class CheckpointForkTests(unittest.TestCase):
    def completed_graph(self):
        graph = build_graph()
        config = {"configurable": {"thread_id": self.id()}}
        graph.invoke({"scenario": "original"}, config)
        return graph, config

    def test_fork_returns_alternative_and_original_is_unchanged(self):
        result = run_demo()
        self.assertTrue(result["original_unchanged"])
        self.assertEqual(result["original_report"], "เส้นทาง A | เอกสารขั้นตอนเดิม")
        self.assertEqual(result["fork_report"], "เส้นทาง B | เอกสารขั้นตอนทางเลือก")
        self.assertNotEqual(
            result["original_final_checkpoint_id"],
            result["fork_start_checkpoint_id"],
        )
        self.assertNotEqual(
            result["fork_start_checkpoint_id"],
            result["fork_final_checkpoint_id"],
        )
        self.assertEqual(
            result["fork_lineage_checkpoint_ids"][0],
            result["fork_final_checkpoint_id"],
        )
        self.assertIn(
            result["fork_start_checkpoint_id"],
            result["fork_lineage_checkpoint_ids"],
        )

    def test_selection_requires_boundary_not_history_position(self):
        graph, config = self.completed_graph()
        selected = select_before_report(graph, config)
        self.assertEqual(selected.next, ("format_report",))
        selected_id = selected.config["configurable"]["checkpoint_id"]
        addressed = select_before_report(graph, config, selected_id)
        self.assertEqual(addressed.config, selected.config)

    def test_terminal_checkpoint_is_not_a_before_report_checkpoint(self):
        graph, config = self.completed_graph()
        terminal = graph.get_state(config)
        self.assertEqual(terminal.next, ())
        terminal_id = terminal.config["configurable"]["checkpoint_id"]
        with self.assertRaises(ValueError):
            select_before_report(graph, config, terminal_id)

    def test_unknown_checkpoint_id_stops_instead_of_guessing(self):
        graph, config = self.completed_graph()
        with self.assertRaises(ValueError):
            select_before_report(graph, config, "missing-checkpoint")

    def test_ambiguous_history_requires_explicit_checkpoint_id(self):
        graph, config = self.completed_graph()
        first = select_before_report(graph, config)
        first_id = first.config["configurable"]["checkpoint_id"]
        graph.invoke({"scenario": "alternate"}, config)
        with self.assertRaises(ValueError):
            select_before_report(graph, config)
        self.assertEqual(
            select_before_report(graph, config, first_id).config,
            first.config,
        )

    def test_failed_fork_leaves_addressed_original_values_unchanged(self):
        graph, config = self.completed_graph()
        original = graph.get_state(config)
        original_config = deepcopy(original.config)
        original_values = deepcopy(original.values)
        selected = select_before_report(graph, config)
        bad_fork = graph.update_state(
            selected.config,
            {"fixture": {"path": "B"}},  # Missing text: synthetic failure.
            as_node="read_fixture",
        )
        with self.assertRaises(ValueError):
            graph.invoke(None, bad_fork)
        self.assertEqual(graph.get_state(original_config).values, original_values)
        self.assertEqual(FIXTURES["original"]["path"], "A")


if __name__ == "__main__":
    unittest.main()

code/README.md · NOT RUN
# Checkpoint fork lesson — synthetic example

Editorial status: **NOT RUN**. The author has not executed, imported, compiled,
installed or tested these files. Expected output and tests describe acceptance
criteria for a reader's own authorized local run.

## Scope

Python 3.12+; LangGraph pinned to1.2.15 (package supports Python>=3.10).
No LLM, API key, environment variables, network requests, files/database writes,
publishing tools or paid calls exist in the example. The mock tool reads two
local fixtures. InMemorySaver stores checkpoints in RAM and loses them at
process exit. This is not a production restart-recovery or live-integration demo.

## Files

- checkpoint_fork.py: independently authored smallStateGraph and fork manifest.
- test_checkpoint_fork.py: six synthetic acceptance/failure tests.
- ../requirements.txt: langgraph==1.2.15; transitive requirements come from its package metadata.
- No pytest dependency: tests use Python standard-library unittest.

## Installation (reader instructions; NOT performed by the writer)

Run these commands in this run directory in a local environment you control.
Package installation accesses the package registry and changes your chosen
virtual environment; it is not part of the automatic editorial run.

Windows PowerShell:

```powershell
py -3.12 -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt
.venv\Scripts\python.exe code\checkpoint_fork.py
.venv\Scripts\python.exe -m unittest discover -s code -p "test_checkpoint_fork.py" -v
```

Linux/macOS withPython3.12:

```sh
python3.12 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python code/checkpoint_fork.py
.venv/bin/python -m unittest discover -s code -p 'test_checkpoint_fork.py' -v
```

No environment variable configuration is required. There is no model/provider
billing from this example; local resources and package download still exist.

## Input and expected output

Input inside run_demo: scenario="original". The controlled fork replaces the
recorded mockfixture with scenario="alternate" afterread_fixture and before
format_report. You may inspect or alter the fixtures locally.

Expected (NOT observed by the writer), omitting runtime-generated IDs:

```json
{
  "mode": "synthetic_no_model_no_network",
  "original_report": "เส้นทาง A | เอกสารขั้นตอนเดิม",
  "fork_report": "เส้นทาง B | เอกสารขั้นตอนทางเลือก",
  "original_unchanged": true
}
```

The executable emits thread, selected, original-final, fork-start, fork-final
and parent/lineage IDs too. Values are generated at a reader's runtime and must
not be copied as claimed logs. fork-start addresses the new checkpoint before
the formatter; fork-final addresses the captured terminal checkpoint.

ONE WORKER assumption: after invoke finishes, this demonstration alone can
advance its thread, so it captures the latest snapshot and stores its full config.
It verifies terminal next, expected parent ancestry through a bounded read chain,
and rereads the fork by that captured config. In a concurrent production thread,
latest cannot be assumed to belong to our fork: use explicit run ownership and
correlation before accepting it.

Acceptance: address the saved original-final config after the fork, compare
deepcopied values, show the alternative output, and record IDs. Selecting a
terminal checkpoint or an unknown/ambiguous checkpoint must stop rather than
guessing an index.

## Design and limits

- StateGraph -> read_fixture mocknode -> format_report pure node -> END.
- select_before_report selects snapshot.next==("format_report",).
- Multiple matches require a checkpoint_id chosen from inspected history.
- update_state usesas_node="read_fixture" because the patch replaces that
  node's already-recorded synthetic output; it does not call the mock tool again.
- invoke(None,fork_start_config) runs the downstream formatter.
- Original verification uses original_final.config, never the latest thread head.
- Fork final is captured only under ONE WORKER, then reread by fork_final.config;
  terminal next, fork-start lineage, and fork-output equality are checked.
- Standardstate channels are used; this is not a repro ofDeltaChannel's
  exit-durability regression and does not prove that bug fixed locally.
- Replay/fork can execute downstream node operations again in a real graph.
  Checkpoints do not undo external sends or publications. Do not add such a
  tool without a separate authorization/idempotency/reconciliation design.
- It intentionally tests one sequential graph without concurrent workers,
  subgraphs, durable storage, migrations, process crashes or model decisions.
- Python assert can be disabled withoptimization(-O); do not use -O for this
  acceptance demo, and use explicit validation exceptions in production logic.

## Sources

Official APIs and package contract:

- https://docs.langchain.com/oss/python/langgraph/use-time-travel
- https://docs.langchain.com/oss/python/langgraph/checkpointers
- https://github.com/langchain-ai/langgraph/blob/1.2.15/libs/langgraph/pyproject.toml
- https://github.com/langchain-ai/langgraph/blob/1.2.15/libs/langgraph/langgraph/graph/__init__.py

Original independent demonstration code; uses documented public API patterns.
Not a copied upstream test. No production-ready claim.

requirements.txt · NOT RUN
langgraph==1.2.15

แหล่งอ้างอิงต้นทาง

  1. LangGraph Python1.2.15 release notes —รายการแก้ไขของรุ่นที่ใช้ตั้งโจทย์
  2. GitHub Release API metadata —วันเผยแพร่ UTC และ commit ของ tag
  3. PR9269: isolate exit-run DeltaChannel writes from an existing newer branch —ขอบเขต fix, tradeoff และรายงานทดสอบของผู้อื่น
  4. LangGraph Persistence —ขอบเขต checkpointer/store และ RAM persistence
  5. LangGraph Checkpointers —state snapshot, history, update และ durability modes
  6. LangGraph Use time-travel —replay/fork/interrupt และขั้นตอนเดินต่อ
  7. Functional API: determinism and idempotency —task results, resume และการทำซ้ำเมื่อ task ไม่เสร็จ
  8. pyproject.toml ที่ tag1.2.15 —runtime/dependencies
  9. DeltaChannel source ที่ tag1.2.15 —beta และ reducer contract
  10. Upstream DeltaChannel exit-mode tests —รูปแบบ regression ต้นทาง ไม่ใช่ผลรันของผู้เขียน
  11. Graph public exports ที่ tag1.2.15 —ชื่อ imports ของตัวอย่าง

เอกสารปัจจุบันเปิดตรวจวันที่ 14 ตุลาคม 2026 แต่เวลาที่แก้แต่ละหน้าไม่ทราบ ส่วน source ที่เกี่ยวกับตัวอย่างตรึง tag1.2.15 หาก API เปลี่ยน ให้ตรวจรุ่นก่อนทำตาม ไม่ใช้บทความนี้รับรอง compatibility ของรุ่นใหม่โดยอัตโนมัติ

ใส่ความเห็น

Discover more from HYBRID MIND

Subscribe now to keep reading and get access to the full archive.

Continue reading