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,691 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections import defaultdict
5
+ from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
6
+ from contextlib import asynccontextmanager
7
+ from typing import Any, cast
8
+
9
+ from coco_core.runnables import RunnableConfig
10
+ from coco_graph.checkpoint.base import (
11
+ WRITES_IDX_MAP,
12
+ ChannelVersions,
13
+ Checkpoint,
14
+ CheckpointMetadata,
15
+ CheckpointTuple,
16
+ DeltaChannelHistory,
17
+ get_checkpoint_id,
18
+ get_serializable_checkpoint_metadata,
19
+ )
20
+ from coco_graph.checkpoint.serde.base import SerializerProtocol
21
+ from coco_graph.checkpoint.serde.types import _DeltaSnapshot
22
+ from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
23
+ from psycopg.rows import DictRow, dict_row
24
+ from psycopg.types.json import Jsonb
25
+ from psycopg_pool import AsyncConnectionPool
26
+
27
+ from coco_graph.checkpoint.postgres import _ainternal
28
+ from coco_graph.checkpoint.postgres.base import (
29
+ _DELTA_PAGE_SIZE,
30
+ BasePostgresSaver,
31
+ _build_delta_stage1_sql,
32
+ _build_delta_stage2_sql,
33
+ _DeltaStage2Row,
34
+ )
35
+ from coco_graph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
36
+
37
+ Conn = _ainternal.Conn # For backward compatibility
38
+
39
+
40
+ class AsyncPostgresSaver(BasePostgresSaver):
41
+ """Asynchronous checkpointer that stores checkpoints in a Postgres database."""
42
+
43
+ lock: asyncio.Lock
44
+
45
+ def __init__(
46
+ self,
47
+ conn: _ainternal.Conn,
48
+ pipe: AsyncPipeline | None = None,
49
+ serde: SerializerProtocol | None = None,
50
+ ) -> None:
51
+ super().__init__(serde=serde)
52
+ if isinstance(conn, AsyncConnectionPool) and pipe is not None:
53
+ raise ValueError(
54
+ "Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool."
55
+ )
56
+
57
+ self.conn = conn
58
+ self.pipe = pipe
59
+ self.lock = asyncio.Lock()
60
+ self.loop = asyncio.get_running_loop()
61
+ self.supports_pipeline = Capabilities().has_pipeline()
62
+
63
+ @classmethod
64
+ @asynccontextmanager
65
+ async def from_conn_string(
66
+ cls,
67
+ conn_string: str,
68
+ *,
69
+ pipeline: bool = False,
70
+ serde: SerializerProtocol | None = None,
71
+ ) -> AsyncIterator[AsyncPostgresSaver]:
72
+ """Create a new AsyncPostgresSaver instance from a connection string.
73
+
74
+ Args:
75
+ conn_string: The Postgres connection info string.
76
+ pipeline: whether to use AsyncPipeline
77
+
78
+ Returns:
79
+ AsyncPostgresSaver: A new AsyncPostgresSaver instance.
80
+ """
81
+ async with await AsyncConnection.connect(
82
+ conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
83
+ ) as conn:
84
+ if pipeline:
85
+ async with conn.pipeline() as pipe:
86
+ yield cls(conn=conn, pipe=pipe, serde=serde)
87
+ else:
88
+ yield cls(conn=conn, serde=serde)
89
+
90
+ async def setup(self) -> None:
91
+ """Set up the checkpoint database asynchronously.
92
+
93
+ This method creates the necessary tables in the Postgres database if they don't
94
+ already exist and runs database migrations. It MUST be called directly by the user
95
+ the first time checkpointer is used.
96
+ """
97
+ async with self._cursor() as cur:
98
+ await cur.execute(self.MIGRATIONS[0])
99
+ results = await cur.execute(
100
+ "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
101
+ )
102
+ row = await results.fetchone()
103
+ if row is None:
104
+ version = -1
105
+ else:
106
+ version = row["v"]
107
+ for v, migration in zip(
108
+ range(version + 1, len(self.MIGRATIONS)),
109
+ self.MIGRATIONS[version + 1 :],
110
+ strict=False,
111
+ ):
112
+ await cur.execute(migration)
113
+ await cur.execute(
114
+ "INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
115
+ )
116
+ if self.pipe:
117
+ await self.pipe.sync()
118
+
119
+ async def alist(
120
+ self,
121
+ config: RunnableConfig | None,
122
+ *,
123
+ filter: dict[str, Any] | None = None,
124
+ before: RunnableConfig | None = None,
125
+ limit: int | None = None,
126
+ ) -> AsyncIterator[CheckpointTuple]:
127
+ """List checkpoints from the database asynchronously.
128
+
129
+ This method retrieves a list of checkpoint tuples from the Postgres database based
130
+ on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
131
+
132
+ Args:
133
+ config: Base configuration for filtering checkpoints.
134
+ filter: Additional filtering criteria for metadata.
135
+ before: If provided, only checkpoints before the specified checkpoint ID are returned.
136
+ limit: Maximum number of checkpoints to return.
137
+
138
+ Yields:
139
+ An asynchronous iterator of matching checkpoint tuples.
140
+ """
141
+ where, args = self._search_where(config, filter, before)
142
+ query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
143
+ params = list(args)
144
+ if limit is not None:
145
+ query += " LIMIT %s"
146
+ params.append(int(limit))
147
+ # if we change this to use .stream() we need to make sure to close the cursor
148
+ async with self._cursor() as cur:
149
+ await cur.execute(query, params, binary=True)
150
+ values = await cur.fetchall()
151
+ if not values:
152
+ return
153
+ # migrate pending sends if necessary
154
+ if to_migrate := [
155
+ v
156
+ for v in values
157
+ if v["checkpoint"]["v"] < 4 and v["parent_checkpoint_id"]
158
+ ]:
159
+ await cur.execute(
160
+ self.SELECT_PENDING_SENDS_SQL,
161
+ (
162
+ values[0]["thread_id"],
163
+ [v["parent_checkpoint_id"] for v in to_migrate],
164
+ ),
165
+ )
166
+ grouped_by_parent = defaultdict(list)
167
+ for value in to_migrate:
168
+ grouped_by_parent[value["parent_checkpoint_id"]].append(value)
169
+ async for sends in cur:
170
+ for value in grouped_by_parent[sends["checkpoint_id"]]:
171
+ if value["channel_values"] is None:
172
+ value["channel_values"] = []
173
+ self._migrate_pending_sends(
174
+ sends["sends"],
175
+ value["checkpoint"],
176
+ value["channel_values"],
177
+ )
178
+ for value in values:
179
+ yield await self._load_checkpoint_tuple(value)
180
+
181
+ async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
182
+ """Get a checkpoint tuple from the database asynchronously.
183
+
184
+ This method retrieves a checkpoint tuple from the Postgres database based on the
185
+ provided config. If the config contains a `checkpoint_id` key, the checkpoint with
186
+ the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
187
+ for the given thread ID is retrieved.
188
+
189
+ Args:
190
+ config: The config to use for retrieving the checkpoint.
191
+
192
+ Returns:
193
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
194
+ """
195
+ thread_id = config["configurable"]["thread_id"]
196
+ checkpoint_id = get_checkpoint_id(config)
197
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
198
+ if checkpoint_id:
199
+ args: tuple[Any, ...] = (thread_id, checkpoint_ns, checkpoint_id)
200
+ where = "WHERE thread_id = %s AND checkpoint_ns = %s AND checkpoint_id = %s"
201
+ else:
202
+ args = (thread_id, checkpoint_ns)
203
+ where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1"
204
+
205
+ async with self._cursor() as cur:
206
+ await cur.execute(
207
+ self.SELECT_SQL + where,
208
+ args,
209
+ binary=True,
210
+ )
211
+ value = await cur.fetchone()
212
+ if value is None:
213
+ return None
214
+
215
+ # migrate pending sends if necessary
216
+ if value["checkpoint"]["v"] < 4 and value["parent_checkpoint_id"]:
217
+ await cur.execute(
218
+ self.SELECT_PENDING_SENDS_SQL,
219
+ (thread_id, [value["parent_checkpoint_id"]]),
220
+ )
221
+ if sends := await cur.fetchone():
222
+ if value["channel_values"] is None:
223
+ value["channel_values"] = []
224
+ self._migrate_pending_sends(
225
+ sends["sends"],
226
+ value["checkpoint"],
227
+ value["channel_values"],
228
+ )
229
+
230
+ return await self._load_checkpoint_tuple(value)
231
+
232
+ async def aput(
233
+ self,
234
+ config: RunnableConfig,
235
+ checkpoint: Checkpoint,
236
+ metadata: CheckpointMetadata,
237
+ new_versions: ChannelVersions,
238
+ ) -> RunnableConfig:
239
+ """Save a checkpoint to the database asynchronously.
240
+
241
+ This method saves a checkpoint to the Postgres database. The checkpoint is associated
242
+ with the provided config and its parent config (if any).
243
+
244
+ Args:
245
+ config: The config to associate with the checkpoint.
246
+ checkpoint: The checkpoint to save.
247
+ metadata: Additional metadata to save with the checkpoint.
248
+ new_versions: New channel versions as of this write.
249
+
250
+ Returns:
251
+ RunnableConfig: Updated configuration after storing the checkpoint.
252
+ """
253
+ configurable = config["configurable"].copy()
254
+ thread_id = configurable.pop("thread_id")
255
+ checkpoint_ns = configurable.pop("checkpoint_ns")
256
+ checkpoint_id = configurable.pop("checkpoint_id", None)
257
+
258
+ copy = checkpoint.copy()
259
+ copy["channel_values"] = copy["channel_values"].copy()
260
+ next_config = {
261
+ "configurable": {
262
+ "thread_id": thread_id,
263
+ "checkpoint_ns": checkpoint_ns,
264
+ "checkpoint_id": checkpoint["id"],
265
+ }
266
+ }
267
+
268
+ # inline primitive values in checkpoint table
269
+ # others are stored in blobs table
270
+ blob_values = {}
271
+ for k, v in checkpoint["channel_values"].items():
272
+ if isinstance(v, _DeltaSnapshot):
273
+ blob_values[k] = copy["channel_values"].pop(k)
274
+ copy["channel_values"][k] = True
275
+ elif v is None or isinstance(v, (str, int, float, bool)):
276
+ pass
277
+ else:
278
+ blob_values[k] = copy["channel_values"].pop(k)
279
+
280
+ async with self._cursor(pipeline=True) as cur:
281
+ if blob_versions := {
282
+ k: v for k, v in new_versions.items() if k in blob_values
283
+ }:
284
+ await cur.executemany(
285
+ self.UPSERT_CHECKPOINT_BLOBS_SQL,
286
+ await asyncio.to_thread(
287
+ self._dump_blobs,
288
+ thread_id,
289
+ checkpoint_ns,
290
+ blob_values,
291
+ blob_versions,
292
+ ),
293
+ )
294
+ await cur.execute(
295
+ self.UPSERT_CHECKPOINTS_SQL,
296
+ (
297
+ thread_id,
298
+ checkpoint_ns,
299
+ checkpoint["id"],
300
+ checkpoint_id,
301
+ Jsonb(copy),
302
+ Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
303
+ ),
304
+ )
305
+ return next_config
306
+
307
+ async def aput_writes(
308
+ self,
309
+ config: RunnableConfig,
310
+ writes: Sequence[tuple[str, Any]],
311
+ task_id: str,
312
+ task_path: str = "",
313
+ ) -> None:
314
+ """Store intermediate writes linked to a checkpoint asynchronously.
315
+
316
+ This method saves intermediate writes associated with a checkpoint to the database.
317
+
318
+ Args:
319
+ config: Configuration of the related checkpoint.
320
+ writes: List of writes to store, each as (channel, value) pair.
321
+ task_id: Identifier for the task creating the writes.
322
+ """
323
+ query = (
324
+ self.UPSERT_CHECKPOINT_WRITES_SQL
325
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
326
+ else self.INSERT_CHECKPOINT_WRITES_SQL
327
+ )
328
+ params = await asyncio.to_thread(
329
+ self._dump_writes,
330
+ config["configurable"]["thread_id"],
331
+ config["configurable"]["checkpoint_ns"],
332
+ config["configurable"]["checkpoint_id"],
333
+ task_id,
334
+ task_path,
335
+ writes,
336
+ )
337
+ async with self._cursor(pipeline=True) as cur:
338
+ await cur.executemany(query, params)
339
+
340
+ async def adelete_thread(self, thread_id: str) -> None:
341
+ """Delete all checkpoints and writes associated with a thread ID.
342
+
343
+ Args:
344
+ thread_id: The thread ID to delete.
345
+
346
+ Returns:
347
+ None
348
+ """
349
+ async with self._cursor(pipeline=True) as cur:
350
+ await cur.execute(
351
+ "DELETE FROM checkpoints WHERE thread_id = %s",
352
+ (str(thread_id),),
353
+ )
354
+ await cur.execute(
355
+ "DELETE FROM checkpoint_blobs WHERE thread_id = %s",
356
+ (str(thread_id),),
357
+ )
358
+ await cur.execute(
359
+ "DELETE FROM checkpoint_writes WHERE thread_id = %s",
360
+ (str(thread_id),),
361
+ )
362
+
363
+ @asynccontextmanager
364
+ async def _cursor(
365
+ self, *, pipeline: bool = False
366
+ ) -> AsyncIterator[AsyncCursor[DictRow]]:
367
+ """Create a database cursor as a context manager.
368
+
369
+ Args:
370
+ pipeline: whether to use pipeline for the DB operations inside the context manager.
371
+ Will be applied regardless of whether the AsyncPostgresSaver instance was initialized with a pipeline.
372
+ If pipeline mode is not supported, will fall back to using transaction context manager.
373
+ """
374
+ async with self.lock, _ainternal.get_connection(self.conn) as conn:
375
+ if self.pipe:
376
+ # a connection in pipeline mode can be used concurrently
377
+ # in multiple threads/coroutines, but only one cursor can be
378
+ # used at a time
379
+ try:
380
+ async with conn.cursor(binary=True, row_factory=dict_row) as cur:
381
+ yield cur
382
+ finally:
383
+ if pipeline:
384
+ await self.pipe.sync()
385
+ elif pipeline:
386
+ # a connection not in pipeline mode can only be used by one
387
+ # thread/coroutine at a time, so we acquire a lock
388
+ if self.supports_pipeline:
389
+ async with (
390
+ conn.pipeline(),
391
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
392
+ ):
393
+ yield cur
394
+ else:
395
+ # Use connection's transaction context manager when pipeline mode not supported
396
+ async with (
397
+ conn.transaction(),
398
+ conn.cursor(binary=True, row_factory=dict_row) as cur,
399
+ ):
400
+ yield cur
401
+ else:
402
+ async with conn.cursor(binary=True, row_factory=dict_row) as cur:
403
+ yield cur
404
+
405
+ async def aget_delta_channel_history(
406
+ self, *, config: RunnableConfig, channels: Sequence[str]
407
+ ) -> Mapping[str, DeltaChannelHistory]:
408
+ """Fast-path override of `BaseCheckpointSaver.aget_delta_channel_history`.
409
+
410
+ See `PostgresSaver.get_delta_channel_history` for design notes; this is
411
+ the async equivalent with internal stage-1 paging and per-channel
412
+ UNION ALL stage-2.
413
+ """
414
+ if not channels:
415
+ return {}
416
+ channels = list(channels)
417
+ thread_id = config["configurable"]["thread_id"]
418
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
419
+ checkpoint_id = get_checkpoint_id(config)
420
+ if checkpoint_id is None:
421
+ target = await self.aget_tuple(config)
422
+ if target is None:
423
+ return {ch: {"writes": []} for ch in channels}
424
+ checkpoint_id = target.config["configurable"]["checkpoint_id"]
425
+
426
+ stage1_sql = _build_delta_stage1_sql(channels, paged=True)
427
+ parent_of: dict[str, str | None] = {}
428
+ ver_by_i_by_cid: list[dict[str, str | None]] = [{} for _ in channels]
429
+ hb_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
430
+ inline_by_i_by_cid: list[dict[str, Any]] = [{} for _ in channels]
431
+ chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
432
+ seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
433
+ seed_inline_by_ch: dict[str, Any] = {}
434
+ walk_cursor_by_ch: dict[str, str | None] = {}
435
+ seeded: set[str] = set()
436
+ cursor: str | None = None
437
+
438
+ async with self._cursor() as cur:
439
+ while True:
440
+ stage1_params: list[Any] = []
441
+ for ch in channels:
442
+ # ver_i, blob channel, blob version, inline_i
443
+ stage1_params.extend([ch, ch, ch, ch])
444
+ stage1_params.extend(
445
+ [thread_id, checkpoint_ns, cursor, cursor, _DELTA_PAGE_SIZE]
446
+ )
447
+ await cur.execute(stage1_sql, stage1_params)
448
+ page = await cur.fetchall()
449
+ if not page:
450
+ break
451
+ oldest = self._ingest_stage1_page(
452
+ cast("list[Mapping[str, Any]]", page),
453
+ channels,
454
+ parent_of,
455
+ ver_by_i_by_cid,
456
+ hb_by_i_by_cid,
457
+ inline_by_i_by_cid,
458
+ )
459
+ self._try_advance_walks(
460
+ checkpoint_id,
461
+ channels,
462
+ parent_of,
463
+ ver_by_i_by_cid,
464
+ hb_by_i_by_cid,
465
+ inline_by_i_by_cid,
466
+ chain_by_ch,
467
+ seed_ver_by_ch,
468
+ seed_inline_by_ch,
469
+ walk_cursor_by_ch,
470
+ seeded,
471
+ )
472
+ if len(seeded) == len(channels) or len(page) < _DELTA_PAGE_SIZE:
473
+ break
474
+ cursor = oldest
475
+
476
+ channels_with_chain = [ch for ch in channels if chain_by_ch[ch]]
477
+ channels_with_seed = [ch for ch in channels if seed_ver_by_ch[ch] is not None]
478
+ stage2_sql = _build_delta_stage2_sql(
479
+ channels_with_chain=channels_with_chain,
480
+ channels_with_seed=channels_with_seed,
481
+ )
482
+
483
+ if stage2_sql:
484
+ stage2_params: list[Any] = []
485
+ for ch in channels_with_chain:
486
+ stage2_params.extend([thread_id, checkpoint_ns, ch, chain_by_ch[ch]])
487
+ for ch in channels_with_seed:
488
+ stage2_params.extend([thread_id, checkpoint_ns, ch, seed_ver_by_ch[ch]])
489
+ async with self._cursor() as cur:
490
+ await cur.execute(stage2_sql, stage2_params)
491
+ stage2_rows = await cur.fetchall()
492
+ else:
493
+ stage2_rows = []
494
+
495
+ return self._build_delta_channels_writes_history(
496
+ channels=channels,
497
+ chain_by_ch=chain_by_ch,
498
+ seed_ver_by_ch=seed_ver_by_ch,
499
+ seed_inline_by_ch=seed_inline_by_ch,
500
+ stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
501
+ )
502
+
503
+ async def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
504
+ """
505
+ Convert a database row into a CheckpointTuple object.
506
+
507
+ Args:
508
+ value: A row from the database containing checkpoint data.
509
+
510
+ Returns:
511
+ CheckpointTuple: A structured representation of the checkpoint,
512
+ including its configuration, metadata, parent checkpoint (if any),
513
+ and pending writes.
514
+ """
515
+ return CheckpointTuple(
516
+ {
517
+ "configurable": {
518
+ "thread_id": value["thread_id"],
519
+ "checkpoint_ns": value["checkpoint_ns"],
520
+ "checkpoint_id": value["checkpoint_id"],
521
+ }
522
+ },
523
+ {
524
+ **value["checkpoint"],
525
+ "channel_values": {
526
+ **(value["checkpoint"].get("channel_values") or {}),
527
+ **self._load_blobs(value["channel_values"]),
528
+ },
529
+ },
530
+ value["metadata"],
531
+ (
532
+ {
533
+ "configurable": {
534
+ "thread_id": value["thread_id"],
535
+ "checkpoint_ns": value["checkpoint_ns"],
536
+ "checkpoint_id": value["parent_checkpoint_id"],
537
+ }
538
+ }
539
+ if value["parent_checkpoint_id"]
540
+ else None
541
+ ),
542
+ await asyncio.to_thread(self._load_writes, value["pending_writes"]),
543
+ )
544
+
545
+ def list(
546
+ self,
547
+ config: RunnableConfig | None,
548
+ *,
549
+ filter: dict[str, Any] | None = None,
550
+ before: RunnableConfig | None = None,
551
+ limit: int | None = None,
552
+ ) -> Iterator[CheckpointTuple]:
553
+ """List checkpoints from the database.
554
+
555
+ This method retrieves a list of checkpoint tuples from the Postgres database based
556
+ on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
557
+
558
+ Args:
559
+ config: Base configuration for filtering checkpoints.
560
+ filter: Additional filtering criteria for metadata.
561
+ before: If provided, only checkpoints before the specified checkpoint ID are returned.
562
+ limit: Maximum number of checkpoints to return.
563
+
564
+ Yields:
565
+ An iterator of matching checkpoint tuples.
566
+ """
567
+ try:
568
+ # check if we are in the main thread, only bg threads can block
569
+ # we don't check in other methods to avoid the overhead
570
+ if asyncio.get_running_loop() is self.loop:
571
+ raise asyncio.InvalidStateError(
572
+ "Synchronous calls to AsyncPostgresSaver are only allowed from a "
573
+ "different thread. From the main thread, use the async interface. "
574
+ "For example, use `checkpointer.alist(...)` or `await "
575
+ "graph.ainvoke(...)`."
576
+ )
577
+ except RuntimeError:
578
+ pass
579
+ aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
580
+ while True:
581
+ try:
582
+ yield asyncio.run_coroutine_threadsafe(
583
+ anext(aiter_), # type: ignore[arg-type]
584
+ self.loop,
585
+ ).result()
586
+ except StopAsyncIteration:
587
+ break
588
+
589
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
590
+ """Get a checkpoint tuple from the database.
591
+
592
+ This method retrieves a checkpoint tuple from the Postgres database based on the
593
+ provided config. If the config contains a `checkpoint_id` key, the checkpoint with
594
+ the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
595
+ for the given thread ID is retrieved.
596
+
597
+ Args:
598
+ config: The config to use for retrieving the checkpoint.
599
+
600
+ Returns:
601
+ The retrieved checkpoint tuple, or None if no matching checkpoint was found.
602
+ """
603
+ try:
604
+ # check if we are in the main thread, only bg threads can block
605
+ # we don't check in other methods to avoid the overhead
606
+ if asyncio.get_running_loop() is self.loop:
607
+ raise asyncio.InvalidStateError(
608
+ "Synchronous calls to AsyncPostgresSaver are only allowed from a "
609
+ "different thread. From the main thread, use the async interface. "
610
+ "For example, use `await checkpointer.aget_tuple(...)` or `await "
611
+ "graph.ainvoke(...)`."
612
+ )
613
+ except RuntimeError:
614
+ pass
615
+ return asyncio.run_coroutine_threadsafe(
616
+ self.aget_tuple(config), self.loop
617
+ ).result()
618
+
619
+ def put(
620
+ self,
621
+ config: RunnableConfig,
622
+ checkpoint: Checkpoint,
623
+ metadata: CheckpointMetadata,
624
+ new_versions: ChannelVersions,
625
+ ) -> RunnableConfig:
626
+ """Save a checkpoint to the database.
627
+
628
+ This method saves a checkpoint to the Postgres database. The checkpoint is associated
629
+ with the provided config and its parent config (if any).
630
+
631
+ Args:
632
+ config: The config to associate with the checkpoint.
633
+ checkpoint: The checkpoint to save.
634
+ metadata: Additional metadata to save with the checkpoint.
635
+ new_versions: New channel versions as of this write.
636
+
637
+ Returns:
638
+ RunnableConfig: Updated configuration after storing the checkpoint.
639
+ """
640
+ return asyncio.run_coroutine_threadsafe(
641
+ self.aput(config, checkpoint, metadata, new_versions), self.loop
642
+ ).result()
643
+
644
+ def put_writes(
645
+ self,
646
+ config: RunnableConfig,
647
+ writes: Sequence[tuple[str, Any]],
648
+ task_id: str,
649
+ task_path: str = "",
650
+ ) -> None:
651
+ """Store intermediate writes linked to a checkpoint.
652
+
653
+ This method saves intermediate writes associated with a checkpoint to the database.
654
+
655
+ Args:
656
+ config: Configuration of the related checkpoint.
657
+ writes: List of writes to store, each as (channel, value) pair.
658
+ task_id: Identifier for the task creating the writes.
659
+ task_path: Path of the task creating the writes.
660
+ """
661
+ return asyncio.run_coroutine_threadsafe(
662
+ self.aput_writes(config, writes, task_id, task_path), self.loop
663
+ ).result()
664
+
665
+ def delete_thread(self, thread_id: str) -> None:
666
+ """Delete all checkpoints and writes associated with a thread ID.
667
+
668
+ Args:
669
+ thread_id: The thread ID to delete.
670
+
671
+ Returns:
672
+ None
673
+ """
674
+ try:
675
+ # check if we are in the main thread, only bg threads can block
676
+ # we don't check in other methods to avoid the overhead
677
+ if asyncio.get_running_loop() is self.loop:
678
+ raise asyncio.InvalidStateError(
679
+ "Synchronous calls to AsyncPostgresSaver are only allowed from a "
680
+ "different thread. From the main thread, use the async interface. "
681
+ "For example, use `await checkpointer.aget_tuple(...)` or `await "
682
+ "graph.ainvoke(...)`."
683
+ )
684
+ except RuntimeError:
685
+ pass
686
+ return asyncio.run_coroutine_threadsafe(
687
+ self.adelete_thread(thread_id), self.loop
688
+ ).result()
689
+
690
+
691
+ __all__ = ["AsyncPostgresSaver", "AsyncShallowPostgresSaver", "Conn"]