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
@@ -50,12 +50,17 @@ from agentpool.agents.acp_agent.client_handler import ACPClientHandler
50
50
  from agentpool.agents.acp_agent.session_state import ACPSessionState
51
51
  from agentpool.agents.base_agent import BaseAgent
52
52
  from agentpool.agents.events import RunStartedEvent, StreamCompleteEvent, ToolCallStartEvent
53
+ from agentpool.agents.modes import ModeInfo
53
54
  from agentpool.log import get_logger
54
55
  from agentpool.messaging import ChatMessage
55
56
  from agentpool.messaging.processing import prepare_prompts
56
57
  from agentpool.models.acp_agents import ACPAgentConfig, MCPCapableACPAgentConfig
57
- from agentpool.talk.stats import MessageStats
58
- from agentpool.utils.streams import merge_queue_into_iterator
58
+ from agentpool.utils.streams import (
59
+ FileTracker,
60
+ merge_queue_into_iterator,
61
+ )
62
+ from agentpool.utils.subprocess_utils import SubprocessError, monitor_process
63
+ from agentpool.utils.token_breakdown import calculate_usage_from_parts
59
64
 
60
65
 
61
66
  if TYPE_CHECKING:
@@ -66,7 +71,8 @@ if TYPE_CHECKING:
66
71
  from evented.configs import EventConfig
67
72
  from exxec import ExecutionEnvironment
68
73
  from pydantic_ai import FinishReason
69
- from tokonomics.model_discovery import ProviderType
74
+ from slashed import BaseCommand
75
+ from tokonomics.model_discovery.model_info import ModelInfo
70
76
 
71
77
  from acp.agent.protocol import Agent as ACPAgentProtocol
72
78
  from acp.client.connection import ClientSideConnection
@@ -80,6 +86,7 @@ if TYPE_CHECKING:
80
86
  from acp.schema.mcp import McpServer
81
87
  from agentpool.agents import AgentContext
82
88
  from agentpool.agents.events import RichAgentStreamEvent
