agentpool 2.2.3__py3-none-any.whl → 2.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- acp/__init__.py +0 -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/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 +18 -49
- 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/task/supervisor.py +2 -2
- acp/utils.py +2 -2
- agentpool/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +278 -263
- agentpool/agents/acp_agent/acp_converters.py +150 -17
- agentpool/agents/acp_agent/client_handler.py +35 -24
- agentpool/agents/acp_agent/session_state.py +14 -6
- agentpool/agents/agent.py +471 -643
- agentpool/agents/agui_agent/agui_agent.py +104 -107
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +485 -32
- 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 +654 -334
- agentpool/agents/claude_code_agent/converters.py +4 -141
- 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/events/__init__.py +22 -0
- agentpool/agents/events/builtin_handlers.py +65 -0
- agentpool/agents/events/event_emitter.py +3 -0
- agentpool/agents/events/events.py +84 -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 +13 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +35 -21
- 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 +9 -8
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +104 -265
- agentpool/delegation/team.py +57 -57
- agentpool/delegation/teamrun.py +50 -55
- agentpool/functional/run.py +10 -4
- agentpool/mcp_server/client.py +73 -38
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +9 -23
- agentpool/mcp_server/registries/official_registry_client.py +10 -1
- agentpool/mcp_server/tool_bridge.py +114 -79
- 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 +87 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +2 -26
- agentpool/messaging/processing.py +10 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -2
- agentpool/models/acp_agents/mcp_capable.py +124 -15
- agentpool/models/acp_agents/non_mcp.py +0 -23
- agentpool/models/agents.py +66 -66
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +111 -17
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +70 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/resource_providers/__init__.py +2 -0
- agentpool/resource_providers/aggregating.py +4 -2
- agentpool/resource_providers/base.py +13 -3
- 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 +66 -12
- agentpool/resource_providers/plan_provider.py +111 -18
- agentpool/resource_providers/pool.py +5 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +2 -2
- agentpool/sessions/__init__.py +2 -0
- agentpool/sessions/manager.py +2 -3
- agentpool/sessions/models.py +9 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/storage/manager.py +361 -54
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +1 -1
- 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/streams.py +21 -96
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +20 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +59 -1
- agentpool_cli/serve_opencode.py +1 -1
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +12 -5
- agentpool_commands/agents.py +1 -1
- 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/utils.py +31 -32
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -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 +1 -5
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +38 -39
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -28
- agentpool_config/toolsets.py +22 -90
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +125 -56
- agentpool_server/acp_server/commands/acp_commands.py +46 -216
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +1 -11
- agentpool_server/acp_server/session.py +90 -410
- 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/ENDPOINTS.md +53 -14
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +0 -8
- agentpool_server/opencode_server/converters.py +132 -26
- agentpool_server/opencode_server/input_provider.py +160 -8
- agentpool_server/opencode_server/models/__init__.py +42 -20
- agentpool_server/opencode_server/models/app.py +12 -0
- agentpool_server/opencode_server/models/events.py +203 -29
- agentpool_server/opencode_server/models/mcp.py +19 -0
- agentpool_server/opencode_server/models/message.py +18 -1
- agentpool_server/opencode_server/models/parts.py +134 -1
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +13 -1
- agentpool_server/opencode_server/routes/__init__.py +4 -0
- agentpool_server/opencode_server/routes/agent_routes.py +33 -2
- agentpool_server/opencode_server/routes/app_routes.py +66 -3
- agentpool_server/opencode_server/routes/config_routes.py +66 -5
- agentpool_server/opencode_server/routes/file_routes.py +184 -5
- agentpool_server/opencode_server/routes/global_routes.py +1 -1
- agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
- agentpool_server/opencode_server/routes/message_routes.py +122 -66
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +23 -22
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +139 -68
- agentpool_server/opencode_server/routes/tui_routes.py +1 -1
- agentpool_server/opencode_server/server.py +47 -2
- agentpool_server/opencode_server/state.py +30 -0
- agentpool_storage/__init__.py +0 -4
- agentpool_storage/base.py +81 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
- agentpool_storage/file_provider.py +149 -15
- agentpool_storage/memory_provider.py +132 -12
- 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/session_store.py +20 -6
- agentpool_storage/sql_provider/sql_provider.py +135 -2
- agentpool_storage/sql_provider/utils.py +2 -12
- 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 -4
- agentpool_toolsets/builtin/code.py +4 -4
- agentpool_toolsets/builtin/debug.py +115 -40
- agentpool_toolsets/builtin/execution_environment.py +54 -165
- agentpool_toolsets/builtin/skills.py +0 -77
- 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/grep.py +25 -5
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +74 -17
- agentpool_toolsets/mcp_run_toolset.py +8 -11
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- 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/opencode_provider.py +0 -730
- agentpool_storage/text_log_provider.py +0 -276
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.2.3.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,7 +19,6 @@ 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
|
|
|
25
24
|
from agentpool.sessions.models import ProjectData
|
|
@@ -42,7 +41,6 @@ class MessageData(TypedDict):
|
|
|
42
41
|
cost: Decimal | None
|
|
43
42
|
token_usage: TokenUsage | None
|
|
44
43
|
response_time: float | None
|
|
45
|
-
forwarded_from: list[str] | None
|
|
46
44
|
provider_name: str | None
|
|
47
45
|
provider_response_id: str | None
|
|
48
46
|
messages: str | None
|
|
@@ -149,14 +147,7 @@ class FileProvider(StorageProvider):
|
|
|
149
147
|
# Apply filters
|
|
150
148
|
if query.name and msg["conversation_id"] != query.name:
|
|
151
149
|
continue
|
|
152
|
-
if query.agents and not
|
|
153
|
-
msg["name"] in query.agents
|
|
154
|
-
or (
|
|
155
|
-
query.include_forwarded
|
|
156
|
-
and msg["forwarded_from"]
|
|
157
|
-
and any(a in query.agents for a in msg["forwarded_from"])
|
|
158
|
-
)
|
|
159
|
-
):
|
|
150
|
+
if query.agents and msg["name"] not in query.agents:
|
|
160
151
|
continue
|
|
161
152
|
cutoff = query.get_time_cutoff()
|
|
162
153
|
timestamp = datetime.fromisoformat(msg["timestamp"])
|
|
@@ -192,7 +183,6 @@ class FileProvider(StorageProvider):
|
|
|
192
183
|
model_name=msg["model"],
|
|
193
184
|
cost_info=cost_info,
|
|
194
185
|
response_time=msg["response_time"],
|
|
195
|
-
forwarded_from=msg["forwarded_from"] or [],
|
|
196
186
|
timestamp=datetime.fromisoformat(msg["timestamp"]),
|
|
197
187
|
provider_name=msg["provider_name"],
|
|
198
188
|
provider_response_id=msg["provider_response_id"],
|
|
@@ -217,7 +207,6 @@ class FileProvider(StorageProvider):
|
|
|
217
207
|
cost_info: TokenCost | None = None,
|
|
218
208
|
model: str | None = None,
|
|
219
209
|
response_time: float | None = None,
|
|
220
|
-
forwarded_from: list[str] | None = None,
|
|
221
210
|
provider_name: str | None = None,
|
|
222
211
|
provider_response_id: str | None = None,
|
|
223
212
|
messages: str | None = None,
|
|
@@ -240,7 +229,6 @@ class FileProvider(StorageProvider):
|
|
|
240
229
|
total=cost_info.token_usage.total_tokens if cost_info else 0,
|
|
241
230
|
),
|
|
242
231
|
"response_time": response_time,
|
|
243
|
-
"forwarded_from": forwarded_from,
|
|
244
232
|
"provider_name": provider_name,
|
|
245
233
|
"provider_response_id": provider_response_id,
|
|
246
234
|
"messages": messages,
|
|
@@ -288,6 +276,152 @@ class FileProvider(StorageProvider):
|
|
|
288
276
|
return conv.get("title")
|
|
289
277
|
return None
|
|
290
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
|
+
|
|
291
425
|
async def log_command(
|
|
292
426
|
self,
|
|
293
427
|
*,
|
|
@@ -22,6 +22,31 @@ if TYPE_CHECKING:
|
|
|
22
22
|
from agentpool_storage.models import MessageData, QueryFilters, StatsFilters, TokenUsage
|
|
23
23
|
|
|
24
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
|
+
|
|
25
50
|
class MemoryStorageProvider(StorageProvider):
|
|
26
51
|
"""In-memory storage provider for testing."""
|
|
27
52
|
|
|
@@ -52,14 +77,7 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
52
77
|
continue
|
|
53
78
|
|
|
54
79
|
# Skip if agent name doesn't match
|
|
55
|
-
if query.agents and not
|
|
56
|
-
msg["name"] in query.agents
|
|
57
|
-
or (
|
|
58
|
-
query.include_forwarded
|
|
59
|
-
and msg["forwarded_from"]
|
|
60
|
-
and any(a in query.agents for a in msg["forwarded_from"])
|
|
61
|
-
)
|
|
62
|
-
):
|
|
80
|
+
if query.agents and msg["name"] not in query.agents:
|
|
63
81
|
continue
|
|
64
82
|
|
|
65
83
|
# Skip if before cutoff time
|
|
@@ -93,7 +111,6 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
93
111
|
model_name=msg["model"],
|
|
94
112
|
cost_info=cost_info,
|
|
95
113
|
response_time=msg["response_time"],
|
|
96
|
-
forwarded_from=msg["forwarded_from"] or [],
|
|
97
114
|
timestamp=msg["timestamp"],
|
|
98
115
|
provider_name=msg["provider_name"],
|
|
99
116
|
provider_response_id=msg["provider_response_id"],
|
|
@@ -119,7 +136,6 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
119
136
|
cost_info: TokenCost | None = None,
|
|
120
137
|
model: str | None = None,
|
|
121
138
|
response_time: float | None = None,
|
|
122
|
-
forwarded_from: list[str] | None = None,
|
|
123
139
|
provider_name: str | None = None,
|
|
124
140
|
provider_response_id: str | None = None,
|
|
125
141
|
messages: str | None = None,
|
|
@@ -134,13 +150,13 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
134
150
|
self.messages.append({
|
|
135
151
|
"conversation_id": conversation_id,
|
|
136
152
|
"message_id": message_id,
|
|
153
|
+
"parent_id": parent_id,
|
|
137
154
|
"content": content,
|
|
138
155
|
"role": role,
|
|
139
156
|
"name": name,
|
|
140
157
|
"cost_info": cost_info.token_usage if cost_info else None,
|
|
141
158
|
"model": model,
|
|
142
159
|
"response_time": response_time,
|
|
143
|
-
"forwarded_from": forwarded_from,
|
|
144
160
|
"provider_name": provider_name,
|
|
145
161
|
"provider_response_id": provider_response_id,
|
|
146
162
|
"messages": messages,
|
|
@@ -187,6 +203,111 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
187
203
|
return conv.get("title")
|
|
188
204
|
return None
|
|
189
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
|
+
|
|
190
311
|
async def log_command(
|
|
191
312
|
self,
|
|
192
313
|
*,
|
|
@@ -265,7 +386,6 @@ class MemoryStorageProvider(StorageProvider):
|
|
|
265
386
|
model_name=msg["model"],
|
|
266
387
|
cost_info=cost_info,
|
|
267
388
|
response_time=msg["response_time"],
|
|
268
|
-
forwarded_from=msg["forwarded_from"],
|
|
269
389
|
timestamp=msg["timestamp"],
|
|
270
390
|
)
|
|
271
391
|
conv_messages.append(chat_msg)
|