coco-framework 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. coco/__init__.py +3 -0
  2. coco/agents/__init__.py +9 -0
  3. coco/agents/_subagent_transformer.py +307 -0
  4. coco/agents/factory.py +2164 -0
  5. coco/agents/middleware/__init__.py +100 -0
  6. coco/agents/middleware/_execution.py +385 -0
  7. coco/agents/middleware/_redaction.py +454 -0
  8. coco/agents/middleware/_retry.py +134 -0
  9. coco/agents/middleware/_trace_policy.py +61 -0
  10. coco/agents/middleware/context_editing.py +306 -0
  11. coco/agents/middleware/file_search.py +433 -0
  12. coco/agents/middleware/human_in_the_loop.py +680 -0
  13. coco/agents/middleware/internal_call_transformer.py +108 -0
  14. coco/agents/middleware/model_call_limit.py +267 -0
  15. coco/agents/middleware/model_fallback.py +417 -0
  16. coco/agents/middleware/model_retry.py +327 -0
  17. coco/agents/middleware/pii.py +878 -0
  18. coco/agents/middleware/provider_tool_search.py +307 -0
  19. coco/agents/middleware/shell_tool.py +949 -0
  20. coco/agents/middleware/summarization.py +910 -0
  21. coco/agents/middleware/todo.py +357 -0
  22. coco/agents/middleware/tool_call_limit.py +495 -0
  23. coco/agents/middleware/tool_emulator.py +251 -0
  24. coco/agents/middleware/tool_error.py +209 -0
  25. coco/agents/middleware/tool_retry.py +421 -0
  26. coco/agents/middleware/tool_selection.py +515 -0
  27. coco/agents/middleware/types.py +2201 -0
  28. coco/agents/structured_output.py +472 -0
  29. coco/chat_models/__init__.py +7 -0
  30. coco/chat_models/base.py +1082 -0
  31. coco/embeddings/__init__.py +18 -0
  32. coco/embeddings/base.py +279 -0
  33. coco/mcp/__init__.py +36 -0
  34. coco/mcp/adapter.py +245 -0
  35. coco/mcp/elicitation.py +394 -0
  36. coco/mcp/tools.py +307 -0
  37. coco/messages/__init__.py +73 -0
  38. coco/py.typed +0 -0
  39. coco/rate_limiters/__init__.py +13 -0
  40. coco/tools/__init__.py +22 -0
  41. coco/tools/tool_node.py +20 -0
  42. coco_anthropic/__init__.py +15 -0
  43. coco_anthropic/_client_utils.py +81 -0
  44. coco_anthropic/_compat.py +228 -0
  45. coco_anthropic/_sdk_compat.py +124 -0
  46. coco_anthropic/_version.py +3 -0
  47. coco_anthropic/chat_models.py +3349 -0
  48. coco_anthropic/data/__init__.py +1 -0
  49. coco_anthropic/data/_profiles.py +491 -0
  50. coco_anthropic/data/profile_augmentations.toml +69 -0
  51. coco_anthropic/experimental.py +140 -0
  52. coco_anthropic/llms.py +428 -0
  53. coco_anthropic/middleware/__init__.py +25 -0
  54. coco_anthropic/middleware/anthropic_tools.py +1197 -0
  55. coco_anthropic/middleware/bash.py +84 -0
  56. coco_anthropic/middleware/file_search.py +360 -0
  57. coco_anthropic/middleware/prompt_caching.py +276 -0
  58. coco_anthropic/output_parsers.py +93 -0
  59. coco_anthropic/py.typed +0 -0
  60. coco_core/__init__.py +20 -0
  61. coco_core/_api/__init__.py +87 -0
  62. coco_core/_api/beta_decorator.py +266 -0
  63. coco_core/_api/deprecation.py +637 -0
  64. coco_core/_api/internal.py +23 -0
  65. coco_core/_api/path.py +50 -0
  66. coco_core/_import_utils.py +41 -0
  67. coco_core/_protocol.py +625 -0
  68. coco_core/_security/__init__.py +36 -0
  69. coco_core/_security/_exceptions.py +9 -0
  70. coco_core/_security/_policy.py +313 -0
  71. coco_core/_security/_ssrf_protection.py +155 -0
  72. coco_core/_security/_transport.py +254 -0
  73. coco_core/agents.py +258 -0
  74. coco_core/caches.py +272 -0
  75. coco_core/callbacks/__init__.py +132 -0
  76. coco_core/callbacks/base.py +1229 -0
  77. coco_core/callbacks/file.py +266 -0
  78. coco_core/callbacks/manager.py +2826 -0
  79. coco_core/callbacks/stdout.py +123 -0
  80. coco_core/callbacks/streaming_stdout.py +154 -0
  81. coco_core/callbacks/usage.py +124 -0
  82. coco_core/chat_history.py +267 -0
  83. coco_core/chat_loaders.py +26 -0
  84. coco_core/chat_sessions.py +19 -0
  85. coco_core/cross_encoders.py +18 -0
  86. coco_core/document_loaders/__init__.py +36 -0
  87. coco_core/document_loaders/base.py +155 -0
  88. coco_core/document_loaders/blob_loaders.py +38 -0
  89. coco_core/documents/__init__.py +55 -0
  90. coco_core/documents/base.py +347 -0
  91. coco_core/documents/compressor.py +74 -0
  92. coco_core/documents/transformers.py +79 -0
  93. coco_core/embeddings/__init__.py +31 -0
  94. coco_core/embeddings/embeddings.py +78 -0
  95. coco_core/embeddings/fake.py +129 -0
  96. coco_core/env.py +22 -0
  97. coco_core/example_selectors/__init__.py +47 -0
  98. coco_core/example_selectors/base.py +60 -0
  99. coco_core/example_selectors/length_based.py +131 -0
  100. coco_core/example_selectors/semantic_similarity.py +366 -0
  101. coco_core/exceptions.py +166 -0
  102. coco_core/globals.py +72 -0
  103. coco_core/indexing/__init__.py +53 -0
  104. coco_core/indexing/api.py +954 -0
  105. coco_core/indexing/base.py +661 -0
  106. coco_core/indexing/in_memory.py +104 -0
  107. coco_core/language_models/__init__.py +116 -0
  108. coco_core/language_models/_compat_bridge.py +844 -0
  109. coco_core/language_models/_utils.py +343 -0
  110. coco_core/language_models/base.py +500 -0
  111. coco_core/language_models/chat_model_stream.py +1525 -0
  112. coco_core/language_models/chat_models.py +2748 -0
  113. coco_core/language_models/fake.py +137 -0
  114. coco_core/language_models/fake_chat_models.py +396 -0
  115. coco_core/language_models/llms.py +1569 -0
  116. coco_core/language_models/model_profile.py +183 -0
  117. coco_core/load/__init__.py +44 -0
  118. coco_core/load/_validation.py +191 -0
  119. coco_core/load/dump.py +120 -0
  120. coco_core/load/load.py +828 -0
  121. coco_core/load/mapping.py +1097 -0
  122. coco_core/load/serializable.py +406 -0
  123. coco_core/messages/__init__.py +198 -0
  124. coco_core/messages/ai.py +854 -0
  125. coco_core/messages/base.py +518 -0
  126. coco_core/messages/block_translators/__init__.py +112 -0
  127. coco_core/messages/block_translators/anthropic.py +534 -0
  128. coco_core/messages/block_translators/bedrock.py +108 -0
  129. coco_core/messages/block_translators/bedrock_converse.py +329 -0
  130. coco_core/messages/block_translators/coco_v0.py +303 -0
  131. coco_core/messages/block_translators/google_genai.py +610 -0
  132. coco_core/messages/block_translators/google_vertexai.py +21 -0
  133. coco_core/messages/block_translators/groq.py +157 -0
  134. coco_core/messages/block_translators/openai.py +1089 -0
  135. coco_core/messages/chat.py +64 -0
  136. coco_core/messages/content.py +1488 -0
  137. coco_core/messages/function.py +62 -0
  138. coco_core/messages/human.py +70 -0
  139. coco_core/messages/modifier.py +33 -0
  140. coco_core/messages/system.py +70 -0
  141. coco_core/messages/tool.py +418 -0
  142. coco_core/messages/utils.py +2442 -0
  143. coco_core/output_parsers/__init__.py +101 -0
  144. coco_core/output_parsers/base.py +361 -0
  145. coco_core/output_parsers/format_instructions.py +16 -0
  146. coco_core/output_parsers/json.py +139 -0
  147. coco_core/output_parsers/list.py +249 -0
  148. coco_core/output_parsers/openai_functions.py +324 -0
  149. coco_core/output_parsers/openai_tools.py +383 -0
  150. coco_core/output_parsers/pydantic.py +141 -0
  151. coco_core/output_parsers/string.py +63 -0
  152. coco_core/output_parsers/transform.py +175 -0
  153. coco_core/output_parsers/xml.py +301 -0
  154. coco_core/outputs/__init__.py +66 -0
  155. coco_core/outputs/chat_generation.py +147 -0
  156. coco_core/outputs/chat_result.py +39 -0
  157. coco_core/outputs/generation.py +80 -0
  158. coco_core/outputs/llm_result.py +112 -0
  159. coco_core/outputs/run_info.py +22 -0
  160. coco_core/prompt_values.py +161 -0
  161. coco_core/prompts/__init__.py +101 -0
  162. coco_core/prompts/base.py +508 -0
  163. coco_core/prompts/chat.py +1495 -0
  164. coco_core/prompts/dict.py +177 -0
  165. coco_core/prompts/few_shot.py +483 -0
  166. coco_core/prompts/few_shot_with_templates.py +239 -0
  167. coco_core/prompts/image.py +177 -0
  168. coco_core/prompts/loading.py +296 -0
  169. coco_core/prompts/message.py +97 -0
  170. coco_core/prompts/prompt.py +312 -0
  171. coco_core/prompts/string.py +399 -0
  172. coco_core/prompts/structured.py +214 -0
  173. coco_core/py.typed +0 -0
  174. coco_core/rate_limiters.py +254 -0
  175. coco_core/retrievers.py +328 -0
  176. coco_core/runnables/__init__.py +136 -0
  177. coco_core/runnables/base.py +6725 -0
  178. coco_core/runnables/branch.py +457 -0
  179. coco_core/runnables/config.py +712 -0
  180. coco_core/runnables/configurable.py +710 -0
  181. coco_core/runnables/fallbacks.py +663 -0
  182. coco_core/runnables/graph.py +741 -0
  183. coco_core/runnables/graph_ascii.py +366 -0
  184. coco_core/runnables/graph_mermaid.py +503 -0
  185. coco_core/runnables/graph_png.py +215 -0
  186. coco_core/runnables/history.py +631 -0
  187. coco_core/runnables/passthrough.py +849 -0
  188. coco_core/runnables/retry.py +379 -0
  189. coco_core/runnables/router.py +239 -0
  190. coco_core/runnables/schema.py +188 -0
  191. coco_core/runnables/utils.py +798 -0
  192. coco_core/stores.py +291 -0
  193. coco_core/structured_query.py +203 -0
  194. coco_core/sys_info.py +154 -0
  195. coco_core/tools/__init__.py +95 -0
  196. coco_core/tools/base.py +1948 -0
  197. coco_core/tools/convert.py +497 -0
  198. coco_core/tools/render.py +67 -0
  199. coco_core/tools/retriever.py +94 -0
  200. coco_core/tools/simple.py +208 -0
  201. coco_core/tools/structured.py +322 -0
  202. coco_core/tracers/__init__.py +47 -0
  203. coco_core/tracers/_compat.py +95 -0
  204. coco_core/tracers/_context.py +337 -0
  205. coco_core/tracers/_streaming.py +53 -0
  206. coco_core/tracers/base.py +955 -0
  207. coco_core/tracers/coco.py +279 -0
  208. coco_core/tracers/context.py +205 -0
  209. coco_core/tracers/core.py +766 -0
  210. coco_core/tracers/event_stream.py +1101 -0
  211. coco_core/tracers/log_stream.py +765 -0
  212. coco_core/tracers/memory_stream.py +200 -0
  213. coco_core/tracers/root_listeners.py +130 -0
  214. coco_core/tracers/run_collector.py +40 -0
  215. coco_core/tracers/schemas.py +14 -0
  216. coco_core/tracers/stdout.py +205 -0
  217. coco_core/utils/__init__.py +111 -0
  218. coco_core/utils/_gateway.py +274 -0
  219. coco_core/utils/_merge.py +218 -0
  220. coco_core/utils/aiter.py +357 -0
  221. coco_core/utils/env.py +86 -0
  222. coco_core/utils/formatting.py +80 -0
  223. coco_core/utils/function_calling.py +869 -0
  224. coco_core/utils/html.py +132 -0
  225. coco_core/utils/image.py +15 -0
  226. coco_core/utils/input.py +82 -0
  227. coco_core/utils/interactive_env.py +12 -0
  228. coco_core/utils/iter.py +229 -0
  229. coco_core/utils/json.py +230 -0
  230. coco_core/utils/json_schema.py +273 -0
  231. coco_core/utils/mustache.py +706 -0
  232. coco_core/utils/pydantic.py +631 -0
  233. coco_core/utils/strings.py +69 -0
  234. coco_core/utils/usage.py +61 -0
  235. coco_core/utils/utils.py +528 -0
  236. coco_core/utils/uuid.py +57 -0
  237. coco_core/vectorstores/__init__.py +53 -0
  238. coco_core/vectorstores/base.py +1111 -0
  239. coco_core/vectorstores/in_memory.py +546 -0
  240. coco_core/vectorstores/utils.py +163 -0
  241. coco_core/version.py +3 -0
  242. coco_framework-0.1.0.dist-info/METADATA +184 -0
  243. coco_framework-0.1.0.dist-info/RECORD +400 -0
  244. coco_framework-0.1.0.dist-info/WHEEL +4 -0
  245. coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
  246. coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
  247. coco_graph/_internal/__init__.py +4 -0
  248. coco_graph/_internal/_cache.py +31 -0
  249. coco_graph/_internal/_config.py +474 -0
  250. coco_graph/_internal/_constants.py +140 -0
  251. coco_graph/_internal/_fields.py +213 -0
  252. coco_graph/_internal/_future.py +220 -0
  253. coco_graph/_internal/_pydantic.py +275 -0
  254. coco_graph/_internal/_queue.py +124 -0
  255. coco_graph/_internal/_replay.py +90 -0
  256. coco_graph/_internal/_retry.py +29 -0
  257. coco_graph/_internal/_runnable.py +969 -0
  258. coco_graph/_internal/_scratchpad.py +19 -0
  259. coco_graph/_internal/_serde.py +253 -0
  260. coco_graph/_internal/_timeout.py +25 -0
  261. coco_graph/_internal/_typing.py +54 -0
  262. coco_graph/cache/base/__init__.py +48 -0
  263. coco_graph/cache/base/py.typed +0 -0
  264. coco_graph/cache/memory/__init__.py +73 -0
  265. coco_graph/cache/redis/__init__.py +144 -0
  266. coco_graph/cache/sqlite/__init__.py +120 -0
  267. coco_graph/callbacks.py +394 -0
  268. coco_graph/channels/__init__.py +29 -0
  269. coco_graph/channels/any_value.py +72 -0
  270. coco_graph/channels/base.py +121 -0
  271. coco_graph/channels/binop.py +155 -0
  272. coco_graph/channels/delta.py +202 -0
  273. coco_graph/channels/ephemeral_value.py +79 -0
  274. coco_graph/channels/last_value.py +151 -0
  275. coco_graph/channels/named_barrier_value.py +167 -0
  276. coco_graph/channels/topic.py +94 -0
  277. coco_graph/channels/untracked_value.py +73 -0
  278. coco_graph/checkpoint/base/__init__.py +861 -0
  279. coco_graph/checkpoint/base/id.py +109 -0
  280. coco_graph/checkpoint/base/py.typed +0 -0
  281. coco_graph/checkpoint/memory/__init__.py +698 -0
  282. coco_graph/checkpoint/memory/py.typed +0 -0
  283. coco_graph/checkpoint/postgres/__init__.py +602 -0
  284. coco_graph/checkpoint/postgres/_ainternal.py +23 -0
  285. coco_graph/checkpoint/postgres/_internal.py +21 -0
  286. coco_graph/checkpoint/postgres/aio.py +691 -0
  287. coco_graph/checkpoint/postgres/base.py +666 -0
  288. coco_graph/checkpoint/postgres/py.typed +0 -0
  289. coco_graph/checkpoint/postgres/shallow.py +967 -0
  290. coco_graph/checkpoint/serde/__init__.py +0 -0
  291. coco_graph/checkpoint/serde/_msgpack.py +97 -0
  292. coco_graph/checkpoint/serde/base.py +64 -0
  293. coco_graph/checkpoint/serde/encrypted.py +80 -0
  294. coco_graph/checkpoint/serde/event_hooks.py +52 -0
  295. coco_graph/checkpoint/serde/jsonplus.py +883 -0
  296. coco_graph/checkpoint/serde/py.typed +0 -0
  297. coco_graph/checkpoint/serde/types.py +68 -0
  298. coco_graph/checkpoint/sqlite/__init__.py +645 -0
  299. coco_graph/checkpoint/sqlite/_delta.py +172 -0
  300. coco_graph/checkpoint/sqlite/aio.py +738 -0
  301. coco_graph/checkpoint/sqlite/py.typed +0 -0
  302. coco_graph/checkpoint/sqlite/utils.py +116 -0
  303. coco_graph/config.py +196 -0
  304. coco_graph/constants.py +64 -0
  305. coco_graph/errors.py +241 -0
  306. coco_graph/func/__init__.py +620 -0
  307. coco_graph/graph/__init__.py +12 -0
  308. coco_graph/graph/_branch.py +225 -0
  309. coco_graph/graph/_node.py +103 -0
  310. coco_graph/graph/message.py +437 -0
  311. coco_graph/graph/state.py +1978 -0
  312. coco_graph/graph/ui.py +227 -0
  313. coco_graph/managed/__init__.py +3 -0
  314. coco_graph/managed/base.py +31 -0
  315. coco_graph/managed/is_last_step.py +24 -0
  316. coco_graph/prebuilt/__init__.py +23 -0
  317. coco_graph/prebuilt/_tool_call_stream.py +117 -0
  318. coco_graph/prebuilt/_tool_call_transformer.py +165 -0
  319. coco_graph/prebuilt/chat_agent_executor.py +1015 -0
  320. coco_graph/prebuilt/interrupt.py +105 -0
  321. coco_graph/prebuilt/py.typed +0 -0
  322. coco_graph/prebuilt/tool_node.py +2030 -0
  323. coco_graph/prebuilt/tool_validator.py +221 -0
  324. coco_graph/pregel/__init__.py +3 -0
  325. coco_graph/pregel/_algo.py +1460 -0
  326. coco_graph/pregel/_call.py +298 -0
  327. coco_graph/pregel/_checkpoint.py +331 -0
  328. coco_graph/pregel/_config.py +0 -0
  329. coco_graph/pregel/_draw.py +294 -0
  330. coco_graph/pregel/_executor.py +223 -0
  331. coco_graph/pregel/_io.py +174 -0
  332. coco_graph/pregel/_log.py +3 -0
  333. coco_graph/pregel/_loop.py +1988 -0
  334. coco_graph/pregel/_messages.py +461 -0
  335. coco_graph/pregel/_read.py +312 -0
  336. coco_graph/pregel/_retry.py +854 -0
  337. coco_graph/pregel/_runner.py +941 -0
  338. coco_graph/pregel/_tools.py +268 -0
  339. coco_graph/pregel/_utils.py +221 -0
  340. coco_graph/pregel/_validate.py +120 -0
  341. coco_graph/pregel/_write.py +192 -0
  342. coco_graph/pregel/debug.py +302 -0
  343. coco_graph/pregel/main.py +4364 -0
  344. coco_graph/pregel/protocol.py +288 -0
  345. coco_graph/pregel/types.py +38 -0
  346. coco_graph/py.typed +0 -0
  347. coco_graph/runtime.py +350 -0
  348. coco_graph/store/base/__init__.py +1322 -0
  349. coco_graph/store/base/batch.py +371 -0
  350. coco_graph/store/base/embed.py +436 -0
  351. coco_graph/store/base/py.typed +0 -0
  352. coco_graph/store/memory/__init__.py +592 -0
  353. coco_graph/store/memory/py.typed +0 -0
  354. coco_graph/store/postgres/__init__.py +4 -0
  355. coco_graph/store/postgres/aio.py +592 -0
  356. coco_graph/store/postgres/base.py +1480 -0
  357. coco_graph/store/postgres/py.typed +0 -0
  358. coco_graph/store/sqlite/__init__.py +4 -0
  359. coco_graph/store/sqlite/aio.py +632 -0
  360. coco_graph/store/sqlite/base.py +1514 -0
  361. coco_graph/stream/__init__.py +45 -0
  362. coco_graph/stream/_convert.py +32 -0
  363. coco_graph/stream/_mux.py +523 -0
  364. coco_graph/stream/_types.py +330 -0
  365. coco_graph/stream/run_stream.py +735 -0
  366. coco_graph/stream/stream_channel.py +341 -0
  367. coco_graph/stream/transformers.py +1039 -0
  368. coco_graph/types.py +1079 -0
  369. coco_graph/typing.py +48 -0
  370. coco_graph/utils/__init__.py +1 -0
  371. coco_graph/utils/config.py +4 -0
  372. coco_graph/utils/runnable.py +3 -0
  373. coco_graph/version.py +13 -0
  374. coco_graph/warnings.py +69 -0
  375. coco_openai/__init__.py +20 -0
  376. coco_openai/_compat.py +45 -0
  377. coco_openai/_version.py +3 -0
  378. coco_openai/chat_models/__init__.py +6 -0
  379. coco_openai/chat_models/_client_utils.py +684 -0
  380. coco_openai/chat_models/_compat.py +525 -0
  381. coco_openai/chat_models/azure.py +1227 -0
  382. coco_openai/chat_models/base.py +5654 -0
  383. coco_openai/chat_models/codex.py +557 -0
  384. coco_openai/chatgpt_oauth.py +1063 -0
  385. coco_openai/data/__init__.py +1 -0
  386. coco_openai/data/_profiles.py +1655 -0
  387. coco_openai/data/profile_augmentations.toml +127 -0
  388. coco_openai/embeddings/__init__.py +6 -0
  389. coco_openai/embeddings/azure.py +240 -0
  390. coco_openai/embeddings/base.py +820 -0
  391. coco_openai/llms/__init__.py +6 -0
  392. coco_openai/llms/azure.py +249 -0
  393. coco_openai/llms/base.py +919 -0
  394. coco_openai/middleware/__init__.py +11 -0
  395. coco_openai/middleware/openai_moderation.py +484 -0
  396. coco_openai/output_parsers/__init__.py +9 -0
  397. coco_openai/output_parsers/tools.py +9 -0
  398. coco_openai/py.typed +0 -0
  399. coco_openai/tools/__init__.py +5 -0
  400. coco_openai/tools/custom_tool.py +111 -0
