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,495 @@
1
+ """Tool call limit middleware for agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Annotated, Any, Literal
6
+
7
+ from coco_core.messages import AIMessage, ToolCall, ToolMessage
8
+ from coco_graph.channels.untracked_value import UntrackedValue
9
+ from coco_graph.typing import ContextT
10
+ from typing_extensions import NotRequired, override
11
+
12
+ from coco.agents.middleware.types import (
13
+ AgentMiddleware,
14
+ AgentState,
15
+ PrivateStateAttr,
16
+ ResponseT,
17
+ hook_config,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from coco_graph.runtime import Runtime
22
+
23
+ ExitBehavior = Literal["continue", "error", "end"]
24
+ """How to handle execution when tool call limits are exceeded.
25
+
26
+ - `'continue'`: Block exceeded tools with error messages, let other tools continue
27
+ (default)
28
+ - `'error'`: Raise a `ToolCallLimitExceededError` exception
29
+ - `'end'`: Stop execution immediately, injecting a `ToolMessage` for each tool call
30
+ that exceeded the limit and an `AIMessage` explaining why. Any other pending tool
31
+ call on the same `AIMessage` (e.g. from parallel tool calling) also gets an
32
+ explanatory `ToolMessage` instead of being executed.
33
+ """
34
+
35
+
36
+ class ToolCallLimitState(AgentState[ResponseT]):
37
+ """State schema for `ToolCallLimitMiddleware`.
38
+
39
+ Extends `AgentState` with tool call tracking fields.
40
+
41
+ The count fields are dictionaries mapping tool names to execution counts. This
42
+ allows multiple middleware instances to track different tools independently. The
43
+ special key `'__all__'` is used for tracking all tool calls globally.
44
+
45
+ Type Parameters:
46
+ ResponseT: The type of the structured response. Defaults to `Any`.
47
+ """
48
+
49
+ thread_tool_call_count: NotRequired[Annotated[dict[str, int], PrivateStateAttr]]
50
+ run_tool_call_count: NotRequired[Annotated[dict[str, int], UntrackedValue, PrivateStateAttr]]
51
+
52
+
53
+ def _build_tool_message_content(tool_name: str | None) -> str:
54
+ """Build the error message content for `ToolMessage` when limit is exceeded.
55
+
56
+ This message is sent to the model, so it should not reference thread/run concepts
57
+ that the model has no notion of.
58
+
59
+ Args:
60
+ tool_name: Tool name being limited (if specific tool), or `None` for all tools.
61
+
62
+ Returns:
63
+ A concise message instructing the model not to call the tool again.
64
+ """
65
+ # Always instruct the model not to call again, regardless of which limit was hit
66
+ if tool_name:
67
+ return f"Tool call limit exceeded. Do not call '{tool_name}' again."
68
+ return "Tool call limit exceeded. Do not make additional tool calls."
69
+
70
+
71
+ def _build_final_ai_message_content(
72
+ thread_count: int,
73
+ run_count: int,
74
+ thread_limit: int | None,
75
+ run_limit: int | None,
76
+ tool_name: str | None,
77
+ ) -> str:
78
+ """Build the final AI message content for `'end'` behavior.
79
+
80
+ This message is displayed to the user, so it should include detailed information
81
+ about which limits were exceeded.
82
+
83
+ Args:
84
+ thread_count: Current thread tool call count.
85
+ run_count: Current run tool call count.
86
+ thread_limit: Thread tool call limit (if set).
87
+ run_limit: Run tool call limit (if set).
88
+ tool_name: Tool name being limited (if specific tool), or `None` for all tools.
89
+
90
+ Returns:
91
+ A formatted message describing which limits were exceeded.
92
+ """
93
+ tool_desc = f"'{tool_name}' tool" if tool_name else "Tool"
94
+ exceeded_limits = []
95
+
96
+ if thread_limit is not None and thread_count > thread_limit:
97
+ exceeded_limits.append(f"thread limit exceeded ({thread_count}/{thread_limit} calls)")
98
+ if run_limit is not None and run_count > run_limit:
99
+ exceeded_limits.append(f"run limit exceeded ({run_count}/{run_limit} calls)")
100
+
101
+ limits_text = " and ".join(exceeded_limits)
102
+ return f"{tool_desc} call limit reached: {limits_text}."
103
+
104
+
105
+ class ToolCallLimitExceededError(Exception):
106
+ """Exception raised when tool call limits are exceeded.
107
+
108
+ This exception is raised when the configured exit behavior is `'error'` and either
109
+ the thread or run tool call limit has been exceeded.
110
+ """
111
+
112
+ def __init__(
113
+ self,
114
+ thread_count: int,
115
+ run_count: int,
116
+ thread_limit: int | None,
117
+ run_limit: int | None,
118
+ tool_name: str | None = None,
119
+ ) -> None:
120
+ """Initialize the exception with call count information.
121
+
122
+ Args:
123
+ thread_count: Current thread tool call count.
124
+ run_count: Current run tool call count.
125
+ thread_limit: Thread tool call limit (if set).
126
+ run_limit: Run tool call limit (if set).
127
+ tool_name: Tool name being limited (if specific tool), or None for all tools.
128
+ """
129
+ self.thread_count = thread_count
130
+ self.run_count = run_count
131
+ self.thread_limit = thread_limit
132
+ self.run_limit = run_limit
133
+ self.tool_name = tool_name
134
+
135
+ msg = _build_final_ai_message_content(
136
+ thread_count, run_count, thread_limit, run_limit, tool_name
137
+ )
138
+ super().__init__(msg)
139
+
140
+
141
+ class ToolCallLimitMiddleware(AgentMiddleware[ToolCallLimitState[ResponseT], ContextT, ResponseT]):
142
+ """Track tool call counts and enforces limits during agent execution.
143
+
144
+ This middleware monitors the number of tool calls made and can terminate or
145
+ restrict execution when limits are exceeded. It supports both thread-level
146
+ (persistent across runs) and run-level (per invocation) call counting.
147
+
148
+ Configuration:
149
+ - `exit_behavior`: How to handle when limits are exceeded
150
+ - `'continue'`: Block exceeded tools, let execution continue (default)
151
+ - `'error'`: Raise an exception
152
+ - `'end'`: Stop immediately with a `ToolMessage` for each exceeded tool
153
+ call, a `ToolMessage` explaining any other pending calls were skipped,
154
+ and an AI message explaining why execution stopped
155
+
156
+ Examples:
157
+ !!! example "Continue execution with blocked tools (default)"
158
+
159
+ ```python
160
+ from coco.agents.middleware.tool_call_limit import ToolCallLimitMiddleware
161
+ from coco.agents import create_agent
162
+
163
+ # Block exceeded tools but let other tools and model continue
164
+ limiter = ToolCallLimitMiddleware(
165
+ thread_limit=20,
166
+ run_limit=10,
167
+ exit_behavior="continue", # default
168
+ )
169
+
170
+ agent = create_agent("openai:gpt-5.5", middleware=[limiter])
171
+ ```
172
+
173
+ !!! example "Stop immediately when limit exceeded"
174
+
175
+ ```python
176
+ # End execution immediately with an AI message
177
+ limiter = ToolCallLimitMiddleware(run_limit=5, exit_behavior="end")
178
+
179
+ agent = create_agent("openai:gpt-5.5", middleware=[limiter])
180
+ ```
181
+
182
+ !!! example "Raise exception on limit"
183
+
184
+ ```python
185
+ # Strict limit with exception handling
186
+ limiter = ToolCallLimitMiddleware(
187
+ tool_name="search", thread_limit=5, exit_behavior="error"
188
+ )
189
+
190
+ agent = create_agent("openai:gpt-5.5", middleware=[limiter])
191
+
192
+ try:
193
+ result = await agent.invoke({"messages": [HumanMessage("Task")]})
194
+ except ToolCallLimitExceededError as e:
195
+ print(f"Search limit exceeded: {e}")
196
+ ```
197
+
198
+ """
199
+
200
+ state_schema = ToolCallLimitState # type: ignore[assignment]
201
+
202
+ def __init__(
203
+ self,
204
+ *,
205
+ tool_name: str | None = None,
206
+ thread_limit: int | None = None,
207
+ run_limit: int | None = None,
208
+ exit_behavior: ExitBehavior = "continue",
209
+ ) -> None:
210
+ """Initialize the tool call limit middleware.
211
+
212
+ Args:
213
+ tool_name: Name of the specific tool to limit. If `None`, limits apply
214
+ to all tools.
215
+ thread_limit: Maximum number of tool calls allowed per thread.
216
+ `None` means no limit.
217
+ run_limit: Maximum number of tool calls allowed per run.
218
+ `None` means no limit.
219
+ exit_behavior: How to handle when limits are exceeded.
220
+
221
+ - `'continue'`: Block exceeded tools with error messages, let other
222
+ tools continue. Model decides when to end.
223
+ - `'error'`: Raise a `ToolCallLimitExceededError` exception
224
+ - `'end'`: Stop execution immediately with a `ToolMessage` for each
225
+ tool call that exceeded the limit and an AI message explaining
226
+ why. Any other pending tool call on the same `AIMessage` (e.g.
227
+ from parallel tool calling) is given an explanatory
228
+ `ToolMessage` instead of being executed.
229
+
230
+ Raises:
231
+ ValueError: If both limits are `None`, if `exit_behavior` is invalid,
232
+ or if `run_limit` exceeds `thread_limit`.
233
+ """
234
+ super().__init__()
235
+
236
+ if thread_limit is None and run_limit is None:
237
+ msg = "At least one limit must be specified (thread_limit or run_limit)"
238
+ raise ValueError(msg)
239
+
240
+ valid_behaviors = ("continue", "error", "end")
241
+ if exit_behavior not in valid_behaviors:
242
+ msg = f"Invalid exit_behavior: {exit_behavior!r}. Must be one of {valid_behaviors}"
243
+ raise ValueError(msg)
244
+
245
+ if thread_limit is not None and run_limit is not None and run_limit > thread_limit:
246
+ msg = (
247
+ f"run_limit ({run_limit}) cannot exceed thread_limit ({thread_limit}). "
248
+ "The run limit should be less than or equal to the thread limit."
249
+ )
250
+ raise ValueError(msg)
251
+
252
+ self.tool_name = tool_name
253
+ self.thread_limit = thread_limit
254
+ self.run_limit = run_limit
255
+ self.exit_behavior = exit_behavior
256
+
257
+ @property
258
+ def name(self) -> str:
259
+ """The name of the middleware instance.
260
+
261
+ Includes the tool name if specified to allow multiple instances
262
+ of this middleware with different tool names.
263
+ """
264
+ base_name = self.__class__.__name__
265
+ if self.tool_name:
266
+ return f"{base_name}[{self.tool_name}]"
267
+ return base_name
268
+
269
+ def _would_exceed_limit(self, thread_count: int, run_count: int) -> bool:
270
+ """Check if incrementing the counts would exceed any configured limit.
271
+
272
+ Args:
273
+ thread_count: Current thread call count.
274
+ run_count: Current run call count.
275
+
276
+ Returns:
277
+ True if either limit would be exceeded by one more call.
278
+ """
279
+ return (self.thread_limit is not None and thread_count + 1 > self.thread_limit) or (
280
+ self.run_limit is not None and run_count + 1 > self.run_limit
281
+ )
282
+
283
+ def _matches_tool_filter(self, tool_call: ToolCall) -> bool:
284
+ """Check if a tool call matches this middleware's tool filter.
285
+
286
+ Args:
287
+ tool_call: The tool call to check.
288
+
289
+ Returns:
290
+ True if this middleware should track this tool call.
291
+ """
292
+ return self.tool_name is None or tool_call["name"] == self.tool_name
293
+
294
+ def _separate_tool_calls(
295
+ self, tool_calls: list[ToolCall], thread_count: int, run_count: int
296
+ ) -> tuple[list[ToolCall], list[ToolCall], int, int]:
297
+ """Separate tool calls into allowed and blocked based on limits.
298
+
299
+ Args:
300
+ tool_calls: List of tool calls to evaluate.
301
+ thread_count: Current thread call count.
302
+ run_count: Current run call count.
303
+
304
+ Returns:
305
+ Tuple of `(allowed_calls, blocked_calls, final_thread_count, final_run_count)`.
306
+ """
307
+ allowed_calls: list[ToolCall] = []
308
+ blocked_calls: list[ToolCall] = []
309
+ temp_thread_count = thread_count
310
+ temp_run_count = run_count
311
+
312
+ for tool_call in tool_calls:
313
+ if not self._matches_tool_filter(tool_call):
314
+ continue
315
+
316
+ if self._would_exceed_limit(temp_thread_count, temp_run_count):
317
+ blocked_calls.append(tool_call)
318
+ else:
319
+ allowed_calls.append(tool_call)
320
+ temp_thread_count += 1
321
+ temp_run_count += 1
322
+
323
+ return allowed_calls, blocked_calls, temp_thread_count, temp_run_count
324
+
325
+ @hook_config(can_jump_to=["end"])
326
+ @override
327
+ def after_model(
328
+ self,
329
+ state: ToolCallLimitState[ResponseT],
330
+ runtime: Runtime[ContextT],
331
+ ) -> dict[str, Any] | None:
332
+ """Increment tool call counts after a model call and check limits.
333
+
334
+ Args:
335
+ state: The current agent state.
336
+ runtime: The coco_graph runtime.
337
+
338
+ Returns:
339
+ State updates with incremented tool call counts. If limits are exceeded
340
+ and exit_behavior is `'end'`, also includes a jump to end with a
341
+ `ToolMessage` for each exceeded tool call, an explanatory
342
+ `ToolMessage` for any other pending tool calls, and an AI message.
343
+
344
+ Raises:
345
+ ToolCallLimitExceededError: If limits are exceeded and `exit_behavior`
346
+ is `'error'`.
347
+ """
348
+ # Get the last AIMessage to check for tool calls
349
+ messages = state.get("messages", [])
350
+ if not messages:
351
+ return None
352
+
353
+ # Find the last AIMessage
354
+ last_ai_message = None
355
+ for message in reversed(messages):
356
+ if isinstance(message, AIMessage):
357
+ last_ai_message = message
358
+ break
359
+
360
+ if not last_ai_message or not last_ai_message.tool_calls:
361
+ return None
362
+
363
+ # Get the count key for this middleware instance
364
+ count_key = self.tool_name or "__all__"
365
+
366
+ # Get current counts
367
+ thread_counts = state.get("thread_tool_call_count", {}).copy()
368
+ run_counts = state.get("run_tool_call_count", {}).copy()
369
+ current_thread_count = thread_counts.get(count_key, 0)
370
+ current_run_count = run_counts.get(count_key, 0)
371
+
372
+ # Separate tool calls into allowed and blocked
373
+ allowed_calls, blocked_calls, new_thread_count, new_run_count = self._separate_tool_calls(
374
+ last_ai_message.tool_calls, current_thread_count, current_run_count
375
+ )
376
+
377
+ # Update counts to include only allowed calls for thread count
378
+ # (blocked calls don't count towards thread-level tracking)
379
+ # But run count includes blocked calls since they were attempted in this run
380
+ thread_counts[count_key] = new_thread_count
381
+ run_counts[count_key] = new_run_count + len(blocked_calls)
382
+
383
+ # If no tool calls are blocked, just update counts
384
+ if not blocked_calls:
385
+ if allowed_calls:
386
+ return {
387
+ "thread_tool_call_count": thread_counts,
388
+ "run_tool_call_count": run_counts,
389
+ }
390
+ return None
391
+
392
+ # Get final counts for building messages
393
+ final_thread_count = thread_counts[count_key]
394
+ final_run_count = run_counts[count_key]
395
+
396
+ # Handle different exit behaviors
397
+ if self.exit_behavior == "error":
398
+ # Use hypothetical thread count to show which limit was exceeded
399
+ hypothetical_thread_count = final_thread_count + len(blocked_calls)
400
+ raise ToolCallLimitExceededError(
401
+ thread_count=hypothetical_thread_count,
402
+ run_count=final_run_count,
403
+ thread_limit=self.thread_limit,
404
+ run_limit=self.run_limit,
405
+ tool_name=self.tool_name,
406
+ )
407
+
408
+ # Build tool message content (sent to model - no thread/run details)
409
+ tool_msg_content = _build_tool_message_content(self.tool_name)
410
+
411
+ # Inject artificial error ToolMessages for blocked tool calls
412
+ artificial_messages: list[ToolMessage | AIMessage] = [
413
+ ToolMessage(
414
+ content=tool_msg_content,
415
+ tool_call_id=tool_call["id"],
416
+ name=tool_call.get("name"),
417
+ status="error",
418
+ )
419
+ for tool_call in blocked_calls
420
+ ]
421
+
422
+ if self.exit_behavior == "end":
423
+ # Since jumping to `"end"` skips tool execution, add `ToolMessage`s for all
424
+ # pending calls so none are executed and the message history stays valid.
425
+ blocked_ids = {tool_call["id"] for tool_call in blocked_calls}
426
+ pending_tool_calls = [
427
+ tc for tc in last_ai_message.tool_calls if tc["id"] not in blocked_ids
428
+ ]
429
+ artificial_messages.extend(
430
+ ToolMessage(
431
+ content=(
432
+ "Execution stopped before this tool call could run because "
433
+ "another tool call in the same batch exceeded the limit."
434
+ ),
435
+ tool_call_id=tool_call["id"],
436
+ name=tool_call.get("name"),
437
+ status="error",
438
+ )
439
+ for tool_call in pending_tool_calls
440
+ )
441
+
442
+ # Build final AI message content (displayed to user - includes thread/run details)
443
+ # Use hypothetical thread count (what it would have been if call wasn't blocked)
444
+ # to show which limit was actually exceeded
445
+ hypothetical_thread_count = final_thread_count + len(blocked_calls)
446
+ final_msg_content = _build_final_ai_message_content(
447
+ hypothetical_thread_count,
448
+ final_run_count,
449
+ self.thread_limit,
450
+ self.run_limit,
451
+ self.tool_name,
452
+ )
453
+ artificial_messages.append(AIMessage(content=final_msg_content))
454
+
455
+ # Since no calls execute after jumping to `"end"`, don't include this batch in
456
+ # the thread-level count. Only the run-level count tracks attempted calls.
457
+ thread_counts[count_key] = current_thread_count
458
+
459
+ return {
460
+ "thread_tool_call_count": thread_counts,
461
+ "run_tool_call_count": run_counts,
462
+ "jump_to": "end",
463
+ "messages": artificial_messages,
464
+ }
465
+
466
+ # For exit_behavior="continue", return error messages to block exceeded tools
467
+ return {
468
+ "thread_tool_call_count": thread_counts,
469
+ "run_tool_call_count": run_counts,
470
+ "messages": artificial_messages,
471
+ }
472
+
473
+ @hook_config(can_jump_to=["end"])
474
+ async def aafter_model(
475
+ self,
476
+ state: ToolCallLimitState[ResponseT],
477
+ runtime: Runtime[ContextT],
478
+ ) -> dict[str, Any] | None:
479
+ """Async increment tool call counts after a model call and check limits.
480
+
481
+ Args:
482
+ state: The current agent state.
483
+ runtime: The coco_graph runtime.
484
+
485
+ Returns:
486
+ State updates with incremented tool call counts. If limits are exceeded
487
+ and exit_behavior is `'end'`, also includes a jump to end with a
488
+ `ToolMessage` for each exceeded tool call, an explanatory
489
+ `ToolMessage` for any other pending tool calls, and an AI message.
490
+
491
+ Raises:
492
+ ToolCallLimitExceededError: If limits are exceeded and `exit_behavior`
493
+ is `'error'`.
494
+ """
495
+ return self.after_model(state, runtime)