agentpool 2.1.9__py3-none-any.whl → 2.2.3__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 -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/notifications.py +2 -1
- acp/stdio.py +39 -9
- acp/transports.py +362 -2
- acp/utils.py +15 -2
- agentpool/__init__.py +4 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +203 -88
- agentpool/agents/acp_agent/acp_converters.py +46 -21
- agentpool/agents/acp_agent/client_handler.py +157 -3
- agentpool/agents/acp_agent/session_state.py +4 -1
- agentpool/agents/agent.py +314 -107
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +90 -21
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/base_agent.py +163 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
- agentpool/agents/claude_code_agent/converters.py +71 -3
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +2 -0
- agentpool/agents/events/builtin_handlers.py +2 -1
- agentpool/agents/events/event_emitter.py +29 -2
- agentpool/agents/events/events.py +20 -0
- agentpool/agents/modes.py +54 -0
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/common_types.py +21 -0
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/claude_code_agent.yml +3 -0
- agentpool/delegation/pool.py +37 -29
- agentpool/delegation/team.py +1 -0
- agentpool/delegation/teamrun.py +1 -0
- 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/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +12 -3
- agentpool/mcp_server/manager.py +25 -31
- agentpool/mcp_server/registries/official_registry_client.py +25 -0
- agentpool/mcp_server/tool_bridge.py +78 -66
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/message_history.py +12 -0
- agentpool/messaging/messages.py +52 -9
- agentpool/messaging/processing.py +3 -1
- agentpool/models/acp_agents/base.py +0 -22
- agentpool/models/acp_agents/mcp_capable.py +8 -148
- agentpool/models/acp_agents/non_mcp.py +129 -72
- agentpool/models/agents.py +35 -13
- agentpool/models/claude_code_agents.py +33 -2
- agentpool/models/manifest.py +43 -0
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +9 -1
- agentpool/resource_providers/aggregating.py +52 -3
- agentpool/resource_providers/base.py +57 -1
- agentpool/resource_providers/mcp_provider.py +23 -0
- agentpool/resource_providers/plan_provider.py +130 -41
- agentpool/resource_providers/pool.py +2 -0
- agentpool/resource_providers/static.py +2 -0
- agentpool/sessions/__init__.py +2 -1
- agentpool/sessions/manager.py +31 -2
- agentpool/sessions/models.py +50 -0
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +217 -1
- agentpool/testing.py +537 -19
- 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 +690 -1
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +4 -0
- agentpool_cli/serve_acp.py +41 -20
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_commands/__init__.py +30 -0
- agentpool_commands/agents.py +74 -1
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +51 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/mcp_server.py +131 -1
- agentpool_config/storage.py +46 -1
- agentpool_config/tools.py +7 -1
- agentpool_config/toolsets.py +92 -148
- agentpool_server/acp_server/acp_agent.py +134 -150
- agentpool_server/acp_server/commands/acp_commands.py +216 -51
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
- agentpool_server/acp_server/server.py +23 -79
- agentpool_server/acp_server/session.py +181 -19
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +362 -0
- agentpool_server/opencode_server/__init__.py +27 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +869 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +269 -0
- agentpool_server/opencode_server/models/__init__.py +228 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +60 -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 +647 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +25 -0
- agentpool_server/opencode_server/models/message.py +162 -0
- agentpool_server/opencode_server/models/parts.py +190 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/session.py +99 -0
- agentpool_server/opencode_server/routes/__init__.py +25 -0
- agentpool_server/opencode_server/routes/agent_routes.py +442 -0
- agentpool_server/opencode_server/routes/app_routes.py +139 -0
- agentpool_server/opencode_server/routes/config_routes.py +241 -0
- agentpool_server/opencode_server/routes/file_routes.py +392 -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 +705 -0
- agentpool_server/opencode_server/routes/pty_routes.py +299 -0
- agentpool_server/opencode_server/routes/session_routes.py +1205 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +430 -0
- agentpool_server/opencode_server/state.py +121 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +16 -0
- agentpool_storage/base.py +103 -0
- agentpool_storage/claude_provider.py +907 -0
- agentpool_storage/file_provider.py +129 -0
- agentpool_storage/memory_provider.py +61 -0
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider.py +730 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +6 -0
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +134 -1
- agentpool_storage/sql_provider/utils.py +10 -1
- agentpool_storage/text_log_provider.py +1 -0
- agentpool_toolsets/builtin/__init__.py +0 -8
- agentpool_toolsets/builtin/code.py +95 -56
- agentpool_toolsets/builtin/debug.py +16 -21
- agentpool_toolsets/builtin/execution_environment.py +99 -103
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +86 -4
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +74 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +454 -0
- agentpool_toolsets/mcp_run_toolset.py +84 -6
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/licenses/LICENSE +0 -0
agentpool/mcp_server/client.py
CHANGED
|
@@ -237,19 +237,28 @@ class MCPClient:
|
|
|
237
237
|
)
|
|
238
238
|
|
|
239
239
|
async def list_tools(self) -> list[MCPTool]:
|
|
240
|
-
"""Get available tools directly from the server.
|
|
240
|
+
"""Get available tools directly from the server.
|
|
241
|
+
|
|
242
|
+
Tools are filtered based on the server config's enabled_tools/disabled_tools settings.
|
|
243
|
+
"""
|
|
241
244
|
if not self.connected:
|
|
242
245
|
msg = "Not connected to MCP server"
|
|
243
246
|
raise RuntimeError(msg)
|
|
244
247
|
|
|
245
248
|
try:
|
|
246
249
|
tools = await self._client.list_tools()
|
|
247
|
-
|
|
250
|
+
# Filter tools based on config
|
|
251
|
+
filtered_tools = [t for t in tools if self.config.is_tool_allowed(t.name)]
|
|
252
|
+
logger.debug(
|
|
253
|
+
"Listed tools from MCP server",
|
|
254
|
+
total_tools=len(tools),
|
|
255
|
+
filtered_tools=len(filtered_tools),
|
|
256
|
+
)
|
|
248
257
|
except Exception as e: # noqa: BLE001
|
|
249
258
|
logger.warning("Failed to list tools", error=e)
|
|
250
259
|
return []
|
|
251
260
|
else:
|
|
252
|
-
return
|
|
261
|
+
return filtered_tools
|
|
253
262
|
|
|
254
263
|
async def list_prompts(self) -> list[MCPPrompt]:
|
|
255
264
|
"""Get available prompts from the server."""
|
agentpool/mcp_server/manager.py
CHANGED
|
@@ -64,7 +64,7 @@ class MCPManager:
|
|
|
64
64
|
|
|
65
65
|
async def __aenter__(self) -> Self:
|
|
66
66
|
try:
|
|
67
|
-
if tasks := [self.
|
|
67
|
+
if tasks := [self.setup_server(server) for server in self.servers]:
|
|
68
68
|
await asyncio.gather(*tasks)
|
|
69
69
|
except Exception as e:
|
|
70
70
|
await self.__aexit__(type(e), e, e.__traceback__)
|
|
@@ -123,10 +123,30 @@ class MCPManager:
|
|
|
123
123
|
logger.exception("Sampling failed")
|
|
124
124
|
return f"Sampling failed: {e!s}"
|
|
125
125
|
|
|
126
|
-
async def
|
|
127
|
-
|
|
126
|
+
async def setup_server(
|
|
127
|
+
self, config: MCPServerConfig, *, add_to_config: bool = False
|
|
128
|
+
) -> MCPResourceProvider | None:
|
|
129
|
+
"""Set up a single MCP server resource provider.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
config: MCP server configuration
|
|
133
|
+
add_to_config: If True, also add config to self.servers list and
|
|
134
|
+
raise ValueError if config is disabled
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
The provider if created, None if config is disabled (only when add_to_config=False)
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
ValueError: If add_to_config=True and config is disabled
|
|
141
|
+
"""
|
|
128
142
|
if not config.enabled:
|
|
129
|
-
|
|
143
|
+
if add_to_config:
|
|
144
|
+
msg = f"Server config {config.client_id} is disabled"
|
|
145
|
+
raise ValueError(msg)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
if add_to_config:
|
|
149
|
+
self.add_server_config(config)
|
|
130
150
|
|
|
131
151
|
provider = MCPResourceProvider(
|
|
132
152
|
server=config,
|
|
@@ -138,6 +158,7 @@ class MCPManager:
|
|
|
138
158
|
)
|
|
139
159
|
provider = await self.exit_stack.enter_async_context(provider)
|
|
140
160
|
self.providers.append(provider)
|
|
161
|
+
return provider
|
|
141
162
|
|
|
142
163
|
def get_mcp_providers(self) -> list[MCPResourceProvider]:
|
|
143
164
|
"""Get all MCP resource providers managed by this manager."""
|
|
@@ -147,33 +168,6 @@ class MCPManager:
|
|
|
147
168
|
"""Get the aggregating provider that contains all MCP providers."""
|
|
148
169
|
return self.aggregating_provider
|
|
149
170
|
|
|
150
|
-
async def setup_server_runtime(self, config: MCPServerConfig) -> MCPResourceProvider:
|
|
151
|
-
"""Set up a single MCP server at runtime while manager is running.
|
|
152
|
-
|
|
153
|
-
Returns:
|
|
154
|
-
The newly created and initialized MCPResourceProvider
|
|
155
|
-
"""
|
|
156
|
-
if not config.enabled:
|
|
157
|
-
msg = f"Server config {config.client_id} is disabled"
|
|
158
|
-
raise ValueError(msg)
|
|
159
|
-
|
|
160
|
-
# Add the config first
|
|
161
|
-
self.add_server_config(config)
|
|
162
|
-
provider = MCPResourceProvider(
|
|
163
|
-
server=config,
|
|
164
|
-
name=f"{self.name}_{config.client_id}",
|
|
165
|
-
owner=self.owner,
|
|
166
|
-
source="pool" if self.owner == "pool" else "node",
|
|
167
|
-
sampling_callback=self._sampling_callback,
|
|
168
|
-
accessible_roots=self._accessible_roots,
|
|
169
|
-
)
|
|
170
|
-
provider = await self.exit_stack.enter_async_context(provider)
|
|
171
|
-
self.providers.append(provider)
|
|
172
|
-
# Note: AggregatingResourceProvider automatically sees the new provider
|
|
173
|
-
# since it references self.providers list
|
|
174
|
-
|
|
175
|
-
return provider
|
|
176
|
-
|
|
177
171
|
async def cleanup(self) -> None:
|
|
178
172
|
"""Clean up all MCP connections and providers."""
|
|
179
173
|
try:
|
|
@@ -98,6 +98,19 @@ class RegistryRemote(Schema):
|
|
|
98
98
|
"""Request headers."""
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
class RegistryIcon(Schema):
|
|
102
|
+
"""Icon configuration for a server."""
|
|
103
|
+
|
|
104
|
+
src: str
|
|
105
|
+
"""Icon source URL."""
|
|
106
|
+
|
|
107
|
+
theme: str | None = None
|
|
108
|
+
"""Theme variant (light, dark)."""
|
|
109
|
+
|
|
110
|
+
mime_type: str | None = Field(None, alias="mimeType")
|
|
111
|
+
"""MIME type of the icon (e.g., image/png)."""
|
|
112
|
+
|
|
113
|
+
|
|
101
114
|
class RegistryServer(Schema):
|
|
102
115
|
"""MCP server entry from the registry."""
|
|
103
116
|
|
|
@@ -122,6 +135,18 @@ class RegistryServer(Schema):
|
|
|
122
135
|
schema_: str | None = Field(None, alias="$schema")
|
|
123
136
|
"""JSON schema URL."""
|
|
124
137
|
|
|
138
|
+
title: str | None = None
|
|
139
|
+
"""Human-readable display title."""
|
|
140
|
+
|
|
141
|
+
website_url: str | None = Field(None, alias="websiteUrl")
|
|
142
|
+
"""Website URL for documentation."""
|
|
143
|
+
|
|
144
|
+
icons: list[RegistryIcon] = Field(default_factory=list)
|
|
145
|
+
"""Server icons for different themes."""
|
|
146
|
+
|
|
147
|
+
meta: dict[str, Any] = Field(default_factory=dict, alias="_meta")
|
|
148
|
+
"""Internal metadata (can appear at server level too)."""
|
|
149
|
+
|
|
125
150
|
def get_preferred_transport(self) -> TransportType:
|
|
126
151
|
"""Select optimal transport method based on availability and performance."""
|
|
127
152
|
# Prefer local packages for better performance/security
|
|
@@ -21,12 +21,11 @@ from uuid import uuid4
|
|
|
21
21
|
import anyio
|
|
22
22
|
from fastmcp import FastMCP
|
|
23
23
|
from fastmcp.tools import Tool as FastMCPTool
|
|
24
|
-
from llmling_models.models.helpers import infer_model
|
|
25
24
|
from pydantic import BaseModel, HttpUrl
|
|
26
25
|
|
|
27
|
-
from agentpool.agents import Agent
|
|
28
|
-
from agentpool.agents.acp_agent.acp_agent import ACPAgent
|
|
26
|
+
from agentpool.agents import Agent
|
|
29
27
|
from agentpool.log import get_logger
|
|
28
|
+
from agentpool.resource_providers import ResourceChangeEvent
|
|
30
29
|
from agentpool.utils.signatures import filter_schema_params, get_params_matching_predicate
|
|
31
30
|
|
|
32
31
|
|
|
@@ -44,6 +43,8 @@ if TYPE_CHECKING:
|
|
|
44
43
|
from agentpool.agents.base_agent import BaseAgent
|
|
45
44
|
from agentpool.tools.base import Tool
|
|
46
45
|
|
|
46
|
+
_ = ResourceChangeEvent # Used at runtime in method signature
|
|
47
|
+
|
|
47
48
|
|
|
48
49
|
logger = get_logger(__name__)
|
|
49
50
|
|
|
@@ -130,11 +131,11 @@ def _create_stub_run_context(ctx: AgentContext[Any]) -> RunContext[Any]:
|
|
|
130
131
|
match ctx.agent:
|
|
131
132
|
case Agent():
|
|
132
133
|
model = ctx.agent._model or TestModel()
|
|
133
|
-
case ACPAgent() | ClaudeCodeAgent():
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
# case ACPAgent() | ClaudeCodeAgent():
|
|
135
|
+
# try:
|
|
136
|
+
# model = infer_model(ctx.agent.model_name or "test")
|
|
137
|
+
# except Exception:
|
|
138
|
+
# model = TestModel()
|
|
138
139
|
case _:
|
|
139
140
|
model = TestModel()
|
|
140
141
|
# Create a minimal usage object
|
|
@@ -244,6 +245,9 @@ class ToolManagerBridge:
|
|
|
244
245
|
config: BridgeConfig = field(default_factory=BridgeConfig)
|
|
245
246
|
"""Bridge configuration."""
|
|
246
247
|
|
|
248
|
+
current_deps: Any = field(default=None, init=False, repr=False)
|
|
249
|
+
"""Current dependencies for tool invocations (set by run_stream)."""
|
|
250
|
+
|
|
247
251
|
_mcp: FastMCP | None = field(default=None, init=False, repr=False)
|
|
248
252
|
"""FastMCP server instance."""
|
|
249
253
|
|
|
@@ -269,10 +273,14 @@ class ToolManagerBridge:
|
|
|
269
273
|
"""Start the HTTP MCP server in the background."""
|
|
270
274
|
self._mcp = FastMCP(name=self.config.server_name)
|
|
271
275
|
await self._register_tools()
|
|
276
|
+
self._subscribe_to_tool_changes()
|
|
272
277
|
await self._start_server()
|
|
273
278
|
|
|
274
279
|
async def stop(self) -> None:
|
|
275
280
|
"""Stop the HTTP MCP server."""
|
|
281
|
+
# Unsubscribe from tool changes
|
|
282
|
+
self._unsubscribe_from_tool_changes()
|
|
283
|
+
|
|
276
284
|
if self._server:
|
|
277
285
|
self._server.should_exit = True
|
|
278
286
|
if self._server_task:
|
|
@@ -288,6 +296,60 @@ class ToolManagerBridge:
|
|
|
288
296
|
self._actual_port = None
|
|
289
297
|
logger.info("ToolManagerBridge stopped")
|
|
290
298
|
|
|
299
|
+
def _subscribe_to_tool_changes(self) -> None:
|
|
300
|
+
"""Subscribe to tool changes from all providers via signals."""
|
|
301
|
+
for provider in self.node.tools.providers:
|
|
302
|
+
provider.tools_changed.connect(self._on_tools_changed)
|
|
303
|
+
|
|
304
|
+
def _unsubscribe_from_tool_changes(self) -> None:
|
|
305
|
+
"""Disconnect from tool change signals on all providers."""
|
|
306
|
+
for provider in self.node.tools.providers:
|
|
307
|
+
provider.tools_changed.disconnect(self._on_tools_changed)
|
|
308
|
+
|
|
309
|
+
async def _on_tools_changed(self, event: ResourceChangeEvent) -> None:
|
|
310
|
+
"""Handle tool changes from a provider."""
|
|
311
|
+
logger.info(
|
|
312
|
+
"Tools changed in provider, refreshing MCP tools",
|
|
313
|
+
provider=event.provider_name,
|
|
314
|
+
provider_kind=event.provider_kind,
|
|
315
|
+
)
|
|
316
|
+
if self._mcp:
|
|
317
|
+
await self._refresh_tools()
|
|
318
|
+
|
|
319
|
+
async def _refresh_tools(self) -> None:
|
|
320
|
+
"""Refresh tools registered with the MCP server.
|
|
321
|
+
|
|
322
|
+
Uses FastMCP's add_tool/remove_tool API which automatically sends
|
|
323
|
+
ToolListChanged notifications when called within a request context.
|
|
324
|
+
|
|
325
|
+
Note: FastMCP only sends notifications when inside a request context
|
|
326
|
+
(ContextVar-based). Outside of requests, tools are updated but clients
|
|
327
|
+
won't receive a push notification - they'll see changes on next list_tools.
|
|
328
|
+
|
|
329
|
+
Future improvement: Access StreamableHTTPSessionManager._server_instances
|
|
330
|
+
to broadcast ToolListChanged to all connected sessions regardless of context.
|
|
331
|
+
"""
|
|
332
|
+
if not self._mcp:
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
# Get current and new tool sets
|
|
336
|
+
current_names = set(self._mcp._tool_manager._tools.keys())
|
|
337
|
+
new_tools = await self.node.tools.get_tools(state="enabled")
|
|
338
|
+
new_names = {t.name for t in new_tools}
|
|
339
|
+
|
|
340
|
+
# Remove tools that are no longer present
|
|
341
|
+
for name in current_names - new_names:
|
|
342
|
+
with suppress(Exception):
|
|
343
|
+
self._mcp.remove_tool(name)
|
|
344
|
+
|
|
345
|
+
# Add/update tools
|
|
346
|
+
for tool in new_tools:
|
|
347
|
+
if tool.name in current_names:
|
|
348
|
+
# Remove and re-add to update
|
|
349
|
+
with suppress(Exception):
|
|
350
|
+
self._mcp.remove_tool(tool.name)
|
|
351
|
+
self._register_single_tool(tool)
|
|
352
|
+
|
|
291
353
|
@property
|
|
292
354
|
def port(self) -> int:
|
|
293
355
|
"""Get the actual port the server is running on."""
|
|
@@ -410,7 +472,9 @@ class ToolManagerBridge:
|
|
|
410
472
|
# Create the ASGI app
|
|
411
473
|
app = self._mcp.http_app(transport=self.config.transport)
|
|
412
474
|
# Configure uvicorn
|
|
413
|
-
cfg = uvicorn.Config(
|
|
475
|
+
cfg = uvicorn.Config(
|
|
476
|
+
app=app, host=self.config.host, port=port, log_level="warning", ws="websockets-sansio"
|
|
477
|
+
)
|
|
414
478
|
self._server = uvicorn.Server(cfg)
|
|
415
479
|
# Start server in background task
|
|
416
480
|
name = f"mcp-bridge-{self.config.server_name}"
|
|
@@ -453,9 +517,13 @@ class _BridgeTool(FastMCPTool):
|
|
|
453
517
|
|
|
454
518
|
# Try to get Claude's original tool_call_id from request metadata
|
|
455
519
|
tool_call_id = _extract_tool_call_id(mcp_context)
|
|
520
|
+
|
|
521
|
+
# Get deps from bridge (set by run_stream on the agent)
|
|
522
|
+
current_deps = self._bridge.current_deps
|
|
523
|
+
|
|
456
524
|
# Create context with tool-specific metadata from node's context.
|
|
457
525
|
ctx = replace(
|
|
458
|
-
self._bridge.node.get_context(),
|
|
526
|
+
self._bridge.node.get_context(data=current_deps),
|
|
459
527
|
tool_name=self._tool.name,
|
|
460
528
|
tool_call_id=tool_call_id,
|
|
461
529
|
tool_input=arguments,
|
|
@@ -490,59 +558,3 @@ async def create_tool_bridge(
|
|
|
490
558
|
bridge = ToolManagerBridge(node=node, config=config)
|
|
491
559
|
async with bridge:
|
|
492
560
|
yield bridge
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
class ToolBridgeRegistry:
|
|
496
|
-
"""Registry for managing multiple tool bridges.
|
|
497
|
-
|
|
498
|
-
Useful when multiple ACP agents need access to different toolsets.
|
|
499
|
-
"""
|
|
500
|
-
|
|
501
|
-
def __init__(self) -> None:
|
|
502
|
-
self._bridges: dict[str, ToolManagerBridge] = {}
|
|
503
|
-
self._port_counter = 18000 # Start port range for auto-allocation
|
|
504
|
-
|
|
505
|
-
async def create_bridge(self, name: str, node: BaseAgent[Any, Any]) -> ToolManagerBridge:
|
|
506
|
-
"""Create and register a new bridge.
|
|
507
|
-
|
|
508
|
-
Args:
|
|
509
|
-
name: Unique name for this bridge
|
|
510
|
-
node: The node whose tools to expose
|
|
511
|
-
|
|
512
|
-
Returns:
|
|
513
|
-
Started ToolManagerBridge
|
|
514
|
-
"""
|
|
515
|
-
if name in self._bridges:
|
|
516
|
-
msg = f"Bridge {name!r} already exists"
|
|
517
|
-
raise ValueError(msg)
|
|
518
|
-
|
|
519
|
-
config = BridgeConfig(port=self._port_counter, server_name=f"agentpool-{name}")
|
|
520
|
-
self._port_counter += 1
|
|
521
|
-
|
|
522
|
-
bridge = ToolManagerBridge(node=node, config=config)
|
|
523
|
-
await bridge.start()
|
|
524
|
-
self._bridges[name] = bridge
|
|
525
|
-
return bridge
|
|
526
|
-
|
|
527
|
-
async def get_bridge(self, name: str) -> ToolManagerBridge:
|
|
528
|
-
"""Get a bridge by name."""
|
|
529
|
-
if name not in self._bridges:
|
|
530
|
-
msg = f"Bridge {name!r} not found"
|
|
531
|
-
raise KeyError(msg)
|
|
532
|
-
return self._bridges[name]
|
|
533
|
-
|
|
534
|
-
async def remove_bridge(self, name: str) -> None:
|
|
535
|
-
"""Stop and remove a bridge."""
|
|
536
|
-
if name in self._bridges:
|
|
537
|
-
await self._bridges[name].stop()
|
|
538
|
-
del self._bridges[name]
|
|
539
|
-
|
|
540
|
-
async def close_all(self) -> None:
|
|
541
|
-
"""Stop all bridges."""
|
|
542
|
-
for bridge in list(self._bridges.values()):
|
|
543
|
-
await bridge.stop()
|
|
544
|
-
self._bridges.clear()
|
|
545
|
-
|
|
546
|
-
def get_all_mcp_configs(self) -> list[HttpMcpServer | SseMcpServer]:
|
|
547
|
-
"""Get MCP server configs for all active bridges."""
|
|
548
|
-
return [bridge.get_mcp_server_config() for bridge in self._bridges.values()]
|
agentpool/messaging/__init__.py
CHANGED
|
@@ -7,7 +7,6 @@ from agentpool.messaging.messagenode import MessageNode
|
|
|
7
7
|
from agentpool.messaging.message_history import MessageHistory
|
|
8
8
|
from agentpool.messaging.compaction import (
|
|
9
9
|
CompactionPipeline,
|
|
10
|
-
CompactionPipelineConfig,
|
|
11
10
|
CompactionStep,
|
|
12
11
|
FilterBinaryContent,
|
|
13
12
|
FilterEmptyMessages,
|
|
@@ -32,7 +31,6 @@ __all__ = [
|
|
|
32
31
|
"ChatMessage",
|
|
33
32
|
"ChatMessageList",
|
|
34
33
|
"CompactionPipeline",
|
|
35
|
-
"CompactionPipelineConfig",
|
|
36
34
|
"CompactionStep",
|
|
37
35
|
"EventManager",
|
|
38
36
|
"FilterBinaryContent",
|
|
@@ -22,7 +22,7 @@ Example:
|
|
|
22
22
|
compacted = await pipeline.apply(messages)
|
|
23
23
|
|
|
24
24
|
# Or via config (for YAML)
|
|
25
|
-
config =
|
|
25
|
+
config = CompactionConfig(steps=[
|
|
26
26
|
FilterThinkingConfig(),
|
|
27
27
|
TruncateToolOutputsConfig(max_length=1000),
|
|
28
28
|
KeepLastMessagesConfig(count=10),
|
|
@@ -50,9 +50,8 @@ from __future__ import annotations
|
|
|
50
50
|
from abc import ABC, abstractmethod
|
|
51
51
|
from collections.abc import Sequence
|
|
52
52
|
from dataclasses import dataclass, field, replace
|
|
53
|
-
from typing import TYPE_CHECKING,
|
|
53
|
+
from typing import TYPE_CHECKING, Any, Self, cast
|
|
54
54
|
|
|
55
|
-
from pydantic import BaseModel, Field
|
|
56
55
|
from pydantic_ai import (
|
|
57
56
|
Agent,
|
|
58
57
|
BinaryContent,
|
|
@@ -73,6 +72,8 @@ if TYPE_CHECKING:
|
|
|
73
72
|
from pydantic_ai import ModelRequestPart, ModelResponsePart
|
|
74
73
|
from tokonomics.model_names import ModelId
|
|
75
74
|
|
|
75
|
+
from agentpool.messaging.message_history import MessageHistory
|
|
76
|
+
|
|
76
77
|
# Type aliases
|
|
77
78
|
ModelMessage = ModelRequest | ModelResponse
|
|
78
79
|
MessageSequence = Sequence[ModelMessage]
|
|
@@ -629,200 +630,6 @@ class WhenMessageCountExceeds(CompactionStep):
|
|
|
629
630
|
return list(messages)
|
|
630
631
|
|
|
631
632
|
|
|
632
|
-
# =============================================================================
|
|
633
|
-
# Configuration Models - For YAML/JSON configuration
|
|
634
|
-
# =============================================================================
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
class FilterThinkingConfig(BaseModel):
|
|
638
|
-
"""Configuration for FilterThinking step."""
|
|
639
|
-
|
|
640
|
-
type: Literal["filter_thinking"] = "filter_thinking"
|
|
641
|
-
|
|
642
|
-
def build(self) -> FilterThinking:
|
|
643
|
-
return FilterThinking()
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
class FilterRetryPromptsConfig(BaseModel):
|
|
647
|
-
"""Configuration for FilterRetryPrompts step."""
|
|
648
|
-
|
|
649
|
-
type: Literal["filter_retry_prompts"] = "filter_retry_prompts"
|
|
650
|
-
|
|
651
|
-
def build(self) -> FilterRetryPrompts:
|
|
652
|
-
return FilterRetryPrompts()
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
class FilterBinaryContentConfig(BaseModel):
|
|
656
|
-
"""Configuration for FilterBinaryContent step."""
|
|
657
|
-
|
|
658
|
-
type: Literal["filter_binary"] = "filter_binary"
|
|
659
|
-
keep_references: bool = False
|
|
660
|
-
|
|
661
|
-
def build(self) -> FilterBinaryContent:
|
|
662
|
-
return FilterBinaryContent(keep_references=self.keep_references)
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
class FilterToolCallsConfig(BaseModel):
|
|
666
|
-
"""Configuration for FilterToolCalls step."""
|
|
667
|
-
|
|
668
|
-
type: Literal["filter_tools"] = "filter_tools"
|
|
669
|
-
exclude_tools: list[str] = Field(default_factory=list)
|
|
670
|
-
include_only: list[str] | None = None
|
|
671
|
-
|
|
672
|
-
def build(self) -> FilterToolCalls:
|
|
673
|
-
return FilterToolCalls(exclude_tools=self.exclude_tools, include_only=self.include_only)
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
class FilterEmptyMessagesConfig(BaseModel):
|
|
677
|
-
"""Configuration for FilterEmptyMessages step."""
|
|
678
|
-
|
|
679
|
-
type: Literal["filter_empty"] = "filter_empty"
|
|
680
|
-
|
|
681
|
-
def build(self) -> FilterEmptyMessages:
|
|
682
|
-
return FilterEmptyMessages()
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
class TruncateToolOutputsConfig(BaseModel):
|
|
686
|
-
"""Configuration for TruncateToolOutputs step."""
|
|
687
|
-
|
|
688
|
-
type: Literal["truncate_tool_outputs"] = "truncate_tool_outputs"
|
|
689
|
-
max_length: int = 2000
|
|
690
|
-
suffix: str = "\n... [truncated]"
|
|
691
|
-
|
|
692
|
-
def build(self) -> TruncateToolOutputs:
|
|
693
|
-
return TruncateToolOutputs(max_length=self.max_length, suffix=self.suffix)
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
class TruncateTextPartsConfig(BaseModel):
|
|
697
|
-
"""Configuration for TruncateTextParts step."""
|
|
698
|
-
|
|
699
|
-
type: Literal["truncate_text"] = "truncate_text"
|
|
700
|
-
max_length: int = 5000
|
|
701
|
-
suffix: str = "\n... [truncated]"
|
|
702
|
-
|
|
703
|
-
def build(self) -> TruncateTextParts:
|
|
704
|
-
return TruncateTextParts(max_length=self.max_length, suffix=self.suffix)
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
class KeepLastMessagesConfig(BaseModel):
|
|
708
|
-
"""Configuration for KeepLastMessages step."""
|
|
709
|
-
|
|
710
|
-
type: Literal["keep_last"] = "keep_last"
|
|
711
|
-
count: int = 10
|
|
712
|
-
count_pairs: bool = True
|
|
713
|
-
|
|
714
|
-
def build(self) -> KeepLastMessages:
|
|
715
|
-
return KeepLastMessages(count=self.count, count_pairs=self.count_pairs)
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
class KeepFirstMessagesConfig(BaseModel):
|
|
719
|
-
"""Configuration for KeepFirstMessages step."""
|
|
720
|
-
|
|
721
|
-
type: Literal["keep_first"] = "keep_first"
|
|
722
|
-
count: int = 2
|
|
723
|
-
|
|
724
|
-
def build(self) -> KeepFirstMessages:
|
|
725
|
-
return KeepFirstMessages(count=self.count)
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
class KeepFirstAndLastConfig(BaseModel):
|
|
729
|
-
"""Configuration for KeepFirstAndLast step."""
|
|
730
|
-
|
|
731
|
-
type: Literal["keep_first_last"] = "keep_first_last"
|
|
732
|
-
first_count: int = 2
|
|
733
|
-
last_count: int = 5
|
|
734
|
-
|
|
735
|
-
def build(self) -> KeepFirstAndLast:
|
|
736
|
-
return KeepFirstAndLast(first_count=self.first_count, last_count=self.last_count)
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
class TokenBudgetConfig(BaseModel):
|
|
740
|
-
"""Configuration for TokenBudget step."""
|
|
741
|
-
|
|
742
|
-
type: Literal["token_budget"] = "token_budget"
|
|
743
|
-
max_tokens: int = 4000
|
|
744
|
-
model: str = "gpt-4o"
|
|
745
|
-
|
|
746
|
-
def build(self) -> TokenBudget:
|
|
747
|
-
return TokenBudget(max_tokens=self.max_tokens, model=self.model)
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
class SummarizeConfig(BaseModel):
|
|
751
|
-
"""Configuration for Summarize step."""
|
|
752
|
-
|
|
753
|
-
type: Literal["summarize"] = "summarize"
|
|
754
|
-
model: str = "openai:gpt-4o-mini"
|
|
755
|
-
threshold: int = 15
|
|
756
|
-
keep_recent: int = 5
|
|
757
|
-
summary_prompt: str | None = None
|
|
758
|
-
|
|
759
|
-
def build(self) -> Summarize:
|
|
760
|
-
kwargs: dict[str, Any] = {
|
|
761
|
-
"model": self.model,
|
|
762
|
-
"threshold": self.threshold,
|
|
763
|
-
"keep_recent": self.keep_recent,
|
|
764
|
-
}
|
|
765
|
-
if self.summary_prompt:
|
|
766
|
-
kwargs["summary_prompt"] = self.summary_prompt
|
|
767
|
-
return Summarize(**kwargs)
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
class WhenMessageCountExceedsConfig(BaseModel):
|
|
771
|
-
"""Configuration for WhenMessageCountExceeds wrapper."""
|
|
772
|
-
|
|
773
|
-
type: Literal["when_count_exceeds"] = "when_count_exceeds"
|
|
774
|
-
threshold: int = 20
|
|
775
|
-
step: "CompactionStepConfig"
|
|
776
|
-
|
|
777
|
-
def build(self) -> WhenMessageCountExceeds:
|
|
778
|
-
return WhenMessageCountExceeds(step=self.step.build(), threshold=self.threshold)
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
# Union of all config types with discriminator
|
|
782
|
-
CompactionStepConfig = Annotated[
|
|
783
|
-
FilterThinkingConfig
|
|
784
|
-
| FilterRetryPromptsConfig
|
|
785
|
-
| FilterBinaryContentConfig
|
|
786
|
-
| FilterToolCallsConfig
|
|
787
|
-
| FilterEmptyMessagesConfig
|
|
788
|
-
| TruncateToolOutputsConfig
|
|
789
|
-
| TruncateTextPartsConfig
|
|
790
|
-
| KeepLastMessagesConfig
|
|
791
|
-
| KeepFirstMessagesConfig
|
|
792
|
-
| KeepFirstAndLastConfig
|
|
793
|
-
| TokenBudgetConfig
|
|
794
|
-
| SummarizeConfig
|
|
795
|
-
| WhenMessageCountExceedsConfig,
|
|
796
|
-
Field(discriminator="type"),
|
|
797
|
-
]
|
|
798
|
-
|
|
799
|
-
# Update forward reference
|
|
800
|
-
WhenMessageCountExceedsConfig.model_rebuild()
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
class CompactionPipelineConfig(BaseModel):
|
|
804
|
-
"""Configuration for a complete compaction pipeline.
|
|
805
|
-
|
|
806
|
-
Example YAML:
|
|
807
|
-
```yaml
|
|
808
|
-
compaction:
|
|
809
|
-
steps:
|
|
810
|
-
- type: filter_thinking
|
|
811
|
-
- type: truncate_tool_outputs
|
|
812
|
-
max_length: 1000
|
|
813
|
-
- type: keep_last
|
|
814
|
-
count: 10
|
|
815
|
-
```
|
|
816
|
-
"""
|
|
817
|
-
|
|
818
|
-
steps: list[CompactionStepConfig] = Field(default_factory=list)
|
|
819
|
-
"""Ordered list of compaction steps to apply."""
|
|
820
|
-
|
|
821
|
-
def build(self) -> CompactionPipeline:
|
|
822
|
-
"""Build a CompactionPipeline from this configuration."""
|
|
823
|
-
return CompactionPipeline(steps=[step.build() for step in self.steps])
|
|
824
|
-
|
|
825
|
-
|
|
826
633
|
# =============================================================================
|
|
827
634
|
# Preset Pipelines - Common configurations
|
|
828
635
|
# =============================================================================
|
|
@@ -877,6 +684,74 @@ def summarizing_context(model: ModelId | str = "openai:gpt-4o-mini") -> Compacti
|
|
|
877
684
|
# =============================================================================
|
|
878
685
|
|
|
879
686
|
|
|
687
|
+
async def compact_conversation(
|
|
688
|
+
pipeline: CompactionPipeline,
|
|
689
|
+
conversation: MessageHistory,
|
|
690
|
+
) -> tuple[int, int]:
|
|
691
|
+
"""Apply a compaction pipeline to a conversation's message history.
|
|
692
|
+
|
|
693
|
+
Extracts model messages from ChatMessages, applies the pipeline,
|
|
694
|
+
and rebuilds the conversation history with compacted messages.
|
|
695
|
+
|
|
696
|
+
Args:
|
|
697
|
+
pipeline: The compaction pipeline to apply
|
|
698
|
+
conversation: The MessageHistory to compact
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
Tuple of (original_count, compacted_count) of model messages
|
|
702
|
+
"""
|
|
703
|
+
from agentpool.messaging.messages import ChatMessage
|
|
704
|
+
|
|
705
|
+
chat_messages = conversation.get_history()
|
|
706
|
+
if not chat_messages:
|
|
707
|
+
return 0, 0
|
|
708
|
+
|
|
709
|
+
# Extract ModelRequest/ModelResponse from ChatMessage.messages
|
|
710
|
+
model_messages: list[ModelMessage] = []
|
|
711
|
+
for chat_msg in chat_messages:
|
|
712
|
+
if chat_msg.messages:
|
|
713
|
+
model_messages.extend(chat_msg.messages)
|
|
714
|
+
|
|
715
|
+
if not model_messages:
|
|
716
|
+
return 0, 0
|
|
717
|
+
|
|
718
|
+
original_count = len(model_messages)
|
|
719
|
+
|
|
720
|
+
# Apply the compaction pipeline
|
|
721
|
+
compacted = await pipeline.apply(model_messages)
|
|
722
|
+
|
|
723
|
+
# Rebuild ChatMessages from compacted model messages
|
|
724
|
+
new_chat_messages: list[ChatMessage[Any]] = []
|
|
725
|
+
current_msgs: list[ModelMessage] = []
|
|
726
|
+
|
|
727
|
+
for msg in compacted:
|
|
728
|
+
current_msgs.append(msg)
|
|
729
|
+
if isinstance(msg, ModelResponse):
|
|
730
|
+
new_chat_messages.append(
|
|
731
|
+
ChatMessage(
|
|
732
|
+
content="[compacted]",
|
|
733
|
+
role="assistant",
|
|
734
|
+
messages=list(current_msgs),
|
|
735
|
+
)
|
|
736
|
+
)
|
|
737
|
+
current_msgs = []
|
|
738
|
+
|
|
739
|
+
# Handle any remaining messages (incomplete pair)
|
|
740
|
+
if current_msgs:
|
|
741
|
+
new_chat_messages.append(
|
|
742
|
+
ChatMessage(
|
|
743
|
+
content="[compacted]",
|
|
744
|
+
role="user",
|
|
745
|
+
messages=list(current_msgs),
|
|
746
|
+
)
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
# Update the conversation history
|
|
750
|
+
conversation.set_history(new_chat_messages)
|
|
751
|
+
|
|
752
|
+
return original_count, len(compacted)
|
|
753
|
+
|
|
754
|
+
|
|
880
755
|
def _extract_text_content(msg: ModelMessage) -> str:
|
|
881
756
|
"""Extract text content from a message for token counting."""
|
|
882
757
|
parts_text: list[str] = []
|