89
+ from agentpool.agents.modes import ModeCategory
83
90
  from agentpool.common_types import (
84
91
  BuiltinEventHandlerType,
85
92
  IndividualEventHandler,
@@ -106,34 +113,6 @@ STOP_REASON_MAP: dict[StopReason, FinishReason] = {
106
113
  }
107
114
 
108
115
 
109
- def extract_file_path_from_tool_call(tool_name: str, raw_input: dict[str, Any]) -> str | None:
110
- """Extract file path from a tool call if it's a file-writing tool.
111
-
112
- Uses simple heuristics by default:
113
- - Tool name contains 'write' or 'edit' (case-insensitive)
114
- - Input contains 'path' or 'file_path' key
115
-
116
- Override in subclasses for agent-specific tool naming conventions.
117
-
118
- Args:
119
- tool_name: Name of the tool being called
120
- raw_input: Tool call arguments
121
-
122
- Returns:
123
- File path if this is a file-writing tool, None otherwise
124
- """
125
- name_lower = tool_name.lower()
126
- if "write" not in name_lower and "edit" not in name_lower:
127
- return None
128
-
129
- # Try common path argument names
130
- for key in ("file_path", "path", "filepath", "filename", "file"):
131
- if key in raw_input and isinstance(val := raw_input[key], str):
132
- return val
133
-
134
- return None
135
-
136
-
137
116
  class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
138
117
  """MessageNode that wraps an external ACP agent subprocess.
139
118
 
@@ -174,6 +153,7 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
174
153
  enable_logging: bool = True,
175
154
  event_configs: Sequence[EventConfig] | None = None,
176
155
  event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
156
+ commands: Sequence[BaseCommand] | None = None,
177
157
  ) -> None: ...
178
158
 
179
159
  @overload
@@ -190,13 +170,13 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
190
170
  env: ExecutionEnvironment | None = None,
191
171
  allow_file_operations: bool = True,
192
172
  allow_terminal: bool = True,
193
- providers: list[ProviderType] | None = None,
194
173
  input_provider: InputProvider | None = None,
195
174
  agent_pool: AgentPool[Any] | None = None,
196
175
  enable_logging: bool = True,
197
176
  event_configs: Sequence[EventConfig] | None = None,
198
177
  event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
199
178
  tool_confirmation_mode: ToolConfirmationMode = "always",
179
+ commands: Sequence[BaseCommand] | None = None,
200
180
  ) -> None: ...
201
181
 
202
182
  def __init__(
@@ -213,13 +193,13 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
213
193
  env: ExecutionEnvironment | None = None,
214
194
  allow_file_operations: bool = True,
215
195
  allow_terminal: bool = True,
216
- providers: list[ProviderType] | None = None,
217
196
  input_provider: InputProvider | None = None,
218
197
  agent_pool: AgentPool[Any] | None = None,
219
198
  enable_logging: bool = True,
220
199
  event_configs: Sequence[EventConfig] | None = None,
221
200
  event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
222
201
  tool_confirmation_mode: ToolConfirmationMode = "always",
202
+ commands: Sequence[BaseCommand] | None = None,
223
203
  ) -> None:
224
204
  # Build config from kwargs if not provided
225
205
  if config is None:
@@ -237,7 +217,6 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
237
217
  allow_file_operations=allow_file_operations,
238
218
  allow_terminal=allow_terminal,
239
219
  requires_tool_confirmation=tool_confirmation_mode,
240
- providers=list(providers) if providers else [],
241
220
  )
242
221
 
243
222
  super().__init__(
@@ -252,6 +231,7 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
252
231
  input_provider=input_provider,
253
232
  tool_confirmation_mode=tool_confirmation_mode,
254
233
  event_handlers=event_handlers,
234
+ commands=commands,
255
235
  )
256
236
 
257
237
  # ACP-specific state
@@ -327,9 +307,13 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
327
307
  """Start subprocess and initialize ACP connection."""
328
308
  await super().__aenter__()
329
309
  await self._setup_toolsets() # Setup toolsets before session creation
330
- await self._start_process()
331
- await self._initialize()
332
- await self._create_session()
310
+ process = await self._start_process()
311
+ try:
312
+ async with monitor_process(process, context="ACP initialization"):
313
+ await self._initialize()
314
+ await self._create_session()
315
+ except SubprocessError as e:
316
+ raise RuntimeError(str(e)) from e
333
317
  await anyio.sleep(0.3) # Small delay to let subprocess fully initialize
334
318
  return self
335
319
 
@@ -343,8 +327,12 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
343
327
  await self._cleanup()
344
328
  await super().__aexit__(exc_type, exc_val, exc_tb)
345
329
 
346
- async def _start_process(self) -> None:
347
- """Start the ACP server subprocess."""
330
+ async def _start_process(self) -> Process:
331
+ """Start the ACP server subprocess.
332
+
333
+ Returns:
334
+ The started Process instance
335
+ """
348
336
  prompt_manager = self.agent_pool.manifest.prompt_manager if self.agent_pool else None
349
337
  args = await self.config.get_args(prompt_manager)
350
338
  cmd = [self.config.get_command(), *args]
@@ -361,9 +349,12 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
361
349
  if not self._process.stdin or not self._process.stdout:
362
350
  msg = "Failed to create subprocess pipes"
363
351
  raise RuntimeError(msg)
352
+ return self._process
364
353
 
365
354
  async def _initialize(self) -> None:
366
355
  """Initialize the ACP connection."""
356
+ from importlib.metadata import metadata
357
+
367
358
  from acp.client.connection import ClientSideConnection
368
359
  from acp.schema import InitializeRequest
369
360
 
@@ -382,9 +373,10 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
382
373
  input_stream=self._process.stdin,
383
374
  output_stream=self._process.stdout,
384
375
  )
376
+ pkg_meta = metadata("agentpool")
385
377
  init_request = InitializeRequest.create(
386
- title="AgentPool",
387
- version="0.1.0",
378
+ title=pkg_meta["Name"],
379
+ version=pkg_meta["Version"],
388
380
  name="agentpool",
389
381
  protocol_version=PROTOCOL_VERSION,
390
382
  terminal=self.config.allow_terminal,
@@ -422,10 +414,6 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
422
414
  model = self._state.current_model_id if self._state else None
423
415
  self.log.info("ACP session created", session_id=self._session_id, model=model)
424
416
 
425
- def add_mcp_server(self, server: McpServer) -> None:
426
- """Add an MCP server to be passed to the next session."""
427
- self._extra_mcp_servers.append(server)
428
-
429
417
  async def add_tool_bridge(self, bridge: ToolManagerBridge) -> None:
430
418
  """Add an external tool bridge to expose its tools via MCP.
431
419
 
@@ -517,6 +505,8 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
517
505
  message_id: str | None = None,
518
506
  input_provider: InputProvider | None = None,
519
507
  message_history: MessageHistory | None = None,
508
+ deps: TDeps | None = None,
509
+ event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
520
510
  ) -> AsyncIterator[RichAgentStreamEvent[str]]:
