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,11 +6,15 @@ from dataclasses import dataclass
6
6
  from typing import TYPE_CHECKING, Any, Literal
7
7
 
8
8
  from agentpool.agents.context import AgentContext # noqa: TC001
9
+ from agentpool.agents.events import TextContentItem
9
10
  from agentpool.resource_providers import ResourceProvider
11
+ from agentpool.tools.base import ToolResult
10
12
  from agentpool.utils.streams import TodoPriority, TodoStatus # noqa: TC001
11
13
 
12
14
 
13
15
  if TYPE_CHECKING:
16
+ from collections.abc import Sequence
17
+
14
18
  from agentpool.tools.base import Tool
15
19
  from agentpool.utils.streams import TodoTracker
16
20
 
@@ -18,7 +22,7 @@ if TYPE_CHECKING:
18
22
  # Keep PlanEntry for backward compatibility with event emitting
19
23
  PlanEntryPriority = Literal["high", "medium", "low"]
20
24
  PlanEntryStatus = Literal["pending", "in_progress", "completed"]
21
- PlanToolMode = Literal["granular", "declarative", "hybrid"]
25
+ PlanToolMode = Literal["granular", "declarative"]
22
26
 
23
27
 
24
28
  @dataclass(kw_only=True)
@@ -59,12 +63,12 @@ class PlanProvider(ResourceProvider):
59
63
 
60
64
  kind = "tools"
61
65
 
62
- def __init__(self, mode: PlanToolMode = "granular") -> None:
66
+ def __init__(self, mode: PlanToolMode = "declarative") -> None:
63
67
  """Initialize plan provider.
64
68
 
65
69
  Args:
66
70
  mode: Tool mode - 'granular' for separate tools, 'declarative' for
67
- single set_plan tool, 'hybrid' for both approaches.
71
+ single set_plan tool.
68
72
  """
69
73
  super().__init__(name="plan")
70
74
  self.mode = mode
@@ -75,21 +79,13 @@ class PlanProvider(ResourceProvider):
75
79
  return agent_ctx.pool.todos
76
80
  return None
77
81
 
78
- async def get_tools(self) -> list[Tool]:
82
+ async def get_tools(self) -> Sequence[Tool]:
79
83
  """Get plan management tools based on mode."""
80
84
  tools: list[Tool] = [self.create_tool(self.get_plan, category="read")]
81
85
 
82
86
  if self.mode == "declarative":
83
87
  # Single bulk tool for capable models
84
88
  tools.append(self.create_tool(self.set_plan, category="other"))
85
- elif self.mode == "hybrid":
86
- # Both approaches - model chooses
87
- tools.extend([
88
- self.create_tool(self.set_plan, category="other"),
89
- self.create_tool(self.add_plan_entry, category="other"),
90
- self.create_tool(self.update_plan_entry, category="edit"),
91
- self.create_tool(self.remove_plan_entry, category="delete"),
92
- ])
93
89
  else:
94
90
  # granular mode (default) - separate tools for simpler models
