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.
Files changed (174) hide show
  1. acp/__init__.py +13 -0
  2. acp/bridge/README.md +15 -2
  3. acp/bridge/__init__.py +3 -2
  4. acp/bridge/__main__.py +60 -19
  5. acp/bridge/ws_server.py +173 -0
  6. acp/bridge/ws_server_cli.py +89 -0
  7. acp/notifications.py +2 -1
  8. acp/stdio.py +39 -9
  9. acp/transports.py +362 -2
  10. acp/utils.py +15 -2
  11. agentpool/__init__.py +4 -1
  12. agentpool/agents/__init__.py +2 -0
  13. agentpool/agents/acp_agent/acp_agent.py +203 -88
  14. agentpool/agents/acp_agent/acp_converters.py +46 -21
  15. agentpool/agents/acp_agent/client_handler.py +157 -3
  16. agentpool/agents/acp_agent/session_state.py +4 -1
  17. agentpool/agents/agent.py +314 -107
  18. agentpool/agents/agui_agent/__init__.py +0 -2
  19. agentpool/agents/agui_agent/agui_agent.py +90 -21
  20. agentpool/agents/agui_agent/agui_converters.py +0 -131
  21. agentpool/agents/base_agent.py +163 -1
  22. agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
  23. agentpool/agents/claude_code_agent/converters.py +71 -3
  24. agentpool/agents/claude_code_agent/history.py +474 -0
  25. agentpool/agents/context.py +40 -0
  26. agentpool/agents/events/__init__.py +2 -0
  27. agentpool/agents/events/builtin_handlers.py +2 -1
  28. agentpool/agents/events/event_emitter.py +29 -2
  29. agentpool/agents/events/events.py +20 -0
  30. agentpool/agents/modes.py +54 -0
  31. agentpool/agents/tool_call_accumulator.py +213 -0
  32. agentpool/common_types.py +21 -0
  33. agentpool/config_resources/__init__.py +38 -1
  34. agentpool/config_resources/claude_code_agent.yml +3 -0
  35. agentpool/delegation/pool.py +37 -29
  36. agentpool/delegation/team.py +1 -0
  37. agentpool/delegation/teamrun.py +1 -0
  38. agentpool/diagnostics/__init__.py +53 -0
  39. agentpool/diagnostics/lsp_manager.py +1593 -0
  40. agentpool/diagnostics/lsp_proxy.py +41 -0
  41. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  42. agentpool/diagnostics/models.py +398 -0
  43. agentpool/mcp_server/__init__.py +0 -2
  44. agentpool/mcp_server/client.py +12 -3
  45. agentpool/mcp_server/manager.py +25 -31
  46. agentpool/mcp_server/registries/official_registry_client.py +25 -0
  47. agentpool/mcp_server/tool_bridge.py +78 -66
  48. agentpool/messaging/__init__.py +0 -2
  49. agentpool/messaging/compaction.py +72 -197
  50. agentpool/messaging/message_history.py +12 -0
  51. agentpool/messaging/messages.py +52 -9
  52. agentpool/messaging/processing.py +3 -1
  53. agentpool/models/acp_agents/base.py +0 -22
  54. agentpool/models/acp_agents/mcp_capable.py +8 -148
  55. agentpool/models/acp_agents/non_mcp.py +129 -72
  56. agentpool/models/agents.py +35 -13
  57. agentpool/models/claude_code_agents.py +33 -2
  58. agentpool/models/manifest.py +43 -0
  59. agentpool/repomap.py +1 -1
  60. agentpool/resource_providers/__init__.py +9 -1
  61. agentpool/resource_providers/aggregating.py +52 -3
  62. agentpool/resource_providers/base.py +57 -1
  63. agentpool/resource_providers/mcp_provider.py +23 -0
  64. agentpool/resource_providers/plan_provider.py +130 -41
  65. agentpool/resource_providers/pool.py +2 -0
  66. agentpool/resource_providers/static.py +2 -0
  67. agentpool/sessions/__init__.py +2 -1
  68. agentpool/sessions/manager.py +31 -2
  69. agentpool/sessions/models.py +50 -0
  70. agentpool/skills/registry.py +13 -8
  71. agentpool/storage/manager.py +217 -1
  72. agentpool/testing.py +537 -19
  73. agentpool/utils/file_watcher.py +269 -0
  74. agentpool/utils/identifiers.py +121 -0
  75. agentpool/utils/pydantic_ai_helpers.py +46 -0
  76. agentpool/utils/streams.py +690 -1
  77. agentpool/utils/subprocess_utils.py +155 -0
  78. agentpool/utils/token_breakdown.py +461 -0
  79. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
  80. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
  81. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
  82. agentpool_cli/__main__.py +4 -0
  83. agentpool_cli/serve_acp.py +41 -20
  84. agentpool_cli/serve_agui.py +87 -0
  85. agentpool_cli/serve_opencode.py +119 -0
  86. agentpool_commands/__init__.py +30 -0
  87. agentpool_commands/agents.py +74 -1
  88. agentpool_commands/history.py +62 -0
  89. agentpool_commands/mcp.py +176 -0
  90. agentpool_commands/models.py +56 -3
  91. agentpool_commands/tools.py +57 -0
  92. agentpool_commands/utils.py +51 -0
  93. agentpool_config/builtin_tools.py +77 -22
  94. agentpool_config/commands.py +24 -1
  95. agentpool_config/compaction.py +258 -0
  96. agentpool_config/mcp_server.py +131 -1
  97. agentpool_config/storage.py +46 -1
  98. agentpool_config/tools.py +7 -1
  99. agentpool_config/toolsets.py +92 -148
  100. agentpool_server/acp_server/acp_agent.py +134 -150
  101. agentpool_server/acp_server/commands/acp_commands.py +216 -51
  102. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
  103. agentpool_server/acp_server/server.py +23 -79
  104. agentpool_server/acp_server/session.py +181 -19
  105. agentpool_server/opencode_server/.rules +95 -0
  106. agentpool_server/opencode_server/ENDPOINTS.md +362 -0
  107. agentpool_server/opencode_server/__init__.py +27 -0
  108. agentpool_server/opencode_server/command_validation.py +172 -0
  109. agentpool_server/opencode_server/converters.py +869 -0
  110. agentpool_server/opencode_server/dependencies.py +24 -0
  111. agentpool_server/opencode_server/input_provider.py +269 -0
  112. agentpool_server/opencode_server/models/__init__.py +228 -0
  113. agentpool_server/opencode_server/models/agent.py +53 -0
  114. agentpool_server/opencode_server/models/app.py +60 -0
  115. agentpool_server/opencode_server/models/base.py +26 -0
  116. agentpool_server/opencode_server/models/common.py +23 -0
  117. agentpool_server/opencode_server/models/config.py +37 -0
  118. agentpool_server/opencode_server/models/events.py +647 -0
  119. agentpool_server/opencode_server/models/file.py +88 -0
  120. agentpool_server/opencode_server/models/mcp.py +25 -0
  121. agentpool_server/opencode_server/models/message.py +162 -0
  122. agentpool_server/opencode_server/models/parts.py +190 -0
  123. agentpool_server/opencode_server/models/provider.py +81 -0
  124. agentpool_server/opencode_server/models/pty.py +43 -0
  125. agentpool_server/opencode_server/models/session.py +99 -0
  126. agentpool_server/opencode_server/routes/__init__.py +25 -0
  127. agentpool_server/opencode_server/routes/agent_routes.py +442 -0
  128. agentpool_server/opencode_server/routes/app_routes.py +139 -0
  129. agentpool_server/opencode_server/routes/config_routes.py +241 -0
  130. agentpool_server/opencode_server/routes/file_routes.py +392 -0
  131. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  132. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  133. agentpool_server/opencode_server/routes/message_routes.py +705 -0
  134. agentpool_server/opencode_server/routes/pty_routes.py +299 -0
  135. agentpool_server/opencode_server/routes/session_routes.py +1205 -0
  136. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  137. agentpool_server/opencode_server/server.py +430 -0
  138. agentpool_server/opencode_server/state.py +121 -0
  139. agentpool_server/opencode_server/time_utils.py +8 -0
  140. agentpool_storage/__init__.py +16 -0
  141. agentpool_storage/base.py +103 -0
  142. agentpool_storage/claude_provider.py +907 -0
  143. agentpool_storage/file_provider.py +129 -0
  144. agentpool_storage/memory_provider.py +61 -0
  145. agentpool_storage/models.py +3 -0
  146. agentpool_storage/opencode_provider.py +730 -0
  147. agentpool_storage/project_store.py +325 -0
  148. agentpool_storage/session_store.py +6 -0
  149. agentpool_storage/sql_provider/__init__.py +4 -2
  150. agentpool_storage/sql_provider/models.py +48 -0
  151. agentpool_storage/sql_provider/sql_provider.py +134 -1
  152. agentpool_storage/sql_provider/utils.py +10 -1
  153. agentpool_storage/text_log_provider.py +1 -0
  154. agentpool_toolsets/builtin/__init__.py +0 -8
  155. agentpool_toolsets/builtin/code.py +95 -56
  156. agentpool_toolsets/builtin/debug.py +16 -21
  157. agentpool_toolsets/builtin/execution_environment.py +99 -103
  158. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  159. agentpool_toolsets/builtin/skills.py +86 -4
  160. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  161. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  162. agentpool_toolsets/fsspec_toolset/grep.py +74 -2
  163. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  164. agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
  165. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  166. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  167. agentpool_toolsets/mcp_discovery/toolset.py +454 -0
  168. agentpool_toolsets/mcp_run_toolset.py +84 -6
  169. agentpool_toolsets/builtin/agent_management.py +0 -239
  170. agentpool_toolsets/builtin/history.py +0 -36
  171. agentpool_toolsets/builtin/integration.py +0 -85
  172. agentpool_toolsets/builtin/tool_management.py +0 -90
  173. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
  174. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -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
- logger.debug("Listed tools from MCP server", num_tools=len(tools))
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 tools
261
+ return filtered_tools
253
262
 
254
263
  async def list_prompts(self) -> list[MCPPrompt]:
255
264
  """Get available prompts from the server."""
@@ -64,7 +64,7 @@ class MCPManager:
64
64
 
65
65
  async def __aenter__(self) -> Self:
66
66
  try:
67
- if tasks := [self._setup_server(server) for server in self.servers]:
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 _setup_server(self, config: MCPServerConfig) -> None:
127
- """Set up a single MCP server resource provider."""
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
- return
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, ClaudeCodeAgent
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
- try:
135
- model = infer_model(ctx.agent.model_name or "test")
136
- except Exception: # noqa: BLE001
137
- model = TestModel()
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(app=app, host=self.config.host, port=port, log_level="warning")
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()]
@@ -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 = CompactionPipelineConfig(steps=[
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, Annotated, Any, Literal, Self, cast
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] = []