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
@@ -8,11 +8,13 @@ from agentpool.resource_providers.base import ResourceChangeEvent, ResourceProvi
8
8
 
9
9
 
10
10
  if TYPE_CHECKING:
11
+ from collections.abc import Sequence
12
+
11
13
  from pydantic_ai import ModelRequestPart
12
14
 
13
15
  from agentpool.prompts.prompts import BasePrompt
16
+ from agentpool.resource_providers.resource_info import ResourceInfo
14
17
  from agentpool.tools.base import Tool
15
- from agentpool_config.resources import ResourceInfo
16
18
 
17
19
  _ = ResourceChangeEvent # Used at runtime in method signatures
18
20
 
@@ -78,7 +80,7 @@ class AggregatingResourceProvider(ResourceProvider):
78
80
  """Forward skills_changed signal from child provider."""
79
81
  await self.skills_changed.emit(event)
80
82
 
81
- async def get_tools(self) -> list[Tool]:
83
+ async def get_tools(self) -> Sequence[Tool]:
82
84
  """Get tools from all providers."""
83
85
  return [t for provider in self.providers for t in await provider.get_tools()]
84
86
 
@@ -13,16 +13,17 @@ from agentpool_config.tools import ToolHints
13
13
 
14
14
 
15
15
  if TYPE_CHECKING:
16
- from collections.abc import Callable
16
+ from collections.abc import Callable, Sequence
17
17
  from types import TracebackType
18
18
 
19
+ from fsspec.asyn import AsyncFileSystem
19
20
  from pydantic_ai import ModelRequestPart
20
21
  from schemez import OpenAIFunctionDefinition
21
22
 
22
23
  from agentpool.prompts.prompts import BasePrompt
24
+ from agentpool.resource_providers.resource_info import ResourceInfo
23
25
  from agentpool.skills.skill import Skill
24
26
  from agentpool.tools.base import ToolKind
25
- from agentpool_config.resources import ResourceInfo
26
27
 
27
28
 
28
29
  logger = get_logger(__name__)
@@ -109,7 +110,7 @@ class ResourceProvider:
109
110
  def __repr__(self) -> str:
110
111
  return f"{self.__class__.__name__}(name={self.name!r})"
111
112
 
112
- async def get_tools(self) -> list[Tool]:
113
+ async def get_tools(self) -> Sequence[Tool]:
113
114
  """Get available tools. Override to provide tools."""
114
115
  return []
115
116
 
@@ -134,6 +135,15 @@ class ResourceProvider:
134
135
  """Get available skills. Override to provide skills."""
135
136
  return []
136
137
 
138
+ def get_fs(self) -> AsyncFileSystem | None:
139
+ """Get filesystem view of provider state/history.
140
+
141
+ Returns:
142
+ AsyncFileSystem or None if not supported.
143
+ Override to expose provider state through filesystem interface.
144
+ """
145
+ return None
146
+
137
147
  async def get_skill_instructions(self, skill_name: str) -> str:
138
148
  """Get full instructions for a specific skill.
139
149
 
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import contextlib
7
7
  from dataclasses import dataclass
8
+ from datetime import UTC, datetime
9
+ import json
8
10
  from typing import TYPE_CHECKING, Any, Self
9
11
 
10
12
  import anyio
@@ -18,9 +20,10 @@ if TYPE_CHECKING:
18
20
  from types import TracebackType
19
21
 
20
22
  from exxec.base import ExecutionEnvironment
21
- from exxec.configs import ExecutionEnvironmentConfig
22
23
  from exxec.models import ServerInfo
24
+ from exxec_config import ExecutionEnvironmentConfig
23
25
  from fastapi import FastAPI
26
+ from fsspec.asyn import AsyncFileSystem
24
27
  from schemez import ToolsetCodeGenerator
25
28
  import uvicorn
26
29
 
@@ -46,6 +49,14 @@ class RemoteCodeExecutor:
46
49
  execution_env: ExecutionEnvironment
47
50
  """Execution environment for running code."""
48
51
 
52
+ def __post_init__(self) -> None:
53
+ """Initialize filesystem for script history after dataclass init."""
54
+ from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
55
+ from fsspec.implementations.memory import MemoryFileSystem
56
+
57
+ self._memory_fs = MemoryFileSystem()
58
+ self._fs = AsyncFileSystemWrapper(self._memory_fs)
59
+
49
60
  @classmethod
50
61
  def from_tools(
51
62
  cls,
@@ -78,16 +89,70 @@ class RemoteCodeExecutor:
78
89
  """Get comprehensive description of available tools."""
79
90
  return self.toolset_generator.generate_tool_description()
80
91
 
81
- async def execute_code(self, code: str) -> Any:
92
+ def get_fs(self) -> AsyncFileSystem:
93
+ """Get filesystem view of script history.
94
+
95
+ Returns:
96
+ AsyncFileSystem containing:
97
+ - scripts/{timestamp}_{title}.py - Executed scripts
98
+ - scripts/{timestamp}_{title}.json - Execution metadata
99
+ """
100
+ return self._fs
101
+
102
+ async def execute_code(self, code: str, title: str) -> Any:
82
103
  """Execute code with tools available via HTTP API.
