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,1460 @@
1
+ from __future__ import annotations
2
+
3
+ import binascii
4
+ import itertools
5
+ import sys
6
+ import threading
7
+ from collections import defaultdict, deque
8
+ from collections.abc import Callable, Iterable, Mapping, Sequence
9
+ from copy import copy
10
+ from functools import partial
11
+ from hashlib import sha1
12
+ from typing import (
13
+ Any,
14
+ Literal,
15
+ NamedTuple,
16
+ Protocol,
17
+ cast,
18
+ overload,
19
+ )
20
+
21
+ from coco_core.callbacks import Callbacks
22
+ from coco_core.callbacks.manager import AsyncParentRunManager, ParentRunManager
23
+ from coco_core.runnables.config import RunnableConfig
24
+ from coco_graph.checkpoint.base import (
25
+ BaseCheckpointSaver,
26
+ ChannelVersions,
27
+ Checkpoint,
28
+ PendingWrite,
29
+ V,
30
+ )
31
+ from coco_graph.store.base import BaseStore
32
+ from xxhash import xxh3_128_hexdigest
33
+
34
+ from coco_graph._internal._config import merge_configs, patch_config
35
+ from coco_graph._internal._constants import (
36
+ CACHE_NS_WRITES,
37
+ CONF,
38
+ CONFIG_KEY_CHECKPOINT_ID,
39
+ CONFIG_KEY_CHECKPOINT_MAP,
40
+ CONFIG_KEY_CHECKPOINT_NS,
41
+ CONFIG_KEY_CHECKPOINTER,
42
+ CONFIG_KEY_NODE_ERROR,
43
+ CONFIG_KEY_READ,
44
+ CONFIG_KEY_RESUME_MAP,
45
+ CONFIG_KEY_RUNTIME,
46
+ CONFIG_KEY_SCRATCHPAD,
47
+ CONFIG_KEY_SEND,
48
+ CONFIG_KEY_TASK_ID,
49
+ CONFIG_KEY_THREAD_ID,
50
+ ERROR,
51
+ ERROR_SOURCE_NODE,
52
+ INTERRUPT,
53
+ NO_WRITES,
54
+ NS_END,
55
+ NS_SEP,
56
+ NULL_TASK_ID,
57
+ PREVIOUS,
58
+ PULL,
59
+ PUSH,
60
+ RESERVED,
61
+ RESUME,
62
+ RETURN,
63
+ TASKS,
64
+ )
65
+ from coco_graph._internal._scratchpad import PregelScratchpad
66
+ from coco_graph._internal._typing import EMPTY_SEQ, MISSING
67
+ from coco_graph.channels.base import BaseChannel
68
+ from coco_graph.channels.topic import Topic
69
+ from coco_graph.channels.untracked_value import UntrackedValue
70
+ from coco_graph.constants import TAG_HIDDEN
71
+ from coco_graph.errors import NodeError
72
+ from coco_graph.managed.base import ManagedValueMapping
73
+ from coco_graph.pregel._call import get_runnable_for_task, identifier
74
+ from coco_graph.pregel._io import read_channels
75
+ from coco_graph.pregel._log import logger
76
+ from coco_graph.pregel._read import INPUT_CACHE_KEY_TYPE, PregelNode
77
+ from coco_graph.runtime import DEFAULT_RUNTIME, ExecutionInfo, Runtime
78
+ from coco_graph.types import (
79
+ All,
80
+ CacheKey,
81
+ CachePolicy,
82
+ PregelExecutableTask,
83
+ PregelTask,
84
+ RetryPolicy,
85
+ Send,
86
+ TimeoutPolicy,
87
+ )
88
+
89
+ GetNextVersion = Callable[[V | None, None], V]
90
+ SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
91
+
92
+
93
+ class WritesProtocol(Protocol):
94
+ """Protocol for objects containing writes to be applied to checkpoint.
95
+ Implemented by PregelTaskWrites and PregelExecutableTask."""
96
+
97
+ @property
98
+ def path(self) -> tuple[str | int | tuple, ...]: ...
99
+
100
+ @property
101
+ def name(self) -> str: ...
102
+
103
+ @property
104
+ def writes(self) -> Sequence[tuple[str, Any]]: ...
105
+
106
+ @property
107
+ def triggers(self) -> Sequence[str]: ...
108
+
109
+
110
+ class PregelTaskWrites(NamedTuple):
111
+ """Simplest implementation of WritesProtocol, for usage with writes that
112
+ don't originate from a runnable task, eg. graph input, update_state, etc."""
113
+
114
+ path: tuple[str | int | tuple, ...]
115
+ name: str
116
+ writes: Sequence[tuple[str, Any]]
117
+ triggers: Sequence[str]
118
+
119
+
120
+ class Call:
121
+ __slots__ = (
122
+ "func",
123
+ "input",
124
+ "retry_policy",
125
+ "cache_policy",
126
+ "callbacks",
127
+ "timeout",
128
+ )
129
+
130
+ func: Callable
131
+ input: tuple[tuple[Any, ...], dict[str, Any]]
132
+ retry_policy: Sequence[RetryPolicy] | None
133
+ cache_policy: CachePolicy | None
134
+ callbacks: Callbacks
135
+ timeout: TimeoutPolicy | None
136
+
137
+ def __init__(
138
+ self,
139
+ func: Callable,
140
+ input: tuple[tuple[Any, ...], dict[str, Any]],
141
+ *,
142
+ retry_policy: Sequence[RetryPolicy] | None,
143
+ cache_policy: CachePolicy | None,
144
+ callbacks: Callbacks,
145
+ timeout: TimeoutPolicy | None = None,
146
+ ) -> None:
147
+ self.func = func
148
+ self.input = input
149
+ self.retry_policy = retry_policy
150
+ self.cache_policy = cache_policy
151
+ self.callbacks = callbacks
152
+ self.timeout = timeout
153
+
154
+
155
+ def should_interrupt(
156
+ checkpoint: Checkpoint,
157
+ interrupt_nodes: All | Sequence[str],
158
+ tasks: Iterable[PregelExecutableTask],
159
+ ) -> list[PregelExecutableTask]:
160
+ """Check if the graph should be interrupted based on current state."""
161
+ version_type = type(next(iter(checkpoint["channel_versions"].values()), None))
162
+ null_version = version_type() # type: ignore[misc]
163
+ seen = checkpoint["versions_seen"].get(INTERRUPT, {})
164
+ # interrupt if any channel has been updated since last interrupt
165
+ any_updates_since_prev_interrupt = any(
166
+ version > seen.get(chan, null_version) # type: ignore[operator]
167
+ for chan, version in checkpoint["channel_versions"].items()
168
+ )
169
+ # and any triggered node is in interrupt_nodes list
170
+ return (
171
+ [
172
+ task
173
+ for task in tasks
174
+ if (
175
+ (
176
+ not task.config
177
+ or TAG_HIDDEN not in task.config.get("tags", EMPTY_SEQ)
178
+ )
179
+ if interrupt_nodes == "*"
180
+ else task.name in interrupt_nodes
181
+ )
182
+ ]
183
+ if any_updates_since_prev_interrupt
184
+ else []
185
+ )
186
+
187
+
188
+ def local_read(
189
+ scratchpad: PregelScratchpad,
190
+ channels: Mapping[str, BaseChannel],
191
+ managed: ManagedValueMapping,
192
+ task: WritesProtocol,
193
+ select: list[str] | str,
194
+ fresh: bool = False,
195
+ ) -> dict[str, Any] | Any:
196
+ """Function injected under CONFIG_KEY_READ in task config, to read current state.
197
+ Used by conditional edges to read a copy of the state with reflecting the writes
198
+ from that node only."""
199
+ updated: dict[str, list[Any]] = defaultdict(list)
200
+ if isinstance(select, str):
201
+ managed_keys = []
202
+ for c, v in task.writes:
203
+ if c == select:
204
+ updated[c].append(v)
205
+ else:
206
+ managed_keys = [k for k in select if k in managed]
207
+ select = [k for k in select if k not in managed]
208
+ for c, v in task.writes:
209
+ if c in select:
210
+ updated[c].append(v)
211
+ if fresh:
212
+ # apply writes
213
+ local_channels: dict[str, BaseChannel] = {}
214
+ for k in channels:
215
+ cc = channels[k].copy()
216
+ cc.update(updated[k])
217
+ local_channels[k] = cc
218
+ # read fresh values
219
+ values = read_channels(local_channels, select)
220
+ else:
221
+ values = read_channels(channels, select)
222
+ if managed_keys:
223
+ values.update({k: managed[k].get(scratchpad) for k in managed_keys})
224
+ return values
225
+
226
+
227
+ def increment(current: int | None, channel: None) -> int:
228
+ """Default channel versioning function, increments the current int version."""
229
+ return current + 1 if current is not None else 1
230
+
231
+
232
+ def apply_writes(
233
+ checkpoint: Checkpoint,
234
+ channels: Mapping[str, BaseChannel],
235
+ tasks: Iterable[WritesProtocol],
236
+ get_next_version: GetNextVersion | None,
237
+ trigger_to_nodes: Mapping[str, Sequence[str]],
238
+ ) -> set[str]:
239
+ """Apply writes from a set of tasks (usually the tasks from a Pregel step)
240
+ to the checkpoint and channels, and return managed values writes to be applied
241
+ externally.
242
+
243
+ Args:
244
+ checkpoint: The checkpoint to update.
245
+ channels: The channels to update.
246
+ tasks: The tasks to apply writes from.
247
+ get_next_version: Optional function to determine the next version of a channel.
248
+ trigger_to_nodes: Mapping of channel names to the set of nodes that can be triggered by updates to that channel.
249
+
250
+ Returns:
251
+ Set of channels that were updated in this step.
252
+ """
253
+ # sort tasks on path, to ensure deterministic order for update application
254
+ # any path parts after the 3rd are ignored for sorting
255
+ # (we use them for eg. task ids which aren't good for sorting)
256
+ tasks = sorted(tasks, key=lambda t: task_path_str(t.path[:3]))
257
+ # if no task has triggers this is applying writes from the null task only
258
+ # so we don't do anything other than update the channels written to
259
+ bump_step = any(t.triggers for t in tasks)
260
+
261
+ # update seen versions
262
+ for task in tasks:
263
+ checkpoint["versions_seen"].setdefault(task.name, {}).update(
264
+ {
265
+ chan: checkpoint["channel_versions"][chan]
266
+ for chan in task.triggers
267
+ if chan in checkpoint["channel_versions"]
268
+ }
269
+ )
270
+
271
+ # Find the highest version of all channels
272
+ if get_next_version is None:
273
+ next_version = None
274
+ else:
275
+ next_version = get_next_version(
276
+ (
277
+ max(checkpoint["channel_versions"].values())
278
+ if checkpoint["channel_versions"]
279
+ else None
280
+ ),
281
+ None,
282
+ )
283
+
284
+ # Consume all channels that were read
285
+ for chan in {
286
+ chan
287
+ for task in tasks
288
+ for chan in task.triggers
289
+ if chan not in RESERVED and chan in channels
290
+ }:
291
+ if channels[chan].consume() and next_version is not None:
292
+ checkpoint["channel_versions"][chan] = next_version
293
+
294
+ # Group writes by channel
295
+ pending_writes_by_channel: dict[str, list[Any]] = defaultdict(list)
296
+ for task in tasks:
297
+ for chan, val in task.writes:
298
+ if chan in (
299
+ NO_WRITES,
300
+ PUSH,
301
+ RESUME,
302
+ INTERRUPT,
303
+ RETURN,
304
+ ERROR,
305
+ ERROR_SOURCE_NODE,
306
+ ):
307
+ pass
308
+ elif chan in channels:
309
+ pending_writes_by_channel[chan].append(val)
310
+ else:
311
+ logger.warning(
312
+ f"Task {task.name} with path {task.path} wrote to unknown channel {chan}, ignoring it."
313
+ )
314
+
315
+ # Apply writes to channels
316
+ updated_channels: set[str] = set()
317
+ for chan, vals in pending_writes_by_channel.items():
318
+ if chan in channels:
319
+ if channels[chan].update(vals) and next_version is not None:
320
+ checkpoint["channel_versions"][chan] = next_version
321
+ # unavailable channels can't trigger tasks, so don't add them
322
+ if channels[chan].is_available():
323
+ updated_channels.add(chan)
324
+
325
+ # Channels that weren't updated in this step are notified of a new step
326
+ if bump_step:
327
+ for chan in channels:
328
+ if channels[chan].is_available() and chan not in updated_channels:
329
+ if channels[chan].update(EMPTY_SEQ) and next_version is not None:
330
+ checkpoint["channel_versions"][chan] = next_version
331
+ # unavailable channels can't trigger tasks, so don't add them
332
+ if channels[chan].is_available():
333
+ updated_channels.add(chan)
334
+
335
+ # If this is (tentatively) the last superstep, notify all channels of finish
336
+ if bump_step and updated_channels.isdisjoint(trigger_to_nodes):
337
+ for chan in channels:
338
+ if channels[chan].finish() and next_version is not None:
339
+ checkpoint["channel_versions"][chan] = next_version
340
+ # unavailable channels can't trigger tasks, so don't add them
341
+ if channels[chan].is_available():
342
+ updated_channels.add(chan)
343
+
344
+ # Return managed values writes to be applied externally
345
+ return updated_channels
346
+
347
+
348
+ @overload
349
+ def prepare_next_tasks(
350
+ checkpoint: Checkpoint,
351
+ pending_writes: list[PendingWrite],
352
+ processes: Mapping[str, PregelNode],
353
+ channels: Mapping[str, BaseChannel],
354
+ managed: ManagedValueMapping,
355
+ config: RunnableConfig,
356
+ step: int,
357
+ stop: int,
358
+ *,
359
+ for_execution: Literal[False],
360
+ store: Literal[None] = None,
361
+ checkpointer: Literal[None] = None,
362
+ manager: Literal[None] = None,
363
+ trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
364
+ updated_channels: set[str] | None = None,
365
+ retry_policy: Sequence[RetryPolicy] = (),
366
+ cache_policy: Literal[None] = None,
367
+ ) -> dict[str, PregelTask]: ...
368
+
369
+
370
+ @overload
371
+ def prepare_next_tasks(
372
+ checkpoint: Checkpoint,
373
+ pending_writes: list[PendingWrite],
374
+ processes: Mapping[str, PregelNode],
375
+ channels: Mapping[str, BaseChannel],
376
+ managed: ManagedValueMapping,
377
+ config: RunnableConfig,
378
+ step: int,
379
+ stop: int,
380
+ *,
381
+ for_execution: Literal[True],
382
+ store: BaseStore | None,
383
+ checkpointer: BaseCheckpointSaver | None,
384
+ manager: None | ParentRunManager | AsyncParentRunManager,
385
+ trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
386
+ updated_channels: set[str] | None = None,
387
+ retry_policy: Sequence[RetryPolicy] = (),
388
+ cache_policy: CachePolicy | None = None,
389
+ ) -> dict[str, PregelExecutableTask]: ...
390
+
391
+
392
+ def prepare_next_tasks(
393
+ checkpoint: Checkpoint,
394
+ pending_writes: list[PendingWrite],
395
+ processes: Mapping[str, PregelNode],
396
+ channels: Mapping[str, BaseChannel],
397
+ managed: ManagedValueMapping,
398
+ config: RunnableConfig,
399
+ step: int,
400
+ stop: int,
401
+ *,
402
+ for_execution: bool,
403
+ store: BaseStore | None = None,
404
+ checkpointer: BaseCheckpointSaver | None = None,
405
+ manager: None | ParentRunManager | AsyncParentRunManager = None,
406
+ trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
407
+ updated_channels: set[str] | None = None,
408
+ retry_policy: Sequence[RetryPolicy] = (),
409
+ cache_policy: CachePolicy | None = None,
410
+ ) -> dict[str, PregelTask] | dict[str, PregelExecutableTask]:
411
+ """Prepare the set of tasks that will make up the next Pregel step.
412
+
413
+ Args:
414
+ checkpoint: The current checkpoint.
415
+ pending_writes: The list of pending writes.
416
+ processes: The mapping of process names to PregelNode instances.
417
+ channels: The mapping of channel names to BaseChannel instances.
418
+ managed: The mapping of managed value names to functions.
419
+ config: The `Runnable` configuration.
420
+ step: The current step.
421
+ for_execution: Whether the tasks are being prepared for execution.
422
+ store: An instance of BaseStore to make it available for usage within tasks.
423
+ checkpointer: `Checkpointer` instance used for saving checkpoints.
424
+ manager: The parent run manager to use for the tasks.
425
+ trigger_to_nodes: Optional: Mapping of channel names to the set of nodes
426
+ that are can be triggered by that channel.
427
+ updated_channels: Optional. Set of channel names that have been updated during
428
+ the previous step. Using in conjunction with trigger_to_nodes to speed
429
+ up the process of determining which nodes should be triggered in the next
430
+ step.
431
+
432
+ Returns:
433
+ A dictionary of tasks to be executed. The keys are the task ids and the values
434
+ are the tasks themselves. This is the union of all PUSH tasks (Sends)
435
+ and PULL tasks (nodes triggered by edges).
436
+ """
437
+ input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] = {}
438
+ checkpoint_id_bytes = binascii.unhexlify(checkpoint["id"].replace("-", ""))
439
+ null_version = checkpoint_null_version(checkpoint)
440
+ tasks: list[PregelTask | PregelExecutableTask] = []
441
+ # Consume pending tasks
442
+ tasks_channel = cast(Topic[Send] | None, channels.get(TASKS))
443
+ if tasks_channel and tasks_channel.is_available():
444
+ for idx, _ in enumerate(tasks_channel.get()):
445
+ if task := prepare_single_task(
446
+ (PUSH, idx),
447
+ None,
448
+ checkpoint=checkpoint,
449
+ checkpoint_id_bytes=checkpoint_id_bytes,
450
+ checkpoint_null_version=null_version,
451
+ pending_writes=pending_writes,
452
+ processes=processes,
453
+ channels=channels,
454
+ managed=managed,
455
+ config=config,
456
+ step=step,
457
+ stop=stop,
458
+ for_execution=for_execution,
459
+ store=store,
460
+ checkpointer=checkpointer,
461
+ manager=manager,
462
+ input_cache=input_cache,
463
+ cache_policy=cache_policy,
464
+ retry_policy=retry_policy,
465
+ ):
466
+ tasks.append(task)
467
+
468
+ # This section is an optimization that allows which nodes will be active
469
+ # during the next step.
470
+ # When there's information about:
471
+ # 1. Which channels were updated in the previous step
472
+ # 2. Which nodes are triggered by which channels
473
+ # Then we can determine which nodes should be triggered in the next step
474
+ # without having to cycle through all nodes.
475
+ if updated_channels and trigger_to_nodes:
476
+ triggered_nodes: set[str] = set()
477
+ # Get all nodes that have triggers associated with an updated channel
478
+ for channel in updated_channels:
479
+ if node_ids := trigger_to_nodes.get(channel):
480
+ triggered_nodes.update(node_ids)
481
+ # Sort the nodes to ensure deterministic order
482
+ candidate_nodes: Iterable[str] = sorted(triggered_nodes)
483
+ elif not checkpoint["channel_versions"]:
484
+ candidate_nodes = ()
485
+ else:
486
+ candidate_nodes = processes.keys()
487
+
488
+ # Check if any processes should be run in next step
489
+ # If so, prepare the values to be passed to them
490
+ for name in candidate_nodes:
491
+ if task := prepare_single_task(
492
+ (PULL, name),
493
+ None,
494
+ checkpoint=checkpoint,
495
+ checkpoint_id_bytes=checkpoint_id_bytes,
496
+ checkpoint_null_version=null_version,
497
+ pending_writes=pending_writes,
498
+ processes=processes,
499
+ channels=channels,
500
+ managed=managed,
501
+ config=config,
502
+ step=step,
503
+ stop=stop,
504
+ for_execution=for_execution,
505
+ store=store,
506
+ checkpointer=checkpointer,
507
+ manager=manager,
508
+ input_cache=input_cache,
509
+ cache_policy=cache_policy,
510
+ retry_policy=retry_policy,
511
+ ):
512
+ tasks.append(task)
513
+ return {t.id: t for t in tasks}
514
+
515
+
516
+ PUSH_TRIGGER = (PUSH,)
517
+
518
+
519
+ class _TaskIDFn(Protocol):
520
+ def __call__(self, namespace: bytes, *parts: str | bytes) -> str:
521
+ pass
522
+
523
+
524
+ def prepare_single_task(
525
+ task_path: tuple[Any, ...],
526
+ task_id_checksum: str | None,
527
+ *,
528
+ checkpoint: Checkpoint,
529
+ checkpoint_id_bytes: bytes,
530
+ checkpoint_null_version: V | None,
531
+ pending_writes: list[PendingWrite],
532
+ processes: Mapping[str, PregelNode],
533
+ channels: Mapping[str, BaseChannel],
534
+ managed: ManagedValueMapping,
535
+ config: RunnableConfig,
536
+ step: int,
537
+ stop: int,
538
+ for_execution: bool,
539
+ store: BaseStore | None = None,
540
+ checkpointer: BaseCheckpointSaver | None = None,
541
+ manager: None | ParentRunManager | AsyncParentRunManager = None,
542
+ input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] | None = None,
543
+ cache_policy: CachePolicy | None = None,
544
+ retry_policy: Sequence[RetryPolicy] = (),
545
+ ) -> None | PregelTask | PregelExecutableTask:
546
+ """Prepares a single task for the next Pregel step, given a task path, which
547
+ uniquely identifies a PUSH or PULL task within the graph."""
548
+ configurable = config.get(CONF, {})
549
+ parent_ns = configurable.get(CONFIG_KEY_CHECKPOINT_NS, "")
550
+ task_id_func = _xxhash_str if checkpoint["v"] > 1 else _uuid5_str
551
+
552
+ if task_path[0] == PUSH and isinstance(task_path[-1], Call):
553
+ return prepare_push_task_functional(
554
+ cast(tuple[str, tuple, int, str, Call], task_path),
555
+ task_id_checksum,
556
+ checkpoint=checkpoint,
557
+ checkpoint_id_bytes=checkpoint_id_bytes,
558
+ pending_writes=pending_writes,
559
+ channels=channels,
560
+ managed=managed,
561
+ config=config,
562
+ step=step,
563
+ stop=stop,
564
+ for_execution=for_execution,
565
+ store=store,
566
+ checkpointer=checkpointer,
567
+ manager=manager,
568
+ cache_policy=cache_policy,
569
+ retry_policy=retry_policy,
570
+ parent_ns=parent_ns,
571
+ task_id_func=task_id_func,
572
+ )
573
+
574
+ elif task_path[0] == PUSH:
575
+ return prepare_push_task_send(
576
+ cast(tuple[str, tuple], task_path),
577
+ task_id_checksum,
578
+ checkpoint=checkpoint,
579
+ checkpoint_id_bytes=checkpoint_id_bytes,
580
+ pending_writes=pending_writes,
581
+ channels=channels,
582
+ managed=managed,
583
+ config=config,
584
+ step=step,
585
+ processes=processes,
586
+ stop=stop,
587
+ for_execution=for_execution,
588
+ store=store,
589
+ checkpointer=checkpointer,
590
+ manager=manager,
591
+ cache_policy=cache_policy,
592
+ retry_policy=retry_policy,
593
+ parent_ns=parent_ns,
594
+ task_id_func=task_id_func,
595
+ )
596
+
597
+ elif task_path[0] == PULL:
598
+ # (PULL, node name)
599
+ name = cast(str, task_path[1])
600
+ if name not in processes:
601
+ return
602
+ proc = processes[name]
603
+ if checkpoint_null_version is None:
604
+ return
605
+ # If any of the channels read by this process were updated
606
+ if _triggers(
607
+ channels,
608
+ checkpoint["channel_versions"],
609
+ checkpoint["versions_seen"].get(name),
610
+ checkpoint_null_version,
611
+ proc,
612
+ ):
613
+ triggers = tuple(sorted(proc.triggers))
614
+ # create task id
615
+ checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
616
+ task_id = task_id_func(
617
+ checkpoint_id_bytes,
618
+ checkpoint_ns,
619
+ str(step),
620
+ name,
621
+ PULL,
622
+ *triggers,
623
+ )
624
+ task_checkpoint_ns = f"{checkpoint_ns}{NS_END}{task_id}"
625
+ # create scratchpad
626
+ scratchpad = _scratchpad(
627
+ config[CONF].get(CONFIG_KEY_SCRATCHPAD),
628
+ pending_writes,
629
+ task_id,
630
+ xxh3_128_hexdigest(task_checkpoint_ns.encode()),
631
+ config[CONF].get(CONFIG_KEY_RESUME_MAP),
632
+ step,
633
+ stop,
634
+ )
635
+ # create task input
636
+ try:
637
+ val = _proc_input(
638
+ proc,
639
+ managed,
640
+ channels,
641
+ for_execution=for_execution,
642
+ input_cache=input_cache,
643
+ scratchpad=scratchpad,
644
+ )
645
+ if val is MISSING:
646
+ return
647
+ except Exception as exc:
648
+ if SUPPORTS_EXC_NOTES:
649
+ exc.add_note(
650
+ f"Before task with name '{name}' and path '{task_path[:3]}'"
651
+ )
652
+ raise
653
+
654
+ metadata = {
655
+ "coco_graph_step": step,
656
+ "coco_graph_node": name,
657
+ "coco_graph_triggers": triggers,
658
+ "coco_graph_path": task_path[:3],
659
+ "coco_graph_checkpoint_ns": task_checkpoint_ns,
660
+ }
661
+ if task_id_checksum is not None:
662
+ assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
663
+ if for_execution:
664
+ if node := proc.node:
665
+ if proc.metadata:
666
+ metadata.update(proc.metadata)
667
+ writes: deque[tuple[str, Any]] = deque()
668
+ cache_policy = proc.cache_policy or cache_policy
669
+ if cache_policy:
670
+ args_key = cache_policy.key_func(val)
671
+ cache_key = CacheKey(
672
+ (
673
+ CACHE_NS_WRITES,
674
+ (identifier(proc) or "__dynamic__"),
675
+ name,
676
+ ),
677
+ xxh3_128_hexdigest(
678
+ (
679
+ args_key.encode()
680
+ if isinstance(args_key, str)
681
+ else args_key
682
+ ),
683
+ ),
684
+ cache_policy.ttl,
685
+ )
686
+ else:
687
+ cache_key = None
688
+ runtime = cast(
689
+ Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
690
+ )
691
+ runtime = runtime.override(
692
+ previous=checkpoint["channel_values"].get(PREVIOUS, None),
693
+ store=store,
694
+ execution_info=ExecutionInfo(
695
+ checkpoint_id=checkpoint["id"],
696
+ checkpoint_ns=task_checkpoint_ns,
697
+ task_id=task_id,
698
+ thread_id=configurable.get(CONFIG_KEY_THREAD_ID),
699
+ run_id=str(rid) if (rid := config.get("run_id")) else None,
700
+ ),
701
+ )
702
+ additional_config = {
703
+ "metadata": metadata,
704
+ "tags": proc.tags,
705
+ }
706
+ return PregelExecutableTask(
707
+ name,
708
+ val,
709
+ node,
710
+ writes,
711
+ patch_config(
712
+ merge_configs(
713
+ config, cast(RunnableConfig, additional_config)
714
+ ),
715
+ run_name=name,
716
+ callbacks=(
717
+ manager.get_child(f"graph:step:{step}")
718
+ if manager
719
+ else None
720
+ ),
721
+ configurable={
722
+ CONFIG_KEY_TASK_ID: task_id,
723
+ # deque.extend is thread-safe
724
+ CONFIG_KEY_SEND: writes.extend,
725
+ CONFIG_KEY_READ: partial(
726
+ local_read,
727
+ scratchpad,
728
+ channels,
729
+ managed,
730
+ PregelTaskWrites(
731
+ task_path[:3],
732
+ name,
733
+ writes,
734
+ triggers,
735
+ ),
736
+ ),
737
+ CONFIG_KEY_CHECKPOINTER: (
738
+ checkpointer
739
+ or configurable.get(CONFIG_KEY_CHECKPOINTER)
740
+ ),
741
+ CONFIG_KEY_CHECKPOINT_MAP: {
742
+ **configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
743
+ parent_ns: checkpoint["id"],
744
+ },
745
+ CONFIG_KEY_CHECKPOINT_ID: None,
746
+ CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
747
+ CONFIG_KEY_SCRATCHPAD: scratchpad,
748
+ CONFIG_KEY_RUNTIME: runtime,
749
+ },
750
+ ),
751
+ triggers,
752
+ proc.retry_policy or retry_policy,
753
+ cache_key,
754
+ task_id,
755
+ task_path[:3],
756
+ writers=proc.flat_writers,
757
+ subgraphs=proc.subgraphs,
758
+ timeout=proc.timeout,
759
+ )
760
+ else:
761
+ return PregelTask(task_id, name, task_path[:3])
762
+
763
+
764
+ def _coerce_pending_error(value: Any) -> BaseException:
765
+ if isinstance(value, BaseException):
766
+ return value
767
+ return Exception(str(value))
768
+
769
+
770
+ def _read_errors_from_pending_writes(
771
+ pending_writes: list[PendingWrite],
772
+ ) -> list[BaseException]:
773
+ errors: list[BaseException] = []
774
+ for _, channel, value in pending_writes:
775
+ if channel == ERROR:
776
+ errors.append(_coerce_pending_error(value))
777
+ return errors
778
+
779
+
780
+ def _read_error_for_task_id_from_pending_writes(
781
+ pending_writes: list[PendingWrite], task_id: str
782
+ ) -> BaseException | None:
783
+ for pending_task_id, channel, value in reversed(pending_writes):
784
+ if pending_task_id == task_id and channel == ERROR:
785
+ return _coerce_pending_error(value)
786
+ return None
787
+
788
+
789
+ def _read_error_source_node_from_pending_writes(
790
+ pending_writes: list[PendingWrite], task_id: str
791
+ ) -> str | None:
792
+ for pending_task_id, channel, value in reversed(pending_writes):
793
+ if pending_task_id == task_id and channel == ERROR_SOURCE_NODE:
794
+ if isinstance(value, str):
795
+ return value
796
+ return str(value)
797
+ return None
798
+
799
+
800
+ def prepare_push_task_functional(
801
+ task_path: tuple[str, tuple, int, str, Call],
802
+ # (PUSH, parent task path, idx of PUSH write, id of parent task, Call)
803
+ task_id_checksum: str | None,
804
+ *,
805
+ checkpoint: Checkpoint,
806
+ checkpoint_id_bytes: bytes,
807
+ pending_writes: list[PendingWrite],
808
+ channels: Mapping[str, BaseChannel],
809
+ managed: ManagedValueMapping,
810
+ config: RunnableConfig,
811
+ step: int,
812
+ stop: int,
813
+ for_execution: bool,
814
+ store: BaseStore | None = None,
815
+ checkpointer: BaseCheckpointSaver | None = None,
816
+ manager: None | ParentRunManager | AsyncParentRunManager = None,
817
+ cache_policy: CachePolicy | None = None,
818
+ retry_policy: Sequence[RetryPolicy] = (),
819
+ parent_ns: str,
820
+ # namespace: bytes, *parts: str | bytes
821
+ task_id_func: _TaskIDFn,
822
+ ) -> PregelTask | PregelExecutableTask:
823
+ """Prepare a push task with an attached caller. Used for the functional API."""
824
+ configurable = config.get(CONF, {})
825
+
826
+ call = task_path[-1]
827
+ proc_ = get_runnable_for_task(call.func)
828
+ name = proc_.name
829
+ if name is None:
830
+ raise ValueError("`call` functions must have a `__name__` attribute")
831
+ # create task id
832
+ triggers: Sequence[str] = PUSH_TRIGGER
833
+ checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
834
+ task_id = task_id_func(
835
+ checkpoint_id_bytes,
836
+ checkpoint_ns,
837
+ str(step),
838
+ name,
839
+ PUSH,
840
+ task_path_str(task_path[1]),
841
+ str(task_path[2]),
842
+ )
843
+ task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
844
+ # we append True to the task path to indicate that a call is being
845
+ # made, so we should not return interrupts from this task (responsibility lies with the parent)
846
+ in_progress_task_path = (*task_path[:3], True)
847
+ metadata = {
848
+ "coco_graph_step": step,
849
+ "coco_graph_node": name,
850
+ "coco_graph_triggers": triggers,
851
+ "coco_graph_path": in_progress_task_path,
852
+ "coco_graph_checkpoint_ns": task_checkpoint_ns,
853
+ }
854
+ if task_id_checksum is not None:
855
+ assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
856
+ if for_execution:
857
+ writes: deque[tuple[str, Any]] = deque()
858
+ cache_policy = call.cache_policy or cache_policy
859
+ if cache_policy:
860
+ args_key = cache_policy.key_func(*call.input[0], **call.input[1])
861
+ cache_key: CacheKey | None = CacheKey(
862
+ (
863
+ CACHE_NS_WRITES,
864
+ (identifier(call.func) or "__dynamic__"),
865
+ ),
866
+ xxh3_128_hexdigest(
867
+ args_key.encode() if isinstance(args_key, str) else args_key,
868
+ ),
869
+ cache_policy.ttl,
870
+ )
871
+ else:
872
+ cache_key = None
873
+ scratchpad = _scratchpad(
874
+ configurable.get(CONFIG_KEY_SCRATCHPAD),
875
+ pending_writes,
876
+ task_id,
877
+ xxh3_128_hexdigest(task_checkpoint_ns.encode()),
878
+ configurable.get(CONFIG_KEY_RESUME_MAP),
879
+ step,
880
+ stop,
881
+ )
882
+ runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
883
+ runtime = runtime.override(
884
+ store=store,
885
+ execution_info=ExecutionInfo(
886
+ checkpoint_id=checkpoint["id"],
887
+ checkpoint_ns=task_checkpoint_ns,
888
+ task_id=task_id,
889
+ thread_id=configurable.get(CONFIG_KEY_THREAD_ID),
890
+ run_id=str(rid) if (rid := config.get("run_id")) else None,
891
+ ),
892
+ )
893
+ return PregelExecutableTask(
894
+ name,
895
+ call.input,
896
+ proc_,
897
+ writes,
898
+ patch_config(
899
+ merge_configs(config, {"metadata": metadata}),
900
+ run_name=name,
901
+ callbacks=call.callbacks
902
+ or (manager.get_child(f"graph:step:{step}") if manager else None),
903
+ configurable={
904
+ CONFIG_KEY_TASK_ID: task_id,
905
+ # deque.extend is thread-safe
906
+ CONFIG_KEY_SEND: writes.extend,
907
+ CONFIG_KEY_READ: partial(
908
+ local_read,
909
+ scratchpad,
910
+ channels,
911
+ managed,
912
+ PregelTaskWrites(in_progress_task_path, name, writes, triggers),
913
+ ),
914
+ CONFIG_KEY_CHECKPOINTER: (
915
+ checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
916
+ ),
917
+ CONFIG_KEY_CHECKPOINT_MAP: {
918
+ **configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
919
+ parent_ns: checkpoint["id"],
920
+ },
921
+ CONFIG_KEY_CHECKPOINT_ID: None,
922
+ CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
923
+ CONFIG_KEY_SCRATCHPAD: scratchpad,
924
+ CONFIG_KEY_RUNTIME: runtime,
925
+ },
926
+ ),
927
+ triggers,
928
+ call.retry_policy or retry_policy,
929
+ cache_key,
930
+ task_id,
931
+ in_progress_task_path,
932
+ timeout=call.timeout,
933
+ )
934
+ else:
935
+ return PregelTask(task_id, name, in_progress_task_path)
936
+
937
+
938
+ def prepare_push_task_send(
939
+ task_path: tuple[str, tuple],
940
+ # (PUSH, parent task path)
941
+ task_id_checksum: str | None,
942
+ *,
943
+ checkpoint: Checkpoint,
944
+ checkpoint_id_bytes: bytes,
945
+ pending_writes: list[PendingWrite],
946
+ channels: Mapping[str, BaseChannel],
947
+ managed: ManagedValueMapping,
948
+ config: RunnableConfig,
949
+ step: int,
950
+ stop: int,
951
+ for_execution: bool,
952
+ store: BaseStore | None = None,
953
+ checkpointer: BaseCheckpointSaver | None = None,
954
+ manager: None | ParentRunManager | AsyncParentRunManager = None,
955
+ cache_policy: CachePolicy | None = None,
956
+ retry_policy: Sequence[RetryPolicy] = (),
957
+ parent_ns: str,
958
+ task_id_func: _TaskIDFn,
959
+ processes: Mapping[str, PregelNode],
960
+ ) -> PregelTask | PregelExecutableTask | None:
961
+ if len(task_path) == 2:
962
+ # SEND tasks, executed in superstep n+1
963
+ # (PUSH, idx of pending send)
964
+ idx = cast(int, task_path[1])
965
+ if not channels[TASKS].is_available():
966
+ return
967
+ sends: Sequence[Send] = channels[TASKS].get()
968
+ if idx < 0 or idx >= len(sends):
969
+ return
970
+ packet = sends[idx]
971
+ if not isinstance(packet, Send):
972
+ logger.warning(
973
+ f"Ignoring invalid packet type {type(packet)} in pending sends"
974
+ )
975
+ return
976
+
977
+ if packet.node not in processes:
978
+ logger.warning(f"Ignoring unknown node name {packet.node} in pending sends")
979
+ return
980
+ # find process
981
+ proc = processes[packet.node]
982
+ proc_node = proc.node
983
+ if proc_node is None:
984
+ return
985
+ # create task id
986
+ triggers = PUSH_TRIGGER
987
+ checkpoint_ns = (
988
+ f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
989
+ )
990
+ task_id = task_id_func(
991
+ checkpoint_id_bytes,
992
+ checkpoint_ns,
993
+ str(step),
994
+ packet.node,
995
+ PUSH,
996
+ str(idx),
997
+ )
998
+ else:
999
+ logger.warning(f"Ignoring invalid PUSH task path {task_path}")
1000
+ return
1001
+ configurable = config.get(CONF, {})
1002
+ task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
1003
+ # we append False to the task path to indicate that a call is not being made
1004
+ # so we should return interrupts from this task
1005
+ translated_task_path = (*task_path[:3], False)
1006
+ metadata = {
1007
+ "coco_graph_step": step,
1008
+ "coco_graph_node": packet.node,
1009
+ "coco_graph_triggers": triggers,
1010
+ "coco_graph_path": translated_task_path,
1011
+ "coco_graph_checkpoint_ns": task_checkpoint_ns,
1012
+ }
1013
+ if task_id_checksum is not None:
1014
+ assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
1015
+ if for_execution:
1016
+ if proc.metadata:
1017
+ metadata.update(proc.metadata)
1018
+ writes: deque[tuple[str, Any]] = deque()
1019
+ cache_policy = proc.cache_policy or cache_policy
1020
+ if cache_policy:
1021
+ args_key = cache_policy.key_func(packet.arg)
1022
+ cache_key = CacheKey(
1023
+ (
1024
+ CACHE_NS_WRITES,
1025
+ (identifier(proc) or "__dynamic__"),
1026
+ packet.node,
1027
+ ),
1028
+ xxh3_128_hexdigest(
1029
+ args_key.encode() if isinstance(args_key, str) else args_key,
1030
+ ),
1031
+ cache_policy.ttl,
1032
+ )
1033
+ else:
1034
+ cache_key = None
1035
+ scratchpad = _scratchpad(
1036
+ config[CONF].get(CONFIG_KEY_SCRATCHPAD),
1037
+ pending_writes,
1038
+ task_id,
1039
+ xxh3_128_hexdigest(task_checkpoint_ns.encode()),
1040
+ config[CONF].get(CONFIG_KEY_RESUME_MAP),
1041
+ step,
1042
+ stop,
1043
+ )
1044
+ runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
1045
+ runtime = runtime.override(
1046
+ store=store,
1047
+ previous=checkpoint["channel_values"].get(PREVIOUS, None),
1048
+ execution_info=ExecutionInfo(
1049
+ checkpoint_id=checkpoint["id"],
1050
+ checkpoint_ns=task_checkpoint_ns,
1051
+ task_id=task_id,
1052
+ thread_id=configurable.get(CONFIG_KEY_THREAD_ID),
1053
+ run_id=str(rid) if (rid := config.get("run_id")) else None,
1054
+ ),
1055
+ )
1056
+ additional_config: RunnableConfig = {
1057
+ "metadata": metadata,
1058
+ "tags": proc.tags,
1059
+ }
1060
+ return PregelExecutableTask(
1061
+ packet.node,
1062
+ packet.arg,
1063
+ proc_node,
1064
+ writes,
1065
+ patch_config(
1066
+ merge_configs(config, additional_config),
1067
+ run_name=packet.node,
1068
+ callbacks=(
1069
+ manager.get_child(f"graph:step:{step}") if manager else None
1070
+ ),
1071
+ configurable={
1072
+ CONFIG_KEY_TASK_ID: task_id,
1073
+ # deque.extend is thread-safe
1074
+ CONFIG_KEY_SEND: writes.extend,
1075
+ CONFIG_KEY_READ: partial(
1076
+ local_read,
1077
+ scratchpad,
1078
+ channels,
1079
+ managed,
1080
+ PregelTaskWrites(
1081
+ translated_task_path, packet.node, writes, triggers
1082
+ ),
1083
+ ),
1084
+ CONFIG_KEY_CHECKPOINTER: (
1085
+ checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
1086
+ ),
1087
+ CONFIG_KEY_CHECKPOINT_MAP: {
1088
+ **configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
1089
+ parent_ns: checkpoint["id"],
1090
+ },
1091
+ CONFIG_KEY_CHECKPOINT_ID: None,
1092
+ CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
1093
+ CONFIG_KEY_SCRATCHPAD: scratchpad,
1094
+ CONFIG_KEY_RUNTIME: runtime,
1095
+ },
1096
+ ),
1097
+ triggers,
1098
+ proc.retry_policy or retry_policy,
1099
+ cache_key,
1100
+ task_id,
1101
+ translated_task_path,
1102
+ writers=proc.flat_writers,
1103
+ subgraphs=proc.subgraphs,
1104
+ timeout=packet.timeout if packet.timeout is not None else proc.timeout,
1105
+ )
1106
+ else:
1107
+ return PregelTask(task_id, packet.node, translated_task_path)
1108
+
1109
+
1110
+ def prepare_node_error_handler_task(
1111
+ failed_task: PregelExecutableTask,
1112
+ *,
1113
+ handler_node_name: str,
1114
+ failed_error: BaseException,
1115
+ checkpoint: Checkpoint,
1116
+ pending_writes: list[PendingWrite],
1117
+ processes: Mapping[str, PregelNode],
1118
+ channels: Mapping[str, BaseChannel],
1119
+ managed: ManagedValueMapping,
1120
+ config: RunnableConfig,
1121
+ step: int,
1122
+ stop: int,
1123
+ store: BaseStore | None = None,
1124
+ checkpointer: BaseCheckpointSaver | None = None,
1125
+ manager: None | ParentRunManager | AsyncParentRunManager = None,
1126
+ cache_policy: CachePolicy | None = None,
1127
+ retry_policy: Sequence[RetryPolicy] = (),
1128
+ ) -> PregelExecutableTask | None:
1129
+ """Prepare an immediate node-level error handler task for a failed task."""
1130
+ if handler_node_name not in processes:
1131
+ return None
1132
+ proc = processes[handler_node_name]
1133
+ proc_node = proc.node
1134
+ if proc_node is None:
1135
+ return None
1136
+
1137
+ checkpoint_id_bytes = binascii.unhexlify(checkpoint["id"].replace("-", ""))
1138
+ task_id_func = _xxhash_str if checkpoint["v"] > 1 else _uuid5_str
1139
+ configurable = config.get(CONF, {})
1140
+ parent_ns = configurable.get(CONFIG_KEY_CHECKPOINT_NS, "")
1141
+ checkpoint_ns = (
1142
+ f"{parent_ns}{NS_SEP}{handler_node_name}" if parent_ns else handler_node_name
1143
+ )
1144
+ task_id = task_id_func(
1145
+ checkpoint_id_bytes,
1146
+ checkpoint_ns,
1147
+ str(step),
1148
+ handler_node_name,
1149
+ PUSH,
1150
+ "node_error_handler",
1151
+ failed_task.id,
1152
+ )
1153
+ task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
1154
+ translated_task_path = (*failed_task.path[:3], "node_error_handler", False)
1155
+ metadata = {
1156
+ "coco_graph_step": step,
1157
+ "coco_graph_node": handler_node_name,
1158
+ "coco_graph_triggers": PUSH_TRIGGER,
1159
+ "coco_graph_path": translated_task_path,
1160
+ "coco_graph_checkpoint_ns": task_checkpoint_ns,
1161
+ }
1162
+ if proc.metadata:
1163
+ metadata.update(proc.metadata)
1164
+ writes: deque[tuple[str, Any]] = deque()
1165
+
1166
+ effective_retry_policy = proc.retry_policy or retry_policy
1167
+ effective_cache_policy = proc.cache_policy or cache_policy
1168
+ if effective_cache_policy:
1169
+ args_key = effective_cache_policy.key_func(failed_task.input)
1170
+ cache_key = CacheKey(
1171
+ (
1172
+ CACHE_NS_WRITES,
1173
+ (identifier(proc) or "__dynamic__"),
1174
+ handler_node_name,
1175
+ ),
1176
+ xxh3_128_hexdigest(
1177
+ args_key.encode() if isinstance(args_key, str) else args_key
1178
+ ),
1179
+ effective_cache_policy.ttl,
1180
+ )
1181
+ else:
1182
+ cache_key = None
1183
+
1184
+ scratchpad = _scratchpad(
1185
+ config[CONF].get(CONFIG_KEY_SCRATCHPAD),
1186
+ pending_writes,
1187
+ task_id,
1188
+ xxh3_128_hexdigest(task_checkpoint_ns.encode()),
1189
+ config[CONF].get(CONFIG_KEY_RESUME_MAP),
1190
+ step,
1191
+ stop,
1192
+ )
1193
+ runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
1194
+ runtime = runtime.override(
1195
+ store=store, previous=checkpoint["channel_values"].get(PREVIOUS, None)
1196
+ )
1197
+ additional_config: RunnableConfig = {
1198
+ "metadata": metadata,
1199
+ "tags": proc.tags,
1200
+ }
1201
+ return PregelExecutableTask(
1202
+ handler_node_name,
1203
+ failed_task.input,
1204
+ proc_node,
1205
+ writes,
1206
+ patch_config(
1207
+ merge_configs(config, additional_config),
1208
+ run_name=handler_node_name,
1209
+ callbacks=manager.get_child(f"graph:step:{step}") if manager else None,
1210
+ configurable={
1211
+ CONFIG_KEY_TASK_ID: task_id,
1212
+ CONFIG_KEY_SEND: writes.extend,
1213
+ CONFIG_KEY_READ: partial(
1214
+ local_read,
1215
+ scratchpad,
1216
+ channels,
1217
+ managed,
1218
+ PregelTaskWrites(
1219
+ translated_task_path,
1220
+ handler_node_name,
1221
+ writes,
1222
+ PUSH_TRIGGER,
1223
+ ),
1224
+ ),
1225
+ CONFIG_KEY_CHECKPOINTER: (
1226
+ checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
1227
+ ),
1228
+ CONFIG_KEY_CHECKPOINT_MAP: {
1229
+ **configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
1230
+ parent_ns: checkpoint["id"],
1231
+ },
1232
+ CONFIG_KEY_CHECKPOINT_ID: None,
1233
+ CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
1234
+ CONFIG_KEY_SCRATCHPAD: scratchpad,
1235
+ CONFIG_KEY_RUNTIME: runtime,
1236
+ CONFIG_KEY_NODE_ERROR: NodeError(
1237
+ node=failed_task.name, error=failed_error
1238
+ ),
1239
+ },
1240
+ ),
1241
+ PUSH_TRIGGER,
1242
+ effective_retry_policy,
1243
+ cache_key,
1244
+ task_id,
1245
+ translated_task_path,
1246
+ writers=proc.flat_writers,
1247
+ subgraphs=proc.subgraphs,
1248
+ )
1249
+
1250
+
1251
+ def checkpoint_null_version(
1252
+ checkpoint: Checkpoint,
1253
+ ) -> V | None:
1254
+ """Get the null version for the checkpoint, if available."""
1255
+ for version in checkpoint["channel_versions"].values():
1256
+ return type(version)()
1257
+ return None
1258
+
1259
+
1260
+ def _triggers(
1261
+ channels: Mapping[str, BaseChannel],
1262
+ versions: ChannelVersions,
1263
+ seen: ChannelVersions | None,
1264
+ null_version: V,
1265
+ proc: PregelNode,
1266
+ ) -> bool:
1267
+ if seen is None:
1268
+ for chan in proc.triggers:
1269
+ if channels[chan].is_available():
1270
+ return True
1271
+ else:
1272
+ for chan in proc.triggers:
1273
+ if channels[chan].is_available() and versions.get( # type: ignore[operator]
1274
+ chan, null_version
1275
+ ) > seen.get(chan, null_version):
1276
+ return True
1277
+ return False
1278
+
1279
+
1280
+ def _scratchpad(
1281
+ parent_scratchpad: PregelScratchpad | None,
1282
+ pending_writes: list[PendingWrite],
1283
+ task_id: str,
1284
+ namespace_hash: str,
1285
+ resume_map: dict[str, Any] | None,
1286
+ step: int,
1287
+ stop: int,
1288
+ ) -> PregelScratchpad:
1289
+ if len(pending_writes) > 0:
1290
+ # find global resume value
1291
+ for w in pending_writes:
1292
+ if w[0] == NULL_TASK_ID and w[1] == RESUME:
1293
+ null_resume_write = w
1294
+ break
1295
+ else:
1296
+ # None cannot be used as a resume value, because it would be difficult to
1297
+ # distinguish from missing when used over http
1298
+ null_resume_write = None
1299
+
1300
+ # find task-specific resume value
1301
+ for w in pending_writes:
1302
+ if w[0] == task_id and w[1] == RESUME:
1303
+ task_resume_write = w[2]
1304
+ if not isinstance(task_resume_write, list):
1305
+ task_resume_write = [task_resume_write]
1306
+ break
1307
+ else:
1308
+ task_resume_write = []
1309
+ del w
1310
+
1311
+ # find namespace and task-specific resume value
1312
+ if resume_map and namespace_hash in resume_map:
1313
+ mapped_resume_write = resume_map[namespace_hash]
1314
+ task_resume_write.append(mapped_resume_write)
1315
+
1316
+ else:
1317
+ null_resume_write = None
1318
+ task_resume_write = []
1319
+
1320
+ def get_null_resume(consume: bool = False) -> Any:
1321
+ if null_resume_write is None:
1322
+ if parent_scratchpad is not None:
1323
+ return parent_scratchpad.get_null_resume(consume)
1324
+ return None
1325
+ if consume:
1326
+ try:
1327
+ pending_writes.remove(null_resume_write)
1328
+ return null_resume_write[2]
1329
+ except ValueError:
1330
+ return None
1331
+ return null_resume_write[2]
1332
+
1333
+ # using itertools.count as an atomic counter (+= 1 is not thread-safe)
1334
+ return PregelScratchpad(
1335
+ step=step,
1336
+ stop=stop,
1337
+ # call
1338
+ call_counter=LazyAtomicCounter(),
1339
+ # interrupt
1340
+ interrupt_counter=LazyAtomicCounter(),
1341
+ resume=task_resume_write,
1342
+ get_null_resume=get_null_resume,
1343
+ # subgraph
1344
+ subgraph_counter=LazyAtomicCounter(),
1345
+ )
1346
+
1347
+
1348
+ def _proc_input(
1349
+ proc: PregelNode,
1350
+ managed: ManagedValueMapping,
1351
+ channels: Mapping[str, BaseChannel],
1352
+ *,
1353
+ for_execution: bool,
1354
+ scratchpad: PregelScratchpad,
1355
+ input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] | None,
1356
+ ) -> Any:
1357
+ """Prepare input for a PULL task, based on the process's channels and triggers."""
1358
+ # if in cache return shallow copy
1359
+ if input_cache is not None and proc.input_cache_key in input_cache:
1360
+ return copy(input_cache[proc.input_cache_key])
1361
+ # If all trigger channels subscribed by this process are not empty
1362
+ # then invoke the process with the values of all non-empty channels
1363
+ if isinstance(proc.channels, list):
1364
+ val: dict[str, Any] = {}
1365
+ for chan in proc.channels:
1366
+ if chan in channels:
1367
+ if channels[chan].is_available():
1368
+ val[chan] = channels[chan].get()
1369
+ else:
1370
+ val[chan] = managed[chan].get(scratchpad)
1371
+ elif isinstance(proc.channels, str):
1372
+ if proc.channels in channels:
1373
+ if channels[proc.channels].is_available():
1374
+ val = channels[proc.channels].get()
1375
+ else:
1376
+ return MISSING
1377
+ else:
1378
+ return MISSING
1379
+ else:
1380
+ raise RuntimeError(
1381
+ f"Invalid channels type, expected list or dict, got {proc.channels}"
1382
+ )
1383
+
1384
+ # If the process has a mapper, apply it to the value
1385
+ if for_execution and proc.mapper is not None:
1386
+ val = proc.mapper(val)
1387
+
1388
+ # Cache the input value
1389
+ if input_cache is not None:
1390
+ input_cache[proc.input_cache_key] = val
1391
+
1392
+ return val
1393
+
1394
+
1395
+ def _uuid5_str(namespace: bytes, *parts: str | bytes) -> str:
1396
+ """Generate a UUID from the SHA-1 hash of a namespace and str parts."""
1397
+
1398
+ sha = sha1(namespace, usedforsecurity=False)
1399
+ sha.update(b"".join(p.encode() if isinstance(p, str) else p for p in parts))
1400
+ hex = sha.hexdigest()
1401
+ return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
1402
+
1403
+
1404
+ def _xxhash_str(namespace: bytes, *parts: str | bytes) -> str:
1405
+ """Generate a UUID from the XXH3 hash of a namespace and str parts."""
1406
+ hex = xxh3_128_hexdigest(
1407
+ namespace + b"".join(p.encode() if isinstance(p, str) else p for p in parts)
1408
+ )
1409
+ return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
1410
+
1411
+
1412
+ def task_path_str(tup: str | int | tuple) -> str:
1413
+ """Generate a string representation of the task path."""
1414
+ return (
1415
+ f"~{', '.join(task_path_str(x) for x in tup)}"
1416
+ if isinstance(tup, (tuple, list))
1417
+ else f"{tup:010d}"
1418
+ if isinstance(tup, int)
1419
+ else str(tup)
1420
+ )
1421
+
1422
+
1423
+ LAZY_ATOMIC_COUNTER_LOCK = threading.Lock()
1424
+
1425
+
1426
+ class LazyAtomicCounter:
1427
+ __slots__ = ("_counter",)
1428
+
1429
+ _counter: Callable[[], int] | None
1430
+
1431
+ def __init__(self) -> None:
1432
+ self._counter = None
1433
+
1434
+ def __call__(self) -> int:
1435
+ if self._counter is None:
1436
+ with LAZY_ATOMIC_COUNTER_LOCK:
1437
+ if self._counter is None:
1438
+ self._counter = itertools.count(0).__next__
1439
+ return self._counter()
1440
+
1441
+
1442
+ def sanitize_untracked_values_in_send(
1443
+ packet: Send, channels: Mapping[str, BaseChannel]
1444
+ ) -> Send:
1445
+ """Pop any values belonging to UntrackedValue channels in Send.arg for safe checkpointing.
1446
+
1447
+ Send is often called with state to be passed to the dest node, which may contain
1448
+ UntrackedValues at the top level. Send is not typed and arg may be a nested dict."""
1449
+
1450
+ if not isinstance(packet.arg, dict):
1451
+ # Command
1452
+ return packet
1453
+
1454
+ # top level keys should be the channel names
1455
+ sanitized_arg = {
1456
+ k: v
1457
+ for k, v in packet.arg.items()
1458
+ if not isinstance(channels.get(k), UntrackedValue)
1459
+ }
1460
+ return Send(node=packet.node, arg=sanitized_arg, timeout=packet.timeout)