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,268 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator, Callable, Iterator
4
+ from contextvars import ContextVar, Token
5
+ from typing import Any, TypeVar, cast
6
+ from uuid import UUID
7
+
8
+ from coco_core.callbacks import BaseCallbackHandler
9
+
10
+ from coco_graph._internal._constants import NS_SEP
11
+ from coco_graph.constants import TAG_NOSTREAM
12
+ from coco_graph.pregel.protocol import StreamChunk
13
+
14
+ try:
15
+ from coco_core.tracers._streaming import _StreamingCallbackHandler
16
+ except ImportError:
17
+ _StreamingCallbackHandler = object # type: ignore[assignment,misc]
18
+
19
+
20
+ T = TypeVar("T")
21
+
22
+ ToolCallWriter = Callable[[Any], None]
23
+ """A closure bound to a single tool call that emits `tool-output-delta` events."""
24
+
25
+ _tool_call_writer: ContextVar[ToolCallWriter | None] = ContextVar(
26
+ "coco_graph_tool_call_writer", default=None
27
+ )
28
+ """ContextVar holding the writer for the currently-executing tool call.
29
+
30
+ Set by `StreamToolCallHandler.on_tool_start` and reset on end/error.
31
+ Read by `ToolRuntime.emit_output_delta` (in `coco_graph.prebuilt`).
32
+ """
33
+
34
+
35
+ class StreamToolCallHandler(BaseCallbackHandler, _StreamingCallbackHandler):
36
+ """Callback handler that emits tool-call lifecycle events on the stream.
37
+
38
+ Fires on Coco's `on_tool_*` callbacks and pushes to the `tools`
39
+ stream mode. Emits `tool-started` / `tool-output-delta` /
40
+ `tool-finished` / `tool-error` payloads keyed by `tool_call_id`.
41
+
42
+ While a tool is executing, this handler sets `_tool_call_writer` to a
43
+ closure bound to that call's namespace and `tool_call_id`.
44
+ `ToolRuntime.emit_output_delta` reads that ContextVar so tool bodies
45
+ can stream partial output without threading the writer through their
46
+ own signature.
47
+
48
+ Attached by `Pregel.stream` / `astream` when `"tools"` is in
49
+ `stream_modes`. `run_inline = True` keeps event ordering
50
+ deterministic.
51
+ """
52
+
53
+ run_inline = True
54
+
55
+ def __init__(
56
+ self,
57
+ stream: Callable[[StreamChunk], None],
58
+ subgraphs: bool,
59
+ *,
60
+ parent_ns: tuple[str, ...] | None = None,
61
+ ) -> None:
62
+ """Configure the handler to stream tool-call events.
63
+
64
+ Args:
65
+ stream: Callable that accepts a `StreamChunk` tuple
66
+ `(namespace, mode, payload)` and enqueues it.
67
+ subgraphs: Whether to emit events from tools called inside
68
+ nested subgraphs. When False, only tools at the
69
+ handler's own scope (`parent_ns`) emit.
70
+ parent_ns: Namespace where the handler was attached.
71
+ Mirrors the `StreamMessagesHandler` escape hatch:
72
+ tools whose containing namespace equals `parent_ns`
73
+ still emit even with `subgraphs=False`, so a node that
74
+ explicitly streams a subgraph with `stream_mode="tools"`
75
+ sees its own tools.
76
+ """
77
+ self.stream = stream
78
+ self.subgraphs = subgraphs
79
+ self.parent_ns = parent_ns
80
+ # run_id → (namespace, tool_call_id, ContextVar token)
81
+ # `on_tool_end` does not receive `tool_call_id` in kwargs, so
82
+ # we correlate by `run_id` which is present on every callback.
83
+ self._run_to_call: dict[
84
+ UUID, tuple[tuple[str, ...], str, Token[ToolCallWriter | None]]
85
+ ] = {}
86
+
87
+ def _ns_for_emit(
88
+ self,
89
+ metadata: dict[str, Any] | None,
90
+ tags: list[str] | None,
91
+ ) -> tuple[str, ...] | None:
92
+ """Resolve the namespace this tool call should emit at, or `None` to skip.
93
+
94
+ Mirrors `StreamMessagesHandler.on_chat_model_start`'s namespace
95
+ derivation: parses `coco_graph_checkpoint_ns` (which ends with
96
+ the `node_name:task_id` of the calling node), drops that
97
+ trailing segment, and returns the containing subgraph's own
98
+ namespace. Returns `None` when the call should be silently
99
+ suppressed:
100
+
101
+ - `metadata` is missing — handler is attached to a context
102
+ without Pregel routing info.
103
+ - `TAG_NOSTREAM` is in `tags` — caller explicitly opted out.
104
+ - Tool runs in a subgraph (`len(ns) > 0`) and the handler was
105
+ attached with `subgraphs=False` and a different `parent_ns`
106
+ than the call's containing subgraph.
107
+ """
108
+ if not metadata:
109
+ return None
110
+ if tags and TAG_NOSTREAM in tags:
111
+ return None
112
+ nskey = metadata.get("coco_graph_checkpoint_ns")
113
+ if not nskey:
114
+ ns: tuple[str, ...] = ()
115
+ else:
116
+ ns = tuple(cast(str, nskey).split(NS_SEP))[:-1]
117
+ if not self.subgraphs and len(ns) > 0 and ns != self.parent_ns:
118
+ return None
119
+ return ns
120
+
121
+ def _start(
122
+ self,
123
+ serialized: dict[str, Any] | None,
124
+ input_str: str,
125
+ *,
126
+ run_id: UUID,
127
+ metadata: dict[str, Any] | None,
128
+ tags: list[str] | None,
129
+ inputs: dict[str, Any] | None,
130
+ kwargs: dict[str, Any],
131
+ ) -> None:
132
+ ns = self._ns_for_emit(metadata, tags)
133
+ if ns is None:
134
+ return
135
+ tool_call_id = cast("str | None", kwargs.get("tool_call_id")) or str(run_id)
136
+ tool_name = (
137
+ (serialized or {}).get("name")
138
+ or cast("str | None", kwargs.get("name"))
139
+ or ""
140
+ )
141
+
142
+ def writer(delta: Any) -> None:
143
+ self.stream(
144
+ (
145
+ ns,
146
+ "tools",
147
+ {
148
+ "event": "tool-output-delta",
149
+ "tool_call_id": tool_call_id,
150
+ "delta": delta,
151
+ },
152
+ )
153
+ )
154
+
155
+ token = _tool_call_writer.set(writer)
156
+ self._run_to_call[run_id] = (ns, tool_call_id, token)
157
+
158
+ payload: dict[str, Any] = {
159
+ "event": "tool-started",
160
+ "tool_call_id": tool_call_id,
161
+ "tool_name": tool_name,
162
+ }
163
+ if inputs is not None:
164
+ payload["input"] = inputs
165
+ self.stream((ns, "tools", payload))
166
+
167
+ def _end(self, output: Any, *, run_id: UUID) -> None:
168
+ info = self._run_to_call.pop(run_id, None)
169
+ if info is None:
170
+ return
171
+ ns, tool_call_id, token = info
172
+ self._reset_writer(token)
173
+ self.stream(
174
+ (
175
+ ns,
176
+ "tools",
177
+ {
178
+ "event": "tool-finished",
179
+ "tool_call_id": tool_call_id,
180
+ "output": output,
181
+ },
182
+ )
183
+ )
184
+
185
+ def _error(self, error: BaseException, *, run_id: UUID) -> None:
186
+ info = self._run_to_call.pop(run_id, None)
187
+ if info is None:
188
+ return
189
+ ns, tool_call_id, token = info
190
+ self._reset_writer(token)
191
+ self.stream(
192
+ (
193
+ ns,
194
+ "tools",
195
+ {
196
+ "event": "tool-error",
197
+ "tool_call_id": tool_call_id,
198
+ "message": str(error),
199
+ },
200
+ )
201
+ )
202
+
203
+ def tap_output_aiter(
204
+ self, run_id: UUID, output: AsyncIterator[T]
205
+ ) -> AsyncIterator[T]:
206
+ """Pass-through — required by the `_StreamingCallbackHandler` protocol."""
207
+ return output
208
+
209
+ def tap_output_iter(self, run_id: UUID, output: Iterator[T]) -> Iterator[T]:
210
+ """Pass-through — sync counterpart to `tap_output_aiter`."""
211
+ return output
212
+
213
+ @staticmethod
214
+ def _reset_writer(token: Token[ToolCallWriter | None]) -> None:
215
+ # Token is invalid if `on_tool_end` runs in a different context
216
+ # than `on_tool_start` (e.g. coco may hand off to a thread
217
+ # worker without copying the context). Swallow that case; the
218
+ # ContextVar lifetime is bounded by the enclosing task anyway.
219
+ try:
220
+ _tool_call_writer.reset(token)
221
+ except ValueError:
222
+ pass
223
+
224
+ # ------------------------------------------------------------------
225
+ # Sync callbacks
226
+ # ------------------------------------------------------------------
227
+
228
+ def on_tool_start(
229
+ self,
230
+ serialized: dict[str, Any],
231
+ input_str: str,
232
+ *,
233
+ run_id: UUID,
234
+ parent_run_id: UUID | None = None,
235
+ tags: list[str] | None = None,
236
+ metadata: dict[str, Any] | None = None,
237
+ inputs: dict[str, Any] | None = None,
238
+ **kwargs: Any,
239
+ ) -> Any:
240
+ self._start(
241
+ serialized,
242
+ input_str,
243
+ run_id=run_id,
244
+ metadata=metadata,
245
+ tags=tags,
246
+ inputs=inputs,
247
+ kwargs=kwargs,
248
+ )
249
+
250
+ def on_tool_end(
251
+ self,
252
+ output: Any,
253
+ *,
254
+ run_id: UUID,
255
+ parent_run_id: UUID | None = None,
256
+ **kwargs: Any,
257
+ ) -> Any:
258
+ self._end(output, run_id=run_id)
259
+
260
+ def on_tool_error(
261
+ self,
262
+ error: BaseException,
263
+ *,
264
+ run_id: UUID,
265
+ parent_run_id: UUID | None = None,
266
+ **kwargs: Any,
267
+ ) -> Any:
268
+ self._error(error, run_id=run_id)
@@ -0,0 +1,221 @@
1
+ from __future__ import annotations
2
+
3
+ import dis
4
+ import re
5
+ from collections.abc import Callable, Sequence
6
+ from functools import partial
7
+ from types import CodeType, FunctionType
8
+ from typing import Any
9
+
10
+ from coco_core.runnables import (
11
+ Runnable,
12
+ RunnableLambda,
13
+ RunnableParallel,
14
+ RunnableSequence,
15
+ )
16
+ from coco_core.runnables.base import RunnableBindingBase
17
+ from coco_core.runnables.config import run_in_executor
18
+ from coco_graph.checkpoint.base import ChannelVersions
19
+
20
+ from coco_graph._internal._runnable import RunnableCallable, RunnableSeq
21
+ from coco_graph._internal._timeout import sync_timeout_unsupported
22
+ from coco_graph.pregel.protocol import PregelProtocol
23
+
24
+ _SEQUENCE_TYPES = (RunnableSeq, RunnableSequence)
25
+
26
+
27
+ def get_new_channel_versions(
28
+ previous_versions: ChannelVersions, current_versions: ChannelVersions
29
+ ) -> ChannelVersions:
30
+ """Get subset of current_versions that are newer than previous_versions."""
31
+ if previous_versions:
32
+ version_type = type(next(iter(current_versions.values()), None))
33
+ null_version = version_type() # type: ignore[misc]
34
+ new_versions = {
35
+ k: v
36
+ for k, v in current_versions.items()
37
+ if v > previous_versions.get(k, null_version) # type: ignore[operator]
38
+ }
39
+ else:
40
+ new_versions = current_versions
41
+
42
+ return new_versions
43
+
44
+
45
+ def find_subgraph_pregel(candidate: Runnable) -> PregelProtocol | None:
46
+ from coco_graph.pregel import Pregel
47
+
48
+ candidates: list[Runnable] = [candidate]
49
+
50
+ for c in candidates:
51
+ if (
52
+ isinstance(c, PregelProtocol)
53
+ # subgraphs that disabled checkpointing are not considered
54
+ and (not isinstance(c, Pregel) or c.checkpointer is not False)
55
+ ):
56
+ return c
57
+ elif isinstance(c, RunnableSequence) or isinstance(c, RunnableSeq):
58
+ candidates.extend(c.steps)
59
+ elif isinstance(c, RunnableLambda):
60
+ candidates.extend(c.deps)
61
+ elif isinstance(c, RunnableCallable):
62
+ if c.func is not None:
63
+ candidates.extend(
64
+ nl.__self__ if hasattr(nl, "__self__") else nl
65
+ for nl in get_function_nonlocals(c.func)
66
+ )
67
+ elif c.afunc is not None:
68
+ candidates.extend(
69
+ nl.__self__ if hasattr(nl, "__self__") else nl
70
+ for nl in get_function_nonlocals(c.afunc)
71
+ )
72
+
73
+ return None
74
+
75
+
76
+ def _sequence_steps(runnable: Runnable) -> Sequence[Runnable] | None:
77
+ if isinstance(runnable, _SEQUENCE_TYPES):
78
+ return runnable.steps
79
+ return None
80
+
81
+
82
+ def _parallel_steps(runnable: Runnable) -> Sequence[Runnable] | None:
83
+ if isinstance(runnable, RunnableParallel):
84
+ return tuple(runnable.steps__.values())
85
+ return None
86
+
87
+
88
+ def _has_method_override(runnable: Runnable, method_name: str) -> bool:
89
+ method = getattr(type(runnable), method_name, None)
90
+ return method is not None and method is not getattr(Runnable, method_name)
91
+
92
+
93
+ def _is_executor_backed_afunc(afunc: Callable[..., Any] | None) -> bool:
94
+ return isinstance(afunc, partial) and afunc.func is run_in_executor
95
+
96
+
97
+ def _has_native_async(runnable: Runnable) -> bool:
98
+ if isinstance(runnable, RunnableCallable):
99
+ return runnable.afunc is not None and not _is_executor_backed_afunc(
100
+ runnable.afunc
101
+ )
102
+ if isinstance(runnable, RunnableLambda):
103
+ return bool(getattr(runnable, "afunc", False))
104
+ return _has_method_override(runnable, "ainvoke")
105
+
106
+
107
+ def _runnable_has_native_async(runnable: Runnable) -> bool:
108
+ """Return whether a runnable can be idle-timed without known sync code.
109
+
110
+ For custom runnable subclasses, an `ainvoke` override is treated as the
111
+ async contract. We do not introspect whether that implementation delegates
112
+ to blocking work internally — e.g. a subclass whose `ainvoke` calls
113
+ `asyncio.to_thread(self.invoke, ...)` will pass this check but the wrapped
114
+ sync work is still uncancellable. Idle-timeout enforcement on such a
115
+ runnable will fire `NodeTimeoutError` correctly, but the background thread
116
+ will keep running until its sync work returns.
117
+ """
118
+
119
+ while isinstance(runnable, RunnableBindingBase):
120
+ runnable = runnable.bound
121
+ steps = _sequence_steps(runnable)
122
+ if steps is None:
123
+ steps = _parallel_steps(runnable)
124
+ if steps is not None:
125
+ return all(_runnable_has_native_async(step) for step in steps)
126
+ # Raw callables and the common composition wrappers created by graph
127
+ # builders fall through here. We do not exhaustively unwrap every Runnable
128
+ # wrapper — wrappers that provide `ainvoke` are treated as owning the async
129
+ # contract.
130
+ return _has_native_async(runnable)
131
+
132
+
133
+ def validate_timeout_supported(runnable: Runnable, *, name: str) -> None:
134
+ if not _runnable_has_native_async(runnable):
135
+ raise sync_timeout_unsupported(name)
136
+
137
+
138
+ # Values treated as dead ends when deciding whether to walk a function's
139
+ # bytecode. A container can hold a graph, but `find_subgraph_pregel` does not
140
+ # look inside one, so skipping it costs nothing while that holds. Matched by
141
+ # exact type, since a subclass of a builtin can carry attributes.
142
+ _LEAF_TYPES = frozenset(
143
+ {
144
+ int,
145
+ float,
146
+ complex,
147
+ bool,
148
+ str,
149
+ bytes,
150
+ bytearray,
151
+ list,
152
+ tuple,
153
+ dict,
154
+ set,
155
+ frozenset,
156
+ type(None),
157
+ }
158
+ )
159
+
160
+
161
+ def get_function_nonlocals(func: Callable) -> list[Any]:
162
+ """Get the values a function reaches from outside its own scope.
163
+
164
+ Args:
165
+ func: The function to check.
166
+
167
+ Returns:
168
+ Every captured cell value, the globals the function names, and each
169
+ value along an attribute path it loads. Over-approximates: a value can
170
+ come back without the function reaching it at runtime.
171
+ """
172
+ func = getattr(func, "__func__", func) # bound method -> function
173
+ wrapped = getattr(func, "__wrapped__", None)
174
+ if callable(wrapped):
175
+ func = getattr(wrapped, "__func__", wrapped)
176
+ if not isinstance(func, FunctionType):
177
+ return []
178
+ code = func.__code__
179
+
180
+ cells: dict[str, Any] = {}
181
+ for name, cell in zip(code.co_freevars, func.__closure__ or ()):
182
+ try:
183
+ cells[name] = cell.cell_contents
184
+ except ValueError:
185
+ continue # empty cell: a recursive def not yet bound
186
+
187
+ # Every captured value counts, referenced or not: over-declaring costs an
188
+ # introspection entry, under-declaring drops the subgraph's checkpoints and
189
+ # stream events. Checking each cell against the bytecode would cost more and
190
+ # only trade the cheap error for the expensive one.
191
+ values: list[Any] = list(cells.values())
192
+ global_ns = func.__globals__
193
+ globals_ = {name: global_ns[name] for name in code.co_names if name in global_ns}
194
+ if all(type(v) in _LEAF_TYPES for v in (*cells.values(), *globals_.values())):
195
+ return values
196
+
197
+ # Nested code objects hold the references made by inner defs, lambdas and
198
+ # comprehensions, which resolve against the namespaces gathered above.
199
+ codes = [code]
200
+ for c in codes:
201
+ codes.extend(k for k in c.co_consts if isinstance(k, CodeType))
202
+ value: Any = None
203
+ for instruction in dis.get_instructions(c):
204
+ opname = instruction.opname
205
+ if opname == "LOAD_GLOBAL":
206
+ value = globals_.get(instruction.argval)
207
+ elif opname == "LOAD_DEREF":
208
+ value = cells.get(instruction.argval)
209
+ elif opname in ("LOAD_ATTR", "LOAD_METHOD"):
210
+ value = getattr(value, instruction.argval, None)
211
+ else:
212
+ value = None # anything else ends the chain: `a, b.c` is not `a.c`
213
+ continue
214
+ if value is not None:
215
+ values.append(value)
216
+ return values
217
+
218
+
219
+ def is_xxh3_128_hexdigest(value: str) -> bool:
220
+ """Check if the given string matches the format of xxh3_128_hexdigest."""
221
+ return bool(re.fullmatch(r"[0-9a-f]{32}", value))
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from typing import Any
5
+
6
+ from coco_graph._internal._constants import RESERVED
7
+ from coco_graph.channels.base import BaseChannel
8
+ from coco_graph.managed.base import ManagedValueMapping
9
+ from coco_graph.pregel._read import PregelNode
10
+ from coco_graph.types import All
11
+
12
+
13
+ def validate_graph(
14
+ nodes: Mapping[str, PregelNode],
15
+ channels: dict[str, BaseChannel],
16
+ managed: ManagedValueMapping,
17
+ input_channels: str | Sequence[str],
18
+ output_channels: str | Sequence[str],
19
+ stream_channels: str | Sequence[str] | None,
20
+ interrupt_after_nodes: All | Sequence[str],
21
+ interrupt_before_nodes: All | Sequence[str],
22
+ ) -> None:
23
+ for chan in channels:
24
+ if chan in RESERVED:
25
+ raise ValueError(f"Channel name '{chan}' is reserved")
26
+ for name in managed:
27
+ if name in RESERVED:
28
+ raise ValueError(f"Managed name '{name}' is reserved")
29
+
30
+ subscribed_channels = set[str]()
31
+ for name, node in nodes.items():
32
+ if name in RESERVED:
33
+ raise ValueError(f"Node name '{name}' is reserved")
34
+ if isinstance(node, PregelNode):
35
+ subscribed_channels.update(node.triggers)
36
+ if isinstance(node.channels, str):
37
+ if node.channels not in channels:
38
+ raise ValueError(
39
+ f"Node {name} reads channel '{node.channels}' "
40
+ f"not in known channels: '{repr(sorted(channels))[:100]}'"
41
+ )
42
+ else:
43
+ for chan in node.channels:
44
+ if chan not in channels and chan not in managed:
45
+ raise ValueError(
46
+ f"Node {name} reads channel '{chan}' "
47
+ f"not in known channels: '{repr(sorted(channels))[:100]}'"
48
+ )
49
+ else:
50
+ raise TypeError(
51
+ f"Invalid node type {type(node)}, expected PregelNode or NodeBuilder"
52
+ )
53
+
54
+ for chan in subscribed_channels:
55
+ if chan not in channels:
56
+ raise ValueError(
57
+ f"Subscribed channel '{chan}' not "
58
+ f"in known channels: '{repr(sorted(channels))[:100]}'"
59
+ )
60
+
61
+ if isinstance(input_channels, str):
62
+ if input_channels not in channels:
63
+ raise ValueError(
64
+ f"Input channel '{input_channels}' not "
65
+ f"in known channels: '{repr(sorted(channels))[:100]}'"
66
+ )
67
+ if input_channels not in subscribed_channels:
68
+ raise ValueError(
69
+ f"Input channel {input_channels} is not subscribed to by any node"
70
+ )
71
+ else:
72
+ for chan in input_channels:
73
+ if chan not in channels:
74
+ raise ValueError(
75
+ f"Input channel '{chan}' not in '{repr(sorted(channels))[:100]}'"
76
+ )
77
+ if all(chan not in subscribed_channels for chan in input_channels):
78
+ raise ValueError(
79
+ f"None of the input channels {input_channels} "
80
+ f"are subscribed to by any node"
81
+ )
82
+
83
+ all_output_channels = set[str]()
84
+ if isinstance(output_channels, str):
85
+ all_output_channels.add(output_channels)
86
+ else:
87
+ all_output_channels.update(output_channels)
88
+ if isinstance(stream_channels, str):
89
+ all_output_channels.add(stream_channels)
90
+ elif stream_channels is not None:
91
+ all_output_channels.update(stream_channels)
92
+
93
+ for chan in all_output_channels:
94
+ if chan not in channels:
95
+ raise ValueError(
96
+ f"Output channel '{chan}' not "
97
+ f"in known channels: '{repr(sorted(channels))[:100]}'"
98
+ )
99
+
100
+ if interrupt_after_nodes != "*":
101
+ for n in interrupt_after_nodes:
102
+ if n not in nodes:
103
+ raise ValueError(f"Node {n} not in nodes")
104
+ if interrupt_before_nodes != "*":
105
+ for n in interrupt_before_nodes:
106
+ if n not in nodes:
107
+ raise ValueError(f"Node {n} not in nodes")
108
+
109
+
110
+ def validate_keys(
111
+ keys: str | Sequence[str] | None,
112
+ channels: Mapping[str, Any],
113
+ ) -> None:
114
+ if isinstance(keys, str):
115
+ if keys not in channels:
116
+ raise ValueError(f"Key {keys} not in channels")
117
+ elif keys is not None:
118
+ for chan in keys:
119
+ if chan not in channels:
120
+ raise ValueError(f"Key {chan} not in channels")