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
agentpool/tools/manager.py
CHANGED
|
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
|
|
|
20
20
|
from agentpool.prompts.prompts import MCPClientPrompt
|
|
21
21
|
from agentpool.resource_providers import ResourceProvider
|
|
22
22
|
from agentpool.resource_providers.codemode.provider import CodeModeResourceProvider
|
|
23
|
+
from agentpool.resource_providers.resource_info import ResourceInfo
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
logger = get_logger(__name__)
|
|
@@ -186,6 +187,8 @@ class ToolManager:
|
|
|
186
187
|
async def list_prompts(self) -> list[MCPClientPrompt]:
|
|
187
188
|
"""Get all prompts from all providers."""
|
|
188
189
|
from agentpool.mcp_server.manager import MCPManager
|
|
190
|
+
from agentpool.prompts.prompts import MCPClientPrompt as MCPPrompt
|
|
191
|
+
from agentpool.resource_providers import AggregatingResourceProvider
|
|
189
192
|
|
|
190
193
|
all_prompts: list[MCPClientPrompt] = []
|
|
191
194
|
# Get prompts from all external providers (check if they're MCP providers)
|
|
@@ -195,12 +198,65 @@ class ToolManager:
|
|
|
195
198
|
# Get prompts from MCP providers via the aggregating provider
|
|
196
199
|
agg_provider = provider.get_aggregating_provider()
|
|
197
200
|
prompts = await agg_provider.get_prompts()
|
|
198
|
-
|
|
201
|
+
# Filter to only MCPClientPrompt instances
|
|
202
|
+
mcp_prompts = [p for p in prompts if isinstance(p, MCPPrompt)]
|
|
203
|
+
all_prompts.extend(mcp_prompts)
|
|
204
|
+
except Exception:
|
|
205
|
+
logger.exception("Failed to get prompts from provider", provider=provider)
|
|
206
|
+
elif isinstance(provider, AggregatingResourceProvider):
|
|
207
|
+
try:
|
|
208
|
+
# AggregatingResourceProvider can directly provide prompts
|
|
209
|
+
prompts = await provider.get_prompts()
|
|
210
|
+
# Filter to only MCPClientPrompt instances
|
|
211
|
+
mcp_prompts = [p for p in prompts if isinstance(p, MCPPrompt)]
|
|
212
|
+
all_prompts.extend(mcp_prompts)
|
|
199
213
|
except Exception:
|
|
200
214
|
logger.exception("Failed to get prompts from provider", provider=provider)
|
|
201
215
|
|
|
202
216
|
return all_prompts
|
|
203
217
|
|
|
218
|
+
async def list_resources(self) -> list[ResourceInfo]:
|
|
219
|
+
"""Get all resources from all providers.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of ResourceInfo objects from all providers
|
|
223
|
+
"""
|
|
224
|
+
all_resources: list[ResourceInfo] = []
|
|
225
|
+
# Get resources from all providers concurrently
|
|
226
|
+
provider_coroutines = [provider.get_resources() for provider in self.providers]
|
|
227
|
+
results = await asyncio.gather(*provider_coroutines, return_exceptions=True)
|
|
228
|
+
|
|
229
|
+
for provider, result in zip(self.providers, results, strict=False):
|
|
230
|
+
if isinstance(result, BaseException):
|
|
231
|
+
logger.warning(
|
|
232
|
+
"Failed to get resources from provider",
|
|
233
|
+
provider=provider.name,
|
|
234
|
+
error=str(result),
|
|
235
|
+
)
|
|
236
|
+
continue
|
|
237
|
+
all_resources.extend(result)
|
|
238
|
+
|
|
239
|
+
return all_resources
|
|
240
|
+
|
|
241
|
+
async def get_resource(self, name: str) -> ResourceInfo:
|
|
242
|
+
"""Get a specific resource by name.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
name: Name of the resource to find
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
ResourceInfo for the requested resource
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
ToolError: If resource not found
|
|
252
|
+
"""
|
|
253
|
+
resources = await self.list_resources()
|
|
254
|
+
resource: ResourceInfo | None = next((r for r in resources if r.name == name), None)
|
|
255
|
+
if not resource:
|
|
256
|
+
msg = f"Resource not found: {name}"
|
|
257
|
+
raise ToolError(msg)
|
|
258
|
+
return resource
|
|
259
|
+
|
|
204
260
|
@asynccontextmanager
|
|
205
261
|
async def temporary_tools(
|
|
206
262
|
self,
|
agentpool/ui/base.py
CHANGED
|
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
|
|
12
12
|
from mcp import types
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
|
|
15
|
-
from agentpool.agents.context import ConfirmationResult
|
|
15
|
+
from agentpool.agents.context import AgentContext, ConfirmationResult
|
|
16
16
|
from agentpool.messaging import ChatMessage
|
|
17
17
|
from agentpool.messaging.context import NodeContext
|
|
18
18
|
from agentpool.tools.base import Tool
|
|
@@ -62,7 +62,7 @@ class InputProvider(ABC):
|
|
|
62
62
|
@abstractmethod
|
|
63
63
|
def get_tool_confirmation(
|
|
64
64
|
self,
|
|
65
|
-
context:
|
|
65
|
+
context: AgentContext[Any],
|
|
66
66
|
tool: Tool,
|
|
67
67
|
args: dict[str, Any],
|
|
68
68
|
message_history: list[ChatMessage[Any]] | None = None,
|
agentpool/ui/mock_provider.py
CHANGED
|
@@ -11,7 +11,7 @@ from agentpool.ui.base import InputProvider
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from mcp import types
|
|
13
13
|
|
|
14
|
-
from agentpool.agents.context import ConfirmationResult
|
|
14
|
+
from agentpool.agents.context import AgentContext, ConfirmationResult
|
|
15
15
|
from agentpool.messaging import ChatMessage
|
|
16
16
|
from agentpool.messaging.context import NodeContext
|
|
17
17
|
from agentpool.tools.base import Tool
|
|
@@ -58,7 +58,7 @@ class MockInputProvider(InputProvider):
|
|
|
58
58
|
|
|
59
59
|
async def get_tool_confirmation(
|
|
60
60
|
self,
|
|
61
|
-
context:
|
|
61
|
+
context: AgentContext[Any],
|
|
62
62
|
tool: Tool,
|
|
63
63
|
args: dict[str, Any],
|
|
64
64
|
message_history: list[ChatMessage[Any]] | None = None,
|
agentpool/ui/stdlib_provider.py
CHANGED
|
@@ -17,7 +17,7 @@ from agentpool.ui.base import InputProvider
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
18
|
from pydantic import BaseModel
|
|
19
19
|
|
|
20
|
-
from agentpool.agents.context import ConfirmationResult
|
|
20
|
+
from agentpool.agents.context import AgentContext, ConfirmationResult
|
|
21
21
|
from agentpool.messaging import ChatMessage
|
|
22
22
|
from agentpool.messaging.context import NodeContext
|
|
23
23
|
from agentpool.tools.base import Tool
|
|
@@ -60,7 +60,7 @@ class StdlibInputProvider(InputProvider):
|
|
|
60
60
|
|
|
61
61
|
async def get_tool_confirmation(
|
|
62
62
|
self,
|
|
63
|
-
context:
|
|
63
|
+
context: AgentContext[Any],
|
|
64
64
|
tool: Tool,
|
|
65
65
|
args: dict[str, Any],
|
|
66
66
|
message_history: list[ChatMessage[Any]] | None = None,
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""File watcher utilities using watchfiles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Awaitable, Callable, Set as AbstractSet
|
|
7
|
+
import contextlib
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Self
|
|
12
|
+
|
|
13
|
+
from watchfiles import Change, awatch
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Callback type for file change notifications
|
|
20
|
+
FileChangeCallback = Callable[[AbstractSet[tuple[Change, str]]], Awaitable[None]]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class FileWatcher:
|
|
25
|
+
"""Async file watcher using watchfiles.
|
|
26
|
+
|
|
27
|
+
Watches specified paths for changes and calls a callback when changes occur.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
```python
|
|
31
|
+
async def on_change(changes):
|
|
32
|
+
for change_type, path in changes:
|
|
33
|
+
print(f"{change_type}: {path}")
|
|
34
|
+
|
|
35
|
+
watcher = FileWatcher(
|
|
36
|
+
paths=["/path/to/.git/HEAD"],
|
|
37
|
+
callback=on_change,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
async with watcher:
|
|
41
|
+
# Watcher is running
|
|
42
|
+
await asyncio.sleep(60)
|
|
43
|
+
# Watcher stopped
|
|
44
|
+
```
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
paths: list[str | Path]
|
|
48
|
+
"""Paths to watch (files or directories)."""
|
|
49
|
+
|
|
50
|
+
callback: FileChangeCallback
|
|
51
|
+
"""Async callback invoked when changes are detected."""
|
|
52
|
+
|
|
53
|
+
debounce: int = 100
|
|
54
|
+
"""Debounce time in milliseconds."""
|
|
55
|
+
|
|
56
|
+
_task: asyncio.Task[None] | None = field(default=None, repr=False)
|
|
57
|
+
"""Background watch task."""
|
|
58
|
+
|
|
59
|
+
_stop_event: asyncio.Event = field(default_factory=asyncio.Event, repr=False)
|
|
60
|
+
"""Event to signal stop."""
|
|
61
|
+
|
|
62
|
+
async def start(self) -> None:
|
|
63
|
+
"""Start watching for file changes."""
|
|
64
|
+
if self._task is not None:
|
|
65
|
+
return # Already running
|
|
66
|
+
|
|
67
|
+
self._stop_event.clear()
|
|
68
|
+
self._task = asyncio.create_task(self._watch_loop())
|
|
69
|
+
|
|
70
|
+
async def stop(self) -> None:
|
|
71
|
+
"""Stop watching for file changes."""
|
|
72
|
+
if self._task is None:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
self._stop_event.set()
|
|
76
|
+
self._task.cancel()
|
|
77
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
78
|
+
await self._task
|
|
79
|
+
self._task = None
|
|
80
|
+
|
|
81
|
+
async def _watch_loop(self) -> None:
|
|
82
|
+
"""Internal watch loop."""
|
|
83
|
+
str_paths = [str(p) for p in self.paths]
|
|
84
|
+
|
|
85
|
+
# Filter to only existing paths
|
|
86
|
+
existing_paths = [p for p in str_paths if Path(p).exists()]
|
|
87
|
+
if not existing_paths:
|
|
88
|
+
logger.warning("FileWatcher: no existing paths to watch from %s", str_paths)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
logger.info("FileWatcher: starting watch loop for %s", existing_paths)
|
|
92
|
+
try:
|
|
93
|
+
async for changes in awatch(
|
|
94
|
+
*existing_paths,
|
|
95
|
+
debounce=self.debounce,
|
|
96
|
+
stop_event=self._stop_event,
|
|
97
|
+
):
|
|
98
|
+
logger.info("FileWatcher detected changes: %s", changes)
|
|
99
|
+
# Don't let callback errors kill the watcher
|
|
100
|
+
try:
|
|
101
|
+
await self.callback(changes)
|
|
102
|
+
except Exception:
|
|
103
|
+
logger.exception("Error in file watcher callback")
|
|
104
|
+
except Exception:
|
|
105
|
+
logger.exception("FileWatcher watch loop failed")
|
|
106
|
+
|
|
107
|
+
async def __aenter__(self) -> Self:
|
|
108
|
+
"""Start watcher on context enter."""
|
|
109
|
+
await self.start()
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
async def __aexit__(self, *args: object) -> None:
|
|
113
|
+
"""Stop watcher on context exit."""
|
|
114
|
+
await self.stop()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def get_git_branch(repo_path: str | Path) -> str | None:
|
|
118
|
+
"""Get the current git branch name.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
repo_path: Path to the git repository
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Branch name or None if not a git repo or on detached HEAD
|
|
125
|
+
|
|
126
|
+
TODO: For remote/ACP support, this should accept an optional ExecutionEnvironment
|
|
127
|
+
and use env.execute_command() instead of subprocess. This would allow git commands
|
|
128
|
+
to run on the client side where the repository lives.
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
proc = await asyncio.create_subprocess_exec(
|
|
132
|
+
"git",
|
|
133
|
+
"rev-parse",
|
|
134
|
+
"--abbrev-ref",
|
|
135
|
+
"HEAD",
|
|
136
|
+
cwd=str(repo_path),
|
|
137
|
+
stdout=asyncio.subprocess.PIPE,
|
|
138
|
+
stderr=asyncio.subprocess.PIPE,
|
|
139
|
+
)
|
|
140
|
+
stdout, _ = await proc.communicate()
|
|
141
|
+
if proc.returncode != 0:
|
|
142
|
+
return None
|
|
143
|
+
except OSError:
|
|
144
|
+
return None
|
|
145
|
+
else:
|
|
146
|
+
branch = stdout.decode().strip()
|
|
147
|
+
return branch if branch != "HEAD" else None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class GitBranchWatcher:
|
|
152
|
+
"""Watches for git branch changes using polling.
|
|
153
|
+
|
|
154
|
+
Polls the current git branch periodically and calls a callback when it changes.
|
|
155
|
+
Uses polling instead of file watching because git uses atomic renames which
|
|
156
|
+
are not reliably detected by inotify/watchfiles.
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
```python
|
|
160
|
+
async def on_branch_change(branch: str | None):
|
|
161
|
+
print(f"Branch changed to: {branch}")
|
|
162
|
+
|
|
163
|
+
watcher = GitBranchWatcher(
|
|
164
|
+
repo_path="/path/to/repo",
|
|
165
|
+
callback=on_branch_change,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
async with watcher:
|
|
169
|
+
await asyncio.sleep(60)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
TODO: For remote/ACP support, this should accept an ExecutionEnvironment
|
|
173
|
+
and run git commands through env.execute_command(). The polling would still
|
|
174
|
+
happen server-side, but the git commands would execute on the client.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
repo_path: str | Path
|
|
178
|
+
"""Path to the git repository."""
|
|
179
|
+
|
|
180
|
+
callback: Callable[[str | None], Awaitable[None]]
|
|
181
|
+
"""Async callback invoked with new branch name when branch changes."""
|
|
182
|
+
|
|
183
|
+
poll_interval: float = 1.0
|
|
184
|
+
"""Polling interval in seconds."""
|
|
185
|
+
|
|
186
|
+
_current_branch: str | None = field(default=None, repr=False)
|
|
187
|
+
"""Cached current branch."""
|
|
188
|
+
|
|
189
|
+
_task: asyncio.Task[None] | None = field(default=None, repr=False)
|
|
190
|
+
"""Background polling task."""
|
|
191
|
+
|
|
192
|
+
_stop_event: asyncio.Event = field(default_factory=asyncio.Event, repr=False)
|
|
193
|
+
"""Event to signal stop."""
|
|
194
|
+
|
|
195
|
+
async def start(self) -> None:
|
|
196
|
+
"""Start watching for branch changes."""
|
|
197
|
+
if self._task is not None:
|
|
198
|
+
return # Already running
|
|
199
|
+
|
|
200
|
+
repo = Path(self.repo_path)
|
|
201
|
+
git_dir = repo / ".git"
|
|
202
|
+
|
|
203
|
+
# Handle git worktrees - .git might be a file pointing to the real git dir
|
|
204
|
+
if git_dir.is_file():
|
|
205
|
+
content = git_dir.read_text().strip()
|
|
206
|
+
if content.startswith("gitdir:"):
|
|
207
|
+
git_dir = Path(content[7:].strip())
|
|
208
|
+
|
|
209
|
+
if not git_dir.exists():
|
|
210
|
+
logger.warning("Git directory not found: %s", git_dir)
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
# Get initial branch
|
|
214
|
+
self._current_branch = await get_git_branch(self.repo_path)
|
|
215
|
+
logger.info(
|
|
216
|
+
"GitBranchWatcher started (polling), repo: %s, initial branch: %s",
|
|
217
|
+
self.repo_path,
|
|
218
|
+
self._current_branch,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
self._stop_event.clear()
|
|
222
|
+
self._task = asyncio.create_task(self._poll_loop())
|
|
223
|
+
|
|
224
|
+
async def _poll_loop(self) -> None:
|
|
225
|
+
"""Internal polling loop."""
|
|
226
|
+
while not self._stop_event.is_set():
|
|
227
|
+
try:
|
|
228
|
+
await asyncio.wait_for(
|
|
229
|
+
self._stop_event.wait(),
|
|
230
|
+
timeout=self.poll_interval,
|
|
231
|
+
)
|
|
232
|
+
break # Stop event was set
|
|
233
|
+
except TimeoutError:
|
|
234
|
+
# Poll interval elapsed, check for changes
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
new_branch = await get_git_branch(self.repo_path)
|
|
239
|
+
if new_branch != self._current_branch:
|
|
240
|
+
logger.info("Branch changed: %s -> %s", self._current_branch, new_branch)
|
|
241
|
+
self._current_branch = new_branch
|
|
242
|
+
await self.callback(new_branch)
|
|
243
|
+
except Exception:
|
|
244
|
+
logger.exception("Error polling git branch")
|
|
245
|
+
|
|
246
|
+
async def stop(self) -> None:
|
|
247
|
+
"""Stop watching for branch changes."""
|
|
248
|
+
if self._task is None:
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
self._stop_event.set()
|
|
252
|
+
self._task.cancel()
|
|
253
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
254
|
+
await self._task
|
|
255
|
+
self._task = None
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def current_branch(self) -> str | None:
|
|
259
|
+
"""Get the current cached branch name."""
|
|
260
|
+
return self._current_branch
|
|
261
|
+
|
|
262
|
+
async def __aenter__(self) -> Self:
|
|
263
|
+
"""Start watcher on context enter."""
|
|
264
|
+
await self.start()
|
|
265
|
+
return self
|
|
266
|
+
|
|
267
|
+
async def __aexit__(self, *args: object) -> None:
|
|
268
|
+
"""Stop watcher on context exit."""
|
|
269
|
+
await self.stop()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Identifier generation utilities.
|
|
2
|
+
|
|
3
|
+
Generates IDs that are lexicographically sortable by creation time.
|
|
4
|
+
Format: {prefix}_{hex_timestamp}{random_base62}
|
|
5
|
+
|
|
6
|
+
Compatible with OpenCode's identifier format.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import secrets
|
|
12
|
+
import time
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
PrefixType = Literal["session", "message", "permission", "user", "part", "pty"]
|
|
17
|
+
|
|
18
|
+
PREFIXES: dict[PrefixType, str] = {
|
|
19
|
+
"session": "ses",
|
|
20
|
+
"message": "msg",
|
|
21
|
+
"permission": "per",
|
|
22
|
+
"user": "usr",
|
|
23
|
+
"part": "prt",
|
|
24
|
+
"pty": "pty",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
28
|
+
ID_LENGTH = 26 # Characters after prefix (12 hex + 14 base62)
|
|
29
|
+
|
|
30
|
+
# State for monotonic ID generation
|
|
31
|
+
_last_timestamp = 0
|
|
32
|
+
_counter = 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _random_base62(length: int) -> str:
|
|
36
|
+
"""Generate random base62 string."""
|
|
37
|
+
return "".join(secrets.choice(BASE62_CHARS) for _ in range(length))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def ascending(prefix: PrefixType, given: str | None = None) -> str:
|
|
41
|
+
"""Generate an ascending (chronologically sortable) ID.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
prefix: The type prefix for the ID
|
|
45
|
+
given: If provided, validate and return this ID instead of generating
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
A sortable ID with the format {prefix}_{hex_timestamp}{random}
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ValueError: If given ID doesn't start with expected prefix
|
|
52
|
+
"""
|
|
53
|
+
if given is not None:
|
|
54
|
+
expected_prefix = PREFIXES[prefix]
|
|
55
|
+
if not given.startswith(expected_prefix):
|
|
56
|
+
msg = f"ID {given} does not start with {expected_prefix}"
|
|
57
|
+
raise ValueError(msg)
|
|
58
|
+
return given
|
|
59
|
+
|
|
60
|
+
return _create(prefix, descending=False)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def descending(prefix: PrefixType) -> str:
|
|
64
|
+
"""Generate a descending (reverse chronologically sortable) ID.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
prefix: The type prefix for the ID
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A reverse-sortable ID
|
|
71
|
+
"""
|
|
72
|
+
return _create(prefix, descending=True)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _create(prefix: PrefixType, *, descending: bool = False) -> str:
|
|
76
|
+
"""Create a new ID with timestamp encoding.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
prefix: The type prefix
|
|
80
|
+
descending: If True, invert the timestamp for reverse sorting
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
A new ID string
|
|
84
|
+
"""
|
|
85
|
+
global _last_timestamp, _counter # noqa: PLW0603
|
|
86
|
+
|
|
87
|
+
current_timestamp = int(time.time() * 1000) # milliseconds
|
|
88
|
+
|
|
89
|
+
if current_timestamp != _last_timestamp:
|
|
90
|
+
_last_timestamp = current_timestamp
|
|
91
|
+
_counter = 0
|
|
92
|
+
_counter += 1
|
|
93
|
+
|
|
94
|
+
# Combine timestamp and counter
|
|
95
|
+
now = current_timestamp * 0x1000 + _counter
|
|
96
|
+
|
|
97
|
+
if descending:
|
|
98
|
+
now = ~now & 0xFFFFFFFFFFFF # Invert for descending order (48 bits)
|
|
99
|
+
|
|
100
|
+
# Encode as 6 bytes (48 bits), big-endian
|
|
101
|
+
time_bytes = bytearray(6)
|
|
102
|
+
for i in range(6):
|
|
103
|
+
time_bytes[i] = (now >> (40 - 8 * i)) & 0xFF
|
|
104
|
+
|
|
105
|
+
time_hex = time_bytes.hex()
|
|
106
|
+
|
|
107
|
+
# Add random suffix (14 chars for 26 total after prefix)
|
|
108
|
+
random_suffix = _random_base62(ID_LENGTH - 12)
|
|
109
|
+
|
|
110
|
+
return f"{PREFIXES[prefix]}_{time_hex}{random_suffix}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def generate_session_id() -> str:
|
|
114
|
+
"""Generate a unique, chronologically sortable session ID.
|
|
115
|
+
|
|
116
|
+
Convenience function for the common case.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
A session ID like 'ses_b71310fdf001ZHcn6VSpkaBcHi'
|
|
120
|
+
"""
|
|
121
|
+
return ascending("session")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Helper utilities for working with pydantic-ai message types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from pydantic_ai.messages import BaseToolCallPart
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pydantic_ai.messages import ToolCallPartDelta
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def safe_args_as_dict(
|
|
15
|
+
part: BaseToolCallPart | ToolCallPartDelta,
|
|
16
|
+
*,
|
|
17
|
+
default: dict[str, Any] | None = None,
|
|
18
|
+
) -> dict[str, Any]:
|
|
19
|
+
"""Safely extract args as dict from a tool call part.
|
|
20
|
+
|
|
21
|
+
Models can return malformed JSON for tool arguments, especially during
|
|
22
|
+
streaming when args are still being assembled. This helper catches parse
|
|
23
|
+
errors and returns a fallback value.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
part: A tool call part (complete or delta) with args to extract
|
|
27
|
+
default: Value to return on parse failure. If None, returns {"_raw_args": ...}
|
|
28
|
+
with the original unparsed args.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The parsed arguments dict, or a fallback on parse failure.
|
|
32
|
+
"""
|
|
33
|
+
if not isinstance(part, BaseToolCallPart):
|
|
34
|
+
# ToolCallPartDelta doesn't have args_as_dict
|
|
35
|
+
if default is not None:
|
|
36
|
+
return default
|
|
37
|
+
raw = getattr(part, "args", None)
|
|
38
|
+
return {"_raw_args": raw} if raw else {}
|
|
39
|
+
try:
|
|
40
|
+
return part.args_as_dict()
|
|
41
|
+
except ValueError:
|
|
42
|
+
# Model returned malformed JSON for tool args
|
|
43
|
+
if default is not None:
|
|
44
|
+
return default
|
|
45
|
+
# Preserve raw args for debugging/inspection
|
|
46
|
+
return {"_raw_args": part.args} if part.args else {}
|