521
511
  """Stream native events as they arrive from ACP agent.
522
512
 
@@ -525,6 +515,8 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
525
515
  message_id: Optional message id for the final message
526
516
  input_provider: Optional input provider for permission requests
527
517
  message_history: Optional MessageHistory to use instead of agent's own
518
+ deps: Optional dependencies accessible via ctx.data in tools
519
+ event_handlers: Optional event handlers for this run (overrides agent's handlers)
528
520
 
529
521
  Yields:
530
522
  RichAgentStreamEvent instances converted from ACP session updates
@@ -541,14 +533,26 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
541
533
  msg = "Agent not initialized - use async context manager"
542
534
  raise RuntimeError(msg)
543
535
 
544
- # Capture state for use in nested function (avoids type narrowing issues)
545
- state = self._state
546
-
547
536
  conversation = message_history if message_history is not None else self.conversation
537
+ # Use provided event handlers or fall back to agent's handlers
538
+ if event_handlers is not None:
539
+ from anyenv import MultiEventHandler
540
+
541
+ from agentpool.agents.events import resolve_event_handlers
542
+
543
+ handler: MultiEventHandler[IndividualEventHandler] = MultiEventHandler(
544
+ resolve_event_handlers(event_handlers)
545
+ )
546
+ else:
547
+ handler = self.event_handler
548
548
  # Prepare user message for history and convert to ACP content blocks
549
- user_msg, processed_prompts, _original_message = await prepare_prompts(*prompts)
549
+ # Get parent_id from last message in history for tree structure
550
+ last_msg_id = conversation.get_last_message_id()
551
+ user_msg, processed_prompts, _original_message = await prepare_prompts(
552
+ *prompts, parent_id=last_msg_id
553
+ )
550
554
  run_id = str(uuid.uuid4())
551
- state.clear() # Reset state
555
+ self._state.clear() # Reset state
552
556
  # Track messages in pydantic-ai format: ModelRequest -> ModelResponse -> ...
553
557
  # This mirrors pydantic-ai's new_messages() which includes the initial user request.
554
558
  model_messages: list[ModelResponse | ModelRequest] = []
@@ -557,25 +561,22 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
557
561
  model_messages.append(initial_request)
558
562
  current_response_parts: list[TextPart | ThinkingPart | ToolCallPart] = []
559
563
  text_chunks: list[str] = [] # For final content string
560
- touched_files: set[str] = set() # Track files modified by tool calls
564
+ file_tracker = FileTracker() # Track files modified by tool calls
561
565
  run_started = RunStartedEvent(
562
566
  thread_id=self.conversation_id,
563
567
  run_id=run_id,
564
568
  agent_name=self.name,
565
569
  )
566
- for handler in self.event_handler._wrapped_handlers:
567
- await handler(None, run_started)
570
+ await handler(None, run_started)
568
571
  yield run_started
569
572
  content_blocks = convert_to_acp_content(processed_prompts)
570
573
  pending_parts = conversation.get_pending_parts()
571
574
  final_blocks = [*to_acp_content_blocks(pending_parts), *content_blocks]
572
575
  prompt_request = PromptRequest(session_id=self._session_id, prompt=final_blocks)
573
576
  self.log.debug("Starting streaming prompt", num_blocks=len(final_blocks))
574
-
575
577
  # Reset cancellation state
576
578
  self._cancelled = False
577
579
  self._current_stream_task = asyncio.current_task()
578
-
579
580
  # Run prompt in background
580
581
  prompt_task = asyncio.create_task(self._connection.prompt(prompt_request))
581
582
  self._prompt_task = prompt_task
@@ -584,6 +585,7 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
584
585
  async def poll_acp_events() -> AsyncIterator[RichAgentStreamEvent[str]]:
585
586
  """Poll events from ACP state until prompt completes."""
586
587
  last_idx = 0
588
+ assert self._state
587
589
  while not prompt_task.done():
588
590
  if self._client_handler:
589
591
  try:
@@ -595,21 +597,26 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
595
597
  pass
596
598
 
597
599
  # Yield new events from state
598
- while last_idx < len(state.events):
599
- yield state.events[last_idx]
600
+ while last_idx < len(self._state.events):
601
+ yield self._state.events[last_idx]
600
602
  last_idx += 1
601
603
 
602
604
  # Yield remaining events after prompt completes
603
- while last_idx < len(state.events):
604
- yield state.events[last_idx]
605
+ while last_idx < len(self._state.events):
606
+ yield self._state.events[last_idx]
605
607
  last_idx += 1
606
608
 
609
+ # Set deps on tool bridge for access during tool invocations
610
+ # (ContextVar doesn't work because MCP server runs in a separate task)
611
+ if self._tool_bridge:
612
+ self._tool_bridge.current_deps = deps
613
+
607
614
  # Merge ACP events with custom events from queue
608
615
  try:
609
616
  async with merge_queue_into_iterator(
610
617
  poll_acp_events(), self._event_queue
611
618
  ) as merged_events:
612
- async for event in merged_events:
619
+ async for event in file_tracker.track(merged_events):
613
620
  # Check for cancellation
614
621
  if self._cancelled:
615
622
  self.log.info("Stream cancelled by user")
@@ -628,40 +635,35 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
628
635
  current_response_parts.append(
629
636
  ToolCallPart(tool_name=tc_name, args=tc_input, tool_call_id=tc_id)
630
637
  )
631
- # Track files modified by write/edit tools
632
- if file_path := extract_file_path_from_tool_call(
633
- tc_name or "", tc_input or {}
634
- ):
635
- touched_files.add(file_path)
636
-
637
- # Distribute to handlers
638
- for handler in self.event_handler._wrapped_handlers:
639
- await handler(None, event)
638
+
639
+ await handler(None, event)
640
640
  yield event
641
641
  except asyncio.CancelledError:
642
642
  self.log.info("Stream cancelled via task cancellation")
643
643
  self._cancelled = True
644
+ finally:
645
+ # Clear deps from tool bridge
646
+ if self._tool_bridge:
647
+ self._tool_bridge.current_deps = None
644
648
 
645
649
  # Handle cancellation - emit partial message
646
650
  if self._cancelled:
647
651
  text_content = "".join(text_chunks)
648
- metadata: SimpleJsonType = {}
649
- if touched_files:
650
- metadata["touched_files"] = sorted(touched_files)
652
+ metadata: SimpleJsonType = file_tracker.get_metadata()
651
653
  message = ChatMessage[str](
652
654
  content=text_content,
653
655
  role="assistant",
654
656
  name=self.name,
655
657
  message_id=message_id or str(uuid.uuid4()),
656
658
  conversation_id=self.conversation_id,
659
+ parent_id=user_msg.message_id,
657
660
  model_name=self.model_name,
658
661
  messages=model_messages,
659
662
  metadata=metadata,
660
663
  finish_reason="stop",
661
664
  )
662
665
  complete_event = StreamCompleteEvent(message=message)
663
- for handler in self.event_handler._wrapped_handlers:
664
- await handler(None, complete_event)
666
+ await handler(None, complete_event)
665
667
  yield complete_event
666
668
  self._current_stream_task = None
667
669
  self._prompt_task = None
@@ -672,27 +674,44 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
672
674
  finish_reason: FinishReason = STOP_REASON_MAP.get(response.stop_reason, "stop")
673
675
  # Flush response parts to model_messages
674
676
  if current_response_parts:
675
- model_messages.append(ModelResponse(parts=current_response_parts))
677
+ model_messages.append(
678
+ ModelResponse(
679
+ parts=current_response_parts,
680
+ finish_reason=finish_reason,
681
+ model_name=self.model_name,
682
+ provider_name=self.config.type,
683
+ )
684
+ )
676
685
 
677
686
  text_content = "".join(text_chunks)
678
- # Build metadata with touched files if any
679
- metadata = {}
680
- if touched_files:
681
- metadata["touched_files"] = sorted(touched_files)
687
+ metadata = file_tracker.get_metadata()
688
+
689
+ # Calculate approximate token usage from what we can observe
690
+ input_parts = [*processed_prompts, *pending_parts]
691
+ usage, cost_info = await calculate_usage_from_parts(
692
+ input_parts=input_parts,
693
+ response_parts=current_response_parts,
694
+ text_content=text_content,
695
+ model_name=self.model_name,
696
+ provider=self.config.type,
697
+ )
698
+
682
699
  message = ChatMessage[str](
683
700
  content=text_content,
684
701
  role="assistant",
685
702
  name=self.name,
686
703
  message_id=message_id or str(uuid.uuid4()),
687
704
  conversation_id=self.conversation_id,
705
+ parent_id=user_msg.message_id,
688
706
  model_name=self.model_name,
689
707
  messages=model_messages,
690
708
  metadata=metadata,
691
709
  finish_reason=finish_reason,
710
+ usage=usage,
711
+ cost_info=cost_info,
692
712
  )
693
713
  complete_event = StreamCompleteEvent(message=message)
694
- for handler in self.event_handler._wrapped_handlers:
695
- await handler(None, complete_event)
714
+ await handler(None, complete_event)
696
715
  yield complete_event # Emit final StreamCompleteEvent with aggregated message
697
716
  self.message_sent.emit(message)
698
717
  conversation.add_chat_messages([user_msg, message]) # Record to conversation history
@@ -754,8 +773,9 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
754
773
  if self._process: # Clean up existing process if any
755
774
  await self._cleanup()
756
775
  self.config = new_config # Update config and restart
757
- await self._start_process()
758
- await self._initialize()
776
+ process = await self._start_process()
777
+ async with monitor_process(process, context="ACP initialization"):
778
+ await self._initialize()
759
779
 
760
780
  async def set_tool_confirmation_mode(self, mode: ToolConfirmationMode) -> None:
761
781
  """Set the tool confirmation mode for this agent.
