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,1039 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Any, Literal, cast
5
+
6
+ from coco_core.language_models._compat_bridge import message_to_events
7
+ from coco_core.language_models.chat_model_stream import (
8
+ AsyncChatModelStream,
9
+ ChatModelStream,
10
+ )
11
+ from coco_core.messages import AIMessageChunk, BaseMessage, ToolMessage
12
+ from coco_core._protocol import LifecycleCause, MessagesData
13
+ from typing_extensions import NotRequired, TypedDict
14
+
15
+ from coco_graph.errors import GraphDrained, GraphInterrupt
16
+ from coco_graph.stream._types import ProtocolEvent, StreamTransformer
17
+ from coco_graph.stream.run_stream import AsyncSubgraphRunStream, SubgraphRunStream
18
+ from coco_graph.stream.stream_channel import StreamChannel
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Awaitable, Callable
22
+
23
+ from coco_graph.stream._mux import StreamMux
24
+
25
+ _logger = logging.getLogger(__name__)
26
+
27
+
28
+ class ValuesTransformer(StreamTransformer):
29
+ """Capture values events as a drainable stream of state snapshots.
30
+
31
+ Provides the `run.values` projection. `run.output`,
32
+ `run.interrupted` and `run.interrupts` are tracked directly
33
+ by the run stream and do not depend on this transformer.
34
+
35
+ Native transformer — projection keys are exposed as direct
36
+ attributes on the run stream (e.g. `run.values`).
37
+
38
+ Only values events at the run's own level are captured; snapshots
39
+ from deeper subgraphs are left in the main event log but excluded
40
+ from the projection. "Own level" is defined by `scope`, which
41
+ `stream_events(version="v3")` / `astream_events(version="v3")` populate from the caller's
42
+ checkpoint namespace so that a nested `stream_events(version="v3")` call still
43
+ sees its own root snapshots.
44
+ """
45
+
46
+ _native = True
47
+ required_stream_modes = ("values",)
48
+
49
+ def __init__(self, scope: tuple[str, ...] = ()) -> None:
50
+ super().__init__(scope)
51
+ self._log: StreamChannel[dict[str, Any]] = StreamChannel()
52
+ self._latest: dict[str, Any] | None = None
53
+ self._interrupted = False
54
+ self._interrupts: list[Any] = []
55
+ # Cached as a list once for cheap equality with the protocol
56
+ # event's `namespace` field, which is `list[str]`.
57
+ self._scope_list: list[str] = list(scope)
58
+
59
+ def init(self) -> dict[str, Any]:
60
+ return {"values": self._log}
61
+
62
+ @property
63
+ def error(self) -> BaseException | None:
64
+ """The error that ended the run, or `None` if it succeeded.
65
+
66
+ Set by the mux when it auto-fails the projection log.
67
+ """
68
+ return self._log._error
69
+
70
+ def process(self, event: ProtocolEvent) -> bool:
71
+ if event["method"] != "values":
72
+ return True
73
+ params = event["params"]
74
+ if params["namespace"] != self._scope_list:
75
+ return True
76
+ self._latest = params["data"]
77
+ interrupts = params.get("interrupts", ())
78
+ if interrupts:
79
+ self._interrupted = True
80
+ self._interrupts.extend(interrupts)
81
+ self._log.push(params["data"])
82
+ return True
83
+
84
+
85
+ class CustomTransformer(StreamTransformer):
86
+ """Capture custom events as a drainable stream of arbitrary payloads.
87
+
88
+ Nodes emit custom data via `get_stream_writer()`. This transformer
89
+ surfaces those events on `run.custom` as a `StreamChannel[Any]`,
90
+ preserving payloads in arrival order.
91
+
92
+ Only events at the run's own scope are captured; custom data from
93
+ deeper subgraphs is available on the respective subgraph handle's
94
+ `.custom` projection.
95
+
96
+ Native transformer — `run.custom` is a direct attribute.
97
+ """
98
+
99
+ _native = True
100
+ required_stream_modes = ("custom",)
101
+
102
+ def __init__(self, scope: tuple[str, ...] = ()) -> None:
103
+ super().__init__(scope)
104
+ self._log: StreamChannel[Any] = StreamChannel()
105
+ self._scope_list: list[str] = list(scope)
106
+
107
+ def init(self) -> dict[str, Any]:
108
+ return {"custom": self._log}
109
+
110
+ def process(self, event: ProtocolEvent) -> bool:
111
+ if event["method"] != "custom":
112
+ return True
113
+ params = event["params"]
114
+ if params["namespace"] != self._scope_list:
115
+ return True
116
+ self._log.push(params["data"])
117
+ return True
118
+
119
+
120
+ class UpdatesTransformer(StreamTransformer):
121
+ """Capture updates events as a drainable stream of node outputs.
122
+
123
+ Surfaces `stream_mode="updates"` data on `run.updates` as a
124
+ `StreamChannel[dict[str, Any]]`. Each item is a dict mapping a node
125
+ (or task) name to the update it returned after a step.
126
+
127
+ Only events at the run's own scope are captured; updates from deeper
128
+ subgraphs are available on the respective subgraph handle's
129
+ `.updates` projection.
130
+
131
+ Native transformer — `run.updates` is a direct attribute.
132
+ """
133
+
134
+ _native = True
135
+ required_stream_modes = ("updates",)
136
+
137
+ def __init__(self, scope: tuple[str, ...] = ()) -> None:
138
+ super().__init__(scope)
139
+ self._log: StreamChannel[dict[str, Any]] = StreamChannel()
140
+ self._scope_list: list[str] = list(scope)
141
+
142
+ def init(self) -> dict[str, Any]:
143
+ return {"updates": self._log}
144
+
145
+ def process(self, event: ProtocolEvent) -> bool:
146
+ if event["method"] != "updates":
147
+ return True
148
+ params = event["params"]
149
+ if params["namespace"] != self._scope_list:
150
+ return True
151
+ self._log.push(params["data"])
152
+ return True
153
+
154
+
155
+ class MessagesTransformer(StreamTransformer):
156
+ """Capture messages events as ChatModelStream objects.
157
+
158
+ The messages projection yields one `ChatModelStream` (or
159
+ `AsyncChatModelStream`) per LLM call. Consumers iterate
160
+ `run.messages` to get stream handles, then use each handle's typed
161
+ projections (`.text`, `.reasoning`, `.tool_calls`, `.usage`,
162
+ `.output`) for per-message content.
163
+
164
+ Two input shapes are handled (via `params["data"] = (payload,
165
+ metadata)` from `StreamMessagesHandler`):
166
+
167
+ 1. Protocol event (dict with `"event"` key) — emitted by
168
+ `stream_events(version="v3")` / `astream_events(version="v3")` via the `on_stream_event`
169
+ callback. Routed to an existing `ChatModelStream` by
170
+ `metadata["run_id"]`. A `message-start` event creates a new
171
+ stream; `message-finish` closes it.
172
+ 2. Whole `AIMessage` — emitted from `on_chain_end` when a node
173
+ returns a finalized message. Replayed as a synthetic protocol
174
+ event lifecycle via `message_to_events`, then the
175
+ already-complete stream is pushed to the log.
176
+
177
+ V1 `AIMessageChunk` tuples (from `on_llm_new_token`) are not
178
+ streamed into this projection: chat models that want to populate
179
+ `run.messages` with content-block streaming must use
180
+ `stream_events(version="v3")` / `astream_events(version="v3")`. Models called via the legacy
181
+ `stream()` method still surface their final `AIMessage` via
182
+ `on_chain_end` when a node returns it as state.
183
+
184
+ Only events at the run's own level are projected; tokens from
185
+ deeper subgraphs are left in the main event log but excluded from
186
+ `.messages`. "Own level" is defined by `scope`, which
187
+ `stream_events(version="v3")` / `astream_events(version="v3")` populate from the caller's checkpoint
188
+ namespace so that a `stream_events(version="v3")` call inside a node still sees its
189
+ own root chat model streams on `.messages`. Consumers that need
190
+ subgraph tokens should iterate the raw event stream or register a
191
+ custom transformer.
192
+
193
+ Native transformer — the `messages` projection is exposed as a
194
+ direct attribute on the run stream.
195
+ """
196
+
197
+ _native = True
198
+ required_stream_modes = ("messages",)
199
+
200
+ def __init__(self, scope: tuple[str, ...] = ()) -> None:
201
+ super().__init__(scope)
202
+ self._log: StreamChannel[ChatModelStream] = StreamChannel()
203
+ # Correlate protocol events back to a ChatModelStream by run_id
204
+ # (attached to the event's metadata by StreamMessagesHandler).
205
+ self._by_run: dict[str, ChatModelStream] = {}
206
+ self._ignored_runs: set[str] = set()
207
+ self._pump_fn: Callable[[], bool] | None = None
208
+ self._apump_fn: Callable[[], Awaitable[bool]] | None = None
209
+ # Cached as a list once for cheap equality with the protocol
210
+ # event's `namespace` field, which is `list[str]`.
211
+ self._scope_list: list[str] = list(scope)
212
+
213
+ def init(self) -> dict[str, Any]:
214
+ return {"messages": self._log}
215
+
216
+ def _bind_pump(self, fn: Callable[[], bool]) -> None:
217
+ """Wire the sync pull callback. Called by GraphRunStream._wire_request_more."""
218
+ self._pump_fn = fn
219
+
220
+ def _bind_apump(self, fn: Callable[[], Awaitable[bool]]) -> None:
221
+ """Wire the async pull callback.
222
+
223
+ Called by `AsyncGraphRunStream._wire_arequest_more` so each
224
+ `AsyncChatModelStream` this transformer creates can drive the
225
+ shared graph pump from its projection cursors.
226
+ """
227
+ self._apump_fn = fn
228
+
229
+ def _make_stream(
230
+ self,
231
+ *,
232
+ namespace: list[str],
233
+ node: str | None,
234
+ message_id: str | None,
235
+ ) -> ChatModelStream:
236
+ """Create a ChatModelStream (sync) or AsyncChatModelStream (async).
237
+
238
+ Wires whichever pump is bound. Prefers the async pump so nested
239
+ iteration under `AsyncGraphRunStream` drives the graph forward
240
+ without a background task. The unwired fallback (no pump bound)
241
+ is used by unit tests that dispatch events manually.
242
+ """
243
+ if self._apump_fn is not None:
244
+ astream = AsyncChatModelStream(
245
+ namespace=namespace,
246
+ node=node,
247
+ message_id=message_id,
248
+ )
249
+ astream.set_arequest_more(self._apump_fn)
250
+ return astream
251
+ if self._pump_fn is not None:
252
+ stream: ChatModelStream = ChatModelStream(
253
+ namespace=namespace,
254
+ node=node,
255
+ message_id=message_id,
256
+ )
257
+ stream.set_request_more(self._pump_fn)
258
+ return stream
259
+ return AsyncChatModelStream(
260
+ namespace=namespace,
261
+ node=node,
262
+ message_id=message_id,
263
+ )
264
+
265
+ def process(self, event: ProtocolEvent) -> bool:
266
+ if event["method"] != "messages":
267
+ return True
268
+ params = event["params"]
269
+ if params["namespace"] != self._scope_list:
270
+ return True
271
+
272
+ payload, metadata = params["data"]
273
+ node: str | None = metadata.get("coco_graph_node")
274
+ run_id = str(metadata.get("run_id", "")) if metadata else ""
275
+
276
+ if isinstance(payload, dict) and "event" in payload:
277
+ self._route_protocol_event(
278
+ cast("MessagesData", payload), run_id=run_id, node=node
279
+ )
280
+ elif (
281
+ isinstance(payload, BaseMessage)
282
+ and not isinstance(payload, AIMessageChunk)
283
+ and not isinstance(payload, ToolMessage)
284
+ ):
285
+ self._route_whole_message(payload, node=node)
286
+ # Legacy AIMessageChunk tuples (from on_llm_new_token) are ignored;
287
+ # v1 streaming callers must switch to stream_events(version="v3") to populate this
288
+ # projection.
289
+
290
+ return True
291
+
292
+ def _route_protocol_event(
293
+ self,
294
+ event: MessagesData,
295
+ *,
296
+ run_id: str,
297
+ node: str | None,
298
+ ) -> None:
299
+ event_type = event.get("event")
300
+ if event_type == "message-start":
301
+ # Tool results are exposed on the tools projection and state
302
+ # snapshots; run.messages is the chat-token projection.
303
+ if event.get("role") == "tool":
304
+ self._ignored_runs.add(run_id)
305
+ return
306
+ message_id = event.get("message_id")
307
+ stream = self._make_stream(
308
+ namespace=[],
309
+ node=node,
310
+ message_id=str(message_id) if message_id is not None else None,
311
+ )
312
+ self._by_run[run_id] = stream
313
+ self._log.push(stream)
314
+ stream.dispatch(event)
315
+ elif run_id in self._ignored_runs:
316
+ if event_type == "message-finish":
317
+ self._ignored_runs.discard(run_id)
318
+ elif run_id in self._by_run:
319
+ stream = self._by_run[run_id]
320
+ stream.dispatch(event)
321
+ if event_type == "message-finish":
322
+ del self._by_run[run_id]
323
+
324
+ def _route_whole_message(self, message: BaseMessage, *, node: str | None) -> None:
325
+ stream = self._make_stream(namespace=[], node=node, message_id=message.id)
326
+ for evt in message_to_events(message, message_id=message.id):
327
+ stream.dispatch(evt)
328
+ self._log.push(stream)
329
+
330
+ def finalize(self) -> None:
331
+ """Clear any routing state — streams close themselves via `message-finish`."""
332
+ self._by_run.clear()
333
+ self._ignored_runs.clear()
334
+
335
+ def fail(self, err: BaseException) -> None:
336
+ """Propagate run error to any streams still open when the graph fails."""
337
+ for stream in list(self._by_run.values()):
338
+ stream.fail(err)
339
+ self._by_run.clear()
340
+ self._ignored_runs.clear()
341
+
342
+
343
+ SubgraphStatus = Literal["started", "completed", "failed", "interrupted", "drained"]
344
+
345
+
346
+ def _parse_ns_segment(segment: str) -> tuple[str, str | None]:
347
+ """Split a namespace segment into `(graph_name, trigger_call_id)`.
348
+
349
+ Segments are formatted `node_name:task_id` by `prepare_next_tasks`.
350
+ Returns `(segment, None)` if no `:` is present.
351
+ """
352
+ name, sep, task_id = segment.partition(":")
353
+ return name, task_id if sep else None
354
+
355
+
356
+ class LifecyclePayload(TypedDict, total=False):
357
+ """Payload of a lifecycle event surfaced on the `lifecycle` channel.
358
+
359
+ Auto-forwarded as `lifecycle` protocol events (no `custom:` prefix
360
+ because `LifecycleTransformer` is a native transformer) so remote
361
+ SDK clients receive the same data in-process consumers see via
362
+ `run.lifecycle`.
363
+ """
364
+
365
+ event: SubgraphStatus
366
+ namespace: list[str]
367
+ graph_name: NotRequired[str]
368
+ trigger_call_id: NotRequired[str]
369
+ cause: NotRequired[LifecycleCause]
370
+ error: NotRequired[str]
371
+
372
+
373
+ class _TasksLifecycleBase(StreamTransformer):
374
+ """Shared bookkeeping for `tasks`-event-driven lifecycle inference.
375
+
376
+ Both `LifecycleTransformer` (wire-serializable channel) and
377
+ `SubgraphTransformer` (in-process navigation handles) discover
378
+ subgraphs by watching the same `tasks` stream — `started` on the
379
+ first event at a tracked namespace, terminal status when the
380
+ parent's `TaskResultPayload` arrives. Centralizing the dispatch
381
+ + open-set bookkeeping here keeps the inference rules from
382
+ drifting between the two surfaces.
383
+
384
+ Subclasses provide three template-method hooks:
385
+
386
+ - `_should_track(ns)` — scope filter (e.g. multi-depth vs
387
+ direct-children-only).
388
+ - `_on_started(ns, graph_name, trigger_call_id)` — first sighting
389
+ action (push payload / build handle / etc.). Called once per
390
+ discovered namespace.
391
+ - `_on_terminal(ns, status, error)` — terminal action (push
392
+ terminal payload / mark handle status). Called once per
393
+ tracked namespace at result time, or via `finalize` / `fail`
394
+ sweeps if no parent result arrived.
395
+
396
+ Tasks events are suppressed from the main event log (`process`
397
+ returns False) — they're folded into whichever projection the
398
+ subclass populates; consumers iterating the raw protocol stream
399
+ see the higher-level view.
400
+ """
401
+
402
+ required_stream_modes = ("tasks",)
403
+
404
+ def __init__(self, scope: tuple[str, ...] = ()) -> None:
405
+ super().__init__(scope)
406
+ self._seen: set[tuple[str, ...]] = set()
407
+ # Maps tracked namespace -> task_id of the parent task whose
408
+ # `TaskResultPayload` will close it.
409
+ self._open: dict[tuple[str, ...], str] = {}
410
+ # lc_agent_name observed at each namespace (first task event wins).
411
+ # Not read by the base discriminator (which only checks whether the
412
+ # current task carries an lc_agent_name); maintained as extension state
413
+ # for subclasses that project named subagents — e.g. a `run.subagents`
414
+ # transformer reads this to filter to nested runs that have a name.
415
+ self._lc_by_ns: dict[tuple[str, ...], str | None] = {}
416
+ # Pregel task_id -> triggering LLM tool_call_id, harvested from a task
417
+ # whose `input` is a `tool_call_with_context` dict (current shape) or a
418
+ # list of tool-call dicts (legacy shape). The child subgraph's segment
419
+ # `node:<task_id>` shares this task_id, so a subagent recovers the tool
420
+ # call that spawned it (cross-payload).
421
+ self._pending_tool_calls: dict[str, str] = {}
422
+ # `cause` for the current `_on_started` dispatch, set immediately before
423
+ # the call and read by overrides that surface it. Keeping `cause` off the
424
+ # `_on_started` signature means overrides predating it (e.g. deepagents'
425
+ # `SubagentTransformer`) don't break.
426
+ self._pending_cause: LifecycleCause | None = None
427
+
428
+ # --- Template-method hooks (subclass overrides) ---
429
+
430
+ def _should_track(self, ns: tuple[str, ...]) -> bool:
431
+ """Scope filter — return True iff `ns` is in this transformer's region."""
432
+ raise NotImplementedError
433
+
434
+ def _on_started(
435
+ self,
436
+ ns: tuple[str, ...],
437
+ graph_name: str | None,
438
+ trigger_call_id: str | None,
439
+ ) -> None:
440
+ """Fired once per discovered namespace (first observed task event).
441
+
442
+ The triggering `cause` (if any) is available as `self._pending_cause`.
443
+ """
444
+ raise NotImplementedError
445
+
446
+ def _on_terminal(
447
+ self,
448
+ ns: tuple[str, ...],
449
+ status: SubgraphStatus,
450
+ error: str | None,
451
+ ) -> None:
452
+ """Fired once per tracked namespace when its parent's result arrives,
453
+ or via finalize/fail safety-net sweeps.
454
+ """
455
+ raise NotImplementedError
456
+
457
+ # --- Dispatch + bookkeeping (shared) ---
458
+
459
+ def process(self, event: ProtocolEvent) -> bool:
460
+ if event["method"] != "tasks":
461
+ return True
462
+ ns = tuple(event["params"]["namespace"])
463
+ data = event["params"]["data"]
464
+ if "result" in data:
465
+ self._handle_task_result(ns, data)
466
+ else:
467
+ self._record_identity(ns, data)
468
+ self._record_pending_tool_calls(data)
469
+ self._handle_task_start(ns, data)
470
+ # Tasks events are folded into the synthesized projections;
471
+ # suppress from the main event log so iterators don't double-see
472
+ # the same information in two shapes.
473
+ return False
474
+
475
+ def _record_identity(self, ns: tuple[str, ...], data: dict[str, Any]) -> None:
476
+ """Record this namespace's `lc_agent_name` (first task event wins).
477
+
478
+ Runs for every task-start event, including `ns == self.scope` and
479
+ tracked children. Pregel emits parent-namespace tasks before
480
+ child-namespace tasks, so under that ordering the parent's identity is
481
+ recorded by the time a child event is evaluated in `_handle_task_start`.
482
+ """
483
+ if ns in self._lc_by_ns:
484
+ return
485
+ metadata = data.get("metadata") or {}
486
+ self._lc_by_ns[ns] = metadata.get("lc_agent_name")
487
+
488
+ def _record_pending_tool_calls(self, data: dict[str, Any]) -> None:
489
+ """Harvest a task's triggering tool_call_id keyed by its task id.
490
+
491
+ A tool-dispatch task seeds `task_id -> tool_call_id`; the spawned
492
+ subgraph's namespace segment `node:<task_id>` shares that id, letting
493
+ a subagent recover the tool call that caused it across payloads. Two
494
+ input shapes are handled: the current Pregel push model schedules each
495
+ tool call as its own task whose `input` is a `tool_call_with_context`
496
+ dict, while a legacy / batched model passes a list of tool-call dicts.
497
+ """
498
+ task_id = data.get("id")
499
+ if not isinstance(task_id, str):
500
+ return
501
+ payload = data.get("input")
502
+ tool_call_id: str | None = None
503
+ # Current coco_graph schedules each tool call as its own push task
504
+ # whose input is a `tool_call_with_context` dict.
505
+ if isinstance(payload, dict) and isinstance(payload.get("tool_call"), dict):
506
+ candidate = payload["tool_call"].get("id")
507
+ if isinstance(candidate, str):
508
+ tool_call_id = candidate
509
+ # Legacy / batched shape: input is a list of tool-call dicts.
510
+ elif isinstance(payload, list):
511
+ for tc in payload:
512
+ if isinstance(tc, dict) and isinstance(tc.get("id"), str):
513
+ tool_call_id = tc["id"] # first wins
514
+ break
515
+ if tool_call_id is not None:
516
+ self._pending_tool_calls[task_id] = tool_call_id
517
+
518
+ def _handle_task_start(self, ns: tuple[str, ...], data: dict[str, Any]) -> None:
519
+ if not self._should_track(ns) or ns in self._seen:
520
+ return
521
+ self._seen.add(ns)
522
+ parsed_name, trigger_call_id = _parse_ns_segment(ns[-1])
523
+ metadata = data.get("metadata") or {}
524
+ child_lc = metadata.get("lc_agent_name")
525
+ # A subagent boundary is any nested run carrying an lc_agent_name (set
526
+ # by create_agent). Unnamed runs (lc_agent_name None) are excluded.
527
+ #
528
+ # A same-named nested agent — e.g. a subagent that invokes itself — is
529
+ # surfaced because it re-asserts its own lc_agent_name. The trade-off:
530
+ # a non-agent subgraph invoked inside a tool inherits the parent's
531
+ # lc_agent_name and will also surface (named after the parent). A caller
532
+ # that needs to exclude such a graph can null lc_agent_name in the
533
+ # config it invokes that graph with.
534
+ is_subagent = child_lc is not None
535
+ graph_name = child_lc if is_subagent else (parsed_name or None)
536
+ cause: LifecycleCause | None = None
537
+ if is_subagent and trigger_call_id is not None:
538
+ tool_call_id = self._pending_tool_calls.get(trigger_call_id)
539
+ if tool_call_id:
540
+ cause = {"type": "toolCall", "tool_call_id": str(tool_call_id)}
541
+ # Deliver `cause` via instance state, not the call signature, so
542
+ # `_on_started` stays backward-compatible with overrides predating it.
543
+ self._pending_cause = cause
544
+ self._on_started(ns, graph_name, trigger_call_id)
545
+ if trigger_call_id is not None:
546
+ self._open[ns] = trigger_call_id
547
+
548
+ def _pop_terminal_transitions(
549
+ self, ns: tuple[str, ...], data: dict[str, Any]
550
+ ) -> list[tuple[tuple[str, ...], SubgraphStatus, str | None]]:
551
+ """Return and remove tracked children closed by this task result."""
552
+ result_id = data.get("id")
553
+ if not result_id:
554
+ return []
555
+ transitions: list[tuple[tuple[str, ...], SubgraphStatus, str | None]] = []
556
+ for child_ns, parent_task_id in list(self._open.items()):
557
+ if child_ns[:-1] != ns or parent_task_id != result_id:
558
+ continue
559
+ status, error = _terminal_from_result(data)
560
+ transitions.append((child_ns, status, error))
561
+ del self._open[child_ns]
562
+ return transitions
563
+
564
+ def _handle_task_result(self, ns: tuple[str, ...], data: dict[str, Any]) -> None:
565
+ for child_ns, status, error in self._pop_terminal_transitions(ns, data):
566
+ self._on_terminal(child_ns, status, error)
567
+
568
+ def finalize(self) -> None:
569
+ """Emit `completed` for any tracked namespace still open at run end."""
570
+ for ns in list(self._open):
571
+ self._on_terminal(ns, "completed", None)
572
+ self._open.clear()
573
+
574
+ def fail(self, err: BaseException) -> None:
575
+ """Emit terminal status for any tracked namespace still open."""
576
+ status, error_str = _status_from_exception(err)
577
+ for ns in list(self._open):
578
+ self._on_terminal(ns, status, error_str)
579
+ self._open.clear()
580
+
581
+
582
+ def _status_from_exception(err: BaseException) -> tuple[SubgraphStatus, str | None]:
583
+ """Map a run exception to a subgraph terminal status and error string."""
584
+ if isinstance(err, GraphDrained):
585
+ return "drained", None
586
+ if isinstance(err, GraphInterrupt):
587
+ return "interrupted", None
588
+ return "failed", str(err)
589
+
590
+
591
+ def _terminal_from_result(
592
+ payload: dict[str, Any],
593
+ ) -> tuple[SubgraphStatus, str | None]:
594
+ """Map a `TaskResultPayload` to a `(status, error)` pair.
595
+
596
+ Order matters: a result with both `error` and `interrupts` prefers
597
+ the interrupt classification, since `GraphInterrupt` manifests as
598
+ a populated `interrupts` list, not as `error`.
599
+ """
600
+ if payload.get("interrupts"):
601
+ return "interrupted", None
602
+ error = payload.get("error")
603
+ if error:
604
+ return "failed", str(error)
605
+ return "completed", None
606
+
607
+
608
+ class LifecycleTransformer(_TasksLifecycleBase):
609
+ """Surface subgraph lifecycle as `lifecycle` protocol events.
610
+
611
+ Pushes `LifecyclePayload` to a `StreamChannel` named `lifecycle`.
612
+ The channel is auto-forwarded by the mux so payloads land in the
613
+ main event log under `method = "lifecycle"` (native transformer —
614
+ no `custom:` prefix) — visible to remote SDK clients over the
615
+ wire and to in-process consumers via `run.lifecycle`.
616
+
617
+ Tracks subgraphs at every depth strictly below the transformer's
618
+ scope, so a graph → subgraph → subgraph chain produces lifecycle
619
+ events for both nested levels in a flat stream.
620
+
621
+ Native transformer — projection key `lifecycle` is exposed as
622
+ `run.lifecycle`.
623
+ """
624
+
625
+ _native = True
626
+
627
+ def __init__(self, scope: tuple[str, ...] = ()) -> None:
628
+ super().__init__(scope)
629
+ self._channel: StreamChannel[LifecyclePayload] = StreamChannel("lifecycle")
630
+
631
+ def init(self) -> dict[str, Any]:
632
+ return {"lifecycle": self._channel}
633
+
634
+ def _should_track(self, ns: tuple[str, ...]) -> bool:
635
+ depth = len(self.scope)
636
+ return len(ns) > depth and ns[:depth] == self.scope
637
+
638
+ def _on_started(
639
+ self,
640
+ ns: tuple[str, ...],
641
+ graph_name: str | None,
642
+ trigger_call_id: str | None,
643
+ ) -> None:
644
+ if trigger_call_id is None:
645
+ # Without a task id we can't correlate a parent-result
646
+ # event back to this namespace — skip the started payload
647
+ # and rely on finalize/fail to close.
648
+ return
649
+ payload: LifecyclePayload = {"event": "started", "namespace": list(ns)}
650
+ if graph_name:
651
+ payload["graph_name"] = graph_name
652
+ payload["trigger_call_id"] = trigger_call_id
653
+ cause = self._pending_cause
654
+ if cause is not None:
655
+ payload["cause"] = cause
656
+ self._channel.push(payload)
657
+
658
+ def _on_terminal(
659
+ self,
660
+ ns: tuple[str, ...],
661
+ status: SubgraphStatus,
662
+ error: str | None,
663
+ ) -> None:
664
+ payload: LifecyclePayload = {"event": status, "namespace": list(ns)}
665
+ if error is not None:
666
+ payload["error"] = error
667
+ self._channel.push(payload)
668
+
669
+
670
+ class SubgraphTransformer(_TasksLifecycleBase):
671
+ """Discover subgraph invocations as in-process navigation handles.
672
+
673
+ Per discovered direct-child subgraph, builds a `SubgraphRunStream`
674
+ (or `AsyncSubgraphRunStream`) wrapping a child mini-mux scoped to
675
+ the subgraph's namespace. Consumers iterate `run.subgraphs` to
676
+ receive handles, then drill into `handle.values` / `handle.messages`
677
+ / `handle.subgraphs` (recursive grandchildren) / `handle.lifecycle`.
678
+
679
+ Each mini-mux owns its own scope and uses its own
680
+ `SubgraphTransformer` to discover its direct children, so
681
+ grandchildren live on the child handle — never on the root's
682
+ `subgraphs` log. Forwarding events into the matching child mini-mux
683
+ is what keeps the child's projections populated.
684
+
685
+ Native transformer — `subgraphs` is exposed as `run.subgraphs`.
686
+ """
687
+
688
+ _native = True
689
+ supports_sync = True
690
+
691
+ def __init__(self, scope: tuple[str, ...] = ()) -> None:
692
+ super().__init__(scope)
693
+ self._log: StreamChannel[SubgraphRunStream | AsyncSubgraphRunStream] = (
694
+ StreamChannel()
695
+ )
696
+ self._handles: dict[
697
+ tuple[str, ...], SubgraphRunStream | AsyncSubgraphRunStream
698
+ ] = {}
699
+ self._mux: StreamMux | None = None
700
+
701
+ def init(self) -> dict[str, Any]:
702
+ return {"subgraphs": self._log}
703
+
704
+ def _on_register(self, mux: Any) -> None:
705
+ self._mux = mux
706
+
707
+ def _should_track(self, ns: tuple[str, ...]) -> bool:
708
+ # Direct children only — grandchildren are picked up by the
709
+ # child mini-mux's own SubgraphTransformer.
710
+ depth = len(self.scope)
711
+ return len(ns) == depth + 1 and ns[:depth] == self.scope
712
+
713
+ def _on_started(
714
+ self,
715
+ ns: tuple[str, ...],
716
+ graph_name: str | None,
717
+ trigger_call_id: str | None,
718
+ ) -> None:
719
+ if self._mux is None:
720
+ return
721
+ try:
722
+ child_mux = self._mux._make_child(ns)
723
+ except RuntimeError:
724
+ return
725
+ handle_cls = AsyncSubgraphRunStream if child_mux.is_async else SubgraphRunStream
726
+ # The triggering `cause` (on `self._pending_cause`) is a wire/lifecycle
727
+ # concern carried on `LifecyclePayload`; the in-process subgraph
728
+ # navigation handle does not expose it.
729
+ handle = handle_cls(
730
+ mux=child_mux,
731
+ path=ns,
732
+ graph_name=graph_name,
733
+ trigger_call_id=trigger_call_id,
734
+ )
735
+ self._handles[ns] = handle
736
+ self._log.push(handle)
737
+
738
+ def _on_terminal(
739
+ self,
740
+ ns: tuple[str, ...],
741
+ status: SubgraphStatus,
742
+ error: str | None,
743
+ ) -> None:
744
+ handle = self._handles.get(ns)
745
+ if handle is None or not self._mark_terminal(handle, status, error):
746
+ return
747
+ self._close_or_fail_handle(handle, status, error)
748
+
749
+ async def _aon_terminal(
750
+ self,
751
+ ns: tuple[str, ...],
752
+ status: SubgraphStatus,
753
+ error: str | None,
754
+ ) -> None:
755
+ handle = self._handles.get(ns)
756
+ if handle is None or not self._mark_terminal(handle, status, error):
757
+ return
758
+ await self._aclose_or_fail_handle(handle, status, error)
759
+
760
+ def _mark_terminal(
761
+ self,
762
+ handle: SubgraphRunStream | AsyncSubgraphRunStream,
763
+ status: SubgraphStatus,
764
+ error: str | None,
765
+ ) -> bool:
766
+ """Mark a handle terminal once. Returns True on first transition."""
767
+ if handle._seen_terminal:
768
+ return False
769
+ handle.status = status
770
+ if error is not None and handle.error is None:
771
+ handle.error = error
772
+ handle._seen_terminal = True
773
+ return True
774
+
775
+ def _close_or_fail_handle(
776
+ self,
777
+ handle: SubgraphRunStream | AsyncSubgraphRunStream,
778
+ status: SubgraphStatus,
779
+ error: str | None,
780
+ ) -> None:
781
+ if handle._mux is None or handle._mux._events._closed:
782
+ return
783
+ if status == "failed":
784
+ handle._mux.fail(RuntimeError(error or "Subgraph failed"))
785
+ else:
786
+ handle._mux.close()
787
+
788
+ async def _aclose_or_fail_handle(
789
+ self,
790
+ handle: SubgraphRunStream | AsyncSubgraphRunStream,
791
+ status: SubgraphStatus,
792
+ error: str | None,
793
+ ) -> None:
794
+ if handle._mux is None or handle._mux._events._closed:
795
+ return
796
+ if status == "failed":
797
+ await handle._mux.afail(RuntimeError(error or "Subgraph failed"))
798
+ else:
799
+ await handle._mux.aclose()
800
+
801
+ def _handle_for_event(
802
+ self, event: ProtocolEvent
803
+ ) -> SubgraphRunStream | AsyncSubgraphRunStream | None:
804
+ ns = tuple(event["params"]["namespace"])
805
+ depth = len(self.scope)
806
+ if len(ns) < depth + 1:
807
+ return None
808
+ handle = self._handles.get(ns[: depth + 1])
809
+ if handle is None or handle._mux is None or handle._mux._events._closed:
810
+ return None
811
+ return handle
812
+
813
+ def process(self, event: ProtocolEvent) -> bool:
814
+ # Run tasks bookkeeping first so a `started` handle exists
815
+ # by the time we forward the event to the child mini-mux.
816
+ keep = super().process(event)
817
+ handle = self._handle_for_event(event)
818
+ if handle is not None:
819
+ handle._observe_event(event)
820
+ handle._mux.push(event)
821
+ return keep
822
+
823
+ async def aprocess(self, event: ProtocolEvent) -> bool:
824
+ # Async counterpart: repeats the tasks bookkeeping here so
825
+ # child mini-muxes receive events through their async lane.
826
+ if event["method"] == "tasks":
827
+ ns = tuple(event["params"]["namespace"])
828
+ data = event["params"]["data"]
829
+ if "result" in data:
830
+ for child_ns, status, error in self._pop_terminal_transitions(ns, data):
831
+ await self._aon_terminal(child_ns, status, error)
832
+ else:
833
+ # Mirror the sync `process` bookkeeping so the async lane
834
+ # observes parent identity / tool calls before discriminating
835
+ # a subagent boundary.
836
+ self._record_identity(ns, data)
837
+ self._record_pending_tool_calls(data)
838
+ self._handle_task_start(ns, data)
839
+ keep = False
840
+ else:
841
+ keep = True
842
+ handle = self._handle_for_event(event)
843
+ if handle is not None:
844
+ handle._observe_event(event)
845
+ await handle._mux.apush(event)
846
+ return keep
847
+
848
+ def _complete_open_handles(self) -> BaseException | None:
849
+ first_error: BaseException | None = None
850
+ for ns in list(self._open):
851
+ try:
852
+ self._on_terminal(ns, "completed", None)
853
+ except BaseException as e:
854
+ if first_error is None:
855
+ first_error = e
856
+ self._open.clear()
857
+ for handle in self._handles.values():
858
+ if self._mark_terminal(handle, "completed", None):
859
+ try:
860
+ self._close_or_fail_handle(handle, "completed", None)
861
+ except BaseException as e:
862
+ if first_error is None:
863
+ first_error = e
864
+ return first_error
865
+
866
+ async def _acomplete_open_handles(self) -> BaseException | None:
867
+ first_error: BaseException | None = None
868
+ for ns in list(self._open):
869
+ try:
870
+ await self._aon_terminal(ns, "completed", None)
871
+ except BaseException as e:
872
+ if first_error is None:
873
+ first_error = e
874
+ self._open.clear()
875
+ for handle in self._handles.values():
876
+ if self._mark_terminal(handle, "completed", None):
877
+ try:
878
+ await self._aclose_or_fail_handle(handle, "completed", None)
879
+ except BaseException as e:
880
+ if first_error is None:
881
+ first_error = e
882
+ return first_error
883
+
884
+ def finalize(self) -> None:
885
+ first_error = self._complete_open_handles()
886
+ if first_error is not None:
887
+ raise first_error
888
+
889
+ async def afinalize(self) -> None:
890
+ first_error = await self._acomplete_open_handles()
891
+ if first_error is not None:
892
+ raise first_error
893
+
894
+ def fail(self, err: BaseException) -> None:
895
+ status, error_str = _status_from_exception(err)
896
+ self._open.clear()
897
+ for handle in self._handles.values():
898
+ self._mark_terminal(handle, status, error_str)
899
+ if handle._mux is not None and not handle._mux._events._closed:
900
+ try:
901
+ handle._mux.fail(err)
902
+ except Exception:
903
+ _logger.warning(
904
+ "Error failing subgraph mini-mux at %s; "
905
+ "subscribers may not see the terminal error.",
906
+ handle.path,
907
+ exc_info=True,
908
+ )
909
+
910
+ async def afail(self, err: BaseException) -> None:
911
+ status, error_str = _status_from_exception(err)
912
+ self._open.clear()
913
+ for handle in self._handles.values():
914
+ self._mark_terminal(handle, status, error_str)
915
+ if handle._mux is not None and not handle._mux._events._closed:
916
+ try:
917
+ await handle._mux.afail(err)
918
+ except Exception:
919
+ _logger.warning(
920
+ "Error failing subgraph mini-mux at %s; "
921
+ "subscribers may not see the terminal error.",
922
+ handle.path,
923
+ exc_info=True,
924
+ )
925
+
926
+
927
+ class CheckpointsTransformer(StreamTransformer):
928
+ """Capture checkpoint events as a drainable stream.
929
+
930
+ Surfaces `stream_mode="checkpoints"` data on `run.checkpoints` as
931
+ a `StreamChannel[dict[str, Any]]`. Each item is in the same format
932
+ as returned by `get_state()`.
933
+
934
+ Checkpoint events are only emitted when a checkpointer is configured
935
+ on the graph. When no checkpointer is present, the projection exists
936
+ but receives no events.
937
+
938
+ Only events at the run's own scope are captured; checkpoint data from
939
+ deeper subgraphs is available on the respective subgraph handle's
940
+ `.checkpoints` projection.
941
+
942
+ Native transformer — `run.checkpoints` is a direct attribute.
943
+ """
944
+
945
+ _native = True
946
+ required_stream_modes = ("checkpoints",)
947
+
948
+ def __init__(self, scope: tuple[str, ...] = ()) -> None:
949
+ super().__init__(scope)
950
+ self._log: StreamChannel[dict[str, Any]] = StreamChannel()
951
+ self._scope_list: list[str] = list(scope)
952
+
953
+ def init(self) -> dict[str, Any]:
954
+ return {"checkpoints": self._log}
955
+
956
+ def process(self, event: ProtocolEvent) -> bool:
957
+ if event["method"] != "checkpoints":
958
+ return True
959
+ params = event["params"]
960
+ if params["namespace"] != self._scope_list:
961
+ return True
962
+ self._log.push(params["data"])
963
+ return True
964
+
965
+
966
+ class DebugTransformer(StreamTransformer):
967
+ """Capture debug events as a drainable stream.
968
+
969
+ Surfaces `stream_mode="debug"` data on `run.debug` as a
970
+ `StreamChannel[dict[str, Any]]`. Each item is a debug event with
971
+ step-level detail (checkpoint snapshots, task payloads, and
972
+ task results wrapped with step number and timestamp).
973
+
974
+ Only events at the run's own scope are captured; debug data from
975
+ deeper subgraphs is available on the respective subgraph handle's
976
+ `.debug` projection.
977
+
978
+ Native transformer — `run.debug` is a direct attribute.
979
+ """
980
+
981
+ _native = True
982
+ required_stream_modes = ("debug",)
983
+
984
+ def __init__(self, scope: tuple[str, ...] = ()) -> None:
985
+ super().__init__(scope)
986
+ self._log: StreamChannel[dict[str, Any]] = StreamChannel()
987
+ self._scope_list: list[str] = list(scope)
988
+
989
+ def init(self) -> dict[str, Any]:
990
+ return {"debug": self._log}
991
+
992
+ def process(self, event: ProtocolEvent) -> bool:
993
+ if event["method"] != "debug":
994
+ return True
995
+ params = event["params"]
996
+ if params["namespace"] != self._scope_list:
997
+ return True
998
+ self._log.push(params["data"])
999
+ return True
1000
+
1001
+
1002
+ class TasksTransformer(StreamTransformer):
1003
+ """Capture raw task events as a drainable stream.
1004
+
1005
+ Surfaces `stream_mode="tasks"` data on `run.tasks` as a
1006
+ `StreamChannel[dict[str, Any]]`. Each item is a task payload
1007
+ (start or result).
1008
+
1009
+ `LifecycleTransformer` and `SubgraphTransformer` also consume
1010
+ `tasks` events for subgraph discovery and lifecycle tracking.
1011
+ This transformer captures the raw payloads independently for
1012
+ consumers who need task-level detail.
1013
+
1014
+ Only events at the run's own scope are captured; task data from
1015
+ deeper subgraphs is available on the respective subgraph handle's
1016
+ `.tasks` projection.
1017
+
1018
+ Native transformer — `run.tasks` is a direct attribute.
1019
+ """
1020
+
1021
+ _native = True
1022
+ required_stream_modes = ("tasks",)
1023
+
1024
+ def __init__(self, scope: tuple[str, ...] = ()) -> None:
1025
+ super().__init__(scope)
1026
+ self._log: StreamChannel[dict[str, Any]] = StreamChannel()
1027
+ self._scope_list: list[str] = list(scope)
1028
+
1029
+ def init(self) -> dict[str, Any]:
1030
+ return {"tasks": self._log}
1031
+
1032
+ def process(self, event: ProtocolEvent) -> bool:
1033
+ if event["method"] != "tasks":
1034
+ return True
1035
+ params = event["params"]
1036
+ if params["namespace"] != self._scope_list:
1037
+ return True
1038
+ self._log.push(params["data"])
1039
+ return True