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,421 @@
1
+ """Tool retry middleware for agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ import warnings
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from coco_core.messages import ToolMessage
11
+ from coco_graph.errors import GraphBubbleUp
12
+
13
+ from coco.agents.middleware._retry import (
14
+ OnFailure,
15
+ RetryOn,
16
+ calculate_delay,
17
+ default_retry_on,
18
+ should_retry_exception,
19
+ validate_retry_params,
20
+ )
21
+ from coco.agents.middleware.types import AgentMiddleware, AgentState, ContextT, ResponseT
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Awaitable, Callable
25
+
26
+ from coco_graph.types import Command
27
+
28
+ from coco.agents.middleware.types import ToolCallRequest
29
+ from coco.tools import BaseTool
30
+
31
+
32
+ class ToolRetryMiddleware(AgentMiddleware[AgentState[ResponseT], ContextT, ResponseT]):
33
+ """Middleware that automatically retries failed tool calls with configurable backoff.
34
+
35
+ Supports retrying on specific exceptions and exponential backoff.
36
+
37
+ Examples:
38
+ !!! example "Basic usage with default settings (2 retries, exponential backoff)"
39
+
40
+ ```python
41
+ from coco.agents import create_agent
42
+ from coco.agents.middleware import ToolRetryMiddleware
43
+
44
+ agent = create_agent(model, tools=[search_tool], middleware=[ToolRetryMiddleware()])
45
+ ```
46
+
47
+ !!! example "Retry specific exceptions only"
48
+
49
+ ```python
50
+ from requests.exceptions import RequestException, Timeout
51
+
52
+ retry = ToolRetryMiddleware(
53
+ max_retries=4,
54
+ retry_on=(RequestException, Timeout),
55
+ backoff_factor=1.5,
56
+ )
57
+ ```
58
+
59
+ !!! example "Custom exception filtering"
60
+
61
+ Exceptions for which the callable returns `False` propagate to the caller.
62
+ They are not passed to `on_failure`.
63
+
64
+ ```python
65
+ from requests.exceptions import HTTPError
66
+
67
+
68
+ def should_retry(exc: Exception) -> bool:
69
+ # Retry 5xx; anything else propagates
70
+ if isinstance(exc, HTTPError):
71
+ return 500 <= exc.status_code < 600
72
+ return False
73
+
74
+
75
+ retry = ToolRetryMiddleware(
76
+ max_retries=3,
77
+ retry_on=should_retry,
78
+ )
79
+ ```
80
+
81
+ !!! example "Apply to specific tools with custom error handling"
82
+
83
+ ```python
84
+ def format_error(exc: Exception) -> str:
85
+ return "Database temporarily unavailable. Please try again later."
86
+
87
+
88
+ retry = ToolRetryMiddleware(
89
+ max_retries=4,
90
+ tools=["search_database"],
91
+ on_failure=format_error,
92
+ )
93
+ ```
94
+
95
+ !!! example "Apply to specific tools using `BaseTool` instances"
96
+
97
+ ```python
98
+ from coco_core.tools import tool
99
+
100
+
101
+ @tool
102
+ def search_database(query: str) -> str:
103
+ '''Search the database.'''
104
+ return results
105
+
106
+
107
+ retry = ToolRetryMiddleware(
108
+ max_retries=4,
109
+ tools=[search_database], # Pass BaseTool instance
110
+ )
111
+ ```
112
+
113
+ !!! example "Constant backoff (no exponential growth)"
114
+
115
+ ```python
116
+ retry = ToolRetryMiddleware(
117
+ max_retries=5,
118
+ backoff_factor=0.0, # No exponential growth
119
+ initial_delay=2.0, # Always wait 2 seconds
120
+ )
121
+ ```
122
+
123
+ !!! example "Raise exception on failure"
124
+
125
+ ```python
126
+ retry = ToolRetryMiddleware(
127
+ max_retries=2,
128
+ on_failure="error", # Re-raise exception instead of returning message
129
+ )
130
+ ```
131
+ """
132
+
133
+ def __init__(
134
+ self,
135
+ *,
136
+ max_retries: int = 2,
137
+ tools: list[BaseTool | str] | None = None,
138
+ retry_on: RetryOn = default_retry_on,
139
+ on_failure: OnFailure = "continue",
140
+ backoff_factor: float = 2.0,
141
+ initial_delay: float = 1.0,
142
+ max_delay: float = 60.0,
143
+ jitter: bool = True,
144
+ ) -> None:
145
+ """Initialize `ToolRetryMiddleware`.
146
+
147
+ Args:
148
+ max_retries: Maximum number of retry attempts after the initial call.
149
+
150
+ Must be `>= 0`.
151
+ tools: Optional list of tools or tool names to apply retry logic to.
152
+
153
+ Can be a list of `BaseTool` instances or tool name strings.
154
+
155
+ If `None`, applies to all tools.
156
+ retry_on: Either a tuple of exception types to retry on, or a callable
157
+ that takes an exception and returns `True` if it should be retried.
158
+
159
+ By default, retryable model errors and all unclassified exceptions are
160
+ retried. Exceptions that do not match propagate immediately and are not
161
+ handled by `on_failure`.
162
+ on_failure: Behavior when all retries are exhausted.
163
+
164
+ Options:
165
+
166
+ - `'continue'`: Return a `ToolMessage` with error details,
167
+ allowing the LLM to handle the failure and potentially recover.
168
+ - `'error'`: Re-raise the exception, stopping agent execution.
169
+ - **Custom callable:** Function that takes the exception and returns a
170
+ string for the `ToolMessage` content, allowing custom error
171
+ formatting.
172
+
173
+ **Deprecated values** (for backwards compatibility):
174
+
175
+ - `'return_message'`: Use `'continue'` instead.
176
+ - `'raise'`: Use `'error'` instead.
177
+ backoff_factor: Multiplier for exponential backoff.
178
+
179
+ Each retry waits `initial_delay * (backoff_factor ** retry_number)`
180
+ seconds.
181
+
182
+ Set to `0.0` for constant delay.
183
+ initial_delay: Initial delay in seconds before first retry.
184
+ max_delay: Maximum delay in seconds between retries.
185
+
186
+ Caps exponential backoff growth.
187
+ jitter: Whether to add random jitter (`±25%`) to delay to avoid thundering herd.
188
+
189
+ Raises:
190
+ ValueError: If `max_retries < 0` or delays are negative.
191
+ """
192
+ super().__init__()
193
+
194
+ # Validate parameters
195
+ validate_retry_params(max_retries, initial_delay, max_delay, backoff_factor)
196
+
197
+ # Handle backwards compatibility for deprecated on_failure values
198
+ if on_failure == "raise": # type: ignore[comparison-overlap]
199
+ msg = ( # type: ignore[unreachable]
200
+ "on_failure='raise' is deprecated and will be removed in a future version. "
201
+ "Use on_failure='error' instead."
202
+ )
203
+ warnings.warn(msg, DeprecationWarning, stacklevel=2)
204
+ on_failure = "error"
205
+ elif on_failure == "return_message": # type: ignore[comparison-overlap]
206
+ msg = ( # type: ignore[unreachable]
207
+ "on_failure='return_message' is deprecated and will be removed "
208
+ "in a future version. Use on_failure='continue' instead."
209
+ )
210
+ warnings.warn(msg, DeprecationWarning, stacklevel=2)
211
+ on_failure = "continue"
212
+
213
+ self.max_retries = max_retries
214
+
215
+ # Extract tool names from BaseTool instances or strings
216
+ self._tool_filter: list[str] | None
217
+ if tools is not None:
218
+ self._tool_filter = [tool.name if not isinstance(tool, str) else tool for tool in tools]
219
+ else:
220
+ self._tool_filter = None
221
+
222
+ self.tools = [] # No additional tools registered by this middleware
223
+ self.retry_on = retry_on
224
+ self.on_failure = on_failure
225
+ self.backoff_factor = backoff_factor
226
+ self.initial_delay = initial_delay
227
+ self.max_delay = max_delay
228
+ self.jitter = jitter
229
+
230
+ def _should_retry_tool(self, tool_name: str) -> bool:
231
+ """Check if retry logic should apply to this tool.
232
+
233
+ Args:
234
+ tool_name: Name of the tool being called.
235
+
236
+ Returns:
237
+ `True` if retry logic should apply, `False` otherwise.
238
+ """
239
+ if self._tool_filter is None:
240
+ return True
241
+ return tool_name in self._tool_filter
242
+
243
+ @staticmethod
244
+ def _format_failure_message(tool_name: str, exc: Exception, attempts_made: int) -> str:
245
+ """Format the failure message when retries are exhausted.
246
+
247
+ Args:
248
+ tool_name: Name of the tool that failed.
249
+ exc: The exception that caused the failure.
250
+ attempts_made: Number of attempts actually made.
251
+
252
+ Returns:
253
+ Formatted error message string.
254
+ """
255
+ exc_type = type(exc).__name__
256
+ exc_msg = str(exc)
257
+ attempt_word = "attempt" if attempts_made == 1 else "attempts"
258
+ return (
259
+ f"Tool '{tool_name}' failed after {attempts_made} {attempt_word} "
260
+ f"with {exc_type}: {exc_msg}. Please try again."
261
+ )
262
+
263
+ def _handle_failure(
264
+ self, tool_name: str, tool_call_id: str | None, exc: Exception, attempts_made: int
265
+ ) -> ToolMessage:
266
+ """Handle failure when all retries are exhausted.
267
+
268
+ Args:
269
+ tool_name: Name of the tool that failed.
270
+ tool_call_id: ID of the tool call (may be `None`).
271
+ exc: The exception that caused the failure.
272
+ attempts_made: Number of attempts actually made.
273
+
274
+ Returns:
275
+ `ToolMessage` with error details.
276
+
277
+ Raises:
278
+ Exception: If `on_failure` is `'error'`, re-raises the exception.
279
+ """
280
+ if self.on_failure == "error":
281
+ raise exc
282
+
283
+ if callable(self.on_failure):
284
+ content = self.on_failure(exc)
285
+ else:
286
+ content = self._format_failure_message(tool_name, exc, attempts_made)
287
+
288
+ return ToolMessage(
289
+ content=content,
290
+ tool_call_id=tool_call_id,
291
+ name=tool_name,
292
+ status="error",
293
+ )
294
+
295
+ def wrap_tool_call(
296
+ self,
297
+ request: ToolCallRequest,
298
+ handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
299
+ ) -> ToolMessage | Command[Any]:
300
+ """Intercept tool execution and retry on failure.
301
+
302
+ Args:
303
+ request: Tool call request with call dict, `BaseTool`, state, and runtime.
304
+ handler: Callable to execute the tool (can be called multiple times).
305
+
306
+ Returns:
307
+ `ToolMessage` or `Command` (the final result).
308
+
309
+ Raises:
310
+ Exception: Any exception not matched by `retry_on` is re-raised immediately,
311
+ without being passed to `on_failure`.
312
+ RuntimeError: If the retry loop completes without returning. This should not happen.
313
+ """
314
+ tool_name = request.tool.name if request.tool else request.tool_call["name"]
315
+
316
+ # Check if retry should apply to this tool
317
+ if not self._should_retry_tool(tool_name):
318
+ return handler(request)
319
+
320
+ tool_call_id = request.tool_call["id"]
321
+
322
+ # Initial attempt + retries
323
+ for attempt in range(self.max_retries + 1):
324
+ try:
325
+ return handler(request)
326
+ except GraphBubbleUp:
327
+ # Control-flow signals (interrupts, parent commands) must
328
+ # propagate, not be retried or converted to error messages.
329
+ raise
330
+ except Exception as exc:
331
+ attempts_made = attempt + 1 # attempt is 0-indexed
332
+
333
+ # Check if we should retry this exception
334
+ if not should_retry_exception(exc, self.retry_on):
335
+ # Exception is not retryable, re-raise immediately
336
+ raise
337
+
338
+ # Check if we have more retries left
339
+ if attempt < self.max_retries:
340
+ # Calculate and apply backoff delay
341
+ delay = calculate_delay(
342
+ attempt,
343
+ backoff_factor=self.backoff_factor,
344
+ initial_delay=self.initial_delay,
345
+ max_delay=self.max_delay,
346
+ jitter=self.jitter,
347
+ )
348
+ if delay > 0:
349
+ time.sleep(delay)
350
+ # Continue to next retry
351
+ else:
352
+ # No more retries, handle failure
353
+ return self._handle_failure(tool_name, tool_call_id, exc, attempts_made)
354
+
355
+ # Unreachable: loop always returns via handler success or _handle_failure
356
+ msg = "Unexpected: retry loop completed without returning"
357
+ raise RuntimeError(msg)
358
+
359
+ async def awrap_tool_call(
360
+ self,
361
+ request: ToolCallRequest,
362
+ handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
363
+ ) -> ToolMessage | Command[Any]:
364
+ """Intercept and control async tool execution with retry logic.
365
+
366
+ Args:
367
+ request: Tool call request with call `dict`, `BaseTool`, state, and runtime.
368
+ handler: Async callable to execute the tool and returns `ToolMessage` or `Command`.
369
+
370
+ Returns:
371
+ `ToolMessage` or `Command` (the final result).
372
+
373
+ Raises:
374
+ Exception: Any exception not matched by `retry_on` is re-raised immediately,
375
+ without being passed to `on_failure`.
376
+ RuntimeError: If the retry loop completes without returning. This should not happen.
377
+ """
378
+ tool_name = request.tool.name if request.tool else request.tool_call["name"]
379
+
380
+ # Check if retry should apply to this tool
381
+ if not self._should_retry_tool(tool_name):
382
+ return await handler(request)
383
+
384
+ tool_call_id = request.tool_call["id"]
385
+
386
+ # Initial attempt + retries
387
+ for attempt in range(self.max_retries + 1):
388
+ try:
389
+ return await handler(request)
390
+ except GraphBubbleUp:
391
+ # Control-flow signals (interrupts, parent commands) must
392
+ # propagate, not be retried or converted to error messages.
393
+ raise
394
+ except Exception as exc:
395
+ attempts_made = attempt + 1 # attempt is 0-indexed
396
+
397
+ # Check if we should retry this exception
398
+ if not should_retry_exception(exc, self.retry_on):
399
+ # Exception is not retryable, re-raise immediately
400
+ raise
401
+
402
+ # Check if we have more retries left
403
+ if attempt < self.max_retries:
404
+ # Calculate and apply backoff delay
405
+ delay = calculate_delay(
406
+ attempt,
407
+ backoff_factor=self.backoff_factor,
408
+ initial_delay=self.initial_delay,
409
+ max_delay=self.max_delay,
410
+ jitter=self.jitter,
411
+ )
412
+ if delay > 0:
413
+ await asyncio.sleep(delay)
414
+ # Continue to next retry
415
+ else:
416
+ # No more retries, handle failure
417
+ return self._handle_failure(tool_name, tool_call_id, exc, attempts_made)
418
+
419
+ # Unreachable: loop always returns via handler success or _handle_failure
420
+ msg = "Unexpected: retry loop completed without returning"
421
+ raise RuntimeError(msg)