@@ -790,10 +810,6 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
790
810
  else:
791
811
  self.log.info("Tool confirmation mode changed (local only)", mode=mode)
792
812
 
793
- async def get_stats(self) -> MessageStats:
794
- """Get message statistics."""
795
- return MessageStats(messages=list(self.conversation.chat_messages))
796
-
797
813
  async def interrupt(self) -> None:
798
814
  """Interrupt the currently running stream.
799
815
 
@@ -822,12 +838,111 @@ class ACPAgent[TDeps = None](BaseAgent[TDeps, str]):
822
838
  if self._current_stream_task and not self._current_stream_task.done():
823
839
  self._current_stream_task.cancel()
824
840
 
841
+ async def get_available_models(self) -> list[ModelInfo] | None:
842
+ """Get available models from the ACP session state.
843
+
844
+ Converts ACP ModelInfo to tokonomics ModelInfo format.
845
+
846
+ Returns:
847
+ List of tokonomics ModelInfo, or None if not available
848
+ """
849
+ from tokonomics.model_discovery.model_info import ModelInfo
850
+
851
+ if not self._state or not self._state.models:
852
+ return None
853
+
854
+ # Convert ACP ModelInfo to tokonomics ModelInfo
855
+ result: list[ModelInfo] = []
856
+ for acp_model in self._state.models.available_models:
857
+ toko_model = ModelInfo(
858
+ id=acp_model.model_id,
859
+ name=acp_model.name,
860
+ description=acp_model.description,
861
+ )
862
+ result.append(toko_model)
863
+ return result
864
+
865
+ def get_modes(self) -> list[ModeCategory]:
866
+ """Get available modes from the ACP session state.
867
+
868
+ Passthrough from remote ACP server's mode state.
869
+
870
+ Returns:
871
+ List of ModeCategory from remote server, empty if not available
872
+ """
873
+ from agentpool.agents.modes import ModeCategory, ModeInfo
874
+
875
+ if not self._state or not self._state.modes:
876
+ return []
877
+
878
+ # Convert ACP SessionModeState to our ModeCategory
879
+ acp_modes = self._state.modes
880
+ category_id = "remote"
881
+ modes = [
882
+ ModeInfo(
883
+ id=m.id,
884
+ name=m.name,
885
+ description=m.description or "",
886
+ category_id=category_id,
887
+ )
888
+ for m in acp_modes.available_modes
889
+ ]
890
+
891
+ return [
892
+ ModeCategory(
893
+ id=category_id,
894
+ name="Mode",
895
+ available_modes=modes,
896
+ current_mode_id=acp_modes.current_mode_id,
897
+ )
898
+ ]
899
+
900
+ async def set_mode(self, mode: ModeInfo | str, category_id: str | None = None) -> None:
901
+ """Set a mode on the remote ACP server.
902
+
903
+ For ACPAgent, this forwards the mode change to the remote ACP server.
904
+
905
+ Args:
906
+ mode: The mode to set - ModeInfo object or mode ID string
907
+ category_id: Optional category ID (ignored for ACP, only one category)
908
+
909
+ Raises:
910
+ RuntimeError: If not connected to ACP server
911
+ ValueError: If mode is not available
912
+ """
913
+ from acp.schema import SetSessionModeRequest
914
+
915
+ # Extract mode_id from ModeInfo if provided
916
+ mode_id = mode.id if isinstance(mode, ModeInfo) else mode
917
+
918
+ if not self._connection or not self._session_id:
919
+ msg = "Not connected to ACP server"
920
+ raise RuntimeError(msg)
921
+
922
+ # Validate mode is available
923
+ available_modes = self.get_modes()
924
+ if available_modes:
925
+ valid_ids = {m.id for cat in available_modes for m in cat.available_modes}
926
+ if mode_id not in valid_ids:
927
+ msg = f"Unknown mode: {mode_id}. Available: {valid_ids}"
928
+ raise ValueError(msg)
929
+
930
+ # Forward mode change to remote ACP server
931
+ request = SetSessionModeRequest(session_id=self._session_id, mode_id=mode_id)
932
+ await self._connection.set_session_mode(request)
933
+
934
+ # Update local state
935
+ if self._state and self._state.modes:
936
+ self._state.modes.current_mode_id = mode_id
937
+
938
+ self.log.info("Mode changed on remote ACP server", mode_id=mode_id)
939
+
825
940
 
826
941
  if __name__ == "__main__":
827
942
 
828
943
  async def main() -> None:
829
944
  """Demo: Basic call to an ACP agent."""
830
- args = ["run", "agentpool", "serve-acp", "--model-provider", "openai"]
945
+ args = ["run", "agentpool", "serve-acp"]
831
946
  cwd = str(Path.cwd())
832
947
  async with ACPAgent(command="uv", args=args, cwd=cwd, event_handlers=["detailed"]) as agent:
833
948
  print("Response (streaming): ", end="", flush=True)
@@ -189,42 +189,56 @@ def acp_to_native_event(update: SessionUpdate) -> RichAgentStreamEvent[Any] | No
189
189
  # Text message chunks -> PartDeltaEvent with TextPartDelta
190
190
  case AgentMessageChunk(content=TextContentBlock(text=text)):
191
191
  return PartDeltaEvent(index=0, delta=TextPartDelta(content_delta=text))
192
+
192
193
  # Thought chunks -> PartDeltaEvent with ThinkingPartDelta
193
194
  case AgentThoughtChunk(content=TextContentBlock(text=text)):
194
195
  return PartDeltaEvent(index=0, delta=ThinkingPartDelta(content_delta=text))
195
- # User message echo - could emit as PartStartEvent if needed
196
+
197
+ # User message echo - usually ignored
196
198
  case UserMessageChunk():
197
- return None # Usually ignored
199
+ return None
200
+
198
201
  # Tool call start -> ToolCallStartEvent
199
- case ToolCallStart() as tc:
202
+ case ToolCallStart(
203
+ tool_call_id=tool_call_id,
204
+ title=title,
205
+ kind=kind,
206
+ content=content,
207
+ locations=locations,
208
+ raw_input=raw_input,
209
+ ):
200
210
  return ToolCallStartEvent(
201
- tool_call_id=tc.tool_call_id,
202
- tool_name=tc.title, # ACP uses title, not separate tool_name
203
- title=tc.title,
204
- kind=tc.kind or "other",
205
- content=convert_acp_content(list(tc.content) if tc.content else None),
206
- locations=convert_acp_locations(list(tc.locations) if tc.locations else None),
207
- raw_input=tc.raw_input or {},
211
+ tool_call_id=tool_call_id,
212
+ tool_name=title, # ACP uses title, not separate tool_name
213
+ title=title,
214
+ kind=kind or "other",
215
+ content=convert_acp_content(list(content) if content else None),
216
+ locations=convert_acp_locations(list(locations) if locations else None),
217
+ raw_input=raw_input or {},
208
218
  )
209
219
 
210
220
  # Tool call progress -> ToolCallProgressEvent
211
- case ToolCallProgress() as tc:
212
- items = convert_acp_content(list(tc.content) if tc.content else None)
221
+ case ToolCallProgress(
222
+ tool_call_id=tool_call_id,
223
+ status=status,
224
+ title=title,
225
+ content=content,
226
+ raw_output=raw_output,
227
+ ):
213
228
  return ToolCallProgressEvent(
214
- tool_call_id=tc.tool_call_id,
215
- status=tc.status or "in_progress",
216
- title=tc.title,
217
- items=items,
218
- message=str(tc.raw_output) if tc.raw_output else None,
229
+ tool_call_id=tool_call_id,
230
+ status=status or "in_progress",
231
+ title=title,
232
+ items=convert_acp_content(list(content) if content else None),
233
+ message=str(raw_output) if raw_output else None,
219
234
  )
220
235
 
221
236
  # Plan update -> PlanUpdateEvent
222
- case AgentPlanUpdate(entries=acp_entries):
237
+ case AgentPlanUpdate(entries=entries):
223
238
  from agentpool.resource_providers.plan_provider import PlanEntry
224
239
 
225
240
  native_entries = [
226
- PlanEntry(content=e.content, priority=e.priority, status=e.status)
227
- for e in acp_entries
241
+ PlanEntry(content=e.content, priority=e.priority, status=e.status) for e in entries
228
242
  ]
229
243
  return PlanUpdateEvent(entries=native_entries)
230
244
 
@@ -251,6 +265,9 @@ def mcp_config_to_acp(config: MCPServerConfig) -> McpServer | None: ...
251
265
  def mcp_config_to_acp(config: MCPServerConfig) -> McpServer | None:
252
266
  """Convert native MCPServerConfig to ACP McpServer format.
