agentpool 2.2.3__py3-none-any.whl → 2.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. acp/__init__.py +0 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/client/connection.py +38 -29
  7. acp/client/implementations/default_client.py +3 -2
  8. acp/client/implementations/headless_client.py +2 -2
  9. acp/connection.py +2 -2
  10. acp/notifications.py +18 -49
  11. acp/schema/__init__.py +2 -0
  12. acp/schema/agent_responses.py +21 -0
  13. acp/schema/client_requests.py +3 -3
  14. acp/schema/session_state.py +63 -29
  15. acp/task/supervisor.py +2 -2
  16. acp/utils.py +2 -2
  17. agentpool/__init__.py +2 -0
  18. agentpool/agents/acp_agent/acp_agent.py +278 -263
  19. agentpool/agents/acp_agent/acp_converters.py +150 -17
  20. agentpool/agents/acp_agent/client_handler.py +35 -24
  21. agentpool/agents/acp_agent/session_state.py +14 -6
  22. agentpool/agents/agent.py +471 -643
  23. agentpool/agents/agui_agent/agui_agent.py +104 -107
  24. agentpool/agents/agui_agent/helpers.py +3 -4
  25. agentpool/agents/base_agent.py +485 -32
  26. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  27. agentpool/agents/claude_code_agent/__init__.py +13 -1
  28. agentpool/agents/claude_code_agent/claude_code_agent.py +654 -334
  29. agentpool/agents/claude_code_agent/converters.py +4 -141
  30. agentpool/agents/claude_code_agent/models.py +77 -0
  31. agentpool/agents/claude_code_agent/static_info.py +100 -0
  32. agentpool/agents/claude_code_agent/usage.py +242 -0
  33. agentpool/agents/events/__init__.py +22 -0
  34. agentpool/agents/events/builtin_handlers.py +65 -0
  35. agentpool/agents/events/event_emitter.py +3 -0
  36. agentpool/agents/events/events.py +84 -3
  37. agentpool/agents/events/infer_info.py +145 -0
  38. agentpool/agents/events/processors.py +254 -0
  39. agentpool/agents/interactions.py +41 -6
  40. agentpool/agents/modes.py +13 -0
  41. agentpool/agents/slashed_agent.py +5 -4
  42. agentpool/agents/tool_wrapping.py +18 -6
  43. agentpool/common_types.py +35 -21
  44. agentpool/config_resources/acp_assistant.yml +2 -2
  45. agentpool/config_resources/agents.yml +3 -0
  46. agentpool/config_resources/agents_template.yml +1 -0
  47. agentpool/config_resources/claude_code_agent.yml +9 -8
  48. agentpool/config_resources/external_acp_agents.yml +2 -1
  49. agentpool/delegation/base_team.py +4 -30
  50. agentpool/delegation/pool.py +104 -265
  51. agentpool/delegation/team.py +57 -57
  52. agentpool/delegation/teamrun.py +50 -55
  53. agentpool/functional/run.py +10 -4
  54. agentpool/mcp_server/client.py +73 -38
  55. agentpool/mcp_server/conversions.py +54 -13
  56. agentpool/mcp_server/manager.py +9 -23
  57. agentpool/mcp_server/registries/official_registry_client.py +10 -1
  58. agentpool/mcp_server/tool_bridge.py +114 -79
  59. agentpool/messaging/connection_manager.py +11 -10
  60. agentpool/messaging/event_manager.py +5 -5
  61. agentpool/messaging/message_container.py +6 -30
  62. agentpool/messaging/message_history.py +87 -8
  63. agentpool/messaging/messagenode.py +52 -14
  64. agentpool/messaging/messages.py +2 -26
  65. agentpool/messaging/processing.py +10 -22
  66. agentpool/models/__init__.py +1 -1
  67. agentpool/models/acp_agents/base.py +6 -2
  68. agentpool/models/acp_agents/mcp_capable.py +124 -15
  69. agentpool/models/acp_agents/non_mcp.py +0 -23
  70. agentpool/models/agents.py +66 -66
  71. agentpool/models/agui_agents.py +1 -1
  72. agentpool/models/claude_code_agents.py +111 -17
  73. agentpool/models/file_parsing.py +0 -1
  74. agentpool/models/manifest.py +70 -50
  75. agentpool/prompts/conversion_manager.py +1 -1
  76. agentpool/prompts/prompts.py +5 -2
  77. agentpool/resource_providers/__init__.py +2 -0
  78. agentpool/resource_providers/aggregating.py +4 -2
  79. agentpool/resource_providers/base.py +13 -3
  80. agentpool/resource_providers/codemode/code_executor.py +72 -5
  81. agentpool/resource_providers/codemode/helpers.py +2 -2
  82. agentpool/resource_providers/codemode/provider.py +64 -12
  83. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  84. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  85. agentpool/resource_providers/filtering.py +3 -1
  86. agentpool/resource_providers/mcp_provider.py +66 -12
  87. agentpool/resource_providers/plan_provider.py +111 -18
  88. agentpool/resource_providers/pool.py +5 -3
  89. agentpool/resource_providers/resource_info.py +111 -0
  90. agentpool/resource_providers/static.py +2 -2
  91. agentpool/sessions/__init__.py +2 -0
  92. agentpool/sessions/manager.py +2 -3
  93. agentpool/sessions/models.py +9 -6
  94. agentpool/sessions/protocol.py +28 -0
  95. agentpool/sessions/session.py +11 -55
  96. agentpool/storage/manager.py +361 -54
  97. agentpool/talk/registry.py +4 -4
  98. agentpool/talk/talk.py +9 -10
  99. agentpool/testing.py +1 -1
  100. agentpool/tool_impls/__init__.py +6 -0
  101. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  102. agentpool/tool_impls/agent_cli/tool.py +95 -0
  103. agentpool/tool_impls/bash/__init__.py +64 -0
  104. agentpool/tool_impls/bash/helpers.py +35 -0
  105. agentpool/tool_impls/bash/tool.py +171 -0
  106. agentpool/tool_impls/delete_path/__init__.py +70 -0
  107. agentpool/tool_impls/delete_path/tool.py +142 -0
  108. agentpool/tool_impls/download_file/__init__.py +80 -0
  109. agentpool/tool_impls/download_file/tool.py +183 -0
  110. agentpool/tool_impls/execute_code/__init__.py +55 -0
  111. agentpool/tool_impls/execute_code/tool.py +163 -0
  112. agentpool/tool_impls/grep/__init__.py +80 -0
  113. agentpool/tool_impls/grep/tool.py +200 -0
  114. agentpool/tool_impls/list_directory/__init__.py +73 -0
  115. agentpool/tool_impls/list_directory/tool.py +197 -0
  116. agentpool/tool_impls/question/__init__.py +42 -0
  117. agentpool/tool_impls/question/tool.py +127 -0
  118. agentpool/tool_impls/read/__init__.py +104 -0
  119. agentpool/tool_impls/read/tool.py +305 -0
  120. agentpool/tools/__init__.py +2 -1
  121. agentpool/tools/base.py +114 -34
  122. agentpool/tools/manager.py +57 -1
  123. agentpool/ui/base.py +2 -2
  124. agentpool/ui/mock_provider.py +2 -2
  125. agentpool/ui/stdlib_provider.py +2 -2
  126. agentpool/utils/streams.py +21 -96
  127. agentpool/vfs_registry.py +7 -2
  128. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
  129. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
  130. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  131. agentpool_cli/__main__.py +20 -0
  132. agentpool_cli/create.py +1 -1
  133. agentpool_cli/serve_acp.py +59 -1
  134. agentpool_cli/serve_opencode.py +1 -1
  135. agentpool_cli/ui.py +557 -0
  136. agentpool_commands/__init__.py +12 -5
  137. agentpool_commands/agents.py +1 -1
  138. agentpool_commands/pool.py +260 -0
  139. agentpool_commands/session.py +1 -1
  140. agentpool_commands/text_sharing/__init__.py +119 -0
  141. agentpool_commands/text_sharing/base.py +123 -0
  142. agentpool_commands/text_sharing/github_gist.py +80 -0
  143. agentpool_commands/text_sharing/opencode.py +462 -0
  144. agentpool_commands/text_sharing/paste_rs.py +59 -0
  145. agentpool_commands/text_sharing/pastebin.py +116 -0
  146. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  147. agentpool_commands/utils.py +31 -32
  148. agentpool_config/__init__.py +30 -2
  149. agentpool_config/agentpool_tools.py +498 -0
  150. agentpool_config/converters.py +1 -1
  151. agentpool_config/event_handlers.py +42 -0
  152. agentpool_config/events.py +1 -1
  153. agentpool_config/forward_targets.py +1 -4
  154. agentpool_config/jinja.py +3 -3
  155. agentpool_config/mcp_server.py +1 -5
  156. agentpool_config/nodes.py +1 -1
  157. agentpool_config/observability.py +44 -0
  158. agentpool_config/session.py +0 -3
  159. agentpool_config/storage.py +38 -39
  160. agentpool_config/task.py +3 -3
  161. agentpool_config/tools.py +11 -28
  162. agentpool_config/toolsets.py +22 -90
  163. agentpool_server/a2a_server/agent_worker.py +307 -0
  164. agentpool_server/a2a_server/server.py +23 -18
  165. agentpool_server/acp_server/acp_agent.py +125 -56
  166. agentpool_server/acp_server/commands/acp_commands.py +46 -216
  167. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
  168. agentpool_server/acp_server/event_converter.py +651 -0
  169. agentpool_server/acp_server/input_provider.py +53 -10
  170. agentpool_server/acp_server/server.py +1 -11
  171. agentpool_server/acp_server/session.py +90 -410
  172. agentpool_server/acp_server/session_manager.py +8 -34
  173. agentpool_server/agui_server/server.py +3 -1
  174. agentpool_server/mcp_server/server.py +5 -2
  175. agentpool_server/opencode_server/ENDPOINTS.md +53 -14
  176. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  177. agentpool_server/opencode_server/__init__.py +0 -8
  178. agentpool_server/opencode_server/converters.py +132 -26
  179. agentpool_server/opencode_server/input_provider.py +160 -8
  180. agentpool_server/opencode_server/models/__init__.py +42 -20
  181. agentpool_server/opencode_server/models/app.py +12 -0
  182. agentpool_server/opencode_server/models/events.py +203 -29
  183. agentpool_server/opencode_server/models/mcp.py +19 -0
  184. agentpool_server/opencode_server/models/message.py +18 -1
  185. agentpool_server/opencode_server/models/parts.py +134 -1
  186. agentpool_server/opencode_server/models/question.py +56 -0
  187. agentpool_server/opencode_server/models/session.py +13 -1
  188. agentpool_server/opencode_server/routes/__init__.py +4 -0
  189. agentpool_server/opencode_server/routes/agent_routes.py +33 -2
  190. agentpool_server/opencode_server/routes/app_routes.py +66 -3
  191. agentpool_server/opencode_server/routes/config_routes.py +66 -5
  192. agentpool_server/opencode_server/routes/file_routes.py +184 -5
  193. agentpool_server/opencode_server/routes/global_routes.py +1 -1
  194. agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
  195. agentpool_server/opencode_server/routes/message_routes.py +122 -66
  196. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  197. agentpool_server/opencode_server/routes/pty_routes.py +23 -22
  198. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  199. agentpool_server/opencode_server/routes/session_routes.py +139 -68
  200. agentpool_server/opencode_server/routes/tui_routes.py +1 -1
  201. agentpool_server/opencode_server/server.py +47 -2
  202. agentpool_server/opencode_server/state.py +30 -0
  203. agentpool_storage/__init__.py +0 -4
  204. agentpool_storage/base.py +81 -2
  205. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  206. agentpool_storage/claude_provider/__init__.py +42 -0
  207. agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
  208. agentpool_storage/file_provider.py +149 -15
  209. agentpool_storage/memory_provider.py +132 -12
  210. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  211. agentpool_storage/opencode_provider/__init__.py +16 -0
  212. agentpool_storage/opencode_provider/helpers.py +414 -0
  213. agentpool_storage/opencode_provider/provider.py +895 -0
  214. agentpool_storage/session_store.py +20 -6
  215. agentpool_storage/sql_provider/sql_provider.py +135 -2
  216. agentpool_storage/sql_provider/utils.py +2 -12
  217. agentpool_storage/zed_provider/__init__.py +16 -0
  218. agentpool_storage/zed_provider/helpers.py +281 -0
  219. agentpool_storage/zed_provider/models.py +130 -0
  220. agentpool_storage/zed_provider/provider.py +442 -0
  221. agentpool_storage/zed_provider.py +803 -0
  222. agentpool_toolsets/__init__.py +0 -2
  223. agentpool_toolsets/builtin/__init__.py +2 -4
  224. agentpool_toolsets/builtin/code.py +4 -4
  225. agentpool_toolsets/builtin/debug.py +115 -40
  226. agentpool_toolsets/builtin/execution_environment.py +54 -165
  227. agentpool_toolsets/builtin/skills.py +0 -77
  228. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  229. agentpool_toolsets/builtin/workers.py +4 -2
  230. agentpool_toolsets/composio_toolset.py +2 -2
  231. agentpool_toolsets/entry_points.py +3 -1
  232. agentpool_toolsets/fsspec_toolset/grep.py +25 -5
  233. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  234. agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
  235. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  236. agentpool_toolsets/mcp_discovery/toolset.py +74 -17
  237. agentpool_toolsets/mcp_run_toolset.py +8 -11
  238. agentpool_toolsets/notifications.py +33 -33
  239. agentpool_toolsets/openapi.py +3 -1
  240. agentpool_toolsets/search_toolset.py +3 -1
  241. agentpool_config/resources.py +0 -33
  242. agentpool_server/acp_server/acp_tools.py +0 -43
  243. agentpool_server/acp_server/commands/spawn.py +0 -210
  244. agentpool_storage/opencode_provider.py +0 -730
  245. agentpool_storage/text_log_provider.py +0 -276
  246. agentpool_toolsets/builtin/chain.py +0 -288
  247. agentpool_toolsets/builtin/user_interaction.py +0 -52
  248. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  249. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  250. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,10 +6,11 @@ import asyncio
