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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. acp/__init__.py +13 -0
  2. acp/bridge/README.md +15 -2
  3. acp/bridge/__init__.py +3 -2
  4. acp/bridge/__main__.py +60 -19
  5. acp/bridge/ws_server.py +173 -0
  6. acp/bridge/ws_server_cli.py +89 -0
  7. acp/notifications.py +2 -1
  8. acp/stdio.py +39 -9
  9. acp/transports.py +362 -2
  10. acp/utils.py +15 -2
  11. agentpool/__init__.py +4 -1
  12. agentpool/agents/__init__.py +2 -0
  13. agentpool/agents/acp_agent/acp_agent.py +203 -88
  14. agentpool/agents/acp_agent/acp_converters.py +46 -21
  15. agentpool/agents/acp_agent/client_handler.py +157 -3
  16. agentpool/agents/acp_agent/session_state.py +4 -1
  17. agentpool/agents/agent.py +314 -107
  18. agentpool/agents/agui_agent/__init__.py +0 -2
  19. agentpool/agents/agui_agent/agui_agent.py +90 -21
  20. agentpool/agents/agui_agent/agui_converters.py +0 -131
  21. agentpool/agents/base_agent.py +163 -1
  22. agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
  23. agentpool/agents/claude_code_agent/converters.py +71 -3
  24. agentpool/agents/claude_code_agent/history.py +474 -0
  25. agentpool/agents/context.py +40 -0
  26. agentpool/agents/events/__init__.py +2 -0
  27. agentpool/agents/events/builtin_handlers.py +2 -1
  28. agentpool/agents/events/event_emitter.py +29 -2
  29. agentpool/agents/events/events.py +20 -0
  30. agentpool/agents/modes.py +54 -0
  31. agentpool/agents/tool_call_accumulator.py +213 -0
  32. agentpool/common_types.py +21 -0
  33. agentpool/config_resources/__init__.py +38 -1
  34. agentpool/config_resources/claude_code_agent.yml +3 -0
  35. agentpool/delegation/pool.py +37 -29
  36. agentpool/delegation/team.py +1 -0
  37. agentpool/delegation/teamrun.py +1 -0
  38. agentpool/diagnostics/__init__.py +53 -0
  39. agentpool/diagnostics/lsp_manager.py +1593 -0
  40. agentpool/diagnostics/lsp_proxy.py +41 -0
  41. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  42. agentpool/diagnostics/models.py +398 -0
  43. agentpool/mcp_server/__init__.py +0 -2
  44. agentpool/mcp_server/client.py +12 -3
  45. agentpool/mcp_server/manager.py +25 -31
  46. agentpool/mcp_server/registries/official_registry_client.py +25 -0
  47. agentpool/mcp_server/tool_bridge.py +78 -66
  48. agentpool/messaging/__init__.py +0 -2
  49. agentpool/messaging/compaction.py +72 -197
  50. agentpool/messaging/message_history.py +12 -0
  51. agentpool/messaging/messages.py +52 -9
  52. agentpool/messaging/processing.py +3 -1
  53. agentpool/models/acp_agents/base.py +0 -22
  54. agentpool/models/acp_agents/mcp_capable.py +8 -148
  55. agentpool/models/acp_agents/non_mcp.py +129 -72
  56. agentpool/models/agents.py +35 -13
  57. agentpool/models/claude_code_agents.py +33 -2
  58. agentpool/models/manifest.py +43 -0
  59. agentpool/repomap.py +1 -1
  60. agentpool/resource_providers/__init__.py +9 -1
  61. agentpool/resource_providers/aggregating.py +52 -3
  62. agentpool/resource_providers/base.py +57 -1
  63. agentpool/resource_providers/mcp_provider.py +23 -0
  64. agentpool/resource_providers/plan_provider.py +130 -41
  65. agentpool/resource_providers/pool.py +2 -0
  66. agentpool/resource_providers/static.py +2 -0
  67. agentpool/sessions/__init__.py +2 -1
  68. agentpool/sessions/manager.py +31 -2
  69. agentpool/sessions/models.py +50 -0
  70. agentpool/skills/registry.py +13 -8
  71. agentpool/storage/manager.py +217 -1
  72. agentpool/testing.py +537 -19
  73. agentpool/utils/file_watcher.py +269 -0
  74. agentpool/utils/identifiers.py +121 -0
  75. agentpool/utils/pydantic_ai_helpers.py +46 -0
  76. agentpool/utils/streams.py +690 -1
  77. agentpool/utils/subprocess_utils.py +155 -0
  78. agentpool/utils/token_breakdown.py +461 -0
  79. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
  80. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
  81. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
  82. agentpool_cli/__main__.py +4 -0
  83. agentpool_cli/serve_acp.py +41 -20
  84. agentpool_cli/serve_agui.py +87 -0
  85. agentpool_cli/serve_opencode.py +119 -0
  86. agentpool_commands/__init__.py +30 -0
  87. agentpool_commands/agents.py +74 -1
  88. agentpool_commands/history.py +62 -0
  89. agentpool_commands/mcp.py +176 -0
  90. agentpool_commands/models.py +56 -3
  91. agentpool_commands/tools.py +57 -0
  92. agentpool_commands/utils.py +51 -0
  93. agentpool_config/builtin_tools.py +77 -22
  94. agentpool_config/commands.py +24 -1
  95. agentpool_config/compaction.py +258 -0
  96. agentpool_config/mcp_server.py +131 -1
  97. agentpool_config/storage.py +46 -1
  98. agentpool_config/tools.py +7 -1
  99. agentpool_config/toolsets.py +92 -148
  100. agentpool_server/acp_server/acp_agent.py +134 -150
  101. agentpool_server/acp_server/commands/acp_commands.py +216 -51
  102. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
  103. agentpool_server/acp_server/server.py +23 -79
  104. agentpool_server/acp_server/session.py +181 -19
  105. agentpool_server/opencode_server/.rules +95 -0
  106. agentpool_server/opencode_server/ENDPOINTS.md +362 -0
  107. agentpool_server/opencode_server/__init__.py +27 -0
  108. agentpool_server/opencode_server/command_validation.py +172 -0
  109. agentpool_server/opencode_server/converters.py +869 -0
  110. agentpool_server/opencode_server/dependencies.py +24 -0
  111. agentpool_server/opencode_server/input_provider.py +269 -0
  112. agentpool_server/opencode_server/models/__init__.py +228 -0
  113. agentpool_server/opencode_server/models/agent.py +53 -0
  114. agentpool_server/opencode_server/models/app.py +60 -0
  115. agentpool_server/opencode_server/models/base.py +26 -0
  116. agentpool_server/opencode_server/models/common.py +23 -0
  117. agentpool_server/opencode_server/models/config.py +37 -0
  118. agentpool_server/opencode_server/models/events.py +647 -0
  119. agentpool_server/opencode_server/models/file.py +88 -0
  120. agentpool_server/opencode_server/models/mcp.py +25 -0
  121. agentpool_server/opencode_server/models/message.py +162 -0
  122. agentpool_server/opencode_server/models/parts.py +190 -0
  123. agentpool_server/opencode_server/models/provider.py +81 -0
  124. agentpool_server/opencode_server/models/pty.py +43 -0
  125. agentpool_server/opencode_server/models/session.py +99 -0
  126. agentpool_server/opencode_server/routes/__init__.py +25 -0
  127. agentpool_server/opencode_server/routes/agent_routes.py +442 -0
  128. agentpool_server/opencode_server/routes/app_routes.py +139 -0
  129. agentpool_server/opencode_server/routes/config_routes.py +241 -0
  130. agentpool_server/opencode_server/routes/file_routes.py +392 -0
  131. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  132. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  133. agentpool_server/opencode_server/routes/message_routes.py +705 -0
  134. agentpool_server/opencode_server/routes/pty_routes.py +299 -0
  135. agentpool_server/opencode_server/routes/session_routes.py +1205 -0
  136. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  137. agentpool_server/opencode_server/server.py +430 -0
  138. agentpool_server/opencode_server/state.py +121 -0
  139. agentpool_server/opencode_server/time_utils.py +8 -0
  140. agentpool_storage/__init__.py +16 -0
  141. agentpool_storage/base.py +103 -0
  142. agentpool_storage/claude_provider.py +907 -0
  143. agentpool_storage/file_provider.py +129 -0
  144. agentpool_storage/memory_provider.py +61 -0
  145. agentpool_storage/models.py +3 -0
  146. agentpool_storage/opencode_provider.py +730 -0
  147. agentpool_storage/project_store.py +325 -0
  148. agentpool_storage/session_store.py +6 -0
  149. agentpool_storage/sql_provider/__init__.py +4 -2
  150. agentpool_storage/sql_provider/models.py +48 -0
  151. agentpool_storage/sql_provider/sql_provider.py +134 -1
  152. agentpool_storage/sql_provider/utils.py +10 -1
  153. agentpool_storage/text_log_provider.py +1 -0
  154. agentpool_toolsets/builtin/__init__.py +0 -8
  155. agentpool_toolsets/builtin/code.py +95 -56
  156. agentpool_toolsets/builtin/debug.py +16 -21
  157. agentpool_toolsets/builtin/execution_environment.py +99 -103
  158. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  159. agentpool_toolsets/builtin/skills.py +86 -4
  160. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  161. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  162. agentpool_toolsets/fsspec_toolset/grep.py +74 -2
  163. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  164. agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
  165. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  166. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  167. agentpool_toolsets/mcp_discovery/toolset.py +454 -0
  168. agentpool_toolsets/mcp_run_toolset.py +84 -6
  169. agentpool_toolsets/builtin/agent_management.py +0 -239
  170. agentpool_toolsets/builtin/history.py +0 -36
  171. agentpool_toolsets/builtin/integration.py +0 -85
  172. agentpool_toolsets/builtin/tool_management.py +0 -90
  173. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
  174. {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -2,7 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any, Self
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Any, Literal, Self
7
+
8
+ from anyenv.signals import Signal
6
9
 
7
10
  from agentpool.log import get_logger
8
11
  from agentpool.tools.base import Tool
@@ -25,19 +28,72 @@ if TYPE_CHECKING:
25
28
  logger = get_logger(__name__)
26
29
 
27
30
 
31
+ ResourceType = Literal["tools", "prompts", "resources", "skills"]
32
+ ProviderKind = Literal[
33
+ "base", "mcp", "mcp_run", "tools", "prompts", "skills", "aggregating", "custom"
34
+ ]
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class ResourceChangeEvent:
39
+ """Event emitted when resources change in a provider.
40
+
41
+ Attributes:
42
+ provider_name: Name of the provider instance
43
+ provider_kind: Kind/type of the provider (e.g., "mcp", "tools")
44
+ resource_type: Type of resource that changed
45
+ owner: Optional owner of the provider (e.g., agent name)
46
+ """
47
+
48
+ provider_name: str
49
+ provider_kind: ProviderKind
50
+ resource_type: ResourceType
51
+ owner: str | None = None
52
+
53
+
28
54
  class ResourceProvider:
29
55
  """Base class for resource providers.
30
56
 
31
57
  Provides tools, prompts, and other resources to agents.
32
58
  Default implementations return empty lists - override as needed.
59
+
60
+ Class Attributes:
61
+ kind: Short slug identifying the provider type (e.g., "mcp", "tools")
62
+
63
+ Change signals (using anyenv.signals.Signal):
64
+ - tools_changed: Emitted when tools change
65
+ - prompts_changed: Emitted when prompts change
66
+ - resources_changed: Emitted when resources change
67
+ - skills_changed: Emitted when skills change
68
+
69
+ Example:
70
+ provider.tools_changed.connect(my_handler)
71
+ await provider.tools_changed.emit(provider.create_change_event("tools"))
33
72
  """
34
73
 
74
+ kind: ProviderKind = "base"
75
+
76
+ # Change signals - emit ResourceChangeEvent when resources change
77
+ tools_changed: Signal[ResourceChangeEvent] = Signal()
78
+ prompts_changed: Signal[ResourceChangeEvent] = Signal()
79
+ resources_changed: Signal[ResourceChangeEvent] = Signal()
80
+ skills_changed: Signal[ResourceChangeEvent] = Signal()
81
+
35
82
  def __init__(self, name: str, owner: str | None = None) -> None:
36
83
  """Initialize the resource provider."""
37
84
  self.name = name
38
85
  self.owner = owner
39
86
  self.log = logger.bind(name=self.name, owner=self.owner)
40
87
 
88
+ def create_change_event(self, resource_type: ResourceType) -> ResourceChangeEvent:
89
+ """Create a ResourceChangeEvent for this provider."""
90
+ return ResourceChangeEvent(
91
+ provider_name=self.name,
92
+ provider_kind=self.kind,
93
+ resource_type=resource_type,
94
+ owner=self.owner,
95
+ )
96
+
41
97
  async def __aenter__(self) -> Self:
42
98
  """Async context entry if required."""
43
99
  return self
@@ -28,6 +28,8 @@ logger = get_logger(__name__)
28
28
  class MCPResourceProvider(ResourceProvider):
29
29
  """Resource provider for a single MCP server."""
30
30
 
31
+ kind = "mcp"
32
+
31
33
  def __init__(
32
34
  self,
33
35
  server: MCPServerConfig | str,
@@ -104,16 +106,22 @@ class MCPResourceProvider(ResourceProvider):
104
106
  logger.info("MCP tool list changed, refreshing provider cache")
105
107
  self._saved_enabled_states = {t.name: t.enabled for t in self._tools_cache or []}
106
108
  self._tools_cache = None
109
+ # Notify subscribers via signal
110
+ await self.tools_changed.emit(self.create_change_event("tools"))
107
111
 
108
112
  async def _on_prompts_changed(self) -> None:
109
113
  """Callback when prompts change on the MCP server."""
110
114
  logger.info("MCP prompt list changed, refreshing provider cache")
111
115
  self._prompts_cache = None
116
+ # Notify subscribers via signal
117
+ await self.prompts_changed.emit(self.create_change_event("prompts"))
112
118
 
113
119
  async def _on_resources_changed(self) -> None:
114
120
  """Callback when resources change on the MCP server."""
115
121
  logger.info("MCP resource list changed, refreshing provider cache")
116
122
  self._resources_cache = None
123
+ # Notify subscribers via signal
124
+ await self.resources_changed.emit(self.create_change_event("resources"))
117
125
 
118
126
  async def refresh_tools_cache(self) -> None:
119
127
  """Refresh the tools cache by fetching from client."""
@@ -204,6 +212,21 @@ class MCPResourceProvider(ResourceProvider):
204
212
 
205
213
  return self._resources_cache or []
206
214
 
215
+ def get_status(self) -> dict[str, str]:
216
+ """Get connection status for this MCP server.
217
+
218
+ Returns:
219
+ Status dict with 'status' key and optionally 'error' key.
220
+ Status can be: 'connected', 'disabled', or 'failed'.
221
+ """
222
+ try:
223
+ if self.client.connected:
224
+ return {"status": "connected"}
225
+ except Exception as e: # noqa: BLE001
226
+ return {"status": "failed", "error": str(e)}
227
+ else:
228
+ return {"status": "disabled"}
229
+
207
230
 
208
231
  if __name__ == "__main__":
209
232
  import anyio
@@ -3,19 +3,22 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
- from typing import TYPE_CHECKING, Literal
6
+ from typing import TYPE_CHECKING, Any, Literal
7
7
 
8
8
  from agentpool.agents.context import AgentContext # noqa: TC001
9
9
  from agentpool.resource_providers import ResourceProvider
10
+ from agentpool.utils.streams import TodoPriority, TodoStatus # noqa: TC001
10
11
 
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from agentpool.tools.base import Tool
15
+ from agentpool.utils.streams import TodoTracker
14
16
 
15
17
 
16
- # Plan entry types - domain models independent of ACP
18
+ # Keep PlanEntry for backward compatibility with event emitting
17
19
  PlanEntryPriority = Literal["high", "medium", "low"]
18
20
  PlanEntryStatus = Literal["pending", "in_progress", "completed"]
21
+ PlanToolMode = Literal["granular", "declarative", "hybrid"]
19
22
 
20
23
 
21
24
  @dataclass(kw_only=True)
@@ -50,22 +53,52 @@ class PlanProvider(ResourceProvider):
50
53
  """Provides plan-related tools for agent planning and task management.
51
54
 
52
55
  This provider creates tools for managing agent plans and tasks,
53
- emitting domain events that can be handled by protocol adapters.
56
+ delegating storage to pool.todos and emitting domain events
57
+ that can be handled by protocol adapters.
54
58
  """
55
59
 
56
- def __init__(self) -> None:
57
- """Initialize plan provider."""
60
+ kind = "tools"
61
+
62
+ def __init__(self, mode: PlanToolMode = "granular") -> None:
63
+ """Initialize plan provider.
64
+
65
+ Args:
66
+ mode: Tool mode - 'granular' for separate tools, 'declarative' for
67
+ single set_plan tool, 'hybrid' for both approaches.
68
+ """
58
69
  super().__init__(name="plan")
59
- self._current_plan: list[PlanEntry] = []
70
+ self.mode = mode
71
+
72
+ def _get_tracker(self, agent_ctx: AgentContext) -> TodoTracker | None:
73
+ """Get the TodoTracker from the pool."""
74
+ if agent_ctx.pool is not None:
75
+ return agent_ctx.pool.todos
76
+ return None
60
77
 
61
78
  async def get_tools(self) -> list[Tool]:
62
- """Get plan management tools."""
63
- return [
64
- self.create_tool(self.get_plan, category="read"),
65
- self.create_tool(self.add_plan_entry, category="other"),
66
- self.create_tool(self.update_plan_entry, category="edit"),
67
- self.create_tool(self.remove_plan_entry, category="delete"),
68
- ]
79
+ """Get plan management tools based on mode."""
80
+ tools: list[Tool] = [self.create_tool(self.get_plan, category="read")]
81
+
82
+ if self.mode == "declarative":
83
+ # Single bulk tool for capable models
84
+ 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
+ else:
94
+ # granular mode (default) - separate tools for simpler models
95
+ tools.extend([
96
+ self.create_tool(self.add_plan_entry, category="other"),
97
+ self.create_tool(self.update_plan_entry, category="edit"),
98
+ self.create_tool(self.remove_plan_entry, category="delete"),
99
+ ])
100
+
101
+ return tools
69
102
 
70
103
  async def get_plan(self, agent_ctx: AgentContext) -> str:
71
104
  """Get the current plan formatted as markdown.
@@ -76,7 +109,8 @@ class PlanProvider(ResourceProvider):
76
109
  Returns:
77
110
  Markdown-formatted plan with all entries and their status
78
111
  """
79
- if not self._current_plan:
112
+ tracker = self._get_tracker(agent_ctx)
113
+ if tracker is None or not tracker.entries:
80
114
  return "## Plan\n\n*No plan entries yet.*"
81
115
 
82
116
  lines = ["## Plan", ""]
@@ -90,18 +124,59 @@ class PlanProvider(ResourceProvider):
90
124
  "medium": "🟡",
91
125
  "low": "🟢",
92
126
  }
93
- for i, entry in enumerate(self._current_plan):
127
+ for i, entry in enumerate(tracker.entries):
94
128
  icon = status_icons.get(entry.status, "?")
95
129
  priority = priority_labels.get(entry.priority, "")
96
130
  lines.append(f"{i}. {icon} {priority} {entry.content} *({entry.status})*")
97
131
 
98
132
  return "\n".join(lines)
99
133
 
134
+ async def set_plan(
135
+ self,
136
+ agent_ctx: AgentContext,
137
+ entries: list[dict[str, Any]],
138
+ ) -> str:
139
+ """Replace the entire plan with new entries (declarative/bulk update).
140
+
141
+ This is more efficient than multiple add/update calls when setting
142
+ or significantly modifying the plan.
143
+
144
+ Args:
145
+ agent_ctx: Agent execution context
146
+ entries: List of plan entries, each with:
147
+ - content (str, required): Task description
148
+ - priority (str, optional): "high", "medium", or "low" (default: "medium")
149
+ - status (str, optional): "pending", "in_progress", or "completed"
150
+ (default: "pending")
151
+
152
+ Returns:
153
+ Success message indicating plan was updated
154
+ """
155
+ tracker = self._get_tracker(agent_ctx)
156
+ if tracker is None:
157
+ return "Error: No pool available for plan tracking"
158
+
159
+ # Clear existing entries
160
+ tracker.clear()
161
+
162
+ # Add all new entries
163
+ for entry in entries:
164
+ content = entry.get("content", "")
165
+ if not content:
166
+ continue
167
+ priority = entry.get("priority", "medium")
168
+ status = entry.get("status", "pending")
169
+ tracker.add(content, priority=priority, status=status)
170
+
171
+ await self._emit_plan_update(agent_ctx)
172
+
173
+ return f"Plan updated with {len(tracker.entries)} entries"
174
+
100
175
  async def add_plan_entry(
101
176
  self,
102
177
  agent_ctx: AgentContext,
103
178
  content: str,
104
- priority: PlanEntryPriority = "medium",
179
+ priority: TodoPriority = "medium",
105
180
  index: int | None = None,
106
181
  ) -> str:
107
182
  """Add a new plan entry.
@@ -115,15 +190,12 @@ class PlanProvider(ResourceProvider):
115
190
  Returns:
116
191
  Success message indicating entry was added
117
192
  """
118
- entry = PlanEntry(content=content, priority=priority, status="pending")
119
- if index is None:
120
- self._current_plan.append(entry)
121
- entry_index = len(self._current_plan) - 1
122
- else:
123
- if index < 0 or index > len(self._current_plan):
124
- return f"Error: Index {index} out of range (0-{len(self._current_plan)})"
125
- self._current_plan.insert(index, entry)
126
- entry_index = index
193
+ tracker = self._get_tracker(agent_ctx)
194
+ if tracker is None:
195
+ return "Error: No pool available for plan tracking"
196
+
197
+ entry = tracker.add(content, priority=priority, index=index)
198
+ entry_index = tracker.entries.index(entry)
127
199
 
128
200
  await self._emit_plan_update(agent_ctx)
129
201
 
@@ -134,8 +206,8 @@ class PlanProvider(ResourceProvider):
134
206
  agent_ctx: AgentContext,
135
207
  index: int,
136
208
  content: str | None = None,
137
- status: PlanEntryStatus | None = None,
138
- priority: PlanEntryPriority | None = None,
209
+ status: TodoStatus | None = None,
210
+ priority: TodoPriority | None = None,
139
211
  ) -> str:
140
212
  """Update an existing plan entry.
141
213
 
@@ -149,27 +221,26 @@ class PlanProvider(ResourceProvider):
149
221
  Returns:
150
222
  Success message indicating what was updated
151
223
  """
152
- if index < 0 or index >= len(self._current_plan):
153
- return f"Error: Index {index} out of range (0-{len(self._current_plan) - 1})"
224
+ tracker = self._get_tracker(agent_ctx)
225
+ if tracker is None:
226
+ return "Error: No pool available for plan tracking"
154
227
 
155
- entry = self._current_plan[index]
156
- updates = []
228
+ if index < 0 or index >= len(tracker.entries):
229
+ return f"Error: Index {index} out of range (0-{len(tracker.entries) - 1})"
157
230
 
231
+ updates = []
158
232
  if content is not None:
159
- entry.content = content
160
233
  updates.append(f"content to {content!r}")
161
-
162
234
  if status is not None:
163
- entry.status = status
164
235
  updates.append(f"status to {status!r}")
165
-
166
236
  if priority is not None:
167
- entry.priority = priority
168
237
  updates.append(f"priority to {priority!r}")
169
238
 
170
239
  if not updates:
171
240
  return "No changes specified"
172
241
 
242
+ tracker.update_by_index(index, content=content, status=status, priority=priority)
243
+
173
244
  await self._emit_plan_update(agent_ctx)