83
104
 
84
105
  Args:
85
106
  code: Python code to execute
107
+ title: Short descriptive title for this script (3-4 words)
86
108
 
87
109
  Returns:
88
110
  Execution result from the environment
89
111
  """
90
- return await self.execution_env.execute(code)
112
+ start_time = datetime.now(UTC)
113
+ exit_code = 0
114
+ error_msg: str | None = None
115
+ exec_result = None
116
+ result_str = ""
117
+
118
+ try:
119
+ exec_result = await self.execution_env.execute(code)
120
+ result_str = str(exec_result)
121
+ # Check if execution failed
122
+ if (
123
+ hasattr(exec_result, "exit_code")
124
+ and exec_result.exit_code is not None
125
+ and exec_result.exit_code != 0
126
+ ):
127
+ exit_code = exec_result.exit_code
128
+ error_msg = getattr(exec_result, "error", None)
129
+ except Exception as e: # noqa: BLE001
130
+ exit_code = 1
131
+ error_msg = str(e)
132
+ result_str = f"Error executing code: {error_msg}"
133
+ finally:
134
+ # Save to filesystem
135
+ end_time = datetime.now(UTC)
136
+ duration = (end_time - start_time).total_seconds()
137
+ timestamp = start_time.strftime("%Y%m%d_%H%M%S")
138
+
139
+ # Write script file
140
+ script_path = f"scripts/{timestamp}_{title}.py"
141
+ self._memory_fs.pipe(script_path, code.encode("utf-8"))
142
+
143
+ # Write metadata file
144
+ metadata = {
145
+ "title": title,
146
+ "timestamp": start_time.isoformat(),
147
+ "exit_code": exit_code,
148
+ "duration": duration,
149
+ "result": result_str,
150
+ "error": error_msg,
151
+ }
152
+ metadata_path = f"scripts/{timestamp}_{title}.json"
153
+ self._memory_fs.pipe(metadata_path, json.dumps(metadata, indent=2).encode("utf-8"))
154
+
155
+ return exec_result
91
156
 
92
157
  async def __aenter__(self) -> Self:
93
158
  """Async context manager entry."""
@@ -196,7 +261,7 @@ class ToolServerLifecycleHandler:
196
261
 
197
262
 
198
263
  if __name__ == "__main__":
199
- from exxec.configs import LocalExecutionEnvironmentConfig
264
+ from exxec_config import LocalExecutionEnvironmentConfig
200
265
 
201
266
  from agentpool.tools.base import Tool
202
267
 
@@ -209,7 +274,9 @@ if __name__ == "__main__":
209
274
  config = LocalExecutionEnvironmentConfig()
210
275
  provider = RemoteCodeExecutor.from_tools(tools, config, server_port=9876)
211
276
  async with provider:
212
- result = await provider.execute_code("_result = await add_numbers(5, 3)")
277
+ result = await provider.execute_code(
278
+ "_result = await add_numbers(5, 3)", title="test_addition"
279
+ )
213
280
  print(f"Result: {result.result}")
214
281
 
215
282
  anyio.run(main)
@@ -69,13 +69,13 @@ def tools_to_codegen(
69
69
  generators = [
70
70
  ToolCodeGenerator(
71
71
  schema=create_schema(
72
- t.callable,
72
+ t.get_callable(),
73
73
  name_override=t.name,
74
74
  description_override=t.description,
75
75
  mode="openai",
76
76
  exclude_types=[AgentContext, RunContext],
77
77
  ),
78
- callable=t.callable,
78
+ callable=t.get_callable(),
79
79
  name_override=t.name,
80
80
  )
81
81
  for t in tools
@@ -2,8 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from datetime import UTC, datetime
5
6
  from functools import partial
6
7
  import inspect
8
+ import json
7
9
  from typing import TYPE_CHECKING, Any
8
10
 
9
11
  from agentpool.agents.context import AgentContext # noqa: TC001
@@ -17,6 +19,9 @@ from agentpool_toolsets.fsspec_toolset.toolset import FSSpecTools
17
19
 
18
20
 
19
21
  if TYPE_CHECKING:
22
+ from collections.abc import Sequence
23
+
24
+ from fsspec.asyn import AsyncFileSystem
20
25
  from schemez import ToolsetCodeGenerator
21
26
 
22
27
  from agentpool.resource_providers import ResourceProvider
@@ -46,7 +51,14 @@ class CodeModeResourceProvider(AggregatingResourceProvider):
46
51
  self._cached_tool: Tool | None = None
47
52
  self.usage_notes = usage_notes
48
53
 
49
- async def get_tools(self) -> list[Tool]:
54
+ # Filesystem for script history
55
+ from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
56
+ from fsspec.implementations.memory import MemoryFileSystem
57
+
58
+ self._memory_fs = MemoryFileSystem()
59
+ self._fs = AsyncFileSystemWrapper(self._memory_fs)
60
+
61
+ async def get_tools(self) -> Sequence[Tool]:
50
62
  """Return single meta-tool for Python execution with available tools."""
51
63
  # Always generate fresh toolset to reflect current tools
52
64
  toolset_generator = await self._get_code_generator()
@@ -55,9 +67,9 @@ class CodeModeResourceProvider(AggregatingResourceProvider):
55
67
 
56
68
  if self._cached_tool is None:
57
69
  # Create a closure that captures self but isn't a bound method
58
- async def execute_tool(ctx: AgentContext, python_code: str) -> Any:
70
+ async def execute_tool(ctx: AgentContext, python_code: str, title: str) -> Any:
59
71
  """These docstings are overriden by description_override."""
60
- return await self.execute(ctx, python_code)
72
+ return await self.execute(ctx, python_code, title)
61
73
 
62
74
  self._cached_tool = self.create_tool(execute_tool, description_override=desc)
63
75
  else:
@@ -66,11 +78,12 @@ class CodeModeResourceProvider(AggregatingResourceProvider):
66
78
 
67
79
  return [self._cached_tool]
68
80
 
69
- async def execute(self, ctx: AgentContext, python_code: str) -> Any: # noqa: D417
81
+ async def execute(self, ctx: AgentContext, python_code: str, title: str) -> Any: # noqa: D417
70
82
  """Execute Python code with all wrapped tools available as functions.
