agentpool 2.1.9__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.

Potentially problematic release.


This version of agentpool might be problematic. Click here for more details.

Files changed (474) hide show
  1. acp/README.md +64 -0
  2. acp/__init__.py +172 -0
  3. acp/__main__.py +10 -0
  4. acp/acp_requests.py +285 -0
  5. acp/agent/__init__.py +6 -0
  6. acp/agent/connection.py +256 -0
  7. acp/agent/implementations/__init__.py +6 -0
  8. acp/agent/implementations/debug_server/__init__.py +1 -0
  9. acp/agent/implementations/debug_server/cli.py +79 -0
  10. acp/agent/implementations/debug_server/debug.html +234 -0
  11. acp/agent/implementations/debug_server/debug_server.py +496 -0
  12. acp/agent/implementations/testing.py +91 -0
  13. acp/agent/protocol.py +65 -0
  14. acp/bridge/README.md +162 -0
  15. acp/bridge/__init__.py +6 -0
  16. acp/bridge/__main__.py +91 -0
  17. acp/bridge/bridge.py +246 -0
  18. acp/bridge/py.typed +0 -0
  19. acp/bridge/settings.py +15 -0
  20. acp/client/__init__.py +7 -0
  21. acp/client/connection.py +251 -0
  22. acp/client/implementations/__init__.py +7 -0
  23. acp/client/implementations/default_client.py +185 -0
  24. acp/client/implementations/headless_client.py +266 -0
  25. acp/client/implementations/noop_client.py +110 -0
  26. acp/client/protocol.py +61 -0
  27. acp/connection.py +280 -0
  28. acp/exceptions.py +46 -0
  29. acp/filesystem.py +524 -0
  30. acp/notifications.py +832 -0
  31. acp/py.typed +0 -0
  32. acp/schema/__init__.py +265 -0
  33. acp/schema/agent_plan.py +30 -0
  34. acp/schema/agent_requests.py +126 -0
  35. acp/schema/agent_responses.py +256 -0
  36. acp/schema/base.py +39 -0
  37. acp/schema/capabilities.py +230 -0
  38. acp/schema/client_requests.py +247 -0
  39. acp/schema/client_responses.py +96 -0
  40. acp/schema/common.py +81 -0
  41. acp/schema/content_blocks.py +188 -0
  42. acp/schema/mcp.py +82 -0
  43. acp/schema/messages.py +171 -0
  44. acp/schema/notifications.py +82 -0
  45. acp/schema/protocol_stuff.md +3 -0
  46. acp/schema/session_state.py +160 -0
  47. acp/schema/session_updates.py +419 -0
  48. acp/schema/slash_commands.py +51 -0
  49. acp/schema/terminal.py +15 -0
  50. acp/schema/tool_call.py +347 -0
  51. acp/stdio.py +250 -0
  52. acp/task/__init__.py +53 -0
  53. acp/task/debug.py +197 -0
  54. acp/task/dispatcher.py +93 -0
  55. acp/task/queue.py +69 -0
  56. acp/task/sender.py +82 -0
  57. acp/task/state.py +87 -0
  58. acp/task/supervisor.py +93 -0
  59. acp/terminal_handle.py +30 -0
  60. acp/tool_call_reporter.py +199 -0
  61. acp/tool_call_state.py +178 -0
  62. acp/transports.py +104 -0
  63. acp/utils.py +240 -0
  64. agentpool/__init__.py +63 -0
  65. agentpool/__main__.py +7 -0
  66. agentpool/agents/__init__.py +30 -0
  67. agentpool/agents/acp_agent/__init__.py +5 -0
  68. agentpool/agents/acp_agent/acp_agent.py +837 -0
  69. agentpool/agents/acp_agent/acp_converters.py +294 -0
  70. agentpool/agents/acp_agent/client_handler.py +317 -0
  71. agentpool/agents/acp_agent/session_state.py +44 -0
  72. agentpool/agents/agent.py +1264 -0
  73. agentpool/agents/agui_agent/__init__.py +19 -0
  74. agentpool/agents/agui_agent/agui_agent.py +677 -0
  75. agentpool/agents/agui_agent/agui_converters.py +423 -0
  76. agentpool/agents/agui_agent/chunk_transformer.py +204 -0
  77. agentpool/agents/agui_agent/event_types.py +83 -0
  78. agentpool/agents/agui_agent/helpers.py +192 -0
  79. agentpool/agents/architect.py +71 -0
  80. agentpool/agents/base_agent.py +177 -0
  81. agentpool/agents/claude_code_agent/__init__.py +11 -0
  82. agentpool/agents/claude_code_agent/claude_code_agent.py +1021 -0
  83. agentpool/agents/claude_code_agent/converters.py +243 -0
  84. agentpool/agents/context.py +105 -0
  85. agentpool/agents/events/__init__.py +61 -0
  86. agentpool/agents/events/builtin_handlers.py +129 -0
  87. agentpool/agents/events/event_emitter.py +320 -0
  88. agentpool/agents/events/events.py +561 -0
  89. agentpool/agents/events/tts_handlers.py +186 -0
  90. agentpool/agents/interactions.py +419 -0
  91. agentpool/agents/slashed_agent.py +244 -0
  92. agentpool/agents/sys_prompts.py +178 -0
  93. agentpool/agents/tool_wrapping.py +184 -0
  94. agentpool/base_provider.py +28 -0
  95. agentpool/common_types.py +226 -0
  96. agentpool/config_resources/__init__.py +16 -0
  97. agentpool/config_resources/acp_assistant.yml +24 -0
  98. agentpool/config_resources/agents.yml +109 -0
  99. agentpool/config_resources/agents_template.yml +18 -0
  100. agentpool/config_resources/agui_test.yml +18 -0
  101. agentpool/config_resources/claude_code_agent.yml +16 -0
  102. agentpool/config_resources/claude_style_subagent.md +30 -0
  103. agentpool/config_resources/external_acp_agents.yml +77 -0
  104. agentpool/config_resources/opencode_style_subagent.md +19 -0
  105. agentpool/config_resources/tts_test_agents.yml +78 -0
  106. agentpool/delegation/__init__.py +8 -0
  107. agentpool/delegation/base_team.py +504 -0
  108. agentpool/delegation/message_flow_tracker.py +39 -0
  109. agentpool/delegation/pool.py +1129 -0
  110. agentpool/delegation/team.py +325 -0
  111. agentpool/delegation/teamrun.py +343 -0
  112. agentpool/docs/__init__.py +5 -0
  113. agentpool/docs/gen_examples.py +42 -0
  114. agentpool/docs/utils.py +370 -0
  115. agentpool/functional/__init__.py +20 -0
  116. agentpool/functional/py.typed +0 -0
  117. agentpool/functional/run.py +80 -0
  118. agentpool/functional/structure.py +136 -0
  119. agentpool/hooks/__init__.py +20 -0
  120. agentpool/hooks/agent_hooks.py +247 -0
  121. agentpool/hooks/base.py +119 -0
  122. agentpool/hooks/callable.py +140 -0
  123. agentpool/hooks/command.py +180 -0
  124. agentpool/hooks/prompt.py +122 -0
  125. agentpool/jinja_filters.py +132 -0
  126. agentpool/log.py +224 -0
  127. agentpool/mcp_server/__init__.py +17 -0
  128. agentpool/mcp_server/client.py +429 -0
  129. agentpool/mcp_server/constants.py +32 -0
  130. agentpool/mcp_server/conversions.py +172 -0
  131. agentpool/mcp_server/helpers.py +47 -0
  132. agentpool/mcp_server/manager.py +232 -0
  133. agentpool/mcp_server/message_handler.py +164 -0
  134. agentpool/mcp_server/registries/__init__.py +1 -0
  135. agentpool/mcp_server/registries/official_registry_client.py +345 -0
  136. agentpool/mcp_server/registries/pulsemcp_client.py +88 -0
  137. agentpool/mcp_server/tool_bridge.py +548 -0
  138. agentpool/messaging/__init__.py +58 -0
  139. agentpool/messaging/compaction.py +928 -0
  140. agentpool/messaging/connection_manager.py +319 -0
  141. agentpool/messaging/context.py +66 -0
  142. agentpool/messaging/event_manager.py +426 -0
  143. agentpool/messaging/events.py +39 -0
  144. agentpool/messaging/message_container.py +209 -0
  145. agentpool/messaging/message_history.py +491 -0
  146. agentpool/messaging/messagenode.py +377 -0
  147. agentpool/messaging/messages.py +655 -0
  148. agentpool/messaging/processing.py +76 -0
  149. agentpool/mime_utils.py +95 -0
  150. agentpool/models/__init__.py +21 -0
  151. agentpool/models/acp_agents/__init__.py +22 -0
  152. agentpool/models/acp_agents/base.py +308 -0
  153. agentpool/models/acp_agents/mcp_capable.py +790 -0
  154. agentpool/models/acp_agents/non_mcp.py +842 -0
  155. agentpool/models/agents.py +450 -0
  156. agentpool/models/agui_agents.py +89 -0
  157. agentpool/models/claude_code_agents.py +238 -0
  158. agentpool/models/file_agents.py +116 -0
  159. agentpool/models/file_parsing.py +367 -0
  160. agentpool/models/manifest.py +658 -0
  161. agentpool/observability/__init__.py +9 -0
  162. agentpool/observability/observability_registry.py +97 -0
  163. agentpool/prompts/__init__.py +1 -0
  164. agentpool/prompts/base.py +27 -0
  165. agentpool/prompts/builtin_provider.py +75 -0
  166. agentpool/prompts/conversion_manager.py +95 -0
  167. agentpool/prompts/convert.py +96 -0
  168. agentpool/prompts/manager.py +204 -0
  169. agentpool/prompts/parts/zed.md +33 -0
  170. agentpool/prompts/prompts.py +581 -0
  171. agentpool/py.typed +0 -0
  172. agentpool/queries/tree-sitter-language-pack/README.md +7 -0
  173. agentpool/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  174. agentpool/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  175. agentpool/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  176. agentpool/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
  177. agentpool/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  178. agentpool/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  179. agentpool/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  180. agentpool/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  181. agentpool/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  182. agentpool/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  183. agentpool/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  184. agentpool/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  185. agentpool/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  186. agentpool/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  187. agentpool/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  188. agentpool/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  189. agentpool/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  190. agentpool/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  191. agentpool/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  192. agentpool/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  193. agentpool/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  194. agentpool/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  195. agentpool/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  196. agentpool/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  197. agentpool/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  198. agentpool/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  199. agentpool/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  200. agentpool/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  201. agentpool/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  202. agentpool/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  203. agentpool/queries/tree-sitter-languages/README.md +24 -0
  204. agentpool/queries/tree-sitter-languages/c-tags.scm +9 -0
  205. agentpool/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  206. agentpool/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  207. agentpool/queries/tree-sitter-languages/dart-tags.scm +91 -0
  208. agentpool/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  209. agentpool/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  210. agentpool/queries/tree-sitter-languages/elm-tags.scm +19 -0
  211. agentpool/queries/tree-sitter-languages/fortran-tags.scm +15 -0
  212. agentpool/queries/tree-sitter-languages/go-tags.scm +30 -0
  213. agentpool/queries/tree-sitter-languages/haskell-tags.scm +3 -0
  214. agentpool/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  215. agentpool/queries/tree-sitter-languages/java-tags.scm +20 -0
  216. agentpool/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  217. agentpool/queries/tree-sitter-languages/julia-tags.scm +60 -0
  218. agentpool/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  219. agentpool/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  220. agentpool/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  221. agentpool/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  222. agentpool/queries/tree-sitter-languages/php-tags.scm +26 -0
  223. agentpool/queries/tree-sitter-languages/python-tags.scm +12 -0
  224. agentpool/queries/tree-sitter-languages/ql-tags.scm +26 -0
  225. agentpool/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  226. agentpool/queries/tree-sitter-languages/rust-tags.scm +60 -0
  227. agentpool/queries/tree-sitter-languages/scala-tags.scm +65 -0
  228. agentpool/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  229. agentpool/queries/tree-sitter-languages/zig-tags.scm +3 -0
  230. agentpool/repomap.py +1231 -0
  231. agentpool/resource_providers/__init__.py +17 -0
  232. agentpool/resource_providers/aggregating.py +54 -0
  233. agentpool/resource_providers/base.py +172 -0
  234. agentpool/resource_providers/codemode/__init__.py +9 -0
  235. agentpool/resource_providers/codemode/code_executor.py +215 -0
  236. agentpool/resource_providers/codemode/default_prompt.py +19 -0
  237. agentpool/resource_providers/codemode/helpers.py +83 -0
  238. agentpool/resource_providers/codemode/progress_executor.py +212 -0
  239. agentpool/resource_providers/codemode/provider.py +150 -0
  240. agentpool/resource_providers/codemode/remote_mcp_execution.py +143 -0
  241. agentpool/resource_providers/codemode/remote_provider.py +171 -0
  242. agentpool/resource_providers/filtering.py +42 -0
  243. agentpool/resource_providers/mcp_provider.py +246 -0
  244. agentpool/resource_providers/plan_provider.py +196 -0
  245. agentpool/resource_providers/pool.py +69 -0
  246. agentpool/resource_providers/static.py +289 -0
  247. agentpool/running/__init__.py +20 -0
  248. agentpool/running/decorators.py +56 -0
  249. agentpool/running/discovery.py +101 -0
  250. agentpool/running/executor.py +284 -0
  251. agentpool/running/injection.py +111 -0
  252. agentpool/running/py.typed +0 -0
  253. agentpool/running/run_nodes.py +87 -0
  254. agentpool/server.py +122 -0
  255. agentpool/sessions/__init__.py +13 -0
  256. agentpool/sessions/manager.py +302 -0
  257. agentpool/sessions/models.py +71 -0
  258. agentpool/sessions/session.py +239 -0
  259. agentpool/sessions/store.py +163 -0
  260. agentpool/skills/__init__.py +5 -0
  261. agentpool/skills/manager.py +120 -0
  262. agentpool/skills/registry.py +210 -0
  263. agentpool/skills/skill.py +36 -0
  264. agentpool/storage/__init__.py +17 -0
  265. agentpool/storage/manager.py +419 -0
  266. agentpool/storage/serialization.py +136 -0
  267. agentpool/talk/__init__.py +13 -0
  268. agentpool/talk/registry.py +128 -0
  269. agentpool/talk/stats.py +159 -0
  270. agentpool/talk/talk.py +604 -0
  271. agentpool/tasks/__init__.py +20 -0
  272. agentpool/tasks/exceptions.py +25 -0
  273. agentpool/tasks/registry.py +33 -0
  274. agentpool/testing.py +129 -0
  275. agentpool/text_templates/__init__.py +39 -0
  276. agentpool/text_templates/system_prompt.jinja +30 -0
  277. agentpool/text_templates/tool_call_default.jinja +13 -0
  278. agentpool/text_templates/tool_call_markdown.jinja +25 -0
  279. agentpool/text_templates/tool_call_simple.jinja +5 -0
  280. agentpool/tools/__init__.py +16 -0
  281. agentpool/tools/base.py +269 -0
  282. agentpool/tools/exceptions.py +9 -0
  283. agentpool/tools/manager.py +255 -0
  284. agentpool/tools/tool_call_info.py +87 -0
  285. agentpool/ui/__init__.py +2 -0
  286. agentpool/ui/base.py +89 -0
  287. agentpool/ui/mock_provider.py +81 -0
  288. agentpool/ui/stdlib_provider.py +150 -0
  289. agentpool/utils/__init__.py +44 -0
  290. agentpool/utils/baseregistry.py +185 -0
  291. agentpool/utils/count_tokens.py +62 -0
  292. agentpool/utils/dag.py +184 -0
  293. agentpool/utils/importing.py +206 -0
  294. agentpool/utils/inspection.py +334 -0
  295. agentpool/utils/model_capabilities.py +25 -0
  296. agentpool/utils/network.py +28 -0
  297. agentpool/utils/now.py +22 -0
  298. agentpool/utils/parse_time.py +87 -0
  299. agentpool/utils/result_utils.py +35 -0
  300. agentpool/utils/signatures.py +305 -0
  301. agentpool/utils/streams.py +112 -0
  302. agentpool/utils/tasks.py +186 -0
  303. agentpool/vfs_registry.py +250 -0
  304. agentpool-2.1.9.dist-info/METADATA +336 -0
  305. agentpool-2.1.9.dist-info/RECORD +474 -0
  306. agentpool-2.1.9.dist-info/WHEEL +4 -0
  307. agentpool-2.1.9.dist-info/entry_points.txt +14 -0
  308. agentpool-2.1.9.dist-info/licenses/LICENSE +22 -0
  309. agentpool_cli/__init__.py +34 -0
  310. agentpool_cli/__main__.py +66 -0
  311. agentpool_cli/agent.py +175 -0
  312. agentpool_cli/cli_types.py +23 -0
  313. agentpool_cli/common.py +163 -0
  314. agentpool_cli/create.py +175 -0
  315. agentpool_cli/history.py +217 -0
  316. agentpool_cli/log.py +78 -0
  317. agentpool_cli/py.typed +0 -0
  318. agentpool_cli/run.py +84 -0
  319. agentpool_cli/serve_acp.py +177 -0
  320. agentpool_cli/serve_api.py +69 -0
  321. agentpool_cli/serve_mcp.py +74 -0
  322. agentpool_cli/serve_vercel.py +233 -0
  323. agentpool_cli/store.py +171 -0
  324. agentpool_cli/task.py +84 -0
  325. agentpool_cli/utils.py +104 -0
  326. agentpool_cli/watch.py +54 -0
  327. agentpool_commands/__init__.py +180 -0
  328. agentpool_commands/agents.py +199 -0
  329. agentpool_commands/base.py +45 -0
  330. agentpool_commands/commands.py +58 -0
  331. agentpool_commands/completers.py +110 -0
  332. agentpool_commands/connections.py +175 -0
  333. agentpool_commands/markdown_utils.py +31 -0
  334. agentpool_commands/models.py +62 -0
  335. agentpool_commands/prompts.py +78 -0
  336. agentpool_commands/py.typed +0 -0
  337. agentpool_commands/read.py +77 -0
  338. agentpool_commands/resources.py +210 -0
  339. agentpool_commands/session.py +48 -0
  340. agentpool_commands/tools.py +269 -0
  341. agentpool_commands/utils.py +189 -0
  342. agentpool_commands/workers.py +163 -0
  343. agentpool_config/__init__.py +53 -0
  344. agentpool_config/builtin_tools.py +265 -0
  345. agentpool_config/commands.py +237 -0
  346. agentpool_config/conditions.py +301 -0
  347. agentpool_config/converters.py +30 -0
  348. agentpool_config/durable.py +331 -0
  349. agentpool_config/event_handlers.py +600 -0
  350. agentpool_config/events.py +153 -0
  351. agentpool_config/forward_targets.py +251 -0
  352. agentpool_config/hook_conditions.py +331 -0
  353. agentpool_config/hooks.py +241 -0
  354. agentpool_config/jinja.py +206 -0
  355. agentpool_config/knowledge.py +41 -0
  356. agentpool_config/loaders.py +350 -0
  357. agentpool_config/mcp_server.py +243 -0
  358. agentpool_config/nodes.py +202 -0
  359. agentpool_config/observability.py +191 -0
  360. agentpool_config/output_types.py +55 -0
  361. agentpool_config/pool_server.py +267 -0
  362. agentpool_config/prompt_hubs.py +105 -0
  363. agentpool_config/prompts.py +185 -0
  364. agentpool_config/py.typed +0 -0
  365. agentpool_config/resources.py +33 -0
  366. agentpool_config/session.py +119 -0
  367. agentpool_config/skills.py +17 -0
  368. agentpool_config/storage.py +288 -0
  369. agentpool_config/system_prompts.py +190 -0
  370. agentpool_config/task.py +162 -0
  371. agentpool_config/teams.py +52 -0
  372. agentpool_config/tools.py +112 -0
  373. agentpool_config/toolsets.py +1033 -0
  374. agentpool_config/workers.py +86 -0
  375. agentpool_prompts/__init__.py +1 -0
  376. agentpool_prompts/braintrust_hub.py +235 -0
  377. agentpool_prompts/fabric.py +75 -0
  378. agentpool_prompts/langfuse_hub.py +79 -0
  379. agentpool_prompts/promptlayer_provider.py +59 -0
  380. agentpool_prompts/py.typed +0 -0
  381. agentpool_server/__init__.py +9 -0
  382. agentpool_server/a2a_server/__init__.py +5 -0
  383. agentpool_server/a2a_server/a2a_types.py +41 -0
  384. agentpool_server/a2a_server/server.py +190 -0
  385. agentpool_server/a2a_server/storage.py +81 -0
  386. agentpool_server/acp_server/__init__.py +22 -0
  387. agentpool_server/acp_server/acp_agent.py +786 -0
  388. agentpool_server/acp_server/acp_tools.py +43 -0
  389. agentpool_server/acp_server/commands/__init__.py +18 -0
  390. agentpool_server/acp_server/commands/acp_commands.py +594 -0
  391. agentpool_server/acp_server/commands/debug_commands.py +376 -0
  392. agentpool_server/acp_server/commands/docs_commands/__init__.py +39 -0
  393. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +169 -0
  394. agentpool_server/acp_server/commands/docs_commands/get_schema.py +176 -0
  395. agentpool_server/acp_server/commands/docs_commands/get_source.py +110 -0
  396. agentpool_server/acp_server/commands/docs_commands/git_diff.py +111 -0
  397. agentpool_server/acp_server/commands/docs_commands/helpers.py +33 -0
  398. agentpool_server/acp_server/commands/docs_commands/url_to_markdown.py +90 -0
  399. agentpool_server/acp_server/commands/spawn.py +210 -0
  400. agentpool_server/acp_server/converters.py +235 -0
  401. agentpool_server/acp_server/input_provider.py +338 -0
  402. agentpool_server/acp_server/server.py +288 -0
  403. agentpool_server/acp_server/session.py +969 -0
  404. agentpool_server/acp_server/session_manager.py +313 -0
  405. agentpool_server/acp_server/syntax_detection.py +250 -0
  406. agentpool_server/acp_server/zed_tools.md +90 -0
  407. agentpool_server/aggregating_server.py +309 -0
  408. agentpool_server/agui_server/__init__.py +11 -0
  409. agentpool_server/agui_server/server.py +128 -0
  410. agentpool_server/base.py +189 -0
  411. agentpool_server/http_server.py +164 -0
  412. agentpool_server/mcp_server/__init__.py +6 -0
  413. agentpool_server/mcp_server/server.py +314 -0
  414. agentpool_server/mcp_server/zed_wrapper.py +110 -0
  415. agentpool_server/openai_api_server/__init__.py +5 -0
  416. agentpool_server/openai_api_server/completions/__init__.py +1 -0
  417. agentpool_server/openai_api_server/completions/helpers.py +81 -0
  418. agentpool_server/openai_api_server/completions/models.py +98 -0
  419. agentpool_server/openai_api_server/responses/__init__.py +1 -0
  420. agentpool_server/openai_api_server/responses/helpers.py +74 -0
  421. agentpool_server/openai_api_server/responses/models.py +96 -0
  422. agentpool_server/openai_api_server/server.py +242 -0
  423. agentpool_server/py.typed +0 -0
  424. agentpool_storage/__init__.py +9 -0
  425. agentpool_storage/base.py +310 -0
  426. agentpool_storage/file_provider.py +378 -0
  427. agentpool_storage/formatters.py +129 -0
  428. agentpool_storage/memory_provider.py +396 -0
  429. agentpool_storage/models.py +108 -0
  430. agentpool_storage/py.typed +0 -0
  431. agentpool_storage/session_store.py +262 -0
  432. agentpool_storage/sql_provider/__init__.py +21 -0
  433. agentpool_storage/sql_provider/cli.py +146 -0
  434. agentpool_storage/sql_provider/models.py +249 -0
  435. agentpool_storage/sql_provider/queries.py +15 -0
  436. agentpool_storage/sql_provider/sql_provider.py +444 -0
  437. agentpool_storage/sql_provider/utils.py +234 -0
  438. agentpool_storage/text_log_provider.py +275 -0
  439. agentpool_toolsets/__init__.py +15 -0
  440. agentpool_toolsets/builtin/__init__.py +33 -0
  441. agentpool_toolsets/builtin/agent_management.py +239 -0
  442. agentpool_toolsets/builtin/chain.py +288 -0
  443. agentpool_toolsets/builtin/code.py +398 -0
  444. agentpool_toolsets/builtin/debug.py +291 -0
  445. agentpool_toolsets/builtin/execution_environment.py +381 -0
  446. agentpool_toolsets/builtin/file_edit/__init__.py +11 -0
  447. agentpool_toolsets/builtin/file_edit/file_edit.py +747 -0
  448. agentpool_toolsets/builtin/file_edit/fuzzy_matcher/__init__.py +5 -0
  449. agentpool_toolsets/builtin/file_edit/fuzzy_matcher/example_usage.py +311 -0
  450. agentpool_toolsets/builtin/file_edit/fuzzy_matcher/streaming_fuzzy_matcher.py +443 -0
  451. agentpool_toolsets/builtin/history.py +36 -0
  452. agentpool_toolsets/builtin/integration.py +85 -0
  453. agentpool_toolsets/builtin/skills.py +77 -0
  454. agentpool_toolsets/builtin/subagent_tools.py +324 -0
  455. agentpool_toolsets/builtin/tool_management.py +90 -0
  456. agentpool_toolsets/builtin/user_interaction.py +52 -0
  457. agentpool_toolsets/builtin/workers.py +128 -0
  458. agentpool_toolsets/composio_toolset.py +96 -0
  459. agentpool_toolsets/config_creation.py +192 -0
  460. agentpool_toolsets/entry_points.py +47 -0
  461. agentpool_toolsets/fsspec_toolset/__init__.py +7 -0
  462. agentpool_toolsets/fsspec_toolset/diagnostics.py +115 -0
  463. agentpool_toolsets/fsspec_toolset/grep.py +450 -0
  464. agentpool_toolsets/fsspec_toolset/helpers.py +631 -0
  465. agentpool_toolsets/fsspec_toolset/streaming_diff_parser.py +249 -0
  466. agentpool_toolsets/fsspec_toolset/toolset.py +1384 -0
  467. agentpool_toolsets/mcp_run_toolset.py +61 -0
  468. agentpool_toolsets/notifications.py +146 -0
  469. agentpool_toolsets/openapi.py +118 -0
  470. agentpool_toolsets/py.typed +0 -0
  471. agentpool_toolsets/search_toolset.py +202 -0
  472. agentpool_toolsets/semantic_memory_toolset.py +536 -0
  473. agentpool_toolsets/streaming_tools.py +265 -0
  474. agentpool_toolsets/vfs_toolset.py +124 -0