@@ -0,0 +1,861 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import logging
5
+ from collections.abc import AsyncIterator, Collection, Iterator, Mapping, Sequence
6
+ from typing import (
7
+ Any,
8
+ Generic,
9
+ Literal,
10
+ NamedTuple,
11
+ TypedDict,
12
+ TypeVar,
13
+ cast,
14
+ )
15
+
16
+ from coco_core.runnables import RunnableConfig
17
+ from typing_extensions import NotRequired
18
+
19
+ from coco_graph.checkpoint.base.id import uuid6
20
+ from coco_graph.checkpoint.serde.base import SerializerProtocol, maybe_add_typed_methods
21
+ from coco_graph.checkpoint.serde.encrypted import EncryptedSerializer
22
+ from coco_graph.checkpoint.serde.jsonplus import JsonPlusSerializer
23
+ from coco_graph.checkpoint.serde.types import (
24
+ ERROR,
25
+ INTERRUPT,
26
+ RESUME,
27
+ SCHEDULED,
28
+ ChannelProtocol,
29
+ )
30
+
31
+ V = TypeVar("V", int, float, str)
32
+ PendingWrite = tuple[str, str, Any]
33
+
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ # Marked as total=False to allow for future expansion.
39
+ class CheckpointMetadata(TypedDict, total=False):
40
+ """Metadata associated with a checkpoint."""
41
+
42
+ source: Literal["input", "loop", "update", "fork"]
43
+ """The source of the checkpoint.
44
+
45
+ - `"input"`: The checkpoint was created from an input to invoke/stream/batch.
46
+ - `"loop"`: The checkpoint was created from inside the pregel loop.
47
+ - `"update"`: The checkpoint was created from a manual state update.
48
+ - `"fork"`: The checkpoint was created as a copy of another checkpoint.
49
+ """
50
+ step: int
51
+ """The step number of the checkpoint.
52
+
53
+ `-1` for the first `"input"` checkpoint.
54
+ `0` for the first `"loop"` checkpoint.
55
+ `...` for the `nth` checkpoint afterwards.
56
+ """
57
+ parents: dict[str, str]
58
+ """The IDs of the parent checkpoints.
59
+
60
+ Mapping from checkpoint namespace to checkpoint ID.
61
+ """
62
+ run_id: str
63
+ """The ID of the run that created this checkpoint."""
64
+ counters_since_delta_snapshot: dict[str, tuple[int, int]]
65
+ """Per-channel counters since the last `_DeltaSnapshot` was written.
66
+
67
+ !!! warning "Beta"
68
+
69
+ This metadata field backs `DeltaChannel` (beta). The key name and
70
+ contents may change while the delta-channel design stabilizes.
71
+
72
+ Maps channel name -> `(updates, supersteps)`:
73
+
74
+ - index 0 (`updates`): number of supersteps that wrote to this channel
75
+ since its last snapshot blob.
76
+ - index 1 (`supersteps`): total supersteps elapsed since this channel's
77
+ last snapshot, regardless of whether the channel was written.
78
+
79
+ A snapshot fires when EITHER `updates >= ch.snapshot_frequency` OR
80
+ `supersteps >= DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT` (system-wide bound,
81
+ default 5000, env `COCO_GRAPH_DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT`).
82
+ The supersteps bound prevents unbounded ancestor walks on threads where
83
+ a delta channel exists but is no longer being updated.
84
+
85
+ Absent on threads that don't use delta channels. Persisted as a
86
+ 2-element list in JSON (no native tuple).
87
+ """
88
+
89
+
90
+ ChannelVersions = dict[str, str | int | float]
91
+
92
+
93
+ class Checkpoint(TypedDict):
94
+ """State snapshot at a given point in time."""
95
+
96
+ v: int
97
+ """The version of the checkpoint format. Currently `1`."""
98
+ id: str
99
+ """The ID of the checkpoint.
100
+
101
+ This is both unique and monotonically increasing, so can be used for sorting
102
+ checkpoints from first to last."""
103
+ ts: str
104
+ """The timestamp of the checkpoint in ISO 8601 format."""
105
+ channel_values: dict[str, Any]
106
+ """The values of the channels at the time of the checkpoint.
107
+
108
+ Mapping from channel name to deserialized channel snapshot value.
109
+ """
110
+ channel_versions: ChannelVersions
111
+ """The versions of the channels at the time of the checkpoint.
112
+
113
+ The keys are channel names and the values are monotonically increasing
114
+ version strings for each channel.
115
+ """
116
+ versions_seen: dict[str, ChannelVersions]
117
+ """Map from node ID to map from channel name to version seen.
118
+
119
+ This keeps track of the versions of the channels that each node has seen.
120
+ Used to determine which nodes to execute next.
121
+ """
122
+ updated_channels: list[str] | None
123
+ """The channels that were updated in this checkpoint.
124
+ """
125
+
126
+
127
+ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint:
128
+ return Checkpoint(
129
+ v=checkpoint["v"],
130
+ ts=checkpoint["ts"],
131
+ id=checkpoint["id"],
132
+ channel_values=checkpoint["channel_values"].copy(),
133
+ channel_versions=checkpoint["channel_versions"].copy(),
134
+ versions_seen={k: v.copy() for k, v in checkpoint["versions_seen"].items()},
135
+ pending_sends=checkpoint.get("pending_sends", []).copy(),
136
+ updated_channels=checkpoint.get("updated_channels", None),
137
+ )
138
+
139
+
140
+ class CheckpointTuple(NamedTuple):
141
+ """A tuple containing a checkpoint and its associated data."""
142
+
143
+ config: RunnableConfig
144
+ checkpoint: Checkpoint
145
+ metadata: CheckpointMetadata
146
+ parent_config: RunnableConfig | None = None
147
+ pending_writes: list[PendingWrite] | None = None
148
+
149
+
150
+ class DeltaChannelHistory(TypedDict):
151
+ """Per-channel result entry from `BaseCheckpointSaver.get_delta_channel_history`.
152
+
153
+ !!! warning "Beta"
154
+
155
+ Part of the `DeltaChannel` support surface; in beta. Field names and
156
+ semantics may change.
157
+
158
+ Storage-level view of what one channel contributed across the ancestor
159
+ chain of a target checkpoint:
160
+
161
+ * `writes` — on-path deltas oldest→newest as `PendingWrite` tuples.
162
+ Always present; possibly empty. Already filtered to one channel.
163
+ Writes stored at the target checkpoint itself are pending for the
164
+ next super-step and are excluded.
165
+ * `seed` — the stored value at the nearest ancestor whose
166
+ `channel_values[ch]` is populated. Omitted if the walk reached the
167
+ root without finding any stored value (consumer treats absence as
168
+ "start empty"). Typically a `_DeltaSnapshot` for delta channels with
169
+ finite snapshot frequency, or a plain value for threads migrated
170
+ from a pre-delta channel type.
171
+ """
172
+
173
+ writes: list[PendingWrite]
174
+ seed: NotRequired[Any]
175
+
176
+
177
+ class BaseCheckpointSaver(Generic[V]):
178
+ """Base class for creating a graph checkpointer.
179
+
180
+ Checkpointers allow CocoGraph agents to persist their state
181
+ within and across multiple interactions.
182
+
183
+ When a checkpointer is configured, you should pass a `thread_id` in the config when
184
+ invoking the graph:
185
+
186
+ ```python
187
+ config = {"configurable": {"thread_id": "my-thread"}}
188
+ graph.invoke(inputs, config)
189
+ ```
190
+
191
+ The `thread_id` is the primary key used to store and retrieve checkpoints. Without
192
+ it, the checkpointer cannot save state, resume from interrupts, or enable
193
+ time-travel debugging.
194
+
195
+ How you choose ``thread_id`` depends on your use case:
196
+
197
+ - **Single-shot workflows**: Use a unique ID (e.g., uuid4) for each run when
198
+ executions are independent.
199
+ - **Conversational memory**: Reuse the same `thread_id` across invocations
200
+ to accumulate state (e.g., chat history) within a conversation.
201
+
202
+ Attributes:
203
+ serde (SerializerProtocol): Serializer for encoding/decoding checkpoints.
204
+
205
+ Note:
206
+ When creating a custom checkpoint saver, consider implementing async
207
+ versions to avoid blocking the main thread.
208
+ """
209
+
210
+ serde: SerializerProtocol = JsonPlusSerializer()
211
+
212
+ def __init__(
213
+ self,
214
+ *,
215
+ serde: SerializerProtocol | None = None,
216
+ ) -> None:
217
+ self.serde = maybe_add_typed_methods(serde or self.serde)
218
+
219
+ @property
220
+ def config_specs(self) -> list:
221
+ """Define the configuration options for the checkpoint saver.
222
+
223
+ Returns:
224
+ list: List of configuration field specs.
225
+ """
226
+ return []
227
+
228
+ def get(self, config: RunnableConfig) -> Checkpoint | None:
229
+ """Fetch a checkpoint using the given configuration.
230
+
231
+ Args:
232
+ config: Configuration specifying which checkpoint to retrieve.
233
+
234
+ Returns:
235
+ The requested checkpoint, or `None` if not found.
236
+ """
237
+ if value := self.get_tuple(config):
238
+ return value.checkpoint
239
+
240
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
241
+ """Fetch a checkpoint tuple using the given configuration.
242
+
243
+ Args:
244
+ config: Configuration specifying which checkpoint to retrieve.
245
+
246
+ Returns:
247
+ The requested checkpoint tuple, or `None` if not found.
248
+
249
+ Raises:
250
+ NotImplementedError: Implement this method in your custom checkpoint saver.
251
+ """
252
+ raise NotImplementedError
253
+
254
+ def list(
255
+ self,
256
+ config: RunnableConfig | None,
257
+ *,
258
+ filter: dict[str, Any] | None = None,
259
+ before: RunnableConfig | None = None,
260
+ limit: int | None = None,
261
+ ) -> Iterator[CheckpointTuple]:
262
+ """List checkpoints that match the given criteria.
263
+
264
+ Args:
265
+ config: Base configuration for filtering checkpoints.
266
+ filter: Additional filtering criteria.
267
+ before: List checkpoints created before this configuration.
268
+ limit: Maximum number of checkpoints to return.
269
+
270
+ Returns:
271
+ Iterator of matching checkpoint tuples.
272
+
273
+ Raises:
274
+ NotImplementedError: Implement this method in your custom checkpoint saver.
275
+ """
276
+ raise NotImplementedError
277
+
278
+ def put(
279
+ self,
280
+ config: RunnableConfig,
281
+ checkpoint: Checkpoint,
282
+ metadata: CheckpointMetadata,
283
+ new_versions: ChannelVersions,
284
+ ) -> RunnableConfig:
285
+ """Store a checkpoint with its configuration and metadata.
286
+
287
+ Args:
288
+ config: Configuration for the checkpoint.
289
+ checkpoint: The checkpoint to store.
290
+ metadata: Additional metadata for the checkpoint.
291
+ new_versions: New channel versions as of this write.
292
+
293
+ Returns:
294
+ RunnableConfig: Updated configuration after storing the checkpoint.
295
+
296
+ Raises:
297
+ NotImplementedError: Implement this method in your custom checkpoint saver.
298
+ """
299
+ raise NotImplementedError
300
+
301
+ def put_writes(
302
+ self,
303
+ config: RunnableConfig,
304
+ writes: Sequence[tuple[str, Any]],
305
+ task_id: str,
306
+ task_path: str = "",
307
+ ) -> None:
308
+ """Store intermediate writes linked to a checkpoint.
309
+
310
+ Args:
311
+ config: Configuration of the related checkpoint.
312
+ writes: List of writes to store.
313
+ task_id: Identifier for the task creating the writes.
314
+ task_path: Path of the task creating the writes.
315
+
316
+ Raises:
317
+ NotImplementedError: Implement this method in your custom checkpoint saver.
318
+ """
319
+ raise NotImplementedError
320
+
321
+ def delete_thread(
322
+ self,
323
+ thread_id: str,
324
+ ) -> None:
325
+ """Delete all checkpoints and writes associated with a specific thread ID.
326
+
327
+ Args:
328
+ thread_id: The thread ID whose checkpoints should be deleted.
329
+ """
330
+ raise NotImplementedError
331
+
332
+ def delete_for_runs(
333
+ self,
334
+ run_ids: Sequence[str],
335
+ ) -> None:
336
+ """Delete all checkpoints and writes associated with the given run IDs.
337
+
338
+ Args:
339
+ run_ids: The run IDs whose checkpoints should be deleted.
340
+
341
+ !!! warning "DeltaChannel"
342
+
343
+ Deleting a run that produced ancestor `checkpoint_writes` — or
344
+ the only `_DeltaSnapshot` blob — for a still-live thread will
345
+ break reconstruction of any `DeltaChannel` whose history
346
+ depended on those rows. See the `DeltaChannel` note on `prune`
347
+ for safe-recovery strategies.
348
+ """
349
+ raise NotImplementedError
350
+
351
+ def copy_thread(
352
+ self,
353
+ source_thread_id: str,
354
+ target_thread_id: str,
355
+ ) -> None:
356
+ """Copy all checkpoints and writes from one thread to another.
357
+
358
+ Args:
359
+ source_thread_id: The thread ID to copy from.
360
+ target_thread_id: The thread ID to copy to.
361
+
362
+ !!! warning "DeltaChannel"
363
+
364
+ Implementations must copy the **complete** parent chain (all
365
+ ancestor checkpoints and their `checkpoint_writes`) — copying
366
+ only the head checkpoint will leave the target thread with
367
+ `DeltaChannel` state that cannot be reconstructed (no path back
368
+ to a `_DeltaSnapshot` ancestor). Equivalently, the copy must
369
+ include enough ancestors that every `DeltaChannel`-backed key
370
+ has either a `_DeltaSnapshot` in `channel_values` somewhere in
371
+ the chain, or a complete write history back to the chain root.
372
+ """
373
+ raise NotImplementedError
374
+
375
+ def prune(
376
+ self,
377
+ thread_ids: Sequence[str],
378
+ *,
379
+ strategy: str = "keep_latest",
380
+ ) -> None:
381
+ """Prune checkpoints for the given threads.
382
+
383
+ Args:
384
+ thread_ids: The thread IDs to prune.
385
+ strategy: The pruning strategy. `"keep_latest"` retains only the most
386
+ recent checkpoint per namespace. `"delete"` removes all checkpoints.
387
+
388
+ !!! warning "DeltaChannel"
389
+
390
+ Custom implementations must be `DeltaChannel`-aware. `DeltaChannel`
391
+ stores only a sentinel in `channel_values` for non-snapshot steps;
392
+ reconstruction walks the parent chain via
393
+ `get_delta_channel_history`, accumulating rows from
394
+ `checkpoint_writes` until it reaches an ancestor whose
395
+ `channel_values` contains a `_DeltaSnapshot` blob (written every
396
+ `snapshot_frequency` updates).
397
+
398
+ A naive `"keep_latest"` that drops intermediate checkpoints and
399
+ their writes can sever that chain: the surviving "latest"
400
+ checkpoint is rarely a snapshot point itself, so its delta
401
+ channels would silently reconstruct as empty (no error raised —
402
+ `get_delta_channel_history` simply returns no `seed`). Safe
403
+ options when the graph uses `DeltaChannel`:
404
+
405
+ * Walk back from each kept checkpoint and preserve every
406
+ ancestor (plus its `checkpoint_writes`) up to the nearest one
407
+ whose `channel_values` already contains a `_DeltaSnapshot` for
408
+ every `DeltaChannel`-backed key.
409
+ * Force a fresh snapshot on the kept checkpoint before deleting
410
+ ancestors — rewrite `channel_values[k] = _DeltaSnapshot(value)`
411
+ for each delta channel `k` (resolving `value` via the existing
412
+ ancestor walk first), then prune.
413
+ * Skip pruning threads whose graph uses `DeltaChannel` until one
414
+ of the above is implemented.
415
+ """
416
+ raise NotImplementedError
417
+
418
+ async def aget(self, config: RunnableConfig) -> Checkpoint | None:
419
+ """Asynchronously fetch a checkpoint using the given configuration.
420
+
421
+ Args:
422
+ config: Configuration specifying which checkpoint to retrieve.
423
+
424
+ Returns:
425
+ The requested checkpoint, or `None` if not found.
426
+ """
427
+ if value := await self.aget_tuple(config):
428
+ return value.checkpoint
429
+
430
+ async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
431
+ """Asynchronously fetch a checkpoint tuple using the given configuration.
432
+
433
+ Args:
434
+ config: Configuration specifying which checkpoint to retrieve.
435
+
436
+ Returns:
437
+ The requested checkpoint tuple, or `None` if not found.
438
+
439
+ Raises:
440
+ NotImplementedError: Implement this method in your custom checkpoint saver.
441
+ """
442
+ raise NotImplementedError
443
+
444
+ async def alist(
445
+ self,
446
+ config: RunnableConfig | None,
447
+ *,
448
+ filter: dict[str, Any] | None = None,
449
+ before: RunnableConfig | None = None,
450
+ limit: int | None = None,
451
+ ) -> AsyncIterator[CheckpointTuple]:
452
+ """Asynchronously list checkpoints that match the given criteria.
453
+
454
+ Args:
455
+ config: Base configuration for filtering checkpoints.
456
+ filter: Additional filtering criteria for metadata.
457
+ before: List checkpoints created before this configuration.
458
+ limit: Maximum number of checkpoints to return.
459
+
460
+ Returns:
461
+ Async iterator of matching checkpoint tuples.
462
+
463
+ Raises:
464
+ NotImplementedError: Implement this method in your custom checkpoint saver.
465
+ """
466
+ raise NotImplementedError
467
+ yield
468
+
469
+ async def aput(
470
+ self,
471
+ config: RunnableConfig,
472
+ checkpoint: Checkpoint,
473
+ metadata: CheckpointMetadata,
474
+ new_versions: ChannelVersions,
475
+ ) -> RunnableConfig:
476
+ """Asynchronously store a checkpoint with its configuration and metadata.
477
+
478
+ Args:
479
+ config: Configuration for the checkpoint.
480
+ checkpoint: The checkpoint to store.
481
+ metadata: Additional metadata for the checkpoint.
482
+ new_versions: New channel versions as of this write.
483
+
484
+ Returns:
485
+ RunnableConfig: Updated configuration after storing the checkpoint.
486
+
487
+ Raises:
488
+ NotImplementedError: Implement this method in your custom checkpoint saver.
489
+ """
490
+ raise NotImplementedError
491
+
492
+ async def aput_writes(
493
+ self,
494
+ config: RunnableConfig,
495
+ writes: Sequence[tuple[str, Any]],
496
+ task_id: str,
497
+ task_path: str = "",
498
+ ) -> None:
499
+ """Asynchronously store intermediate writes linked to a checkpoint.
500
+
501
+ Args:
502
+ config: Configuration of the related checkpoint.
503
+ writes: List of writes to store.
504
+ task_id: Identifier for the task creating the writes.
505
+ task_path: Path of the task creating the writes.
506
+
507
+ Raises:
508
+ NotImplementedError: Implement this method in your custom checkpoint saver.
509
+ """
510
+ raise NotImplementedError
511
+
512
+ async def adelete_thread(
513
+ self,
514
+ thread_id: str,
515
+ ) -> None:
516
+ """Delete all checkpoints and writes associated with a specific thread ID.
517
+
518
+ Args:
519
+ thread_id: The thread ID whose checkpoints should be deleted.
520
+ """
521
+ raise NotImplementedError
522
+
523
+ async def adelete_for_runs(
524
+ self,
525
+ run_ids: Sequence[str],
526
+ ) -> None:
527
+ """Asynchronously delete all checkpoints and writes for the given run IDs.
528
+
529
+ Args:
530
+ run_ids: The run IDs whose checkpoints should be deleted.
531
+
532
+ !!! warning "DeltaChannel"
533
+
534
+ See `delete_for_runs` — deleting rows a still-live thread's
535
+ `DeltaChannel` reconstruction depends on (writes between the
536
+ head and its nearest `_DeltaSnapshot` ancestor) will silently
537
+ corrupt that channel's state.
538
+ """
539
+ raise NotImplementedError
540
+
541
+ async def acopy_thread(
542
+ self,
543
+ source_thread_id: str,
544
+ target_thread_id: str,
545
+ ) -> None:
546
+ """Asynchronously copy all checkpoints and writes from one thread to another.
547
+
548
+ Args:
549
+ source_thread_id: The thread ID to copy from.
550
+ target_thread_id: The thread ID to copy to.
551
+
552
+ !!! warning "DeltaChannel"
553
+
554
+ See `copy_thread` — the copy must carry the complete parent
555
+ chain (or at least back to a `_DeltaSnapshot` ancestor for every
556
+ `DeltaChannel`) so the target thread can reconstruct delta
557
+ state.
558
+ """
559
+ raise NotImplementedError
560
+
561
+ async def aprune(
562
+ self,
563
+ thread_ids: Sequence[str],
564
+ *,
565
+ strategy: str = "keep_latest",
566
+ ) -> None:
567
+ """Asynchronously prune checkpoints for the given threads.
568
+
569
+ Args:
570
+ thread_ids: The thread IDs to prune.
571
+ strategy: The pruning strategy. `"keep_latest"` retains only the most
572
+ recent checkpoint per namespace. `"delete"` removes all checkpoints.
573
+
574
+ !!! warning "DeltaChannel"
575
+
576
+ See `prune` for the full `DeltaChannel` caveat. In short:
577
+ `"keep_latest"` must not drop ancestor checkpoints / writes that
578
+ sit between the kept checkpoint and the nearest `_DeltaSnapshot`
579
+ ancestor, or delta channels will silently reconstruct as empty.
580
+ """
581
+ raise NotImplementedError
582
+
583
+ def get_delta_channel_history(
584
+ self, *, config: RunnableConfig, channels: Sequence[str]
585
+ ) -> Mapping[str, DeltaChannelHistory]:
586
+ """Walk the parent chain returning per-channel writes + seed.
587
+
588
+ !!! warning "Beta"
589
+
590
+ This method is part of the `DeltaChannel` support surface and is
591
+ in beta. The signature, return shape (`DeltaChannelHistory`), and
592
+ interaction with `_DeltaSnapshot` blobs may change. Override at
593
+ your own risk; the default implementation will continue to work
594
+ against the public `BaseCheckpointSaver` contract.
595
+
596
+ For each requested channel, walks ancestors of the checkpoint
597
+ identified by `config` (following `parent_config`) and accumulates
598
+ `pending_writes` for that channel. The walk terminates per-channel
599
+ at the nearest ancestor whose `channel_values[ch]` is populated;
600
+ that value is returned as `seed`. If the walk reaches the root
601
+ without finding a stored value, `seed` is omitted from that
602
+ channel's entry — the consumer treats the absence as "start
603
+ empty."
604
+
605
+ Walks the **parent chain** (not `list(before=...)`): for forked
606
+ threads, only on-path ancestors contribute.
607
+
608
+ The default implementation walks `get_tuple` + `parent_config`
609
+ once for all channels — each ancestor visited once, not once per
610
+ channel. Savers with direct storage access (`InMemorySaver`,
611
+ `PostgresSaver`) override for performance; the return contract is
612
+ fixed here.
613
+
614
+ Args:
615
+ config: Configuration identifying the target checkpoint.
616
+ channels: Channel names to walk for. Empty → empty mapping.
617
+
618
+ Returns:
619
+ Per-channel `DeltaChannelHistory` for every name in `channels`.
620
+ """
621
+ if not channels:
622
+ return {}
623
+ collected_by_ch: dict[str, list[PendingWrite]] = {c: [] for c in channels}
624
+ seed_by_ch: dict[str, Any] = {}
625
+ remaining: set[str] = set(channels)
626
+ target_tuple = self.get_tuple(config)
627
+ cursor_config: RunnableConfig | None = (
628
+ target_tuple.parent_config if target_tuple else None
629
+ )
630
+ while cursor_config is not None and remaining:
631
+ tup = self.get_tuple(cursor_config)
632
+ if tup is None:
633
+ break
634
+ if tup.pending_writes:
635
+ for write in reversed(tup.pending_writes):
636
+ ch = write[1]
637
+ if ch in remaining:
638
+ collected_by_ch[ch].append(write)
639
+ for ch in list(remaining):
640
+ if ch in tup.checkpoint["channel_values"]:
641
+ seed_by_ch[ch] = tup.checkpoint["channel_values"][ch]
642
+ remaining.discard(ch)
643
+ cursor_config = tup.parent_config
644
+ result: dict[str, DeltaChannelHistory] = {}
645
+ for ch in channels:
646
+ entry: DeltaChannelHistory = {"writes": list(reversed(collected_by_ch[ch]))}
647
+ if ch in seed_by_ch:
648
+ entry["seed"] = seed_by_ch[ch]
649
+ result[ch] = entry
650
+ return result
651
+
652
+ async def aget_delta_channel_history(
653
+ self, *, config: RunnableConfig, channels: Sequence[str]
654
+ ) -> Mapping[str, DeltaChannelHistory]:
655
+ """Async version of `get_delta_channel_history`.
656
+
657
+ !!! warning "Beta"
658
+
659
+ This method is part of the `DeltaChannel` support surface and is
660
+ in beta. See `get_delta_channel_history` for caveats.
661
+ """
662
+ if not channels:
663
+ return {}
664
+ collected_by_ch: dict[str, list[PendingWrite]] = {c: [] for c in channels}
665
+ seed_by_ch: dict[str, Any] = {}
666
+ remaining: set[str] = set(channels)
667
+ target_tuple = await self.aget_tuple(config)
668
+ cursor_config: RunnableConfig | None = (
669
+ target_tuple.parent_config if target_tuple else None
670
+ )
671
+ while cursor_config is not None and remaining:
672
+ tup = await self.aget_tuple(cursor_config)
673
+ if tup is None:
674
+ break
675
+ if tup.pending_writes:
676
+ for write in reversed(tup.pending_writes):
677
+ ch = write[1]
678
+ if ch in remaining:
679
+ collected_by_ch[ch].append(write)
680
+ for ch in list(remaining):
681
+ if ch in tup.checkpoint["channel_values"]:
682
+ seed_by_ch[ch] = tup.checkpoint["channel_values"][ch]
683
+ remaining.discard(ch)
684
+ cursor_config = tup.parent_config
685
+ result: dict[str, DeltaChannelHistory] = {}
686
+ for ch in channels:
687
+ entry: DeltaChannelHistory = {"writes": list(reversed(collected_by_ch[ch]))}
688
+ if ch in seed_by_ch:
689
+ entry["seed"] = seed_by_ch[ch]
690
+ result[ch] = entry
691
+ return result
692
+
693
+ def get_next_version(self, current: V | None, channel: None) -> V:
694
+ """Generate the next version ID for a channel.
695
+
696
+ Default is to use integer versions, incrementing by `1`.
697
+
698
+ If you override, you can use `str`/`int`/`float` versions, as long as they are monotonically increasing.
699
+
700
+ Args:
701
+ current: The current version identifier (`int`, `float`, or `str`).
702
+ channel: Deprecated argument, kept for backwards compatibility.
703
+
704
+ Returns:
705
+ V: The next version identifier, which must be increasing.
706
+ """
707
+ if isinstance(current, str):
708
+ raise NotImplementedError
709
+ elif current is None:
710
+ return 1
711
+ else:
712
+ return current + 1
713
+
714
+ def with_allowlist(
715
+ self, extra_allowlist: Collection[tuple[str, ...]]
716
+ ) -> BaseCheckpointSaver[V]:
717
+ """Return a shallow clone with a derived msgpack allowlist."""
718
+ serde = _with_msgpack_allowlist(self.serde, extra_allowlist)
719
+ if serde is self.serde:
720
+ return self
721
+ clone = copy.copy(self)
722
+ clone.serde = maybe_add_typed_methods(serde)
723
+ return clone
724
+
725
+
726
+ def _with_msgpack_allowlist(
727
+ serde: SerializerProtocol, extra_allowlist: Collection[tuple[str, ...]]
728
+ ) -> SerializerProtocol:
729
+ if isinstance(serde, JsonPlusSerializer):
730
+ return serde.with_msgpack_allowlist(extra_allowlist)
731
+ if isinstance(serde, EncryptedSerializer):
732
+ inner = serde.serde
733
+ if isinstance(inner, JsonPlusSerializer):
734
+ updated_inner = inner.with_msgpack_allowlist(extra_allowlist)
735
+ if updated_inner is inner:
736
+ return serde
737
+ return EncryptedSerializer(serde.cipher, updated_inner)
738
+ logger.warning(
739
+ "Serializer %s does not support msgpack allowlist. "
740
+ "Strict msgpack deserialization will not be enforced.",
741
+ type(serde).__name__,
742
+ )
743
+ return serde
744
+
745
+
746
+ class EmptyChannelError(Exception):
747
+ """Raised when attempting to get the value of a channel that hasn't been updated
748
+ for the first time yet."""
749
+
750
+ pass
751
+
752
+
753
+ def get_checkpoint_id(config: RunnableConfig) -> str | None:
754
+ """Get checkpoint ID."""
755
+ return config["configurable"].get("checkpoint_id")
756
+
757
+
758
+ def get_checkpoint_metadata(
759
+ config: RunnableConfig, metadata: CheckpointMetadata
760
+ ) -> CheckpointMetadata:
761
+ """Get checkpoint metadata in a backwards-compatible manner."""
762
+ metadata = {
763
+ k: v.replace("\u0000", "") if isinstance(v, str) else v
764
+ for k, v in metadata.items()
765
+ }
766
+ for obj in (config.get("metadata"), config.get("configurable")):
767
+ if not obj:
768
+ continue
769
+ for key, v in obj.items():
770
+ if key in metadata or key in EXCLUDED_METADATA_KEYS or key.startswith("__"):
771
+ continue
772
+ elif isinstance(v, str):
773
+ metadata[key] = v.replace("\u0000", "")
774
+ elif isinstance(v, (int, bool, float)):
775
+ metadata[key] = v
776
+ return metadata
777
+
778
+
779
+ def get_serializable_checkpoint_metadata(
780
+ config: RunnableConfig, metadata: CheckpointMetadata
781
+ ) -> CheckpointMetadata:
782
+ """Get checkpoint metadata in a backwards-compatible manner."""
783
+ checkpoint_metadata = get_checkpoint_metadata(config, metadata)
784
+ if "writes" in checkpoint_metadata:
785
+ cast(dict[str, Any], checkpoint_metadata).pop("writes")
786
+ return checkpoint_metadata
787
+
788
+
789
+ """
790
+ Mapping from error type to error index.
791
+ Regular writes just map to their index in the list of writes being saved.
792
+ Special writes (e.g. errors) map to negative indices, to avoid those writes from
793
+ conflicting with regular writes.
794
+ Each Checkpointer implementation should use this mapping in put_writes.
795
+ """
796
+ WRITES_IDX_MAP = {ERROR: -1, SCHEDULED: -2, INTERRUPT: -3, RESUME: -4}
797
+
798
+ EXCLUDED_METADATA_KEYS = {
799
+ "thread_id",
800
+ "checkpoint_id",
801
+ "checkpoint_ns",
802
+ "checkpoint_map",
803
+ "coco_graph_step",
804
+ "coco_graph_node",
805
+ "coco_graph_triggers",
806
+ "coco_graph_path",
807
+ "coco_graph_checkpoint_ns",
808
+ }
809
+
810
+ # --- below are deprecated utilities used by past versions of CocoGraph ---
811
+
812
+ LATEST_VERSION = 2
813
+
814
+
815
+ def empty_checkpoint() -> Checkpoint:
816
+ from datetime import datetime, timezone
817
+
818
+ return Checkpoint(
819
+ v=LATEST_VERSION,
820
+ id=str(uuid6(clock_seq=-2)),
821
+ ts=datetime.now(timezone.utc).isoformat(),
822
+ channel_values={},
823
+ channel_versions={},
824
+ versions_seen={},
825
+ pending_sends=[],
826
+ updated_channels=None,
827
+ )
828
+
829
+
830
+ def create_checkpoint(
831
+ checkpoint: Checkpoint,
832
+ channels: Mapping[str, ChannelProtocol] | None,
833
+ step: int,
834
+ *,
835
+ id: str | None = None,
836
+ ) -> Checkpoint:
837
+ """Create a checkpoint for the given channels."""
838
+ from datetime import datetime, timezone
839
+
840
+ ts = datetime.now(timezone.utc).isoformat()
841
+ if channels is None:
842
+ values = checkpoint["channel_values"]
843
+ else:
844
+ values = {}
845
+ for k, v in channels.items():
846
+ if k not in checkpoint["channel_versions"]:
847
+ continue
848
+ try:
849
+ values[k] = v.checkpoint()
850
+ except EmptyChannelError:
851
+ pass
852
+ return Checkpoint(
853
+ v=LATEST_VERSION,
854
+ ts=ts,
855
+ id=id or str(uuid6(clock_seq=step)),
856
+ channel_values=values,
857
+ channel_versions=checkpoint["channel_versions"],
858
+ versions_seen=checkpoint["versions_seen"],
859
+ pending_sends=checkpoint.get("pending_sends", []),
860
+ updated_channels=None,
861
+ )