6
6
  from collections import deque
7
7
  from contextlib import asynccontextmanager
8
8
  from dataclasses import dataclass, field
9
+ import json
9
10
  from typing import TYPE_CHECKING, Any, Self, assert_never
10
11
  from uuid import UUID, uuid4
11
12
 
12
- from psygnal import Signal
13
+ from anyenv.signals import Signal
13
14
  from upathtools import read_path, to_upath
14
15
 
15
16
  from agentpool.log import get_logger
@@ -24,6 +25,7 @@ if TYPE_CHECKING:
24
25
  from datetime import datetime
25
26
  from types import TracebackType
26
27
 
28
+ from fsspec.asyn import AsyncFileSystem
27
29
  from pydantic_ai import UserContent
28
30
  from toprompt import AnyPromptType
29
31
  from upathtools import JoinablePathLike
@@ -48,7 +50,7 @@ class MessageHistory:
48
50
  session_id: str
49
51
  timestamp: datetime = field(default_factory=get_now)
50
52
 
51
- history_cleared = Signal(HistoryCleared)
53
+ history_cleared = Signal[HistoryCleared]()
52
54
 
53
55
  def __init__(
54
56
  self,
@@ -94,6 +96,14 @@ class MessageHistory:
94
96
  # Note: max_messages and max_tokens will be handled in add_message/get_history
95
97
  # to maintain the rolling window during conversation
96
98
 
99
+ # Filesystem for message history
100
+ from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
101
+ from fsspec.implementations.memory import MemoryFileSystem
102
+
103
+ self._memory_fs = MemoryFileSystem()
104
+ self._fs = AsyncFileSystemWrapper(self._memory_fs)
105
+ self._fs_initialized = False
106
+
97
107
  @property
98
108
  def storage(self) -> StorageManager:
99
109
  return self._storage
@@ -150,17 +160,20 @@ class MessageHistory:
150
160
  self,
151
161
  *,
152
162
  max_tokens: int | None = None,
153
- include_system: bool = False,
154
163
  format_template: str | None = None,
155
- num_messages: int | None = None, # Add this parameter
164
+ num_messages: int | None = None,
156
165
  ) -> str:
157
166
  """Format conversation history as a single context message.
158
167
 
159
168
  Args:
160
169
  max_tokens: Optional limit to include only last N tokens
161
- include_system: Whether to include system messages
162
170
  format_template: Optional custom format (defaults to agent/message pairs)
163
171
  num_messages: Optional limit to include only last N messages
172
+
173
+ Note:
174
+ System prompts are stored as metadata (ModelRequest.instructions),
175
+ not as separate messages with role="system". ChatMessage.role only
176
+ supports "user" and "assistant".
164
177
  """
165
178
  template = format_template or "Agent {agent}: {content}\n"
166
179
  messages: list[str] = []
@@ -323,14 +336,14 @@ class MessageHistory:
323
336
  self.chat_messages.clear()
324
337
  self.chat_messages.extend(history)
325
338
 
326
- def clear(self) -> None:
339
+ async def clear(self) -> None:
327
340
  """Clear conversation history and prompts."""
328
341
  from agentpool.messaging import ChatMessageList
329
342
 
330
343
  self.chat_messages = ChatMessageList()
331
344
  self._last_messages = []
332
345
  event = self.HistoryCleared(session_id=str(self.id))
333
- self.history_cleared.emit(event)
346
+ await self.history_cleared.emit(event)
334
347
 
335
348
  @asynccontextmanager