@@ -0,0 +1,631 @@
1
+ """FSSpec filesystem toolset helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import difflib
7
+ import re
8
+ from typing import Any
9
+
10
+ from pydantic_ai import ModelRetry
11
+
12
+ from agentpool.log import get_logger
13
+ from agentpool_toolsets.builtin.file_edit.fuzzy_matcher import StreamingFuzzyMatcher
14
+
15
+
16
+ logger = get_logger(__name__)
17
+
18
+ # Default maximum size for file operations (64KB)
19
+ DEFAULT_MAX_SIZE = 64_000
20
+
21
+
22
+ @dataclass
23
+ class DiffHunk:
24
+ """A single diff hunk representing one edit operation."""
25
+
26
+ old_text: str
27
+ """The text to find/replace (context + removed lines)."""
28
+
29
+ new_text: str
30
+ """The replacement text (context + added lines)."""
31
+
32
+ raw: str
33
+ """The raw diff text for this hunk."""
34
+
35
+
36
+ def parse_locationless_diff(diff_text: str) -> list[DiffHunk]:
37
+ """Parse a locationless unified diff into old/new text pairs.
38
+
39
+ Handles diff format without line numbers - the location is inferred
40
+ by matching context in the file.
41
+
42
+ Format expected:
43
+ ```
44
+ context line (unchanged)
45
+ -removed line
46
+ +added line
47
+ more context
48
+ ```
49
+
50
+ Multiple hunks are separated by:
51
+ - Blank lines (empty line not starting with space)
52
+ - Non-diff content lines
53
+
54
+ Args:
55
+ diff_text: The diff text (may contain multiple hunks)
56
+
57
+ Returns:
58
+ List of DiffHunk objects with old_text/new_text pairs
59
+ """
60
+ hunks: list[DiffHunk] = []
61
+
62
+ # Extract content between <diff> tags if present
63
+ diff_match = re.search(r"<diff>(.*?)</diff>", diff_text, re.DOTALL)
64
+ if diff_match:
65
+ diff_text = diff_match.group(1)
66
+
67
+ # Also handle ```diff ... ``` code blocks
68
+ # Use \n? instead of \s* to avoid consuming leading spaces on first diff line
69
+ code_block_match = re.search(r"```diff\n?(.*?)```", diff_text, re.DOTALL)
70
+ if code_block_match:
71
+ diff_text = code_block_match.group(1)
72
+
73
+ # Strip only leading/trailing newlines, not spaces (which are meaningful in diffs)
74
+ diff_text = diff_text.strip("\n\r")
75
+ lines = diff_text.split("\n")
76
+
77
+ current_hunk_lines: list[str] = []
78
+
79
+ for line in lines:
80
+ # Skip standard diff headers
81
+ if line.startswith(("---", "+++", "@@", "diff --git", "index ")):
82
+ continue
83
+
84
+ # Check if this is a diff line (starts with +, -, or space for context)
85
+ is_diff_line = line.startswith(("+", "-", " "))
86
+
87
+ # Empty line (not starting with space) = hunk separator
88
+ if line == "" or (not is_diff_line and not line.strip()):
89
+ if current_hunk_lines:
90
+ hunk = _parse_single_hunk(current_hunk_lines)
91
+ if hunk:
92
+ hunks.append(hunk)
93
+ current_hunk_lines = []
94
+ continue
95
+
96
+ # Non-diff content line = hunk separator
97
+ if not is_diff_line and line.strip():
98
+ if current_hunk_lines:
99
+ hunk = _parse_single_hunk(current_hunk_lines)
100
+ if hunk:
101
+ hunks.append(hunk)
102
+ current_hunk_lines = []
103
+ continue
104
+
105
+ # Accumulate diff lines
106
+ current_hunk_lines.append(line)
107
+
108
+ # Don't forget the last hunk
109
+ if current_hunk_lines:
110
+ hunk = _parse_single_hunk(current_hunk_lines)
111
+ if hunk:
112
+ hunks.append(hunk)
113
+
114
+ return hunks
115
+
116
+
117
+ def _parse_single_hunk(lines: list[str]) -> DiffHunk | None:
118
+ """Parse a single diff hunk into old/new text.
119
+
120
+ Args:
121
+ lines: Lines of the hunk (each starting with +, -, or space)
122
+
123
+ Returns:
124
+ DiffHunk or None if the hunk is empty/invalid
125
+ """
126
+ old_lines: list[str] = []
127
+ new_lines: list[str] = []
128
+
129
+ for line in lines:
130
+ if line.startswith("-"):
131
+ # Removed line - only in old
132
+ old_lines.append(line[1:])
133
+ elif line.startswith("+"):
134
+ # Added line - only in new
135
+ new_lines.append(line[1:])
136
+ elif line.startswith(" "):
137
+ # Context line - in both
138
+ content = line[1:] if len(line) > 1 else ""
139
+ old_lines.append(content)
140
+ new_lines.append(content)
141
+ # Skip lines that don't match the pattern
142
+
143
+ if not old_lines and not new_lines:
144
+ return None
145
+
146
+ old_text = "\n".join(old_lines)
147
+ new_text = "\n".join(new_lines)
148
+ raw = "\n".join(lines)
149
+
150
+ return DiffHunk(old_text=old_text, new_text=new_text, raw=raw)
151
+
152
+
153
+ async def apply_diff_edits(
154
+ original_content: str,
155
+ diff_response: str,
156
+ *,
157
+ use_fuzzy: bool = True,
158
+ ) -> str:
159
+ """Apply locationless diff edits to content.
160
+
161
+ Parses diff format and applies each hunk using content matching.
162
+
163
+ Args:
164
+ original_content: The original file content
165
+ diff_response: The agent's response containing diffs
166
+ use_fuzzy: Whether to use fuzzy matching for finding locations
167
+
168
+ Returns:
169
+ The modified content
170
+
171
+ Raises:
172
+ ModelRetry: If edits cannot be applied (for agent retry)
173
+ """
174
+ from agentpool_toolsets.builtin.file_edit import replace_content
175
+
176
+ hunks = parse_locationless_diff(diff_response)
177
+
178
+ if not hunks:
179
+ logger.warning("No diff hunks found in response")
180
+ # Try falling back to structured edits format
181
+ return await apply_structured_edits(original_content, diff_response)
182
+
183
+ content = original_content
184
+ applied_edits = 0
185
+ failed_hunks: list[str] = []
186
+
187
+ for hunk in hunks:
188
+ if not hunk.old_text.strip():
189
+ # Pure insertion - would need line context to place
190
+ # For now, skip pure insertions without context
191
+ logger.warning("Skipping pure insertion hunk (no context)")
192
+ continue
193
+
194
+ try:
195
+ # Use the existing smart replace with fuzzy matching
196
+ new_content = replace_content(
197
+ content,
198
+ hunk.old_text,
199
+ hunk.new_text,
200
+ replace_all=False,
201
+ )
202
+ content = new_content
203
+ applied_edits += 1
204
+ except ValueError as e:
205
+ # Match failed
206
+ logger.warning("Failed to apply hunk", error=str(e), hunk=hunk.raw[:100])
207
+ failed_hunks.append(hunk.old_text[:50])
208
+
209
+ if applied_edits == 0 and hunks:
210
+ msg = (
211
+ f"None of the {len(hunks)} diff hunks could be applied. "
212
+ "The context lines don't match the current file content. "
213
+ "Please read the file again and provide accurate diff context."
214
+ )
215
+ raise ModelRetry(msg)
216
+
217
+ if failed_hunks:
218
+ logger.warning(
219
+ "Some hunks failed",
220
+ applied=applied_edits,
221
+ failed=len(failed_hunks),
222
+ )
223
+
224
+ logger.info("Applied diff edits", applied=applied_edits, total=len(hunks))
225
+ return content
226
+
227
+
228
+ async def apply_diff_edits_streaming(
229
+ original_content: str,
230
+ diff_response: str,
231
+ *,
232
+ line_hint: int | None = None,
233
+ ) -> str:
234
+ """Apply locationless diff edits using streaming fuzzy matcher (Zed-style).
235
+
236
+ Alternative to `apply_diff_edits` that uses a dynamic programming based
237
+ fuzzy matcher to locate where edits should be applied. This approach:
238
+ - Uses line-by-line fuzzy matching with Levenshtein distance
239
+ - Handles indentation differences gracefully
240
+ - Can use line hints to disambiguate multiple matches
241
+ - Matches Zed's edit resolution algorithm
242
+
243
+ Args:
244
+ original_content: The original file content
245
+ diff_response: The agent's response containing diffs
246
+ line_hint: Optional line number hint for disambiguation
247
+
248
+ Returns:
249
+ The modified content
250
+
251
+ Raises:
252
+ ModelRetry: If edits cannot be applied (for agent retry)
253
+ """
254
+ hunks = parse_locationless_diff(diff_response)
255
+
256
+ if not hunks:
257
+ logger.warning("No diff hunks found in response (streaming)")
258
+ # Try falling back to structured edits format
259
+ return await apply_structured_edits(original_content, diff_response)
260
+
261
+ content = original_content
262
+ applied_edits = 0
263
+ failed_hunks: list[str] = []
264
+ ambiguous_hunks: list[str] = []
265
+
266
+ for hunk in hunks:
267
+ if not hunk.old_text.strip():
268
+ # Pure insertion - skip for now (needs anchor point)
269
+ logger.warning("Skipping pure insertion hunk (no context)")
270
+ continue
271
+
272
+ # Use streaming fuzzy matcher to find where old_text matches
273
+ matcher = StreamingFuzzyMatcher(content)
274
+
275
+ # Feed the old_text lines to the matcher
276
+ # Simulate streaming by pushing line by line
277
+ old_lines = hunk.old_text.split("\n")
278
+ for i, line in enumerate(old_lines):
279
+ # Add newline except for last line
280
+ chunk = line + ("\n" if i < len(old_lines) - 1 else "")
281
+ matcher.push(chunk, line_hint=line_hint)
282
+
283
+ # Get final matches
284
+ matches = matcher.finish()
285
+
286
+ if not matches:
287
+ logger.warning("No match found for hunk", hunk=hunk.old_text[:50])
288
+ failed_hunks.append(hunk.old_text[:50])
289
+ continue
290
+
291
+ # Try to select best match
292
+ best_match = matcher.select_best_match()
293
+
294
+ if best_match is None and len(matches) > 1:
295
+ # Multiple ambiguous matches
296
+ logger.warning(
297
+ "Ambiguous matches for hunk",
298
+ hunk=hunk.old_text[:50],
299
+ match_count=len(matches),
300
+ )
301
+ ambiguous_hunks.append(hunk.old_text[:50])
302
+ continue
303
+
304
+ # Use best match or first match
305
+ match_range = best_match or matches[0]
306
+
307
+ # Extract the matched text and replace with new_text
308
+ matched_text = content[match_range.start : match_range.end]
309
+
310
+ # Apply the replacement
311
+ # We need to be careful about indentation - the matcher finds the range,
312
+ # but we should preserve the original indentation structure
313
+ old_indent = _get_leading_indent(matched_text)
314
+ new_indent = _get_leading_indent(hunk.old_text)
315
+
316
+ # Calculate indent delta
317
+ indent_delta = len(old_indent) - len(new_indent)
318
+
319
+ # Reindent new_text to match the file's indentation
320
+ if indent_delta != 0:
321
+ reindented_new = _reindent_text(
322
+ hunk.new_text, indent_delta, old_indent[0] if old_indent else " "
323
+ )
324
+ else:
325
+ reindented_new = hunk.new_text
326
+
327
+ # Apply the edit
328
+ content = content[: match_range.start] + reindented_new + content[match_range.end :]
329
+ applied_edits += 1
330
+
331
+ logger.debug(
332
+ "Applied streaming edit",
333
+ start=match_range.start,
334
+ end=match_range.end,
335
+ old_len=len(matched_text),
336
+ new_len=len(reindented_new),
337
+ )
338
+
339
+ if applied_edits == 0 and hunks:
340
+ if ambiguous_hunks:
341
+ matches_str = ", ".join(ambiguous_hunks[:3])
342
+ msg = (
343
+ f"Edit locations are ambiguous - multiple matches found for: {matches_str}... "
344
+ "Please include more context lines in the diff to uniquely identify the location."
345
+ )
346
+ else:
347
+ msg = (
348
+ f"None of the {len(hunks)} diff hunks could be applied. "
349
+ "The context lines don't match the current file content. "
350
+ "Please read the file again and provide accurate diff context."
351
+ )
352
+ raise ModelRetry(msg)
353
+
354
+ if failed_hunks or ambiguous_hunks:
355
+ logger.warning(
356
+ "Some hunks failed (streaming)",
357
+ applied=applied_edits,
358
+ failed=len(failed_hunks),
359
+ ambiguous=len(ambiguous_hunks),
360
+ )
361
+
362
+ logger.info("Applied diff edits (streaming)", applied=applied_edits, total=len(hunks))
363
+ return content
364
+
365
+
366
+ def _get_leading_indent(text: str) -> str:
367
+ """Get the leading whitespace of the first non-empty line."""
368
+ for line in text.split("\n"):
369
+ if line.strip():
370
+ return line[: len(line) - len(line.lstrip())]
371
+ return ""
372
+
373
+
374
+ def _reindent_text(text: str, delta: int, indent_char: str = " ") -> str:
375
+ """Reindent text by adding/removing leading whitespace.
376
+
377
+ Args:
378
+ text: Text to reindent
379
+ delta: Number of characters to add (positive) or remove (negative)
380
+ indent_char: Character to use for indentation (space or tab)
381
+
382
+ Returns:
383
+ Reindented text
384
+ """
385
+ if delta == 0:
386
+ return text
387
+
388
+ lines = text.split("\n")
389
+ result_lines = []
390
+
391
+ for line in lines:
392
+ if not line.strip():
393
+ # Empty or whitespace-only line - keep as is
394
+ result_lines.append(line)
395
+ continue
396
+
397
+ current_indent = len(line) - len(line.lstrip())
398
+
399
+ if delta > 0:
400
+ # Add indentation
401
+ new_line = (indent_char * delta) + line
402
+ else:
403
+ # Remove indentation (but don't go negative)
404
+ chars_to_remove = min(abs(delta), current_indent)
405
+ new_line = line[chars_to_remove:]
406
+
407
+ result_lines.append(new_line)
408
+
409
+ return "\n".join(result_lines)
410
+
411
+
412
+ async def apply_structured_edits(original_content: str, edits_response: str) -> str:
413
+ """Apply structured edits from the agent response."""
414
+ # Parse the edits from the response
415
+ edits_match = re.search(r"<edits>(.*?)</edits>", edits_response, re.DOTALL)
416
+ if not edits_match:
417
+ logger.warning("No edits block found in response")
418
+ return original_content
419
+
420
+ edits_content = edits_match.group(1)
421
+
422
+ # Find all old_text/new_text pairs
423
+ old_text_pattern = r"<old_text[^>]*>(.*?)</old_text>"
424
+ new_text_pattern = r"<new_text>(.*?)</new_text>"
425
+
426
+ old_texts = re.findall(old_text_pattern, edits_content, re.DOTALL)
427
+ new_texts = re.findall(new_text_pattern, edits_content, re.DOTALL)
428
+
429
+ if len(old_texts) != len(new_texts):
430
+ logger.warning("Mismatch between old_text and new_text blocks")
431
+ return original_content
432
+
433
+ # Apply edits sequentially
434
+ content = original_content
435
+ applied_edits = 0
436
+
437
+ failed_matches = []
438
+ multiple_matches = []
439
+
440
+ for old_text, new_text in zip(old_texts, new_texts, strict=False):
441
+ old_cleaned = old_text.strip()
442
+ new_cleaned = new_text.strip()
443
+
444
+ # Check for multiple matches (ambiguity)
445
+ match_count = content.count(old_cleaned)
446
+ if match_count > 1:
447
+ multiple_matches.append(old_cleaned[:50])
448
+ elif match_count == 1:
449
+ content = content.replace(old_cleaned, new_cleaned, 1)
450
+ applied_edits += 1
451
+ else:
452
+ failed_matches.append(old_cleaned[:50])
453
+
454
+ # Raise ModelRetry for specific failure cases
455
+ if applied_edits == 0 and len(old_cleaned) > 0:
456
+ msg = (
457
+ "Some edits were produced but none of them could be applied. "
458
+ "Read the relevant sections of the file again so that "
459
+ "I can perform the requested edits."
460
+ )
461
+ raise ModelRetry(msg)
462
+
463
+ if multiple_matches:
464
+ matches_str = ", ".join(multiple_matches)
465
+ msg = (
466
+ f"<old_text> matches multiple positions in the file: {matches_str}... "
467
+ "Read the relevant sections of the file again and extend <old_text> "
468
+ "to be more specific."
469
+ )
470
+ raise ModelRetry(msg)
471
+
472
+ logger.info("Applied structured edits", num=applied_edits, total=len(old_texts))
473
+ return content
474
+
475
+
476
+ def get_changed_lines(original_content: str, new_content: str, path: str) -> list[str]:
477
+ old = original_content.splitlines(keepends=True)
478
+ new = new_content.splitlines(keepends=True)
479
+ diff = list(difflib.unified_diff(old, new, fromfile=path, tofile=path, lineterm=""))
480
+ return [line for line in diff if line.startswith(("+", "-"))]
481
+
482
+
483
+ def get_changed_line_numbers(original_content: str, new_content: str) -> list[int]:
484
+ """Extract line numbers where changes occurred for ACP UI highlighting.
485
+
486
+ Similar to Claude Code's line tracking for precise change location reporting.
487
+ Returns line numbers in the new content where changes happened.
488
+
489
+ Args:
490
+ original_content: Original file content
491
+ new_content: Modified file content
492
+
493
+ Returns:
494
+ List of line numbers (1-based) where changes occurred in new content
495
+ """
496
+ old_lines = original_content.splitlines(keepends=True)
497
+ new_lines = new_content.splitlines(keepends=True)
498
+ # Use SequenceMatcher to find changed blocks
499
+ matcher = difflib.SequenceMatcher(None, old_lines, new_lines)
500
+ changed_line_numbers = set()
501
+ for tag, _i1, _i2, j1, j2 in matcher.get_opcodes():
502
+ if tag in ("replace", "insert", "delete"):
503
+ # For replacements and insertions, mark lines in new content
504
+ # For deletions, mark the position where deletion occurred
505
+ if tag == "delete":
506
+ # Mark the line where deletion occurred (or next line if at end)
507
+ line_num = min(j1 + 1, len(new_lines))
508
+ if line_num > 0:
509
+ changed_line_numbers.add(line_num)
510
+ else:
511
+ # Mark all affected lines in new content
512
+ for line_num in range(j1 + 1, j2 + 1): # Convert to 1-based
513
+ changed_line_numbers.add(line_num)
514
+
515
+ return sorted(changed_line_numbers)
516
+
517
+
518
+ def truncate_content(content: str, max_size: int = DEFAULT_MAX_SIZE) -> tuple[str, bool]:
519
+ """Truncate text content to a maximum size in bytes.
520
+
521
+ Args:
522
+ content: Text content to truncate
523
+ max_size: Maximum size in bytes (default: 64KB)
524
+
525
+ Returns:
526
+ Tuple of (truncated_content, was_truncated)
527
+ """
528
+ content_bytes = content.encode("utf-8")
529
+ if len(content_bytes) <= max_size:
530
+ return content, False
531
+
532
+ # Truncate at byte boundary and decode safely
533
+ truncated_bytes = content_bytes[:max_size]
534
+ # Avoid breaking UTF-8 sequences by decoding with error handling
535
+ truncated = truncated_bytes.decode("utf-8", errors="ignore")
536
+ return truncated, True
537
+
538
+
539
+ def truncate_lines(
540
+ lines: list[str], offset: int = 0, limit: int | None = None, max_bytes: int = DEFAULT_MAX_SIZE
541
+ ) -> tuple[list[str], bool]:
542
+ """Truncate lines with offset/limit and byte size constraints.
543
+
544
+ Args:
545
+ lines: List of text lines
546
+ offset: Starting line index (0-based)
547
+ limit: Maximum number of lines to include (None = no limit)
548
+ max_bytes: Maximum total bytes (default: 64KB)
549
+
550
+ Returns:
551
+ Tuple of (truncated_lines, was_truncated)
552
+ """
553
+ # Apply offset
554
+ start_idx = max(0, offset)
555
+ if start_idx >= len(lines):
556
+ return [], False
557
+
558
+ # Apply line limit
559
+ end_idx = min(len(lines), start_idx + limit) if limit is not None else len(lines)
560
+
561
+ selected_lines = lines[start_idx:end_idx]
562
+
563
+ # Apply byte limit
564
+ result_lines: list[str] = []
565
+ total_bytes = 0
566
+
567
+ for line in selected_lines:
568
+ line_bytes = len(line.encode("utf-8"))
569
+ if total_bytes + line_bytes > max_bytes:
570
+ # Would exceed limit - this is actual truncation
571
+ return result_lines, True
572
+
573
+ result_lines.append(line)
574
+ total_bytes += line_bytes
575
+
576
+ # Successfully returned all requested content - not truncated
577
+ # (byte truncation already handled above with early return)
578
+ return result_lines, False
579
+
580
+
581
+ def _format_size(size: int) -> str:
582
+ """Format byte size as human-readable string."""
583
+ if size < 1024: # noqa: PLR2004
584
+ return f"{size} B"
585
+ if size < 1024 * 1024:
586
+ return f"{size / 1024:.1f} KB"
587
+ return f"{size / (1024 * 1024):.1f} MB"
588
+
589
+
590
+ def format_directory_listing(
591
+ path: str,
592
+ directories: list[dict[str, Any]],
593
+ files: list[dict[str, Any]],
594
+ pattern: str = "*",
595
+ ) -> str:
596
+ """Format directory listing as markdown table.
597
+
598
+ Args:
599
+ path: Base directory path
600
+ directories: List of directory info dicts
601
+ files: List of file info dicts
602
+ pattern: Glob pattern used
603
+
604
+ Returns:
605
+ Formatted markdown string
606
+ """
607
+ lines = [f"## {path}"]
608
+ if pattern != "*":
609
+ lines.append(f"Pattern: `{pattern}`")
610
+ lines.append("")
611
+
612
+ if not directories and not files:
613
+ lines.append("*Empty directory*")
614
+ return "\n".join(lines)
615
+
616
+ lines.append("| Name | Type | Size |")
617
+ lines.append("|------|------|------|")
618
+
619
+ # Directories first (sorted)
620
+ for d in sorted(directories, key=lambda x: x["name"]):
621
+ lines.append(f"| {d['name']}/ | dir | - |") # noqa: PERF401
622
+
623
+ # Then files (sorted)
624
+ for f in sorted(files, key=lambda x: x["name"]):
625
+ size_str = _format_size(f.get("size", 0))
626
+ lines.append(f"| {f['name']} | file | {size_str} |")
627
+
628
+ lines.append("")
629
+ lines.append(f"*{len(directories)} directories, {len(files)} files*")
630
+
631
+ return "\n".join(lines)