71
83
 
72
84
  Args:
73
85
  python_code: Python code to execute
86
+ title: Short descriptive title for this script (3-4 words)
74
87
 
75
88
  Returns:
76
89
  Result of the last expression or explicit return value
@@ -93,18 +106,47 @@ class CodeModeResourceProvider(AggregatingResourceProvider):
93
106
  # namespace["report_progress"] = NamespaceCallable(report_progress)
94
107
 
95
108
  validate_code(python_code)
109
+ start_time = datetime.now(UTC)
110
+ exit_code = 0
111
+ error_msg = None
112
+ result_value = None
113
+
96
114
  try:
97
115
  exec(python_code, namespace)
98
- result = await namespace["main"]()
116
+ result_value = await namespace["main"]()
99
117
  # Handle edge cases with coroutines and return values
100
- if inspect.iscoroutine(result):
101
- result = await result
102
- if not result: # in order to not confuse the model, return a success message.
103
- return "Code executed successfully"
118
+ if inspect.iscoroutine(result_value):
119
+ result_value = await result_value
120
+
121
+ if not result_value: # in order to not confuse the model, return a success message.
122
+ result_value = "Code executed successfully"
104
123
  except Exception as e: # noqa: BLE001
105
- return f"Error executing code: {e!s}"
106
- else:
107
- return result
124
+ exit_code = 1
125
+ error_msg = f"{e!s}"
126
+ result_value = f"Error executing code: {error_msg}"
127
+ finally:
128
+ # Save to filesystem
129
+ end_time = datetime.now(UTC)
130
+ duration = (end_time - start_time).total_seconds()
131
+ timestamp = start_time.strftime("%Y%m%d_%H%M%S")
132
+
133
+ # Write script file
134
+ script_path = f"scripts/{timestamp}_{title}.py"
135
+ self._memory_fs.pipe(script_path, python_code.encode("utf-8"))
136
+
137
+ # Write metadata file
138
+ metadata = {
139
+ "title": title,
140
+ "timestamp": start_time.isoformat(),
141
+ "exit_code": exit_code,
142
+ "duration": duration,
143
+ "result": str(result_value),
144
+ "error": error_msg,
145
+ }
146
+ metadata_path = f"scripts/{timestamp}_{title}.json"
147
+ self._memory_fs.pipe(metadata_path, json.dumps(metadata, indent=2).encode("utf-8"))
148
+
149
+ return result_value
108
150
 
