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
coco_graph/types.py ADDED
@@ -0,0 +1,1079 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from collections import deque
5
+ from collections.abc import Callable, Hashable, Sequence
6
+ from dataclasses import asdict, dataclass
7
+ from datetime import timedelta
8
+ from typing import (
9
+ TYPE_CHECKING,
10
+ Any,
11
+ ClassVar,
12
+ Generic,
13
+ Literal,
14
+ NamedTuple,
15
+ final,
16
+ overload,
17
+ )
18
+ from warnings import warn
19
+
20
+ from coco_core.messages import AnyMessage
21
+ from coco_core.runnables import Runnable, RunnableConfig
22
+ from coco_graph.checkpoint.base import BaseCheckpointSaver, CheckpointMetadata
23
+ from pydantic import TypeAdapter
24
+ from typing_extensions import (
25
+ NotRequired,
26
+ TypeAliasType,
27
+ TypedDict,
28
+ TypeVar,
29
+ Unpack,
30
+ deprecated,
31
+ )
32
+ from xxhash import xxh3_128_hexdigest
33
+
34
+ from coco_graph._internal._cache import default_cache_key
35
+ from coco_graph._internal._constants import INTERRUPT as _INTERRUPT_KEY
36
+ from coco_graph._internal._fields import get_cached_annotated_keys, get_update_as_tuples
37
+ from coco_graph._internal._retry import default_retry_on
38
+ from coco_graph._internal._typing import MISSING, DeprecatedKwargs
39
+ from coco_graph.warnings import CocoGraphDeprecatedSinceV10, CocoGraphDeprecatedSinceV11
40
+
41
+ # Local TypeVars for generic stream TypedDicts.
42
+ # We use separate TypeVars here (rather than importing from coco_graph.typing)
43
+ # because the typing module TypeVars have defaults that cause type checker issues
44
+ # when used in standalone type aliases.
45
+ StateT = TypeVar("StateT")
46
+ OutputT = TypeVar("OutputT")
47
+ ResponseT = TypeVar("ResponseT", default=Any)
48
+
49
+ if TYPE_CHECKING:
50
+ from coco_graph.pregel.protocol import PregelProtocol
51
+
52
+
53
+ try:
54
+ from coco_core.messages.tool import ToolOutputMixin
55
+ except ImportError:
56
+
57
+ class ToolOutputMixin: # type: ignore[no-redef]
58
+ pass
59
+
60
+
61
+ __all__ = (
62
+ "All",
63
+ "Checkpointer",
64
+ "StreamMode",
65
+ "StreamWriter",
66
+ "StreamPart",
67
+ "ValuesStreamPart",
68
+ "UpdatesStreamPart",
69
+ "MessagesStreamPart",
70
+ "CustomStreamPart",
71
+ "CheckpointStreamPart",
72
+ "TasksStreamPart",
73
+ "DebugStreamPart",
74
+ "TaskPayload",
75
+ "TaskResultPayload",
76
+ "CheckpointTask",
77
+ "CheckpointPayload",
78
+ "DebugPayload",
79
+ "RetryPolicy",
80
+ "TimeoutPolicy",
81
+ "CachePolicy",
82
+ "TracePolicy",
83
+ "omit_payload",
84
+ "Interrupt",
85
+ "StateUpdate",
86
+ "PregelTask",
87
+ "PregelExecutableTask",
88
+ "StateSnapshot",
89
+ "Send",
90
+ "Command",
91
+ "Durability",
92
+ "interrupt",
93
+ "Overwrite",
94
+ "GraphOutput",
95
+ "ensure_valid_checkpointer",
96
+ )
97
+
98
+ Durability = Literal["sync", "async", "exit"]
99
+ """Durability mode for the graph execution.
100
+
101
+ - `'sync'`: Changes are persisted synchronously before the next step starts.
102
+ - `'async'`: Changes are persisted asynchronously while the next step executes.
103
+ - `'exit'`: Changes are persisted only when the graph exits.
104
+ """
105
+
106
+ All = Literal["*"]
107
+ """Special value to indicate that graph should interrupt on all nodes."""
108
+
109
+ Checkpointer = None | bool | BaseCheckpointSaver
110
+ """Type of the checkpointer to use for a subgraph.
111
+
112
+ - `True` enables persistent checkpointing for this subgraph.
113
+ - `False` disables checkpointing, even if the parent graph has a checkpointer.
114
+ - `None` inherits checkpointer from the parent graph.
115
+ """
116
+
117
+
118
+ def ensure_valid_checkpointer(checkpointer: Checkpointer) -> Checkpointer:
119
+ if checkpointer not in (None, True, False) and not isinstance(
120
+ checkpointer, BaseCheckpointSaver
121
+ ):
122
+ raise TypeError(
123
+ "Invalid checkpointer provided. Expected an instance of "
124
+ "`BaseCheckpointSaver`, `True`, `False`, or `None`. "
125
+ f"Received {type(checkpointer).__name__!s}. "
126
+ "Pass a proper saver (e.g., InMemorySaver, AsyncPostgresSaver)."
127
+ )
128
+ return checkpointer
129
+
130
+
131
+ StreamMode = Literal[
132
+ "values", "updates", "checkpoints", "tasks", "debug", "messages", "custom"
133
+ ]
134
+ """How the stream method should emit outputs.
135
+
136
+ - `"values"`: Emit all values in the state after each step, including interrupts.
137
+ When used with functional API, values are emitted once at the end of the workflow.
138
+ - `"updates"`: Emit only the node or task names and updates returned by the nodes or tasks after each step.
139
+ If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are emitted separately.
140
+ - `"custom"`: Emit custom data using from inside nodes or tasks using `StreamWriter`.
141
+ - `"messages"`: Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes or tasks.
142
+ - `"checkpoints"`: Emit an event when a checkpoint is created, in the same format as returned by `get_state()`.
143
+ - `"tasks"`: Emit events when tasks start and finish, including their results and errors.
144
+ - `"debug"`: Emit `"checkpoints"` and `"tasks"` events for debugging purposes.
145
+ """
146
+
147
+ StreamWriter = Callable[[Any], None]
148
+ """`Callable` that accepts a single argument and writes it to the output stream.
149
+ Always injected into nodes if requested as a keyword argument, but it's a no-op
150
+ when not using `stream_mode="custom"`."""
151
+
152
+
153
+ class TaskPayload(TypedDict):
154
+ """Payload for a task start event."""
155
+
156
+ id: str
157
+ """Unique identifier for this task."""
158
+ name: str
159
+ """Name of the node being executed."""
160
+ input: Any
161
+ """Input data passed to the task."""
162
+ triggers: list[str]
163
+ """List of triggers that caused this task to be executed (e.g. channel writes)."""
164
+ metadata: NotRequired[dict[str, Any]]
165
+ """Framework-resolved metadata associated with the task.
166
+
167
+ Generic dict carrier following the messages-stream pattern. Populated by
168
+ `map_debug_tasks` from `task.config["metadata"]` when non-empty, so the
169
+ same keys `stream_mode="messages"` consumers see (e.g. `lc_agent_name`,
170
+ `coco_graph_node`, `coco_graph_step`) are available to stream transformers.
171
+
172
+ Consumers should ignore unrecognized keys.
173
+ """
174
+
175
+
176
+ class TaskResultPayload(TypedDict):
177
+ """Payload for a task result event."""
178
+
179
+ id: str
180
+ """Unique identifier for this task."""
181
+ name: str
182
+ """Name of the node that was executed."""
183
+ error: str | None
184
+ """Error message if the task failed, otherwise `None`."""
185
+ interrupts: list[dict]
186
+ """List of interrupts that occurred during task execution."""
187
+ result: dict[str, Any]
188
+ """Mapping of channel names to the values written by this task."""
189
+
190
+
191
+ class CheckpointTask(TypedDict):
192
+ """A task entry within a `CheckpointPayload`.
193
+
194
+ The keys present depend on the task's state:
195
+
196
+ - **Error:** `id`, `name`, `error`, `state`
197
+ - **Has result:** `id`, `name`, `result`, `interrupts`, `state`
198
+ - **Pending:** `id`, `name`, `interrupts`, `state`
199
+ """
200
+
201
+ id: str
202
+ """Unique identifier for this task."""
203
+ name: str
204
+ """Name of the node being executed."""
205
+ error: NotRequired[str]
206
+ """Error message, present only if the task failed."""
207
+ result: NotRequired[Any]
208
+ """Result of the task, present only if the task completed successfully."""
209
+ interrupts: NotRequired[list[dict]]
210
+ """List of interrupts, present when the task has been interrupted or completed."""
211
+ state: StateSnapshot | RunnableConfig | None
212
+ """Snapshot of the subgraph state, or a `RunnableConfig` pointing to it. `None` if not a subgraph."""
213
+
214
+
215
+ class CheckpointPayload(TypedDict, Generic[StateT]):
216
+ """Payload for a checkpoint event."""
217
+
218
+ config: RunnableConfig | None
219
+ """Configuration for this checkpoint, including the `thread_id` and `checkpoint_id`."""
220
+ metadata: CheckpointMetadata
221
+ """Metadata associated with this checkpoint (e.g. step number, source, writes)."""
222
+ values: StateT
223
+ """Current state values at the time of this checkpoint."""
224
+ next: list[str]
225
+ """Names of the nodes scheduled to execute next."""
226
+ parent_config: RunnableConfig | None
227
+ """Configuration of the parent checkpoint, or `None` if this is the first checkpoint."""
228
+ tasks: list[CheckpointTask]
229
+ """List of tasks associated with this checkpoint."""
230
+
231
+
232
+ class _DebugCheckpointPayload(TypedDict, Generic[StateT]):
233
+ step: int
234
+ """The step number in the graph execution."""
235
+ timestamp: str
236
+ """ISO 8601 timestamp of when this event occurred."""
237
+ type: Literal["checkpoint"]
238
+ """Event type discriminator, always `"checkpoint"`."""
239
+ payload: CheckpointPayload[StateT]
240
+ """The checkpoint payload."""
241
+
242
+
243
+ class _DebugTaskPayload(TypedDict):
244
+ step: int
245
+ """The step number in the graph execution."""
246
+ timestamp: str
247
+ """ISO 8601 timestamp of when this event occurred."""
248
+ type: Literal["task"]
249
+ """Event type discriminator, always `"task"`."""
250
+ payload: TaskPayload
251
+ """The task start payload."""
252
+
253
+
254
+ class _DebugTaskResultPayload(TypedDict):
255
+ step: int
256
+ """The step number in the graph execution."""
257
+ timestamp: str
258
+ """ISO 8601 timestamp of when this event occurred."""
259
+ type: Literal["task_result"]
260
+ """Event type discriminator, always `"task_result"`."""
261
+ payload: TaskResultPayload
262
+ """The task result payload."""
263
+
264
+
265
+ DebugPayload = TypeAliasType(
266
+ "DebugPayload",
267
+ _DebugCheckpointPayload[StateT] | _DebugTaskPayload | _DebugTaskResultPayload,
268
+ type_params=(StateT,),
269
+ )
270
+ """Wrapper payload for debug events. Discriminate on `type`."""
271
+
272
+
273
+ class ValuesStreamPart(TypedDict, Generic[OutputT]):
274
+ """Stream part emitted for `stream_mode="values"`.
275
+
276
+ `data` contains the full state after each step, as returned by `read_channels()`.
277
+ """
278
+
279
+ type: Literal["values"]
280
+ ns: tuple[str, ...]
281
+ data: OutputT
282
+ interrupts: tuple[Interrupt, ...]
283
+
284
+
285
+ class UpdatesStreamPart(TypedDict):
286
+ """Stream part emitted for `stream_mode="updates"`.
287
+
288
+ `data` maps node names to their outputs. May also contain
289
+ `__interrupt__` (tuple of `Interrupt` dicts) and `__metadata__` keys.
290
+ """
291
+
292
+ type: Literal["updates"]
293
+ ns: tuple[str, ...]
294
+ data: dict[str, Any]
295
+
296
+
297
+ class MessagesStreamPart(TypedDict):
298
+ """Stream part emitted for `stream_mode="messages"`.
299
+
300
+ `data` is a 2-tuple of `(message, metadata)` where `message` is a
301
+ `BaseMessage` (e.g. `AIMessageChunk`) and `metadata` is a dict containing
302
+ keys like `coco_graph_step`, `coco_graph_node`, `coco_graph_triggers`, etc.
303
+ """
304
+
305
+ type: Literal["messages"]
306
+ ns: tuple[str, ...]
307
+ data: tuple[AnyMessage, dict[str, Any]]
308
+
309
+
310
+ class CustomStreamPart(TypedDict):
311
+ """Stream part emitted for `stream_mode="custom"`.
312
+
313
+ `data` is whatever value was passed to `StreamWriter` inside a node.
314
+ """
315
+
316
+ type: Literal["custom"]
317
+ ns: tuple[str, ...]
318
+ data: Any
319
+
320
+
321
+ class CheckpointStreamPart(TypedDict, Generic[StateT]):
322
+ """Stream part emitted for `stream_mode="checkpoints"`."""
323
+
324
+ type: Literal["checkpoints"]
325
+ ns: tuple[str, ...]
326
+ data: CheckpointPayload[StateT]
327
+
328
+
329
+ class TasksStreamPart(TypedDict):
330
+ """Stream part emitted for `stream_mode="tasks"`.
331
+
332
+ For task start events, `data` is a `TaskPayload` with `id`, `name`,
333
+ `input`, and `triggers` keys.
334
+
335
+ For task result events, `data` is a `TaskResultPayload` with `id`,
336
+ `name`, `error`, `interrupts`, and `result` keys.
337
+ """
338
+
339
+ type: Literal["tasks"]
340
+ ns: tuple[str, ...]
341
+ data: TaskPayload | TaskResultPayload
342
+
343
+
344
+ class DebugStreamPart(TypedDict, Generic[StateT]):
345
+ """Stream part emitted for `stream_mode="debug"`."""
346
+
347
+ type: Literal["debug"]
348
+ ns: tuple[str, ...]
349
+ data: DebugPayload[StateT]
350
+
351
+
352
+ StreamPart = TypeAliasType(
353
+ "StreamPart",
354
+ ValuesStreamPart[OutputT]
355
+ | UpdatesStreamPart
356
+ | MessagesStreamPart
357
+ | CustomStreamPart
358
+ | CheckpointStreamPart[StateT]
359
+ | TasksStreamPart
360
+ | DebugStreamPart[StateT],
361
+ type_params=(StateT, OutputT),
362
+ )
363
+ """A discriminated union of all v2 stream part types.
364
+
365
+ Use `part["type"]` to narrow the type:
366
+
367
+ ```python
368
+ async for part in graph.astream(input, version="v2"):
369
+ if part["type"] == "values":
370
+ part["data"] # OutputT — full state (pydantic/dataclass/dict)
371
+ elif part["type"] == "messages":
372
+ part["data"] # tuple[BaseMessage, dict] — (message, metadata)
373
+ elif part["type"] == "custom":
374
+ part["data"] # Any — user-defined
375
+ ```
376
+ """
377
+
378
+
379
+ @dataclass(frozen=True)
380
+ class GraphOutput(Generic[OutputT]):
381
+ """Typed container returned by `invoke()` / `ainvoke()` with `version="v2"`.
382
+
383
+ Attributes:
384
+ value: The final output of the graph (dict, Pydantic model, dataclass, etc.).
385
+ interrupts: Any interrupts that occurred during execution.
386
+ """
387
+
388
+ value: OutputT
389
+ interrupts: tuple[Interrupt, ...] = ()
390
+
391
+ def __getitem__(self, key: str) -> Any:
392
+ """Backward compat: `result['__interrupt__']` and dict-key access."""
393
+ warn(
394
+ "Accessing GraphOutput via `result[key]` is deprecated. "
395
+ "Use `result.value` to access the output value directly, "
396
+ "or `result.interrupts` for interrupts.",
397
+ CocoGraphDeprecatedSinceV11,
398
+ stacklevel=2,
399
+ )
400
+ if key == _INTERRUPT_KEY:
401
+ return self.interrupts
402
+ if isinstance(self.value, dict):
403
+ return self.value[key]
404
+ try:
405
+ return getattr(self.value, key)
406
+ except AttributeError:
407
+ raise KeyError(key)
408
+
409
+ def __contains__(self, key: object) -> bool:
410
+ warn(
411
+ "Accessing GraphOutput via `key in result` is deprecated. "
412
+ "Use `result.value` to access the output value directly, "
413
+ "or `result.interrupts` for interrupts.",
414
+ CocoGraphDeprecatedSinceV11,
415
+ stacklevel=2,
416
+ )
417
+ if key == _INTERRUPT_KEY:
418
+ return bool(self.interrupts)
419
+ if isinstance(self.value, dict):
420
+ return key in self.value
421
+ return isinstance(key, str) and hasattr(self.value, key)
422
+
423
+
424
+ _DC_KWARGS = {"kw_only": True, "slots": True, "frozen": True}
425
+
426
+
427
+ class RetryPolicy(NamedTuple):
428
+ """Configuration for retrying nodes.
429
+
430
+ !!! version-added "Added in version 0.2.24"
431
+ """
432
+
433
+ initial_interval: float = 0.5
434
+ """Amount of time that must elapse before the first retry occurs. In seconds."""
435
+ backoff_factor: float = 2.0
436
+ """Multiplier by which the interval increases after each retry."""
437
+ max_interval: float = 128.0
438
+ """Maximum amount of time that may elapse between retries. In seconds."""
439
+ max_attempts: int = 3
440
+ """Maximum number of attempts to make before giving up, including the first."""
441
+ jitter: bool = True
442
+ """Whether to add random jitter to the interval between retries."""
443
+ retry_on: (
444
+ type[Exception] | Sequence[type[Exception]] | Callable[[Exception], bool]
445
+ ) = default_retry_on
446
+ """List of exception classes that should trigger a retry, or a callable that returns `True` for exceptions that should trigger a retry."""
447
+
448
+
449
+ def _coerce_timeout_seconds(
450
+ value: float | timedelta | None, *, field: str
451
+ ) -> float | None:
452
+ if value is None:
453
+ return None
454
+ seconds = value.total_seconds() if isinstance(value, timedelta) else float(value)
455
+ if seconds <= 0:
456
+ raise ValueError(f"{field} must be greater than 0")
457
+ return seconds
458
+
459
+
460
+ @dataclass(**_DC_KWARGS)
461
+ class TimeoutPolicy:
462
+ """Configuration for timing out node attempts.
463
+
464
+ !!! note "Cooperative cancellation"
465
+
466
+ Timeouts rely on asyncio cancellation. If your node uses synchronous
467
+ time.sleep() or other CPU-bound work that blocks the GIL, the timeout will not
468
+ be fired until after the event loop has been released.
469
+
470
+ !!! note "Inline callback dispatch"
471
+
472
+ Under `refresh_on="auto"`, an internal handler refreshes the timeout on any
473
+ callback event that occurs in the execution of the node or its nested descendants.
474
+ """
475
+
476
+ run_timeout: float | timedelta | None = None
477
+ """Hard wall-clock cap (in seconds) for a single node attempt.
478
+
479
+ This timeout is never refreshed by progress signals or `runtime.heartbeat()`.
480
+ """
481
+
482
+ idle_timeout: float | timedelta | None = None
483
+ """Maximum time (in seconds) a single node attempt may go without observable progress."""
484
+
485
+ refresh_on: Literal["auto", "heartbeat"] = "auto"
486
+ """Which signals refresh `idle_timeout`.
487
+
488
+ `"auto"` refreshes on standard graph progress signals and explicit heartbeats.
489
+ `"heartbeat"` refreshes only on explicit `runtime.heartbeat()` calls.
490
+ """
491
+
492
+ @classmethod
493
+ def coerce(
494
+ cls, value: float | timedelta | TimeoutPolicy | None
495
+ ) -> TimeoutPolicy | None:
496
+ """Normalize a timeout value to positive-second policy fields."""
497
+ if value is None:
498
+ return None
499
+ if isinstance(value, TimeoutPolicy):
500
+ # Fast path: a policy already produced by coerce() has float
501
+ # timeouts and a validated refresh_on, so we can return it as-is.
502
+ # `frozen=True` makes this safe to share.
503
+ rt, it = value.run_timeout, value.idle_timeout
504
+ if (
505
+ value.refresh_on in ("auto", "heartbeat")
506
+ and (rt is None or (type(rt) is float and rt > 0))
507
+ and (it is None or (type(it) is float and it > 0))
508
+ and (rt is not None or it is not None)
509
+ ):
510
+ return value
511
+ else:
512
+ value = cls(run_timeout=value)
513
+ if value.refresh_on not in ("auto", "heartbeat"):
514
+ raise ValueError("refresh_on must be 'auto' or 'heartbeat'")
515
+ run_timeout = _coerce_timeout_seconds(value.run_timeout, field="run_timeout")
516
+ idle_timeout = _coerce_timeout_seconds(value.idle_timeout, field="idle_timeout")
517
+ if run_timeout is None and idle_timeout is None:
518
+ return None
519
+ return cls(
520
+ run_timeout=run_timeout,
521
+ idle_timeout=idle_timeout,
522
+ refresh_on=value.refresh_on,
523
+ )
524
+
525
+
526
+ KeyFuncT = TypeVar("KeyFuncT", bound=Callable[..., str | bytes])
527
+
528
+
529
+ @dataclass(**_DC_KWARGS)
530
+ class CachePolicy(Generic[KeyFuncT]):
531
+ """Configuration for caching nodes."""
532
+
533
+ key_func: KeyFuncT = default_cache_key # type: ignore[assignment]
534
+ """Function to generate a cache key from the node's input.
535
+ Defaults to hashing the input with pickle."""
536
+
537
+ ttl: int | None = None
538
+ """Time to live for the cache entry in seconds. If `None`, the entry never expires."""
539
+
540
+
541
+ @dataclass(**_DC_KWARGS)
542
+ class TracePolicy:
543
+ """Configuration for how a node's run is traced.
544
+
545
+ Scope: this only transforms what the node's *own* run records. Child runs created
546
+ by a traced `bound` runnable and the root graph run are not affected. Plain
547
+ function nodes are traced with `trace=False`, so they have no such child runs.
548
+
549
+ Not intended to redact secrets. To redact inputs/outputs across all runs
550
+ (children included), use the LangSmith client's
551
+ `hide_inputs`/`hide_outputs`/`anonymizer` instead.
552
+
553
+ Each processor receives the node's raw input/output value (not a normalized
554
+ kwargs dict) and returns the value to record.
555
+ """
556
+
557
+ process_inputs: Callable[[Any], Any] | None = None
558
+ """Optional callable to transform the node's input before it is recorded on the
559
+ node's trace run. Can be used to omit or summarize large payloads
560
+ (e.g. message history). Not intended to affect the value passed to the node; avoid
561
+ mutating arguments in place."""
562
+
563
+ process_outputs: Callable[[Any], Any] | None = None
564
+ """Optional callable to transform the node's output before it is recorded on the
565
+ node's trace run. Can be used to omit or summarize large payloads
566
+ (e.g. message history). Not intended to affect the value returned by the node; avoid
567
+ mutating arguments in place."""
568
+
569
+
570
+ def omit_payload(_value: Any) -> dict[str, Any]:
571
+ """`TracePolicy` helper that records an empty payload, dropping the value entirely.
572
+
573
+ Use as `process_inputs` and/or `process_outputs` on a `TracePolicy` to keep a node's
574
+ span and its timing while omitting its inputs/outputs from the trace.
575
+ """
576
+ return {}
577
+
578
+
579
+ _DEFAULT_INTERRUPT_ID = "placeholder-id"
580
+
581
+
582
+ @final
583
+ @dataclass(init=False, slots=True)
584
+ class Interrupt(Generic[ResponseT]):
585
+ """Information about an interrupt that occurred in a node.
586
+
587
+ !!! version-added "Added in version 0.2.24"
588
+
589
+ !!! version-changed "Changed in version v0.4.0"
590
+ * `interrupt_id` was introduced as a property
591
+
592
+ !!! version-changed "Changed in version v0.6.0"
593
+
594
+ The following attributes have been removed:
595
+
596
+ * `ns`
597
+ * `when`
598
+ * `resumable`
599
+ * `interrupt_id`, deprecated in favor of `id`
600
+ """
601
+
602
+ value: Any
603
+ """The value associated with the interrupt."""
604
+
605
+ id: str
606
+ """The ID of the interrupt. Can be used to resume the interrupt directly."""
607
+
608
+ response_schema: type[ResponseT] | dict[str, Any] | None = None
609
+ """Schema for the value expected when resuming this interrupt, if the graph provided one.
610
+
611
+ A surfaced interrupt carries JSON Schema (a `dict`); `type[ResponseT]` records the
612
+ Python type at construction so `Interrupt[Decision]` is meaningful to type checkers."""
613
+
614
+ def __init__(
615
+ self,
616
+ value: Any,
617
+ id: str = _DEFAULT_INTERRUPT_ID,
618
+ *,
619
+ response_schema: type[ResponseT] | dict[str, Any] | None = None,
620
+ **deprecated_kwargs: Unpack[DeprecatedKwargs],
621
+ ) -> None:
622
+ self.value = value
623
+ self.response_schema = response_schema
624
+
625
+ if (
626
+ (ns := deprecated_kwargs.get("ns", MISSING)) is not MISSING
627
+ and (id == _DEFAULT_INTERRUPT_ID)
628
+ and (isinstance(ns, Sequence))
629
+ ):
630
+ self.id = xxh3_128_hexdigest("|".join(ns).encode())
631
+ else:
632
+ self.id = id
633
+
634
+ @classmethod
635
+ def from_ns(
636
+ cls,
637
+ value: Any,
638
+ ns: str,
639
+ *,
640
+ response_schema: type[ResponseT] | dict[str, Any] | None = None,
641
+ ) -> Interrupt[ResponseT]:
642
+ return cls(
643
+ value=value,
644
+ id=xxh3_128_hexdigest(ns.encode()),
645
+ response_schema=response_schema,
646
+ )
647
+
648
+ @property
649
+ @deprecated("`interrupt_id` is deprecated. Use `id` instead.", category=None)
650
+ def interrupt_id(self) -> str:
651
+ warn(
652
+ "`interrupt_id` is deprecated. Use `id` instead.",
653
+ CocoGraphDeprecatedSinceV10,
654
+ stacklevel=2,
655
+ )
656
+ return self.id
657
+
658
+
659
+ class StateUpdate(NamedTuple):
660
+ values: dict[str, Any] | None
661
+ as_node: str | None = None
662
+ task_id: str | None = None
663
+
664
+
665
+ class PregelTask(NamedTuple):
666
+ """A Pregel task."""
667
+
668
+ id: str
669
+ name: str
670
+ path: tuple[str | int | tuple, ...]
671
+ error: Exception | None = None
672
+ interrupts: tuple[Interrupt, ...] = ()
673
+ state: None | RunnableConfig | StateSnapshot = None
674
+ result: Any | None = None
675
+
676
+
677
+ if sys.version_info > (3, 11):
678
+ _T_DC_KWARGS = {"weakref_slot": True, "slots": True, "frozen": True}
679
+ else:
680
+ _T_DC_KWARGS = {"frozen": True}
681
+
682
+
683
+ class CacheKey(NamedTuple):
684
+ """Cache key for a task."""
685
+
686
+ ns: tuple[str, ...]
687
+ """Namespace for the cache entry."""
688
+ key: str
689
+ """Key for the cache entry."""
690
+ ttl: int | None
691
+ """Time to live for the cache entry in seconds."""
692
+
693
+
694
+ @dataclass(**_T_DC_KWARGS)
695
+ class PregelExecutableTask:
696
+ name: str
697
+ input: Any
698
+ proc: Runnable
699
+ writes: deque[tuple[str, Any]]
700
+ config: RunnableConfig
701
+ triggers: Sequence[str]
702
+ retry_policy: Sequence[RetryPolicy]
703
+ cache_key: CacheKey | None
704
+ id: str
705
+ path: tuple[str | int | tuple, ...]
706
+ writers: Sequence[Runnable] = ()
707
+ subgraphs: Sequence[PregelProtocol] = ()
708
+ timeout: TimeoutPolicy | None = None
709
+
710
+
711
+ class StateSnapshot(NamedTuple):
712
+ """Snapshot of the state of the graph at the beginning of a step."""
713
+
714
+ values: dict[str, Any] | Any
715
+ """Current values of channels."""
716
+ next: tuple[str, ...]
717
+ """The name of the node to execute in each task for this step."""
718
+ config: RunnableConfig
719
+ """Config used to fetch this snapshot."""
720
+ metadata: CheckpointMetadata | None
721
+ """Metadata associated with this snapshot."""
722
+ created_at: str | None
723
+ """Timestamp of snapshot creation."""
724
+ parent_config: RunnableConfig | None
725
+ """Config used to fetch the parent snapshot, if any."""
726
+ tasks: tuple[PregelTask, ...]
727
+ """Tasks to execute in this step. If already attempted, may contain an error."""
728
+ interrupts: tuple[Interrupt, ...]
729
+ """Interrupts that occurred in this step that are pending resolution."""
730
+
731
+
732
+ class Send:
733
+ """A message or packet to send to a specific node in the graph.
734
+
735
+ The `Send` class is used within a `StateGraph`'s conditional edges to
736
+ dynamically invoke a node with a custom state at the next step.
737
+
738
+ Importantly, the sent state can differ from the core graph's state,
739
+ allowing for flexible and dynamic workflow management.
740
+
741
+ One such example is a "map-reduce" workflow where your graph invokes
742
+ the same node multiple times in parallel with different states,
743
+ before aggregating the results back into the main graph's state.
744
+
745
+ Attributes:
746
+ node (str): The name of the target node to send the message to.
747
+ arg (Any): The state or message to send to the target node.
748
+ timeout (TimeoutPolicy | None): Optional timeout policy for this specific
749
+ pushed task. If omitted, the target node's timeout policy is used.
750
+
751
+ !!! example
752
+
753
+ ```python
754
+ from typing import Annotated
755
+ from coco_graph.types import Send
756
+ from coco_graph.graph import END, START
757
+ from coco_graph.graph import StateGraph
758
+ import operator
759
+
760
+ class OverallState(TypedDict):
761
+ subjects: list[str]
762
+ jokes: Annotated[list[str], operator.add]
763
+
764
+ def continue_to_jokes(state: OverallState):
765
+ return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
766
+
767
+ builder = StateGraph(OverallState)
768
+ builder.add_node("generate_joke", lambda state: {"jokes": [f"Joke about {state['subject']}"]})
769
+ builder.add_conditional_edges(START, continue_to_jokes)
770
+ builder.add_edge("generate_joke", END)
771
+ graph = builder.compile()
772
+
773
+ # Invoking with two subjects results in a generated joke for each
774
+ graph.invoke({"subjects": ["cats", "dogs"]})
775
+ # {'subjects': ['cats', 'dogs'], 'jokes': ['Joke about cats', 'Joke about dogs']}
776
+ ```
777
+ """
778
+
779
+ __slots__ = ("node", "arg", "timeout")
780
+
781
+ node: str
782
+ arg: Any
783
+ timeout: TimeoutPolicy | None
784
+
785
+ def __init__(
786
+ self,
787
+ /,
788
+ node: str,
789
+ arg: Any,
790
+ *,
791
+ timeout: float | timedelta | TimeoutPolicy | None = None,
792
+ ) -> None:
793
+ """
794
+ Initialize a new instance of the `Send` class.
795
+
796
+ Args:
797
+ node: The name of the target node to send the message to.
798
+ arg: The state or message to send to the target node.
799
+ timeout: Optional timeout policy for this specific pushed task. A
800
+ number or `timedelta` is treated as a hard `run_timeout`.
801
+ """
802
+ self.node = node
803
+ self.arg = arg
804
+ self.timeout = TimeoutPolicy.coerce(timeout)
805
+
806
+ def __hash__(self) -> int:
807
+ return hash((self.node, self.arg, self.timeout))
808
+
809
+ def __repr__(self) -> str:
810
+ if self.timeout is None:
811
+ return f"Send(node={self.node!r}, arg={self.arg!r})"
812
+ return f"Send(node={self.node!r}, arg={self.arg!r}, timeout={self.timeout!r})"
813
+
814
+ def __eq__(self, value: object) -> bool:
815
+ return (
816
+ isinstance(value, Send)
817
+ and self.node == value.node
818
+ and self.arg == value.arg
819
+ and self.timeout == value.timeout
820
+ )
821
+
822
+
823
+ N = TypeVar("N", bound=Hashable)
824
+
825
+
826
+ @dataclass(**_DC_KWARGS)
827
+ class Command(Generic[N], ToolOutputMixin):
828
+ """One or more commands to update the graph's state and send messages to nodes.
829
+
830
+ Args:
831
+ graph: Graph to send the command to. Supported values are:
832
+
833
+ - `None`: the current graph
834
+ - `Command.PARENT`: closest parent graph
835
+ update: Update to apply to the graph's state.
836
+ resume: Value to resume execution with. To be used together with [`interrupt()`][coco_graph.types.interrupt].
837
+ Can be one of the following:
838
+
839
+ - Mapping of interrupt ids to resume values
840
+ - A single value with which to resume the next interrupt
841
+ goto: Can be one of the following:
842
+
843
+ - Name of the node to navigate to next (any node that belongs to the specified `graph`)
844
+ - Sequence of node names to navigate to next
845
+ - `Send` object (to execute a node with the input provided)
846
+ - Sequence of `Send` objects
847
+ """
848
+
849
+ graph: str | None = None
850
+ update: Any | None = None
851
+ resume: dict[str, Any] | Any | None = None
852
+ goto: Send | Sequence[Send | N] | N = ()
853
+
854
+ def __repr__(self) -> str:
855
+ # get all non-None values
856
+ contents = ", ".join(
857
+ f"{key}={value!r}" for key, value in asdict(self).items() if value
858
+ )
859
+ return f"Command({contents})"
860
+
861
+ def _update_as_tuples(self) -> Sequence[tuple[str, Any]]:
862
+ if isinstance(self.update, dict):
863
+ return list(self.update.items())
864
+ elif isinstance(self.update, (list, tuple)) and all(
865
+ isinstance(t, tuple) and len(t) == 2 and isinstance(t[0], str)
866
+ for t in self.update
867
+ ):
868
+ return self.update
869
+ elif keys := get_cached_annotated_keys(type(self.update)):
870
+ return get_update_as_tuples(self.update, keys)
871
+ elif self.update is not None:
872
+ return [("__root__", self.update)]
873
+ else:
874
+ return []
875
+
876
+ PARENT: ClassVar[Literal["__parent__"]] = "__parent__"
877
+
878
+
879
+ @overload
880
+ def interrupt(value: Any, *, response_schema: type[ResponseT]) -> ResponseT: ...
881
+
882
+
883
+ @overload
884
+ def interrupt(value: Any, *, response_schema: dict[str, Any] | None = None) -> Any: ...
885
+
886
+
887
+ def interrupt(
888
+ value: Any, *, response_schema: dict[str, Any] | type | None = None
889
+ ) -> Any:
890
+ """Interrupt the graph with a resumable exception from within a node.
891
+
892
+ The `interrupt` function enables human-in-the-loop workflows by pausing graph
893
+ execution and surfacing a value to the client. This value can communicate context
894
+ or request input required to resume execution.
895
+
896
+ In a given node, the first invocation of this function raises a `GraphInterrupt`
897
+ exception, halting execution. The provided `value` is included with the exception
898
+ and sent to the client executing the graph.
899
+
900
+ A client resuming the graph must use the [`Command`][coco_graph.types.Command]
901
+ primitive to specify a value for the interrupt and continue execution.
902
+ The graph resumes from the start of the node, **re-executing** all logic.
903
+
904
+ If a node contains multiple `interrupt` calls, CocoGraph matches resume values
905
+ to interrupts based on their order in the node. This list of resume values
906
+ is scoped to the specific task executing the node and is not shared across tasks.
907
+
908
+ To use an `interrupt`, you must enable a checkpointer, as the feature relies
909
+ on persisting the graph state.
910
+
911
+ !!! example
912
+
913
+ ```python
914
+ import uuid
915
+ from typing import Optional
916
+ from typing_extensions import TypedDict
917
+
918
+ from coco_graph.checkpoint.memory import InMemorySaver
919
+ from coco_graph.constants import START
920
+ from coco_graph.graph import StateGraph
921
+ from coco_graph.types import interrupt, Command
922
+
923
+
924
+ class State(TypedDict):
925
+ \"\"\"The graph state.\"\"\"
926
+
927
+ foo: str
928
+ human_value: Optional[str]
929
+ \"\"\"Human value will be updated using an interrupt.\"\"\"
930
+
931
+
932
+ def node(state: State):
933
+ answer = interrupt(
934
+ # This value will be sent to the client
935
+ # as part of the interrupt information.
936
+ \"what is your age?\"
937
+ )
938
+ print(f\"> Received an input from the interrupt: {answer}\")
939
+ return {\"human_value\": answer}
940
+
941
+
942
+ builder = StateGraph(State)
943
+ builder.add_node(\"node\", node)
944
+ builder.add_edge(START, \"node\")
945
+
946
+ # A checkpointer must be enabled for interrupts to work!
947
+ checkpointer = InMemorySaver()
948
+ graph = builder.compile(checkpointer=checkpointer)
949
+
950
+ config = {
951
+ \"configurable\": {
952
+ \"thread_id\": uuid.uuid4(),
953
+ }
954
+ }
955
+
956
+ for chunk in graph.stream({\"foo\": \"abc\"}, config):
957
+ print(chunk)
958
+
959
+ # > {'__interrupt__': (Interrupt(value='what is your age?', id='45fda8478b2ef754419799e10992af06', response_schema=None),)}
960
+
961
+ command = Command(resume=\"some input from a human!!!\")
962
+
963
+ for chunk in graph.stream(Command(resume=\"some input from a human!!!\"), config):
964
+ print(chunk)
965
+
966
+ # > Received an input from the interrupt: some input from a human!!!
967
+ # > {'node': {'human_value': 'some input from a human!!!'}}
968
+ ```
969
+
970
+ Args:
971
+ value: The value to surface to the client when the graph is interrupted.
972
+ response_schema: Optional schema for the value expected on resume, surfaced
973
+ to clients so they can render a typed input form. Accepts a JSON Schema
974
+ `dict` (used as-is, resume values are not validated), or a Pydantic model
975
+ class, `TypedDict`, or dataclass, which are converted to JSON Schema for
976
+ clients and used to validate the resume value; the validated object is
977
+ what `interrupt` returns.
978
+
979
+ Returns:
980
+ Any: On subsequent invocations within the same node (same task to be precise), returns the value provided during the first invocation,
981
+ validated against `response_schema` when one that supports validation was given.
982
+
983
+ Raises:
984
+ GraphInterrupt: On the first invocation within the node, halts execution and surfaces the provided value to the client.
985
+ pydantic.ValidationError: When a resume value does not match a Pydantic model, `TypedDict`, or dataclass `response_schema`.
986
+ """
987
+ from coco_graph._internal._constants import (
988
+ CONFIG_KEY_CHECKPOINT_NS,
989
+ CONFIG_KEY_SCRATCHPAD,
990
+ CONFIG_KEY_SEND,
991
+ RESUME,
992
+ )
993
+ from coco_graph.config import get_config
994
+ from coco_graph.errors import GraphInterrupt
995
+
996
+ conf = get_config()["configurable"]
997
+ adapter = (
998
+ None
999
+ if response_schema is None or isinstance(response_schema, dict)
1000
+ else TypeAdapter(response_schema)
1001
+ )
1002
+ # track interrupt index
1003
+ scratchpad = conf[CONFIG_KEY_SCRATCHPAD]
1004
+ idx = scratchpad.interrupt_counter()
1005
+ # find previous resume values
1006
+ if scratchpad.resume:
1007
+ if idx < len(scratchpad.resume):
1008
+ v = scratchpad.resume[idx]
1009
+ validated = adapter.validate_python(v) if adapter else v
1010
+ conf[CONFIG_KEY_SEND]([(RESUME, scratchpad.resume[: idx + 1])])
1011
+ return validated
1012
+ # find current resume value
1013
+ v = scratchpad.get_null_resume(True)
1014
+ if v is not None:
1015
+ assert len(scratchpad.resume) == idx, (scratchpad.resume, idx)
1016
+ validated = adapter.validate_python(v) if adapter else v
1017
+ scratchpad.resume.append(v)
1018
+ conf[CONFIG_KEY_SEND]([(RESUME, scratchpad.resume)])
1019
+ return validated
1020
+ # no resume value found
1021
+ raise GraphInterrupt(
1022
+ (
1023
+ Interrupt.from_ns(
1024
+ value=value,
1025
+ ns=conf[CONFIG_KEY_CHECKPOINT_NS],
1026
+ response_schema=adapter.json_schema() if adapter else response_schema,
1027
+ ),
1028
+ )
1029
+ )
1030
+
1031
+
1032
+ @dataclass(slots=True)
1033
+ class Overwrite:
1034
+ """Bypass a reducer and write the wrapped value directly to a `BinaryOperatorAggregate` channel.
1035
+
1036
+ Receiving multiple `Overwrite` values for the same channel in a single super-step
1037
+ will raise an `InvalidUpdateError`.
1038
+
1039
+ !!! example
1040
+
1041
+ ```python
1042
+ from typing import Annotated
1043
+ import operator
1044
+ from coco_graph.graph import StateGraph
1045
+ from coco_graph.types import Overwrite
1046
+
1047
+ class State(TypedDict):
1048
+ messages: Annotated[list, operator.add]
1049
+
1050
+ def node_a(state: TypedDict):
1051
+ # Normal update: uses the reducer (operator.add)
1052
+ return {"messages": ["a"]}
1053
+
1054
+ def node_b(state: State):
1055
+ # Overwrite: bypasses the reducer and replaces the entire value
1056
+ return {"messages": Overwrite(value=["b"])}
1057
+
1058
+ builder = StateGraph(State)
1059
+ builder.add_node("node_a", node_a)
1060
+ builder.add_node("node_b", node_b)
1061
+ builder.set_entry_point("node_a")
1062
+ builder.add_edge("node_a", "node_b")
1063
+ graph = builder.compile()
1064
+
1065
+ # Without Overwrite in node_b, messages would be ["START", "a", "b"]
1066
+ # With Overwrite, messages is just ["b"]
1067
+ result = graph.invoke({"messages": ["START"]})
1068
+ assert result == {"messages": ["b"]}
1069
+ ```
1070
+ """
1071
+
1072
+ value: Any
1073
+ """The value to write directly to the channel, bypassing any reducer."""
1074
+
1075
+ type: Literal["__overwrite__"] = "__overwrite__"
1076
+ """Discriminator field. Lets the channel reducer recognise an `Overwrite`
1077
+ even after its dataclass form is JSON-serialised and the typed instance
1078
+ is lost (e.g. an `orjson`-encoded state update routed through the
1079
+ CocoGraph API server)."""