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,251 @@
1
+ """Tool emulator middleware for testing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from typing import TYPE_CHECKING, Any, Generic
7
+
8
+ from coco_core.language_models.chat_models import BaseChatModel
9
+ from coco_core.messages import HumanMessage, ToolMessage
10
+
11
+ from coco.agents.middleware.internal_call_transformer import (
12
+ InternalCallTransformer,
13
+ internal_call_metadata,
14
+ )
15
+ from coco.agents.middleware.types import AgentMiddleware, AgentState, ContextT
16
+ from coco.chat_models.base import init_chat_model
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Awaitable, Callable
20
+
21
+ from coco_graph.types import Command
22
+
23
+ from coco.agents.middleware.types import ToolCallRequest
24
+ from coco.tools import BaseTool
25
+
26
+ _DEFAULT_EMULATOR_MODEL = "anthropic:claude-sonnet-4-5-20250929"
27
+
28
+
29
+ class LLMToolEmulator(AgentMiddleware[AgentState[Any], ContextT], Generic[ContextT]):
30
+ """Emulates specified tools using an LLM instead of executing them.
31
+
32
+ This middleware allows selective emulation of tools for testing purposes.
33
+
34
+ By default (when `tools=None`), all tools are emulated. You can specify which
35
+ tools to emulate by passing a list of tool names or `BaseTool` instances.
36
+
37
+ Examples:
38
+ !!! example "Emulate all tools (default behavior)"
39
+
40
+ ```python
41
+ from coco.agents.middleware import LLMToolEmulator
42
+
43
+ middleware = LLMToolEmulator()
44
+
45
+ agent = create_agent(
46
+ model="openai:gpt-5.5",
47
+ tools=[get_weather, get_user_location, calculator],
48
+ middleware=[middleware],
49
+ )
50
+ ```
51
+
52
+ !!! example "Emulate specific tools by name"
53
+
54
+ ```python
55
+ middleware = LLMToolEmulator(tools=["get_weather", "get_user_location"])
56
+ ```
57
+
58
+ !!! example "Use a custom model for emulation"
59
+
60
+ ```python
61
+ middleware = LLMToolEmulator(
62
+ tools=["get_weather"], model="anthropic:claude-sonnet-4-5-20250929"
63
+ )
64
+ ```
65
+
66
+ !!! example "Emulate specific tools by passing tool instances"
67
+
68
+ ```python
69
+ middleware = LLMToolEmulator(tools=[get_weather, get_user_location])
70
+ ```
71
+ """
72
+
73
+ transformers = (InternalCallTransformer,)
74
+ """Keeps the emulation model call's tokens out of `run.messages`.
75
+
76
+ Registered only when this middleware is used — see
77
+ `InternalCallTransformer` for why the call needs tagging and filtering.
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ *,
83
+ tools: list[str | BaseTool] | None = None,
84
+ model: str | BaseChatModel | None = None,
85
+ ) -> None:
86
+ """Initialize the tool emulator.
87
+
88
+ Args:
89
+ tools: List of tool names (`str`) or `BaseTool` instances to emulate.
90
+
91
+ If `None`, ALL tools will be emulated.
92
+
93
+ If empty list, no tools will be emulated.
94
+ model: Model to use for emulation.
95
+
96
+ Defaults to `'anthropic:claude-sonnet-4-5-20250929'`, which requires
97
+ `trustcoco-anthropic` to be installed.
98
+
99
+ !!! warning "Deprecated"
100
+ Relying on the implicit default is deprecated and will be
101
+ removed in a future release, since it makes this middleware
102
+ depend on `trustcoco-anthropic` even when unspecified. Pass
103
+ `model` explicitly instead.
104
+
105
+ Can be a model identifier string or `BaseChatModel` instance.
106
+ """
107
+ super().__init__()
108
+
109
+ # Extract tool names from tools
110
+ # None means emulate all tools
111
+ self.emulate_all = tools is None
112
+ self.tools_to_emulate: set[str] = set()
113
+
114
+ if tools is not None:
115
+ for tool in tools:
116
+ if isinstance(tool, str):
117
+ self.tools_to_emulate.add(tool)
118
+ else:
119
+ # Assume BaseTool with .name attribute
120
+ self.tools_to_emulate.add(tool.name)
121
+
122
+ # Initialize emulator model
123
+ if model is None:
124
+ warnings.warn(
125
+ "LLMToolEmulator's default model "
126
+ f"({_DEFAULT_EMULATOR_MODEL!r}) is deprecated and will be removed "
127
+ "in a future release. Pass `model` explicitly instead.",
128
+ DeprecationWarning,
129
+ stacklevel=2,
130
+ )
131
+ try:
132
+ self.model = init_chat_model(_DEFAULT_EMULATOR_MODEL, temperature=1)
133
+ except ImportError as e:
134
+ msg = (
135
+ "LLMToolEmulator's default model requires `trustcoco-anthropic` "
136
+ "to be installed. Install it with `pip install trustcoco-anthropic`, "
137
+ "or pass `model=...` explicitly to use a different provider."
138
+ )
139
+ raise ImportError(msg) from e
140
+ elif isinstance(model, BaseChatModel):
141
+ self.model = model
142
+ else:
143
+ self.model = init_chat_model(model, temperature=1)
144
+
145
+ def wrap_tool_call(
146
+ self,
147
+ request: ToolCallRequest,
148
+ handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
149
+ ) -> ToolMessage | Command[Any]:
150
+ """Emulate tool execution using LLM if tool should be emulated.
151
+
152
+ Args:
153
+ request: Tool call request to potentially emulate.
154
+ handler: Callback to execute the tool (can be called multiple times).
155
+
156
+ Returns:
157
+ ToolMessage with emulated response if tool should be emulated,
158
+ otherwise calls handler for normal execution.
159
+ """
160
+ tool_name = request.tool_call["name"]
161
+
162
+ # Check if this tool should be emulated
163
+ should_emulate = self.emulate_all or tool_name in self.tools_to_emulate
164
+
165
+ if not should_emulate:
166
+ # Let it execute normally by calling the handler
167
+ return handler(request)
168
+
169
+ # Extract tool information for emulation
170
+ tool_args = request.tool_call["args"]
171
+ tool_description = request.tool.description if request.tool else "No description available"
172
+
173
+ # Build prompt for emulator LLM
174
+ prompt = (
175
+ f"You are emulating a tool call for testing purposes.\n\n"
176
+ f"Tool: {tool_name}\n"
177
+ f"Description: {tool_description}\n"
178
+ f"Arguments: {tool_args}\n\n"
179
+ f"Generate a realistic response that this tool would return "
180
+ f"given these arguments.\n"
181
+ f"Return ONLY the tool's output, no explanation or preamble. "
182
+ f"Introduce variation into your responses."
183
+ )
184
+
185
+ # Get emulated response from LLM
186
+ response = self.model.invoke(
187
+ [HumanMessage(prompt)],
188
+ config={"metadata": {"lc_source": "tool_emulation", **internal_call_metadata()}},
189
+ )
190
+
191
+ # Short-circuit: return emulated result without executing real tool
192
+ return ToolMessage(
193
+ content=response.content,
194
+ tool_call_id=request.tool_call["id"],
195
+ name=tool_name,
196
+ )
197
+
198
+ async def awrap_tool_call(
199
+ self,
200
+ request: ToolCallRequest,
201
+ handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
202
+ ) -> ToolMessage | Command[Any]:
203
+ """Async version of `wrap_tool_call`.
204
+
205
+ Emulate tool execution using LLM if tool should be emulated.
206
+
207
+ Args:
208
+ request: Tool call request to potentially emulate.
209
+ handler: Async callback to execute the tool (can be called multiple times).
210
+
211
+ Returns:
212
+ ToolMessage with emulated response if tool should be emulated,
213
+ otherwise calls handler for normal execution.
214
+ """
215
+ tool_name = request.tool_call["name"]
216
+
217
+ # Check if this tool should be emulated
218
+ should_emulate = self.emulate_all or tool_name in self.tools_to_emulate
219
+
220
+ if not should_emulate:
221
+ # Let it execute normally by calling the handler
222
+ return await handler(request)
223
+
224
+ # Extract tool information for emulation
225
+ tool_args = request.tool_call["args"]
226
+ tool_description = request.tool.description if request.tool else "No description available"
227
+
228
+ # Build prompt for emulator LLM
229
+ prompt = (
230
+ f"You are emulating a tool call for testing purposes.\n\n"
231
+ f"Tool: {tool_name}\n"
232
+ f"Description: {tool_description}\n"
233
+ f"Arguments: {tool_args}\n\n"
234
+ f"Generate a realistic response that this tool would return "
235
+ f"given these arguments.\n"
236
+ f"Return ONLY the tool's output, no explanation or preamble. "
237
+ f"Introduce variation into your responses."
238
+ )
239
+
240
+ # Get emulated response from LLM (using async invoke)
241
+ response = await self.model.ainvoke(
242
+ [HumanMessage(prompt)],
243
+ config={"metadata": {"lc_source": "tool_emulation", **internal_call_metadata()}},
244
+ )
245
+
246
+ # Short-circuit: return emulated result without executing real tool
247
+ return ToolMessage(
248
+ content=response.content,
249
+ tool_call_id=request.tool_call["id"],
250
+ name=tool_name,
251
+ )
@@ -0,0 +1,209 @@
1
+ """Tool error middleware for agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, cast
6
+
7
+ from coco_core.messages import ToolMessage
8
+ from coco_graph.errors import GraphBubbleUp
9
+
10
+ from coco.agents.middleware.types import (
11
+ AgentMiddleware,
12
+ AgentState,
13
+ ContextT,
14
+ ResponseT,
15
+ TracePolicy,
16
+ omit_payload,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Awaitable, Callable
21
+
22
+ from coco_core.messages import ContentBlock
23
+ from coco_graph.types import Command
24
+
25
+ from coco.agents.middleware.types import ToolCallRequest
26
+ from coco.tools import BaseTool
27
+
28
+ OnError = Callable[[Exception, ToolCallRequest], str | list[ContentBlock] | None]
29
+ """Sync handler: return content to surface the error as a `ToolMessage`; return
30
+ `None` (or nothing) to let the exception propagate."""
31
+
32
+ AOnError = Callable[[Exception, ToolCallRequest], Awaitable[str | list[ContentBlock] | None]]
33
+ """Async handler: return content to surface the error as a `ToolMessage`; return
34
+ `None` (or nothing) to let the exception propagate."""
35
+
36
+
37
+ class ToolErrorMiddleware(AgentMiddleware[AgentState[ResponseT], ContextT, ResponseT]):
38
+ """Return selected tool-execution exceptions to the model as error `ToolMessage`s.
39
+
40
+ `on_error` is called for each exception raised by tool execution. Return content
41
+ (a `str` or a list of content blocks) to convert the exception into a
42
+ `ToolMessage(status="error")`; return `None` — or simply don't return — to let the
43
+ exception propagate (halting the run). Handling is therefore opt-in — exceptions you
44
+ do not return content for propagate unchanged, so arbitrary internal exceptions are
45
+ never serialized to the model or end user unless you choose to surface them.
46
+
47
+ CocoGraph control-flow signals (interrupts, parent commands) always propagate and
48
+ never reach `on_error`.
49
+
50
+ Prefer returning content that names the exception type over the raw exception message,
51
+ which may carry sensitive or internal detail.
52
+
53
+ Provide at least one of `on_error` or `aon_error`. `aon_error` handles errors on the
54
+ async execution path (falling back to `on_error` when omitted); the sync path only
55
+ ever calls `on_error`. For async-only usage, pass `aon_error` alone — running such a
56
+ middleware on the sync path raises, since the async handler cannot be awaited there.
57
+
58
+ This middleware does not retry. For retries, compose with `ToolRetryMiddleware`
59
+ placed *inner* and configured with `on_failure="error"` so exceptions reach this
60
+ middleware.
61
+
62
+ This middleware only sees exceptions raised by tool *execution*. Argument-binding
63
+ and validation errors are handled upstream by `ToolNode` (converted to an error
64
+ `ToolMessage` before the tool runs), so they do not reach `on_error`.
65
+
66
+ Example:
67
+ ```python
68
+ from coco.agents import create_agent
69
+ from coco.agents.middleware import ToolErrorMiddleware
70
+
71
+
72
+ def on_error(exc: Exception, request: ToolCallRequest) -> str | None:
73
+ if isinstance(exc, ValueError):
74
+ return f"`{request.tool_call['name']}` failed; fix the input and retry."
75
+ return None # propagate everything else
76
+
77
+
78
+ agent = create_agent(model, tools=[...], middleware=[ToolErrorMiddleware(on_error)])
79
+ ```
80
+ """
81
+
82
+ trace_policy = TracePolicy(process_inputs=omit_payload)
83
+
84
+ def __init__(
85
+ self,
86
+ on_error: OnError | None = None,
87
+ *,
88
+ aon_error: AOnError | None = None,
89
+ tools: list[BaseTool | str] | None = None,
90
+ ) -> None:
91
+ """Initialize `ToolErrorMiddleware`.
92
+
93
+ Args:
94
+ on_error: Handler called for each exception raised by tool execution. Return
95
+ content (`str` or list of content blocks) to convert the exception into an
96
+ error `ToolMessage`. Return `None` — or simply don't return — to let the
97
+ exception propagate. Falling through without a return therefore re-raises,
98
+ so handle only the exceptions you mean to. Receives the exception and the
99
+ tool call request (tool name, args, call id). Used on the sync path and,
100
+ unless `aon_error` is given, on the async path.
101
+ aon_error: Optional async handler, used on the async execution path. Falls back
102
+ to `on_error` when not provided.
103
+ tools: Optional list of tools or tool names to apply handling to. If `None`,
104
+ applies to all tools.
105
+
106
+ Raises:
107
+ ValueError: If neither `on_error` nor `aon_error` is provided.
108
+ """
109
+ super().__init__()
110
+
111
+ if on_error is None and aon_error is None:
112
+ msg = "ToolErrorMiddleware requires `on_error` and/or `aon_error`."
113
+ raise ValueError(msg)
114
+
115
+ self.on_error = on_error
116
+ self.aon_error = aon_error
117
+
118
+ # Extract tool names from BaseTool instances or strings
119
+ self._tool_filter: list[str] | None
120
+ if tools is not None:
121
+ self._tool_filter = [tool.name if not isinstance(tool, str) else tool for tool in tools]
122
+ else:
123
+ self._tool_filter = None
124
+
125
+ self.tools = [] # No additional tools registered by this middleware
126
+
127
+ def _should_handle_tool(self, tool_name: str) -> bool:
128
+ """Check if error handling should apply to this tool."""
129
+ if self._tool_filter is None:
130
+ return True
131
+ return tool_name in self._tool_filter
132
+
133
+ def wrap_tool_call(
134
+ self,
135
+ request: ToolCallRequest,
136
+ handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
137
+ ) -> ToolMessage | Command[Any]:
138
+ """Intercept tool execution and convert handled exceptions to error messages.
139
+
140
+ Args:
141
+ request: Tool call request with call dict, `BaseTool`, state, and runtime.
142
+ handler: Callable to execute the tool.
143
+
144
+ Returns:
145
+ `ToolMessage` or `Command` (the final result).
146
+ """
147
+ tool_name = request.tool.name if request.tool else request.tool_call["name"]
148
+
149
+ if not self._should_handle_tool(tool_name):
150
+ return handler(request)
151
+
152
+ try:
153
+ return handler(request)
154
+ except GraphBubbleUp:
155
+ # Control-flow signals (interrupts, parent commands) must propagate.
156
+ raise
157
+ except Exception as exc:
158
+ if self.on_error is None:
159
+ # Async-only config (aon_error) cannot be awaited on the sync path.
160
+ msg = (
161
+ "ToolErrorMiddleware has no sync `on_error`; run async "
162
+ "(ainvoke/astream) or provide `on_error`."
163
+ )
164
+ raise RuntimeError(msg) from exc
165
+ content = self.on_error(exc, request)
166
+ if content is None:
167
+ raise
168
+ return ToolMessage(
169
+ content=cast("str | list[str | dict[Any, Any]]", content),
170
+ tool_call_id=request.tool_call["id"],
171
+ name=tool_name,
172
+ status="error",
173
+ )
174
+
175
+ async def awrap_tool_call(
176
+ self,
177
+ request: ToolCallRequest,
178
+ handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
179
+ ) -> ToolMessage | Command[Any]:
180
+ """Async version of `wrap_tool_call`.
181
+
182
+ Uses `aon_error` if provided, otherwise the sync `on_error`. The sync path never
183
+ awaits.
184
+ """
185
+ tool_name = request.tool.name if request.tool else request.tool_call["name"]
186
+
187
+ if not self._should_handle_tool(tool_name):
188
+ return await handler(request)
189
+
190
+ try:
191
+ return await handler(request)
192
+ except GraphBubbleUp:
193
+ # Control-flow signals (interrupts, parent commands) must propagate.
194
+ raise
195
+ except Exception as exc:
196
+ if self.aon_error is not None:
197
+ content = await self.aon_error(exc, request)
198
+ elif self.on_error is not None:
199
+ content = self.on_error(exc, request)
200
+ else: # pragma: no cover - __init__ guarantees at least one handler
201
+ raise
202
+ if content is None:
203
+ raise
204
+ return ToolMessage(
205
+ content=cast("str | list[str | dict[Any, Any]]", content),
206
+ tool_call_id=request.tool_call["id"],
207
+ name=tool_name,
208
+ status="error",
209
+ )