253
267
 
268
+ If the config has tool filtering (enabled_tools or disabled_tools),
269
+ the server is wrapped with mcp-filter proxy to apply the filtering.
270
+
254
271
  Args:
255
272
  config: agentpool MCP server configuration
256
273
 
@@ -265,19 +282,27 @@ def mcp_config_to_acp(config: MCPServerConfig) -> McpServer | None:
265
282
  StreamableHTTPMCPServerConfig,
266
283
  )
267
284
 
285
+ # If filtering is configured, wrap with mcp-filter first
286
+ if config.needs_tool_filtering():
287
+ config = config.wrap_with_mcp_filter()
288
+
268
289
  match config:
269
290
  case StdioMCPServerConfig(command=command, args=args):
270
291
  env_vars = config.get_env_vars() if hasattr(config, "get_env_vars") else {}
292
+ env_list = [EnvVariable(name=k, value=v) for k, v in env_vars.items()]
271
293
  return StdioMcpServer(
272
294
  name=config.name or command,
273
295
  command=command,
274
296
  args=list(args) if args else [],
275
- env=[EnvVariable(name=k, value=v) for k, v in env_vars.items()],
297
+ env=env_list,
276
298
  )
299
+
277
300
  case SSEMCPServerConfig(url=url):
278
301
  return SseMcpServer(name=config.name or str(url), url=url, headers=[])
302
+
279
303
  case StreamableHTTPMCPServerConfig(url=url):
280
304
  return HttpMcpServer(name=config.name or str(url), url=url, headers=[])
305
+
281
306
  case _:
282
307
  return None
283
308