109
151
  def invalidate_cache(self) -> None:
110
152
  """Invalidate cached tool when providers change."""
@@ -116,6 +158,16 @@ class CodeModeResourceProvider(AggregatingResourceProvider):
116
158
  tools = await super().get_tools()
117
159
  return tools_to_codegen(tools=tools, include_docstrings=self.include_docstrings)
118
160
 
161
+ def get_fs(self) -> AsyncFileSystem:
162
+ """Get filesystem view of script history.
163
+
164
+ Returns:
165
+ AsyncFileSystem containing:
166
+ - scripts/{timestamp}_{title}.py - Executed scripts
167
+ - scripts/{timestamp}_{title}.json - Execution metadata
168
+ """
169
+ return self._fs
170
+
119
171
 
120
172
  if __name__ == "__main__":
121
173
  import anyio
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
13
13
  from types import TracebackType
14
14
 
15
15
  from exxec.base import ExecutionEnvironment
16
- from exxec.configs import ExecutionEnvironmentConfig
16
+ from exxec_config import ExecutionEnvironmentConfig
17
17
  from schemez import ToolsetCodeGenerator
18
18
 
19
19
  from agentpool.tools.base import Tool
@@ -103,7 +103,7 @@ class RemoteMCPExecutor:
103
103
 
104
104
  if __name__ == "__main__":
105
105
  import anyio
106
- from exxec.configs import LocalExecutionEnvironmentConfig
106
+ from exxec_config import LocalExecutionEnvironmentConfig
107
107
 
108
108
  from agentpool.tools.base import Tool
109
109
 
@@ -6,7 +6,7 @@ import asyncio
6
6
  import contextlib
7
7
  from typing import TYPE_CHECKING, Any, Self
8
8
 
9
- from exxec.configs import LocalExecutionEnvironmentConfig
9
+ from exxec_config import LocalExecutionEnvironmentConfig
10
10
 
11
11
  from agentpool.agents.context import AgentContext # noqa: TC001
12
12
  from agentpool.log import get_logger
@@ -26,7 +26,7 @@ logger = get_logger(__name__)
26
26
  if TYPE_CHECKING:
27
27
  from types import TracebackType
28
28
 
29
- from exxec.configs import ExecutionEnvironmentConfig
29
+ from exxec_config import ExecutionEnvironmentConfig
30
30
 
31
31
  from agentpool.resource_providers import ResourceProvider
32
32
 
@@ -75,11 +75,12 @@ class RemoteCodeModeResourceProvider(CodeModeResourceProvider):
75
75
  self._code_executor: RemoteCodeExecutor | None = None
76
76
  self._provider_lock = asyncio.Lock()
77
77
 