174
245
  return f"Updated entry {index}: {', '.join(updates)}"
175
246
 
@@ -183,14 +254,32 @@ class PlanProvider(ResourceProvider):
183
254
  Returns:
184
255
  Success message indicating entry was removed
185
256
  """
186
- if index < 0 or index >= len(self._current_plan):
187
- return f"Error: Index {index} out of range (0-{len(self._current_plan) - 1})"
188
- removed_entry = self._current_plan.pop(index)
257
+ tracker = self._get_tracker(agent_ctx)
258
+ if tracker is None:
259
+ return "Error: No pool available for plan tracking"
260
+
261
+ if index < 0 or index >= len(tracker.entries):
262
+ return f"Error: Index {index} out of range (0-{len(tracker.entries) - 1})"
263
+
264
+ removed_entry = tracker.remove_by_index(index)
189
265
  await self._emit_plan_update(agent_ctx)
190
- if self._current_plan:
266
+
267
+ if removed_entry is None:
268
+ return f"Error: Could not remove entry at index {index}"
269
+
270
+ if tracker.entries:
191
271
  return f"Removed entry {index}: {removed_entry.content!r}, remaining entries reindexed"
192
272
  return f"Removed entry {index}: {removed_entry.content!r}, plan is now empty"
193
273
 
194
274
  async def _emit_plan_update(self, agent_ctx: AgentContext) -> None:
195
275
  """Emit plan update event."""
196
- await agent_ctx.events.plan_updated(self._current_plan)
276
+ tracker = self._get_tracker(agent_ctx)
277
+ if tracker is None:
278
+ return
279
+
280
+ # Convert TodoEntry to PlanEntry for event compatibility
281
+ entries = [
282
+ PlanEntry(content=e.content, priority=e.priority, status=e.status)
283
+ for e in tracker.entries
284
+ ]
285
+ await agent_ctx.events.plan_updated(entries)
@@ -20,6 +20,8 @@ logger = get_logger(__name__)
20
20
  class PoolResourceProvider(ResourceProvider):
21
21
  """Provider that exposes an AgentPool's resources."""
