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,433 @@
1
+ """File search middleware for Anthropic text editor and memory tools.
2
+
3
+ This module provides Glob and Grep search tools that operate on files stored
4
+ in state or filesystem.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import fnmatch
10
+ import json
11
+ import operator
12
+ import os
13
+ import re
14
+ import subprocess
15
+ from contextlib import suppress
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+ from typing import Literal
19
+
20
+ from coco_core.tools import tool
21
+
22
+ from coco.agents.middleware.types import AgentMiddleware, AgentState, ContextT, ResponseT
23
+
24
+
25
+ def _is_within_root(candidate: Path, root: Path) -> bool:
26
+ """Return True iff `candidate` resolves to a path inside `root` (symlinks resolved).
27
+
28
+ Args:
29
+ candidate: The path to check. It is resolved (following symlinks and
30
+ normalizing `..`) before the containment check.
31
+ root: The allowed root directory. Also resolved before comparison.
32
+
33
+ Returns:
34
+ `True` if the fully resolved `candidate` lies inside the resolved `root`,
35
+ using path-segment boundaries; `False` otherwise (including on resolution
36
+ errors).
37
+ """
38
+ try:
39
+ return candidate.resolve().is_relative_to(root.resolve())
40
+ except (OSError, ValueError):
41
+ return False
42
+
43
+
44
+ def _expand_include_patterns(pattern: str) -> list[str] | None:
45
+ """Expand brace patterns like `*.{py,pyi}` into a list of globs."""
46
+ if "}" in pattern and "{" not in pattern:
47
+ return None
48
+
49
+ expanded: list[str] = []
50
+
51
+ def _expand(current: str) -> None:
52
+ start = current.find("{")
53
+ if start == -1:
54
+ expanded.append(current)
55
+ return
56
+
57
+ end = current.find("}", start)
58
+ if end == -1:
59
+ raise ValueError
60
+
61
+ prefix = current[:start]
62
+ suffix = current[end + 1 :]
63
+ inner = current[start + 1 : end]
64
+ if not inner:
65
+ raise ValueError
66
+
67
+ for option in inner.split(","):
68
+ _expand(prefix + option + suffix)
69
+
70
+ try:
71
+ _expand(pattern)
72
+ except ValueError:
73
+ return None
74
+
75
+ return expanded
76
+
77
+
78
+ def _is_valid_include_pattern(pattern: str) -> bool:
79
+ """Validate glob pattern used for include filters."""
80
+ if not pattern:
81
+ return False
82
+
83
+ if any(char in pattern for char in ("\x00", "\n", "\r")):
84
+ return False
85
+
86
+ expanded = _expand_include_patterns(pattern)
87
+ if expanded is None:
88
+ return False
89
+
90
+ try:
91
+ for candidate in expanded:
92
+ re.compile(fnmatch.translate(candidate))
93
+ except re.error:
94
+ return False
95
+
96
+ return True
97
+
98
+
99
+ def _match_include_pattern(basename: str, pattern: str) -> bool:
100
+ """Return True if the basename matches the include pattern."""
101
+ expanded = _expand_include_patterns(pattern)
102
+ if not expanded:
103
+ return False
104
+
105
+ return any(fnmatch.fnmatch(basename, candidate) for candidate in expanded)
106
+
107
+
108
+ class FilesystemFileSearchMiddleware(AgentMiddleware[AgentState[ResponseT], ContextT, ResponseT]):
109
+ """Provides Glob and Grep search over filesystem files.
110
+
111
+ This middleware adds two tools that search through local filesystem:
112
+
113
+ - Glob: Fast file pattern matching by file path
114
+ - Grep: Fast content search using ripgrep or Python fallback
115
+
116
+ Example:
117
+ ```python
118
+ from coco.agents import create_agent
119
+ from coco.agents.middleware import (
120
+ FilesystemFileSearchMiddleware,
121
+ )
122
+
123
+ agent = create_agent(
124
+ model=model,
125
+ tools=[], # Add tools as needed
126
+ middleware=[
127
+ FilesystemFileSearchMiddleware(root_path="/workspace"),
128
+ ],
129
+ )
130
+ ```
131
+ """
132
+
133
+ def __init__(
134
+ self,
135
+ *,
136
+ root_path: str,
137
+ use_ripgrep: bool = True,
138
+ max_file_size_mb: int = 10,
139
+ ) -> None:
140
+ """Initialize the search middleware.
141
+
142
+ Args:
143
+ root_path: Root directory to search.
144
+ use_ripgrep: Whether to use `ripgrep` for search.
145
+
146
+ Falls back to Python if `ripgrep` unavailable.
147
+ max_file_size_mb: Maximum file size to search in MB.
148
+ """
149
+ self.root_path = Path(root_path).resolve()
150
+ self.use_ripgrep = use_ripgrep
151
+ self.max_file_size_bytes = max_file_size_mb * 1024 * 1024
152
+
153
+ # Create tool instances as closures that capture self
154
+ @tool
155
+ def glob_search(pattern: str, path: str = "/") -> str:
156
+ """Fast file pattern matching tool that works with any codebase size.
157
+
158
+ Supports glob patterns like `**/*.js` or `src/**/*.ts`.
159
+
160
+ Returns matching file paths sorted by modification time.
161
+
162
+ Use this tool when you need to find files by name patterns.
163
+
164
+ Args:
165
+ pattern: The glob pattern to match files against.
166
+ path: The directory to search in. If not specified, searches from root.
167
+
168
+ Returns:
169
+ Newline-separated list of matching file paths, sorted by modification
170
+ time (most recently modified first). Returns `'No files found'` if no
171
+ matches.
172
+ """
173
+ try:
174
+ base_full = self._validate_and_resolve_path(path)
175
+ except ValueError:
176
+ return "No files found"
177
+
178
+ if not base_full.exists() or not base_full.is_dir():
179
+ return "No files found"
180
+
181
+ # Reject glob patterns that could escape the root before expanding them.
182
+ # `Path.glob` expands a `..` pattern like "../../etc/passwd" against the
183
+ # base directory and yields paths outside the allowed root, and raises
184
+ # for absolute patterns. (A `~` pattern is never expanduser'd by glob,
185
+ # and any escape that slips through is dropped by the per-match
186
+ # containment check below.)
187
+ if pattern.startswith("/") or any(part == ".." for part in pattern.split("/")):
188
+ return "No files found"
189
+
190
+ # Use pathlib glob
191
+ matching: list[tuple[str, str]] = []
192
+ for match in base_full.glob(pattern):
193
+ # Re-check containment after resolving so an in-root symlink that
194
+ # points outside the root is never enumerated.
195
+ if match.is_file() and _is_within_root(match, self.root_path):
196
+ # Convert to virtual path
197
+ virtual_path = "/" + str(match.relative_to(self.root_path))
198
+ stat = match.stat()
199
+ modified_at = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat()
200
+ matching.append((virtual_path, modified_at))
201
+
202
+ if not matching:
203
+ return "No files found"
204
+
205
+ matching.sort(key=operator.itemgetter(1), reverse=True)
206
+ file_paths = [p for p, _ in matching]
207
+ return "\n".join(file_paths)
208
+
209
+ @tool
210
+ def grep_search(
211
+ pattern: str,
212
+ path: str = "/",
213
+ include: str | None = None,
214
+ output_mode: Literal["files_with_matches", "content", "count"] = "files_with_matches",
215
+ ) -> str:
216
+ """Fast content search tool that works with any codebase size.
217
+
218
+ Searches file contents using regular expressions. Supports full regex
219
+ syntax and filters files by pattern with the include parameter.
220
+
221
+ Args:
222
+ pattern: The regular expression pattern to search for in file contents.
223
+ path: The directory to search in. If not specified, searches from root.
224
+ include: File pattern to filter (e.g., `'*.js'`, `'*.{ts,tsx}'`).
225
+ output_mode: Output format:
226
+
227
+ - `'files_with_matches'`: Only file paths containing matches
228
+ - `'content'`: Matching lines with `file:line:content` format
229
+ - `'count'`: Count of matches per file
230
+
231
+ Returns:
232
+ Search results formatted according to `output_mode`.
233
+ Returns `'No matches found'` if no results.
234
+ """
235
+ # Compile regex pattern (for validation)
236
+ try:
237
+ re.compile(pattern)
238
+ except re.error as e:
239
+ return f"Invalid regex pattern: {e}"
240
+
241
+ if include and not _is_valid_include_pattern(include):
242
+ return "Invalid include pattern"
243
+
244
+ # Try ripgrep first if enabled
245
+ results = None
246
+ if self.use_ripgrep:
247
+ with suppress(
248
+ FileNotFoundError,
249
+ subprocess.CalledProcessError,
250
+ subprocess.TimeoutExpired,
251
+ ):
252
+ results = self._ripgrep_search(pattern, path, include)
253
+
254
+ # Python fallback if ripgrep failed or is disabled
255
+ if results is None:
256
+ results = self._python_search(pattern, path, include)
257
+
258
+ if not results:
259
+ return "No matches found"
260
+
261
+ # Format output based on mode
262
+ return self._format_grep_results(results, output_mode)
263
+
264
+ self.glob_search = glob_search
265
+ self.grep_search = grep_search
266
+ self.tools = [glob_search, grep_search]
267
+
268
+ def _validate_and_resolve_path(self, path: str) -> Path:
269
+ """Validate and resolve a virtual path to filesystem path."""
270
+ # Normalize path
271
+ if not path.startswith("/"):
272
+ path = "/" + path
273
+
274
+ # Check for path traversal
275
+ if ".." in path or "~" in path:
276
+ msg = "Path traversal not allowed"
277
+ raise ValueError(msg)
278
+
279
+ # Convert virtual path to filesystem path
280
+ relative = path.lstrip("/")
281
+ full_path = (self.root_path / relative).resolve()
282
+
283
+ # Ensure path is within root
284
+ try:
285
+ full_path.relative_to(self.root_path)
286
+ except ValueError:
287
+ msg = f"Path outside root directory: {path}"
288
+ raise ValueError(msg) from None
289
+
290
+ return full_path
291
+
292
+ def _ripgrep_search(
293
+ self, pattern: str, base_path: str, include: str | None
294
+ ) -> dict[str, list[tuple[int, str]]]:
295
+ """Search using ripgrep subprocess."""
296
+ try:
297
+ base_full = self._validate_and_resolve_path(base_path)
298
+ except ValueError:
299
+ return {}
300
+
301
+ if not base_full.exists():
302
+ return {}
303
+
304
+ # Build ripgrep command
305
+ cmd = ["rg", "--json"]
306
+
307
+ if include:
308
+ # Convert glob pattern to ripgrep glob
309
+ cmd.extend(["--glob", include])
310
+
311
+ cmd.extend(["--", pattern, str(base_full)])
312
+
313
+ try:
314
+ result = subprocess.run( # noqa: S603
315
+ cmd,
316
+ capture_output=True,
317
+ text=True,
318
+ timeout=30,
319
+ check=False,
320
+ )
321
+ except (subprocess.TimeoutExpired, FileNotFoundError):
322
+ # Fallback to Python search if ripgrep unavailable or times out
323
+ return self._python_search(pattern, base_path, include)
324
+
325
+ # Parse ripgrep JSON output
326
+ results: dict[str, list[tuple[int, str]]] = {}
327
+ for line in result.stdout.splitlines():
328
+ try:
329
+ data = json.loads(line)
330
+ if data["type"] == "match":
331
+ path = data["data"]["path"]["text"]
332
+ # Defense in depth: drop any result whose resolved path lies
333
+ # outside the root (e.g. surfaced via an in-root symlink).
334
+ if not _is_within_root(Path(path), self.root_path):
335
+ continue
336
+ # Convert to virtual path
337
+ virtual_path = "/" + str(Path(path).relative_to(self.root_path))
338
+ line_num = data["data"]["line_number"]
339
+ line_text = data["data"]["lines"]["text"].rstrip("\n")
340
+
341
+ if virtual_path not in results:
342
+ results[virtual_path] = []
343
+ results[virtual_path].append((line_num, line_text))
344
+ except (json.JSONDecodeError, KeyError):
345
+ continue
346
+
347
+ return results
348
+
349
+ def _python_search(
350
+ self, pattern: str, base_path: str, include: str | None
351
+ ) -> dict[str, list[tuple[int, str]]]:
352
+ """Search using Python regex (fallback)."""
353
+ try:
354
+ base_full = self._validate_and_resolve_path(base_path)
355
+ except ValueError:
356
+ return {}
357
+
358
+ if not base_full.exists():
359
+ return {}
360
+
361
+ regex = re.compile(pattern)
362
+ results: dict[str, list[tuple[int, str]]] = {}
363
+
364
+ # Walk directory tree without following symlinked directories so traversal
365
+ # cannot leave the root via a symlinked subdirectory.
366
+ for walk_root, _dirs, files in os.walk(base_full, followlinks=False):
367
+ for name in files:
368
+ file_path = Path(walk_root) / name
369
+
370
+ # Re-check containment after resolving so an in-root symlinked file
371
+ # pointing outside the root is never read.
372
+ if not _is_within_root(file_path, self.root_path):
373
+ continue
374
+
375
+ if not file_path.is_file():
376
+ continue
377
+
378
+ # Check include filter
379
+ if include and not _match_include_pattern(file_path.name, include):
380
+ continue
381
+
382
+ # Skip files that are too large
383
+ if file_path.stat().st_size > self.max_file_size_bytes:
384
+ continue
385
+
386
+ try:
387
+ content = file_path.read_text()
388
+ except (UnicodeDecodeError, PermissionError):
389
+ continue
390
+
391
+ # Search content
392
+ for line_num, line in enumerate(content.splitlines(), 1):
393
+ if regex.search(line):
394
+ virtual_path = "/" + str(file_path.relative_to(self.root_path))
395
+ if virtual_path not in results:
396
+ results[virtual_path] = []
397
+ results[virtual_path].append((line_num, line))
398
+
399
+ return results
400
+
401
+ @staticmethod
402
+ def _format_grep_results(
403
+ results: dict[str, list[tuple[int, str]]],
404
+ output_mode: str,
405
+ ) -> str:
406
+ """Format grep results based on output mode."""
407
+ if output_mode == "files_with_matches":
408
+ # Just return file paths
409
+ return "\n".join(sorted(results.keys()))
410
+
411
+ if output_mode == "content":
412
+ # Return file:line:content format
413
+ lines = []
414
+ for file_path in sorted(results.keys()):
415
+ for line_num, line in results[file_path]:
416
+ lines.append(f"{file_path}:{line_num}:{line}")
417
+ return "\n".join(lines)
418
+
419
+ if output_mode == "count":
420
+ # Return file:count format
421
+ lines = []
422
+ for file_path in sorted(results.keys()):
423
+ count = len(results[file_path])
424
+ lines.append(f"{file_path}:{count}")
425
+ return "\n".join(lines)
426
+
427
+ # Default to files_with_matches
428
+ return "\n".join(sorted(results.keys()))
429
+
430
+
431
+ __all__ = [
432
+ "FilesystemFileSearchMiddleware",
433
+ ]