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,18 +2,24 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  from typing import TYPE_CHECKING, Any
6
7
  import uuid
7
8
 
9
+ import anyenv
8
10
  from pydantic_ai import (
9
- TextPart as PydanticTextPart,
10
- ToolCallPart as PydanticToolCallPart,
11
- )
12
- from pydantic_ai.messages import (
11
+ AudioUrl,
12
+ BinaryContent,
13
+ DocumentUrl,
14
+ ImageUrl,
13
15
  ModelRequest,
14
16
  ModelResponse,
17
+ RetryPromptPart,
18
+ TextPart as PydanticTextPart,
19
+ ToolCallPart as PydanticToolCallPart,
15
20
  ToolReturnPart as PydanticToolReturnPart,
16
21
  UserPromptPart,
22
+ VideoUrl,
17
23
  )
18
24
 
19
25
  from agentpool_server.opencode_server.models import (
@@ -37,6 +43,8 @@ from agentpool_server.opencode_server.models import (
37
43
  from agentpool_server.opencode_server.models.common import TimeCreated
38
44
  from agentpool_server.opencode_server.models.message import UserMessageModel
39
45
  from agentpool_server.opencode_server.models.parts import (
46
+ APIErrorInfo,
47
+ RetryPart,
40
48
  StepFinishPart,
41
49
  StepFinishTokens,
42
50
  StepStartPart,
@@ -49,9 +57,7 @@ from agentpool_server.opencode_server.time_utils import now_ms
49
57
  if TYPE_CHECKING:
50
58
  from collections.abc import Sequence
51
59
 
52
- from pydantic_ai import (
53
- UserContent,
54
- )
60
+ from pydantic_ai import UserContent
55
61
 
56
62
  from agentpool.agents.events import (
57
63
  ToolCallCompleteEvent,
@@ -62,6 +68,8 @@ if TYPE_CHECKING:
62
68
  from agentpool_server.opencode_server.models import Part
63
69
 
64
70
 
71
+ logger = logging.getLogger(__name__)
72
+
65
73
  # Parameter name mapping from snake_case to camelCase for OpenCode TUI compatibility
66
74
  _PARAM_NAME_MAP: dict[str, str] = {
67
75
  "path": "filePath",
@@ -175,6 +183,7 @@ def convert_pydantic_tool_return_part(
175
183
  title=f"Completed {part.tool_name}",
176
184
  input=existing_input,
177
185
  output=output,
186
+ metadata=part.metadata or {}, # Extract metadata from ToolReturnPart
178
187
  time=TimeStartEndCompacted(start=now_ms() - 1000, end=now_ms()),
179
188
  )
180
189
 
@@ -287,6 +296,7 @@ def convert_tool_complete_event(
287
296
  title=f"Completed {existing_part.tool}",
288
297
  input=existing_input,
289
298
  output=output,
299
+ metadata=event.metadata or {},
290
300
  time=TimeStartEndCompacted(start=now_ms() - 1000, end=now_ms()),
291
301
  )
292
302
 
@@ -320,14 +330,6 @@ def _convert_file_part_to_user_content(part: dict[str, Any]) -> Any:
320
330
  Returns:
321
331
  Appropriate pydantic-ai content type
322
332
  """
323
- from pydantic_ai.messages import (
324
- AudioUrl,
325
- BinaryContent,
326
- DocumentUrl,
327
- ImageUrl,
328
- VideoUrl,
329
- )
330
-
331
333
  mime = part.get("mime", "")
332
334
  url = part.get("url", "")
333
335
 
@@ -378,6 +380,73 @@ def extract_user_prompt_from_parts(
378
380
  content = _convert_file_part_to_user_content(part)
379
381
  result.append(content)
380
382
 
383
+ elif part_type == "agent":
384
+ # Agent mention - inject instruction to delegate to sub-agent
385
+ # This mirrors OpenCode's server-side behavior: inject a synthetic
386
+ # text instruction telling the LLM to call the task tool
387
+ agent_name = part.get("name", "")
388
+ if agent_name:
389
+ # TODO: Implement proper agent delegation via task tool
390
+ # For now, we add the instruction as text that the LLM will see
391
+ instruction = (
392
+ f"Use the above message and context to generate a prompt "
393
+ f"and call the task tool with subagent: {agent_name}"
394
+ )
395
+ result.append(instruction)
396
+
397
+ elif part_type == "snapshot":
398
+ # File system snapshot reference
399
+ # TODO: Implement snapshot restoration/reference
400
+ snapshot_id = part.get("snapshot", "")
401
+ logger.debug("Ignoring snapshot part: %s", snapshot_id)
402
+
403
+ elif part_type == "patch":
404
+ # Diff/patch content
405
+ # TODO: Implement patch application
406
+ patch_hash = part.get("hash", "")
407
+ files = part.get("files", [])
408
+ logger.debug("Ignoring patch part: hash=%s, files=%s", patch_hash, files)
409
+
410
+ elif part_type == "reasoning":
411
+ # Extended thinking/reasoning content from the model
412
+ # Include as text context since it contains useful reasoning
413
+ reasoning_text = part.get("text", "")
414
+ if reasoning_text:
415
+ result.append(f"[Reasoning]: {reasoning_text}")
416
+
417
+ elif part_type == "compaction":
418
+ # Marks where conversation was compacted
419
+ # TODO: Handle compaction markers for context management
420
+ auto = part.get("auto", False)
421
+ logger.debug("Ignoring compaction part: auto=%s", auto)
422
+
423
+ elif part_type == "subtask":
424
+ # References a spawned subtask
425
+ # TODO: Implement subtask tracking/results
426
+ subtask_agent = part.get("agent", "")
427
+ subtask_desc = part.get("description", "")
428
+ logger.debug(
429
+ "Ignoring subtask part: agent=%s, description=%s", subtask_agent, subtask_desc
430
+ )
431
+
432
+ elif part_type == "retry":
433
+ # Marks a retry of a failed operation
434
+ # TODO: Handle retry tracking
435
+ attempt = part.get("attempt", 0)
436
+ logger.debug("Ignoring retry part: attempt=%s", attempt)
437
+
438
+ elif part_type == "step-start":
439
+ # Step start marker - informational only
440
+ logger.debug("Ignoring step-start part")
441
+
442
+ elif part_type == "step-finish":
443
+ # Step finish marker - informational only
444
+ logger.debug("Ignoring step-finish part")
445
+
446
+ else:
447
+ # Unknown part type
448
+ logger.warning("Unknown part type: %s", part_type)
449
+
381
450
  # If only text parts, join them as a single string for simplicity
382
451
  if all(isinstance(item, str) for item in result):
383
452
  return "\n".join(result) # type: ignore[arg-type]
@@ -595,9 +664,52 @@ def chat_message_to_opencode( # noqa: PLR0915
595
664
  parts.append(tool_part)
596
665
 
597
666
  elif isinstance(model_msg, ModelRequest):
598
- # Check for tool returns in requests (they come after responses)
667
+ # Check for tool returns and retries in requests (they come after responses)
599
668
  for part in model_msg.parts:
600
- if isinstance(part, PydanticToolReturnPart):
669
+ if isinstance(part, RetryPromptPart):
670
+ # Track retry attempts - count RetryPromptParts in message history
671
+ retry_count = sum(
672
+ 1
673
+ for m in msg.messages
674
+ if isinstance(m, ModelRequest)
675
+ for p in m.parts
676
+ if isinstance(p, RetryPromptPart)
677
+ )
678
+
679
+ # Create error info from retry content
680
+ error_message = part.model_response()
681
+
682
+ # Try to extract more info if we have structured error details
683
+ is_retryable = True
684
+ if isinstance(part.content, list):
685
+ # Validation errors - always retryable
686
+ error_type = "validation_error"
687
+ elif part.tool_name:
688
+ # Tool-related retry
689
+ error_type = "tool_error"
690
+ else:
691
+ # Generic retry
692
+ error_type = "retry"
693
+
694
+ api_error = APIErrorInfo(
695
+ message=error_message,
696
+ status_code=None, # Not available from pydantic-ai
697
+ is_retryable=is_retryable,
698
+ metadata={"error_type": error_type} if error_type else None,
699
+ )
700
+
701
+ parts.append(
702
+ RetryPart(
703
+ id=generate_part_id(),
704
+ message_id=message_id,
705
+ session_id=session_id,
706
+ attempt=retry_count,
707
+ error=api_error,
708
+ time=TimeCreated(created=int(part.timestamp.timestamp() * 1000)),
709
+ )
710
+ )
711
+
712
+ elif isinstance(part, PydanticToolReturnPart):
601
713
  call_id = part.tool_call_id or ""
602
714
  existing = tool_calls.get(call_id)
603
715
 
@@ -606,19 +718,13 @@ def chat_message_to_opencode( # noqa: PLR0915
606
718
  if isinstance(content, str):
607
719
  output = content
608
720
  elif isinstance(content, dict):
609
- import json
610
-
611
- output = json.dumps(content, indent=2)
721
+ output = anyenv.dump_json(content, indent=True)
612
722
  else:
613
723
  output = str(content) if content is not None else ""
614
-
615
- # Check for error
616
- is_error = isinstance(content, dict) and "error" in content
617
-
618
724
  if existing:
619
725
  # Update existing tool part with completion
620
726
  existing_input = _get_input_from_state(existing.state)
621
- if is_error:
727
+ if isinstance(content, dict) and "error" in content:
622
728
  existing.state = ToolStateError(
623
729
  status="error",
624
730
  error=str(content.get("error", "Unknown error")),
@@ -636,7 +742,7 @@ def chat_message_to_opencode( # noqa: PLR0915
636
742
  else:
637
743
  # Orphan return - create completed tool part
638
744
  state: ToolStateCompleted | ToolStateError
639
- if is_error:
745
+ if isinstance(content, dict) and "error" in content:
640
746
  state = ToolStateError(
641
747
  status="error",
642
748
  error=str(content.get("error", "Unknown error")),
@@ -14,9 +14,8 @@ from agentpool_server.opencode_server.models.events import PermissionRequestEven
14
14
 
15
15
 
16
16
  if TYPE_CHECKING:
17
- from agentpool.agents.context import ConfirmationResult
17
+ from agentpool.agents.context import AgentContext, ConfirmationResult
18
18
  from agentpool.messaging import ChatMessage
19
- from agentpool.messaging.context import NodeContext
20
19
  from agentpool.tools.base import Tool
21
20
  from agentpool_server.opencode_server.state import ServerState
22
21
 
@@ -68,7 +67,7 @@ class OpenCodeInputProvider(InputProvider):
68
67
 
69
68
  async def get_tool_confirmation(
70
69
  self,
71
- context: NodeContext[Any],
70
+ context: AgentContext[Any],
72
71
  tool: Tool,
73
72
  args: dict[str, Any],
74
73
  message_history: list[ChatMessage[Any]] | None = None,
@@ -217,11 +216,10 @@ class OpenCodeInputProvider(InputProvider):
217
216
  self,
218
217
  params: types.ElicitRequestParams,
219
218
  ) -> types.ElicitResult | types.ErrorData:
220
- """Get user response to elicitation request.
219
+ """Get user response to elicitation request via OpenCode questions.
221
220
 
222
- For now, this returns a basic decline since OpenCode doesn't have
223
- a full elicitation UI like ACP. Future versions could add support
224
- for more complex elicitation flows.
221
+ Translates MCP elicitation requests to OpenCode question system.
222
+ Supports both single-select and multi-select questions.
225
223
 
226
224
  Args:
227
225
  params: MCP elicit request parameters
@@ -239,7 +237,22 @@ class OpenCodeInputProvider(InputProvider):
239
237
  # Could potentially open URL in browser here
240
238
  return types.ElicitResult(action="decline")
241
239
 
242
- # For form elicitation, we don't have UI support yet
240
+ # For form elicitation with enum schema, use OpenCode questions
241
+ if isinstance(params, types.ElicitRequestFormParams):
242
+ schema = params.requestedSchema
243
+
244
+ # Check if schema defines options (enum)
245
+ enum_values = schema.get("enum")
246
+ if enum_values:
247
+ return await self._handle_question_elicitation(params, schema)
248
+
249
+ # Check if it's an array schema with enum items
250
+ if schema.get("type") == "array":
251
+ items = schema.get("items", {})
252
+ if items.get("enum"):
253
+ return await self._handle_question_elicitation(params, schema)
254
+
255
+ # For other form elicitation, we don't have UI support yet
243
256
  logger.info(
244
257
  "Form elicitation request (not supported)",
245
258
  message=params.message,
@@ -247,12 +260,151 @@ class OpenCodeInputProvider(InputProvider):
247
260
  )
248
261
  return types.ElicitResult(action="decline")
249
262
 
263
+ async def _handle_question_elicitation(
264
+ self,
265
+ params: types.ElicitRequestFormParams,
266
+ schema: dict[str, Any],
267
+ ) -> types.ElicitResult | types.ErrorData:
268
+ """Handle elicitation via OpenCode question system.
269
+
270
+ Args:
271
+ params: Form elicitation parameters
272
+ schema: JSON schema with enum values
273
+
274
+ Returns:
275
+ Elicit result with user's answer
276
+ """
277
+ import asyncio
278
+
279
+ from agentpool_server.opencode_server.models.events import QuestionAskedEvent
280
+
281
+ # Extract enum values
282
+ is_multi = schema.get("type") == "array"
283
+ if is_multi:
284
+ enum_values = schema.get("items", {}).get("enum", [])
285
+ else:
286
+ enum_values = schema.get("enum", [])
287
+
288
+ if not enum_values:
289
+ return types.ElicitResult(action="decline")
290
+
291
+ # Extract descriptions if available (custom x-option-descriptions field)
292
+ descriptions = schema.get("x-option-descriptions", {})
293
+
294
+ # Build OpenCode question format
295
+ from agentpool_server.opencode_server.models.question import (
296
+ QuestionInfo,
297
+ QuestionOption,
298
+ )
299
+
300
+ question_id = self._generate_permission_id() # Reuse ID generator
301
+ question_info = QuestionInfo(
302
+ question=params.message,
303
+ header=params.message[:12], # Truncate to 12 chars
304
+ options=[
305
+ QuestionOption(
306
+ label=str(val),
307
+ description=descriptions.get(str(val), ""),
308
+ )
309
+ for val in enum_values
310
+ ],
311
+ multiple=is_multi or None,
312
+ )
313
+
314
+ # Create future to wait for answer
315
+ future: asyncio.Future[list[list[str]]] = asyncio.get_event_loop().create_future()
316
+
317
+ # Store pending question
318
+ from agentpool_server.opencode_server.state import PendingQuestion
319
+
320
+ self.state.pending_questions[question_id] = PendingQuestion(
321
+ session_id=self.session_id,
322
+ questions=[question_info],
323
+ future=future,
324
+ tool=None, # Not associated with a specific tool call
325
+ )
326
+
327
+ # Broadcast event (serialize QuestionInfo to dict)
328
+ event = QuestionAskedEvent.create(
329
+ request_id=question_id,
330
+ session_id=self.session_id,
331
+ questions=[question_info.model_dump(mode="json", by_alias=True)],
332
+ )
333
+ await self.state.broadcast_event(event)
334
+
335
+ logger.info(
336
+ "Question asked",
337
+ question_id=question_id,
338
+ message=params.message,
339
+ is_multi=is_multi,
340
+ )
341
+
342
+ # Wait for answer
343
+ try:
344
+ answers = await future # list[list[str]]
345
+ answer = answers[0] if answers else [] # Get first question's answer
346
+
347
+ # ElicitResult content must be a dict, not a plain value
348
+ # Wrap the answer in a dict with a "value" key
349
+ # Multi-select: return list in dict
350
+ # Single-select: return string in dict
351
+ content: dict[str, str | list[str]] = (
352
+ {"value": answer} if is_multi else {"value": answer[0] if answer else ""}
353
+ )
354
+ return types.ElicitResult(action="accept", content=content)
355
+ except asyncio.CancelledError:
356
+ logger.info("Question cancelled", question_id=question_id)
357
+ return types.ElicitResult(action="cancel")
358
+ except Exception as e:
359
+ logger.exception("Question failed", question_id=question_id)
360
+ return types.ErrorData(
361
+ code=-1, # Generic error code
362
+ message=f"Elicitation failed: {e}",
363
+ )
364
+ finally:
365
+ # Clean up pending question
366
+ self.state.pending_questions.pop(question_id, None)
367
+
250
368
  def clear_tool_approvals(self) -> None:
251
369
  """Clear all stored tool approval decisions."""
252
370
  approval_count = len(self._tool_approvals)
253
371
  self._tool_approvals.clear()
254
372
  logger.info("Cleared tool approval decisions", count=approval_count)
255
373
 
374
+ def resolve_question(
375
+ self,
376
+ question_id: str,
377
+ answers: list[list[str]],
378
+ ) -> bool:
379
+ """Resolve a pending question request.
380
+
381
+ Called by the REST endpoint when the client responds.
382
+
383
+ Args:
384
+ question_id: The question request ID
385
+ answers: User's answers (array of arrays per OpenCode format)
386
+
387
+ Returns:
388
+ True if the question was found and resolved, False otherwise
389
+ """
390
+ pending = self.state.pending_questions.get(question_id)
391
+ if pending is None:
392
+ logger.warning("Question not found", question_id=question_id)
393
+ return False
394
+
395
+ future = pending.future
396
+ if future.done():
397
+ logger.warning("Question already resolved", question_id=question_id)
398
+ return False
399
+
400
+ future.set_result(answers)
401
+ logger.info(
402
+ "Question resolved",
403
+ question_id=question_id,
404
+ answers=answers,
405
+ )
406
+ return True
407
+
256
408
  def cancel_all_pending(self) -> int:
257
409
  """Cancel all pending permission requests.
258
410
 
@@ -17,6 +17,7 @@ from agentpool_server.opencode_server.models.app import (
17
17
  PathInfo,
18
18
  Project,
19
19
  ProjectTime,
20
+ ProjectUpdateRequest,
20
21
  VcsInfo,
21
22
  )
22
23
  from agentpool_server.opencode_server.models.provider import (
@@ -37,6 +38,7 @@ from agentpool_server.opencode_server.models.session import (
37
38
  SessionRevert,
38
39
  SessionShare,
39
40
  SessionStatus,
41
+ SessionSummary,
40
42
  SessionUpdateRequest,
41
43
  SummarizeRequest,
42
44
  Todo,
@@ -45,8 +47,10 @@ from agentpool_server.opencode_server.models.message import (
45
47
  AssistantMessage,
46
48
  CommandRequest,
47
49
  FilePartInput,
50
+ MessageInfo,
48
51
  MessagePath,
49
52
  MessageRequest,
53
+ MessageSummary,
50
54
  MessageTime,
51
55
  MessageWithParts,
52
56
  PartInput,
@@ -55,12 +59,20 @@ from agentpool_server.opencode_server.models.message import (
55
59
  Tokens,
56
60
  TokensCache,
57
61
  UserMessage,
62
+ UserMessageModel,
58
63
  )
59
64
  from agentpool_server.opencode_server.models.parts import (
65
+ AgentPart,
66
+ CompactionPart,
60
67
  FilePart,
61
68
  Part,
69
+ PatchPart,
70
+ ReasoningPart,
71
+ RetryPart,
72
+ SnapshotPart,
62
73
  StepFinishPart,
63
74
  StepStartPart,
75
+ SubtaskPart,
64
76
  TextPart,
65
77
  TimeStart,
66
78
  TimeStartEnd,
@@ -92,10 +104,13 @@ from agentpool_server.opencode_server.models.pty import (
92
104
  )
93
105
  from agentpool_server.opencode_server.models.events import (
94
106
  Event,
107
+ MessageRemovedEvent,
95
108
  MessageUpdatedEvent,
96
109
  MessageUpdatedEventProperties,
110
+ PartRemovedEvent,
97
111
  PartUpdatedEvent,
98
112
  PartUpdatedEventProperties,
113
+ ProjectUpdatedEvent,
99
114
  ServerConnectedEvent,
100
115
  SessionCompactedEvent,
101
116
  SessionCompactedProperties,
@@ -112,71 +127,76 @@ from agentpool_server.opencode_server.models.events import (
112
127
  SessionStatusProperties,
113
128
  SessionUpdatedEvent,
114
129
  )
115
- from agentpool_server.opencode_server.models.mcp import (
116
- LogRequest,
117
- MCPStatus,
118
- )
119
- from agentpool_server.opencode_server.models.config import (
120
- Config,
130
+ from agentpool_server.opencode_server.models.mcp import LogRequest, MCPStatus, McpResource
131
+ from agentpool_server.opencode_server.models.config import Config
132
+ from agentpool_server.opencode_server.models.question import (
133
+ QuestionInfo,
134
+ QuestionOption,
135
+ QuestionReply,
136
+ QuestionRequest,
121
137
  )
122
138
 
123
139
  __all__ = [
124
- # Agent
125
140
  "Agent",
126
- # App
141
+ "AgentPart",
127
142
  "App",
128
143
  "AppTimeInfo",
129
- # Message
130
144
  "AssistantMessage",
131
145
  "Command",
132
146
  "CommandRequest",
133
- # Config
147
+ "CompactionPart",
134
148
  "Config",
135
- # Events
136
149
  "Event",
137
- # File
138
150
  "FileContent",
139
151
  "FileNode",
140
- # Parts
141
152
  "FilePart",
142
153
  "FilePartInput",
143
154
  "FileStatus",
144
155
  "FindMatch",
145
156
  "HealthResponse",
146
- # MCP
147
157
  "LogRequest",
148
158
  "MCPStatus",
159
+ "McpResource",
160
+ "MessageInfo",
149
161
  "MessagePath",
162
+ "MessageRemovedEvent",
150
163
  "MessageRequest",
164
+ "MessageSummary",
151
165
  "MessageTime",
152
166
  "MessageUpdatedEvent",
153
167
  "MessageUpdatedEventProperties",
154
168
  "MessageWithParts",
155
169
  "Mode",
156
170
  "ModeModel",
157
- # Provider
158
171
  "Model",
159
172
  "ModelCost",
160
173
  "ModelLimit",
161
- # Base
162
174
  "OpenCodeBaseModel",
163
175
  "Part",
164
176
  "PartInput",
177
+ "PartRemovedEvent",
165
178
  "PartUpdatedEvent",
166
179
  "PartUpdatedEventProperties",
180
+ "PatchPart",
167
181
  "PathInfo",
168
182
  "Project",
169
183
  "ProjectTime",
184
+ "ProjectUpdateRequest",
185
+ "ProjectUpdatedEvent",
170
186
  "Provider",
171
187
  "ProviderListResponse",
172
188
  "ProvidersResponse",
173
- # PTY
174
189
  "PtyCreateRequest",
175
190
  "PtyInfo",
176
191
  "PtySize",
177
192
  "PtyUpdateRequest",
193
+ "QuestionInfo",
194
+ "QuestionOption",
195
+ "QuestionReply",
196
+ "QuestionRequest",
197
+ "ReasoningPart",
198
+ "RetryPart",
178
199
  "ServerConnectedEvent",
179
- # Session
180
200
  "Session",
181
201
  "SessionCompactedEvent",
182
202
  "SessionCompactedProperties",
@@ -197,19 +217,20 @@ __all__ = [
197
217
  "SessionStatus",
198
218
  "SessionStatusEvent",
199
219
  "SessionStatusProperties",
220
+ "SessionSummary",
200
221
  "SessionUpdateRequest",
201
222
  "SessionUpdatedEvent",
202
223
  "ShellRequest",
224
+ "SnapshotPart",
203
225
  "StepFinishPart",
204
226
  "StepStartPart",
227
+ "SubtaskPart",
205
228
  "SummarizeRequest",
206
229
  "Symbol",
207
230
  "TextPart",
208
231
  "TextPartInput",
209
- # Common
210
232
  "TimeCreated",
211
233
  "TimeCreatedUpdated",
212
- # Time types (from parts.py)
213
234
  "TimeStart",
214
235
  "TimeStartEnd",
215
236
  "TimeStartEndCompacted",
@@ -224,5 +245,6 @@ __all__ = [
224
245
  "ToolStatePending",
225
246
  "ToolStateRunning",
226
247
  "UserMessage",
248
+ "UserMessageModel",
227
249
  "VcsInfo",
228
250
  ]
@@ -1,5 +1,7 @@
1
1
  """App, project, and path related models."""
2
2
 
3
+ from typing import Any
4
+
3
5
  from agentpool_server.opencode_server.models.base import OpenCodeBaseModel
4
6
 
5
7
 
@@ -58,3 +60,13 @@ class VcsInfo(OpenCodeBaseModel):
58
60
  branch: str | None = None
59
61
  dirty: bool = False
60
62
  commit: str | None = None
63
+
64
+
65
+ class ProjectUpdateRequest(OpenCodeBaseModel):
66
+ """Request to update project metadata."""
67
+
68
+ name: str | None = None
69
+ """Optional friendly name for the project."""
70
+
71
+ settings: dict[str, Any] | None = None
72
+ """Optional project-specific settings to update."""