78
- async def execute(self, ctx: AgentContext, python_code: str) -> Any: # noqa: D417
78
+ async def execute(self, ctx: AgentContext, python_code: str, title: str) -> Any: # noqa: D417
79
79
  """Execute Python code in secure environment with tools available via HTTP.
80
80
 
81
81
  Args:
82
82
  python_code: Python code to execute
83
+ title: Short descriptive title for this script (3-4 words)
83
84
 
84
85
  Returns:
85
86
  Result of the code execution
@@ -90,16 +91,12 @@ class RemoteCodeModeResourceProvider(CodeModeResourceProvider):
90
91
  full_code = f"{PROGRESS_HELPER}\n\n{python_code}"
91
92
  logger.info("Complete code", code=full_code)
92
93
  try:
93
- result = await code_provider.execution_env.execute(full_code)
94
- if result.success:
95
- logger.info("Code executed successfully")
96
- if result.result is None:
97
- return "Code executed successfully"
98
- return result.result
94
+ result = await code_provider.execute_code(full_code, title)
99
95
  except Exception as e: # noqa: BLE001
100
96
  return f"Error in secure execution: {e!s}"
101
97
  else:
102
- return f"Error executing code: {result.error}"
98
+ logger.info("Code executed")
99
+ return result
103
100
 
104
101
  async def _get_code_executor(self) -> RemoteCodeExecutor:
105
102
  """Get cached code execution provider with thread-safe initialization."""
@@ -139,7 +136,7 @@ if __name__ == "__main__":
139
136
  import webbrowser
140
137
 
141
138
  import anyio
142
- from exxec.configs import LocalExecutionEnvironmentConfig
139
+ from exxec_config import LocalExecutionEnvironmentConfig
143
140
 
144
141
  from agentpool import Agent, log
145
142
  from agentpool.resource_providers import StaticResourceProvider
@@ -153,7 +150,7 @@ if __name__ == "__main__":
153
150
  async def main() -> None:
154
151
  tools = [Tool.from_callable(open_browser)]
155
152
  static_provider = StaticResourceProvider(tools=tools)
156
- config = LocalExecutionEnvironmentConfig(timeout=30.0)
153
+ config = LocalExecutionEnvironmentConfig(default_command_timeout=30.0)
157
154
  provider = RemoteCodeModeResourceProvider(
158
155
  providers=[static_provider],
159
156
  execution_config=config,
@@ -8,6 +8,8 @@ from agentpool.resource_providers import ResourceProvider
8
8
 
9
9
 
10
10
  if TYPE_CHECKING:
11
+ from collections.abc import Sequence
12
+
11
13
  from agentpool.tools.base import Tool
12
14
 
13
15
 
@@ -32,7 +34,7 @@ class FilteringResourceProvider(ResourceProvider):
32
34
  """Delegate attribute access to wrapped provider."""
33
35
  return getattr(self._provider, name)
34
36
 
35
- async def get_tools(self) -> list[Tool]:
37
+ async def get_tools(self) -> Sequence[Tool]:
36
38
  """Get filtered tools from wrapped provider.
37
39
 
38
40
  Returns only tools where the filter value is True. Tools not in the filter
@@ -8,17 +8,19 @@ from typing import TYPE_CHECKING, Any, Self
8
8
 
9
9
  from agentpool.log import get_logger
10
10
  from agentpool.resource_providers import ResourceProvider
11
+ from agentpool.resource_providers.resource_info import ResourceInfo
11
12
  from agentpool_config.mcp_server import BaseMCPServerConfig
12
- from agentpool_config.resources import ResourceInfo
13
13
 
14
14
 
15
15
  if TYPE_CHECKING:
16
+ from collections.abc import Sequence
16
17
  from typing import Literal
17
18
 
18
19
  from fastmcp.client.sampling import SamplingHandler
20
+ from mcp.types import ResourceTemplate
19
21
 
20
22
  from agentpool.prompts.prompts import MCPClientPrompt
21
- from agentpool.tools.base import Tool
23
+ from agentpool.tools.base import FunctionTool, Tool
22
24
  from agentpool_config.mcp_server import MCPServerConfig
23
25
 
24
26
 
@@ -45,19 +47,14 @@ class MCPResourceProvider(ResourceProvider):
45
47
  self.server = BaseMCPServerConfig.from_string(server) if isinstance(server, str) else server
46
48
  self.source = source
47
49
  self.exit_stack = AsyncExitStack()
50
+
48
51
  self._accessible_roots = accessible_roots
49
52
  self._sampling_callback = sampling_callback
50
53
 
