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.
Files changed (250) hide show
  1. acp/__init__.py +0 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/client/connection.py +38 -29
  7. acp/client/implementations/default_client.py +3 -2
  8. acp/client/implementations/headless_client.py +2 -2
  9. acp/connection.py +2 -2
  10. acp/notifications.py +18 -49
  11. acp/schema/__init__.py +2 -0
  12. acp/schema/agent_responses.py +21 -0
  13. acp/schema/client_requests.py +3 -3
  14. acp/schema/session_state.py +63 -29
  15. acp/task/supervisor.py +2 -2
  16. acp/utils.py +2 -2
  17. agentpool/__init__.py +2 -0
  18. agentpool/agents/acp_agent/acp_agent.py +278 -263
  19. agentpool/agents/acp_agent/acp_converters.py +150 -17
  20. agentpool/agents/acp_agent/client_handler.py +35 -24
  21. agentpool/agents/acp_agent/session_state.py +14 -6
  22. agentpool/agents/agent.py +471 -643
  23. agentpool/agents/agui_agent/agui_agent.py +104 -107
  24. agentpool/agents/agui_agent/helpers.py +3 -4
  25. agentpool/agents/base_agent.py +485 -32
  26. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  27. agentpool/agents/claude_code_agent/__init__.py +13 -1
  28. agentpool/agents/claude_code_agent/claude_code_agent.py +654 -334
  29. agentpool/agents/claude_code_agent/converters.py +4 -141
  30. agentpool/agents/claude_code_agent/models.py +77 -0
  31. agentpool/agents/claude_code_agent/static_info.py +100 -0
  32. agentpool/agents/claude_code_agent/usage.py +242 -0
  33. agentpool/agents/events/__init__.py +22 -0
  34. agentpool/agents/events/builtin_handlers.py +65 -0
  35. agentpool/agents/events/event_emitter.py +3 -0
  36. agentpool/agents/events/events.py +84 -3
  37. agentpool/agents/events/infer_info.py +145 -0
  38. agentpool/agents/events/processors.py +254 -0
  39. agentpool/agents/interactions.py +41 -6
  40. agentpool/agents/modes.py +13 -0
  41. agentpool/agents/slashed_agent.py +5 -4
  42. agentpool/agents/tool_wrapping.py +18 -6
  43. agentpool/common_types.py +35 -21
  44. agentpool/config_resources/acp_assistant.yml +2 -2
  45. agentpool/config_resources/agents.yml +3 -0
  46. agentpool/config_resources/agents_template.yml +1 -0
  47. agentpool/config_resources/claude_code_agent.yml +9 -8
  48. agentpool/config_resources/external_acp_agents.yml +2 -1
  49. agentpool/delegation/base_team.py +4 -30
  50. agentpool/delegation/pool.py +104 -265
  51. agentpool/delegation/team.py +57 -57
  52. agentpool/delegation/teamrun.py +50 -55
  53. agentpool/functional/run.py +10 -4
  54. agentpool/mcp_server/client.py +73 -38
  55. agentpool/mcp_server/conversions.py +54 -13
  56. agentpool/mcp_server/manager.py +9 -23
  57. agentpool/mcp_server/registries/official_registry_client.py +10 -1
  58. agentpool/mcp_server/tool_bridge.py +114 -79
  59. agentpool/messaging/connection_manager.py +11 -10
  60. agentpool/messaging/event_manager.py +5 -5
  61. agentpool/messaging/message_container.py +6 -30
  62. agentpool/messaging/message_history.py +87 -8
  63. agentpool/messaging/messagenode.py +52 -14
  64. agentpool/messaging/messages.py +2 -26
  65. agentpool/messaging/processing.py +10 -22
  66. agentpool/models/__init__.py +1 -1
  67. agentpool/models/acp_agents/base.py +6 -2
  68. agentpool/models/acp_agents/mcp_capable.py +124 -15
  69. agentpool/models/acp_agents/non_mcp.py +0 -23
  70. agentpool/models/agents.py +66 -66
  71. agentpool/models/agui_agents.py +1 -1
  72. agentpool/models/claude_code_agents.py +111 -17
  73. agentpool/models/file_parsing.py +0 -1
  74. agentpool/models/manifest.py +70 -50
  75. agentpool/prompts/conversion_manager.py +1 -1
  76. agentpool/prompts/prompts.py +5 -2
  77. agentpool/resource_providers/__init__.py +2 -0
  78. agentpool/resource_providers/aggregating.py +4 -2
  79. agentpool/resource_providers/base.py +13 -3
  80. agentpool/resource_providers/codemode/code_executor.py +72 -5
  81. agentpool/resource_providers/codemode/helpers.py +2 -2
  82. agentpool/resource_providers/codemode/provider.py +64 -12
  83. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  84. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  85. agentpool/resource_providers/filtering.py +3 -1
  86. agentpool/resource_providers/mcp_provider.py +66 -12
  87. agentpool/resource_providers/plan_provider.py +111 -18
  88. agentpool/resource_providers/pool.py +5 -3
  89. agentpool/resource_providers/resource_info.py +111 -0
  90. agentpool/resource_providers/static.py +2 -2
  91. agentpool/sessions/__init__.py +2 -0
  92. agentpool/sessions/manager.py +2 -3
  93. agentpool/sessions/models.py +9 -6
  94. agentpool/sessions/protocol.py +28 -0
  95. agentpool/sessions/session.py +11 -55
  96. agentpool/storage/manager.py +361 -54
  97. agentpool/talk/registry.py +4 -4
  98. agentpool/talk/talk.py +9 -10
  99. agentpool/testing.py +1 -1
  100. agentpool/tool_impls/__init__.py +6 -0
  101. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  102. agentpool/tool_impls/agent_cli/tool.py +95 -0
  103. agentpool/tool_impls/bash/__init__.py +64 -0
  104. agentpool/tool_impls/bash/helpers.py +35 -0
  105. agentpool/tool_impls/bash/tool.py +171 -0
  106. agentpool/tool_impls/delete_path/__init__.py +70 -0
  107. agentpool/tool_impls/delete_path/tool.py +142 -0
  108. agentpool/tool_impls/download_file/__init__.py +80 -0
  109. agentpool/tool_impls/download_file/tool.py +183 -0
  110. agentpool/tool_impls/execute_code/__init__.py +55 -0
  111. agentpool/tool_impls/execute_code/tool.py +163 -0
  112. agentpool/tool_impls/grep/__init__.py +80 -0
  113. agentpool/tool_impls/grep/tool.py +200 -0
  114. agentpool/tool_impls/list_directory/__init__.py +73 -0
  115. agentpool/tool_impls/list_directory/tool.py +197 -0
  116. agentpool/tool_impls/question/__init__.py +42 -0
  117. agentpool/tool_impls/question/tool.py +127 -0
  118. agentpool/tool_impls/read/__init__.py +104 -0
  119. agentpool/tool_impls/read/tool.py +305 -0
  120. agentpool/tools/__init__.py +2 -1
  121. agentpool/tools/base.py +114 -34
  122. agentpool/tools/manager.py +57 -1
  123. agentpool/ui/base.py +2 -2
  124. agentpool/ui/mock_provider.py +2 -2
  125. agentpool/ui/stdlib_provider.py +2 -2
  126. agentpool/utils/streams.py +21 -96
  127. agentpool/vfs_registry.py +7 -2
  128. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
  129. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
  130. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  131. agentpool_cli/__main__.py +20 -0
  132. agentpool_cli/create.py +1 -1
  133. agentpool_cli/serve_acp.py +59 -1
  134. agentpool_cli/serve_opencode.py +1 -1
  135. agentpool_cli/ui.py +557 -0
  136. agentpool_commands/__init__.py +12 -5
  137. agentpool_commands/agents.py +1 -1
  138. agentpool_commands/pool.py +260 -0
  139. agentpool_commands/session.py +1 -1
  140. agentpool_commands/text_sharing/__init__.py +119 -0
  141. agentpool_commands/text_sharing/base.py +123 -0
  142. agentpool_commands/text_sharing/github_gist.py +80 -0
  143. agentpool_commands/text_sharing/opencode.py +462 -0
  144. agentpool_commands/text_sharing/paste_rs.py +59 -0
  145. agentpool_commands/text_sharing/pastebin.py +116 -0
  146. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  147. agentpool_commands/utils.py +31 -32
  148. agentpool_config/__init__.py +30 -2
  149. agentpool_config/agentpool_tools.py +498 -0
  150. agentpool_config/converters.py +1 -1
  151. agentpool_config/event_handlers.py +42 -0
  152. agentpool_config/events.py +1 -1
  153. agentpool_config/forward_targets.py +1 -4
  154. agentpool_config/jinja.py +3 -3
  155. agentpool_config/mcp_server.py +1 -5
  156. agentpool_config/nodes.py +1 -1
  157. agentpool_config/observability.py +44 -0
  158. agentpool_config/session.py +0 -3
  159. agentpool_config/storage.py +38 -39
  160. agentpool_config/task.py +3 -3
  161. agentpool_config/tools.py +11 -28
  162. agentpool_config/toolsets.py +22 -90
  163. agentpool_server/a2a_server/agent_worker.py +307 -0
  164. agentpool_server/a2a_server/server.py +23 -18
  165. agentpool_server/acp_server/acp_agent.py +125 -56
  166. agentpool_server/acp_server/commands/acp_commands.py +46 -216
  167. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
  168. agentpool_server/acp_server/event_converter.py +651 -0
  169. agentpool_server/acp_server/input_provider.py +53 -10
  170. agentpool_server/acp_server/server.py +1 -11
  171. agentpool_server/acp_server/session.py +90 -410
  172. agentpool_server/acp_server/session_manager.py +8 -34
  173. agentpool_server/agui_server/server.py +3 -1
  174. agentpool_server/mcp_server/server.py +5 -2
  175. agentpool_server/opencode_server/ENDPOINTS.md +53 -14
  176. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  177. agentpool_server/opencode_server/__init__.py +0 -8
  178. agentpool_server/opencode_server/converters.py +132 -26
  179. agentpool_server/opencode_server/input_provider.py +160 -8
  180. agentpool_server/opencode_server/models/__init__.py +42 -20
  181. agentpool_server/opencode_server/models/app.py +12 -0
  182. agentpool_server/opencode_server/models/events.py +203 -29
  183. agentpool_server/opencode_server/models/mcp.py +19 -0
  184. agentpool_server/opencode_server/models/message.py +18 -1
  185. agentpool_server/opencode_server/models/parts.py +134 -1
  186. agentpool_server/opencode_server/models/question.py +56 -0
  187. agentpool_server/opencode_server/models/session.py +13 -1
  188. agentpool_server/opencode_server/routes/__init__.py +4 -0
  189. agentpool_server/opencode_server/routes/agent_routes.py +33 -2
  190. agentpool_server/opencode_server/routes/app_routes.py +66 -3
  191. agentpool_server/opencode_server/routes/config_routes.py +66 -5
  192. agentpool_server/opencode_server/routes/file_routes.py +184 -5
  193. agentpool_server/opencode_server/routes/global_routes.py +1 -1
  194. agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
  195. agentpool_server/opencode_server/routes/message_routes.py +122 -66
  196. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  197. agentpool_server/opencode_server/routes/pty_routes.py +23 -22
  198. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  199. agentpool_server/opencode_server/routes/session_routes.py +139 -68
  200. agentpool_server/opencode_server/routes/tui_routes.py +1 -1
  201. agentpool_server/opencode_server/server.py +47 -2
  202. agentpool_server/opencode_server/state.py +30 -0
  203. agentpool_storage/__init__.py +0 -4
  204. agentpool_storage/base.py +81 -2
  205. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  206. agentpool_storage/claude_provider/__init__.py +42 -0
  207. agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
  208. agentpool_storage/file_provider.py +149 -15
  209. agentpool_storage/memory_provider.py +132 -12
  210. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  211. agentpool_storage/opencode_provider/__init__.py +16 -0
  212. agentpool_storage/opencode_provider/helpers.py +414 -0
  213. agentpool_storage/opencode_provider/provider.py +895 -0
  214. agentpool_storage/session_store.py +20 -6
  215. agentpool_storage/sql_provider/sql_provider.py +135 -2
  216. agentpool_storage/sql_provider/utils.py +2 -12
  217. agentpool_storage/zed_provider/__init__.py +16 -0
  218. agentpool_storage/zed_provider/helpers.py +281 -0
  219. agentpool_storage/zed_provider/models.py +130 -0
  220. agentpool_storage/zed_provider/provider.py +442 -0
  221. agentpool_storage/zed_provider.py +803 -0
  222. agentpool_toolsets/__init__.py +0 -2
  223. agentpool_toolsets/builtin/__init__.py +2 -4
  224. agentpool_toolsets/builtin/code.py +4 -4
  225. agentpool_toolsets/builtin/debug.py +115 -40
  226. agentpool_toolsets/builtin/execution_environment.py +54 -165
  227. agentpool_toolsets/builtin/skills.py +0 -77
  228. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  229. agentpool_toolsets/builtin/workers.py +4 -2
  230. agentpool_toolsets/composio_toolset.py +2 -2
  231. agentpool_toolsets/entry_points.py +3 -1
  232. agentpool_toolsets/fsspec_toolset/grep.py +25 -5
  233. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  234. agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
  235. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  236. agentpool_toolsets/mcp_discovery/toolset.py +74 -17
  237. agentpool_toolsets/mcp_run_toolset.py +8 -11
  238. agentpool_toolsets/notifications.py +33 -33
  239. agentpool_toolsets/openapi.py +3 -1
  240. agentpool_toolsets/search_toolset.py +3 -1
  241. agentpool_config/resources.py +0 -33
  242. agentpool_server/acp_server/acp_tools.py +0 -43
  243. agentpool_server/acp_server/commands/spawn.py +0 -210
  244. agentpool_storage/opencode_provider.py +0 -730
  245. agentpool_storage/text_log_provider.py +0 -276
  246. agentpool_toolsets/builtin/chain.py +0 -288
  247. agentpool_toolsets/builtin/user_interaction.py +0 -52
  248. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  249. {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  250. {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 # noqa: TC001
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 ( # noqa: TC001
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(data: SessionData) -> Session:
67
- """Convert SessionData to OpenCode Session model."""
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=data.title or "New Session",
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
- session = session_data_to_opencode(data)
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
- # TODO: Actually abort running operations
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
- response: Literal["once", "always", "reject"]
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
- request: PermissionResponse,
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, request.response)
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=request.response,
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
- Restores files to their state before the specified message's changes.
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 not file_ops.changes:
994
- raise HTTPException(status_code=400, detail="No file changes to revert")
995
- # Get revert operations for changes since this message
996
- revert_ops = file_ops.get_revert_operations(since_message_id=request.message_id)
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 not file_ops.reverted_changes:
1044
- raise HTTPException(status_code=400, detail="No reverted changes to restore")
1045
- # Get unrevert operations
1046
- unrevert_ops = file_ops.get_unrevert_operations()
1047
- # Get filesystem from the agent's environment
1048
- fs = state.agent.env.get_fs()
1049
- # Apply unrevert - write back the new_content
1050
- for path, content in unrevert_ops:
1051
- try:
1052
- if content is None:
1053
- # File was deleted in the original change, delete it again
1054
- await fs._rm_file(path)
1055
- else:
1056
- # Restore the changed content
1057
- content_bytes = content.encode("utf-8")
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 # noqa: TC001
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
- yield
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
 
@@ -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
  *,