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.
- acp/__init__.py +13 -0
- acp/bridge/README.md +15 -2
- acp/bridge/__init__.py +3 -2
- acp/bridge/__main__.py +60 -19
- acp/bridge/ws_server.py +173 -0
- acp/bridge/ws_server_cli.py +89 -0
- acp/notifications.py +2 -1
- acp/stdio.py +39 -9
- acp/transports.py +362 -2
- acp/utils.py +15 -2
- agentpool/__init__.py +4 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +203 -88
- agentpool/agents/acp_agent/acp_converters.py +46 -21
- agentpool/agents/acp_agent/client_handler.py +157 -3
- agentpool/agents/acp_agent/session_state.py +4 -1
- agentpool/agents/agent.py +314 -107
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +90 -21
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/base_agent.py +163 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +626 -179
- agentpool/agents/claude_code_agent/converters.py +71 -3
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +2 -0
- agentpool/agents/events/builtin_handlers.py +2 -1
- agentpool/agents/events/event_emitter.py +29 -2
- agentpool/agents/events/events.py +20 -0
- agentpool/agents/modes.py +54 -0
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/common_types.py +21 -0
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/claude_code_agent.yml +3 -0
- agentpool/delegation/pool.py +37 -29
- agentpool/delegation/team.py +1 -0
- agentpool/delegation/teamrun.py +1 -0
- agentpool/diagnostics/__init__.py +53 -0
- agentpool/diagnostics/lsp_manager.py +1593 -0
- agentpool/diagnostics/lsp_proxy.py +41 -0
- agentpool/diagnostics/lsp_proxy_script.py +229 -0
- agentpool/diagnostics/models.py +398 -0
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +12 -3
- agentpool/mcp_server/manager.py +25 -31
- agentpool/mcp_server/registries/official_registry_client.py +25 -0
- agentpool/mcp_server/tool_bridge.py +78 -66
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/message_history.py +12 -0
- agentpool/messaging/messages.py +52 -9
- agentpool/messaging/processing.py +3 -1
- agentpool/models/acp_agents/base.py +0 -22
- agentpool/models/acp_agents/mcp_capable.py +8 -148
- agentpool/models/acp_agents/non_mcp.py +129 -72
- agentpool/models/agents.py +35 -13
- agentpool/models/claude_code_agents.py +33 -2
- agentpool/models/manifest.py +43 -0
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +9 -1
- agentpool/resource_providers/aggregating.py +52 -3
- agentpool/resource_providers/base.py +57 -1
- agentpool/resource_providers/mcp_provider.py +23 -0
- agentpool/resource_providers/plan_provider.py +130 -41
- agentpool/resource_providers/pool.py +2 -0
- agentpool/resource_providers/static.py +2 -0
- agentpool/sessions/__init__.py +2 -1
- agentpool/sessions/manager.py +31 -2
- agentpool/sessions/models.py +50 -0
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +217 -1
- agentpool/testing.py +537 -19
- agentpool/utils/file_watcher.py +269 -0
- agentpool/utils/identifiers.py +121 -0
- agentpool/utils/pydantic_ai_helpers.py +46 -0
- agentpool/utils/streams.py +690 -1
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/METADATA +27 -7
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/RECORD +170 -112
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +4 -0
- agentpool_cli/serve_acp.py +41 -20
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_commands/__init__.py +30 -0
- agentpool_commands/agents.py +74 -1
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +51 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/mcp_server.py +131 -1
- agentpool_config/storage.py +46 -1
- agentpool_config/tools.py +7 -1
- agentpool_config/toolsets.py +92 -148
- agentpool_server/acp_server/acp_agent.py +134 -150
- agentpool_server/acp_server/commands/acp_commands.py +216 -51
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +10 -10
- agentpool_server/acp_server/server.py +23 -79
- agentpool_server/acp_server/session.py +181 -19
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +362 -0
- agentpool_server/opencode_server/__init__.py +27 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +869 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +269 -0
- agentpool_server/opencode_server/models/__init__.py +228 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +60 -0
- agentpool_server/opencode_server/models/base.py +26 -0
- agentpool_server/opencode_server/models/common.py +23 -0
- agentpool_server/opencode_server/models/config.py +37 -0
- agentpool_server/opencode_server/models/events.py +647 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +25 -0
- agentpool_server/opencode_server/models/message.py +162 -0
- agentpool_server/opencode_server/models/parts.py +190 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/session.py +99 -0
- agentpool_server/opencode_server/routes/__init__.py +25 -0
- agentpool_server/opencode_server/routes/agent_routes.py +442 -0
- agentpool_server/opencode_server/routes/app_routes.py +139 -0
- agentpool_server/opencode_server/routes/config_routes.py +241 -0
- agentpool_server/opencode_server/routes/file_routes.py +392 -0
- agentpool_server/opencode_server/routes/global_routes.py +94 -0
- agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
- agentpool_server/opencode_server/routes/message_routes.py +705 -0
- agentpool_server/opencode_server/routes/pty_routes.py +299 -0
- agentpool_server/opencode_server/routes/session_routes.py +1205 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +430 -0
- agentpool_server/opencode_server/state.py +121 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +16 -0
- agentpool_storage/base.py +103 -0
- agentpool_storage/claude_provider.py +907 -0
- agentpool_storage/file_provider.py +129 -0
- agentpool_storage/memory_provider.py +61 -0
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider.py +730 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +6 -0
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +134 -1
- agentpool_storage/sql_provider/utils.py +10 -1
- agentpool_storage/text_log_provider.py +1 -0
- agentpool_toolsets/builtin/__init__.py +0 -8
- agentpool_toolsets/builtin/code.py +95 -56
- agentpool_toolsets/builtin/debug.py +16 -21
- agentpool_toolsets/builtin/execution_environment.py +99 -103
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +86 -4
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +74 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +159 -38
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +454 -0
- agentpool_toolsets/mcp_run_toolset.py +84 -6
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- {agentpool-2.1.9.dist-info → agentpool-2.2.3.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
#
|
|
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
|
-
|
|
56
|
+
delegating storage to pool.todos and emitting domain events
|
|
57
|
+
that can be handled by protocol adapters.
|
|
54
58
|
"""
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
self.create_tool(self.
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
119
|
-
if
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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:
|
|
138
|
-
priority:
|
|
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
|
-
|
|
153
|
-
|
|
224
|
+
tracker = self._get_tracker(agent_ctx)
|
|
225
|
+
if tracker is None:
|
|
226
|
+
return "Error: No pool available for plan tracking"
|
|
154
227
|
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
agentpool/sessions/__init__.py
CHANGED
|
@@ -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",
|
agentpool/sessions/manager.py
CHANGED
|
@@ -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
|
-
|
|
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
|
)
|
agentpool/sessions/models.py
CHANGED
|
@@ -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
|
|
agentpool/skills/registry.py
CHANGED
|
@@ -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
|
-
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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(
|
|
110
|
+
await fs._cat_file(fs_skill_md_path)
|
|
106
111
|
except FileNotFoundError:
|
|
107
112
|
continue
|
|
108
113
|
|