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
@@ -5,16 +5,6 @@ from __future__ import annotations
5
5
  import base64
6
6
  from typing import TYPE_CHECKING, Any, assert_never
7
7
 
8
- from mcp.types import (
9
- AudioContent,
10
- BlobResourceContents,
11
- EmbeddedResource,
12
- ImageContent,
13
- PromptMessage,
14
- ResourceLink,
15
- TextContent,
16
- TextResourceContents,
17
- )
18
8
  from pydantic_ai import (
19
9
  AudioUrl,
20
10
  BinaryContent,
@@ -35,9 +25,14 @@ if TYPE_CHECKING:
35
25
  from collections.abc import Sequence
36
26
 
37
27
  from fastmcp import Client
38
- from mcp.types import ContentBlock
39
- from pydantic_ai import ModelRequestPart, ModelResponsePart
40
-
28
+ from mcp.types import (
29
+ BlobResourceContents,
30
+ ContentBlock,
31
+ PromptMessage,
32
+ SamplingMessage,
33
+ TextResourceContents,
34
+ )
35
+ from pydantic_ai import ModelRequestPart, ModelResponsePart, UserContent
41
36
 
42
37
  logger = get_logger(__name__)
43
38
 
@@ -46,6 +41,13 @@ def to_mcp_messages(
46
41
  part: ModelRequestPart | ModelResponsePart,
47
42
  ) -> list[PromptMessage]:
48
43
  """Convert internal PromptMessage to MCP PromptMessage."""
44
+ from mcp.types import (
45
+ AudioContent,
46
+ ImageContent,
47
+ PromptMessage,
48
+ TextContent,
49
+ )
50
+
49
51
  messages = []
50
52
  match part:
51
53
  case UserPromptPart(content=str() as c):
@@ -94,6 +96,24 @@ def _url_from_mime_type(uri: str, mime_type: str | None) -> FileUrl:
94
96
  return DocumentUrl(url=uri)
95
97
 
96
98
 
99
+ def sampling_messages_to_user_content(msgs: list[SamplingMessage]) -> list[UserContent]:
100
+ from mcp import types
101
+
102
+ # Convert messages to prompts for the agent
103
+ prompts: list[UserContent] = []
104
+ for mcp_msg in msgs:
105
+ match mcp_msg.content:
106
+ case types.TextContent(text=text):
107
+ prompts.append(text)
108
+ case types.ImageContent(data=data, mimeType=mime_type):
109
+ binary_data = base64.b64decode(data)
110
+ prompts.append(BinaryImage(data=binary_data, media_type=mime_type))
111
+ case types.AudioContent(data=data, mimeType=mime_type):
112
+ binary_data = base64.b64decode(data)
113
+ prompts.append(BinaryContent(data=binary_data, media_type=mime_type))
114
+ return prompts
115
+
116
+
97
117
  async def from_mcp_content(
98
118
  mcp_content: Sequence[ContentBlock | TextResourceContents | BlobResourceContents],
99
119
  client: Client[Any] | None = None,
@@ -103,6 +123,16 @@ async def from_mcp_content(
103
123
  If a FastMCP client is given, this function will try to resolve the ResourceLinks.
104
124
 
105
125
  """
126
+ from mcp.types import (
127
+ AudioContent,
128
+ BlobResourceContents,
129
+ EmbeddedResource,
130
+ ImageContent,
131
+ ResourceLink,
132
+ TextContent,
133
+ TextResourceContents,
134
+ )
135
+
106
136
  contents: list[Any] = []
107
137
 
108
138
  for block in mcp_content:
@@ -151,6 +181,17 @@ async def from_mcp_content(
151
181
 
152
182
 
153
183
  def content_block_as_text(content: ContentBlock) -> str:
184
+
185
+ from mcp.types import (
186
+ AudioContent,
187
+ BlobResourceContents,
188
+ EmbeddedResource,
189
+ ImageContent,
190
+ ResourceLink,
191
+ TextContent,
192
+ TextResourceContents,
193
+ )
194
+
154
195
  match content:
155
196
  case TextContent(text=text):
156
197
  return text
@@ -3,12 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import base64
7
6
  from contextlib import AsyncExitStack
8
7
  from typing import TYPE_CHECKING, Any, Self, cast
9
8
 
10
9
  import anyio
11
- from pydantic_ai import BinaryContent, BinaryImage, UsageLimits
12
10
 
13
11
  from agentpool.log import get_logger
14
12
  from agentpool.resource_providers import AggregatingResourceProvider, ResourceProvider
@@ -23,7 +21,6 @@ if TYPE_CHECKING:
23
21
  from mcp import types
24
22
  from mcp.shared.context import RequestContext
25
23
  from mcp.types import SamplingMessage
26
- from pydantic_ai import UserContent
27
24
 
28
25
  from agentpool_config.mcp_server import MCPServerConfig
29
26
 
@@ -38,6 +35,7 @@ class MCPManager:
38
35
  self,
39
36
  name: str = "mcp",
40
37
  owner: str | None = None,
38
+ sampling_model: str = "openai:gpt-5-nano",
41
39
  servers: Sequence[MCPServerConfig | str] | None = None,
42
40
  accessible_roots: list[str] | None = None,
43
41
  ) -> None:
@@ -47,6 +45,7 @@ class MCPManager:
47
45
  for server in servers or []:
48
46
  self.add_server_config(server)
49
47
  self.providers: list[MCPResourceProvider] = []
48
+ self.sampling_model = sampling_model
50
49
  self.aggregating_provider = AggregatingResourceProvider(
51
50
  providers=cast(list[ResourceProvider], self.providers),
52
51
  name=f"{name}_aggregated",
@@ -88,35 +87,22 @@ class MCPManager:
88
87
  context: RequestContext[Any, Any, Any],
89
88
  ) -> str:
90
89
  """Handle MCP sampling by creating a new agent with specified preferences."""
91
- from mcp import types
92
-
93
90
  from agentpool.agents import Agent
91
+ from agentpool.mcp_server.conversions import sampling_messages_to_user_content
94
92
 
95
- # Convert messages to prompts for the agent
96
- prompts: list[UserContent] = []
97
- for mcp_msg in messages:
98
- match mcp_msg.content:
99
- case types.TextContent(text=text):
100
- prompts.append(text)
101
- case types.ImageContent(data=data, mimeType=mime_type):
102
- binary_data = base64.b64decode(data)
103
- prompts.append(BinaryImage(data=binary_data, media_type=mime_type))
104
- case types.AudioContent(data=data, mimeType=mime_type):
105
- binary_data = base64.b64decode(data)
106
- prompts.append(BinaryContent(data=binary_data, media_type=mime_type))
107
-
108
- # Extract model from preferences
109
- model = None
93
+ prompts = sampling_messages_to_user_content(messages)
94
+ model = self.sampling_model
110
95
  if (prefs := params.modelPreferences) and prefs.hints and prefs.hints[0].name:
111
- model = prefs.hints[0].name
96
+ model = prefs.hints[0].name # Extract model from preferences
112
97
  # Create usage limits from sampling parameters
113
- limits = UsageLimits(output_tokens_limit=params.maxTokens, request_limit=1)
98
+ # limits = UsageLimits(output_tokens_limit=params.maxTokens, request_limit=1)
99
+ # TODO: Re-add per-turn usage_limits once implemented for all agents
114
100
  # TODO: Apply temperature from params.temperature
115
101
  sys_prompt = params.systemPrompt or ""
116
102
  agent = Agent(name="sampling-agent", model=model, system_prompt=sys_prompt, session=False)
117
103
  try:
118
104
  async with agent:
119
- result = await agent.run(*prompts, store_history=False, usage_limits=limits)
105
+ result = await agent.run(*prompts, store_history=False)
120
106
  return result.content
121
107
 
122
108
  except Exception as e:
@@ -51,6 +51,9 @@ class RegistryTransport(Schema):
51
51
  url: str | None = None
52
52
  """URL for HTTP transports."""
53
53
 
54
+ headers: list[dict[str, Any]] = Field(default_factory=list)
55
+ """Request headers."""
56
+
54
57
 
55
58
  class RegistryPackage(Schema):
56
59
  """Package information for installing an MCP server."""
@@ -75,6 +78,9 @@ class RegistryPackage(Schema):
75
78
  package_arguments: list[dict[str, Any]] = Field(default_factory=list, alias="packageArguments")
76
79
  """Package arguments."""
77
80
 
81
+ runtime_arguments: list[dict[str, Any]] = Field(default_factory=list, alias="runtimeArguments")
82
+ """Runtime arguments."""
83
+
78
84
  runtime_hint: str | None = Field(None, alias="runtimeHint")
79
85
  """Runtime hint."""
80
86
 
@@ -97,6 +103,9 @@ class RegistryRemote(Schema):
97
103
  headers: list[dict[str, Any]] = Field(default_factory=list)
98
104
  """Request headers."""
99
105
 
106
+ variables: dict[str, Any] = Field(default_factory=dict)
107
+ """URL template variables."""
108
+
100
109
 
101
110
  class RegistryIcon(Schema):
102
111
  """Icon configuration for a server."""
@@ -123,7 +132,7 @@ class RegistryServer(Schema):
123
132
  version: str
124
133
  """Server version."""
125
134
 
126
- repository: RegistryRepository
135
+ repository: RegistryRepository | None = None
127
136
  """Repository information."""
128
137
 
129
138
  packages: list[RegistryPackage] = Field(default_factory=list)
@@ -15,12 +15,10 @@ import asyncio
15
15
  from contextlib import asynccontextmanager, suppress
16
16
  from dataclasses import dataclass, field, replace
17
17
  import inspect
18
- from typing import TYPE_CHECKING, Any, Literal, Self, get_args, get_origin
18
+ from typing import TYPE_CHECKING, Any, Self, get_args, get_origin
19
19
  from uuid import uuid4
20
20
 
21
21
  import anyio
22
- from fastmcp import FastMCP
23
- from fastmcp.tools import Tool as FastMCPTool
24
22
  from pydantic import BaseModel, HttpUrl
25
23
 
26
24
  from agentpool.agents import Agent
@@ -33,12 +31,12 @@ if TYPE_CHECKING:
33
31
  from collections.abc import AsyncIterator, Callable
34
32
 
35
33
  from claude_agent_sdk.types import McpServerConfig
36
- from fastmcp import Context
34
+ from fastmcp import Context, FastMCP
37
35
  from fastmcp.tools.tool import ToolResult
38
36
  from pydantic_ai import RunContext
39
37
  from uvicorn import Server
40
38
 
41
- from acp.schema.mcp import HttpMcpServer, SseMcpServer
39
+ from acp.schema.mcp import HttpMcpServer
42
40
  from agentpool.agents import AgentContext
43
41
  from agentpool.agents.base_agent import BaseAgent
44
42
  from agentpool.tools.base import Tool
@@ -154,25 +152,35 @@ def _convert_to_tool_result(result: Any) -> ToolResult:
154
152
  """Convert a tool's return value to a FastMCP ToolResult.
155
153
 
156
154
  Handles different result types appropriately:
157
- - ToolResult: Pass through unchanged
155
+ - FastMCP ToolResult: Pass through unchanged
156
+ - AgentPool ToolResult: Convert to FastMCP format
158
157
  - dict: Use as structured_content (enables programmatic access by clients)
159
158
  - Pydantic models: Serialize to dict for structured_content
160
159
  - Other types: Pass to ToolResult(content=...) which handles conversion internally
161
160
  """
162
- from fastmcp.tools.tool import ToolResult
161
+ from fastmcp.tools.tool import ToolResult as FastMCPToolResult
162
+
163
+ from agentpool.tools.base import ToolResult as AgentPoolToolResult
163
164
 
164
- # Already a ToolResult - pass through
165
- if isinstance(result, ToolResult):
165
+ # Already a FastMCP ToolResult - pass through
166
+ if isinstance(result, FastMCPToolResult):
166
167
  return result
168
+ # AgentPool ToolResult - convert to FastMCP format
169
+ if isinstance(result, AgentPoolToolResult):
170
+ return FastMCPToolResult(
171
+ content=result.content,
172
+ structured_content=result.structured_content,
173
+ meta=result.metadata,
174
+ )
167
175
  # Dict - use as structured_content (FastMCP auto-populates content as JSON)
168
176
  if isinstance(result, dict):
169
- return ToolResult(structured_content=result)
177
+ return FastMCPToolResult(structured_content=result)
170
178
  # Pydantic model - serialize to dict for structured_content
171
179
  if isinstance(result, BaseModel):
172
- return ToolResult(structured_content=result.model_dump(mode="json"))
180
+ return FastMCPToolResult(structured_content=result.model_dump(mode="json"))
173
181
  # All other types (str, list, ContentBlock, Image, None, primitives, etc.)
174
182
  # ToolResult's internal _convert_to_content handles these correctly
175
- return ToolResult(content=result if result is not None else "")
183
+ return FastMCPToolResult(content=result if result is not None else "")
176
184
 
177
185
 
178
186
  def _extract_tool_call_id(context: Context | None) -> str:
@@ -210,9 +218,6 @@ class BridgeConfig:
210
218
  port: int = 0
211
219
  """Port to bind to (0 = auto-select available port)."""
212
220
 
213
- transport: Literal["sse", "streamable-http"] = "sse"
214
- """Transport protocol: 'sse' or 'streamable-http'."""
215
-
216
221
  server_name: str = "agentpool-toolmanager"
217
222
  """Name for the MCP server."""
218
223
 
@@ -271,6 +276,8 @@ class ToolManagerBridge:
271
276
 
272
277
  async def start(self) -> None:
273
278
  """Start the HTTP MCP server in the background."""
279
+ from fastmcp import FastMCP
280
+
274
281
  self._mcp = FastMCP(name=self.config.server_name)
275
282
  await self._register_tools()
276
283
  self._subscribe_to_tool_changes()
@@ -333,7 +340,22 @@ class ToolManagerBridge:
333
340
  return
334
341
 
335
342
  # Get current and new tool sets
336
- current_names = set(self._mcp._tool_manager._tools.keys())
343
+ # Support both old and new FastMCP API
344
+ if hasattr(self._mcp, "_tool_manager"):
345
+ # Old API (<=2.12.4): direct access to _tool_manager
346
+ current_names = set(self._mcp._tool_manager._tools.keys())
347
+ elif hasattr(self._mcp, "_local_provider"):
348
+ # New API (git): tools stored in _local_provider._components
349
+ # Keys are prefixed with 'tool:', e.g., 'tool:bash'
350
+ current_names = {
351
+ key.removeprefix("tool:")
352
+ for key in self._mcp._local_provider._components
353
+ if key.startswith("tool:")
354
+ }
355
+ else:
356
+ # Fallback: use async get_tools() method
357
+ current_tools = await self._mcp.get_tools()
358
+ current_names = {t.name for t in current_tools} # type: ignore[attr-defined]
337
359
  new_tools = await self.node.tools.get_tools(state="enabled")
338
360
  new_names = {t.name for t in new_tools}
339
361
 
@@ -361,19 +383,16 @@ class ToolManagerBridge:
361
383
  @property
362
384
  def url(self) -> str:
363
385
  """Get the server URL."""
364
- path = "/sse" if self.config.transport == "sse" else "/mcp"
365
- return f"http://{self.config.host}:{self.port}{path}"
386
+ return f"http://{self.config.host}:{self.port}/mcp"
366
387
 
367
- def get_mcp_server_config(self) -> HttpMcpServer | SseMcpServer:
388
+ def get_mcp_server_config(self) -> HttpMcpServer:
368
389
  """Get ACP-compatible MCP server configuration.
369
390
 
370
391
  Returns config suitable for passing to ACP agent's NewSessionRequest.
371
392
  """
372
- from acp.schema import HttpMcpServer, SseMcpServer
393
+ from acp.schema import HttpMcpServer
373
394
 
374
395
  url = HttpUrl(self.url)
375
- if self.config.transport == "sse":
376
- return SseMcpServer(name=self.config.server_name, url=url, headers=[])
377
396
  return HttpMcpServer(name=self.config.server_name, url=url, headers=[])
378
397
 
379
398
  def get_claude_mcp_server_config(self) -> dict[str, McpServerConfig]:
@@ -416,6 +435,75 @@ class ToolManagerBridge:
416
435
  """Register a single tool with the FastMCP server."""
417
436
  if not self._mcp:
418
437
  return
438
+ from fastmcp.tools import Tool as FastMCPTool
439
+
440
+ class _BridgeTool(FastMCPTool):
441
+ """Custom FastMCP Tool that wraps a agentpool Tool.
442
+
443
+ This allows us to use our own schema and invoke tools with AgentContext.
444
+ """
445
+
446
+ def __init__(self, tool: Tool, bridge: ToolManagerBridge) -> None:
447
+ # Get input schema from our tool
448
+ schema = tool.schema["function"]
449
+ input_schema = schema.get("parameters", {"type": "object", "properties": {}})
450
+ # Filter out context parameters - they're auto-injected by the bridge
451
+ context_params = _get_context_param_names(tool.get_callable())
452
+ run_context_params = _get_run_context_param_names(tool.get_callable())
453
+ all_context_params = context_params | run_context_params
454
+ filtered_schema = filter_schema_params(input_schema, all_context_params)
455
+ desc = tool.description or "No description"
456
+ super().__init__(name=tool.name, description=desc, parameters=filtered_schema)
457
+ # Set these AFTER super().__init__() to avoid being overwritten
458
+ self._tool = tool
459
+ self._bridge = bridge
460
+
461
+ async def run(self, arguments: dict[str, Any]) -> ToolResult:
462
+ """Execute the wrapped tool with context bridging."""
463
+ from fastmcp.server.dependencies import get_context
464
+
465
+ # Get FastMCP context from context variable (not passed as parameter)
466
+ try:
467
+ mcp_context: Context | None = get_context()
468
+ except LookupError:
469
+ mcp_context = None
470
+
471
+ # Try to get Claude's original tool_call_id from request metadata
472
+ tool_call_id = _extract_tool_call_id(mcp_context)
473
+ # Get deps from bridge (set by run_stream on the agent)
474
+ current_deps = self._bridge.current_deps
475
+ # Create context with tool-specific metadata from node's context.
476
+ ctx = replace(
477
+ self._bridge.node.get_context(data=current_deps),
478
+ tool_name=self._tool.name,
479
+ tool_call_id=tool_call_id,
480
+ tool_input=arguments,
481
+ )
482
+ # Invoke with context - copy arguments since invoke_tool_with_context
483
+ # modifies kwargs in-place to inject context parameters
484
+ result = await self._bridge.invoke_tool_with_context(
485
+ self._tool, ctx, arguments.copy()
486
+ )
487
+
488
+ # Emit metadata event for ClaudeCodeAgent to correlate
489
+ # (works around Claude SDK stripping MCP _meta field)
490
+ from agentpool.agents.events import ToolResultMetadataEvent
491
+ from agentpool.tools.base import ToolResult as AgentPoolToolResult
492
+
493
+ if isinstance(result, AgentPoolToolResult) and result.metadata:
494
+ logger.info(
495
+ "Emitting ToolResultMetadataEvent",
496
+ tool_call_id=tool_call_id,
497
+ metadata_keys=list(result.metadata.keys()),
498
+ )
499
+ event = ToolResultMetadataEvent(
500
+ tool_call_id=tool_call_id,
501
+ metadata=result.metadata,
502
+ )
503
+ await ctx.events.emit_event(event)
504
+
505
+ return _convert_to_tool_result(result)
506
+
419
507
  # Create a custom FastMCP Tool that wraps our tool
420
508
  bridge_tool = _BridgeTool(tool=tool, bridge=self)
421
509
  self._mcp.add_tool(bridge_tool)
@@ -430,7 +518,7 @@ class ToolManagerBridge:
430
518
 
431
519
  Handles tools that expect AgentContext, RunContext, or neither.
432
520
  """
433
- fn = tool.callable
521
+ fn = tool.get_callable()
434
522
 
435
523
  # Inject AgentContext parameters
436
524
  context_param_names = _get_context_param_names(fn)
@@ -470,7 +558,7 @@ class ToolManagerBridge:
470
558
  port = s.getsockname()[1]
471
559
  self._actual_port = port
472
560
  # Create the ASGI app
473
- app = self._mcp.http_app(transport=self.config.transport)
561
+ app = self._mcp.http_app(transport="http")
474
562
  # Configure uvicorn
475
563
  cfg = uvicorn.Config(
476
564
  app=app, host=self.config.host, port=port, log_level="warning", ws="websockets-sansio"
@@ -481,58 +569,7 @@ class ToolManagerBridge:
481
569
  self._server_task = asyncio.create_task(self._server.serve(), name=name)
482
570
  await anyio.sleep(0.1) # Wait briefly for server to start
483
571
  msg = "ToolManagerBridge started"
484
- logger.info(msg, url=self.url, transport=self.config.transport)
485
-
486
-
487
- class _BridgeTool(FastMCPTool):
488
- """Custom FastMCP Tool that wraps a agentpool Tool.
489
-
490
- This allows us to use our own schema and invoke tools with AgentContext.
491
- """
492
-
493
- def __init__(self, tool: Tool, bridge: ToolManagerBridge) -> None:
494
- # Get input schema from our tool
495
- schema = tool.schema["function"]
496
- input_schema = schema.get("parameters", {"type": "object", "properties": {}})
497
- # Filter out context parameters - they're auto-injected by the bridge
498
- context_params = _get_context_param_names(tool.callable)
499
- run_context_params = _get_run_context_param_names(tool.callable)
500
- all_context_params = context_params | run_context_params
501
- filtered_schema = filter_schema_params(input_schema, all_context_params)
502
- desc = tool.description or "No description"
503
- super().__init__(name=tool.name, description=desc, parameters=filtered_schema)
504
- # Set these AFTER super().__init__() to avoid being overwritten
505
- self._tool = tool
506
- self._bridge = bridge
507
-
508
- async def run(self, arguments: dict[str, Any]) -> ToolResult:
509
- """Execute the wrapped tool with context bridging."""
510
- from fastmcp.server.dependencies import get_context
511
-
512
- # Get FastMCP context from context variable (not passed as parameter)
513
- try:
514
- mcp_context: Context | None = get_context()
515
- except LookupError:
516
- mcp_context = None
517
-
518
- # Try to get Claude's original tool_call_id from request metadata
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
-
524
- # Create context with tool-specific metadata from node's context.
525
- ctx = replace(
526
- self._bridge.node.get_context(data=current_deps),
527
- tool_name=self._tool.name,
528
- tool_call_id=tool_call_id,
529
- tool_input=arguments,
530
- )
531
-
532
- # Invoke with context - copy arguments since invoke_tool_with_context
533
- # modifies kwargs in-place to inject context parameters
534
- result = await self._bridge.invoke_tool_with_context(self._tool, ctx, arguments.copy())
535
- return _convert_to_tool_result(result)
572
+ logger.info(msg, url=self.url)
536
573
 
537
574
 
538
575
  @asynccontextmanager
@@ -541,7 +578,6 @@ async def create_tool_bridge(
541
578
  *,
542
579
  host: str = "127.0.0.1",
543
580
  port: int = 0,
544
- transport: Literal["sse", "streamable-http"] = "sse",
545
581
  ) -> AsyncIterator[ToolManagerBridge]:
546
582
  """Create and start a ToolManagerBridge as a context manager.
547
583
 
@@ -549,12 +585,11 @@ async def create_tool_bridge(
549
585
  node: The node whose tools to expose
550
586
  host: Host to bind to
551
587
  port: Port to bind to (0 = auto-select)
552
- transport: Transport protocol ('sse' or 'streamable-http')
553
588
 
554
589
  Yields:
555
590
  Running ToolManagerBridge instance
556
591
  """
557
- config = BridgeConfig(host=host, port=port, transport=transport)
592
+ config = BridgeConfig(host=host, port=port)
558
593
  bridge = ToolManagerBridge(node=node, config=config)
559
594
  async with bridge:
560
595
  yield bridge
@@ -6,7 +6,8 @@ from collections.abc import Sequence
6
6
  from contextlib import asynccontextmanager
7
7
  from typing import TYPE_CHECKING, Any, Self
8
8
 
9
- from psygnal import Signal
9
+ from anyenv.signals import Signal
10
+ from psygnal import Signal as Psygnal
10
11
  from psygnal.containers import EventedList
11
12
 
12
13
  from agentpool.log import get_logger
@@ -27,10 +28,10 @@ logger = get_logger(__name__)
27
28
  class ConnectionManager:
28
29
  """Manages connections for both Agents and Teams."""
29
30
 
30
- connection_processed = Signal(Talk.ConnectionProcessed)
31
+ connection_processed = Signal[Talk.ConnectionProcessed]()
31
32
 
32
- node_connected = Signal(object) # Node
33
- connection_added = Signal(Talk) # Agent
33
+ node_connected = Psygnal(object) # Node
34
+ connection_added = Psygnal(Talk) # Agent
34
35
 
35
36
  def __init__(self, owner: MessageNode[Any, Any]) -> None:
36
37
  self.owner = owner
@@ -54,9 +55,9 @@ class ConnectionManager:
54
55
  old.connection_processed.disconnect(self._handle_message_flow)
55
56
  new.connection_processed.connect(self._handle_message_flow)
56
57
 
57
- def _handle_message_flow(self, event: Talk.ConnectionProcessed) -> None:
58
+ async def _handle_message_flow(self, event: Talk.ConnectionProcessed) -> None:
58
59
  """Forward message flow to our aggregated signal."""
59
- self.connection_processed.emit(event)
60
+ await self.connection_processed.emit(event)
60
61
 
61
62
  def set_wait_state(self, target: MessageNode[Any, Any] | AgentName, wait: bool = True) -> None:
62
63
  """Set waiting behavior for target."""
@@ -308,10 +309,10 @@ class ConnectionManager:
308
309
  if __name__ == "__main__":
309
310
  from agentpool.agents import Agent
310
311
 
311
- agent = Agent("test_agent")
312
- agent_2 = Agent("test_agent_2")
313
- agent_3 = Agent("test_agent_3")
314
- agent_4 = Agent("test_agent_4")
312
+ agent = Agent("test_agent", model="openai:gpt-5-nano")
313
+ agent_2 = Agent("test_agent_2", model="openai:gpt-5-nano")
314
+ agent_3 = Agent("test_agent_3", model="openai:gpt-5-nano")
315
+ agent_4 = Agent("test_agent_4", model="openai:gpt-5-nano")
315
316
  _conn_1 = agent >> agent_2
316
317
  _conn_2 = agent >> agent_3
317
318
  _conn_3 = agent_2 >> agent_4
@@ -10,9 +10,9 @@ from functools import wraps
10
10
  import inspect
11
11
  from typing import TYPE_CHECKING, Any, Self
12
12
 
13
- from evented.configs import EmailConfig, FileWatchConfig, TimeEventConfig, WebhookConfig
13
+ from anyenv.signals import Signal
14
14
  from evented.event_data import EventData, FunctionResultEventData
15
- from psygnal import Signal
15
+ from evented_config import EmailConfig, FileWatchConfig, TimeEventConfig, WebhookConfig
16
16
  from pydantic import SecretStr
17
17
 
18
18
  from agentpool.log import get_logger
@@ -27,8 +27,8 @@ if TYPE_CHECKING:
27
27
  from types import TracebackType
28
28
 
29
29
  from evented.base import EventSource
30
- from evented.configs import EventConfig
31
30
  from evented.timed_watcher import TimeEventSource
31
+ from evented_config import EventConfig
32
32
 
33
33
 
34
34
  logger = get_logger(__name__)
@@ -40,7 +40,7 @@ type EventCallback = Callable[[EventData], None | Awaitable[None]]
40
40
  class EventManager:
41
41
  """Manages multiple event sources and their lifecycles."""
42
42
 
43
- event_processed = Signal(EventData)
43
+ event_processed = Signal[EventData]()
44
44
 
45
45
  def __init__(
46
46
  self,
@@ -83,7 +83,7 @@ class EventManager:
83
83
  except Exception:
84
84
  logger.exception("Error in event callback", name=get_fn_name(callback))
85
85
 
86
- self.event_processed.emit(event)
86
+ await self.event_processed.emit(event)
87
87
 
88
88
  async def add_file_watch(
89
89
  self,
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import itertools
6
5
  from typing import TYPE_CHECKING, Any, Literal
7
6
 
8
7
  from psygnal.containers import EventedList
@@ -145,36 +144,13 @@ class ChatMessageList(EventedList[ChatMessage[Any]]):
145
144
  message: Message to build flow DAG for
146
145
 
147
146
  Returns:
148
- Root DAGNode of the graph
147
+ Root DAGNode of the graph, or None
148
+
149
+ Note:
150
+ forwarded_from has been removed. This method now returns None.
151
+ Flow tracking can be reconstructed from parent_id chain or pool history.
149
152
  """
150
- from agentpool.utils.dag import DAGNode
151
-
152
- # Get messages from this conversation
153
- conv_messages = [m for m in self if m.conversation_id == message.conversation_id]
154
- nodes: dict[str, DAGNode] = {}
155
- for msg in conv_messages: # First create all nodes
156
- if msg.forwarded_from:
157
- chain = [*msg.forwarded_from, msg.name or "unknown"]
158
- for name in chain:
159
- if name not in nodes:
160
- nodes[name] = DAGNode(name)
161
-
162
- # Then set up parent relationships
163
- for msg in conv_messages:
164
- if msg.forwarded_from:
165
- chain = [*msg.forwarded_from, msg.name or "unknown"]
166
- # Connect consecutive nodes
167
- for parent_name, child_name in itertools.pairwise(chain):
168
- parent = nodes[parent_name]
169
- child = nodes[child_name]
170
- if parent not in child.parents:
171
- child.add_parent(parent)
172
-
173
- # Find root nodes (those without parents)
174
- roots = [node for node in nodes.values() if node.is_root]
175
- if not roots:
176
- return None
177
- return roots[0] # Return first root for now
153
+ return None
178
154
 
179
155
  def to_mermaid_graph(
180
156
  self,