22
22
 
23
+ kind = "tools"
24
+
23
25
  def __init__(
24
26
  self,
25
27
  pool: AgentPool[Any],
@@ -26,6 +26,8 @@ class StaticResourceProvider(ResourceProvider):
26
26
  to the common ResourceProvider interface.
27
27
  """
28
28
 
29
+ kind = "tools"
30
+
29
31
  def __init__(
30
32
  self,
31
33
  name: str = "static",
@@ -1,12 +1,13 @@
1
1
  """Session management package."""
2
2
 
3
- from agentpool.sessions.models import SessionData
3
+ 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
7
 
8
8
  __all__ = [
9
9
  "ClientSession",
10
+ "ProjectData",
10
11
  "SessionData",
11
12
  "SessionManager",
12
13
  "SessionStore",
@@ -17,6 +17,7 @@ if TYPE_CHECKING:
17
17
 
18
18
  from agentpool.delegation.pool import AgentPool
19
19
  from agentpool.sessions.store import SessionStore
20
+ from agentpool.storage.manager import StorageManager
20
21
 
21
22
  logger = get_logger(__name__)
22
23
 
@@ -37,6 +38,7 @@ class SessionManager:
37
38
  pool: AgentPool[Any],
38
39
  store: SessionStore | None = None,
39
40
  pool_id: str | None = None,
41
+ storage: StorageManager | None = None,
40
42
  ) -> None:
41
43
  """Initialize session manager.
42
44
 
@@ -44,10 +46,12 @@ class SessionManager:
44
46
  pool: Agent pool for agent access
45
47
  store: Session persistence backend (defaults to MemorySessionStore)
46
48
  pool_id: Optional identifier for this pool (for multi-pool setups)
49
+ storage: Optional storage manager for project tracking
47
50
  """
48
51
  self._pool = pool
49
52
  self._store = store or MemorySessionStore()
50
53
  self._pool_id = pool_id
54
+ self._storage = storage
51
55
  self._active: dict[str, ClientSession] = {}
52
56
  self._lock = asyncio.Lock()
53
57
  logger.debug("Initialized session manager", pool_id=pool_id)
@@ -94,8 +98,14 @@ class SessionManager:
94
98
  logger.debug("Session manager closed", session_count=len(sessions))
95
99
 
96
100
  def generate_session_id(self) -> str:
97
- """Generate a unique session ID."""
98
- return f"sess_{uuid4().hex[:12]}"
101
+ """Generate a unique, chronologically sortable session ID.
102
+
103
+ Uses OpenCode-compatible format: ses_{hex_timestamp}{random_base62}
104
+ IDs are lexicographically sortable by creation time.
105
+ """
106
+ from agentpool.utils.identifiers import generate_session_id
107
+
108
+ return generate_session_id()
99
109
 
100
110
  async def create(
101
111
  self,
@@ -139,12 +149,31 @@ class SessionManager:
139
149
  msg = f"Session '{session_id}' already exists"
140
150
  raise ValueError(msg)
141
151
 
152
+ # Get or create project if cwd provided and storage available
153
+ project_id: str | None = None
154
+ if cwd and self._storage:
155
+ try:
156
+ from agentpool_storage.project_store import ProjectStore
157
+
158
+ project_store = ProjectStore(self._storage)
159
+ project = await project_store.get_or_create(cwd)
160
+ project_id = project.project_id
161
+ logger.debug(
162
+ "Associated session with project",
163
+ session_id=session_id,
164
+ project_id=project_id,
165
+ worktree=project.worktree,
166
+ )
167
+ except Exception:
168
+ logger.exception("Failed to create/get project for session")
169
+
142
170
  # Create session data
143
171
  data = SessionData(
144
172
  session_id=session_id,
145
173
  agent_name=agent_name,
146
174
  conversation_id=conversation_id or f"conv_{uuid4().hex[:12]}",
147
175
  pool_id=self._pool_id,
176
+ project_id=project_id,
148
177
  cwd=cwd,
149
178
  metadata=metadata or {},
150
179
  )
@@ -11,6 +11,47 @@ from schemez import Schema
11
11
  from agentpool.utils.now import get_now
12
12
 
13
13
 
14
+ class ProjectData(Schema):
15
+ """Persistable project/worktree state.
16
+
17
+ Represents a codebase/worktree that agentpool operates on.
18
+ Sessions are associated with projects.
19
+ """
20
+
21
+ project_id: str
22
+ """Unique identifier (hash of canonical worktree path)."""
23
+
24
+ worktree: str
25
+ """Absolute path to the project root/worktree."""
26
+
27
+ name: str | None = None
28
+ """Optional friendly name for the project."""
29
+
30
+ vcs: str | None = None
31
+ """Version control system type ('git', 'hg', or None)."""
32
+
33
+ config_path: str | None = None
34
+ """Path to the project's config file, or None for auto-discovery."""
35
+
36
+ created_at: datetime = Field(default_factory=get_now)
37
+ """When the project was first registered."""
38
+
39
+ last_active: datetime = Field(default_factory=get_now)
40
+ """Last activity timestamp."""
41
+
42
+ settings: dict[str, Any] = Field(default_factory=dict)
43
+ """Project-specific settings overrides."""
44
+
45
+ def touch(self) -> ProjectData:
46
+ """Return copy with updated last_active timestamp."""
47
+ return self.model_copy(update={"last_active": get_now()})
48
+
49
+ def with_settings(self, **kwargs: Any) -> ProjectData:
50
+ """Return copy with updated settings."""
51
+ new_settings = {**self.settings, **kwargs}
52
+ return self.model_copy(update={"settings": new_settings, "last_active": get_now()})
53
+
54
+
14
55
  class SessionData(Schema):
15
56
  """Persistable session state.
16
57
 
@@ -33,6 +74,15 @@ class SessionData(Schema):
33
74
  pool_id: str | None = None
34
75
  """Optional pool/manifest identifier for multi-pool setups."""
35
76
 
77
+ project_id: str | None = None
78
+ """Project identifier (e.g., for OpenCode compatibility)."""
79
+
80
+ parent_id: str | None = None
81
+ """Parent session ID for forked sessions."""
82
+
83
+ version: str = "1"
84
+ """Session version string."""
85
+
36
86
  cwd: str | None = None
37
87
  """Working directory for the session."""
38
88
 
@@ -72,7 +72,9 @@ class SkillsRegistry(BaseRegistry[str, Skill]):
72
72
  if not isinstance(fs, AsyncFileSystem):
73
73
  fs = AsyncFileSystemWrapper(fs)
74
74
  search_path = base_path if base_path is not None else fs.root_marker
75
+ original_skills_dir: UPath | None = None
75
76
  else:
77
+ original_skills_dir = to_upath(skills_dir)
76
78
  fs = upath_to_fs(skills_dir, **storage_options)
77
79
  search_path = fs.root_marker
78
80
 
@@ -93,16 +95,19 @@ class SkillsRegistry(BaseRegistry[str, Skill]):
93
95
  return
94
96
  logger.info("Found skills", skills=skill_dirs, skills_dir=search_path)
95
97
  for skill_entry in skill_dirs:
96
- skill_name = skill_entry["name"].lstrip("./")
97
- # Construct full path for skill directory
98
- if search_path and search_path != fs.root_marker:
99
- skill_dir_path = to_upath(f"{search_path}/{skill_name}")
100
- skill_md_path = f"{search_path}/{skill_name}/SKILL.md"
98
+ # entry["name"] is relative to the filesystem root
99
+ # We need to construct the full path for _parse_skill
100
+ entry_name = skill_entry["name"]
101
+ if original_skills_dir is not None:
102
+ # When we created fs from a path, entry names are relative to that path
103
+ skill_dir_path = original_skills_dir / entry_name
101
104
  else:
102
- skill_dir_path = to_upath(skill_name)
103
- skill_md_path = f"{skill_name}/SKILL.md"
105
+ # When fs was provided directly, entry names should be usable as-is
106
+ skill_dir_path = to_upath(entry_name)
107
+ # For fs._cat_file, use the path relative to the filesystem
108
+ fs_skill_md_path = f"{entry_name}/SKILL.md"
104
109
  try:
105
- await fs._cat_file(skill_md_path)
110
+ await fs._cat_file(fs_skill_md_path)
106
111
  except FileNotFoundError:
107
112
  continue
108
113