agentpool 2.1.9__py3-none-any.whl → 2.5.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.
- acp/__init__.py +13 -4
- acp/acp_requests.py +20 -77
- acp/agent/connection.py +8 -0
- acp/agent/implementations/debug_server/debug_server.py +6 -2
- acp/agent/protocol.py +6 -0
- acp/bridge/README.md +15 -2
- acp/bridge/__init__.py +3 -2
- acp/bridge/__main__.py +60 -19
- acp/bridge/ws_server.py +173 -0
- acp/bridge/ws_server_cli.py +89 -0
- acp/client/connection.py +38 -29
- acp/client/implementations/default_client.py +3 -2
- acp/client/implementations/headless_client.py +2 -2
- acp/connection.py +2 -2
- acp/notifications.py +20 -50
- acp/schema/__init__.py +2 -0
- acp/schema/agent_responses.py +21 -0
- acp/schema/client_requests.py +3 -3
- acp/schema/session_state.py +63 -29
- acp/stdio.py +39 -9
- acp/task/supervisor.py +2 -2
- acp/transports.py +362 -2
- acp/utils.py +17 -4
- agentpool/__init__.py +6 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +407 -277
- agentpool/agents/acp_agent/acp_converters.py +196 -38
- agentpool/agents/acp_agent/client_handler.py +191 -26
- agentpool/agents/acp_agent/session_state.py +17 -6
- agentpool/agents/agent.py +607 -572
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +176 -110
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +632 -17
- agentpool/agents/claude_code_agent/FORKING.md +191 -0
- agentpool/agents/claude_code_agent/__init__.py +13 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
- agentpool/agents/claude_code_agent/converters.py +74 -143
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/claude_code_agent/models.py +77 -0
- agentpool/agents/claude_code_agent/static_info.py +100 -0
- agentpool/agents/claude_code_agent/usage.py +242 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +24 -0
- agentpool/agents/events/builtin_handlers.py +67 -1
- agentpool/agents/events/event_emitter.py +32 -2
- agentpool/agents/events/events.py +104 -3
- agentpool/agents/events/infer_info.py +145 -0
- agentpool/agents/events/processors.py +254 -0
- agentpool/agents/interactions.py +41 -6
- agentpool/agents/modes.py +67 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +56 -21
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/acp_assistant.yml +2 -2
- agentpool/config_resources/agents.yml +3 -0
- agentpool/config_resources/agents_template.yml +1 -0
- agentpool/config_resources/claude_code_agent.yml +10 -6
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +136 -289
- agentpool/delegation/team.py +58 -57
- agentpool/delegation/teamrun.py +51 -55
- agentpool/diagnostics/__init__.py +53 -0
- agentpool/diagnostics/lsp_manager.py +1593 -0
- agentpool/diagnostics/lsp_proxy.py +41 -0
- agentpool/diagnostics/lsp_proxy_script.py +229 -0
- agentpool/diagnostics/models.py +398 -0
- agentpool/functional/run.py +10 -4
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +76 -32
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +34 -54
- agentpool/mcp_server/registries/official_registry_client.py +35 -1
- agentpool/mcp_server/tool_bridge.py +186 -139
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/connection_manager.py +11 -10
- agentpool/messaging/event_manager.py +5 -5
- agentpool/messaging/message_container.py +6 -30
- agentpool/messaging/message_history.py +99 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +54 -35
- agentpool/messaging/processing.py +12 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -24
- agentpool/models/acp_agents/mcp_capable.py +126 -157
- agentpool/models/acp_agents/non_mcp.py +129 -95
- agentpool/models/agents.py +98 -76
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +144 -19
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +113 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +11 -1
- agentpool/resource_providers/aggregating.py +56 -5
- agentpool/resource_providers/base.py +70 -4
- agentpool/resource_providers/codemode/code_executor.py +72 -5
- agentpool/resource_providers/codemode/helpers.py +2 -2
- agentpool/resource_providers/codemode/provider.py +64 -12
- agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
- agentpool/resource_providers/codemode/remote_provider.py +9 -12
- agentpool/resource_providers/filtering.py +3 -1
- agentpool/resource_providers/mcp_provider.py +89 -12
- agentpool/resource_providers/plan_provider.py +228 -46
- agentpool/resource_providers/pool.py +7 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +4 -2
- agentpool/sessions/__init__.py +4 -1
- agentpool/sessions/manager.py +33 -5
- agentpool/sessions/models.py +59 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +572 -49
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +538 -20
- agentpool/tool_impls/__init__.py +6 -0
- agentpool/tool_impls/agent_cli/__init__.py +42 -0
- agentpool/tool_impls/agent_cli/tool.py +95 -0
- agentpool/tool_impls/bash/__init__.py +64 -0
- agentpool/tool_impls/bash/helpers.py +35 -0
- agentpool/tool_impls/bash/tool.py +171 -0
- agentpool/tool_impls/delete_path/__init__.py +70 -0
- agentpool/tool_impls/delete_path/tool.py +142 -0
- agentpool/tool_impls/download_file/__init__.py +80 -0
- agentpool/tool_impls/download_file/tool.py +183 -0
- agentpool/tool_impls/execute_code/__init__.py +55 -0
- agentpool/tool_impls/execute_code/tool.py +163 -0
- agentpool/tool_impls/grep/__init__.py +80 -0
- agentpool/tool_impls/grep/tool.py +200 -0
- agentpool/tool_impls/list_directory/__init__.py +73 -0
- agentpool/tool_impls/list_directory/tool.py +197 -0
- agentpool/tool_impls/question/__init__.py +42 -0
- agentpool/tool_impls/question/tool.py +127 -0
- agentpool/tool_impls/read/__init__.py +104 -0
- agentpool/tool_impls/read/tool.py +305 -0
- agentpool/tools/__init__.py +2 -1
- agentpool/tools/base.py +114 -34
- agentpool/tools/manager.py +57 -1
- agentpool/ui/base.py +2 -2
- agentpool/ui/mock_provider.py +2 -2
- agentpool/ui/stdlib_provider.py +2 -2
- agentpool/utils/file_watcher.py +269 -0
- agentpool/utils/identifiers.py +121 -0
- agentpool/utils/pydantic_ai_helpers.py +46 -0
- agentpool/utils/streams.py +616 -2
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
- agentpool-2.5.0.dist-info/RECORD +579 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +24 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +100 -21
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +42 -5
- agentpool_commands/agents.py +75 -2
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/pool.py +260 -0
- agentpool_commands/session.py +1 -1
- agentpool_commands/text_sharing/__init__.py +119 -0
- agentpool_commands/text_sharing/base.py +123 -0
- agentpool_commands/text_sharing/github_gist.py +80 -0
- agentpool_commands/text_sharing/opencode.py +462 -0
- agentpool_commands/text_sharing/paste_rs.py +59 -0
- agentpool_commands/text_sharing/pastebin.py +116 -0
- agentpool_commands/text_sharing/shittycodingagent.py +112 -0
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +80 -30
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/converters.py +1 -1
- agentpool_config/event_handlers.py +42 -0
- agentpool_config/events.py +1 -1
- agentpool_config/forward_targets.py +1 -4
- agentpool_config/jinja.py +3 -3
- agentpool_config/mcp_server.py +132 -6
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +82 -38
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -22
- agentpool_config/toolsets.py +109 -233
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +234 -181
- agentpool_server/acp_server/commands/acp_commands.py +151 -156
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +24 -90
- agentpool_server/acp_server/session.py +173 -331
- agentpool_server/acp_server/session_manager.py +8 -34
- agentpool_server/agui_server/server.py +3 -1
- agentpool_server/mcp_server/server.py +5 -2
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +401 -0
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +19 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +975 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +421 -0
- agentpool_server/opencode_server/models/__init__.py +250 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +72 -0
- agentpool_server/opencode_server/models/base.py +26 -0
- agentpool_server/opencode_server/models/common.py +23 -0
- agentpool_server/opencode_server/models/config.py +37 -0
- agentpool_server/opencode_server/models/events.py +821 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +44 -0
- agentpool_server/opencode_server/models/message.py +179 -0
- agentpool_server/opencode_server/models/parts.py +323 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +111 -0
- agentpool_server/opencode_server/routes/__init__.py +29 -0
- agentpool_server/opencode_server/routes/agent_routes.py +473 -0
- agentpool_server/opencode_server/routes/app_routes.py +202 -0
- agentpool_server/opencode_server/routes/config_routes.py +302 -0
- agentpool_server/opencode_server/routes/file_routes.py +571 -0
- agentpool_server/opencode_server/routes/global_routes.py +94 -0
- agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
- agentpool_server/opencode_server/routes/message_routes.py +761 -0
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +300 -0
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +1276 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +475 -0
- agentpool_server/opencode_server/state.py +151 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +12 -0
- agentpool_storage/base.py +184 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/claude_provider/provider.py +1089 -0
- agentpool_storage/file_provider.py +278 -15
- agentpool_storage/memory_provider.py +193 -12
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
- agentpool_storage/opencode_provider/__init__.py +16 -0
- agentpool_storage/opencode_provider/helpers.py +414 -0
- agentpool_storage/opencode_provider/provider.py +895 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +26 -6
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +269 -3
- agentpool_storage/sql_provider/utils.py +12 -13
- agentpool_storage/zed_provider/__init__.py +16 -0
- agentpool_storage/zed_provider/helpers.py +281 -0
- agentpool_storage/zed_provider/models.py +130 -0
- agentpool_storage/zed_provider/provider.py +442 -0
- agentpool_storage/zed_provider.py +803 -0
- agentpool_toolsets/__init__.py +0 -2
- agentpool_toolsets/builtin/__init__.py +2 -12
- agentpool_toolsets/builtin/code.py +96 -57
- agentpool_toolsets/builtin/debug.py +118 -48
- agentpool_toolsets/builtin/execution_environment.py +115 -230
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +9 -4
- agentpool_toolsets/builtin/subagent_tools.py +64 -51
- agentpool_toolsets/builtin/workers.py +4 -2
- agentpool_toolsets/composio_toolset.py +2 -2
- agentpool_toolsets/entry_points.py +3 -1
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +99 -7
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +511 -0
- agentpool_toolsets/mcp_run_toolset.py +87 -12
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- agentpool-2.1.9.dist-info/RECORD +0 -474
- agentpool_config/resources.py +0 -33
- agentpool_server/acp_server/acp_tools.py +0 -43
- agentpool_server/acp_server/commands/spawn.py +0 -210
- agentpool_storage/text_log_provider.py +0 -275
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -85,6 +85,66 @@ async def detect_grep_backend(env: ExecutionEnvironment) -> GrepBackend:
|
|
|
85
85
|
return GrepBackend.PYTHON
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def _is_path_inside_ignored_dir(path: str) -> bool:
|
|
89
|
+
"""Check if path is explicitly inside a commonly ignored directory.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
path: The search path
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
True if path is inside a directory that would typically be gitignored
|
|
96
|
+
"""
|
|
97
|
+
# Common ignored directory patterns (without trailing slash)
|
|
98
|
+
ignored_dirs = [
|
|
99
|
+
".venv",
|
|
100
|
+
"venv",
|
|
101
|
+
".env",
|
|
102
|
+
"env",
|
|
103
|
+
"node_modules",
|
|
104
|
+
".git",
|
|
105
|
+
"__pycache__",
|
|
106
|
+
".pytest_cache",
|
|
107
|
+
".mypy_cache",
|
|
108
|
+
".tox",
|
|
109
|
+
".nox",
|
|
110
|
+
]
|
|
111
|
+
for ignored in ignored_dirs:
|
|
112
|
+
if (
|
|
113
|
+
path.startswith((f"{ignored}/", f"./{ignored}/"))
|
|
114
|
+
or f"/{ignored}/" in path
|
|
115
|
+
or path == ignored
|
|
116
|
+
):
|
|
117
|
+
return True
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _filter_exclude_patterns(path: str, exclude_patterns: list[str]) -> list[str]:
|
|
122
|
+
"""Filter out exclude patterns that the search path is explicitly inside.
|
|
123
|
+
|
|
124
|
+
If user explicitly searches inside an excluded directory (e.g., .venv/),
|
|
125
|
+
don't exclude that directory from results.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
path: The search path
|
|
129
|
+
exclude_patterns: List of patterns to potentially exclude
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Filtered list of exclude patterns
|
|
133
|
+
"""
|
|
134
|
+
filtered = []
|
|
135
|
+
for pattern in exclude_patterns:
|
|
136
|
+
# Normalize pattern (remove trailing slash for comparison)
|
|
137
|
+
pattern_normalized = pattern.rstrip("/")
|
|
138
|
+
# Check if the search path starts with or contains this excluded dir
|
|
139
|
+
# e.g., path=".venv/lib/python" should not exclude ".venv/"
|
|
140
|
+
if not (
|
|
141
|
+
path.startswith((pattern_normalized, f"./{pattern_normalized}"))
|
|
142
|
+
or f"/{pattern_normalized}/" in path
|
|
143
|
+
):
|
|
144
|
+
filtered.append(pattern)
|
|
145
|
+
return filtered
|
|
146
|
+
|
|
147
|
+
|
|
88
148
|
async def grep_with_subprocess(
|
|
89
149
|
env: ExecutionEnvironment,
|
|
90
150
|
pattern: str,
|
|
@@ -124,11 +184,23 @@ async def grep_with_subprocess(
|
|
|
124
184
|
msg = "Subprocess grep requested but no grep/ripgrep found"
|
|
125
185
|
raise ValueError(msg)
|
|
126
186
|
|
|
127
|
-
|
|
187
|
+
base_exclude = exclude_patterns or DEFAULT_EXCLUDE_PATTERNS
|
|
188
|
+
# Filter out patterns that the user is explicitly searching inside
|
|
189
|
+
exclude = _filter_exclude_patterns(path, base_exclude)
|
|
190
|
+
|
|
191
|
+
# Disable gitignore if explicitly searching inside an ignored directory
|
|
192
|
+
# (e.g., searching .venv/ when .venv is in .gitignore)
|
|
193
|
+
effective_use_gitignore = use_gitignore and not _is_path_inside_ignored_dir(path)
|
|
128
194
|
|
|
129
195
|
if backend == GrepBackend.RIPGREP:
|
|
130
196
|
cmd_list = _build_ripgrep_command(
|
|
131
|
-
pattern,
|
|
197
|
+
pattern,
|
|
198
|
+
path,
|
|
199
|
+
case_sensitive,
|
|
200
|
+
max_matches,
|
|
201
|
+
exclude,
|
|
202
|
+
effective_use_gitignore,
|
|
203
|
+
context_lines,
|
|
132
204
|
)
|
|
133
205
|
else:
|
|
134
206
|
cmd_list = _build_gnu_grep_command(
|
|
@@ -256,19 +328,27 @@ def _parse_ripgrep_json_output(stdout: str, max_output_bytes: int) -> dict[str,
|
|
|
256
328
|
matches: list[dict[str, Any]] = []
|
|
257
329
|
total_bytes = 0
|
|
258
330
|
was_truncated = False
|
|
331
|
+
match_count = 0 # Track actual matches (not context lines)
|
|
259
332
|
|
|
260
333
|
for line in stdout.splitlines():
|
|
261
334
|
if not line.strip():
|
|
262
335
|
continue
|
|
263
336
|
try:
|
|
264
337
|
data = anyenv.load_json(line, return_type=dict)
|
|
265
|
-
|
|
338
|
+
entry_type = data.get("type")
|
|
339
|
+
|
|
340
|
+
if entry_type in ("match", "context"):
|
|
266
341
|
match_data = data.get("data", {})
|
|
267
342
|
path = match_data.get("path", {}).get("text", "")
|
|
268
343
|
line_num = match_data.get("line_number", 0)
|
|
269
344
|
content = match_data.get("lines", {}).get("text", "").rstrip("\n")
|
|
270
345
|
|
|
271
|
-
match_entry = {
|
|
346
|
+
match_entry = {
|
|
347
|
+
"path": path,
|
|
348
|
+
"line": line_num,
|
|
349
|
+
"content": content,
|
|
350
|
+
"is_match": entry_type == "match",
|
|
351
|
+
}
|
|
272
352
|
entry_size = len(path) + len(str(line_num)) + len(content) + 10
|
|
273
353
|
|
|
274
354
|
if total_bytes + entry_size > max_output_bytes:
|
|
@@ -277,6 +357,9 @@ def _parse_ripgrep_json_output(stdout: str, max_output_bytes: int) -> dict[str,
|
|
|
277
357
|
|
|
278
358
|
matches.append(match_entry)
|
|
279
359
|
total_bytes += entry_size
|
|
360
|
+
|
|
361
|
+
if entry_type == "match":
|
|
362
|
+
match_count += 1
|
|
280
363
|
except anyenv.JsonLoadError:
|
|
281
364
|
continue
|
|
282
365
|
|
|
@@ -291,11 +374,13 @@ def _parse_ripgrep_json_output(stdout: str, max_output_bytes: int) -> dict[str,
|
|
|
291
374
|
# Truncate long lines for readability
|
|
292
375
|
if len(content) > 100: # noqa: PLR2004
|
|
293
376
|
content = content[:97] + "..."
|
|
294
|
-
|
|
377
|
+
# Mark match lines with indicator if there are context lines
|
|
378
|
+
prefix = "→ " if m.get("is_match") and match_count < len(matches) else ""
|
|
379
|
+
table_lines.append(f"| {m['path']} | {m['line']} | {prefix}{content} |")
|
|
295
380
|
|
|
296
381
|
return {
|
|
297
382
|
"matches": "\n".join(table_lines),
|
|
298
|
-
"match_count":
|
|
383
|
+
"match_count": match_count,
|
|
299
384
|
"was_truncated": was_truncated,
|
|
300
385
|
}
|
|
301
386
|
|
|
@@ -334,10 +419,17 @@ def _parse_gnu_grep_output(stdout: str, max_output_bytes: int) -> dict[str, Any]
|
|
|
334
419
|
|
|
335
420
|
table_lines = ["| File | Line | Content |", "|------|------|---------|"]
|
|
336
421
|
for m in matches:
|
|
422
|
+
# Handle separator lines
|
|
423
|
+
if m.get("is_separator"):
|
|
424
|
+
table_lines.append("| | | |")
|
|
425
|
+
continue
|
|
426
|
+
|
|
337
427
|
content = m["content"].replace("|", "\\|")
|
|
338
428
|
if len(content) > 100: # noqa: PLR2004
|
|
339
429
|
content = content[:97] + "..."
|
|
340
|
-
|
|
430
|
+
# Mark match lines with indicator if there are context lines
|
|
431
|
+
prefix = "→ " if m.get("is_match") else ""
|
|
432
|
+
table_lines.append(f"| {m['path']} | {m['line']} | {prefix}{content} |")
|
|
341
433
|
|
|
342
434
|
return {
|
|
343
435
|
"matches": "\n".join(table_lines),
|
|
@@ -550,8 +550,9 @@ def truncate_lines(
|
|
|
550
550
|
Returns:
|
|
551
551
|
Tuple of (truncated_lines, was_truncated)
|
|
552
552
|
"""
|
|
553
|
-
# Apply offset
|
|
554
|
-
start_idx = max(0, offset)
|
|
553
|
+
# Apply offset (supports negative indexing like Python lists)
|
|
554
|
+
start_idx = max(0, len(lines) + offset) if offset < 0 else min(offset, len(lines))
|
|
555
|
+
|
|
555
556
|
if start_idx >= len(lines):
|
|
556
557
|
return [], False
|
|
557
558
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Image processing utilities for the fsspec toolset."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# 4.5MB - provides headroom below Anthropic's 5MB limit
|
|
9
|
+
DEFAULT_MAX_BYTES = int(4.5 * 1024 * 1024)
|
|
10
|
+
|
|
11
|
+
# Minimum dimension for resized images
|
|
12
|
+
MIN_DIMENSION = 100
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _pick_smaller(
|
|
16
|
+
a: tuple[bytes, str],
|
|
17
|
+
b: tuple[bytes, str],
|
|
18
|
+
) -> tuple[bytes, str]:
|
|
19
|
+
"""Pick the smaller of two image buffers."""
|
|
20
|
+
return a if len(a[0]) <= len(b[0]) else b
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _make_resize_note(
|
|
24
|
+
original_width: int,
|
|
25
|
+
original_height: int,
|
|
26
|
+
final_width: int,
|
|
27
|
+
final_height: int,
|
|
28
|
+
suffix: str = "",
|
|
29
|
+
) -> str:
|
|
30
|
+
"""Create a resize note string."""
|
|
31
|
+
scale = original_width / final_width if final_width > 0 else 1.0
|
|
32
|
+
base = f"[Image resized: {original_width}x{original_height} → {final_width}x{final_height}"
|
|
33
|
+
return f"{base}, scale={scale:.2f}x{suffix}]"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resize_image_if_needed(
|
|
37
|
+
data: bytes,
|
|
38
|
+
media_type: str,
|
|
39
|
+
max_size: int,
|
|
40
|
+
max_bytes: int | None = None,
|
|
41
|
+
jpeg_quality: int = 80,
|
|
42
|
+
) -> tuple[bytes, str, str | None]:
|
|
43
|
+
"""Resize image if it exceeds limits, preserving aspect ratio.
|
|
44
|
+
|
|
45
|
+
Returns the original image if it already fits within all limits.
|
|
46
|
+
|
|
47
|
+
Strategy for staying under max_bytes:
|
|
48
|
+
1. First resize to max_size dimensions
|
|
49
|
+
2. Try both PNG and JPEG formats, pick the smaller one
|
|
50
|
+
3. If still too large, try JPEG with decreasing quality
|
|
51
|
+
4. If still too large, progressively reduce dimensions
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
data: Raw image bytes
|
|
55
|
+
media_type: MIME type of the image
|
|
56
|
+
max_size: Maximum width/height in pixels
|
|
57
|
+
max_bytes: Maximum file size in bytes (default: 4.5MB)
|
|
58
|
+
jpeg_quality: Initial quality for JPEG output (1-100)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Tuple of (image_data, media_type, dimension_note).
|
|
62
|
+
dimension_note is None if no resizing was needed, otherwise contains
|
|
63
|
+
a message about the resize for the model to map coordinates.
|
|
64
|
+
"""
|
|
65
|
+
from PIL import Image
|
|
66
|
+
|
|
67
|
+
max_bytes = max_bytes if max_bytes is not None else DEFAULT_MAX_BYTES
|
|
68
|
+
|
|
69
|
+
img = Image.open(BytesIO(data))
|
|
70
|
+
original_width, original_height = img.size
|
|
71
|
+
|
|
72
|
+
# Check if already within all limits (dimensions AND size)
|
|
73
|
+
original_size = len(data)
|
|
74
|
+
if original_width <= max_size and original_height <= max_size and original_size <= max_bytes:
|
|
75
|
+
return data, media_type, None
|
|
76
|
+
|
|
77
|
+
# Calculate initial dimensions respecting max limits
|
|
78
|
+
target_width = original_width
|
|
79
|
+
target_height = original_height
|
|
80
|
+
|
|
81
|
+
if target_width > max_size:
|
|
82
|
+
target_height = round(target_height * max_size / target_width)
|
|
83
|
+
target_width = max_size
|
|
84
|
+
if target_height > max_size:
|
|
85
|
+
target_width = round(target_width * max_size / target_height)
|
|
86
|
+
target_height = max_size
|
|
87
|
+
|
|
88
|
+
def try_both_formats(
|
|
89
|
+
img: Image.Image,
|
|
90
|
+
width: int,
|
|
91
|
+
height: int,
|
|
92
|
+
quality: int,
|
|
93
|
+
) -> tuple[bytes, str]:
|
|
94
|
+
"""Resize and encode in both formats, returning the smaller one."""
|
|
95
|
+
resized = img.resize((width, height), Image.Resampling.LANCZOS)
|
|
96
|
+
|
|
97
|
+
# Convert to RGB for JPEG if needed (handles RGBA, P mode, etc.)
|
|
98
|
+
if resized.mode in ("RGBA", "LA", "P"):
|
|
99
|
+
rgb_img = Image.new("RGB", resized.size, (255, 255, 255))
|
|
100
|
+
if resized.mode == "P":
|
|
101
|
+
resized = resized.convert("RGBA")
|
|
102
|
+
rgb_img.paste(resized, mask=resized.split()[-1] if resized.mode == "RGBA" else None)
|
|
103
|
+
jpeg_source = rgb_img
|
|
104
|
+
else:
|
|
105
|
+
jpeg_source = resized.convert("RGB") if resized.mode != "RGB" else resized
|
|
106
|
+
|
|
107
|
+
# Try PNG
|
|
108
|
+
png_buf = BytesIO()
|
|
109
|
+
resized.save(png_buf, format="PNG", optimize=True)
|
|
110
|
+
png_data = png_buf.getvalue()
|
|
111
|
+
|
|
112
|
+
# Try JPEG
|
|
113
|
+
jpeg_buf = BytesIO()
|
|
114
|
+
jpeg_source.save(jpeg_buf, format="JPEG", quality=quality, optimize=True)
|
|
115
|
+
jpeg_data = jpeg_buf.getvalue()
|
|
116
|
+
|
|
117
|
+
return _pick_smaller((png_data, "image/png"), (jpeg_data, "image/jpeg"))
|
|
118
|
+
|
|
119
|
+
# Quality and scale steps for progressive reduction
|
|
120
|
+
quality_steps = [85, 70, 55, 40]
|
|
121
|
+
scale_steps = [1.0, 0.75, 0.5, 0.35, 0.25]
|
|
122
|
+
|
|
123
|
+
final_width = target_width
|
|
124
|
+
final_height = target_height
|
|
125
|
+
|
|
126
|
+
# First attempt: resize to target dimensions, try both formats
|
|
127
|
+
best_data, best_type = try_both_formats(img, target_width, target_height, jpeg_quality)
|
|
128
|
+
|
|
129
|
+
if len(best_data) <= max_bytes:
|
|
130
|
+
note = _make_resize_note(original_width, original_height, final_width, final_height)
|
|
131
|
+
return best_data, best_type, note
|
|
132
|
+
|
|
133
|
+
# Still too large - try with decreasing quality
|
|
134
|
+
for quality in quality_steps:
|
|
135
|
+
best_data, best_type = try_both_formats(img, target_width, target_height, quality)
|
|
136
|
+
|
|
137
|
+
if len(best_data) <= max_bytes:
|
|
138
|
+
note = _make_resize_note(original_width, original_height, final_width, final_height)
|
|
139
|
+
return best_data, best_type, note
|
|
140
|
+
|
|
141
|
+
# Still too large - reduce dimensions progressively
|
|
142
|
+
for scale_factor in scale_steps:
|
|
143
|
+
final_width = round(target_width * scale_factor)
|
|
144
|
+
final_height = round(target_height * scale_factor)
|
|
145
|
+
|
|
146
|
+
# Skip if dimensions are too small
|
|
147
|
+
if final_width < MIN_DIMENSION or final_height < MIN_DIMENSION:
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
for quality in quality_steps:
|
|
151
|
+
best_data, best_type = try_both_formats(img, final_width, final_height, quality)
|
|
152
|
+
|
|
153
|
+
if len(best_data) <= max_bytes:
|
|
154
|
+
note = _make_resize_note(original_width, original_height, final_width, final_height)
|
|
155
|
+
return best_data, best_type, note
|
|
156
|
+
|
|
157
|
+
# Last resort: return smallest version we produced even if over limit
|
|
158
|
+
note = _make_resize_note(
|
|
159
|
+
original_width, original_height, final_width, final_height, " (may exceed size limit)"
|
|
160
|
+
)
|
|
161
|
+
return best_data, best_type, note
|