agentpool 2.1.9__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.
- acp/__init__.py +13 -4
- acp/acp_requests.py +20 -77
- acp/agent/connection.py +8 -0
- acp/agent/implementations/debug_server/debug_server.py +6 -2
- acp/agent/protocol.py +6 -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/client/connection.py +38 -29
- acp/client/implementations/default_client.py +3 -2
- acp/client/implementations/headless_client.py +2 -2
- acp/connection.py +2 -2
- acp/notifications.py +20 -50
- acp/schema/__init__.py +2 -0
- acp/schema/agent_responses.py +21 -0
- acp/schema/client_requests.py +3 -3
- acp/schema/session_state.py +63 -29
- acp/stdio.py +39 -9
- acp/task/supervisor.py +2 -2
- acp/transports.py +362 -2
- acp/utils.py +17 -4
- agentpool/__init__.py +6 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +407 -277
- agentpool/agents/acp_agent/acp_converters.py +196 -38
- agentpool/agents/acp_agent/client_handler.py +191 -26
- agentpool/agents/acp_agent/session_state.py +17 -6
- agentpool/agents/agent.py +607 -572
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +176 -110
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +632 -17
- agentpool/agents/claude_code_agent/FORKING.md +191 -0
- agentpool/agents/claude_code_agent/__init__.py +13 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
- agentpool/agents/claude_code_agent/converters.py +74 -143
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/claude_code_agent/models.py +77 -0
- agentpool/agents/claude_code_agent/static_info.py +100 -0
- agentpool/agents/claude_code_agent/usage.py +242 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +24 -0
- agentpool/agents/events/builtin_handlers.py +67 -1
- agentpool/agents/events/event_emitter.py +32 -2
- agentpool/agents/events/events.py +104 -3
- agentpool/agents/events/infer_info.py +145 -0
- agentpool/agents/events/processors.py +254 -0
- agentpool/agents/interactions.py +41 -6
- agentpool/agents/modes.py +67 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +56 -21
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/acp_assistant.yml +2 -2
- agentpool/config_resources/agents.yml +3 -0
- agentpool/config_resources/agents_template.yml +1 -0
- agentpool/config_resources/claude_code_agent.yml +10 -6
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +136 -289
- agentpool/delegation/team.py +58 -57
- agentpool/delegation/teamrun.py +51 -55
- 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/functional/run.py +10 -4
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +76 -32
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +34 -54
- agentpool/mcp_server/registries/official_registry_client.py +35 -1
- agentpool/mcp_server/tool_bridge.py +186 -139
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/connection_manager.py +11 -10
- agentpool/messaging/event_manager.py +5 -5
- agentpool/messaging/message_container.py +6 -30
- agentpool/messaging/message_history.py +99 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +54 -35
- agentpool/messaging/processing.py +12 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -24
- agentpool/models/acp_agents/mcp_capable.py +126 -157
- agentpool/models/acp_agents/non_mcp.py +129 -95
- agentpool/models/agents.py +98 -76
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +144 -19
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +113 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +11 -1
- agentpool/resource_providers/aggregating.py +56 -5
- agentpool/resource_providers/base.py +70 -4
- agentpool/resource_providers/codemode/code_executor.py +72 -5
- agentpool/resource_providers/codemode/helpers.py +2 -2
- agentpool/resource_providers/codemode/provider.py +64 -12
- agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
- agentpool/resource_providers/codemode/remote_provider.py +9 -12
- agentpool/resource_providers/filtering.py +3 -1
- agentpool/resource_providers/mcp_provider.py +89 -12
- agentpool/resource_providers/plan_provider.py +228 -46
- agentpool/resource_providers/pool.py +7 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +4 -2
- agentpool/sessions/__init__.py +4 -1
- agentpool/sessions/manager.py +33 -5
- agentpool/sessions/models.py +59 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +572 -49
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +538 -20
- agentpool/tool_impls/__init__.py +6 -0
- agentpool/tool_impls/agent_cli/__init__.py +42 -0
- agentpool/tool_impls/agent_cli/tool.py +95 -0
- agentpool/tool_impls/bash/__init__.py +64 -0
- agentpool/tool_impls/bash/helpers.py +35 -0
- agentpool/tool_impls/bash/tool.py +171 -0
- agentpool/tool_impls/delete_path/__init__.py +70 -0
- agentpool/tool_impls/delete_path/tool.py +142 -0
- agentpool/tool_impls/download_file/__init__.py +80 -0
- agentpool/tool_impls/download_file/tool.py +183 -0
- agentpool/tool_impls/execute_code/__init__.py +55 -0
- agentpool/tool_impls/execute_code/tool.py +163 -0
- agentpool/tool_impls/grep/__init__.py +80 -0
- agentpool/tool_impls/grep/tool.py +200 -0
- agentpool/tool_impls/list_directory/__init__.py +73 -0
- agentpool/tool_impls/list_directory/tool.py +197 -0
- agentpool/tool_impls/question/__init__.py +42 -0
- agentpool/tool_impls/question/tool.py +127 -0
- agentpool/tool_impls/read/__init__.py +104 -0
- agentpool/tool_impls/read/tool.py +305 -0
- agentpool/tools/__init__.py +2 -1
- agentpool/tools/base.py +114 -34
- agentpool/tools/manager.py +57 -1
- agentpool/ui/base.py +2 -2
- agentpool/ui/mock_provider.py +2 -2
- agentpool/ui/stdlib_provider.py +2 -2
- 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 +616 -2
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
- agentpool-2.5.0.dist-info/RECORD +579 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +24 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +100 -21
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +42 -5
- agentpool_commands/agents.py +75 -2
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/pool.py +260 -0
- agentpool_commands/session.py +1 -1
- agentpool_commands/text_sharing/__init__.py +119 -0
- agentpool_commands/text_sharing/base.py +123 -0
- agentpool_commands/text_sharing/github_gist.py +80 -0
- agentpool_commands/text_sharing/opencode.py +462 -0
- agentpool_commands/text_sharing/paste_rs.py +59 -0
- agentpool_commands/text_sharing/pastebin.py +116 -0
- agentpool_commands/text_sharing/shittycodingagent.py +112 -0
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +80 -30
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/converters.py +1 -1
- agentpool_config/event_handlers.py +42 -0
- agentpool_config/events.py +1 -1
- agentpool_config/forward_targets.py +1 -4
- agentpool_config/jinja.py +3 -3
- agentpool_config/mcp_server.py +132 -6
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +82 -38
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -22
- agentpool_config/toolsets.py +109 -233
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +234 -181
- agentpool_server/acp_server/commands/acp_commands.py +151 -156
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +24 -90
- agentpool_server/acp_server/session.py +173 -331
- agentpool_server/acp_server/session_manager.py +8 -34
- agentpool_server/agui_server/server.py +3 -1
- agentpool_server/mcp_server/server.py +5 -2
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +401 -0
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +19 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +975 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +421 -0
- agentpool_server/opencode_server/models/__init__.py +250 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +72 -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 +821 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +44 -0
- agentpool_server/opencode_server/models/message.py +179 -0
- agentpool_server/opencode_server/models/parts.py +323 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +111 -0
- agentpool_server/opencode_server/routes/__init__.py +29 -0
- agentpool_server/opencode_server/routes/agent_routes.py +473 -0
- agentpool_server/opencode_server/routes/app_routes.py +202 -0
- agentpool_server/opencode_server/routes/config_routes.py +302 -0
- agentpool_server/opencode_server/routes/file_routes.py +571 -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 +761 -0
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +300 -0
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +1276 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +475 -0
- agentpool_server/opencode_server/state.py +151 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +12 -0
- agentpool_storage/base.py +184 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/claude_provider/provider.py +1089 -0
- agentpool_storage/file_provider.py +278 -15
- agentpool_storage/memory_provider.py +193 -12
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
- agentpool_storage/opencode_provider/__init__.py +16 -0
- agentpool_storage/opencode_provider/helpers.py +414 -0
- agentpool_storage/opencode_provider/provider.py +895 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +26 -6
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +269 -3
- agentpool_storage/sql_provider/utils.py +12 -13
- agentpool_storage/zed_provider/__init__.py +16 -0
- agentpool_storage/zed_provider/helpers.py +281 -0
- agentpool_storage/zed_provider/models.py +130 -0
- agentpool_storage/zed_provider/provider.py +442 -0
- agentpool_storage/zed_provider.py +803 -0
- agentpool_toolsets/__init__.py +0 -2
- agentpool_toolsets/builtin/__init__.py +2 -12
- agentpool_toolsets/builtin/code.py +96 -57
- agentpool_toolsets/builtin/debug.py +118 -48
- agentpool_toolsets/builtin/execution_environment.py +115 -230
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +9 -4
- agentpool_toolsets/builtin/subagent_tools.py +64 -51
- agentpool_toolsets/builtin/workers.py +4 -2
- agentpool_toolsets/composio_toolset.py +2 -2
- agentpool_toolsets/entry_points.py +3 -1
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +99 -7
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +511 -0
- agentpool_toolsets/mcp_run_toolset.py +87 -12
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- agentpool-2.1.9.dist-info/RECORD +0 -474
- agentpool_config/resources.py +0 -33
- agentpool_server/acp_server/acp_tools.py +0 -43
- agentpool_server/acp_server/commands/spawn.py +0 -210
- agentpool_storage/text_log_provider.py +0 -275
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,9 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from decimal import Decimal
|
|
7
|
-
from typing import TYPE_CHECKING, TypedDict, cast
|
|
7
|
+
from typing import TYPE_CHECKING, Any, TypedDict, cast
|
|
8
8
|
|
|
9
|
-
from pydantic_ai import RunUsage
|
|
9
|
+
from pydantic_ai import FinishReason, RunUsage # noqa: TC002
|
|
10
10
|
from upathtools import to_upath
|
|
11
11
|
|
|
12
12
|
from agentpool.common_types import JsonValue, MessageRole # noqa: TC001
|
|
@@ -19,9 +19,9 @@ from agentpool_storage.models import TokenUsage
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
|
-
from pydantic_ai import FinishReason
|
|
23
22
|
from yamling import FormatType
|
|
24
23
|
|
|
24
|
+
from agentpool.sessions.models import ProjectData
|
|
25
25
|
from agentpool_config.session import SessionQuery
|
|
26
26
|
from agentpool_config.storage import FileStorageConfig
|
|
27
27
|
|
|
@@ -41,11 +41,11 @@ class MessageData(TypedDict):
|
|
|
41
41
|
cost: Decimal | None
|
|
42
42
|
token_usage: TokenUsage | None
|
|
43
43
|
response_time: float | None
|
|
44
|
-
forwarded_from: list[str] | None
|
|
45
44
|
provider_name: str | None
|
|
46
45
|
provider_response_id: str | None
|
|
47
46
|
messages: str | None
|
|
48
47
|
finish_reason: FinishReason | None
|
|
48
|
+
parent_id: str | None
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
class ConversationData(TypedDict):
|
|
@@ -68,12 +68,26 @@ class CommandData(TypedDict):
|
|
|
68
68
|
metadata: dict[str, JsonValue]
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
class ProjectDataDict(TypedDict):
|
|
72
|
+
"""Data structure for storing project information."""
|
|
73
|
+
|
|
74
|
+
project_id: str
|
|
75
|
+
worktree: str
|
|
76
|
+
name: str | None
|
|
77
|
+
vcs: str | None
|
|
78
|
+
config_path: str | None
|
|
79
|
+
created_at: str
|
|
80
|
+
last_active: str
|
|
81
|
+
settings: dict[str, JsonValue]
|
|
82
|
+
|
|
83
|
+
|
|
71
84
|
class StorageData(TypedDict):
|
|
72
85
|
"""Data structure for storing storage information."""
|
|
73
86
|
|
|
74
87
|
messages: list[MessageData]
|
|
75
88
|
conversations: list[ConversationData]
|
|
76
89
|
commands: list[CommandData]
|
|
90
|
+
projects: list[ProjectDataDict]
|
|
77
91
|
|
|
78
92
|
|
|
79
93
|
class FileProvider(StorageProvider):
|
|
@@ -99,6 +113,7 @@ class FileProvider(StorageProvider):
|
|
|
99
113
|
"messages": [],
|
|
100
114
|
"conversations": [],
|
|
101
115
|
"commands": [],
|
|
116
|
+
"projects": [],
|
|
102
117
|
}
|
|
103
118
|
self._load()
|
|
104
119
|
|
|
@@ -132,14 +147,7 @@ class FileProvider(StorageProvider):
|
|
|
132
147
|
# Apply filters
|
|
133
148
|
if query.name and msg["conversation_id"] != query.name:
|
|
134
149
|
continue
|
|
135
|
-
if query.agents and not
|
|
136
|
-
msg["name"] in query.agents
|
|
137
|
-
or (
|
|
138
|
-
query.include_forwarded
|
|
139
|
-
and msg["forwarded_from"]
|
|
140
|
-
and any(a in query.agents for a in msg["forwarded_from"])
|
|
141
|
-
)
|
|
142
|
-
):
|
|
150
|
+
if query.agents and msg["name"] not in query.agents:
|
|
143
151
|
continue
|
|
144
152
|
cutoff = query.get_time_cutoff()
|
|
145
153
|
timestamp = datetime.fromisoformat(msg["timestamp"])
|
|
@@ -175,7 +183,6 @@ class FileProvider(StorageProvider):
|
|
|
175
183
|
model_name=msg["model"],
|
|
176
184
|
cost_info=cost_info,
|
|
177
185
|
response_time=msg["response_time"],
|
|
178
|
-
forwarded_from=msg["forwarded_from"] or [],
|
|
179
186
|
timestamp=datetime.fromisoformat(msg["timestamp"]),
|
|
180
187
|
provider_name=msg["provider_name"],
|
|
181
188
|
provider_response_id=msg["provider_response_id"],
|
|
@@ -200,11 +207,11 @@ class FileProvider(StorageProvider):
|
|
|
200
207
|
cost_info: TokenCost | None = None,
|
|
201
208
|
model: str | None = None,
|
|
202
209
|
response_time: float | None = None,
|
|
203
|
-
forwarded_from: list[str] | None = None,
|
|
204
210
|
provider_name: str | None = None,
|
|
205
211
|
provider_response_id: str | None = None,
|
|
206
212
|
messages: str | None = None,
|
|
207
213
|
finish_reason: FinishReason | None = None,
|
|
214
|
+
parent_id: str | None = None,
|
|
208
215
|
) -> None:
|
|
209
216
|
"""Log a new message."""
|
|
210
217
|
self._data["messages"].append({
|
|
@@ -222,11 +229,11 @@ class FileProvider(StorageProvider):
|
|
|
222
229
|
total=cost_info.token_usage.total_tokens if cost_info else 0,
|
|
223
230
|
),
|
|
224
231
|
"response_time": response_time,
|
|
225
|
-
"forwarded_from": forwarded_from,
|
|
226
232
|
"provider_name": provider_name,
|
|
227
233
|
"provider_response_id": provider_response_id,
|
|
228
234
|
"messages": messages,
|
|
229
235
|
"finish_reason": finish_reason,
|
|
236
|
+
"parent_id": parent_id,
|
|
230
237
|
})
|
|
231
238
|
self._save()
|
|
232
239
|
|
|
@@ -269,6 +276,152 @@ class FileProvider(StorageProvider):
|
|
|
269
276
|
return conv.get("title")
|
|
270
277
|
return None
|
|
271
278
|
|
|
279
|
+
async def get_conversation_messages(
|
|
280
|
+
self,
|
|
281
|
+
conversation_id: str,
|
|
282
|
+
*,
|
|
283
|
+
include_ancestors: bool = False,
|
|
284
|
+
) -> list[ChatMessage[str]]:
|
|
285
|
+
"""Get all messages for a conversation."""
|
|
286
|
+
messages: list[ChatMessage[str]] = []
|
|
287
|
+
for msg in self._data["messages"]:
|
|
288
|
+
if msg["conversation_id"] != conversation_id:
|
|
289
|
+
continue
|
|
290
|
+
chat_msg = self._to_chat_message(msg)
|
|
291
|
+
messages.append(chat_msg)
|
|
292
|
+
|
|
293
|
+
# Sort by timestamp
|
|
294
|
+
messages.sort(key=lambda m: m.timestamp or get_now())
|
|
295
|
+
|
|
296
|
+
if not include_ancestors or not messages:
|
|
297
|
+
return messages
|
|
298
|
+
|
|
299
|
+
# Get ancestor chain if first message has parent_id
|
|
300
|
+
first_msg = messages[0]
|
|
301
|
+
if first_msg.parent_id:
|
|
302
|
+
ancestors = await self.get_message_ancestry(first_msg.parent_id)
|
|
303
|
+
return ancestors + messages
|
|
304
|
+
|
|
305
|
+
return messages
|
|
306
|
+
|
|
307
|
+
def _to_chat_message(self, msg: MessageData) -> ChatMessage[str]:
|
|
308
|
+
"""Convert stored message data to ChatMessage."""
|
|
309
|
+
cost_info = None
|
|
310
|
+
if msg.get("token_usage"):
|
|
311
|
+
usage = msg["token_usage"]
|
|
312
|
+
cost_info = TokenCost(
|
|
313
|
+
token_usage=RunUsage(
|
|
314
|
+
input_tokens=usage.get("prompt", 0) if usage else 0,
|
|
315
|
+
output_tokens=usage.get("completion", 0) if usage else 0,
|
|
316
|
+
),
|
|
317
|
+
total_cost=Decimal(str(msg.get("cost") or 0)),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Build kwargs, only including timestamp/message_id if they have values
|
|
321
|
+
kwargs: dict[str, Any] = {
|
|
322
|
+
"content": msg["content"],
|
|
323
|
+
"role": cast(MessageRole, msg["role"]),
|
|
324
|
+
"name": msg.get("name"),
|
|
325
|
+
"model_name": msg.get("model"),
|
|
326
|
+
"cost_info": cost_info,
|
|
327
|
+
"response_time": msg.get("response_time"),
|
|
328
|
+
"parent_id": msg.get("parent_id"),
|
|
329
|
+
"conversation_id": msg.get("conversation_id"),
|
|
330
|
+
"messages": deserialize_messages(msg.get("messages")),
|
|
331
|
+
"finish_reason": msg.get("finish_reason"),
|
|
332
|
+
}
|
|
333
|
+
if msg.get("timestamp"):
|
|
334
|
+
kwargs["timestamp"] = datetime.fromisoformat(msg["timestamp"])
|
|
335
|
+
if msg.get("message_id"):
|
|
336
|
+
kwargs["message_id"] = msg["message_id"]
|
|
337
|
+
|
|
338
|
+
return ChatMessage[str](**kwargs)
|
|
339
|
+
|
|
340
|
+
async def get_message(self, message_id: str) -> ChatMessage[str] | None:
|
|
341
|
+
"""Get a single message by ID."""
|
|
342
|
+
for msg in self._data["messages"]:
|
|
343
|
+
if msg.get("message_id") == message_id:
|
|
344
|
+
return self._to_chat_message(msg)
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
async def get_message_ancestry(self, message_id: str) -> list[ChatMessage[str]]:
|
|
348
|
+
"""Get the ancestry chain of a message."""
|
|
349
|
+
ancestors: list[ChatMessage[str]] = []
|
|
350
|
+
current_id: str | None = message_id
|
|
351
|
+
|
|
352
|
+
while current_id:
|
|
353
|
+
msg = await self.get_message(current_id)
|
|
354
|
+
if not msg:
|
|
355
|
+
break
|
|
356
|
+
ancestors.append(msg)
|
|
357
|
+
current_id = msg.parent_id
|
|
358
|
+
|
|
359
|
+
# Reverse to get oldest first
|
|
360
|
+
ancestors.reverse()
|
|
361
|
+
return ancestors
|
|
362
|
+
|
|
363
|
+
async def fork_conversation(
|
|
364
|
+
self,
|
|
365
|
+
*,
|
|
366
|
+
source_conversation_id: str,
|
|
367
|
+
new_conversation_id: str,
|
|
368
|
+
fork_from_message_id: str | None = None,
|
|
369
|
+
new_agent_name: str | None = None,
|
|
370
|
+
) -> str | None:
|
|
371
|
+
"""Fork a conversation at a specific point."""
|
|
372
|
+
# Find source conversation
|
|
373
|
+
source_conv = next(
|
|
374
|
+
(c for c in self._data["conversations"] if c["id"] == source_conversation_id),
|
|
375
|
+
None,
|
|
376
|
+
)
|
|
377
|
+
if not source_conv:
|
|
378
|
+
msg = f"Source conversation not found: {source_conversation_id}"
|
|
379
|
+
raise ValueError(msg)
|
|
380
|
+
|
|
381
|
+
# Determine fork point
|
|
382
|
+
fork_point_id: str | None = None
|
|
383
|
+
if fork_from_message_id:
|
|
384
|
+
# Verify message exists in source conversation
|
|
385
|
+
msg_exists = any(
|
|
386
|
+
m.get("message_id") == fork_from_message_id
|
|
387
|
+
and m["conversation_id"] == source_conversation_id
|
|
388
|
+
for m in self._data["messages"]
|
|
389
|
+
)
|
|
390
|
+
if not msg_exists:
|
|
391
|
+
err = f"Message {fork_from_message_id} not found in conversation"
|
|
392
|
+
raise ValueError(err)
|
|
393
|
+
fork_point_id = fork_from_message_id
|
|
394
|
+
else:
|
|
395
|
+
# Find last message in source conversation
|
|
396
|
+
conv_messages = [
|
|
397
|
+
m for m in self._data["messages"] if m["conversation_id"] == source_conversation_id
|
|
398
|
+
]
|
|
399
|
+
if conv_messages:
|
|
400
|
+
conv_messages.sort(
|
|
401
|
+
key=lambda m: (
|
|
402
|
+
datetime.fromisoformat(m["timestamp"]) if m.get("timestamp") else get_now()
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
fork_point_id = conv_messages[-1].get("message_id")
|
|
406
|
+
|
|
407
|
+
# Create new conversation
|
|
408
|
+
agent_name = new_agent_name or source_conv["agent_name"]
|
|
409
|
+
title = (
|
|
410
|
+
f"{source_conv.get('title') or 'Conversation'} (fork)"
|
|
411
|
+
if source_conv.get("title")
|
|
412
|
+
else None
|
|
413
|
+
)
|
|
414
|
+
new_conv: ConversationData = {
|
|
415
|
+
"id": new_conversation_id,
|
|
416
|
+
"agent_name": agent_name,
|
|
417
|
+
"title": title,
|
|
418
|
+
"start_time": get_now().isoformat(),
|
|
419
|
+
}
|
|
420
|
+
self._data["conversations"].append(new_conv)
|
|
421
|
+
self._save()
|
|
422
|
+
|
|
423
|
+
return fork_point_id
|
|
424
|
+
|
|
272
425
|
async def log_command(
|
|
273
426
|
self,
|
|
274
427
|
*,
|
|
@@ -329,6 +482,7 @@ class FileProvider(StorageProvider):
|
|
|
329
482
|
"messages": [],
|
|
330
483
|
"conversations": [],
|
|
331
484
|
"commands": [],
|
|
485
|
+
"projects": [],
|
|
332
486
|
}
|
|
333
487
|
self._save()
|
|
334
488
|
return conv_count, msg_count
|
|
@@ -376,3 +530,112 @@ class FileProvider(StorageProvider):
|
|
|
376
530
|
msg_count = len(self._data["messages"])
|
|
377
531
|
|
|
378
532
|
return conv_count, msg_count
|
|
533
|
+
|
|
534
|
+
async def delete_conversation_messages(
|
|
535
|
+
self,
|
|
536
|
+
conversation_id: str,
|
|
537
|
+
) -> int:
|
|
538
|
+
"""Delete all messages for a conversation."""
|
|
539
|
+
original_count = len(self._data["messages"])
|
|
540
|
+
self._data["messages"] = [
|
|
541
|
+
m for m in self._data["messages"] if m["conversation_id"] != conversation_id
|
|
542
|
+
]
|
|
543
|
+
deleted = original_count - len(self._data["messages"])
|
|
544
|
+
if deleted > 0:
|
|
545
|
+
self._save()
|
|
546
|
+
return deleted
|
|
547
|
+
|
|
548
|
+
# Project methods
|
|
549
|
+
|
|
550
|
+
def _to_project_data(self, data: ProjectDataDict) -> ProjectData:
|
|
551
|
+
"""Convert dict to ProjectData."""
|
|
552
|
+
from datetime import datetime
|
|
553
|
+
|
|
554
|
+
from agentpool.sessions.models import ProjectData
|
|
555
|
+
|
|
556
|
+
return ProjectData(
|
|
557
|
+
project_id=data["project_id"],
|
|
558
|
+
worktree=data["worktree"],
|
|
559
|
+
name=data["name"],
|
|
560
|
+
vcs=data["vcs"],
|
|
561
|
+
config_path=data["config_path"],
|
|
562
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
563
|
+
last_active=datetime.fromisoformat(data["last_active"]),
|
|
564
|
+
settings=data["settings"],
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
def _to_project_dict(self, project: ProjectData) -> ProjectDataDict:
|
|
568
|
+
"""Convert ProjectData to dict."""
|
|
569
|
+
return ProjectDataDict(
|
|
570
|
+
project_id=project.project_id,
|
|
571
|
+
worktree=project.worktree,
|
|
572
|
+
name=project.name,
|
|
573
|
+
vcs=project.vcs,
|
|
574
|
+
config_path=project.config_path,
|
|
575
|
+
created_at=project.created_at.isoformat(),
|
|
576
|
+
last_active=project.last_active.isoformat(),
|
|
577
|
+
settings=project.settings,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
async def save_project(self, project: ProjectData) -> None:
|
|
581
|
+
"""Save or update a project."""
|
|
582
|
+
# Remove existing if present
|
|
583
|
+
self._data["projects"] = [
|
|
584
|
+
p for p in self._data.get("projects", []) if p["project_id"] != project.project_id
|
|
585
|
+
]
|
|
586
|
+
# Add new/updated
|
|
587
|
+
self._data["projects"].append(self._to_project_dict(project))
|
|
588
|
+
self._save()
|
|
589
|
+
logger.debug("Saved project", project_id=project.project_id)
|
|
590
|
+
|
|
591
|
+
async def get_project(self, project_id: str) -> ProjectData | None:
|
|
592
|
+
"""Get a project by ID."""
|
|
593
|
+
for p in self._data.get("projects", []):
|
|
594
|
+
if p["project_id"] == project_id:
|
|
595
|
+
return self._to_project_data(p)
|
|
596
|
+
return None
|
|
597
|
+
|
|
598
|
+
async def get_project_by_worktree(self, worktree: str) -> ProjectData | None:
|
|
599
|
+
"""Get a project by worktree path."""
|
|
600
|
+
for p in self._data.get("projects", []):
|
|
601
|
+
if p["worktree"] == worktree:
|
|
602
|
+
return self._to_project_data(p)
|
|
603
|
+
return None
|
|
604
|
+
|
|
605
|
+
async def get_project_by_name(self, name: str) -> ProjectData | None:
|
|
606
|
+
"""Get a project by friendly name."""
|
|
607
|
+
for p in self._data.get("projects", []):
|
|
608
|
+
if p["name"] == name:
|
|
609
|
+
return self._to_project_data(p)
|
|
610
|
+
return None
|
|
611
|
+
|
|
612
|
+
async def list_projects(self, limit: int | None = None) -> list[ProjectData]:
|
|
613
|
+
"""List all projects, ordered by last_active descending."""
|
|
614
|
+
projects = sorted(
|
|
615
|
+
self._data.get("projects", []),
|
|
616
|
+
key=lambda p: p["last_active"],
|
|
617
|
+
reverse=True,
|
|
618
|
+
)
|
|
619
|
+
if limit is not None:
|
|
620
|
+
projects = projects[:limit]
|
|
621
|
+
return [self._to_project_data(p) for p in projects]
|
|
622
|
+
|
|
623
|
+
async def delete_project(self, project_id: str) -> bool:
|
|
624
|
+
"""Delete a project."""
|
|
625
|
+
original_count = len(self._data.get("projects", []))
|
|
626
|
+
self._data["projects"] = [
|
|
627
|
+
p for p in self._data.get("projects", []) if p["project_id"] != project_id
|
|
628
|
+
]
|
|
629
|
+
deleted = original_count > len(self._data["projects"])
|
|
630
|
+
if deleted:
|
|
631
|
+
self._save()
|
|
632
|
+
logger.debug("Deleted project", project_id=project_id)
|
|
633
|
+
return deleted
|
|
634
|
+
|
|
635
|
+
async def touch_project(self, project_id: str) -> None:
|
|
636
|
+
"""Update project's last_active timestamp."""
|
|
637
|
+
for p in self._data.get("projects", []):
|
|
638
|
+
if p["project_id"] == project_id:
|
|
639
|
+
p["last_active"] = get_now().isoformat()
|
|
640
|
+
self._save()
|
|
641
|
+
return
|
|
@@ -16,11 +16,37 @@ if TYPE_CHECKING:
|
|
|
16
16
|
from collections.abc import Sequence
|
|
17
17
|
|
|
18
18
|
from agentpool.common_types import JsonValue
|
|
19
|
+
from agentpool.sessions.models import ProjectData
|
|
19
20
|
from agentpool_config.session import SessionQuery
|
|
20
21
|
from agentpool_config.storage import MemoryStorageConfig
|
|
21
22
|
from agentpool_storage.models import MessageData, QueryFilters, StatsFilters, TokenUsage
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
def _dict_to_chat_message(msg: dict[str, Any]) -> ChatMessage[str]:
|
|
26
|
+
"""Convert a stored message dict to ChatMessage."""
|
|
27
|
+
cost_info = None
|
|
28
|
+
if msg.get("cost_info"):
|
|
29
|
+
cost_info = TokenCost(token_usage=msg["cost_info"], total_cost=msg.get("cost", 0.0))
|
|
30
|
+
|
|
31
|
+
# Build kwargs, only including timestamp/message_id if they exist
|
|
32
|
+
kwargs: dict[str, Any] = {
|
|
33
|
+
"content": msg["content"],
|
|
34
|
+
"role": msg["role"],
|
|
35
|
+
"name": msg.get("name"),
|
|
36
|
+
"model_name": msg.get("model"),
|
|
37
|
+
"cost_info": cost_info,
|
|
38
|
+
"response_time": msg.get("response_time"),
|
|
39
|
+
"parent_id": msg.get("parent_id"),
|
|
40
|
+
"conversation_id": msg.get("conversation_id"),
|
|
41
|
+
}
|
|
42
|
+
if msg.get("timestamp"):
|
|
43
|
+
kwargs["timestamp"] = msg["timestamp"]
|
|
44
|
+
if msg.get("message_id"):
|
|
45
|
+
kwargs["message_id"] = msg["message_id"]
|
|
46
|
+
|
|
47
|
+
return ChatMessage[str](**kwargs)
|
|
48
|
+
|
|
49
|
+
|
|
24
50
|
class MemoryStorageProvider(StorageProvider):
|
|
25
51
|
"""In-memory storage provider for testing."""
|
|
26
52
|
|
|
@@ -31,12 +57,14 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
31
57
|
self.messages: list[dict[str, Any]] = []
|
|
32
58
|
self.conversations: list[dict[str, Any]] = []
|
|
33
59
|
self.commands: list[dict[str, Any]] = []
|
|
60
|
+
self.projects: dict[str, ProjectData] = {}
|
|
34
61
|
|
|
35
62
|
def cleanup(self) -> None:
|
|
36
63
|
"""Clear all stored data."""
|
|
37
64
|
self.messages.clear()
|
|
38
65
|
self.conversations.clear()
|
|
39
66
|
self.commands.clear()
|
|
67
|
+
self.projects.clear()
|
|
40
68
|
|
|
41
69
|
async def filter_messages(self, query: SessionQuery) -> list[ChatMessage[str]]:
|
|
42
70
|
"""Filter messages from memory."""
|
|
@@ -49,14 +77,7 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
49
77
|
continue
|
|
50
78
|
|
|
51
79
|
# Skip if agent name doesn't match
|
|
52
|
-
if query.agents and not
|
|
53
|
-
msg["name"] in query.agents
|
|
54
|
-
or (
|
|
55
|
-
query.include_forwarded
|
|
56
|
-
and msg["forwarded_from"]
|
|
57
|
-
and any(a in query.agents for a in msg["forwarded_from"])
|
|
58
|
-
)
|
|
59
|
-
):
|
|
80
|
+
if query.agents and msg["name"] not in query.agents:
|
|
60
81
|
continue
|
|
61
82
|
|
|
62
83
|
# Skip if before cutoff time
|
|
@@ -90,7 +111,6 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
90
111
|
model_name=msg["model"],
|
|
91
112
|
cost_info=cost_info,
|
|
92
113
|
response_time=msg["response_time"],
|
|
93
|
-
forwarded_from=msg["forwarded_from"] or [],
|
|
94
114
|
timestamp=msg["timestamp"],
|
|
95
115
|
provider_name=msg["provider_name"],
|
|
96
116
|
provider_response_id=msg["provider_response_id"],
|
|
@@ -116,11 +136,11 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
116
136
|
cost_info: TokenCost | None = None,
|
|
117
137
|
model: str | None = None,
|
|
118
138
|
response_time: float | None = None,
|
|
119
|
-
forwarded_from: list[str] | None = None,
|
|
120
139
|
provider_name: str | None = None,
|
|
121
140
|
provider_response_id: str | None = None,
|
|
122
141
|
messages: str | None = None,
|
|
123
142
|
finish_reason: str | None = None,
|
|
143
|
+
parent_id: str | None = None,
|
|
124
144
|
) -> None:
|
|
125
145
|
"""Store message in memory."""
|
|
126
146
|
if next((i for i in self.messages if i["message_id"] == message_id), None):
|
|
@@ -130,13 +150,13 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
130
150
|
self.messages.append({
|
|
131
151
|
"conversation_id": conversation_id,
|
|
132
152
|
"message_id": message_id,
|
|
153
|
+
"parent_id": parent_id,
|
|
133
154
|
"content": content,
|
|
134
155
|
"role": role,
|
|
135
156
|
"name": name,
|
|
136
157
|
"cost_info": cost_info.token_usage if cost_info else None,
|
|
137
158
|
"model": model,
|
|
138
159
|
"response_time": response_time,
|
|
139
|
-
"forwarded_from": forwarded_from,
|
|
140
160
|
"provider_name": provider_name,
|
|
141
161
|
"provider_response_id": provider_response_id,
|
|
142
162
|
"messages": messages,
|
|
@@ -183,6 +203,111 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
183
203
|
return conv.get("title")
|
|
184
204
|
return None
|
|
185
205
|
|
|
206
|
+
async def get_conversation_messages(
|
|
207
|
+
self,
|
|
208
|
+
conversation_id: str,
|
|
209
|
+
*,
|
|
210
|
+
include_ancestors: bool = False,
|
|
211
|
+
) -> list[ChatMessage[str]]:
|
|
212
|
+
"""Get all messages for a conversation."""
|
|
213
|
+
messages: list[ChatMessage[str]] = []
|
|
214
|
+
for msg in self.messages:
|
|
215
|
+
if msg.get("conversation_id") != conversation_id:
|
|
216
|
+
continue
|
|
217
|
+
messages.append(_dict_to_chat_message(msg))
|
|
218
|
+
|
|
219
|
+
# Sort by timestamp
|
|
220
|
+
messages.sort(key=lambda m: m.timestamp or get_now())
|
|
221
|
+
|
|
222
|
+
if not include_ancestors or not messages:
|
|
223
|
+
return messages
|
|
224
|
+
|
|
225
|
+
# Get ancestor chain if first message has parent_id
|
|
226
|
+
first_msg = messages[0]
|
|
227
|
+
if first_msg.parent_id:
|
|
228
|
+
ancestors = await self.get_message_ancestry(first_msg.parent_id)
|
|
229
|
+
return ancestors + messages
|
|
230
|
+
|
|
231
|
+
return messages
|
|
232
|
+
|
|
233
|
+
async def get_message(self, message_id: str) -> ChatMessage[str] | None:
|
|
234
|
+
"""Get a single message by ID."""
|
|
235
|
+
for msg in self.messages:
|
|
236
|
+
if msg.get("message_id") == message_id:
|
|
237
|
+
return _dict_to_chat_message(msg)
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
async def get_message_ancestry(self, message_id: str) -> list[ChatMessage[str]]:
|
|
241
|
+
"""Get the ancestry chain of a message."""
|
|
242
|
+
ancestors: list[ChatMessage[str]] = []
|
|
243
|
+
current_id: str | None = message_id
|
|
244
|
+
|
|
245
|
+
while current_id:
|
|
246
|
+
msg = await self.get_message(current_id)
|
|
247
|
+
if not msg:
|
|
248
|
+
break
|
|
249
|
+
ancestors.append(msg)
|
|
250
|
+
current_id = msg.parent_id
|
|
251
|
+
|
|
252
|
+
# Reverse to get oldest first
|
|
253
|
+
ancestors.reverse()
|
|
254
|
+
return ancestors
|
|
255
|
+
|
|
256
|
+
async def fork_conversation(
|
|
257
|
+
self,
|
|
258
|
+
*,
|
|
259
|
+
source_conversation_id: str,
|
|
260
|
+
new_conversation_id: str,
|
|
261
|
+
fork_from_message_id: str | None = None,
|
|
262
|
+
new_agent_name: str | None = None,
|
|
263
|
+
) -> str | None:
|
|
264
|
+
"""Fork a conversation at a specific point."""
|
|
265
|
+
# Find source conversation
|
|
266
|
+
source_conv = next(
|
|
267
|
+
(c for c in self.conversations if c["id"] == source_conversation_id), None
|
|
268
|
+
)
|
|
269
|
+
if not source_conv:
|
|
270
|
+
msg = f"Source conversation not found: {source_conversation_id}"
|
|
271
|
+
raise ValueError(msg)
|
|
272
|
+
|
|
273
|
+
# Determine fork point
|
|
274
|
+
fork_point_id: str | None = None
|
|
275
|
+
if fork_from_message_id:
|
|
276
|
+
# Verify message exists in source conversation
|
|
277
|
+
msg_exists = any(
|
|
278
|
+
m.get("message_id") == fork_from_message_id
|
|
279
|
+
and m["conversation_id"] == source_conversation_id
|
|
280
|
+
for m in self.messages
|
|
281
|
+
)
|
|
282
|
+
if not msg_exists:
|
|
283
|
+
err = f"Message {fork_from_message_id} not found in conversation"
|
|
284
|
+
raise ValueError(err)
|
|
285
|
+
fork_point_id = fork_from_message_id
|
|
286
|
+
else:
|
|
287
|
+
# Find last message in source conversation
|
|
288
|
+
conv_messages = [
|
|
289
|
+
m for m in self.messages if m["conversation_id"] == source_conversation_id
|
|
290
|
+
]
|
|
291
|
+
if conv_messages:
|
|
292
|
+
conv_messages.sort(key=lambda m: m.get("timestamp") or get_now())
|
|
293
|
+
fork_point_id = conv_messages[-1].get("message_id")
|
|
294
|
+
|
|
295
|
+
# Create new conversation
|
|
296
|
+
agent_name = new_agent_name or source_conv["agent_name"]
|
|
297
|
+
title = (
|
|
298
|
+
f"{source_conv.get('title') or 'Conversation'} (fork)"
|
|
299
|
+
if source_conv.get("title")
|
|
300
|
+
else None
|
|
301
|
+
)
|
|
302
|
+
self.conversations.append({
|
|
303
|
+
"id": new_conversation_id,
|
|
304
|
+
"agent_name": agent_name,
|
|
305
|
+
"title": title,
|
|
306
|
+
"start_time": get_now(),
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
return fork_point_id
|
|
310
|
+
|
|
186
311
|
async def log_command(
|
|
187
312
|
self,
|
|
188
313
|
*,
|
|
@@ -261,7 +386,6 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
261
386
|
model_name=msg["model"],
|
|
262
387
|
cost_info=cost_info,
|
|
263
388
|
response_time=msg["response_time"],
|
|
264
|
-
forwarded_from=msg["forwarded_from"],
|
|
265
389
|
timestamp=msg["timestamp"],
|
|
266
390
|
)
|
|
267
391
|
conv_messages.append(chat_msg)
|
|
@@ -394,3 +518,60 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
394
518
|
msg_count = len(self.messages)
|
|
395
519
|
|
|
396
520
|
return conv_count, msg_count
|
|
521
|
+
|
|
522
|
+
async def delete_conversation_messages(
|
|
523
|
+
self,
|
|
524
|
+
conversation_id: str,
|
|
525
|
+
) -> int:
|
|
526
|
+
"""Delete all messages for a conversation."""
|
|
527
|
+
original_count = len(self.messages)
|
|
528
|
+
self.messages = [m for m in self.messages if m["conversation_id"] != conversation_id]
|
|
529
|
+
return original_count - len(self.messages)
|
|
530
|
+
|
|
531
|
+
# Project methods
|
|
532
|
+
|
|
533
|
+
async def save_project(self, project: ProjectData) -> None:
|
|
534
|
+
"""Save or update a project."""
|
|
535
|
+
self.projects[project.project_id] = project
|
|
536
|
+
|
|
537
|
+
async def get_project(self, project_id: str) -> ProjectData | None:
|
|
538
|
+
"""Get a project by ID."""
|
|
539
|
+
return self.projects.get(project_id)
|
|
540
|
+
|
|
541
|
+
async def get_project_by_worktree(self, worktree: str) -> ProjectData | None:
|
|
542
|
+
"""Get a project by worktree path."""
|
|
543
|
+
for project in self.projects.values():
|
|
544
|
+
if project.worktree == worktree:
|
|
545
|
+
return project
|
|
546
|
+
return None
|
|
547
|
+
|
|
548
|
+
async def get_project_by_name(self, name: str) -> ProjectData | None:
|
|
549
|
+
"""Get a project by friendly name."""
|
|
550
|
+
for project in self.projects.values():
|
|
551
|
+
if project.name == name:
|
|
552
|
+
return project
|
|
553
|
+
return None
|
|
554
|
+
|
|
555
|
+
async def list_projects(self, limit: int | None = None) -> list[ProjectData]:
|
|
556
|
+
"""List all projects, ordered by last_active descending."""
|
|
557
|
+
projects = sorted(
|
|
558
|
+
self.projects.values(),
|
|
559
|
+
key=lambda p: p.last_active,
|
|
560
|
+
reverse=True,
|
|
561
|
+
)
|
|
562
|
+
if limit is not None:
|
|
563
|
+
projects = projects[:limit]
|
|
564
|
+
return list(projects)
|
|
565
|
+
|
|
566
|
+
async def delete_project(self, project_id: str) -> bool:
|
|
567
|
+
"""Delete a project."""
|
|
568
|
+
if project_id in self.projects:
|
|
569
|
+
del self.projects[project_id]
|
|
570
|
+
return True
|
|
571
|
+
return False
|
|
572
|
+
|
|
573
|
+
async def touch_project(self, project_id: str) -> None:
|
|
574
|
+
"""Update project's last_active timestamp."""
|
|
575
|
+
if project_id in self.projects:
|
|
576
|
+
project = self.projects[project_id]
|
|
577
|
+
self.projects[project_id] = project.touch()
|
agentpool_storage/models.py
CHANGED
|
@@ -36,6 +36,9 @@ class MessageData(TypedDict):
|
|
|
36
36
|
timestamp: str
|
|
37
37
|
"""When the message was sent (ISO format)"""
|
|
38
38
|
|
|
39
|
+
parent_id: str | None
|
|
40
|
+
"""ID of the parent message for tree-structured conversations."""
|
|
41
|
+
|
|
39
42
|
model: str | None
|
|
40
43
|
"""Name of the model that generated this message"""
|
|
41
44
|
|