51
- # Tool caching
52
- self._tools_cache: list[Tool] | None = None
53
54
  self._saved_enabled_states: dict[str, bool] = {}
54
-
55
- # Prompt caching
55
+ self._tools_cache: list[FunctionTool] | None = None
56
56
  self._prompts_cache: list[MCPClientPrompt] | None = None
57
-
58
- # Resource caching
59
57
  self._resources_cache: list[ResourceInfo] | None = None
60
-
61
58
  self.client = MCPClient(
62
59
  config=self.server,
63
60
  sampling_callback=self._sampling_callback,
@@ -128,7 +125,7 @@ class MCPResourceProvider(ResourceProvider):
128
125
  try:
129
126
  # Get fresh tools from client
130
127
  mcp_tools = await self.client.list_tools()
131
- all_tools: list[Tool] = []
128
+ all_tools: list[FunctionTool] = []
132
129
 
133
130
  for tool in mcp_tools:
134
131
  try:
@@ -149,7 +146,7 @@ class MCPResourceProvider(ResourceProvider):
149
146
  logger.exception("Failed to refresh MCP tools cache")
150
147
  self._tools_cache = []
151
148
 
152
- async def get_tools(self) -> list[Tool]:
149
+ async def get_tools(self) -> Sequence[Tool]:
153
150
  """Get cached tools, refreshing if necessary."""
154
151
  if self._tools_cache is None:
155
152
  await self.refresh_tools_cache()
@@ -193,7 +190,11 @@ class MCPResourceProvider(ResourceProvider):
193
190
 
194
191
  for resource in result:
195
192
  try:
196
- converted = await ResourceInfo.from_mcp_resource(resource)
193
+ converted = await ResourceInfo.from_mcp_resource(
194
+ resource,
195
+ client_name=self.name,
196
+ reader=self.read_resource,
197
+ )
197
198
  all_resources.append(converted)
198
199
  except Exception:
199
200
  logger.exception("Failed to convert resource", name=resource.name)
@@ -212,6 +213,59 @@ class MCPResourceProvider(ResourceProvider):
212
213
 
213
214
  return self._resources_cache or []
214
215
 
216
+ async def read_resource(self, uri: str) -> list[str]:
217
+ """Read resource content by URI.
218
+
219
+ Args:
220
+ uri: URI of the resource to read
221
+
222
+ Returns:
223
+ List of text contents from the resource
224
+
225
+ Raises:
226
+ RuntimeError: If resource cannot be read
227
+ """
228
+ contents = await self.client.read_resource(uri)
229
+ result: list[str] = []
230
+ for content in contents:
231
+ if hasattr(content, "text") and content.text is not None:
232
+ result.append(str(content.text))
233
+ elif hasattr(content, "blob") and content.blob is not None:
234
+ # Binary content - return placeholder or base64
235
+ import base64
236
+
237
+ blob_data = content.blob
238
+ if isinstance(blob_data, str):
239
+ result.append(f"[Binary data: {len(blob_data)} bytes]")
240
+ elif isinstance(blob_data, bytes):
241
+ encoded = base64.b64encode(blob_data).decode("utf-8")
242
+ result.append(encoded)
243
+ else:
244
+ result.append("[Binary data: unknown format]")
245
+ return result
246
+
247
+ async def list_resource_templates(self) -> list[ResourceTemplate]:
248
+ """Get available resource templates from the MCP server.
249
+
250
+ Resource templates define URI patterns with placeholders that can be
251
+ expanded into concrete resource URIs. For example:
252
+ - Template: "file:///{path}" with path="config.json"
253
+ - Expands to: "file:///config.json"
254
+
255
+ TODO: Decide on integration strategy:
256
+ - Option 1: Templates as separate concept with expand() -> ResourceInfo
257
+ - Option 2: Unified ResourceInfo with is_template flag and read(**kwargs)
258
+ - Option 3: ResourceTemplateInfo class that produces ResourceInfo
259
+
260
+ Returns:
261
+ List of ResourceTemplate objects from the server
262
+ """
263
+ try:
264
+ return await self.client.list_resource_templates()
265
+ except Exception:
266
+ logger.exception("Failed to list resource templates")
267
+ return []
268
+
215
269
  def get_status(self) -> dict[str, str]:
216
270
  """Get connection status for this MCP server.
217
271