336
349
  async def temporary_state(
@@ -489,12 +502,78 @@ class MessageHistory:
489
502
  return None
490
503
  return self.chat_messages[-1].message_id
491
504
 
505
+ def _update_filesystem(self) -> None:
506
+ """Update filesystem with current message history."""
507
+ # Clear existing files
508
+ self._memory_fs.store.clear()
509
+ self._memory_fs.pseudo_dirs.clear()
510
+
511
+ # Create directory structure
512
+ self._memory_fs.makedirs("messages", exist_ok=True)
513
+ self._memory_fs.makedirs("by_role", exist_ok=True)
514
+
515
+ for msg in self.chat_messages:
516
+ # Format: {timestamp}_{role}_{message_id}
517
+ timestamp = msg.timestamp.strftime("%Y%m%d_%H%M%S_%f")
518
+ base_name = f"{timestamp}_{msg.role}_{msg.message_id}"
519
+
520
+ # Write message content
521
+ content_path = f"messages/{base_name}.txt"
522
+ content = str(msg.content)
523
+ self._memory_fs.pipe(content_path, content.encode("utf-8"))
524
+
525
+ # Write metadata
526
+ metadata = {
527
+ "message_id": msg.message_id,
528
+ "role": msg.role,
529
+ "timestamp": msg.timestamp.isoformat(),
530
+ "parent_id": msg.parent_id,
531
+ "model_name": msg.model_name,
532
+ "tokens": msg.usage.total_tokens if msg.usage else None,
533
+ "cost": float(msg.cost_info.total_cost) if msg.cost_info else None,
534
+ }
535
+ metadata_path = f"messages/{base_name}.json"
536
+ self._memory_fs.pipe(metadata_path, json.dumps(metadata, indent=2).encode("utf-8"))
537
+
538
+ # Create role-based directory symlinks (by storing paths)
539
+ role_dir = f"by_role/{msg.role}"
540
+ self._memory_fs.makedirs(role_dir, exist_ok=True)
541
+
542
+ # Write summary
543
+ summary = {
544
+ "session_id": self.id,
545
+ "total_messages": len(self.chat_messages),
546
+ "total_tokens": self.get_history_tokens(),
547
+ "total_cost": self.chat_messages.get_total_cost(),
548
+ "roles": {
549
+ "user": len([m for m in self.chat_messages if m.role == "user"]),
550
+ "assistant": len([m for m in self.chat_messages if m.role == "assistant"]),
551
+ },
552
+ }
553
+ self._memory_fs.pipe("summary.json", json.dumps(summary, indent=2).encode("utf-8"))
554
+
555
+ self._fs_initialized = True
556
+
557
+ def get_fs(self) -> AsyncFileSystem:
558
+ """Get filesystem view of message history.
559
+
560
+ Returns:
561
+ AsyncFileSystem containing:
562
+ - messages/{timestamp}_{role}_{message_id}.txt - Message content
563
+ - messages/{timestamp}_{role}_{message_id}.json - Message metadata
564
+ - by_role/{role}/ - Messages organized by role
565
+ - summary.json - Conversation statistics
566
+ """
567
+ # Update filesystem on access
568
+ self._update_filesystem()
569
+ return self._fs
570
+
492
571
 
493
572
  if __name__ == "__main__":
494
573
  from agentpool import Agent
495
574
 
496
575
  async def main() -> None:
497
- async with Agent() as agent:
576
+ async with Agent(model="openai:gpt-5-nano") as agent:
498
577
  await agent.conversation.add_context_from_path("E:/mcp_zed.yml")
499
578
  print(agent.conversation.get_history())
500
579
 
@@ -5,9 +5,8 @@ from __future__ import annotations
5
5
  from abc import ABC, abstractmethod
6
6
  from collections.abc import Sequence
7
7
  from typing import TYPE_CHECKING, Any, Literal, Self, overload
8
- from uuid import uuid4
9
8
 
10
- from psygnal import Signal
9
+ from anyenv.signals import Signal
11
10
 
12
11
  from agentpool.log import get_logger
13
12
  from agentpool.messaging import ChatMessage
@@ -20,8 +19,8 @@ if TYPE_CHECKING:
20
19
  from datetime import timedelta
21
20
  from types import TracebackType
22
21
 
23
- from evented.configs import EventConfig
24
22
  from evented.event_data import EventData
23
+ from evented_config import EventConfig
25
24
 
26
25
  from agentpool.common_types import (
27
26
  AnyTransformFn,
@@ -34,7 +33,7 @@ if TYPE_CHECKING:
34
33
  from agentpool.storage import StorageManager
35
34
  from agentpool.talk import Talk, TeamTalk
36
35
  from agentpool.talk.stats import AggregatedMessageStats, MessageStats
37
- from agentpool.tools.base import Tool
36
+ from agentpool.tools.base import FunctionTool
38
37
  from agentpool_config.forward_targets import ConnectionType
39
38
  from agentpool_config.mcp_server import MCPServerConfig
40
39
 
@@ -45,10 +44,10 @@ logger = get_logger(__name__)
45
44
  class MessageNode[TDeps, TResult](ABC):
46
45
  """Base class for all message processing nodes."""
47
46
 
48
- message_received = Signal(ChatMessage)
47
+ message_received = Signal[ChatMessage[Any]]()
49
48
  """Signal emitted when node receives a message."""
50
49
 
51
- message_sent = Signal(ChatMessage)
50
+ message_sent = Signal[ChatMessage[Any]]()
52
51
  """Signal emitted when node creates a message."""
53
52
 
54
53
  def __init__(
@@ -86,18 +85,33 @@ class MessageNode[TDeps, TResult](ABC):
86
85
  name_ = f"node_{self._name}"
87
86
  self.mcp = MCPManager(name_, servers=mcp_servers, owner=self.name)
88
87
  self.enable_db_logging = enable_logging
89
- self.conversation_id = str(uuid4())
90
- # Connect to the combined signal to capture all messages
91
- # TODO: need to check this
92
- # node.message_received.connect(self.log_message)
88
+ self.conversation_id: str | None = None
89
+ self.conversation_title: str | None = None
93
90
 
94
- async def __aenter__(self) -> Self:
95
- """Initialize base message node."""
96
- if self.enable_db_logging and self.storage:
91
+ def _set_conversation_title(self, title: str) -> None:
92
+ """Callback for setting conversation title (called by storage manager)."""
93
+ self.conversation_title = title
94
+
95
+ async def log_conversation(self, initial_prompt: str | None = None) -> None:
96
+ """Log conversation to storage if enabled.
97
+
98
+ Should be called at the start of run_stream() after conversation_id is set.
99
+ For native agents, generate conversation_id first with uuid4().
100
+ For wrapped agents (Claude Code), set conversation_id from SDK session first.
101
+
102
+ Args:
103
+ initial_prompt: Optional initial prompt to trigger title generation.
104
+ """
105
+ if self.enable_db_logging and self.storage and self.conversation_id:
97
106
  await self.storage.log_conversation(
98
107
  conversation_id=self.conversation_id,
99
108
  node_name=self.name,
109
+ initial_prompt=initial_prompt,
110
+ on_title_generated=self._set_conversation_title,
100
111
  )
112
+
113
+ async def __aenter__(self) -> Self:
114
+ """Initialize base message node."""
101
115
  try:
102
116
  await self._events.__aenter__()
103
117
  await self.mcp.__aenter__()
@@ -161,7 +175,7 @@ class MessageNode[TDeps, TResult](ABC):
161
175
 
162
176
  def to_tool(
163
177
  self, *, name: str | None = None, description: str | None = None, **kwargs: Any
164
- ) -> Tool[TResult]:
178
+ ) -> FunctionTool[TResult]:
165
179
  """Convert node to a callable tool.
166
180
 
167
181
  Args:
@@ -354,6 +368,30 @@ class MessageNode[TDeps, TResult](ABC):
354
368
  async def run(self, *prompts: Any, **kwargs: Any) -> ChatMessage[TResult]:
355
369
  """Execute node with prompts. Implementation-specific run logic."""
356
370
 
371
+ async def run_message(
372
+ self,
373
+ message: ChatMessage[Any],
374
+ **kwargs: Any,
375
+ ) -> ChatMessage[TResult]:
376
+ """Run with an incoming ChatMessage (e.g., from Talk routing).
377
+
378
+ Extracts content from the message, preserves conversation_id,
379
+ and sets parent_id to track the message chain.
380
+
381
+ Args:
382
+ message: The incoming ChatMessage to process
383
+ **kwargs: Additional arguments passed to run()
384
+
385
+ Returns:
386
+ Response ChatMessage with message chain tracked via parent_id
387
+ """
388
+ return await self.run(
389
+ message.content,
390
+ conversation_id=message.conversation_id,
391
+ parent_id=message.message_id,
392
+ **kwargs,
393
+ )
394
+
357
395
  async def get_message_history(self, limit: int | None = None) -> list[ChatMessage[Any]]:
358
396
  """Get message history from storage."""
359
397
  from agentpool_config.session import SessionQuery
@@ -72,9 +72,7 @@ Metadata:
72
72
  {{ key }}: {{ value }}
73
73
  {%- endfor %}
74
74
  {%- endif %}
75
- {%- if forwarded_from %}
76
- Forwarded via: {{ forwarded_from|join(' -> ') }}
77
- {%- endif %}"""
75
+ """
78
76
 
79
77
  MARKDOWN_TEMPLATE = """## {{ name or role.title() }}
80
78
  *{{ timestamp.strftime('%Y-%m-%d %H:%M:%S') }}*
@@ -98,10 +96,7 @@ MARKDOWN_TEMPLATE = """## {{ name or role.title() }}
98
96
  ```
99
97
  {%- endif %}
100
98
 
101
- {% if forwarded_from %}
102
-
103
- *Forwarded via: {{ forwarded_from|join(' → ') }}*
104
- {% endif %}"""
99
+ """
105
100
 
106
101
  MESSAGE_TEMPLATES = {
107
102
  "simple": SIMPLE_TEMPLATE,
@@ -216,9 +211,6 @@ class ChatMessage[TContent]:
216
211
  name: str | None = None
217
212
  """Display name for the message sender in UI."""
218
213
 
219
- forwarded_from: list[str] = field(default_factory=list)
220
- """List of agent names (the chain) that forwarded this message to the sender."""
221
-
222
214
  provider_details: dict[str, Any] = field(default_factory=dict)
223
215
  """Provider specific metadata / extra information."""
224
216
 
@@ -335,7 +327,6 @@ class ChatMessage[TContent]:
335
327
  message: ModelMessage,
336
328
  conversation_id: str | None = None,
337
329
  name: str | None = None,
338
- forwarded_from: list[str] | None = None,
339
330
  parent_id: str | None = None,
340
331
  ) -> ChatMessage[TContentType]:
341
332
  """Convert a Pydantic model to a ChatMessage."""
@@ -346,8 +337,6 @@ class ChatMessage[TContent]:
346
337
  content=content,
347
338
  role="user",
348
339
  message_id=run_id or str(uuid.uuid4()),
349
- # instructions=instructions,
350
- forwarded_from=forwarded_from or [],
351
340
  name=name,
352
341
  parent_id=parent_id,
353
342
  )
@@ -375,7 +364,6 @@ class ChatMessage[TContent]:
375
364
  finish_reason=finish_reason,
376
365
  provider_response_id=provider_response_id,
377
366
  name=name,
378
- forwarded_from=forwarded_from or [],
379
367
  parent_id=parent_id,
380
368
  )
381
369
  case _ as unreachable:
@@ -443,18 +431,6 @@ class ChatMessage[TContent]:
443
431
  metadata=metadata or {},
444
432
  )
445
433
 
446
- def forwarded(self, previous_message: ChatMessage[Any]) -> Self:
447
- """Create new message showing it was forwarded from another message.
448
-
449
- Args:
450
- previous_message: The message that led to this one's creation
451
-
452
- Returns:
453
- New message with updated chain showing the path through previous message
454
- """
455
- from_ = [*previous_message.forwarded_from, previous_message.name or "unknown"]
456
- return replace(self, forwarded_from=from_)
457
-
458
434
  @property
459
435
  def response(self) -> ModelResponse:
460
436
  """Return the last response from the message history."""
@@ -17,34 +17,27 @@ if TYPE_CHECKING:
17
17
 
18
18
 
19
19
  async def prepare_prompts(
20
- *prompt: PromptCompatible | ChatMessage[Any],
20
+ *prompt: PromptCompatible,
21
21
  parent_id: str | None = None,
22
- ) -> tuple[ChatMessage[Any], list[UserContent], ChatMessage[Any] | None]:
22
+ conversation_id: str | None = None,
23
+ ) -> tuple[ChatMessage[Any], list[UserContent]]:
23
24
  """Prepare prompts for processing.
24
25
 
25
- Extracted from MessageNode.pre_run logic.
26
-
27
26
  Args:
28
27
  *prompt: The prompt(s) to prepare.
29
28
  parent_id: Optional ID of the parent message (typically the previous response).
29
+ conversation_id: Optional conversation ID for the user message.
30
30
 
31
31
  Returns:
32
32
  A tuple of:
33
- - Either incoming message, or a constructed incoming message based
34
- on the prompt(s).
33
+ - A ChatMessage representing the user prompt.
35
34
  - A list of prompts to be sent to the model.
36
- - The original ChatMessage if forwarded, None otherwise
37
35
  """
38
- if len(prompt) == 1 and isinstance(prompt[0], ChatMessage):
39
- original_msg = prompt[0]
40
- # Update received message's chain to show it came through its source
41
- user_msg = original_msg.forwarded(original_msg).to_request()
42
- prompts = await convert_prompts([user_msg.content])
43
- # clear cost info to avoid double-counting
44
- return user_msg, prompts, original_msg
45
36
  prompts = await convert_prompts(prompt)
46
- user_msg = ChatMessage.user_prompt(message=prompts, parent_id=parent_id)
47
- return user_msg, prompts, None
37
+ user_msg = ChatMessage.user_prompt(
38
+ message=prompts, parent_id=parent_id, conversation_id=conversation_id
39
+ )
40
+ return user_msg, prompts
48
41
 
49
42
 
50
43
  async def finalize_message(
@@ -52,7 +45,6 @@ async def finalize_message(
52
45
  previous_message: ChatMessage[Any] | None,
53
46
  node: MessageNode[Any, Any],
54
47
  connections: ConnectionManager,
55
- original_message: ChatMessage[Any] | None,
56
48
  wait_for_connections: bool | None = None,
57
49
  ) -> ChatMessage[Any]:
58
50
  """Handle message finalization and routing.
@@ -62,16 +54,12 @@ async def finalize_message(
62
54
  previous_message: The original user message (if any)
63
55
  node: The message node that produced the message
64
56
  connections: Connection manager for routing
65
- original_message: The original ChatMessage if forwarded, None otherwise
66
57
  wait_for_connections: Whether to wait for connected nodes
67
58
 
68
59
  Returns:
69
60
  The finalized message
70
61
  """
71
- # For chain processing, update the response's chain if input was forwarded
72
- if original_message:
73
- message = message.forwarded(original_message)
74
- node.message_sent.emit(message) # Emit signals
62
+ await node.message_sent.emit(message) # Emit signals
75
63
  await node.log_message(message) # Log message if enabled
76
64
  # Route to connections
77
65
  await connections.route_message(message, wait=wait_for_connections)
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from agentpool.models.acp_agents import ACPAgentConfig, ACPAgentConfigTypes, BaseACPAgentConfig
6
- from agentpool.models.agents import NativeAgentConfig
6
+ from agentpool.models.agents import AnyToolConfig, NativeAgentConfig # noqa: F401
7
7
  from agentpool.models.agui_agents import AGUIAgentConfig
8
8
  from agentpool.models.claude_code_agents import ClaudeCodeAgentConfig
9
9
  from agentpool.models.manifest import AgentsManifest, AnyAgentConfig
@@ -7,10 +7,10 @@ import os
7
7
  import tempfile
8
8
  from typing import TYPE_CHECKING, Annotated, Any, Literal
9
9
 
10
- from exxec import ExecutionEnvironmentStr, get_environment # noqa: TC002
11
- from exxec.configs import (
10
+ from exxec_config import (
12
11
  E2bExecutionEnvironmentConfig,
13
12
  ExecutionEnvironmentConfig, # noqa: TC002
13
+ ExecutionEnvironmentStr, # noqa: TC002
14
14
  )
15
15
  from pydantic import ConfigDict, Field
16
16
 
@@ -223,6 +223,8 @@ class BaseACPAgentConfig(NodeConfig):
223
223
 
224
224
  def get_execution_environment(self) -> ExecutionEnvironment:
225
225
  """Create execution environment from config."""
226
+ from exxec import get_environment
227
+
226
228
  if isinstance(self.execution_environment, str):
227
229
  return get_environment(self.execution_environment)
228
230
  return self.execution_environment.get_provider()
@@ -232,6 +234,8 @@ class BaseACPAgentConfig(NodeConfig):
232
234
 
233
235
  Returns None if not configured (caller should fall back to main env).
234
236
  """
237
+ from exxec import get_environment
238
+
235
239
  if self.client_execution_environment is None:
236
240
  return None
237
241
  if isinstance(self.client_execution_environment, str):
@@ -9,12 +9,14 @@ import anyenv
9
9
  from pydantic import BaseModel, ConfigDict, Field
10
10
 
11
11
  from agentpool.models.acp_agents.base import BaseACPAgentConfig
12
+ from agentpool_config import AnyToolConfig, BaseToolConfig # noqa: TC001
12
13
  from agentpool_config.output_types import StructuredResponseConfig # noqa: TC001
13
- from agentpool_config.toolsets import ToolsetConfig # noqa: TC001
14
+ from agentpool_config.toolsets import BaseToolsetConfig
14
15
 
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  from agentpool.prompts.manager import PromptManager
19
+ from agentpool.resource_providers import ResourceProvider
18
20
 
19
21
 
20
22
  ClaudeCodeModelName = Literal["default", "sonnet", "opus", "haiku", "sonnet[1m]", "opusplan"]
@@ -23,6 +25,7 @@ ClaudeCodeToolName = Literal[
23
25
  "Bash",
24
26
  "BashOutput",
25
27
  "Edit",
28
+ "EnterPlanMode",
26
29
  "ExitPlanMode",
27
30
  "Glob",
28
31
  "Grep",
@@ -47,23 +50,51 @@ class MCPCapableACPAgentConfig(BaseACPAgentConfig):
47
50
  that can be exposed via an internal MCP bridge.
48
51
  """
49
52
 
50
- toolsets: list[ToolsetConfig] = Field(
53
+ tools: list[AnyToolConfig | str] = Field(
51
54
  default_factory=list,
52
- title="Toolsets",
55
+ title="Tools",
53
56
  examples=[
54
57
  [
55
58
  {"type": "subagent"},
56
59
  {"type": "agent_management"},
60
+ "webbrowser:open",
57
61
  ],
58
62
  ],
59
63
  )
60
- """Toolsets to expose to this ACP agent via MCP bridge.
64
+ """Tools and toolsets to expose to this ACP agent via MCP bridge.
61
65
 
62
- These toolsets will be started as an in-process MCP server and made
63
- available to the external ACP agent. This allows ACP agents to use
64
- internal agentpool toolsets like subagent delegation.
66
+ Supports both single tools and toolsets. These will be started as an
67
+ in-process MCP server and made available to the external ACP agent.
65
68
  """
66
69
 
70
+ def get_tool_providers(self) -> list[ResourceProvider]:
71
+ """Get all resource providers for this agent's tools.
72
+
73
+ Returns:
74
+ List of ResourceProvider instances
75
+ """
76
+ from agentpool.resource_providers import StaticResourceProvider
77
+ from agentpool.tools.base import Tool
78
+
79
+ providers: list[ResourceProvider] = []
80
+ static_tools: list[Tool] = []
81
+
82
+ for tool_config in self.tools:
83
+ try:
84
+ if isinstance(tool_config, BaseToolsetConfig):
85
+ providers.append(tool_config.get_provider())
86
+ elif isinstance(tool_config, str):
87
+ static_tools.append(Tool.from_callable(tool_config))
88
+ elif isinstance(tool_config, BaseToolConfig):
89
+ static_tools.append(tool_config.get_tool())
90
+ except Exception: # noqa: BLE001
91
+ continue
92
+
93
+ if static_tools:
94
+ providers.append(StaticResourceProvider(name="tools", tools=static_tools))
95
+
96
+ return providers
97
+
67
98
  def build_mcp_config_json(self) -> str | None:
68
99
  """Convert inherited mcp_servers to standard MCP config JSON format.
69
100
 
@@ -197,12 +228,12 @@ class ClaudeACPAgentConfig(MCPCapableACPAgentConfig):
197
228
  )
198
229
  """Additional directories to allow tool access to."""
199
230
 
200
- tools: list[ClaudeCodeToolName | str] | None = Field(
231
+ builtin_tools: list[ClaudeCodeToolName | str] | None = Field(
201
232
  default=None,
202
- title="Tools",
233
+ title="Built-in Tools",
203
234
  examples=[["Bash", "Edit", "Read"], []],
204
235
  )
205
- """Available tools from built-in set. Empty list disables all tools."""
236
+ """Available tools from Claude's built-in set. Empty list disables all tools."""
206
237
 
207
238
  fallback_model: ClaudeCodeModelName | None = Field(
208
239
  default=None,
@@ -260,9 +291,9 @@ class ClaudeACPAgentConfig(MCPCapableACPAgentConfig):
260
291
  args.append("--strict-mcp-config")
261
292
  if self.add_dir:
262
293
  args.extend(["--add-dir", *self.add_dir])
263
- if self.tools is not None:
264
- if self.tools:
265
- args.extend(["--tools", ",".join(self.tools)])
294
+ if self.builtin_tools is not None:
295
+ if self.builtin_tools:
296
+ args.extend(["--tools", ",".join(self.builtin_tools)])
266
297
  else:
267
298
  args.extend(["--tools", ""])
268
299
  if self.fallback_model:
@@ -307,7 +338,7 @@ class FastAgentACPAgentConfig(MCPCapableACPAgentConfig):
307
338
  provider: fast-agent
308
339
  cwd: /path/to/project
309
340
  model: claude-3.5-sonnet-20241022
310
- toolsets:
341
+ tools:
311
342
  - type: subagent
312
343
  - type: agent_management
313
344
  skills_dir: ./my-skills
@@ -644,7 +675,85 @@ class KimiACPAgentConfig(MCPCapableACPAgentConfig):
644
675
  return args
645
676
 
646
677
 
678
+ class AgentpoolACPAgentConfig(MCPCapableACPAgentConfig):
679
+ """Configuration for agentpool's own ACP server.
680
+
681
+ This allows using agentpool serve-acp as an ACP agent, with MCP bridge support
682
+ for tool metadata preservation.
683
+
684
+ Example:
685
+ ```yaml
686
+ acp_agents:
687
+ my_agentpool:
688
+ type: agentpool
689
+ config_path: path/to/agent_config.yml
690
+ agent: agent_name # Optional: specific agent to use
691
+ mcp_servers:
692
+ - type: stdio
693
+ command: mcp-server-filesystem
694
+ args: ["--root", "/workspace"]
695
+ ```
696
+ """
697
+
698
+ model_config = ConfigDict(title="Agentpool ACP Agent")
699
+
700
+ provider: Literal["agentpool"] = Field("agentpool", init=False)
701
+ """Discriminator for agentpool ACP agent."""
702
+
703
+ config_path: str | None = None
704
+ """Path to agentpool configuration file (optional)."""
705
+
706
+ agent: str | None = None
707
+ """Specific agent name to use from config (defaults to first agent)."""
708
+
709
+ file_access: bool = True
710
+ """Enable file system access for the agent."""
711
+
712
+ terminal_access: bool = True
713
+ """Enable terminal access for the agent."""
714
+
715
+ load_skills: bool = True
716
+ """Load client-side skills from .claude/skills directory."""
717
+
718
+ def get_command(self) -> str:
719
+ """Get the command to run agentpool serve-acp."""
720
+ return "agentpool"
721
+
722
+ async def get_args(self, prompt_manager: PromptManager | None = None) -> list[str]:
723
+ """Build command arguments for agentpool serve-acp."""
724
+ args = ["serve-acp"]
725
+
726
+ # Add config path if specified
727
+ if self.config_path:
728
+ args.append(self.config_path)
729
+
730
+ # Add agent selection
731
+ if self.agent:
732
+ args.extend(["--agent", self.agent])
733
+
734
+ # Add file/terminal access flags
735
+ if not self.file_access:
736
+ args.append("--no-file-access")
737
+ if not self.terminal_access:
738
+ args.append("--no-terminal-access")
739
+
740
+ # Add skills flag
741
+ if not self.load_skills:
742
+ args.append("--no-skills")
743
+
744
+ # Convert inherited mcp_servers to --mcp-config format
745
+ mcp_json = self.build_mcp_config_json()
746
+ if mcp_json:
747
+ args.extend(["--mcp-config", mcp_json])
748
+
749
+ return args
750
+
751
+
647
752
  # Union of all ACP agent config types
648
753
  MCPCapableACPAgentConfigTypes = (
649
- ClaudeACPAgentConfig | FastAgentACPAgentConfig | AuggieACPAgentConfig | KimiACPAgentConfig
754
+ ClaudeACPAgentConfig
755
+ | FastAgentACPAgentConfig
756
+ | AuggieACPAgentConfig
757
+ | KimiACPAgentConfig
758
+ | AgentpoolACPAgentConfig
650
759
  )