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,2201 @@
1
+ """Types for middleware and agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from collections.abc import Awaitable, Callable, Sequence
7
+ from dataclasses import dataclass, field, replace
8
+ from inspect import iscoroutinefunction
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ Annotated,
12
+ Any,
13
+ Generic,
14
+ Literal,
15
+ cast,
16
+ overload,
17
+ )
18
+
19
+ # Needed as top level import for Pydantic schema generation on AgentState
20
+ from coco_core.messages import (
21
+ AIMessage,
22
+ AnyMessage,
23
+ BaseMessage,
24
+ SystemMessage,
25
+ ToolMessage,
26
+ )
27
+ from coco_graph.channels.ephemeral_value import EphemeralValue
28
+ from coco_graph.graph.message import add_messages
29
+ from coco_graph.prebuilt.tool_node import ToolCallRequest, ToolCallWrapper
30
+ from coco_graph.runtime import Runtime
31
+ from coco_graph.types import Command, TracePolicy, omit_payload
32
+ from coco_graph.typing import ContextT
33
+ from typing_extensions import NotRequired, Required, TypedDict, TypeVar, Unpack
34
+
35
+ if TYPE_CHECKING:
36
+ from coco_core.language_models.chat_models import BaseChatModel
37
+ from coco_core.tools import BaseTool
38
+ from coco_graph.stream._mux import TransformerFactory
39
+
40
+ from coco.agents.structured_output import ResponseFormat
41
+
42
+ __all__ = [
43
+ "AgentMiddleware",
44
+ "AgentState",
45
+ "ContextT",
46
+ "ExtendedModelResponse",
47
+ "InputAgentState",
48
+ "ModelCallResult",
49
+ "ModelRequest",
50
+ "ModelResponse",
51
+ "OmitFromSchema",
52
+ "OutputAgentState",
53
+ "ResponseT",
54
+ "StateT_co",
55
+ "ToolCallRequest",
56
+ "ToolCallWrapper",
57
+ "TracePolicy",
58
+ "after_agent",
59
+ "after_model",
60
+ "before_agent",
61
+ "before_model",
62
+ "dynamic_prompt",
63
+ "hook_config",
64
+ "omit_payload",
65
+ "wrap_tool_call",
66
+ ]
67
+
68
+ JumpTo = Literal["tools", "model", "end"]
69
+ """Destination to jump to when a middleware node returns."""
70
+
71
+ ResponseT = TypeVar("ResponseT", default=Any)
72
+
73
+
74
+ class _ModelRequestOverrides(TypedDict, total=False):
75
+ """Possible overrides for `ModelRequest.override()` method."""
76
+
77
+ model: BaseChatModel
78
+ system_message: SystemMessage | None
79
+ messages: list[AnyMessage]
80
+ tool_choice: Any | None
81
+ tools: list[BaseTool | dict[str, Any]]
82
+ response_format: ResponseFormat[Any] | None
83
+ model_settings: dict[str, Any]
84
+ state: AgentState[Any]
85
+
86
+
87
+ @dataclass(init=False)
88
+ class ModelRequest(Generic[ContextT]):
89
+ """Model request information for the agent.
90
+
91
+ Type Parameters:
92
+ ContextT: The type of the runtime context.
93
+
94
+ Defaults to `None` if not specified.
95
+ """
96
+
97
+ model: BaseChatModel
98
+ messages: list[AnyMessage] # excluding system message
99
+ system_message: SystemMessage | None
100
+ tool_choice: Any | None
101
+ tools: list[BaseTool | dict[str, Any]]
102
+ response_format: ResponseFormat[Any] | None
103
+ state: AgentState[Any]
104
+ runtime: Runtime[ContextT]
105
+ model_settings: dict[str, Any] = field(default_factory=dict)
106
+
107
+ def __init__(
108
+ self,
109
+ *,
110
+ model: BaseChatModel,
111
+ messages: list[AnyMessage],
112
+ system_message: SystemMessage | None = None,
113
+ system_prompt: str | None = None,
114
+ tool_choice: Any | None = None,
115
+ tools: list[BaseTool | dict[str, Any]] | None = None,
116
+ response_format: ResponseFormat[Any] | None = None,
117
+ state: AgentState[Any] | None = None,
118
+ runtime: Runtime[ContextT] | None = None,
119
+ model_settings: dict[str, Any] | None = None,
120
+ ) -> None:
121
+ """Initialize `ModelRequest` with backward compatibility for `system_prompt`.
122
+
123
+ Args:
124
+ model: The chat model to use.
125
+ messages: List of messages (excluding system prompt).
126
+ tool_choice: Tool choice configuration.
127
+ tools: List of available tools.
128
+ response_format: Response format specification.
129
+ state: Agent state.
130
+ runtime: Runtime context.
131
+ model_settings: Additional model settings.
132
+ system_message: System message instance (preferred).
133
+ system_prompt: System prompt string (deprecated, converted to `SystemMessage`).
134
+
135
+ Raises:
136
+ ValueError: If both `system_prompt` and `system_message` are provided.
137
+ """
138
+ # Handle system_prompt/system_message conversion and validation
139
+ if system_prompt is not None and system_message is not None:
140
+ msg = "Cannot specify both system_prompt and system_message"
141
+ raise ValueError(msg)
142
+
143
+ if system_prompt is not None:
144
+ system_message = SystemMessage(content=system_prompt)
145
+
146
+ with warnings.catch_warnings():
147
+ warnings.simplefilter("ignore", category=DeprecationWarning)
148
+ self.model = model
149
+ self.messages = messages
150
+ self.system_message = system_message
151
+ self.tool_choice = tool_choice
152
+ self.tools = tools if tools is not None else []
153
+ self.response_format = response_format
154
+ self.state = state if state is not None else {"messages": []}
155
+ self.runtime = runtime # type: ignore[assignment]
156
+ self.model_settings = model_settings if model_settings is not None else {}
157
+
158
+ @property
159
+ def system_prompt(self) -> str | None:
160
+ """Get system prompt text from system_message.
161
+
162
+ Returns:
163
+ The content of the system message if present, otherwise `None`.
164
+ """
165
+ if self.system_message is None:
166
+ return None
167
+ return self.system_message.text
168
+
169
+ def __setattr__(self, name: str, value: Any) -> None:
170
+ """Set an attribute with a deprecation warning.
171
+
172
+ Direct attribute assignment on `ModelRequest` is deprecated. Use the
173
+ `override()` method instead to create a new request with modified attributes.
174
+
175
+ Args:
176
+ name: Attribute name.
177
+ value: Attribute value.
178
+ """
179
+ # Special handling for system_prompt - convert to system_message
180
+ if name == "system_prompt":
181
+ warnings.warn(
182
+ "Direct attribute assignment to ModelRequest.system_prompt is deprecated. "
183
+ "Use request.override(system_message=SystemMessage(...)) instead to create "
184
+ "a new request with the modified system message.",
185
+ DeprecationWarning,
186
+ stacklevel=2,
187
+ )
188
+ if value is None:
189
+ object.__setattr__(self, "system_message", None)
190
+ else:
191
+ object.__setattr__(self, "system_message", SystemMessage(content=value))
192
+ return
193
+
194
+ warnings.warn(
195
+ f"Direct attribute assignment to ModelRequest.{name} is deprecated. "
196
+ f"Use request.override({name}=...) instead to create a new request "
197
+ f"with the modified attribute.",
198
+ DeprecationWarning,
199
+ stacklevel=2,
200
+ )
201
+ object.__setattr__(self, name, value)
202
+
203
+ def override(self, **overrides: Unpack[_ModelRequestOverrides]) -> ModelRequest[ContextT]:
204
+ """Replace the request with a new request with the given overrides.
205
+
206
+ Returns a new `ModelRequest` instance with the specified attributes replaced.
207
+
208
+ This follows an immutable pattern, leaving the original request unchanged.
209
+
210
+ Args:
211
+ **overrides: Keyword arguments for attributes to override.
212
+
213
+ Supported keys:
214
+
215
+ - `model`: `BaseChatModel` instance
216
+ - `system_prompt`: deprecated, use `system_message` instead
217
+ - `system_message`: `SystemMessage` instance
218
+ - `messages`: `list` of messages
219
+ - `tool_choice`: Tool choice configuration
220
+ - `tools`: `list` of available tools
221
+ - `response_format`: Response format specification
222
+ - `model_settings`: Additional model settings
223
+ - `state`: Agent state dictionary
224
+
225
+ Returns:
226
+ New `ModelRequest` instance with specified overrides applied.
227
+
228
+ Examples:
229
+ !!! example "Create a new request with different model"
230
+
231
+ ```python
232
+ new_request = request.override(model=different_model)
233
+ ```
234
+
235
+ !!! example "Override system message (preferred)"
236
+
237
+ ```python
238
+ from coco_core.messages import SystemMessage
239
+
240
+ new_request = request.override(
241
+ system_message=SystemMessage(content="New instructions")
242
+ )
243
+ ```
244
+
245
+ !!! example "Override multiple attributes"
246
+
247
+ ```python
248
+ new_request = request.override(
249
+ model=ChatOpenAI(model="gpt-5.5"),
250
+ system_message=SystemMessage(content="New instructions"),
251
+ )
252
+ ```
253
+
254
+ Raises:
255
+ ValueError: If both `system_prompt` and `system_message` are provided.
256
+ """
257
+ # Handle system_prompt/system_message conversion
258
+ if "system_prompt" in overrides and "system_message" in overrides:
259
+ msg = "Cannot specify both system_prompt and system_message"
260
+ raise ValueError(msg)
261
+
262
+ if "system_prompt" in overrides:
263
+ system_prompt = cast("str | None", overrides.pop("system_prompt")) # type: ignore[typeddict-item]
264
+ if system_prompt is None:
265
+ overrides["system_message"] = None
266
+ else:
267
+ overrides["system_message"] = SystemMessage(content=system_prompt)
268
+
269
+ return replace(self, **overrides)
270
+
271
+
272
+ @dataclass
273
+ class ModelResponse(Generic[ResponseT]):
274
+ """Response from model execution including messages and optional structured output.
275
+
276
+ The result will usually contain a single `AIMessage`, but may include an additional
277
+ `ToolMessage` if the model used a tool for structured output.
278
+
279
+ Type Parameters:
280
+ ResponseT: The type of the structured response. Defaults to `Any` if not specified.
281
+ """
282
+
283
+ result: list[BaseMessage]
284
+ """List of messages from model execution."""
285
+
286
+ structured_response: ResponseT | None = None
287
+ """Parsed structured output if `response_format` was specified, `None` otherwise."""
288
+
289
+
290
+ @dataclass
291
+ class ExtendedModelResponse(Generic[ResponseT]):
292
+ """Model response with an optional 'Command' from 'wrap_model_call' middleware.
293
+
294
+ Use this to return a 'Command' alongside the model response from a
295
+ 'wrap_model_call' handler. The command is applied as an additional state
296
+ update after the model node completes, using the graph's reducers (e.g.
297
+ 'add_messages' for the 'messages' key).
298
+
299
+ Because each 'Command' is applied through the reducer, messages in the
300
+ command are **added alongside** the model response messages rather than
301
+ replacing them. For non-reducer state fields, later commands overwrite
302
+ earlier ones (outermost middleware wins over inner).
303
+
304
+ Type Parameters:
305
+ ResponseT: The type of the structured response. Defaults to 'Any' if not specified.
306
+ """
307
+
308
+ model_response: ModelResponse[ResponseT]
309
+ """The underlying model response."""
310
+
311
+ command: Command[Any] | None = None
312
+ """Optional command to apply as an additional state update."""
313
+
314
+
315
+ ModelCallResult = ModelResponse[ResponseT] | AIMessage | ExtendedModelResponse[ResponseT]
316
+ """Return type for model call handlers.
317
+
318
+ Middleware can return either:
319
+
320
+ - `ModelResponse`: Full response with messages and optional structured output
321
+ - `AIMessage`: Simplified return for simple use cases
322
+ - `ExtendedModelResponse`: Response with an optional `Command` for additional state updates
323
+ `goto`, `resume`, and `graph` are not yet supported on these commands.
324
+ A `NotImplementedError` will be raised if you try to use them.
325
+ """
326
+
327
+
328
+ @dataclass
329
+ class OmitFromSchema:
330
+ """Annotation used to mark state attributes as omitted from input or output schemas."""
331
+
332
+ input: bool = True
333
+ """Whether to omit the attribute from the input schema."""
334
+
335
+ output: bool = True
336
+ """Whether to omit the attribute from the output schema."""
337
+
338
+
339
+ OmitFromInput = OmitFromSchema(input=True, output=False)
340
+ """Annotation used to mark state attributes as omitted from input schema."""
341
+
342
+ OmitFromOutput = OmitFromSchema(input=False, output=True)
343
+ """Annotation used to mark state attributes as omitted from output schema."""
344
+
345
+ PrivateStateAttr = OmitFromSchema(input=True, output=True)
346
+ """Annotation used to mark state attributes as purely internal for a given middleware."""
347
+
348
+
349
+ class AgentState(TypedDict, Generic[ResponseT]):
350
+ """State schema for the agent."""
351
+
352
+ messages: Required[Annotated[list[AnyMessage], add_messages]]
353
+ jump_to: NotRequired[Annotated[JumpTo | None, EphemeralValue, PrivateStateAttr]]
354
+ structured_response: NotRequired[Annotated[ResponseT, OmitFromInput]]
355
+
356
+
357
+ class InputAgentState(TypedDict):
358
+ """Input state schema for the agent."""
359
+
360
+ messages: Required[Annotated[list[AnyMessage | dict[str, Any]], add_messages]]
361
+
362
+
363
+ class OutputAgentState(TypedDict, Generic[ResponseT]):
364
+ """Output state schema for the agent."""
365
+
366
+ messages: Required[Annotated[list[AnyMessage], add_messages]]
367
+ structured_response: NotRequired[ResponseT]
368
+
369
+
370
+ # Deprecated aliases kept for backwards compatibility with external consumers that
371
+ # imported the previously private names. Remove in a future release.
372
+ _InputAgentState = InputAgentState
373
+ _OutputAgentState = OutputAgentState
374
+
375
+
376
+ StateT = TypeVar("StateT", bound=AgentState[Any], default=AgentState[Any])
377
+ StateT_co = TypeVar("StateT_co", bound=AgentState[Any], default=AgentState[Any], covariant=True)
378
+ StateT_contra = TypeVar("StateT_contra", bound=AgentState[Any], contravariant=True)
379
+
380
+
381
+ class _DefaultAgentState(AgentState[Any]):
382
+ """AgentMiddleware default state."""
383
+
384
+
385
+ class AgentMiddleware(Generic[StateT, ContextT, ResponseT]):
386
+ """Base middleware class for an agent.
387
+
388
+ Subclass this and implement any of the defined methods to customize agent behavior
389
+ between steps in the main agent loop.
390
+
391
+ Type Parameters:
392
+ StateT: The type of the agent state. Defaults to `AgentState[Any]`.
393
+ ContextT: The type of the runtime context. Defaults to `None`.
394
+ ResponseT: The type of the structured response. Defaults to `Any`.
395
+ """
396
+
397
+ state_schema: type[StateT] = cast("type[StateT]", _DefaultAgentState)
398
+ """The schema for state passed to the middleware nodes."""
399
+
400
+ tools: Sequence[BaseTool]
401
+ """Additional tools registered by the middleware."""
402
+
403
+ trace_policy: TracePolicy | None = None
404
+ """Optional trace policy for this middleware's hook spans (`wrap_model_call`/
405
+ `wrap_tool_call` and the `before_*`/`after_*` node hooks).
406
+
407
+ By default (`None`), hook spans are traced normally. Set a `TracePolicy` to shape
408
+ what they record -- e.g. `TracePolicy(process_inputs=omit_payload)` to drop the
409
+ conversation `messages`/`state` payload while keeping the span and its timing.
410
+ Messages are still captured on the inner model-call span.
411
+ """
412
+
413
+ transformers: Sequence[TransformerFactory] = ()
414
+ """Stream transformer factories registered by the middleware.
415
+
416
+ Each entry is a scope-aware factory invoked as `factory(scope)` so every
417
+ invocation receives a fresh instance. Factories are merged with the
418
+ `transformers` argument of [`create_agent`][coco.agents.create_agent]
419
+ at graph compile time, after the `ToolCallTransformer` and before any
420
+ user-supplied entries.
421
+ """
422
+
423
+ @property
424
+ def name(self) -> str:
425
+ """The name of the middleware instance.
426
+
427
+ Defaults to the class name, but can be overridden for custom naming.
428
+ """
429
+ return self.__class__.__name__
430
+
431
+ def before_agent(self, state: StateT, runtime: Runtime[ContextT]) -> dict[str, Any] | None:
432
+ """Logic to run before the agent execution starts.
433
+
434
+ Args:
435
+ state: The current agent state.
436
+ runtime: The runtime context.
437
+
438
+ Returns:
439
+ Agent state updates to apply before agent execution.
440
+ """
441
+
442
+ async def abefore_agent(
443
+ self, state: StateT, runtime: Runtime[ContextT]
444
+ ) -> dict[str, Any] | None:
445
+ """Async logic to run before the agent execution starts.
446
+
447
+ Args:
448
+ state: The current agent state.
449
+ runtime: The runtime context.
450
+
451
+ Returns:
452
+ Agent state updates to apply before agent execution.
453
+ """
454
+
455
+ def before_model(self, state: StateT, runtime: Runtime[ContextT]) -> dict[str, Any] | None:
456
+ """Logic to run before the model is called.
457
+
458
+ Args:
459
+ state: The current agent state.
460
+ runtime: The runtime context.
461
+
462
+ Returns:
463
+ Agent state updates to apply before model call.
464
+ """
465
+
466
+ async def abefore_model(
467
+ self, state: StateT, runtime: Runtime[ContextT]
468
+ ) -> dict[str, Any] | None:
469
+ """Async logic to run before the model is called.
470
+
471
+ Args:
472
+ state: The agent state.
473
+ runtime: The runtime context.
474
+
475
+ Returns:
476
+ Agent state updates to apply before model call.
477
+ """
478
+
479
+ def after_model(self, state: StateT, runtime: Runtime[ContextT]) -> dict[str, Any] | None:
480
+ """Logic to run after the model is called.
481
+
482
+ Args:
483
+ state: The current agent state.
484
+ runtime: The runtime context.
485
+
486
+ Returns:
487
+ Agent state updates to apply after model call.
488
+ """
489
+
490
+ async def aafter_model(
491
+ self, state: StateT, runtime: Runtime[ContextT]
492
+ ) -> dict[str, Any] | None:
493
+ """Async logic to run after the model is called.
494
+
495
+ Args:
496
+ state: The current agent state.
497
+ runtime: The runtime context.
498
+
499
+ Returns:
500
+ Agent state updates to apply after model call.
501
+ """
502
+
503
+ def wrap_model_call(
504
+ self,
505
+ request: ModelRequest[ContextT],
506
+ handler: Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]],
507
+ ) -> ModelResponse[ResponseT] | AIMessage | ExtendedModelResponse[ResponseT]:
508
+ """Intercept and control model execution via handler callback.
509
+
510
+ Async version is `awrap_model_call`
511
+
512
+ The handler callback executes the model request and returns a `ModelResponse`.
513
+ Middleware can call the handler multiple times for retry logic, skip calling
514
+ it to short-circuit, or modify the request/response. Multiple middleware
515
+ compose with first in list as outermost layer.
516
+
517
+ Args:
518
+ request: Model request to execute (includes state and runtime).
519
+ handler: Callback that executes the model request and returns
520
+ `ModelResponse`.
521
+
522
+ Call this to execute the model.
523
+
524
+ Can be called multiple times for retry logic.
525
+
526
+ Can skip calling it to short-circuit.
527
+
528
+ Returns:
529
+ The model call result.
530
+
531
+ Examples:
532
+ !!! example "Retry on error"
533
+
534
+ ```python
535
+ def wrap_model_call(self, request, handler):
536
+ for attempt in range(3):
537
+ try:
538
+ return handler(request)
539
+ except Exception:
540
+ if attempt == 2:
541
+ raise
542
+ ```
543
+
544
+ !!! example "Rewrite response"
545
+
546
+ ```python
547
+ def wrap_model_call(self, request, handler):
548
+ response = handler(request)
549
+ ai_msg = response.result[0]
550
+ return ModelResponse(
551
+ result=[AIMessage(content=f"[{ai_msg.content}]")],
552
+ structured_response=response.structured_response,
553
+ )
554
+ ```
555
+
556
+ !!! example "Error to fallback"
557
+
558
+ ```python
559
+ def wrap_model_call(self, request, handler):
560
+ try:
561
+ return handler(request)
562
+ except Exception:
563
+ return ModelResponse(result=[AIMessage(content="Service unavailable")])
564
+ ```
565
+
566
+ !!! example "Cache/short-circuit"
567
+
568
+ ```python
569
+ def wrap_model_call(self, request, handler):
570
+ if cached := get_cache(request):
571
+ return cached # Short-circuit with cached result
572
+ response = handler(request)
573
+ save_cache(request, response)
574
+ return response
575
+ ```
576
+
577
+ !!! example "Simple `AIMessage` return (converted automatically)"
578
+
579
+ ```python
580
+ def wrap_model_call(self, request, handler):
581
+ response = handler(request)
582
+ # Can return AIMessage directly for simple cases
583
+ return AIMessage(content="Simplified response")
584
+ ```
585
+ """
586
+ msg = (
587
+ "Synchronous implementation of wrap_model_call is not available. "
588
+ "You are likely encountering this error because you defined only the async version "
589
+ "(awrap_model_call) and invoked your agent in a synchronous context "
590
+ "(e.g., using `stream()` or `invoke()`). "
591
+ "To resolve this, either: "
592
+ "(1) subclass AgentMiddleware and implement the synchronous wrap_model_call method, "
593
+ "(2) use the @wrap_model_call decorator on a standalone sync function, or "
594
+ "(3) invoke your agent asynchronously using `astream()` or `ainvoke()`."
595
+ )
596
+ raise NotImplementedError(msg)
597
+
598
+ async def awrap_model_call(
599
+ self,
600
+ request: ModelRequest[ContextT],
601
+ handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]],
602
+ ) -> ModelResponse[ResponseT] | AIMessage | ExtendedModelResponse[ResponseT]:
603
+ """Intercept and control async model execution via handler callback.
604
+
605
+ The handler callback executes the model request and returns a `ModelResponse`.
606
+
607
+ Middleware can call the handler multiple times for retry logic, skip calling
608
+ it to short-circuit, or modify the request/response. Multiple middleware
609
+ compose with first in list as outermost layer.
610
+
611
+ Args:
612
+ request: Model request to execute (includes state and runtime).
613
+ handler: Async callback that executes the model request and returns
614
+ `ModelResponse`.
615
+
616
+ Call this to execute the model.
617
+
618
+ Can be called multiple times for retry logic.
619
+
620
+ Can skip calling it to short-circuit.
621
+
622
+ Returns:
623
+ The model call result.
624
+
625
+ Examples:
626
+ !!! example "Retry on error"
627
+
628
+ ```python
629
+ async def awrap_model_call(self, request, handler):
630
+ for attempt in range(3):
631
+ try:
632
+ return await handler(request)
633
+ except Exception:
634
+ if attempt == 2:
635
+ raise
636
+ ```
637
+ """
638
+ msg = (
639
+ "Asynchronous implementation of awrap_model_call is not available. "
640
+ "You are likely encountering this error because you defined only the sync version "
641
+ "(wrap_model_call) and invoked your agent in an asynchronous context "
642
+ "(e.g., using `astream()` or `ainvoke()`). "
643
+ "To resolve this, either: "
644
+ "(1) subclass AgentMiddleware and implement the asynchronous awrap_model_call method, "
645
+ "(2) use the @wrap_model_call decorator on a standalone async function, or "
646
+ "(3) invoke your agent synchronously using `stream()` or `invoke()`."
647
+ )
648
+ raise NotImplementedError(msg)
649
+
650
+ def after_agent(self, state: StateT, runtime: Runtime[ContextT]) -> dict[str, Any] | None:
651
+ """Logic to run after the agent execution completes.
652
+
653
+ Args:
654
+ state: The current agent state.
655
+ runtime: The runtime context.
656
+
657
+ Returns:
658
+ Agent state updates to apply after agent execution.
659
+ """
660
+
661
+ async def aafter_agent(
662
+ self, state: StateT, runtime: Runtime[ContextT]
663
+ ) -> dict[str, Any] | None:
664
+ """Async logic to run after the agent execution completes.
665
+
666
+ Args:
667
+ state: The current agent state.
668
+ runtime: The runtime context.
669
+
670
+ Returns:
671
+ Agent state updates to apply after agent execution.
672
+ """
673
+
674
+ def wrap_tool_call(
675
+ self,
676
+ request: ToolCallRequest,
677
+ handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
678
+ ) -> ToolMessage | Command[Any]:
679
+ """Intercept tool execution for retries, monitoring, or modification.
680
+
681
+ Async version is `awrap_tool_call`
682
+
683
+ Multiple middleware compose automatically (first defined = outermost).
684
+
685
+ Exceptions propagate unless `handle_tool_errors` is configured on `ToolNode`.
686
+
687
+ Args:
688
+ request: Tool call request with call `dict`, `BaseTool`, state, and runtime.
689
+
690
+ Access state via `request.state` and runtime via `request.runtime`.
691
+ handler: `Callable` to execute the tool (can be called multiple times).
692
+
693
+ Returns:
694
+ `ToolMessage` or `Command` (the final result).
695
+
696
+ The handler `Callable` can be invoked multiple times for retry logic.
697
+
698
+ Each call to handler is independent and stateless.
699
+
700
+ Examples:
701
+ !!! example "Modify request before execution"
702
+
703
+ ```python
704
+ def wrap_tool_call(self, request, handler):
705
+ modified_call = {
706
+ **request.tool_call,
707
+ "args": {
708
+ **request.tool_call["args"],
709
+ "value": request.tool_call["args"]["value"] * 2,
710
+ },
711
+ }
712
+ request = request.override(tool_call=modified_call)
713
+ return handler(request)
714
+ ```
715
+
716
+ !!! example "Retry on error (call handler multiple times)"
717
+
718
+ ```python
719
+ def wrap_tool_call(self, request, handler):
720
+ for attempt in range(3):
721
+ try:
722
+ result = handler(request)
723
+ if is_valid(result):
724
+ return result
725
+ except Exception:
726
+ if attempt == 2:
727
+ raise
728
+ return result
729
+ ```
730
+
731
+ !!! example "Conditional retry based on response"
732
+
733
+ ```python
734
+ def wrap_tool_call(self, request, handler):
735
+ for attempt in range(3):
736
+ result = handler(request)
737
+ if isinstance(result, ToolMessage) and result.status != "error":
738
+ return result
739
+ if attempt < 2:
740
+ continue
741
+ return result
742
+ ```
743
+ """
744
+ msg = (
745
+ "Synchronous implementation of wrap_tool_call is not available. "
746
+ "You are likely encountering this error because you defined only the async version "
747
+ "(awrap_tool_call) and invoked your agent in a synchronous context "
748
+ "(e.g., using `stream()` or `invoke()`). "
749
+ "To resolve this, either: "
750
+ "(1) subclass AgentMiddleware and implement the synchronous wrap_tool_call method, "
751
+ "(2) use the @wrap_tool_call decorator on a standalone sync function, or "
752
+ "(3) invoke your agent asynchronously using `astream()` or `ainvoke()`."
753
+ )
754
+ raise NotImplementedError(msg)
755
+
756
+ async def awrap_tool_call(
757
+ self,
758
+ request: ToolCallRequest,
759
+ handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
760
+ ) -> ToolMessage | Command[Any]:
761
+ """Intercept and control async tool execution via handler callback.
762
+
763
+ The handler callback executes the tool call and returns a `ToolMessage` or
764
+ `Command`. Middleware can call the handler multiple times for retry logic, skip
765
+ calling it to short-circuit, or modify the request/response. Multiple middleware
766
+ compose with first in list as outermost layer.
767
+
768
+ Args:
769
+ request: Tool call request with call `dict`, `BaseTool`, state, and runtime.
770
+
771
+ Access state via `request.state` and runtime via `request.runtime`.
772
+ handler: Async callable to execute the tool and returns `ToolMessage` or
773
+ `Command`.
774
+
775
+ Call this to execute the tool.
776
+
777
+ Can be called multiple times for retry logic.
778
+
779
+ Can skip calling it to short-circuit.
780
+
781
+ Returns:
782
+ `ToolMessage` or `Command` (the final result).
783
+
784
+ The handler `Callable` can be invoked multiple times for retry logic.
785
+
786
+ Each call to handler is independent and stateless.
787
+
788
+ Examples:
789
+ !!! example "Async retry on error"
790
+
791
+ ```python
792
+ async def awrap_tool_call(self, request, handler):
793
+ for attempt in range(3):
794
+ try:
795
+ result = await handler(request)
796
+ if is_valid(result):
797
+ return result
798
+ except Exception:
799
+ if attempt == 2:
800
+ raise
801
+ return result
802
+ ```
803
+
804
+ ```python
805
+ async def awrap_tool_call(self, request, handler):
806
+ if cached := await get_cache_async(request):
807
+ return ToolMessage(content=cached, tool_call_id=request.tool_call["id"])
808
+ result = await handler(request)
809
+ await save_cache_async(request, result)
810
+ return result
811
+ ```
812
+ """
813
+ msg = (
814
+ "Asynchronous implementation of awrap_tool_call is not available. "
815
+ "You are likely encountering this error because you defined only the sync version "
816
+ "(wrap_tool_call) and invoked your agent in an asynchronous context "
817
+ "(e.g., using `astream()` or `ainvoke()`). "
818
+ "To resolve this, either: "
819
+ "(1) subclass AgentMiddleware and implement the asynchronous awrap_tool_call method, "
820
+ "(2) use the @wrap_tool_call decorator on a standalone async function, or "
821
+ "(3) invoke your agent synchronously using `stream()` or `invoke()`."
822
+ )
823
+ raise NotImplementedError(msg)
824
+
825
+
826
+ _SyncCallableWithStateAndRuntime = Callable[
827
+ [StateT_contra, Runtime[ContextT]], dict[str, Any] | Command[Any] | None
828
+ ]
829
+ _AsyncCallableWithStateAndRuntime = Callable[
830
+ [StateT_contra, Runtime[ContextT]], Awaitable[dict[str, Any] | Command[Any] | None]
831
+ ]
832
+ _CallableWithStateAndRuntime = (
833
+ _SyncCallableWithStateAndRuntime[StateT_contra, ContextT]
834
+ | _AsyncCallableWithStateAndRuntime[StateT_contra, ContextT]
835
+ )
836
+
837
+ _SyncCallableReturningSystemMessage = Callable[[ModelRequest[ContextT]], str | SystemMessage]
838
+ _AsyncCallableReturningSystemMessage = Callable[
839
+ [ModelRequest[ContextT]], Awaitable[str | SystemMessage]
840
+ ]
841
+ _CallableReturningSystemMessage = (
842
+ _SyncCallableReturningSystemMessage[ContextT] | _AsyncCallableReturningSystemMessage[ContextT]
843
+ )
844
+
845
+ # Sync/async signatures for `wrap_model_call` interception; see `@wrap_model_call`.
846
+ _SyncCallableReturningModelResponse = Callable[
847
+ [ModelRequest[ContextT], Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]]],
848
+ ModelCallResult,
849
+ ]
850
+ _AsyncCallableReturningModelResponse = Callable[
851
+ [
852
+ ModelRequest[ContextT],
853
+ Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]],
854
+ ],
855
+ Awaitable[ModelCallResult],
856
+ ]
857
+ _CallableReturningModelResponse = (
858
+ _SyncCallableReturningModelResponse[ContextT, ResponseT]
859
+ | _AsyncCallableReturningModelResponse[ContextT, ResponseT]
860
+ )
861
+
862
+ # Sync/async signatures for `wrap_tool_call` interception; see `@wrap_tool_call`.
863
+ _SyncCallableReturningToolResponse = Callable[
864
+ [ToolCallRequest, Callable[[ToolCallRequest], ToolMessage | Command[Any]]],
865
+ ToolMessage | Command[Any],
866
+ ]
867
+ _AsyncCallableReturningToolResponse = Callable[
868
+ [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],
869
+ Awaitable[ToolMessage | Command[Any]],
870
+ ]
871
+ _CallableReturningToolResponse = (
872
+ _SyncCallableReturningToolResponse | _AsyncCallableReturningToolResponse
873
+ )
874
+
875
+
876
+ CallableT = TypeVar("CallableT", bound=Callable[..., Any])
877
+
878
+
879
+ def hook_config(
880
+ *,
881
+ can_jump_to: list[JumpTo] | None = None,
882
+ ) -> Callable[[CallableT], CallableT]:
883
+ """Decorator to configure hook behavior in middleware methods.
884
+
885
+ Use this decorator on `before_model` or `after_model` methods in middleware classes
886
+ to configure their behavior. Currently supports specifying which destinations they
887
+ can jump to, which establishes conditional edges in the agent graph.
888
+
889
+ Args:
890
+ can_jump_to: Optional list of valid jump destinations.
891
+
892
+ Can be:
893
+
894
+ - `'tools'`: Jump to the tools node
895
+ - `'model'`: Jump back to the model node
896
+ - `'end'`: Jump to the end of the graph
897
+
898
+ Returns:
899
+ Decorator function that marks the method with configuration metadata.
900
+
901
+ Examples:
902
+ !!! example "Using decorator on a class method"
903
+
904
+ ```python
905
+ class MyMiddleware(AgentMiddleware):
906
+ @hook_config(can_jump_to=["end", "model"])
907
+ def before_model(self, state: AgentState) -> dict[str, Any] | None:
908
+ if some_condition(state):
909
+ return {"jump_to": "end"}
910
+ return None
911
+ ```
912
+
913
+ Alternative: Use the `can_jump_to` parameter in `before_model`/`after_model`
914
+ decorators:
915
+
916
+ ```python
917
+ @before_model(can_jump_to=["end"])
918
+ def conditional_middleware(state: AgentState) -> dict[str, Any] | None:
919
+ if should_exit(state):
920
+ return {"jump_to": "end"}
921
+ return None
922
+ ```
923
+ """
924
+
925
+ def decorator(func: CallableT) -> CallableT:
926
+ if can_jump_to is not None:
927
+ func.__can_jump_to__ = can_jump_to # type: ignore[attr-defined]
928
+ return func
929
+
930
+ return decorator
931
+
932
+
933
+ @overload
934
+ def before_model(
935
+ func: _CallableWithStateAndRuntime[StateT, ContextT],
936
+ ) -> AgentMiddleware[StateT, ContextT]: ...
937
+
938
+
939
+ @overload
940
+ def before_model(
941
+ func: None = None,
942
+ *,
943
+ state_schema: type[StateT] | None = None,
944
+ tools: list[BaseTool] | None = None,
945
+ can_jump_to: list[JumpTo] | None = None,
946
+ name: str | None = None,
947
+ ) -> Callable[
948
+ [_CallableWithStateAndRuntime[StateT, ContextT]], AgentMiddleware[StateT, ContextT]
949
+ ]: ...
950
+
951
+
952
+ def before_model(
953
+ func: _CallableWithStateAndRuntime[StateT, ContextT] | None = None,
954
+ *,
955
+ state_schema: type[StateT] | None = None,
956
+ tools: list[BaseTool] | None = None,
957
+ can_jump_to: list[JumpTo] | None = None,
958
+ name: str | None = None,
959
+ ) -> (
960
+ Callable[[_CallableWithStateAndRuntime[StateT, ContextT]], AgentMiddleware[StateT, ContextT]]
961
+ | AgentMiddleware[StateT, ContextT]
962
+ ):
963
+ """Decorator used to dynamically create a middleware with the `before_model` hook.
964
+
965
+ Args:
966
+ func: The function to be decorated.
967
+
968
+ Must accept: `state: StateT, runtime: Runtime[ContextT]` - State and runtime
969
+ context
970
+ state_schema: Optional custom state schema type.
971
+
972
+ If not provided, uses the default `AgentState` schema.
973
+ tools: Optional list of additional tools to register with this middleware.
974
+ can_jump_to: Optional list of valid jump destinations for conditional edges.
975
+
976
+ Valid values are: `'tools'`, `'model'`, `'end'`
977
+ name: Optional name for the generated middleware class.
978
+
979
+ If not provided, uses the decorated function's name.
980
+
981
+ Returns:
982
+ Either an `AgentMiddleware` instance (if func is provided directly) or a
983
+ decorator function that can be applied to a function it is wrapping.
984
+
985
+ The decorated function should return:
986
+
987
+ - `dict[str, Any]` - State updates to merge into the agent state
988
+ - `Command` - A command to control flow (e.g., jump to different node)
989
+ - `None` - No state updates or flow control
990
+
991
+ Examples:
992
+ !!! example "Basic usage"
993
+
994
+ ```python
995
+ @before_model
996
+ def log_before_model(state: AgentState, runtime: Runtime) -> None:
997
+ print(f"About to call model with {len(state['messages'])} messages")
998
+ ```
999
+
1000
+ !!! example "With conditional jumping"
1001
+
1002
+ ```python
1003
+ @before_model(can_jump_to=["end"])
1004
+ def conditional_before_model(
1005
+ state: AgentState, runtime: Runtime
1006
+ ) -> dict[str, Any] | None:
1007
+ if some_condition(state):
1008
+ return {"jump_to": "end"}
1009
+ return None
1010
+ ```
1011
+
1012
+ !!! example "With custom state schema"
1013
+
1014
+ Use a custom state schema when your middleware needs to read or write additional
1015
+ state fields that aren't part of the default agent state.
1016
+
1017
+ ```python
1018
+ @before_model(state_schema=MyCustomState)
1019
+ def custom_before_model(state: MyCustomState, runtime: Runtime) -> dict[str, Any]:
1020
+ return {"custom_field": "updated_value"}
1021
+ ```
1022
+
1023
+ !!! example "Streaming custom events before model call"
1024
+
1025
+ Use `runtime.stream_writer` to emit custom events before each model invocation.
1026
+ Events are received when streaming with `stream_mode="custom"`.
1027
+
1028
+ ```python
1029
+ @before_model
1030
+ async def notify_model_call(state: AgentState, runtime: Runtime) -> None:
1031
+ '''Notify user before model is called.'''
1032
+ runtime.stream_writer(
1033
+ {
1034
+ "type": "status",
1035
+ "message": "Thinking...",
1036
+ }
1037
+ )
1038
+ ```
1039
+ """
1040
+
1041
+ def decorator(
1042
+ func: _CallableWithStateAndRuntime[StateT, ContextT],
1043
+ ) -> AgentMiddleware[StateT, ContextT]:
1044
+ is_async = iscoroutinefunction(func)
1045
+
1046
+ func_can_jump_to = (
1047
+ can_jump_to if can_jump_to is not None else getattr(func, "__can_jump_to__", [])
1048
+ )
1049
+
1050
+ if is_async:
1051
+
1052
+ async def async_wrapped(
1053
+ _self: AgentMiddleware[StateT, ContextT],
1054
+ state: StateT,
1055
+ runtime: Runtime[ContextT],
1056
+ ) -> dict[str, Any] | Command[Any] | None:
1057
+ # `iscoroutinefunction` narrows `func` at runtime, but type checkers
1058
+ # cannot narrow this sync-or-async callable union.
1059
+ return await cast("_AsyncCallableWithStateAndRuntime[StateT, ContextT]", func)(
1060
+ state, runtime
1061
+ )
1062
+
1063
+ # Preserve can_jump_to metadata on the wrapped function
1064
+ if func_can_jump_to:
1065
+ async_wrapped.__can_jump_to__ = func_can_jump_to # type: ignore[attr-defined]
1066
+
1067
+ middleware_name = name or cast(
1068
+ "str", getattr(func, "__name__", "BeforeModelMiddleware")
1069
+ )
1070
+
1071
+ # `type(...)` builds the correct middleware subclass at runtime, but
1072
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
1073
+ return cast(
1074
+ "AgentMiddleware[StateT, ContextT]",
1075
+ type(
1076
+ middleware_name,
1077
+ (AgentMiddleware,),
1078
+ {
1079
+ "state_schema": state_schema or AgentState,
1080
+ "tools": tools or [],
1081
+ "abefore_model": async_wrapped,
1082
+ },
1083
+ )(),
1084
+ )
1085
+
1086
+ def wrapped(
1087
+ _self: AgentMiddleware[StateT, ContextT],
1088
+ state: StateT,
1089
+ runtime: Runtime[ContextT],
1090
+ ) -> dict[str, Any] | Command[Any] | None:
1091
+ # `iscoroutinefunction` narrows `func` at runtime, but type checkers
1092
+ # cannot narrow this sync-or-async callable union.
1093
+ return cast("_SyncCallableWithStateAndRuntime[StateT, ContextT]", func)(state, runtime)
1094
+
1095
+ # Preserve can_jump_to metadata on the wrapped function
1096
+ if func_can_jump_to:
1097
+ wrapped.__can_jump_to__ = func_can_jump_to # type: ignore[attr-defined]
1098
+
1099
+ # Use function name as default if no name provided
1100
+ middleware_name = name or cast("str", getattr(func, "__name__", "BeforeModelMiddleware"))
1101
+
1102
+ # `type(...)` builds the correct middleware subclass at runtime, but
1103
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
1104
+ return cast(
1105
+ "AgentMiddleware[StateT, ContextT]",
1106
+ type(
1107
+ middleware_name,
1108
+ (AgentMiddleware,),
1109
+ {
1110
+ "state_schema": state_schema or AgentState,
1111
+ "tools": tools or [],
1112
+ "before_model": wrapped,
1113
+ },
1114
+ )(),
1115
+ )
1116
+
1117
+ if func is not None:
1118
+ return decorator(func)
1119
+ return decorator
1120
+
1121
+
1122
+ @overload
1123
+ def after_model(
1124
+ func: _CallableWithStateAndRuntime[StateT, ContextT],
1125
+ ) -> AgentMiddleware[StateT, ContextT]: ...
1126
+
1127
+
1128
+ @overload
1129
+ def after_model(
1130
+ func: None = None,
1131
+ *,
1132
+ state_schema: type[StateT] | None = None,
1133
+ tools: list[BaseTool] | None = None,
1134
+ can_jump_to: list[JumpTo] | None = None,
1135
+ name: str | None = None,
1136
+ ) -> Callable[
1137
+ [_CallableWithStateAndRuntime[StateT, ContextT]], AgentMiddleware[StateT, ContextT]
1138
+ ]: ...
1139
+
1140
+
1141
+ def after_model(
1142
+ func: _CallableWithStateAndRuntime[StateT, ContextT] | None = None,
1143
+ *,
1144
+ state_schema: type[StateT] | None = None,
1145
+ tools: list[BaseTool] | None = None,
1146
+ can_jump_to: list[JumpTo] | None = None,
1147
+ name: str | None = None,
1148
+ ) -> (
1149
+ Callable[[_CallableWithStateAndRuntime[StateT, ContextT]], AgentMiddleware[StateT, ContextT]]
1150
+ | AgentMiddleware[StateT, ContextT]
1151
+ ):
1152
+ """Decorator used to dynamically create a middleware with the `after_model` hook.
1153
+
1154
+ Args:
1155
+ func: The function to be decorated.
1156
+
1157
+ Must accept: `state: StateT, runtime: Runtime[ContextT]` - State and runtime
1158
+ context
1159
+ state_schema: Optional custom state schema type.
1160
+
1161
+ If not provided, uses the default `AgentState` schema.
1162
+ tools: Optional list of additional tools to register with this middleware.
1163
+ can_jump_to: Optional list of valid jump destinations for conditional edges.
1164
+
1165
+ Valid values are: `'tools'`, `'model'`, `'end'`
1166
+ name: Optional name for the generated middleware class.
1167
+
1168
+ If not provided, uses the decorated function's name.
1169
+
1170
+ Returns:
1171
+ Either an `AgentMiddleware` instance (if func is provided) or a decorator
1172
+ function that can be applied to a function.
1173
+
1174
+ The decorated function should return:
1175
+
1176
+ - `dict[str, Any]` - State updates to merge into the agent state
1177
+ - `Command` - A command to control flow (e.g., jump to different node)
1178
+ - `None` - No state updates or flow control
1179
+
1180
+ Examples:
1181
+ !!! example "Basic usage for logging model responses"
1182
+
1183
+ ```python
1184
+ @after_model
1185
+ def log_latest_message(state: AgentState, runtime: Runtime) -> None:
1186
+ print(state["messages"][-1].content)
1187
+ ```
1188
+
1189
+ !!! example "With custom state schema"
1190
+
1191
+ Use a custom state schema when your middleware needs to read or write additional
1192
+ state fields that aren't part of the default agent state.
1193
+
1194
+ ```python
1195
+ @after_model(state_schema=MyCustomState, name="MyAfterModelMiddleware")
1196
+ def custom_after_model(state: MyCustomState, runtime: Runtime) -> dict[str, Any]:
1197
+ return {"custom_field": "updated_after_model"}
1198
+ ```
1199
+
1200
+ !!! example "Streaming custom events after model call"
1201
+
1202
+ Use `runtime.stream_writer` to emit custom events after model responds.
1203
+ Events are received when streaming with `stream_mode="custom"`.
1204
+
1205
+ ```python
1206
+ @after_model
1207
+ async def notify_model_response(state: AgentState, runtime: Runtime) -> None:
1208
+ '''Notify user after model has responded.'''
1209
+ last_message = state["messages"][-1]
1210
+ has_tool_calls = hasattr(last_message, "tool_calls") and last_message.tool_calls
1211
+ runtime.stream_writer(
1212
+ {
1213
+ "type": "status",
1214
+ "message": "Using tools..." if has_tool_calls else "Response ready!",
1215
+ }
1216
+ )
1217
+ ```
1218
+ """
1219
+
1220
+ def decorator(
1221
+ func: _CallableWithStateAndRuntime[StateT, ContextT],
1222
+ ) -> AgentMiddleware[StateT, ContextT]:
1223
+ is_async = iscoroutinefunction(func)
1224
+ # Extract can_jump_to from decorator parameter or from function metadata
1225
+ func_can_jump_to = (
1226
+ can_jump_to if can_jump_to is not None else getattr(func, "__can_jump_to__", [])
1227
+ )
1228
+
1229
+ if is_async:
1230
+
1231
+ async def async_wrapped(
1232
+ _self: AgentMiddleware[StateT, ContextT],
1233
+ state: StateT,
1234
+ runtime: Runtime[ContextT],
1235
+ ) -> dict[str, Any] | Command[Any] | None:
1236
+ # `iscoroutinefunction` narrows `func` at runtime, but type checkers
1237
+ # cannot narrow this sync-or-async callable union.
1238
+ return await cast("_AsyncCallableWithStateAndRuntime[StateT, ContextT]", func)(
1239
+ state, runtime
1240
+ )
1241
+
1242
+ # Preserve can_jump_to metadata on the wrapped function
1243
+ if func_can_jump_to:
1244
+ async_wrapped.__can_jump_to__ = func_can_jump_to # type: ignore[attr-defined]
1245
+
1246
+ middleware_name = name or cast("str", getattr(func, "__name__", "AfterModelMiddleware"))
1247
+
1248
+ # `type(...)` builds the correct middleware subclass at runtime, but
1249
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
1250
+ return cast(
1251
+ "AgentMiddleware[StateT, ContextT]",
1252
+ type(
1253
+ middleware_name,
1254
+ (AgentMiddleware,),
1255
+ {
1256
+ "state_schema": state_schema or AgentState,
1257
+ "tools": tools or [],
1258
+ "aafter_model": async_wrapped,
1259
+ },
1260
+ )(),
1261
+ )
1262
+
1263
+ def wrapped(
1264
+ _self: AgentMiddleware[StateT, ContextT],
1265
+ state: StateT,
1266
+ runtime: Runtime[ContextT],
1267
+ ) -> dict[str, Any] | Command[Any] | None:
1268
+ # `iscoroutinefunction` narrows `func` at runtime, but type checkers
1269
+ # cannot narrow this sync-or-async callable union.
1270
+ return cast("_SyncCallableWithStateAndRuntime[StateT, ContextT]", func)(state, runtime)
1271
+
1272
+ # Preserve can_jump_to metadata on the wrapped function
1273
+ if func_can_jump_to:
1274
+ wrapped.__can_jump_to__ = func_can_jump_to # type: ignore[attr-defined]
1275
+
1276
+ # Use function name as default if no name provided
1277
+ middleware_name = name or cast("str", getattr(func, "__name__", "AfterModelMiddleware"))
1278
+
1279
+ # `type(...)` builds the correct middleware subclass at runtime, but
1280
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
1281
+ return cast(
1282
+ "AgentMiddleware[StateT, ContextT]",
1283
+ type(
1284
+ middleware_name,
1285
+ (AgentMiddleware,),
1286
+ {
1287
+ "state_schema": state_schema or AgentState,
1288
+ "tools": tools or [],
1289
+ "after_model": wrapped,
1290
+ },
1291
+ )(),
1292
+ )
1293
+
1294
+ if func is not None:
1295
+ return decorator(func)
1296
+ return decorator
1297
+
1298
+
1299
+ @overload
1300
+ def before_agent(
1301
+ func: _CallableWithStateAndRuntime[StateT, ContextT],
1302
+ ) -> AgentMiddleware[StateT, ContextT]: ...
1303
+
1304
+
1305
+ @overload
1306
+ def before_agent(
1307
+ func: None = None,
1308
+ *,
1309
+ state_schema: type[StateT] | None = None,
1310
+ tools: list[BaseTool] | None = None,
1311
+ can_jump_to: list[JumpTo] | None = None,
1312
+ name: str | None = None,
1313
+ ) -> Callable[
1314
+ [_CallableWithStateAndRuntime[StateT, ContextT]], AgentMiddleware[StateT, ContextT]
1315
+ ]: ...
1316
+
1317
+
1318
+ def before_agent(
1319
+ func: _CallableWithStateAndRuntime[StateT, ContextT] | None = None,
1320
+ *,
1321
+ state_schema: type[StateT] | None = None,
1322
+ tools: list[BaseTool] | None = None,
1323
+ can_jump_to: list[JumpTo] | None = None,
1324
+ name: str | None = None,
1325
+ ) -> (
1326
+ Callable[[_CallableWithStateAndRuntime[StateT, ContextT]], AgentMiddleware[StateT, ContextT]]
1327
+ | AgentMiddleware[StateT, ContextT]
1328
+ ):
1329
+ """Decorator used to dynamically create a middleware with the `before_agent` hook.
1330
+
1331
+ Args:
1332
+ func: The function to be decorated.
1333
+
1334
+ Must accept: `state: StateT, runtime: Runtime[ContextT]` - State and runtime
1335
+ context
1336
+ state_schema: Optional custom state schema type.
1337
+
1338
+ If not provided, uses the default `AgentState` schema.
1339
+ tools: Optional list of additional tools to register with this middleware.
1340
+ can_jump_to: Optional list of valid jump destinations for conditional edges.
1341
+
1342
+ Valid values are: `'tools'`, `'model'`, `'end'`
1343
+ name: Optional name for the generated middleware class.
1344
+
1345
+ If not provided, uses the decorated function's name.
1346
+
1347
+ Returns:
1348
+ Either an `AgentMiddleware` instance (if func is provided directly) or a
1349
+ decorator function that can be applied to a function it is wrapping.
1350
+
1351
+ The decorated function should return:
1352
+
1353
+ - `dict[str, Any]` - State updates to merge into the agent state
1354
+ - `Command` - A command to control flow (e.g., jump to different node)
1355
+ - `None` - No state updates or flow control
1356
+
1357
+ Examples:
1358
+ !!! example "Basic usage"
1359
+
1360
+ ```python
1361
+ @before_agent
1362
+ def log_before_agent(state: AgentState, runtime: Runtime) -> None:
1363
+ print(f"Starting agent with {len(state['messages'])} messages")
1364
+ ```
1365
+
1366
+ !!! example "With conditional jumping"
1367
+
1368
+ ```python
1369
+ @before_agent(can_jump_to=["end"])
1370
+ def conditional_before_agent(
1371
+ state: AgentState, runtime: Runtime
1372
+ ) -> dict[str, Any] | None:
1373
+ if some_condition(state):
1374
+ return {"jump_to": "end"}
1375
+ return None
1376
+ ```
1377
+
1378
+ !!! example "With custom state schema"
1379
+
1380
+ Use a custom state schema when your middleware needs to read or write additional
1381
+ state fields that aren't part of the default agent state.
1382
+
1383
+ ```python
1384
+ @before_agent(state_schema=MyCustomState)
1385
+ def custom_before_agent(state: MyCustomState, runtime: Runtime) -> dict[str, Any]:
1386
+ return {"custom_field": "initialized_value"}
1387
+ ```
1388
+
1389
+ !!! example "Streaming custom events"
1390
+
1391
+ Use `runtime.stream_writer` to emit custom events during agent execution.
1392
+ Events are received when streaming with `stream_mode="custom"`.
1393
+
1394
+ ```python
1395
+ from coco.agents import create_agent
1396
+ from coco.agents.middleware import before_agent, AgentState
1397
+ from coco.messages import HumanMessage
1398
+ from coco_graph.runtime import Runtime
1399
+
1400
+
1401
+ @before_agent
1402
+ async def notify_start(state: AgentState, runtime: Runtime) -> None:
1403
+ '''Notify user that agent is starting.'''
1404
+ runtime.stream_writer(
1405
+ {
1406
+ "type": "status",
1407
+ "message": "Initializing agent session...",
1408
+ }
1409
+ )
1410
+ # Perform prerequisite tasks here
1411
+ runtime.stream_writer({"type": "status", "message": "Agent ready!"})
1412
+
1413
+
1414
+ agent = create_agent(
1415
+ model="openai:gpt-5.2",
1416
+ tools=[...],
1417
+ middleware=[notify_start],
1418
+ )
1419
+
1420
+ # Consume with stream_mode="custom" to receive events
1421
+ async for mode, event in agent.astream(
1422
+ {"messages": [HumanMessage("Hello")]},
1423
+ stream_mode=["updates", "custom"],
1424
+ ):
1425
+ if mode == "custom":
1426
+ print(f"Status: {event}")
1427
+ ```
1428
+ """
1429
+
1430
+ def decorator(
1431
+ func: _CallableWithStateAndRuntime[StateT, ContextT],
1432
+ ) -> AgentMiddleware[StateT, ContextT]:
1433
+ is_async = iscoroutinefunction(func)
1434
+
1435
+ func_can_jump_to = (
1436
+ can_jump_to if can_jump_to is not None else getattr(func, "__can_jump_to__", [])
1437
+ )
1438
+
1439
+ if is_async:
1440
+
1441
+ async def async_wrapped(
1442
+ _self: AgentMiddleware[StateT, ContextT],
1443
+ state: StateT,
1444
+ runtime: Runtime[ContextT],
1445
+ ) -> dict[str, Any] | Command[Any] | None:
1446
+ # `iscoroutinefunction` narrows `func` at runtime, but type checkers
1447
+ # cannot narrow this sync-or-async callable union.
1448
+ return await cast("_AsyncCallableWithStateAndRuntime[StateT, ContextT]", func)(
1449
+ state, runtime
1450
+ )
1451
+
1452
+ # Preserve can_jump_to metadata on the wrapped function
1453
+ if func_can_jump_to:
1454
+ async_wrapped.__can_jump_to__ = func_can_jump_to # type: ignore[attr-defined]
1455
+
1456
+ middleware_name = name or cast(
1457
+ "str", getattr(func, "__name__", "BeforeAgentMiddleware")
1458
+ )
1459
+
1460
+ # `type(...)` builds the correct middleware subclass at runtime, but
1461
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
1462
+ return cast(
1463
+ "AgentMiddleware[StateT, ContextT]",
1464
+ type(
1465
+ middleware_name,
1466
+ (AgentMiddleware,),
1467
+ {
1468
+ "state_schema": state_schema or AgentState,
1469
+ "tools": tools or [],
1470
+ "abefore_agent": async_wrapped,
1471
+ },
1472
+ )(),
1473
+ )
1474
+
1475
+ def wrapped(
1476
+ _self: AgentMiddleware[StateT, ContextT],
1477
+ state: StateT,
1478
+ runtime: Runtime[ContextT],
1479
+ ) -> dict[str, Any] | Command[Any] | None:
1480
+ # `iscoroutinefunction` narrows `func` at runtime, but type checkers
1481
+ # cannot narrow this sync-or-async callable union.
1482
+ return cast("_SyncCallableWithStateAndRuntime[StateT, ContextT]", func)(state, runtime)
1483
+
1484
+ # Preserve can_jump_to metadata on the wrapped function
1485
+ if func_can_jump_to:
1486
+ wrapped.__can_jump_to__ = func_can_jump_to # type: ignore[attr-defined]
1487
+
1488
+ # Use function name as default if no name provided
1489
+ middleware_name = name or cast("str", getattr(func, "__name__", "BeforeAgentMiddleware"))
1490
+
1491
+ # `type(...)` builds the correct middleware subclass at runtime, but
1492
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
1493
+ return cast(
1494
+ "AgentMiddleware[StateT, ContextT]",
1495
+ type(
1496
+ middleware_name,
1497
+ (AgentMiddleware,),
1498
+ {
1499
+ "state_schema": state_schema or AgentState,
1500
+ "tools": tools or [],
1501
+ "before_agent": wrapped,
1502
+ },
1503
+ )(),
1504
+ )
1505
+
1506
+ if func is not None:
1507
+ return decorator(func)
1508
+ return decorator
1509
+
1510
+
1511
+ @overload
1512
+ def after_agent(
1513
+ func: _CallableWithStateAndRuntime[StateT, ContextT],
1514
+ ) -> AgentMiddleware[StateT, ContextT]: ...
1515
+
1516
+
1517
+ @overload
1518
+ def after_agent(
1519
+ func: None = None,
1520
+ *,
1521
+ state_schema: type[StateT] | None = None,
1522
+ tools: list[BaseTool] | None = None,
1523
+ can_jump_to: list[JumpTo] | None = None,
1524
+ name: str | None = None,
1525
+ ) -> Callable[
1526
+ [_CallableWithStateAndRuntime[StateT, ContextT]], AgentMiddleware[StateT, ContextT]
1527
+ ]: ...
1528
+
1529
+
1530
+ def after_agent(
1531
+ func: _CallableWithStateAndRuntime[StateT, ContextT] | None = None,
1532
+ *,
1533
+ state_schema: type[StateT] | None = None,
1534
+ tools: list[BaseTool] | None = None,
1535
+ can_jump_to: list[JumpTo] | None = None,
1536
+ name: str | None = None,
1537
+ ) -> (
1538
+ Callable[[_CallableWithStateAndRuntime[StateT, ContextT]], AgentMiddleware[StateT, ContextT]]
1539
+ | AgentMiddleware[StateT, ContextT]
1540
+ ):
1541
+ """Decorator used to dynamically create a middleware with the `after_agent` hook.
1542
+
1543
+ Async version is `aafter_agent`.
1544
+
1545
+ Args:
1546
+ func: The function to be decorated.
1547
+
1548
+ Must accept: `state: StateT, runtime: Runtime[ContextT]` - State and runtime
1549
+ context
1550
+ state_schema: Optional custom state schema type.
1551
+
1552
+ If not provided, uses the default `AgentState` schema.
1553
+ tools: Optional list of additional tools to register with this middleware.
1554
+ can_jump_to: Optional list of valid jump destinations for conditional edges.
1555
+
1556
+ Valid values are: `'tools'`, `'model'`, `'end'`
1557
+ name: Optional name for the generated middleware class.
1558
+
1559
+ If not provided, uses the decorated function's name.
1560
+
1561
+ Returns:
1562
+ Either an `AgentMiddleware` instance (if func is provided) or a decorator
1563
+ function that can be applied to a function.
1564
+
1565
+ The decorated function should return:
1566
+
1567
+ - `dict[str, Any]` - State updates to merge into the agent state
1568
+ - `Command` - A command to control flow (e.g., jump to different node)
1569
+ - `None` - No state updates or flow control
1570
+
1571
+ Examples:
1572
+ !!! example "Basic usage for logging agent completion"
1573
+
1574
+ ```python
1575
+ @after_agent
1576
+ def log_completion(state: AgentState, runtime: Runtime) -> None:
1577
+ print(f"Agent completed with {len(state['messages'])} messages")
1578
+ ```
1579
+
1580
+ !!! example "With custom state schema"
1581
+
1582
+ Use a custom state schema when your middleware needs to read or write additional
1583
+ state fields that aren't part of the default agent state.
1584
+
1585
+ ```python
1586
+ @after_agent(state_schema=MyCustomState, name="MyAfterAgentMiddleware")
1587
+ def custom_after_agent(state: MyCustomState, runtime: Runtime) -> dict[str, Any]:
1588
+ return {"custom_field": "finalized_value"}
1589
+ ```
1590
+
1591
+ !!! example "Streaming custom events on completion"
1592
+
1593
+ Use `runtime.stream_writer` to emit custom events when agent completes.
1594
+ Events are received when streaming with `stream_mode="custom"`.
1595
+
1596
+ ```python
1597
+ @after_agent
1598
+ async def notify_completion(state: AgentState, runtime: Runtime) -> None:
1599
+ '''Notify user that agent has completed.'''
1600
+ runtime.stream_writer(
1601
+ {
1602
+ "type": "status",
1603
+ "message": "Agent execution complete!",
1604
+ "total_messages": len(state["messages"]),
1605
+ }
1606
+ )
1607
+ ```
1608
+ """
1609
+
1610
+ def decorator(
1611
+ func: _CallableWithStateAndRuntime[StateT, ContextT],
1612
+ ) -> AgentMiddleware[StateT, ContextT]:
1613
+ is_async = iscoroutinefunction(func)
1614
+ # Extract can_jump_to from decorator parameter or from function metadata
1615
+ func_can_jump_to = (
1616
+ can_jump_to if can_jump_to is not None else getattr(func, "__can_jump_to__", [])
1617
+ )
1618
+
1619
+ if is_async:
1620
+
1621
+ async def async_wrapped(
1622
+ _self: AgentMiddleware[StateT, ContextT],
1623
+ state: StateT,
1624
+ runtime: Runtime[ContextT],
1625
+ ) -> dict[str, Any] | Command[Any] | None:
1626
+ # `iscoroutinefunction` narrows `func` at runtime, but type checkers
1627
+ # cannot narrow this sync-or-async callable union.
1628
+ return await cast("_AsyncCallableWithStateAndRuntime[StateT, ContextT]", func)(
1629
+ state, runtime
1630
+ )
1631
+
1632
+ # Preserve can_jump_to metadata on the wrapped function
1633
+ if func_can_jump_to:
1634
+ async_wrapped.__can_jump_to__ = func_can_jump_to # type: ignore[attr-defined]
1635
+
1636
+ middleware_name = name or cast("str", getattr(func, "__name__", "AfterAgentMiddleware"))
1637
+
1638
+ # `type(...)` builds the correct middleware subclass at runtime, but
1639
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
1640
+ return cast(
1641
+ "AgentMiddleware[StateT, ContextT]",
1642
+ type(
1643
+ middleware_name,
1644
+ (AgentMiddleware,),
1645
+ {
1646
+ "state_schema": state_schema or AgentState,
1647
+ "tools": tools or [],
1648
+ "aafter_agent": async_wrapped,
1649
+ },
1650
+ )(),
1651
+ )
1652
+
1653
+ def wrapped(
1654
+ _self: AgentMiddleware[StateT, ContextT],
1655
+ state: StateT,
1656
+ runtime: Runtime[ContextT],
1657
+ ) -> dict[str, Any] | Command[Any] | None:
1658
+ # `iscoroutinefunction` narrows `func` at runtime, but type checkers
1659
+ # cannot narrow this sync-or-async callable union.
1660
+ return cast("_SyncCallableWithStateAndRuntime[StateT, ContextT]", func)(state, runtime)
1661
+
1662
+ # Preserve can_jump_to metadata on the wrapped function
1663
+ if func_can_jump_to:
1664
+ wrapped.__can_jump_to__ = func_can_jump_to # type: ignore[attr-defined]
1665
+
1666
+ # Use function name as default if no name provided
1667
+ middleware_name = name or cast("str", getattr(func, "__name__", "AfterAgentMiddleware"))
1668
+
1669
+ # `type(...)` builds the correct middleware subclass at runtime, but
1670
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
1671
+ return cast(
1672
+ "AgentMiddleware[StateT, ContextT]",
1673
+ type(
1674
+ middleware_name,
1675
+ (AgentMiddleware,),
1676
+ {
1677
+ "state_schema": state_schema or AgentState,
1678
+ "tools": tools or [],
1679
+ "after_agent": wrapped,
1680
+ },
1681
+ )(),
1682
+ )
1683
+
1684
+ if func is not None:
1685
+ return decorator(func)
1686
+ return decorator
1687
+
1688
+
1689
+ @overload
1690
+ def dynamic_prompt(
1691
+ func: _CallableReturningSystemMessage[ContextT],
1692
+ ) -> AgentMiddleware[StateT, ContextT]: ...
1693
+
1694
+
1695
+ @overload
1696
+ def dynamic_prompt(
1697
+ func: None = None,
1698
+ ) -> Callable[
1699
+ [_CallableReturningSystemMessage[ContextT]],
1700
+ AgentMiddleware[StateT, ContextT],
1701
+ ]: ...
1702
+
1703
+
1704
+ def dynamic_prompt(
1705
+ func: _CallableReturningSystemMessage[ContextT] | None = None,
1706
+ ) -> (
1707
+ Callable[
1708
+ [_CallableReturningSystemMessage[ContextT]],
1709
+ AgentMiddleware[StateT, ContextT],
1710
+ ]
1711
+ | AgentMiddleware[StateT, ContextT]
1712
+ ):
1713
+ """Decorator used to dynamically generate system prompts for the model.
1714
+
1715
+ This is a convenience decorator that creates middleware using `wrap_model_call`
1716
+ specifically for dynamic prompt generation. The decorated function should return
1717
+ a string that will be set as the system prompt for the model request.
1718
+
1719
+ Args:
1720
+ func: The function to be decorated.
1721
+
1722
+ Must accept: `request: ModelRequest` - Model request (contains state and
1723
+ runtime)
1724
+
1725
+ Returns:
1726
+ Either an `AgentMiddleware` instance (if func is provided) or a decorator
1727
+ function that can be applied to a function.
1728
+
1729
+ The decorated function should return:
1730
+ - `str` – The system prompt string to use for the model request
1731
+ - `SystemMessage` – A complete system message to use for the model request
1732
+
1733
+ Examples:
1734
+ Basic usage with dynamic content:
1735
+
1736
+ ```python
1737
+ @dynamic_prompt
1738
+ def my_prompt(request: ModelRequest) -> str:
1739
+ user_name = request.runtime.context.get("user_name", "User")
1740
+ return f"You are a helpful assistant helping {user_name}."
1741
+ ```
1742
+
1743
+ Using state to customize the prompt:
1744
+
1745
+ ```python
1746
+ @dynamic_prompt
1747
+ def context_aware_prompt(request: ModelRequest) -> str:
1748
+ msg_count = len(request.state["messages"])
1749
+ if msg_count > 10:
1750
+ return "You are in a long conversation. Be concise."
1751
+ return "You are a helpful assistant."
1752
+ ```
1753
+
1754
+ Using with agent:
1755
+
1756
+ ```python
1757
+ agent = create_agent(model, middleware=[my_prompt])
1758
+ ```
1759
+ """
1760
+
1761
+ def decorator(
1762
+ func: _CallableReturningSystemMessage[ContextT],
1763
+ ) -> AgentMiddleware[StateT, ContextT]:
1764
+ is_async = iscoroutinefunction(func)
1765
+
1766
+ if is_async:
1767
+
1768
+ async def async_wrapped(
1769
+ _self: AgentMiddleware[StateT, ContextT],
1770
+ request: ModelRequest[ContextT],
1771
+ handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse[Any]]],
1772
+ ) -> ModelResponse[Any] | AIMessage:
1773
+ prompt = await cast("_AsyncCallableReturningSystemMessage[ContextT]", func)(request)
1774
+ if isinstance(prompt, SystemMessage):
1775
+ request = request.override(system_message=prompt)
1776
+ else:
1777
+ request = request.override(system_message=SystemMessage(content=prompt))
1778
+ return await handler(request)
1779
+
1780
+ middleware_name = cast("str", getattr(func, "__name__", "DynamicPromptMiddleware"))
1781
+
1782
+ # `type(...)` builds the correct middleware subclass at runtime, but
1783
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
1784
+ return cast(
1785
+ "AgentMiddleware[StateT, ContextT]",
1786
+ type(
1787
+ middleware_name,
1788
+ (AgentMiddleware,),
1789
+ {
1790
+ "state_schema": AgentState,
1791
+ "tools": [],
1792
+ "awrap_model_call": async_wrapped,
1793
+ },
1794
+ )(),
1795
+ )
1796
+
1797
+ def wrapped(
1798
+ _self: AgentMiddleware[StateT, ContextT],
1799
+ request: ModelRequest[ContextT],
1800
+ handler: Callable[[ModelRequest[ContextT]], ModelResponse[Any]],
1801
+ ) -> ModelResponse[Any] | AIMessage:
1802
+ prompt = cast("_SyncCallableReturningSystemMessage[ContextT]", func)(request)
1803
+ if isinstance(prompt, SystemMessage):
1804
+ request = request.override(system_message=prompt)
1805
+ else:
1806
+ request = request.override(system_message=SystemMessage(content=prompt))
1807
+ return handler(request)
1808
+
1809
+ async def async_wrapped_from_sync(
1810
+ _self: AgentMiddleware[StateT, ContextT],
1811
+ request: ModelRequest[ContextT],
1812
+ handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse[Any]]],
1813
+ ) -> ModelResponse[Any] | AIMessage:
1814
+ # Delegate to sync function
1815
+ prompt = cast("_SyncCallableReturningSystemMessage[ContextT]", func)(request)
1816
+ if isinstance(prompt, SystemMessage):
1817
+ request = request.override(system_message=prompt)
1818
+ else:
1819
+ request = request.override(system_message=SystemMessage(content=prompt))
1820
+ return await handler(request)
1821
+
1822
+ middleware_name = cast("str", getattr(func, "__name__", "DynamicPromptMiddleware"))
1823
+
1824
+ # `type(...)` builds the correct middleware subclass at runtime, but
1825
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
1826
+ return cast(
1827
+ "AgentMiddleware[StateT, ContextT]",
1828
+ type(
1829
+ middleware_name,
1830
+ (AgentMiddleware,),
1831
+ {
1832
+ "state_schema": AgentState,
1833
+ "tools": [],
1834
+ "wrap_model_call": wrapped,
1835
+ "awrap_model_call": async_wrapped_from_sync,
1836
+ },
1837
+ )(),
1838
+ )
1839
+
1840
+ if func is not None:
1841
+ return decorator(func)
1842
+ return decorator
1843
+
1844
+
1845
+ @overload
1846
+ def wrap_model_call(
1847
+ func: _CallableReturningModelResponse[ContextT, ResponseT],
1848
+ ) -> AgentMiddleware[StateT, ContextT]: ...
1849
+
1850
+
1851
+ @overload
1852
+ def wrap_model_call(
1853
+ func: None = None,
1854
+ *,
1855
+ state_schema: type[StateT] | None = None,
1856
+ tools: list[BaseTool] | None = None,
1857
+ name: str | None = None,
1858
+ ) -> Callable[
1859
+ [_CallableReturningModelResponse[ContextT, ResponseT]],
1860
+ AgentMiddleware[StateT, ContextT],
1861
+ ]: ...
1862
+
1863
+
1864
+ def wrap_model_call(
1865
+ func: _CallableReturningModelResponse[ContextT, ResponseT] | None = None,
1866
+ *,
1867
+ state_schema: type[StateT] | None = None,
1868
+ tools: list[BaseTool] | None = None,
1869
+ name: str | None = None,
1870
+ ) -> (
1871
+ Callable[
1872
+ [_CallableReturningModelResponse[ContextT, ResponseT]],
1873
+ AgentMiddleware[StateT, ContextT],
1874
+ ]
1875
+ | AgentMiddleware[StateT, ContextT]
1876
+ ):
1877
+ """Create middleware with `wrap_model_call` hook from a function.
1878
+
1879
+ Converts a function with handler callback into middleware that can intercept model
1880
+ calls, implement retry logic, handle errors, and rewrite responses.
1881
+
1882
+ Args:
1883
+ func: Function accepting (request, handler) that calls handler(request)
1884
+ to execute the model and returns `ModelResponse` or `AIMessage`.
1885
+
1886
+ Request contains state and runtime.
1887
+ state_schema: Custom state schema.
1888
+
1889
+ Defaults to `AgentState`.
1890
+ tools: Additional tools to register with this middleware.
1891
+ name: Middleware class name.
1892
+
1893
+ Defaults to function name.
1894
+
1895
+ Returns:
1896
+ `AgentMiddleware` instance if func provided, otherwise a decorator.
1897
+
1898
+ Examples:
1899
+ !!! example "Basic retry logic"
1900
+
1901
+ ```python
1902
+ @wrap_model_call
1903
+ def retry_on_error(request, handler):
1904
+ max_retries = 3
1905
+ for attempt in range(max_retries):
1906
+ try:
1907
+ return handler(request)
1908
+ except Exception:
1909
+ if attempt == max_retries - 1:
1910
+ raise
1911
+ ```
1912
+
1913
+ !!! example "Model fallback"
1914
+
1915
+ ```python
1916
+ @wrap_model_call
1917
+ def fallback_model(request, handler):
1918
+ # Try primary model
1919
+ try:
1920
+ return handler(request)
1921
+ except Exception:
1922
+ pass
1923
+
1924
+ # Try fallback model
1925
+ request = request.override(model=fallback_model_instance)
1926
+ return handler(request)
1927
+ ```
1928
+
1929
+ !!! example "Rewrite response content (full `ModelResponse`)"
1930
+
1931
+ ```python
1932
+ @wrap_model_call
1933
+ def uppercase_responses(request, handler):
1934
+ response = handler(request)
1935
+ ai_msg = response.result[0]
1936
+ return ModelResponse(
1937
+ result=[AIMessage(content=ai_msg.content.upper())],
1938
+ structured_response=response.structured_response,
1939
+ )
1940
+ ```
1941
+
1942
+ !!! example "Simple `AIMessage` return (converted automatically)"
1943
+
1944
+ ```python
1945
+ @wrap_model_call
1946
+ def simple_response(request, handler):
1947
+ # AIMessage is automatically converted to ModelResponse
1948
+ return AIMessage(content="Simple response")
1949
+ ```
1950
+ """
1951
+
1952
+ def decorator(
1953
+ func: _CallableReturningModelResponse[ContextT, ResponseT],
1954
+ ) -> AgentMiddleware[StateT, ContextT]:
1955
+ is_async = iscoroutinefunction(func)
1956
+
1957
+ if is_async:
1958
+
1959
+ async def async_wrapped(
1960
+ _self: AgentMiddleware[StateT, ContextT],
1961
+ request: ModelRequest[ContextT],
1962
+ handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse[ResponseT]]],
1963
+ ) -> ModelCallResult[ResponseT]:
1964
+ return await cast(
1965
+ "_AsyncCallableReturningModelResponse[ContextT, ResponseT]", func
1966
+ )(request, handler)
1967
+
1968
+ middleware_name = name or cast(
1969
+ "str", getattr(func, "__name__", "WrapModelCallMiddleware")
1970
+ )
1971
+
1972
+ # `type(...)` builds the correct middleware subclass at runtime, but
1973
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
1974
+ return cast(
1975
+ "AgentMiddleware[StateT, ContextT]",
1976
+ type(
1977
+ middleware_name,
1978
+ (AgentMiddleware,),
1979
+ {
1980
+ "state_schema": state_schema or AgentState,
1981
+ "tools": tools or [],
1982
+ "awrap_model_call": async_wrapped,
1983
+ },
1984
+ )(),
1985
+ )
1986
+
1987
+ def wrapped(
1988
+ _self: AgentMiddleware[StateT, ContextT],
1989
+ request: ModelRequest[ContextT],
1990
+ handler: Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]],
1991
+ ) -> ModelCallResult[ResponseT]:
1992
+ return cast("_SyncCallableReturningModelResponse[ContextT, ResponseT]", func)(
1993
+ request, handler
1994
+ )
1995
+
1996
+ middleware_name = name or cast("str", getattr(func, "__name__", "WrapModelCallMiddleware"))
1997
+
1998
+ # `type(...)` builds the correct middleware subclass at runtime, but
1999
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
2000
+ return cast(
2001
+ "AgentMiddleware[StateT, ContextT]",
2002
+ type(
2003
+ middleware_name,
2004
+ (AgentMiddleware,),
2005
+ {
2006
+ "state_schema": state_schema or AgentState,
2007
+ "tools": tools or [],
2008
+ "wrap_model_call": wrapped,
2009
+ },
2010
+ )(),
2011
+ )
2012
+
2013
+ if func is not None:
2014
+ return decorator(func)
2015
+ return decorator
2016
+
2017
+
2018
+ @overload
2019
+ def wrap_tool_call(
2020
+ func: _CallableReturningToolResponse,
2021
+ ) -> AgentMiddleware[StateT, ContextT]: ...
2022
+
2023
+
2024
+ @overload
2025
+ def wrap_tool_call(
2026
+ func: None = None,
2027
+ *,
2028
+ state_schema: type[StateT] | None = None,
2029
+ tools: list[BaseTool] | None = None,
2030
+ name: str | None = None,
2031
+ ) -> Callable[
2032
+ [_CallableReturningToolResponse],
2033
+ AgentMiddleware[StateT, ContextT],
2034
+ ]: ...
2035
+
2036
+
2037
+ def wrap_tool_call(
2038
+ func: _CallableReturningToolResponse | None = None,
2039
+ *,
2040
+ state_schema: type[StateT] | None = None,
2041
+ tools: list[BaseTool] | None = None,
2042
+ name: str | None = None,
2043
+ ) -> (
2044
+ Callable[
2045
+ [_CallableReturningToolResponse],
2046
+ AgentMiddleware[StateT, ContextT],
2047
+ ]
2048
+ | AgentMiddleware[StateT, ContextT]
2049
+ ):
2050
+ """Create middleware with `wrap_tool_call` hook from a function.
2051
+
2052
+ Async version is `awrap_tool_call`.
2053
+
2054
+ Converts a function with handler callback into middleware that can intercept
2055
+ tool calls, implement retry logic, monitor execution, and modify responses.
2056
+
2057
+ Args:
2058
+ func: Function accepting (request, handler) that calls
2059
+ handler(request) to execute the tool and returns final `ToolMessage` or
2060
+ `Command`.
2061
+
2062
+ Can be sync or async.
2063
+ state_schema: Optional custom state schema type.
2064
+
2065
+ If not provided, uses the default `AgentState` schema.
2066
+ tools: Additional tools to register with this middleware.
2067
+ name: Middleware class name.
2068
+
2069
+ Defaults to function name.
2070
+
2071
+ Returns:
2072
+ `AgentMiddleware` instance if func provided, otherwise a decorator.
2073
+
2074
+ Examples:
2075
+ !!! example "Retry logic"
2076
+
2077
+ ```python
2078
+ @wrap_tool_call
2079
+ def retry_on_error(request, handler):
2080
+ max_retries = 3
2081
+ for attempt in range(max_retries):
2082
+ try:
2083
+ return handler(request)
2084
+ except Exception:
2085
+ if attempt == max_retries - 1:
2086
+ raise
2087
+ ```
2088
+
2089
+ !!! example "Async retry logic"
2090
+
2091
+ ```python
2092
+ @wrap_tool_call
2093
+ async def async_retry(request, handler):
2094
+ for attempt in range(3):
2095
+ try:
2096
+ return await handler(request)
2097
+ except Exception:
2098
+ if attempt == 2:
2099
+ raise
2100
+ ```
2101
+
2102
+ !!! example "Modify request"
2103
+
2104
+ ```python
2105
+ @wrap_tool_call
2106
+ def modify_args(request, handler):
2107
+ modified_call = {
2108
+ **request.tool_call,
2109
+ "args": {
2110
+ **request.tool_call["args"],
2111
+ "value": request.tool_call["args"]["value"] * 2,
2112
+ },
2113
+ }
2114
+ request = request.override(tool_call=modified_call)
2115
+ return handler(request)
2116
+ ```
2117
+
2118
+ !!! example "Short-circuit with cached result"
2119
+
2120
+ ```python
2121
+ @wrap_tool_call
2122
+ def with_cache(request, handler):
2123
+ if cached := get_cache(request):
2124
+ return ToolMessage(content=cached, tool_call_id=request.tool_call["id"])
2125
+ result = handler(request)
2126
+ save_cache(request, result)
2127
+ return result
2128
+ ```
2129
+
2130
+ !!! example "With custom state schema"
2131
+
2132
+ Use a custom state schema when your middleware needs to read or write additional
2133
+ state fields that aren't part of the default agent state.
2134
+
2135
+ ```python
2136
+ @wrap_tool_call(state_schema=MyCustomState)
2137
+ def custom_wrap_tool_call(request, handler):
2138
+ return handler(request)
2139
+ ```
2140
+ """
2141
+
2142
+ def decorator(
2143
+ func: _CallableReturningToolResponse,
2144
+ ) -> AgentMiddleware[StateT, ContextT]:
2145
+ is_async = iscoroutinefunction(func)
2146
+
2147
+ if is_async:
2148
+
2149
+ async def async_wrapped(
2150
+ _self: AgentMiddleware[StateT, ContextT],
2151
+ request: ToolCallRequest,
2152
+ handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
2153
+ ) -> ToolMessage | Command[Any]:
2154
+ return await cast("_AsyncCallableReturningToolResponse", func)(request, handler)
2155
+
2156
+ middleware_name = name or cast(
2157
+ "str", getattr(func, "__name__", "WrapToolCallMiddleware")
2158
+ )
2159
+
2160
+ # `type(...)` builds the correct middleware subclass at runtime, but
2161
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
2162
+ return cast(
2163
+ "AgentMiddleware[StateT, ContextT]",
2164
+ type(
2165
+ middleware_name,
2166
+ (AgentMiddleware,),
2167
+ {
2168
+ "state_schema": state_schema or AgentState,
2169
+ "tools": tools or [],
2170
+ "awrap_tool_call": async_wrapped,
2171
+ },
2172
+ )(),
2173
+ )
2174
+
2175
+ def wrapped(
2176
+ _self: AgentMiddleware[StateT, ContextT],
2177
+ request: ToolCallRequest,
2178
+ handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
2179
+ ) -> ToolMessage | Command[Any]:
2180
+ return cast("_SyncCallableReturningToolResponse", func)(request, handler)
2181
+
2182
+ middleware_name = name or cast("str", getattr(func, "__name__", "WrapToolCallMiddleware"))
2183
+
2184
+ # `type(...)` builds the correct middleware subclass at runtime, but
2185
+ # type checkers cannot infer its generic `AgentMiddleware` parameters.
2186
+ return cast(
2187
+ "AgentMiddleware[StateT, ContextT]",
2188
+ type(
2189
+ middleware_name,
2190
+ (AgentMiddleware,),
2191
+ {
2192
+ "state_schema": state_schema or AgentState,
2193
+ "tools": tools or [],
2194
+ "wrap_tool_call": wrapped,
2195
+ },
2196
+ )(),
2197
+ )
2198
+
2199
+ if func is not None:
2200
+ return decorator(func)
2201
+ return decorator