95
91
  tools.extend([
@@ -100,7 +96,7 @@ class PlanProvider(ResourceProvider):
100
96
 
101
97
  return tools
102
98
 
103
- async def get_plan(self, agent_ctx: AgentContext) -> str:
99
+ async def get_plan(self, agent_ctx: AgentContext) -> ToolResult:
104
100
  """Get the current plan formatted as markdown.
105
101
 
106
102
  Args:
@@ -111,7 +107,15 @@ class PlanProvider(ResourceProvider):
111
107
  """
112
108
  tracker = self._get_tracker(agent_ctx)
113
109
  if tracker is None or not tracker.entries:
114
- return "## Plan\n\n*No plan entries yet.*"
110
+ # Emit progress for empty plan
111
+ await agent_ctx.events.tool_call_progress(
112
+ title="Fetched plan (empty)",
113
+ items=[TextContentItem(text="*No tasks in plan yet.*")],
114
+ )
115
+ return ToolResult(
116
+ content="## Plan\n\n*No plan entries yet.",
117
+ metadata={"todos": []},
118
+ )
115
119
 
116
120
  lines = ["## Plan", ""]
117
121
  status_icons = {
@@ -129,13 +133,35 @@ class PlanProvider(ResourceProvider):
129
133
  priority = priority_labels.get(entry.priority, "")
130
134
  lines.append(f"{i}. {icon} {priority} {entry.content} *({entry.status})*")
131
135
 
132
- return "\n".join(lines)
136
+ # Count completed entries for summary
137
+ completed = sum(1 for e in tracker.entries if e.status == "completed")
138
+ total = len(tracker.entries)
139
+
140
+ # Build title with summary
141
+ title = "Fetched plan with 1 task" if total == 1 else f"Fetched plan with {total} tasks"
142
+ if completed > 0:
143
+ title += f" ({completed} completed)"
144
+
145
+ # Emit progress with plan preview
146
+ plan_text = "\n".join(lines)
147
+ await agent_ctx.events.tool_call_progress(
148
+ title=title,
149
+ items=[TextContentItem(text=plan_text)],
150
+ )
151
+
152
+ # Convert to OpenCode format for metadata
153
+ todos = [{"content": e.content, "status": e.status} for e in tracker.entries]
154
+
155
+ return ToolResult(
156
+ content=plan_text,
157
+ metadata={"todos": todos},
158
+ )
133
159
 
134
160
  async def set_plan(
135
161
  self,
136
162
  agent_ctx: AgentContext,
137
163
  entries: list[dict[str, Any]],
138
- ) -> str:
164
+ ) -> ToolResult:
139
165
  """Replace the entire plan with new entries (declarative/bulk update).
140
166
 
141
167
  This is more efficient than multiple add/update calls when setting
@@ -154,7 +180,10 @@ class PlanProvider(ResourceProvider):
154
180
  """
155
181
  tracker = self._get_tracker(agent_ctx)
156
182
  if tracker is None:
157
- return "Error: No pool available for plan tracking"
183
+ return ToolResult(
184
+ content="Error: No pool available for plan tracking",
185
+ metadata={"todos": []},
186
+ )
158
187
 
159
188
  # Clear existing entries
160
189
  tracker.clear()
@@ -170,7 +199,40 @@ class PlanProvider(ResourceProvider):
170
199
 
171
200
  await self._emit_plan_update(agent_ctx)
172
201
 
173
- return f"Plan updated with {len(tracker.entries)} entries"
202
+ # Build summary for user feedback
203
+ entry_count = len(tracker.entries)
204
+ if entry_count == 0:
205
+ title = "Cleared plan"
206
+ elif entry_count == 1:
207
+ title = "Set plan with 1 task"
208
+ else:
209
+ title = f"Set plan with {entry_count} tasks"
210
+
211
+ # Format entries list for details
212
+ if tracker.entries:
213
+ lines = ["**New Plan:**"]
214
+ for i, e in enumerate(tracker.entries):
215
+ priority_emoji = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(e.priority, "")
216
+ status_emoji = {"pending": "⬚", "in_progress": "◐", "completed": "✓"}.get(
217
+ e.status, ""
218
+ )
219
+ lines.append(f"{i + 1}. {priority_emoji} {status_emoji} {e.content}")
220
+ details = "\n".join(lines)
221
+ else:
222
+ details = "*Plan is empty*"
223
+
224
+ await agent_ctx.events.tool_call_progress(
225
+ title=title,
226
+ items=[TextContentItem(text=details)],
227
+ )
228
+
229
+ # Convert to OpenCode format for metadata
230
+ todos = [{"content": e.content, "status": e.status} for e in tracker.entries]
231
+
232
+ return ToolResult(
233
+ content=f"Plan updated with {entry_count} entries",
234
+ metadata={"todos": todos},
235
+ )
174
236
 
175
237
  async def add_plan_entry(
176
238
  self,
@@ -199,6 +261,15 @@ class PlanProvider(ResourceProvider):
199
261
 
200
262
  await self._emit_plan_update(agent_ctx)
201
263
 
264
+ # User feedback
265
+ priority_emoji = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(priority, "")
266
+ title = f"Added task {entry_index + 1} {priority_emoji}"
267
+ details = f"**Task {entry_index + 1}**: {content}"
268
+ await agent_ctx.events.tool_call_progress(
269
+ title=title,
270
+ items=[TextContentItem(text=details)],
271
+ )
272
+
202
273
  return f"Added plan entry at index {entry_index}: {content!r} (priority={priority!r})"
203
274
 
204
275
  async def update_plan_entry(
@@ -242,6 +313,19 @@ class PlanProvider(ResourceProvider):
242
313
  tracker.update_by_index(index, content=content, status=status, priority=priority)
243
314
 
244
315
  await self._emit_plan_update(agent_ctx)
316
+
317
+ # Build title with key info
318
+ entry = tracker.entries[index]
319
+ status_emoji = {"pending": "⬚", "in_progress": "◐", "completed": "✓"}.get(entry.status, "")
320
+ title = f"Updated task {index + 1} {status_emoji}"
321
+
322
+ # Send detailed content
323
+ details = f"**Task {index + 1}**: {entry.content}\n\nChanges: {', '.join(updates)}"
324
+ await agent_ctx.events.tool_call_progress(
325
+ title=title,
326
+ items=[TextContentItem(text=details)],
327
+ )
328
+
245
329
  return f"Updated entry {index}: {', '.join(updates)}"
246
330
 
247
331
  async def remove_plan_entry(self, agent_ctx: AgentContext, index: int) -> str:
@@ -267,6 +351,15 @@ class PlanProvider(ResourceProvider):
267
351
  if removed_entry is None:
268
352
  return f"Error: Could not remove entry at index {index}"
269
353
 
354
+ # User feedback
355
+ remaining = len(tracker.entries)
356
+ title = f"Removed task {index + 1}"
357
+ details = f"**Removed**: {removed_entry.content}\n\nRemaining tasks: {remaining}"
358
+ await agent_ctx.events.tool_call_progress(
359
+ title=title,
360
+ items=[TextContentItem(text=details)],
361
+ )
362
+
270
363
  if tracker.entries:
271
364
  return f"Removed entry {index}: {removed_entry.content!r}, remaining entries reindexed"
272
365
  return f"Removed entry {index}: {removed_entry.content!r}, plan is now empty"
@@ -9,10 +9,12 @@ from agentpool.resource_providers import ResourceProvider
9
9
 
10
10
 
11
11
  if TYPE_CHECKING:
12
+ from collections.abc import Sequence
13
+
12
14
  from agentpool import AgentPool
13
15
  from agentpool.prompts.prompts import BasePrompt
14
- from agentpool.tools.base import Tool
15
- from agentpool_config.resources import ResourceInfo
16
+ from agentpool.resource_providers.resource_info import ResourceInfo
17
+ from agentpool.tools import Tool
16
18
 
17
19
  logger = get_logger(__name__)
18
20
 
@@ -43,7 +45,7 @@ class PoolResourceProvider(ResourceProvider):
43
45
  self.zed_mode = zed_mode
44
46
  self.include_team_members = include_team_members
45
47
 
46
- async def get_tools(self) -> list[Tool]:
48
+ async def get_tools(self) -> Sequence[Tool]:
47
49
  """Get tools from all agents in pool."""
48
50
  team_tools = [team.to_tool() for team in self.pool.teams.values()]
49
51
  agents = list(self.pool.agents.values())
@@ -0,0 +1,111 @@
1
+ """Resource information model with read capability."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from dataclasses import dataclass, field
7
+ from typing import TYPE_CHECKING, Any, Self
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from mcp.types import Resource as MCPResource
12
+
13
+
14
+ # Type alias for the reader function
15
+ ResourceReader = Callable[[str], Awaitable[list[str]]]
16
+
17
+
18
+ @dataclass
19
+ class ResourceInfo:
20
+ """Information about an available resource with read capability.
21
+
22
+ This class provides essential information about a resource and can read
23
+ its content when a reader is available.
24
+
25
+ Example:
26
+ ```python
27
+ # List resources from tool manager
28
+ resources = await agent.tools.list_resources()
29
+
30
+ # Read a specific resource
31
+ for resource in resources:
32
+ if resource.name == "config.json":
33
+ content = await resource.read()
34
+ print(content)
35
+ ```
36
+ """
37
+
38
+ name: str
39
+ """Name of the resource"""
40
+
41
+ uri: str
42
+ """URI identifying the resource location"""
43
+
44
+ description: str | None = None
45
+ """Optional description of the resource's content or purpose"""
46
+
47
+ mime_type: str | None = None
48
+ """MIME type of the resource content"""
49
+
50
+ client: str | None = None
51
+ """Name of the MCP client/server providing this resource"""
52
+
53
+ annotations: dict[str, Any] = field(default_factory=dict)
54
+ """Additional annotations/metadata for the resource"""
55
+
56
+ _reader: ResourceReader | None = field(default=None, repr=False, compare=False)
57
+ """Internal reader function for fetching content"""
58
+
59
+ async def read(self) -> list[str]:
60
+ """Read the resource content.
61
+
62
+ Returns:
63
+ List of text contents from the resource
64
+
65
+ Raises:
66
+ RuntimeError: If no reader is available or read fails
67
+ """
68
+ if self._reader is None:
69
+ msg = f"No reader available for resource: {self.uri}"
70
+ raise RuntimeError(msg)
71
+ return await self._reader(self.uri)
72
+
73
+ @property
74
+ def can_read(self) -> bool:
75
+ """Check if this resource can be read."""
76
+ return self._reader is not None
77
+
78
+ @classmethod
79
+ async def from_mcp_resource(
80
+ cls,
81
+ resource: MCPResource,
82
+ client_name: str | None = None,
83
+ reader: ResourceReader | None = None,
84
+ ) -> Self:
85
+ """Create ResourceInfo from MCP resource.
86
+
87
+ Args:
88
+ resource: MCP resource object
89
+ client_name: Name of the MCP client providing this resource
90
+ reader: Optional reader function for fetching content
91
+
92
+ Returns:
93
+ ResourceInfo instance
94
+ """
95
+ annotations: dict[str, Any] = {}
96
+ if resource.annotations:
97
+ # Convert annotations to simple dict
98
+ if hasattr(resource.annotations, "model_dump"):
99
+ annotations = resource.annotations.model_dump(exclude_none=True)
100
+ elif isinstance(resource.annotations, dict):
101
+ annotations = resource.annotations
102
+
103
+ return cls(
104
+ name=resource.name,
105
+ uri=str(resource.uri),
106
+ description=resource.description,
107
+ mime_type=resource.mimeType,
108
+ client=client_name,
109
+ annotations=annotations,
110
+ _reader=reader,
111
+ )
@@ -14,8 +14,8 @@ if TYPE_CHECKING:
14
14
  from agentpool import Agent, MessageNode
15
15
  from agentpool.common_types import ToolSource, ToolType
16
16
  from agentpool.prompts.prompts import BasePrompt
17
+ from agentpool.resource_providers.resource_info import ResourceInfo
17
18
  from agentpool.tools.base import Tool
18
- from agentpool_config.resources import ResourceInfo
19
19
 
20
20
 
21
21
  class StaticResourceProvider(ResourceProvider):
@@ -48,7 +48,7 @@ class StaticResourceProvider(ResourceProvider):
48
48
  self._prompts = list(prompts) if prompts else []
49
49
  self._resources = list(resources) if resources else []
50
50
 
51
- async def get_tools(self) -> list[Tool]:
51
+ async def get_tools(self) -> Sequence[Tool]:
52
52
  """Get pre-configured tools."""
53
53
  return self._tools
54
54
 
@@ -4,11 +4,13 @@ from agentpool.sessions.models import ProjectData, SessionData
4
4
  from agentpool.sessions.store import SessionStore
5
5
  from agentpool.sessions.manager import SessionManager
6
6
  from agentpool.sessions.session import ClientSession
7
+ from agentpool.sessions.protocol import SessionInfo
7
8
 
8
9
  __all__ = [
9
10
  "ClientSession",
10
11
  "ProjectData",
11
12
  "SessionData",
13
+ "SessionInfo",
12
14
  "SessionManager",
13
15
  "SessionStore",
14
16
  ]
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  from typing import TYPE_CHECKING, Any, Self
7
- from uuid import uuid4
8
7
 
9
8
  from agentpool.log import get_logger
10
9
  from agentpool.sessions.models import SessionData
@@ -171,7 +170,7 @@ class SessionManager:
171
170
  data = SessionData(
172
171
  session_id=session_id,
173
172
  agent_name=agent_name,
174
- conversation_id=conversation_id or f"conv_{uuid4().hex[:12]}",
173
+ conversation_id=conversation_id or session_id,
175
174
  pool_id=self._pool_id,
176
175
  project_id=project_id,
177
176
  cwd=cwd,
@@ -309,7 +308,7 @@ class SessionManager:
309
308
  if active_only:
310
309
  sessions = list(self._active.keys())
311
310
  if agent_name:
312
- sessions = [sid for sid, s in self._active.items() if s.agent_name == agent_name]
311
+ sessions = [sid for sid, s in self._active.items() if s.agent.name == agent_name]
313
312
  return sessions
314
313
 
315
314
  return await self._store.list_sessions(
@@ -68,9 +68,6 @@ class SessionData(Schema):
68
68
  conversation_id: str
69
69
  """Links to conversation in StorageManager."""
70
70
 
71
- title: str | None = None
72
- """AI-generated or user-provided title for the conversation."""
73
-
74
71
  pool_id: str | None = None
75
72
  """Optional pool/manifest identifier for multi-pool setups."""
76
73
 
@@ -116,6 +113,12 @@ class SessionData(Schema):
116
113
  new_metadata = {**self.metadata, **kwargs}
117
114
  return self.model_copy(update={"metadata": new_metadata, "last_active": get_now()})
118
115
 
119
- def with_title(self, title: str) -> SessionData:
120
- """Return copy with updated title."""
121
- return self.model_copy(update={"title": title, "last_active": get_now()})
116
+ @property
117
+ def title(self) -> str | None:
118
+ """Human-readable title (from metadata, for protocol compatibility)."""
119
+ return self.metadata.get("title")
120
+
121
+ @property
122
+ def updated_at(self) -> str | None:
123
+ """ISO timestamp of last activity (for protocol compatibility)."""
124
+ return self.last_active.isoformat() if self.last_active else None
@@ -0,0 +1,28 @@
1
+ """Session protocol for unified session management across agent types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol, runtime_checkable
6
+
7
+
8
+ @runtime_checkable
9
+ class SessionInfo(Protocol):
10
+ """Protocol for session information.
11
+
12
+ This protocol provides a unified interface for session metadata across
13
+ different agent implementations (ACP, ClaudeCode, native agents).
14
+
15
+ Both ACP's SessionInfo and our SessionData can fulfill this protocol.
16
+ """
17
+
18
+ session_id: str
19
+ """Unique identifier for the session."""
20
+
21
+ cwd: str | None
22
+ """Working directory for the session (absolute path)."""
23
+
24
+ title: str | None
25
+ """Human-readable title for the session."""
26
+
27
+ updated_at: str | None
28
+ """ISO 8601 timestamp of last activity."""
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import asyncio
6
5
  from typing import TYPE_CHECKING, Any, Self
7
6
 
8
7
  from agentpool.log import get_logger
@@ -57,16 +56,9 @@ class ClientSession:
57
56
  self._manager = manager
58
57
  self._agent: Agent[Any, Any] | None = None
59
58
  self._closed = False
60
- self._title_generation_triggered = False
61
- self._title_task: asyncio.Task[None] | None = None
62
59
  # Session owns conversation history - agent is stateless
63
60
  self._history = MessageHistory()
64
-
65
- logger.debug(
66
- "Created client session",
67
- session_id=data.session_id,
68
- agent=data.agent_name,
69
- )
61
+ logger.debug("Created client session", session_id=data.session_id, agent=data.agent_name)
70
62
 
71
63
  @property
72
64
  def session_id(self) -> str:
@@ -90,16 +82,16 @@ class ClientSession:
90
82
  self._agent = self._pool.get_agent(self._data.agent_name)
91
83
  return self._agent
92
84
 
93
- @property
94
- def agent_name(self) -> str:
95
- """Get current agent name."""
96
- return self._data.agent_name
97
-
98
85
  @property
99
86
  def conversation_id(self) -> str:
100
87
  """Get conversation ID for message storage."""
101
88
  return self._data.conversation_id
102
89
 
90
+ @property
91
+ def title(self) -> str | None:
92
+ """Get conversation title (delegated to agent)."""
93
+ return self._agent.conversation_title if self._agent is not None else None
94
+
103
95
  @property
104
96
  def history(self) -> MessageHistory:
105
97
  """Get the session's conversation history."""
@@ -130,6 +122,9 @@ class ClientSession:
130
122
  the agent stateless from the session's perspective. Messages
131
123
  are automatically added to the session's history.
132
124
 
125
+ Title generation is handled automatically by the agent's log_conversation
126
+ call when the conversation is first created.
127
+
133
128
  Args:
134
129
  prompt: User prompt to send to the agent
135
130
  **kwargs: Additional arguments passed to agent.run()
@@ -137,42 +132,13 @@ class ClientSession:
137
132
  Returns:
138
133
  The agent's response message
139
134
  """
140
- result = await self.agent.run(
135
+ return await self.agent.run(
141
136
  prompt,
142
137
  message_history=self._history,
143
138
  conversation_id=self.conversation_id,
144
139
  **kwargs,
145
140
  )
146
141
 
147
- # Trigger title generation after first exchange (fire-and-forget)
148
- # Note: Message logging is handled by the agent itself via MessageNode.log_message
149
- if not self._title_generation_triggered and self._pool.storage:
150
- self._title_generation_triggered = True
151
- self._title_task = asyncio.create_task(self._generate_title())
152
-
153
- return result
154
-
155
- async def _generate_title(self) -> None:
156
- """Generate conversation title in the background."""
157
- if not self._pool.storage:
158
- return
159
- try:
160
- messages = self._history.get_history()
161
- if messages:
162
- title = await self._pool.storage.generate_conversation_title(
163
- self.conversation_id,
164
- messages,
165
- )
166
- # Also update SessionData so title is available when listing sessions
167
- if title and self._manager:
168
- self._data = self._data.with_title(title)
169
- await self._manager.save(self._data)
170
- except Exception:
171
- logger.exception(
172
- "Failed to generate conversation title",
173
- conversation_id=self.conversation_id,
174
- )
175
-
176
142
  async def switch_agent(self, agent_name: str) -> None:
177
143
  """Switch to a different agent.
178
144
 
@@ -190,16 +156,10 @@ class ClientSession:
190
156
 
191
157
  self._agent = self._pool.get_agent(agent_name)
192
158
  self._data = self._data.with_agent(agent_name)
193
-
194
159
  # Persist the change
195
160
  if self._manager:
196
161
  await self._manager.save(self._data)
197
-
198
- logger.info(
199
- "Switched agent",
200
- session_id=self.session_id,
201
- agent=agent_name,
202
- )
162
+ logger.info("Switched agent", session_id=self.session_id, agent=agent_name)
203
163
 
204
164
  async def touch(self) -> None:
205
165
  """Update last_active timestamp and persist."""
@@ -230,10 +190,6 @@ class ClientSession:
230
190
  """
231
191
  self._data = self._data.with_metadata(**kwargs)
232
192
 
233
- def clear_history(self) -> None:
234
- """Clear the session's conversation history."""
235
- self._history.clear()
236
-
237
193
  def get_history_messages(self) -> list[ChatMessage[Any]]:
238
194
  """Get all messages in the session's history."""
239
195
  return self._history.get_history()