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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import contextlib
|
|
6
7
|
from datetime import UTC, datetime
|
|
7
8
|
from typing import TYPE_CHECKING, Any, Literal
|
|
@@ -16,9 +17,9 @@ from agentpool.utils import identifiers as identifier
|
|
|
16
17
|
from agentpool_config.session import SessionQuery
|
|
17
18
|
from agentpool_server.opencode_server.command_validation import validate_command
|
|
18
19
|
from agentpool_server.opencode_server.converters import chat_message_to_opencode
|
|
19
|
-
from agentpool_server.opencode_server.dependencies import StateDep
|
|
20
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
20
21
|
from agentpool_server.opencode_server.input_provider import OpenCodeInputProvider
|
|
21
|
-
from agentpool_server.opencode_server.models import (
|
|
22
|
+
from agentpool_server.opencode_server.models import (
|
|
22
23
|
AssistantMessage,
|
|
23
24
|
CommandRequest,
|
|
24
25
|
MessagePath,
|
|
@@ -63,8 +64,16 @@ if TYPE_CHECKING:
|
|
|
63
64
|
# =============================================================================
|
|
64
65
|
|
|
65
66
|
|
|
66
|
-
def session_data_to_opencode(
|
|
67
|
-
|
|
67
|
+
def session_data_to_opencode(
|
|
68
|
+
data: SessionData,
|
|
69
|
+
title: str | None = None,
|
|
70
|
+
) -> Session:
|
|
71
|
+
"""Convert SessionData to OpenCode Session model.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
data: SessionData to convert
|
|
75
|
+
title: Optional title (fetched from storage by caller)
|
|
76
|
+
"""
|
|
68
77
|
# Convert datetime to milliseconds timestamp
|
|
69
78
|
created_ms = int(data.created_at.timestamp() * 1000)
|
|
70
79
|
updated_ms = int(data.last_active.timestamp() * 1000)
|
|
@@ -81,7 +90,7 @@ def session_data_to_opencode(data: SessionData) -> Session:
|
|
|
81
90
|
id=data.session_id,
|
|
82
91
|
project_id=data.project_id or "default",
|
|
83
92
|
directory=data.cwd or "",
|
|
84
|
-
title=
|
|
93
|
+
title=title or "New Session",
|
|
85
94
|
version=data.version,
|
|
86
95
|
time=TimeCreatedUpdated(created=created_ms, updated=updated_ms),
|
|
87
96
|
parent_id=data.parent_id,
|
|
@@ -110,7 +119,6 @@ def opencode_to_session_data(
|
|
|
110
119
|
session_id=session.id,
|
|
111
120
|
agent_name=agent_name,
|
|
112
121
|
conversation_id=session.id, # Use session_id as conversation_id
|
|
113
|
-
title=session.title,
|
|
114
122
|
pool_id=pool_id,
|
|
115
123
|
project_id=session.project_id,
|
|
116
124
|
parent_id=session.parent_id,
|
|
@@ -173,7 +181,11 @@ async def get_or_load_session(state: ServerState, session_id: str) -> Session |
|
|
|
173
181
|
# Try to load from storage
|
|
174
182
|
data = await state.pool.sessions.store.load(session_id)
|
|
175
183
|
if data is not None:
|
|
176
|
-
|
|
184
|
+
# Fetch title from conversation storage
|
|
185
|
+
title = None
|
|
186
|
+
if state.pool.storage:
|
|
187
|
+
title = await state.pool.storage.get_conversation_title(data.conversation_id)
|
|
188
|
+
session = session_data_to_opencode(data, title=title)
|
|
177
189
|
# Cache it
|
|
178
190
|
state.sessions[session_id] = session
|
|
179
191
|
# Initialize runtime state
|
|
@@ -311,12 +323,23 @@ async def delete_session(session_id: str, state: StateDep) -> bool:
|
|
|
311
323
|
|
|
312
324
|
@router.post("/{session_id}/abort")
|
|
313
325
|
async def abort_session(session_id: str, state: StateDep) -> bool:
|
|
314
|
-
"""Abort a running session."""
|
|
326
|
+
"""Abort a running session by interrupting the agent."""
|
|
315
327
|
session = await get_or_load_session(state, session_id)
|
|
316
328
|
if session is None:
|
|
317
329
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
318
|
-
|
|
330
|
+
|
|
331
|
+
# Interrupt the agent to cancel any ongoing stream
|
|
332
|
+
try:
|
|
333
|
+
await state.agent.interrupt()
|
|
334
|
+
# Give a moment for the cancellation to propagate
|
|
335
|
+
await asyncio.sleep(0.1)
|
|
336
|
+
except Exception: # noqa: BLE001
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
# Update and broadcast session status to notify clients
|
|
319
340
|
state.session_status[session_id] = SessionStatus(type="idle")
|
|
341
|
+
await state.broadcast_event(SessionStatusEvent.create(session_id, SessionStatus(type="idle")))
|
|
342
|
+
|
|
320
343
|
return True
|
|
321
344
|
|
|
322
345
|
|
|
@@ -650,7 +673,7 @@ async def run_shell_command(
|
|
|
650
673
|
class PermissionResponse(OpenCodeBaseModel):
|
|
651
674
|
"""Request body for responding to a permission request."""
|
|
652
675
|
|
|
653
|
-
|
|
676
|
+
reply: Literal["once", "always", "reject"]
|
|
654
677
|
|
|
655
678
|
|
|
656
679
|
@router.get("/{session_id}/permissions")
|
|
@@ -675,7 +698,7 @@ async def get_pending_permissions(session_id: str, state: StateDep) -> list[dict
|
|
|
675
698
|
async def respond_to_permission(
|
|
676
699
|
session_id: str,
|
|
677
700
|
permission_id: str,
|
|
678
|
-
|
|
701
|
+
body: PermissionResponse,
|
|
679
702
|
state: StateDep,
|
|
680
703
|
) -> bool:
|
|
681
704
|
"""Respond to a pending permission request.
|
|
@@ -695,7 +718,7 @@ async def respond_to_permission(
|
|
|
695
718
|
raise HTTPException(status_code=404, detail="No input provider for session")
|
|
696
719
|
|
|
697
720
|
# Resolve the permission
|
|
698
|
-
resolved = input_provider.resolve_permission(permission_id,
|
|
721
|
+
resolved = input_provider.resolve_permission(permission_id, body.reply)
|
|
699
722
|
if not resolved:
|
|
700
723
|
raise HTTPException(status_code=404, detail="Permission not found or already resolved")
|
|
701
724
|
|
|
@@ -703,7 +726,7 @@ async def respond_to_permission(
|
|
|
703
726
|
PermissionResolvedEvent.create(
|
|
704
727
|
session_id=session_id,
|
|
705
728
|
request_id=permission_id,
|
|
706
|
-
reply=
|
|
729
|
+
reply=body.reply,
|
|
707
730
|
)
|
|
708
731
|
)
|
|
709
732
|
|
|
@@ -982,49 +1005,78 @@ class RevertRequest(OpenCodeBaseModel):
|
|
|
982
1005
|
|
|
983
1006
|
@router.post("/{session_id}/revert")
|
|
984
1007
|
async def revert_session(session_id: str, request: RevertRequest, state: StateDep) -> Session:
|
|
985
|
-
"""Revert file changes from a specific message.
|
|
1008
|
+
"""Revert file changes and messages from a specific message.
|
|
986
1009
|
|
|
987
|
-
|
|
1010
|
+
Removes messages from the revert point onwards and restores files to their
|
|
1011
|
+
state before the specified message's changes.
|
|
988
1012
|
"""
|
|
1013
|
+
from agentpool_server.opencode_server.models import MessageRemovedEvent, PartRemovedEvent
|
|
1014
|
+
|
|
989
1015
|
session = await get_or_load_session(state, session_id)
|
|
990
1016
|
if session is None:
|
|
991
1017
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
1018
|
+
|
|
1019
|
+
# Get messages for this session
|
|
1020
|
+
messages = state.messages.get(session_id, [])
|
|
1021
|
+
if not messages:
|
|
1022
|
+
raise HTTPException(status_code=400, detail="No messages to revert")
|
|
1023
|
+
|
|
1024
|
+
# Find the revert message index
|
|
1025
|
+
revert_index = None
|
|
1026
|
+
for i, msg in enumerate(messages):
|
|
1027
|
+
if msg.info.id == request.message_id:
|
|
1028
|
+
revert_index = i
|
|
1029
|
+
break
|
|
1030
|
+
|
|
1031
|
+
if revert_index is None:
|
|
1032
|
+
raise HTTPException(status_code=404, detail=f"Message {request.message_id} not found")
|
|
1033
|
+
|
|
1034
|
+
# Split messages: keep messages before revert point, remove from revert point onwards
|
|
1035
|
+
messages_to_keep = messages[:revert_index]
|
|
1036
|
+
messages_to_remove = messages[revert_index:]
|
|
1037
|
+
|
|
1038
|
+
if not messages_to_remove:
|
|
1039
|
+
raise HTTPException(status_code=400, detail="No messages to revert")
|
|
1040
|
+
|
|
1041
|
+
# Store removed messages for unrevert
|
|
1042
|
+
state.reverted_messages[session_id] = messages_to_remove
|
|
1043
|
+
|
|
1044
|
+
# Update message list - keep only messages before revert point
|
|
1045
|
+
state.messages[session_id] = messages_to_keep
|
|
1046
|
+
|
|
1047
|
+
# Emit message.removed and part.removed events for all removed messages
|
|
1048
|
+
for msg in messages_to_remove:
|
|
1049
|
+
# Emit message.removed event
|
|
1050
|
+
await state.broadcast_event(MessageRemovedEvent.create(session_id, msg.info.id))
|
|
1051
|
+
|
|
1052
|
+
# Emit part.removed events for all parts
|
|
1053
|
+
for part in msg.parts:
|
|
1054
|
+
await state.broadcast_event(PartRemovedEvent.create(session_id, msg.info.id, part.id))
|
|
1055
|
+
|
|
1056
|
+
# Also revert file changes if any
|
|
992
1057
|
file_ops = state.pool.file_ops
|
|
993
|
-
if
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1058
|
+
if file_ops.changes:
|
|
1059
|
+
revert_ops = file_ops.get_revert_operations(since_message_id=request.message_id)
|
|
1060
|
+
if revert_ops:
|
|
1061
|
+
fs = state.agent.env.get_fs()
|
|
1062
|
+
for path, content in revert_ops:
|
|
1063
|
+
try:
|
|
1064
|
+
if content is None:
|
|
1065
|
+
await fs._rm_file(path)
|
|
1066
|
+
else:
|
|
1067
|
+
content_bytes = content.encode("utf-8")
|
|
1068
|
+
await fs._pipe_file(path, content_bytes)
|
|
1069
|
+
except Exception as e:
|
|
1070
|
+
detail = f"Failed to revert {path}: {e}"
|
|
1071
|
+
raise HTTPException(status_code=500, detail=detail) from e
|
|
1072
|
+
file_ops.remove_changes_since_message(request.message_id)
|
|
997
1073
|
|
|
998
|
-
if not revert_ops:
|
|
999
|
-
detail = f"No changes found for message {request.message_id}"
|
|
1000
|
-
raise HTTPException(status_code=404, detail=detail)
|
|
1001
|
-
# Get filesystem from the agent's environment
|
|
1002
|
-
fs = state.agent.env.get_fs()
|
|
1003
|
-
# Apply reverts using the filesystem
|
|
1004
|
-
# TODO: Currently write operations only track "existed vs created", not full old content.
|
|
1005
|
-
# Files that existed before a write will be restored as empty, not their original content.
|
|
1006
|
-
reverted_paths = []
|
|
1007
|
-
for path, content in revert_ops:
|
|
1008
|
-
try:
|
|
1009
|
-
if content is None:
|
|
1010
|
-
# File was created (old_text=None), delete it
|
|
1011
|
-
await fs._rm_file(path)
|
|
1012
|
-
else:
|
|
1013
|
-
# Restore original content
|
|
1014
|
-
content_bytes = content.encode("utf-8")
|
|
1015
|
-
await fs._pipe_file(path, content_bytes)
|
|
1016
|
-
reverted_paths.append(path)
|
|
1017
|
-
except Exception as e:
|
|
1018
|
-
raise HTTPException(status_code=500, detail=f"Failed to revert {path}: {e}") from e
|
|
1019
|
-
|
|
1020
|
-
# Remove the reverted changes from the tracker
|
|
1021
|
-
file_ops.remove_changes_since_message(request.message_id)
|
|
1022
1074
|
# Update session with revert info
|
|
1023
1075
|
session = state.sessions[session_id]
|
|
1024
|
-
# TODO: include the diff?
|
|
1025
1076
|
revert_info = SessionRevert(message_id=request.message_id, part_id=request.part_id)
|
|
1026
1077
|
updated_session = session.model_copy(update={"revert": revert_info})
|
|
1027
1078
|
state.sessions[session_id] = updated_session
|
|
1079
|
+
|
|
1028
1080
|
# Broadcast session update
|
|
1029
1081
|
await state.broadcast_event(SessionUpdatedEvent.create(updated_session))
|
|
1030
1082
|
return updated_session
|
|
@@ -1032,36 +1084,55 @@ async def revert_session(session_id: str, request: RevertRequest, state: StateDe
|
|
|
1032
1084
|
|
|
1033
1085
|
@router.post("/{session_id}/unrevert")
|
|
1034
1086
|
async def unrevert_session(session_id: str, state: StateDep) -> Session:
|
|
1035
|
-
"""Restore all reverted file changes.
|
|
1087
|
+
"""Restore all reverted messages and file changes.
|
|
1036
1088
|
|
|
1037
|
-
Re-applies the changes that were previously reverted.
|
|
1089
|
+
Re-applies the messages and changes that were previously reverted.
|
|
1038
1090
|
"""
|
|
1091
|
+
from agentpool_server.opencode_server.models import MessageUpdatedEvent, PartUpdatedEvent
|
|
1092
|
+
|
|
1039
1093
|
session = await get_or_load_session(state, session_id)
|
|
1040
1094
|
if session is None:
|
|
1041
1095
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
1096
|
+
|
|
1097
|
+
# Restore reverted messages
|
|
1098
|
+
reverted_messages = state.reverted_messages.get(session_id, [])
|
|
1099
|
+
if not reverted_messages:
|
|
1100
|
+
raise HTTPException(status_code=400, detail="No reverted messages to restore")
|
|
1101
|
+
|
|
1102
|
+
# Restore messages to conversation
|
|
1103
|
+
if session_id not in state.messages:
|
|
1104
|
+
state.messages[session_id] = []
|
|
1105
|
+
state.messages[session_id].extend(reverted_messages)
|
|
1106
|
+
|
|
1107
|
+
# Emit message.updated and part.updated events for restored messages
|
|
1108
|
+
for msg in reverted_messages:
|
|
1109
|
+
# Emit message.updated event
|
|
1110
|
+
await state.broadcast_event(MessageUpdatedEvent.create(msg.info))
|
|
1111
|
+
|
|
1112
|
+
# Emit part.updated events for all parts
|
|
1113
|
+
for part in msg.parts:
|
|
1114
|
+
await state.broadcast_event(PartUpdatedEvent.create(part))
|
|
1115
|
+
|
|
1116
|
+
# Clear reverted messages
|
|
1117
|
+
state.reverted_messages.pop(session_id, None)
|
|
1118
|
+
|
|
1119
|
+
# Also unrevert file changes if any
|
|
1042
1120
|
file_ops = state.pool.file_ops
|
|
1043
|
-
if
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
await fs._pipe_file(path, content_bytes)
|
|
1059
|
-
except Exception as e:
|
|
1060
|
-
detail = f"Failed to unrevert {path}: {e}"
|
|
1061
|
-
raise HTTPException(status_code=500, detail=detail) from e
|
|
1062
|
-
|
|
1063
|
-
# Restore the changes to the tracker
|
|
1064
|
-
file_ops.restore_reverted_changes()
|
|
1121
|
+
if file_ops.reverted_changes:
|
|
1122
|
+
unrevert_ops = file_ops.get_unrevert_operations()
|
|
1123
|
+
fs = state.agent.env.get_fs()
|
|
1124
|
+
for path, content in unrevert_ops:
|
|
1125
|
+
try:
|
|
1126
|
+
if content is None:
|
|
1127
|
+
await fs._rm_file(path)
|
|
1128
|
+
else:
|
|
1129
|
+
content_bytes = content.encode("utf-8")
|
|
1130
|
+
await fs._pipe_file(path, content_bytes)
|
|
1131
|
+
except Exception as e:
|
|
1132
|
+
detail = f"Failed to unrevert {path}: {e}"
|
|
1133
|
+
raise HTTPException(status_code=500, detail=detail) from e
|
|
1134
|
+
file_ops.restore_reverted_changes()
|
|
1135
|
+
|
|
1065
1136
|
# Clear revert info from session
|
|
1066
1137
|
updated_session = session.model_copy(update={"revert": None})
|
|
1067
1138
|
state.sessions[session_id] = updated_session
|
|
@@ -11,7 +11,7 @@ from typing import Literal
|
|
|
11
11
|
from fastapi import APIRouter
|
|
12
12
|
from pydantic import BaseModel, Field
|
|
13
13
|
|
|
14
|
-
from agentpool_server.opencode_server.dependencies import StateDep
|
|
14
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
15
15
|
from agentpool_server.opencode_server.models.events import (
|
|
16
16
|
TuiCommandExecuteEvent,
|
|
17
17
|
TuiPromptAppendEvent,
|
|
@@ -11,7 +11,6 @@ from pathlib import Path
|
|
|
11
11
|
from typing import TYPE_CHECKING, Any
|
|
12
12
|
|
|
13
13
|
from fastapi import FastAPI, Request # noqa: TC002
|
|
14
|
-
from fastapi.encoders import jsonable_encoder
|
|
15
14
|
from fastapi.exceptions import RequestValidationError
|
|
16
15
|
from fastapi.middleware.cors import CORSMiddleware
|
|
17
16
|
from fastapi.responses import JSONResponse, RedirectResponse, Response
|
|
@@ -25,7 +24,9 @@ from agentpool_server.opencode_server.routes import (
|
|
|
25
24
|
global_router,
|
|
26
25
|
lsp_router,
|
|
27
26
|
message_router,
|
|
27
|
+
permission_router,
|
|
28
28
|
pty_router,
|
|
29
|
+
question_router,
|
|
29
30
|
session_router,
|
|
30
31
|
tui_router,
|
|
31
32
|
)
|
|
@@ -36,12 +37,16 @@ class OpenCodeJSONResponse(JSONResponse):
|
|
|
36
37
|
"""Custom JSON response that excludes None values (like OpenCode does)."""
|
|
37
38
|
|
|
38
39
|
def render(self, content: Any) -> bytes:
|
|
40
|
+
from fastapi.encoders import jsonable_encoder
|
|
41
|
+
|
|
39
42
|
return super().render(jsonable_encoder(content, exclude_none=True))
|
|
40
43
|
|
|
41
44
|
|
|
42
45
|
if TYPE_CHECKING:
|
|
43
46
|
from collections.abc import AsyncIterator, Set as AbstractSet
|
|
44
47
|
|
|
48
|
+
from agentpool.storage.manager import TitleGeneratedEvent
|
|
49
|
+
|
|
45
50
|
|
|
46
51
|
VERSION = "0.1.0"
|
|
47
52
|
|
|
@@ -146,6 +151,42 @@ def create_app( # noqa: PLR0915
|
|
|
146
151
|
|
|
147
152
|
pool.todos.on_change = on_todo_change
|
|
148
153
|
|
|
154
|
+
# Set up title generation callback to update OpenCode sessions
|
|
155
|
+
|
|
156
|
+
async def on_title_generated(event: TitleGeneratedEvent) -> None:
|
|
157
|
+
"""Update session when title is generated by StorageManager."""
|
|
158
|
+
import logging
|
|
159
|
+
|
|
160
|
+
from agentpool_server.opencode_server.models.events import SessionUpdatedEvent
|
|
161
|
+
from agentpool_server.opencode_server.routes.session_routes import opencode_to_session_data
|
|
162
|
+
|
|
163
|
+
log = logging.getLogger(__name__)
|
|
164
|
+
log.info("on_title_generated called: %s, title=%s", event.conversation_id, event.title)
|
|
165
|
+
|
|
166
|
+
session_id = event.conversation_id
|
|
167
|
+
if session_id in state.sessions:
|
|
168
|
+
# Update in-memory session
|
|
169
|
+
session = state.sessions[session_id]
|
|
170
|
+
updated_session = session.model_copy(update={"title": event.title})
|
|
171
|
+
state.sessions[session_id] = updated_session
|
|
172
|
+
|
|
173
|
+
# Persist to storage
|
|
174
|
+
session_data = opencode_to_session_data(
|
|
175
|
+
updated_session,
|
|
176
|
+
agent_name=state.agent.name,
|
|
177
|
+
pool_id=state.pool.manifest.config_file_path,
|
|
178
|
+
)
|
|
179
|
+
await state.pool.sessions.store.save(session_data)
|
|
180
|
+
|
|
181
|
+
# Broadcast session update to UI
|
|
182
|
+
await state.broadcast_event(SessionUpdatedEvent.create(updated_session))
|
|
183
|
+
else:
|
|
184
|
+
log.warning("Session %s not found in state.sessions", session_id)
|
|
185
|
+
|
|
186
|
+
# Connect to storage manager's title_generated signal
|
|
187
|
+
if pool.storage:
|
|
188
|
+
pool.storage.title_generated.connect(on_title_generated)
|
|
189
|
+
|
|
149
190
|
# Watchers for VCS and file events
|
|
150
191
|
git_branch_watcher: Any = None
|
|
151
192
|
project_file_watcher: Any = None
|
|
@@ -228,7 +269,9 @@ def create_app( # noqa: PLR0915
|
|
|
228
269
|
# Register callback to run when first SSE client connects
|
|
229
270
|
state.on_first_subscriber = check_for_updates
|
|
230
271
|
|
|
231
|
-
|
|
272
|
+
# Enter pool context to initialize session store and other components
|
|
273
|
+
async with pool:
|
|
274
|
+
yield
|
|
232
275
|
|
|
233
276
|
# Shutdown - clean up
|
|
234
277
|
pool.todos.on_change = None
|
|
@@ -281,6 +324,8 @@ def create_app( # noqa: PLR0915
|
|
|
281
324
|
app.include_router(message_router)
|
|
282
325
|
app.include_router(file_router)
|
|
283
326
|
app.include_router(agent_router)
|
|
327
|
+
app.include_router(permission_router)
|
|
328
|
+
app.include_router(question_router)
|
|
284
329
|
app.include_router(pty_router)
|
|
285
330
|
app.include_router(tui_router)
|
|
286
331
|
app.include_router(lsp_router)
|
|
@@ -16,8 +16,10 @@ if TYPE_CHECKING:
|
|
|
16
16
|
from agentpool.diagnostics.lsp_manager import LSPManager
|
|
17
17
|
from agentpool_server.opencode_server.input_provider import OpenCodeInputProvider
|
|
18
18
|
from agentpool_server.opencode_server.models import (
|
|
19
|
+
Config,
|
|
19
20
|
Event,
|
|
20
21
|
MessageWithParts,
|
|
22
|
+
QuestionInfo,
|
|
21
23
|
Session,
|
|
22
24
|
SessionStatus,
|
|
23
25
|
Todo,
|
|
@@ -27,6 +29,23 @@ if TYPE_CHECKING:
|
|
|
27
29
|
OnFirstSubscriberCallback = Callable[[], Coroutine[Any, Any, None]]
|
|
28
30
|
|
|
29
31
|
|
|
32
|
+
@dataclass
|
|
33
|
+
class PendingQuestion:
|
|
34
|
+
"""Pending question awaiting user response."""
|
|
35
|
+
|
|
36
|
+
session_id: str
|
|
37
|
+
"""Session that owns this question."""
|
|
38
|
+
|
|
39
|
+
questions: list[QuestionInfo]
|
|
40
|
+
"""Questions to ask."""
|
|
41
|
+
|
|
42
|
+
future: asyncio.Future[list[list[str]]]
|
|
43
|
+
"""Future that resolves when user answers."""
|
|
44
|
+
|
|
45
|
+
tool: dict[str, str] | None = None
|
|
46
|
+
"""Optional tool context: {message_id, call_id}."""
|
|
47
|
+
|
|
48
|
+
|
|
30
49
|
@dataclass
|
|
31
50
|
class ServerState:
|
|
32
51
|
"""Shared state for the OpenCode server.
|
|
@@ -40,6 +59,10 @@ class ServerState:
|
|
|
40
59
|
agent: BaseAgent[Any, Any]
|
|
41
60
|
start_time: float = field(default_factory=time.time)
|
|
42
61
|
|
|
62
|
+
# Configuration (mutable runtime config)
|
|
63
|
+
# Initialized after state creation
|
|
64
|
+
config: Config | None = None
|
|
65
|
+
|
|
43
66
|
# Active sessions cache (session_id -> OpenCode Session model)
|
|
44
67
|
# This is a cache of sessions loaded from pool.sessions
|
|
45
68
|
sessions: dict[str, Session] = field(default_factory=dict)
|
|
@@ -49,6 +72,10 @@ class ServerState:
|
|
|
49
72
|
# Runtime cache - messages are also persisted via pool.storage
|
|
50
73
|
messages: dict[str, list[MessageWithParts]] = field(default_factory=dict)
|
|
51
74
|
|
|
75
|
+
# Reverted messages storage (session_id -> removed messages)
|
|
76
|
+
# Stores messages removed during revert for unrevert operation
|
|
77
|
+
reverted_messages: dict[str, list[MessageWithParts]] = field(default_factory=dict)
|
|
78
|
+
|
|
52
79
|
# Todo storage (session_id -> todos)
|
|
53
80
|
# Uses pool.todos for persistence
|
|
54
81
|
todos: dict[str, list[Todo]] = field(default_factory=dict)
|
|
@@ -56,6 +83,9 @@ class ServerState:
|
|
|
56
83
|
# Input providers for permission handling (session_id -> provider)
|
|
57
84
|
input_providers: dict[str, OpenCodeInputProvider] = field(default_factory=dict)
|
|
58
85
|
|
|
86
|
+
# Question storage (question_id -> pending question info)
|
|
87
|
+
pending_questions: dict[str, PendingQuestion] = field(default_factory=dict)
|
|
88
|
+
|
|
59
89
|
# SSE event subscribers
|
|
60
90
|
event_subscribers: list[asyncio.Queue[Event]] = field(default_factory=list)
|
|
61
91
|
|
agentpool_storage/__init__.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""Storage provider package."""
|
|
2
2
|
|
|
3
3
|
from agentpool_storage.base import StorageProvider
|
|
4
|
-
from agentpool_storage.claude_provider import ClaudeStorageProvider
|
|
5
|
-
from agentpool_storage.opencode_provider import OpenCodeStorageProvider
|
|
6
4
|
from agentpool_storage.project_store import (
|
|
7
5
|
ProjectStore,
|
|
8
6
|
detect_project_root,
|
|
@@ -13,8 +11,6 @@ from agentpool_storage.project_store import (
|
|
|
13
11
|
from agentpool_storage.session_store import SQLSessionStore
|
|
14
12
|
|
|
15
13
|
__all__ = [
|
|
16
|
-
"ClaudeStorageProvider",
|
|
17
|
-
"OpenCodeStorageProvider",
|
|
18
14
|
"ProjectStore",
|
|
19
15
|
"SQLSessionStore",
|
|
20
16
|
"StorageProvider",
|
agentpool_storage/base.py
CHANGED
|
@@ -38,7 +38,6 @@ class StoredMessage:
|
|
|
38
38
|
token_usage: dict[str, int] | None = None
|
|
39
39
|
cost: float | None = None
|
|
40
40
|
response_time: float | None = None
|
|
41
|
-
forwarded_from: list[str] | None = None
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
class StoredConversation:
|
|
@@ -103,7 +102,6 @@ class StorageProvider:
|
|
|
103
102
|
cost_info: TokenCost | None = None,
|
|
104
103
|
model: str | None = None,
|
|
105
104
|
response_time: float | None = None,
|
|
106
|
-
forwarded_from: list[str] | None = None,
|
|
107
105
|
provider_name: str | None = None,
|
|
108
106
|
provider_response_id: str | None = None,
|
|
109
107
|
messages: str | None = None,
|
|
@@ -146,6 +144,87 @@ class StorageProvider:
|
|
|
146
144
|
"""
|
|
147
145
|
return None
|
|
148
146
|
|
|
147
|
+
async def get_conversation_messages(
|
|
148
|
+
self,
|
|
149
|
+
conversation_id: str,
|
|
150
|
+
*,
|
|
151
|
+
include_ancestors: bool = False,
|
|
152
|
+
) -> list[ChatMessage[str]]:
|
|
153
|
+
"""Get all messages for a conversation.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
conversation_id: ID of the conversation
|
|
157
|
+
include_ancestors: If True, also include messages from ancestor
|
|
158
|
+
conversations (following parent_id chain). Useful for forked
|
|
159
|
+
conversations where you want the full history.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List of messages ordered by timestamp.
|
|
163
|
+
"""
|
|
164
|
+
msg = f"{self.__class__.__name__} does not support getting conversation messages"
|
|
165
|
+
raise NotImplementedError(msg)
|
|
166
|
+
|
|
167
|
+
async def get_message(
|
|
168
|
+
self,
|
|
169
|
+
message_id: str,
|
|
170
|
+
) -> ChatMessage[str] | None:
|
|
171
|
+
"""Get a single message by ID.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
message_id: ID of the message
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The message if found, None otherwise.
|
|
178
|
+
"""
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
async def get_message_ancestry(
|
|
182
|
+
self,
|
|
183
|
+
message_id: str,
|
|
184
|
+
) -> list[ChatMessage[str]]:
|
|
185
|
+
"""Get the ancestry chain of a message.
|
|
186
|
+
|
|
187
|
+
Traverses the parent_id chain to build full history leading to this message.
|
|
188
|
+
Useful for forked conversations where you need context from the fork point.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
message_id: ID of the message to get ancestry for
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
List of messages from oldest ancestor to the specified message.
|
|
195
|
+
"""
|
|
196
|
+
msg = f"{self.__class__.__name__} does not support message ancestry"
|
|
197
|
+
raise NotImplementedError(msg)
|
|
198
|
+
|
|
199
|
+
async def fork_conversation(
|
|
200
|
+
self,
|
|
201
|
+
*,
|
|
202
|
+
source_conversation_id: str,
|
|
203
|
+
new_conversation_id: str,
|
|
204
|
+
fork_from_message_id: str | None = None,
|
|
205
|
+
new_agent_name: str | None = None,
|
|
206
|
+
) -> str | None:
|
|
207
|
+
"""Fork a conversation at a specific point.
|
|
208
|
+
|
|
209
|
+
Creates a new conversation that branches from the source conversation.
|
|
210
|
+
The new conversation's first message will have parent_id pointing to
|
|
211
|
+
the fork point, allowing history traversal.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
source_conversation_id: ID of the conversation to fork from
|
|
215
|
+
new_conversation_id: ID for the new forked conversation
|
|
216
|
+
fork_from_message_id: Message ID to fork from. If None, forks from
|
|
217
|
+
the last message in the source conversation.
|
|
218
|
+
new_agent_name: Agent name for the new conversation. If None,
|
|
219
|
+
inherits from source.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
The message_id of the fork point (the parent for new messages),
|
|
223
|
+
or None if the source conversation is empty.
|
|
224
|
+
"""
|
|
225
|
+
msg = f"{self.__class__.__name__} does not support forking conversations"
|
|
226
|
+
raise NotImplementedError(msg)
|
|
227
|
+
|
|
149
228
|
async def log_command(
|
|
150
229
|
self,
|
|
151
230
|
*,
|