agentpool 2.1.9__py3-none-any.whl → 2.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. acp/__init__.py +13 -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/bridge/README.md +15 -2
  7. acp/bridge/__init__.py +3 -2
  8. acp/bridge/__main__.py +60 -19
  9. acp/bridge/ws_server.py +173 -0
  10. acp/bridge/ws_server_cli.py +89 -0
  11. acp/client/connection.py +38 -29
  12. acp/client/implementations/default_client.py +3 -2
  13. acp/client/implementations/headless_client.py +2 -2
  14. acp/connection.py +2 -2
  15. acp/notifications.py +20 -50
  16. acp/schema/__init__.py +2 -0
  17. acp/schema/agent_responses.py +21 -0
  18. acp/schema/client_requests.py +3 -3
  19. acp/schema/session_state.py +63 -29
  20. acp/stdio.py +39 -9
  21. acp/task/supervisor.py +2 -2
  22. acp/transports.py +362 -2
  23. acp/utils.py +17 -4
  24. agentpool/__init__.py +6 -1
  25. agentpool/agents/__init__.py +2 -0
  26. agentpool/agents/acp_agent/acp_agent.py +407 -277
  27. agentpool/agents/acp_agent/acp_converters.py +196 -38
  28. agentpool/agents/acp_agent/client_handler.py +191 -26
  29. agentpool/agents/acp_agent/session_state.py +17 -6
  30. agentpool/agents/agent.py +607 -572
  31. agentpool/agents/agui_agent/__init__.py +0 -2
  32. agentpool/agents/agui_agent/agui_agent.py +176 -110
  33. agentpool/agents/agui_agent/agui_converters.py +0 -131
  34. agentpool/agents/agui_agent/helpers.py +3 -4
  35. agentpool/agents/base_agent.py +632 -17
  36. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  37. agentpool/agents/claude_code_agent/__init__.py +13 -1
  38. agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
  39. agentpool/agents/claude_code_agent/converters.py +74 -143
  40. agentpool/agents/claude_code_agent/history.py +474 -0
  41. agentpool/agents/claude_code_agent/models.py +77 -0
  42. agentpool/agents/claude_code_agent/static_info.py +100 -0
  43. agentpool/agents/claude_code_agent/usage.py +242 -0
  44. agentpool/agents/context.py +40 -0
  45. agentpool/agents/events/__init__.py +24 -0
  46. agentpool/agents/events/builtin_handlers.py +67 -1
  47. agentpool/agents/events/event_emitter.py +32 -2
  48. agentpool/agents/events/events.py +104 -3
  49. agentpool/agents/events/infer_info.py +145 -0
  50. agentpool/agents/events/processors.py +254 -0
  51. agentpool/agents/interactions.py +41 -6
  52. agentpool/agents/modes.py +67 -0
  53. agentpool/agents/slashed_agent.py +5 -4
  54. agentpool/agents/tool_call_accumulator.py +213 -0
  55. agentpool/agents/tool_wrapping.py +18 -6
  56. agentpool/common_types.py +56 -21
  57. agentpool/config_resources/__init__.py +38 -1
  58. agentpool/config_resources/acp_assistant.yml +2 -2
  59. agentpool/config_resources/agents.yml +3 -0
  60. agentpool/config_resources/agents_template.yml +1 -0
  61. agentpool/config_resources/claude_code_agent.yml +10 -6
  62. agentpool/config_resources/external_acp_agents.yml +2 -1
  63. agentpool/delegation/base_team.py +4 -30
  64. agentpool/delegation/pool.py +136 -289
  65. agentpool/delegation/team.py +58 -57
  66. agentpool/delegation/teamrun.py +51 -55
  67. agentpool/diagnostics/__init__.py +53 -0
  68. agentpool/diagnostics/lsp_manager.py +1593 -0
  69. agentpool/diagnostics/lsp_proxy.py +41 -0
  70. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  71. agentpool/diagnostics/models.py +398 -0
  72. agentpool/functional/run.py +10 -4
  73. agentpool/mcp_server/__init__.py +0 -2
  74. agentpool/mcp_server/client.py +76 -32
  75. agentpool/mcp_server/conversions.py +54 -13
  76. agentpool/mcp_server/manager.py +34 -54
  77. agentpool/mcp_server/registries/official_registry_client.py +35 -1
  78. agentpool/mcp_server/tool_bridge.py +186 -139
  79. agentpool/messaging/__init__.py +0 -2
  80. agentpool/messaging/compaction.py +72 -197
  81. agentpool/messaging/connection_manager.py +11 -10
  82. agentpool/messaging/event_manager.py +5 -5
  83. agentpool/messaging/message_container.py +6 -30
  84. agentpool/messaging/message_history.py +99 -8
  85. agentpool/messaging/messagenode.py +52 -14
  86. agentpool/messaging/messages.py +54 -35
  87. agentpool/messaging/processing.py +12 -22
  88. agentpool/models/__init__.py +1 -1
  89. agentpool/models/acp_agents/base.py +6 -24
  90. agentpool/models/acp_agents/mcp_capable.py +126 -157
  91. agentpool/models/acp_agents/non_mcp.py +129 -95
  92. agentpool/models/agents.py +98 -76
  93. agentpool/models/agui_agents.py +1 -1
  94. agentpool/models/claude_code_agents.py +144 -19
  95. agentpool/models/file_parsing.py +0 -1
  96. agentpool/models/manifest.py +113 -50
  97. agentpool/prompts/conversion_manager.py +1 -1
  98. agentpool/prompts/prompts.py +5 -2
  99. agentpool/repomap.py +1 -1
  100. agentpool/resource_providers/__init__.py +11 -1
  101. agentpool/resource_providers/aggregating.py +56 -5
  102. agentpool/resource_providers/base.py +70 -4
  103. agentpool/resource_providers/codemode/code_executor.py +72 -5
  104. agentpool/resource_providers/codemode/helpers.py +2 -2
  105. agentpool/resource_providers/codemode/provider.py +64 -12
  106. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  107. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  108. agentpool/resource_providers/filtering.py +3 -1
  109. agentpool/resource_providers/mcp_provider.py +89 -12
  110. agentpool/resource_providers/plan_provider.py +228 -46
  111. agentpool/resource_providers/pool.py +7 -3
  112. agentpool/resource_providers/resource_info.py +111 -0
  113. agentpool/resource_providers/static.py +4 -2
  114. agentpool/sessions/__init__.py +4 -1
  115. agentpool/sessions/manager.py +33 -5
  116. agentpool/sessions/models.py +59 -6
  117. agentpool/sessions/protocol.py +28 -0
  118. agentpool/sessions/session.py +11 -55
  119. agentpool/skills/registry.py +13 -8
  120. agentpool/storage/manager.py +572 -49
  121. agentpool/talk/registry.py +4 -4
  122. agentpool/talk/talk.py +9 -10
  123. agentpool/testing.py +538 -20
  124. agentpool/tool_impls/__init__.py +6 -0
  125. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  126. agentpool/tool_impls/agent_cli/tool.py +95 -0
  127. agentpool/tool_impls/bash/__init__.py +64 -0
  128. agentpool/tool_impls/bash/helpers.py +35 -0
  129. agentpool/tool_impls/bash/tool.py +171 -0
  130. agentpool/tool_impls/delete_path/__init__.py +70 -0
  131. agentpool/tool_impls/delete_path/tool.py +142 -0
  132. agentpool/tool_impls/download_file/__init__.py +80 -0
  133. agentpool/tool_impls/download_file/tool.py +183 -0
  134. agentpool/tool_impls/execute_code/__init__.py +55 -0
  135. agentpool/tool_impls/execute_code/tool.py +163 -0
  136. agentpool/tool_impls/grep/__init__.py +80 -0
  137. agentpool/tool_impls/grep/tool.py +200 -0
  138. agentpool/tool_impls/list_directory/__init__.py +73 -0
  139. agentpool/tool_impls/list_directory/tool.py +197 -0
  140. agentpool/tool_impls/question/__init__.py +42 -0
  141. agentpool/tool_impls/question/tool.py +127 -0
  142. agentpool/tool_impls/read/__init__.py +104 -0
  143. agentpool/tool_impls/read/tool.py +305 -0
  144. agentpool/tools/__init__.py +2 -1
  145. agentpool/tools/base.py +114 -34
  146. agentpool/tools/manager.py +57 -1
  147. agentpool/ui/base.py +2 -2
  148. agentpool/ui/mock_provider.py +2 -2
  149. agentpool/ui/stdlib_provider.py +2 -2
  150. agentpool/utils/file_watcher.py +269 -0
  151. agentpool/utils/identifiers.py +121 -0
  152. agentpool/utils/pydantic_ai_helpers.py +46 -0
  153. agentpool/utils/streams.py +616 -2
  154. agentpool/utils/subprocess_utils.py +155 -0
  155. agentpool/utils/token_breakdown.py +461 -0
  156. agentpool/vfs_registry.py +7 -2
  157. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
  158. agentpool-2.5.0.dist-info/RECORD +579 -0
  159. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  160. agentpool_cli/__main__.py +24 -0
  161. agentpool_cli/create.py +1 -1
  162. agentpool_cli/serve_acp.py +100 -21
  163. agentpool_cli/serve_agui.py +87 -0
  164. agentpool_cli/serve_opencode.py +119 -0
  165. agentpool_cli/ui.py +557 -0
  166. agentpool_commands/__init__.py +42 -5
  167. agentpool_commands/agents.py +75 -2
  168. agentpool_commands/history.py +62 -0
  169. agentpool_commands/mcp.py +176 -0
  170. agentpool_commands/models.py +56 -3
  171. agentpool_commands/pool.py +260 -0
  172. agentpool_commands/session.py +1 -1
  173. agentpool_commands/text_sharing/__init__.py +119 -0
  174. agentpool_commands/text_sharing/base.py +123 -0
  175. agentpool_commands/text_sharing/github_gist.py +80 -0
  176. agentpool_commands/text_sharing/opencode.py +462 -0
  177. agentpool_commands/text_sharing/paste_rs.py +59 -0
  178. agentpool_commands/text_sharing/pastebin.py +116 -0
  179. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  180. agentpool_commands/tools.py +57 -0
  181. agentpool_commands/utils.py +80 -30
  182. agentpool_config/__init__.py +30 -2
  183. agentpool_config/agentpool_tools.py +498 -0
  184. agentpool_config/builtin_tools.py +77 -22
  185. agentpool_config/commands.py +24 -1
  186. agentpool_config/compaction.py +258 -0
  187. agentpool_config/converters.py +1 -1
  188. agentpool_config/event_handlers.py +42 -0
  189. agentpool_config/events.py +1 -1
  190. agentpool_config/forward_targets.py +1 -4
  191. agentpool_config/jinja.py +3 -3
  192. agentpool_config/mcp_server.py +132 -6
  193. agentpool_config/nodes.py +1 -1
  194. agentpool_config/observability.py +44 -0
  195. agentpool_config/session.py +0 -3
  196. agentpool_config/storage.py +82 -38
  197. agentpool_config/task.py +3 -3
  198. agentpool_config/tools.py +11 -22
  199. agentpool_config/toolsets.py +109 -233
  200. agentpool_server/a2a_server/agent_worker.py +307 -0
  201. agentpool_server/a2a_server/server.py +23 -18
  202. agentpool_server/acp_server/acp_agent.py +234 -181
  203. agentpool_server/acp_server/commands/acp_commands.py +151 -156
  204. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
  205. agentpool_server/acp_server/event_converter.py +651 -0
  206. agentpool_server/acp_server/input_provider.py +53 -10
  207. agentpool_server/acp_server/server.py +24 -90
  208. agentpool_server/acp_server/session.py +173 -331
  209. agentpool_server/acp_server/session_manager.py +8 -34
  210. agentpool_server/agui_server/server.py +3 -1
  211. agentpool_server/mcp_server/server.py +5 -2
  212. agentpool_server/opencode_server/.rules +95 -0
  213. agentpool_server/opencode_server/ENDPOINTS.md +401 -0
  214. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  215. agentpool_server/opencode_server/__init__.py +19 -0
  216. agentpool_server/opencode_server/command_validation.py +172 -0
  217. agentpool_server/opencode_server/converters.py +975 -0
  218. agentpool_server/opencode_server/dependencies.py +24 -0
  219. agentpool_server/opencode_server/input_provider.py +421 -0
  220. agentpool_server/opencode_server/models/__init__.py +250 -0
  221. agentpool_server/opencode_server/models/agent.py +53 -0
  222. agentpool_server/opencode_server/models/app.py +72 -0
  223. agentpool_server/opencode_server/models/base.py +26 -0
  224. agentpool_server/opencode_server/models/common.py +23 -0
  225. agentpool_server/opencode_server/models/config.py +37 -0
  226. agentpool_server/opencode_server/models/events.py +821 -0
  227. agentpool_server/opencode_server/models/file.py +88 -0
  228. agentpool_server/opencode_server/models/mcp.py +44 -0
  229. agentpool_server/opencode_server/models/message.py +179 -0
  230. agentpool_server/opencode_server/models/parts.py +323 -0
  231. agentpool_server/opencode_server/models/provider.py +81 -0
  232. agentpool_server/opencode_server/models/pty.py +43 -0
  233. agentpool_server/opencode_server/models/question.py +56 -0
  234. agentpool_server/opencode_server/models/session.py +111 -0
  235. agentpool_server/opencode_server/routes/__init__.py +29 -0
  236. agentpool_server/opencode_server/routes/agent_routes.py +473 -0
  237. agentpool_server/opencode_server/routes/app_routes.py +202 -0
  238. agentpool_server/opencode_server/routes/config_routes.py +302 -0
  239. agentpool_server/opencode_server/routes/file_routes.py +571 -0
  240. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  241. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  242. agentpool_server/opencode_server/routes/message_routes.py +761 -0
  243. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  244. agentpool_server/opencode_server/routes/pty_routes.py +300 -0
  245. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  246. agentpool_server/opencode_server/routes/session_routes.py +1276 -0
  247. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  248. agentpool_server/opencode_server/server.py +475 -0
  249. agentpool_server/opencode_server/state.py +151 -0
  250. agentpool_server/opencode_server/time_utils.py +8 -0
  251. agentpool_storage/__init__.py +12 -0
  252. agentpool_storage/base.py +184 -2
  253. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  254. agentpool_storage/claude_provider/__init__.py +42 -0
  255. agentpool_storage/claude_provider/provider.py +1089 -0
  256. agentpool_storage/file_provider.py +278 -15
  257. agentpool_storage/memory_provider.py +193 -12
  258. agentpool_storage/models.py +3 -0
  259. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  260. agentpool_storage/opencode_provider/__init__.py +16 -0
  261. agentpool_storage/opencode_provider/helpers.py +414 -0
  262. agentpool_storage/opencode_provider/provider.py +895 -0
  263. agentpool_storage/project_store.py +325 -0
  264. agentpool_storage/session_store.py +26 -6
  265. agentpool_storage/sql_provider/__init__.py +4 -2
  266. agentpool_storage/sql_provider/models.py +48 -0
  267. agentpool_storage/sql_provider/sql_provider.py +269 -3
  268. agentpool_storage/sql_provider/utils.py +12 -13
  269. agentpool_storage/zed_provider/__init__.py +16 -0
  270. agentpool_storage/zed_provider/helpers.py +281 -0
  271. agentpool_storage/zed_provider/models.py +130 -0
  272. agentpool_storage/zed_provider/provider.py +442 -0
  273. agentpool_storage/zed_provider.py +803 -0
  274. agentpool_toolsets/__init__.py +0 -2
  275. agentpool_toolsets/builtin/__init__.py +2 -12
  276. agentpool_toolsets/builtin/code.py +96 -57
  277. agentpool_toolsets/builtin/debug.py +118 -48
  278. agentpool_toolsets/builtin/execution_environment.py +115 -230
  279. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  280. agentpool_toolsets/builtin/skills.py +9 -4
  281. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  282. agentpool_toolsets/builtin/workers.py +4 -2
  283. agentpool_toolsets/composio_toolset.py +2 -2
  284. agentpool_toolsets/entry_points.py +3 -1
  285. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  286. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  287. agentpool_toolsets/fsspec_toolset/grep.py +99 -7
  288. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  289. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  290. agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
  291. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  292. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  293. agentpool_toolsets/mcp_discovery/toolset.py +511 -0
  294. agentpool_toolsets/mcp_run_toolset.py +87 -12
  295. agentpool_toolsets/notifications.py +33 -33
  296. agentpool_toolsets/openapi.py +3 -1
  297. agentpool_toolsets/search_toolset.py +3 -1
  298. agentpool-2.1.9.dist-info/RECORD +0 -474
  299. agentpool_config/resources.py +0 -33
  300. agentpool_server/acp_server/acp_tools.py +0 -43
  301. agentpool_server/acp_server/commands/spawn.py +0 -210
  302. agentpool_storage/text_log_provider.py +0 -275
  303. agentpool_toolsets/builtin/agent_management.py +0 -239
  304. agentpool_toolsets/builtin/chain.py +0 -288
  305. agentpool_toolsets/builtin/history.py +0 -36
  306. agentpool_toolsets/builtin/integration.py +0 -85
  307. agentpool_toolsets/builtin/tool_management.py +0 -90
  308. agentpool_toolsets/builtin/user_interaction.py +0 -52
  309. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  310. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  311. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,761 @@
1
+ """Message routes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from fastapi import APIRouter, HTTPException, Query, status
8
+ from pydantic_ai import FunctionToolCallEvent
9
+ from pydantic_ai.messages import (
10
+ PartDeltaEvent,
11
+ PartStartEvent,
12
+ TextPart as PydanticTextPart,
13
+ TextPartDelta,
14
+ ToolCallPart as PydanticToolCallPart,
15
+ )
16
+
17
+ from agentpool.agents.events import (
18
+ CompactionEvent,
19
+ FileContentItem,
20
+ LocationContentItem,
21
+ StreamCompleteEvent,
22
+ SubAgentEvent,
23
+ TextContentItem,
24
+ ToolCallCompleteEvent,
25
+ ToolCallProgressEvent,
26
+ ToolCallStartEvent,
27
+ )
28
+ from agentpool.agents.events.infer_info import derive_rich_tool_info
29
+ from agentpool.utils import identifiers as identifier
30
+ from agentpool.utils.pydantic_ai_helpers import safe_args_as_dict
31
+ from agentpool_server.opencode_server.converters import (
32
+ _convert_params_for_ui,
33
+ extract_user_prompt_from_parts,
34
+ opencode_to_chat_message,
35
+ )
36
+ from agentpool_server.opencode_server.dependencies import StateDep
37
+ from agentpool_server.opencode_server.models import (
38
+ AssistantMessage,
39
+ MessagePath,
40
+ MessageRequest,
41
+ MessageTime,
42
+ MessageUpdatedEvent,
43
+ MessageWithParts,
44
+ PartUpdatedEvent,
45
+ SessionCompactedEvent,
46
+ SessionErrorEvent,
47
+ SessionIdleEvent,
48
+ SessionStatus,
49
+ SessionStatusEvent,
50
+ StepFinishPart,
51
+ StepStartPart,
52
+ TextPart,
53
+ TimeCreated,
54
+ TimeCreatedUpdated,
55
+ TimeStartEnd,
56
+ Tokens,
57
+ TokensCache,
58
+ ToolPart,
59
+ ToolStateCompleted,
60
+ ToolStateError,
61
+ ToolStateRunning,
62
+ UserMessage,
63
+ )
64
+ from agentpool_server.opencode_server.models.message import UserMessageModel
65
+ from agentpool_server.opencode_server.models.parts import (
66
+ StepFinishTokens,
67
+ TimeStart,
68
+ TimeStartEndCompacted,
69
+ TimeStartEndOptional,
70
+ TokenCache,
71
+ )
72
+ from agentpool_server.opencode_server.routes.session_routes import get_or_load_session
73
+ from agentpool_server.opencode_server.time_utils import now_ms
74
+
75
+
76
+ if TYPE_CHECKING:
77
+ from agentpool_server.opencode_server.models import (
78
+ Part,
79
+ )
80
+ from agentpool_server.opencode_server.state import ServerState
81
+
82
+
83
+ def _warmup_lsp_for_files(state: ServerState, file_paths: list[str]) -> None:
84
+ """Warm up LSP servers for the given file paths.
85
+
86
+ This starts LSP servers asynchronously based on file extensions.
87
+ Like OpenCode's LSP.touchFile(), this triggers server startup without waiting.
88
+
89
+ Args:
90
+ state: Server state with LSP manager
91
+ file_paths: List of file paths that were accessed
92
+ """
93
+ import logging
94
+
95
+ logging.getLogger(__name__)
96
+ print(f"[LSP] _warmup_lsp_for_files called with: {file_paths}")
97
+
98
+ try:
99
+ lsp_manager = state.get_or_create_lsp_manager()
100
+ print("[LSP] Got LSP manager successfully")
101
+ except RuntimeError as e:
102
+ # No execution environment available for LSP
103
+ print(f"[LSP] No LSP manager: {e}")
104
+ return
105
+
106
+ async def warmup_files() -> None:
107
+ """Start LSP servers for each file path."""
108
+ print("[LSP] warmup_files task started")
109
+ from agentpool_server.opencode_server.models.events import LspUpdatedEvent
110
+
111
+ servers_started = False
112
+ for path in file_paths:
113
+ # Find appropriate server for this file
114
+ server_info = lsp_manager.get_server_for_file(path)
115
+ print(f"[LSP] Server for {path}: {server_info.id if server_info else None}")
116
+ if server_info is None:
117
+ continue
118
+
119
+ server_id = server_info.id
120
+ if lsp_manager.is_running(server_id):
121
+ print(f"[LSP] Server {server_id} already running")
122
+ continue
123
+
124
+ # Start server for workspace root
125
+ root_uri = f"file://{state.working_dir}"
126
+ try:
127
+ print(f"[LSP] Starting server {server_id}...")
128
+ await lsp_manager.start_server(server_id, root_uri)
129
+ servers_started = True
130
+ print(f"[LSP] Server {server_id} started successfully")
131
+ except Exception as e: # noqa: BLE001
132
+ # Don't fail on LSP startup errors
133
+ print(f"[LSP] Failed to start server {server_id}: {e}")
134
+
135
+ # Emit lsp.updated event if any servers started
136
+ if servers_started:
137
+ print("[LSP] Broadcasting LspUpdatedEvent")
138
+ await state.broadcast_event(LspUpdatedEvent.create())
139
+ print("[LSP] warmup_files task completed")
140
+
141
+ # Run warmup in background (don't block the event handler)
142
+ print("[LSP] Creating background task for warmup")
143
+ state.create_background_task(warmup_files(), name="lsp-warmup")
144
+
145
+
146
+ async def persist_message_to_storage(
147
+ state: ServerState,
148
+ msg: MessageWithParts,
149
+ session_id: str,
150
+ ) -> None:
151
+ """Persist an OpenCode message to storage.
152
+
153
+ Converts the OpenCode MessageWithParts to ChatMessage and saves it.
154
+
155
+ Args:
156
+ state: Server state with pool reference
157
+ msg: OpenCode message to persist
158
+ session_id: Session/conversation ID
159
+ """
160
+ if state.pool.storage is None:
161
+ return
162
+
163
+ try:
164
+ # Convert to ChatMessage
165
+ chat_msg = opencode_to_chat_message(msg, conversation_id=session_id)
166
+ # Persist via storage manager
167
+ await state.pool.storage.log_message(chat_msg)
168
+ except Exception: # noqa: BLE001
169
+ # Don't fail the request if storage fails
170
+ pass
171
+
172
+
173
+ router = APIRouter(prefix="/session/{session_id}", tags=["message"])
174
+
175
+
176
+ @router.get("/message")
177
+ async def list_messages(
178
+ session_id: str,
179
+ state: StateDep,
180
+ limit: int | None = Query(default=None),
181
+ ) -> list[MessageWithParts]:
182
+ """List messages in a session."""
183
+ session = await get_or_load_session(state, session_id)
184
+ if session is None:
185
+ raise HTTPException(status_code=404, detail="Session not found")
186
+
187
+ messages = state.messages.get(session_id, [])
188
+ if limit:
189
+ messages = messages[-limit:]
190
+ return messages
191
+
192
+
193
+ async def _process_message( # noqa: PLR0915
194
+ session_id: str,
195
+ request: MessageRequest,
196
+ state: StateDep,
197
+ ) -> MessageWithParts:
198
+ """Internal helper to process a message request.
199
+
200
+ This does the actual work of creating messages, running the agent,
201
+ and broadcasting events. Used by both sync and async endpoints.
202
+ """
203
+ session = await get_or_load_session(state, session_id)
204
+ if session is None:
205
+ raise HTTPException(status_code=404, detail="Session not found")
206
+
207
+ now = now_ms()
208
+ # Create user message with sortable ID
209
+ user_msg_id = identifier.ascending("message", request.message_id)
210
+ user_message = UserMessage(
211
+ id=user_msg_id,
212
+ session_id=session_id,
213
+ time=TimeCreated(created=now),
214
+ agent=request.agent or "default",
215
+ model=UserMessageModel(
216
+ provider_id=request.model.provider_id if request.model else "agentpool",
217
+ model_id=request.model.model_id if request.model else "default",
218
+ ),
219
+ )
220
+
221
+ # Create parts from request
222
+ user_parts: list[Part] = [
223
+ TextPart(
224
+ id=identifier.ascending("part"),
225
+ message_id=user_msg_id,
226
+ session_id=session_id,
227
+ text=part.text,
228
+ )
229
+ for part in request.parts
230
+ if part.type == "text"
231
+ ]
232
+ user_msg_with_parts = MessageWithParts(info=user_message, parts=user_parts)
233
+ state.messages[session_id].append(user_msg_with_parts)
234
+ # Persist user message to storage
235
+ await persist_message_to_storage(state, user_msg_with_parts, session_id)
236
+ # Broadcast user message created event
237
+ await state.broadcast_event(MessageUpdatedEvent.create(user_message))
238
+ # Broadcast user message parts so they appear in UI
239
+ for part in user_parts:
240
+ await state.broadcast_event(PartUpdatedEvent.create(part))
241
+ state.session_status[session_id] = SessionStatus(type="busy")
242
+ status_event = SessionStatusEvent.create(session_id, SessionStatus(type="busy"))
243
+ await state.broadcast_event(status_event)
244
+ # Extract user prompt text
245
+ user_prompt = extract_user_prompt_from_parts([p.model_dump() for p in request.parts])
246
+ # Create assistant message with sortable ID (must come after user message)
247
+ assistant_msg_id = identifier.ascending("message")
248
+ tokens = Tokens(cache=TokensCache(read=0, write=0))
249
+ assistant_message = AssistantMessage(
250
+ id=assistant_msg_id,
251
+ session_id=session_id,
252
+ parent_id=user_msg_id, # Link to user message
253
+ model_id=request.model.model_id if request.model else "default",
254
+ provider_id=request.model.provider_id if request.model else "agentpool",
255
+ mode=request.agent or "default",
256
+ agent=request.agent or "default",
257
+ path=MessagePath(cwd=state.working_dir, root=state.working_dir),
258
+ time=MessageTime(created=now, completed=None),
259
+ tokens=tokens,
260
+ cost=0.0,
261
+ )
262
+ # Initialize assistant message with empty parts
263
+ assistant_msg_with_parts = MessageWithParts(info=assistant_message, parts=[])
264
+ state.messages[session_id].append(assistant_msg_with_parts)
265
+ # Broadcast assistant message created
266
+ await state.broadcast_event(MessageUpdatedEvent.create(assistant_message))
267
+ # Add step-start part
268
+ step_start = StepStartPart(
269
+ id=identifier.ascending("part"),
270
+ message_id=assistant_msg_id,
271
+ session_id=session_id,
272
+ )
273
+ assistant_msg_with_parts.parts.append(step_start)
274
+ await state.broadcast_event(PartUpdatedEvent.create(step_start))
275
+ # Call the agent
276
+ response_text = ""
277
+ input_tokens = 0
278
+ output_tokens = 0
279
+ total_cost = 0.0 # Cost in dollars
280
+ tool_parts: dict[str, ToolPart] = {} # Track tool parts by call_id
281
+ tool_outputs: dict[str, str] = {} # Track accumulated output per tool call
282
+ tool_inputs: dict[str, dict[str, Any]] = {} # Track inputs per tool call
283
+ # Track streaming text part for incremental updates
284
+ text_part: TextPart | None = None
285
+ text_part_id: str | None = None
286
+
287
+ try:
288
+ # Get the specified agent from the pool, or fall back to default
289
+ agent = state.agent
290
+ if request.agent and state.agent.agent_pool is not None:
291
+ agent = state.agent.agent_pool.all_agents.get(request.agent, state.agent)
292
+
293
+ # Stream events from the agent
294
+ async for event in agent.run_stream(user_prompt, conversation_id=session_id):
295
+ match event:
296
+ # Text streaming start
297
+ case PartStartEvent(part=PydanticTextPart(content=delta)):
298
+ response_text = delta
299
+ text_part_id = identifier.ascending("part")
300
+ text_part = TextPart(
301
+ id=text_part_id,
302
+ message_id=assistant_msg_id,
303
+ session_id=session_id,
304
+ text=delta,
305
+ )
306
+ assistant_msg_with_parts.parts.append(text_part)
307
+ await state.broadcast_event(PartUpdatedEvent.create(text_part, delta=delta))
308
+
309
+ # Text streaming delta
310
+ case PartDeltaEvent(delta=TextPartDelta(content_delta=delta)) if delta:
311
+ response_text += delta
312
+ if text_part is not None:
313
+ text_part = TextPart(
314
+ id=text_part.id,
315
+ message_id=assistant_msg_id,
316
+ session_id=session_id,
317
+ text=response_text,
318
+ )
319
+ # Update in parts list
320
+ for i, p in enumerate(assistant_msg_with_parts.parts):
321
+ if isinstance(p, TextPart) and p.id == text_part.id:
322
+ assistant_msg_with_parts.parts[i] = text_part
323
+ break
324
+ await state.broadcast_event(PartUpdatedEvent.create(text_part, delta=delta))
325
+
326
+ # Tool call start - from Claude Code agent or toolsets
327
+ case ToolCallStartEvent(
328
+ tool_name=tool_name,
329
+ tool_call_id=tool_call_id,
330
+ raw_input=raw_input,
331
+ title=title,
332
+ ):
333
+ # Convert param names for OpenCode TUI compatibility
334
+ ui_input = _convert_params_for_ui(raw_input) if raw_input else {}
335
+ if tool_call_id in tool_parts:
336
+ # Update existing part with the custom title
337
+ existing = tool_parts[tool_call_id]
338
+ tool_inputs[tool_call_id] = ui_input or tool_inputs.get(tool_call_id, {})
339
+
340
+ updated = ToolPart(
341
+ id=existing.id,
342
+ message_id=existing.message_id,
343
+ session_id=existing.session_id,
344
+ tool=existing.tool,
345
+ call_id=existing.call_id,
346
+ state=ToolStateRunning(
347
+ status="running",
348
+ time=TimeStart(start=now_ms()),
349
+ input=tool_inputs[tool_call_id],
350
+ title=title,
351
+ ),
352
+ )
353
+ tool_parts[tool_call_id] = updated
354
+ for i, p in enumerate(assistant_msg_with_parts.parts):
355
+ if isinstance(p, ToolPart) and p.id == existing.id:
356
+ assistant_msg_with_parts.parts[i] = updated
357
+ break
358
+ await state.broadcast_event(PartUpdatedEvent.create(updated))
359
+ else:
360
+ # Create new tool part with the title
361
+ tool_inputs[tool_call_id] = ui_input
362
+ tool_outputs[tool_call_id] = ""
363
+ tool_state = ToolStateRunning(
364
+ status="running",
365
+ time=TimeStart(start=now_ms()),
366
+ input=ui_input,
367
+ title=title,
368
+ )
369
+ tool_part = ToolPart(
370
+ id=identifier.ascending("part"),
371
+ message_id=assistant_msg_id,
372
+ session_id=session_id,
373
+ tool=tool_name,
374
+ call_id=tool_call_id,
375
+ state=tool_state,
376
+ )
377
+ tool_parts[tool_call_id] = tool_part
378
+ assistant_msg_with_parts.parts.append(tool_part)
379
+ await state.broadcast_event(PartUpdatedEvent.create(tool_part))
380
+
381
+ # Pydantic-ai tool call events (fallback for pydantic-ai agents)
382
+ case (
383
+ FunctionToolCallEvent(part=tc_part)
384
+ | PartStartEvent(part=PydanticToolCallPart() as tc_part)
385
+ ) if tc_part.tool_call_id not in tool_parts:
386
+ tool_call_id = tc_part.tool_call_id
387
+ tool_name = tc_part.tool_name
388
+ raw_input = safe_args_as_dict(tc_part)
389
+ # Convert param names for OpenCode TUI compatibility
390
+ ui_input = _convert_params_for_ui(raw_input)
391
+ # Store input and initialize output accumulator
392
+ tool_inputs[tool_call_id] = ui_input
393
+ tool_outputs[tool_call_id] = ""
394
+ # Derive initial title; toolset events may update it later
395
+ rich_info = derive_rich_tool_info(tool_name, raw_input)
396
+ tool_state = ToolStateRunning(
397
+ status="running",
398
+ time=TimeStart(start=now_ms()),
399
+ input=ui_input,
400
+ title=rich_info.title,
401
+ )
402
+ tool_part = ToolPart(
403
+ id=identifier.ascending("part"),
404
+ message_id=assistant_msg_id,
405
+ session_id=session_id,
406
+ tool=tool_name,
407
+ call_id=tool_call_id,
408
+ state=tool_state,
409
+ )
410
+ tool_parts[tool_call_id] = tool_part
411
+ assistant_msg_with_parts.parts.append(tool_part)
412
+ await state.broadcast_event(PartUpdatedEvent.create(tool_part))
413
+
414
+ # Tool call progress
415
+ case ToolCallProgressEvent(
416
+ tool_call_id=tool_call_id,
417
+ title=title,
418
+ items=items,
419
+ tool_name=tool_name,
420
+ tool_input=event_tool_input,
421
+ ) if tool_call_id:
422
+ # Extract text content from items and accumulate
423
+ # TODO: Handle TerminalContentItem for bash tool streaming - need to
424
+ # properly stream terminal output to OpenCode UI metadata
425
+ new_output = ""
426
+ file_paths: list[str] = []
427
+ for item in items:
428
+ if isinstance(item, TextContentItem):
429
+ new_output += item.text
430
+ elif isinstance(item, FileContentItem):
431
+ new_output += item.content
432
+ file_paths.append(item.path)
433
+ elif isinstance(item, LocationContentItem):
434
+ file_paths.append(item.path)
435
+
436
+ # Warm up LSP servers for accessed files (async, don't wait)
437
+ if file_paths:
438
+ _warmup_lsp_for_files(state, file_paths)
439
+
440
+ # Accumulate output (OpenCode streams via metadata.output)
441
+ if new_output:
442
+ tool_outputs[tool_call_id] = tool_outputs.get(tool_call_id, "") + new_output
443
+
444
+ if tool_call_id in tool_parts:
445
+ # Update existing part
446
+ existing = tool_parts[tool_call_id]
447
+ existing_title = getattr(existing.state, "title", "")
448
+ tool_input = tool_inputs.get(tool_call_id, {})
449
+ accumulated_output = tool_outputs.get(tool_call_id, "")
450
+ tool_state = ToolStateRunning(
451
+ status="running",
452
+ time=TimeStart(start=now_ms()),
453
+ title=title or existing_title,
454
+ input=tool_input,
455
+ metadata={"output": accumulated_output} if accumulated_output else None,
456
+ )
457
+ updated = ToolPart(
458
+ id=existing.id,
459
+ message_id=existing.message_id,
460
+ session_id=existing.session_id,
461
+ tool=existing.tool,
462
+ call_id=existing.call_id,
463
+ state=tool_state,
464
+ )
465
+ tool_parts[tool_call_id] = updated
466
+ for i, p in enumerate(assistant_msg_with_parts.parts):
467
+ if isinstance(p, ToolPart) and p.id == existing.id:
468
+ assistant_msg_with_parts.parts[i] = updated
469
+ break
470
+ await state.broadcast_event(PartUpdatedEvent.create(updated))
471
+ else:
472
+ # Create new tool part from progress event
473
+ ui_input = (
474
+ _convert_params_for_ui(event_tool_input) if event_tool_input else {}
475
+ )
476
+ tool_inputs[tool_call_id] = ui_input
477
+ accumulated_output = tool_outputs.get(tool_call_id, "")
478
+ tool_state = ToolStateRunning(
479
+ status="running",
480
+ time=TimeStart(start=now_ms()),
481
+ input=ui_input,
482
+ title=title or tool_name or "Running...",
483
+ metadata={"output": accumulated_output} if accumulated_output else None,
484
+ )
485
+ tool_part = ToolPart(
486
+ id=identifier.ascending("part"),
487
+ message_id=assistant_msg_id,
488
+ session_id=session_id,
489
+ tool=tool_name or "unknown",
490
+ call_id=tool_call_id,
491
+ state=tool_state,
492
+ )
493
+ tool_parts[tool_call_id] = tool_part
494
+ assistant_msg_with_parts.parts.append(tool_part)
495
+ await state.broadcast_event(PartUpdatedEvent.create(tool_part))
496
+
497
+ # Tool call complete
498
+ case ToolCallCompleteEvent(
499
+ tool_call_id=tool_call_id,
500
+ tool_result=result,
501
+ metadata=event_metadata,
502
+ ) if tool_call_id in tool_parts:
503
+ existing = tool_parts[tool_call_id]
504
+ result_str = str(result) if result else ""
505
+ tool_input = tool_inputs.get(tool_call_id, {})
506
+ is_error = isinstance(result, dict) and result.get("error")
507
+
508
+ if is_error:
509
+ new_state: ToolStateCompleted | ToolStateError = ToolStateError(
510
+ status="error",
511
+ error=str(result.get("error", "Unknown error")),
512
+ input=tool_input,
513
+ time=TimeStartEnd(start=now, end=now_ms()),
514
+ )
515
+ else:
516
+ new_state = ToolStateCompleted(
517
+ status="completed",
518
+ title=f"Completed {existing.tool}",
519
+ input=tool_input,
520
+ output=result_str,
521
+ metadata=event_metadata or {},
522
+ time=TimeStartEndCompacted(start=now, end=now_ms()),
523
+ )
524
+
525
+ updated = ToolPart(
526
+ id=existing.id,
527
+ message_id=existing.message_id,
528
+ session_id=existing.session_id,
529
+ tool=existing.tool,
530
+ call_id=existing.call_id,
531
+ state=new_state,
532
+ )
533
+ tool_parts[tool_call_id] = updated
534
+ for i, p in enumerate(assistant_msg_with_parts.parts):
535
+ if isinstance(p, ToolPart) and p.id == existing.id:
536
+ assistant_msg_with_parts.parts[i] = updated
537
+ break
538
+ await state.broadcast_event(PartUpdatedEvent.create(updated))
539
+
540
+ # Stream complete - extract token usage and cost
541
+ case StreamCompleteEvent(message=msg) if msg:
542
+ if msg.usage:
543
+ input_tokens = msg.usage.input_tokens or 0
544
+ output_tokens = msg.usage.output_tokens or 0
545
+ if msg.cost_info and msg.cost_info.total_cost:
546
+ # Cost is in Decimal dollars, OpenCode expects float dollars
547
+ total_cost = float(msg.cost_info.total_cost)
548
+
549
+ # Sub-agent/team event - show final results only
550
+ case SubAgentEvent(
551
+ source_name=source_name,
552
+ source_type=source_type,
553
+ event=wrapped_event,
554
+ depth=depth,
555
+ ):
556
+ indent = " " * (depth - 1)
557
+
558
+ match wrapped_event:
559
+ # Final message from sub-agent/team
560
+ case StreamCompleteEvent(message=msg):
561
+ # Show indicator
562
+ icon = "⚡" if source_type == "team_parallel" else "→"
563
+ type_label = (
564
+ " (parallel)"
565
+ if source_type == "team_parallel"
566
+ else " (sequential)"
567
+ if source_type == "team_sequential"
568
+ else ""
569
+ )
570
+ indicator = f"{indent}{icon} {source_name}{type_label}"
571
+
572
+ indicator_part = TextPart(
573
+ id=identifier.ascending("part"),
574
+ message_id=assistant_msg_id,
575
+ session_id=session_id,
576
+ text=indicator,
577
+ time=TimeStartEndOptional(start=now_ms()),
578
+ )
579
+ assistant_msg_with_parts.parts.append(indicator_part)
580
+ await state.broadcast_event(PartUpdatedEvent.create(indicator_part))
581
+
582
+ # Show complete message content
583
+ content = str(msg.content) if msg.content else "(no output)"
584
+ content_part = TextPart(
585
+ id=identifier.ascending("part"),
586
+ message_id=assistant_msg_id,
587
+ session_id=session_id,
588
+ text=content,
589
+ time=TimeStartEndOptional(start=now_ms()),
590
+ )
591
+ assistant_msg_with_parts.parts.append(content_part)
592
+ await state.broadcast_event(PartUpdatedEvent.create(content_part))
593
+
594
+ # Tool call completed - show one-line summary
595
+ case ToolCallCompleteEvent(tool_name=tool_name, tool_result=result):
596
+ # Preview result (first 60 chars)
597
+ result_str = str(result) if result else ""
598
+ preview = (
599
+ result_str[:60] + "..." if len(result_str) > 60 else result_str # noqa: PLR2004
600
+ )
601
+ summary = f"{indent} ├─ {tool_name}: {preview}"
602
+
603
+ summary_part = TextPart(
604
+ id=identifier.ascending("part"),
605
+ message_id=assistant_msg_id,
606
+ session_id=session_id,
607
+ text=summary,
608
+ time=TimeStartEndOptional(start=now_ms()),
609
+ )
610
+ assistant_msg_with_parts.parts.append(summary_part)
611
+ await state.broadcast_event(PartUpdatedEvent.create(summary_part))
612
+
613
+ # Compaction event - emit session.compacted SSE event
614
+ case CompactionEvent(session_id=compact_session_id, phase=phase):
615
+ if phase == "completed":
616
+ await state.broadcast_event(
617
+ SessionCompactedEvent.create(session_id=compact_session_id)
618
+ )
619
+
620
+ except Exception as e: # noqa: BLE001
621
+ response_text = f"Error calling agent: {e}"
622
+ # Emit session error event
623
+ await state.broadcast_event(
624
+ SessionErrorEvent.create(
625
+ session_id=session_id,
626
+ error_name=type(e).__name__,
627
+ error_message=str(e),
628
+ )
629
+ )
630
+
631
+ response_time = now_ms()
632
+
633
+ # Create text part with response (only if we didn't stream it already)
634
+ if response_text and text_part is None:
635
+ text_part = TextPart(
636
+ id=identifier.ascending("part"),
637
+ message_id=assistant_msg_id,
638
+ session_id=session_id,
639
+ text=response_text,
640
+ time=TimeStartEndOptional(start=now, end=response_time),
641
+ )
642
+ assistant_msg_with_parts.parts.append(text_part)
643
+
644
+ # Broadcast text part update
645
+ await state.broadcast_event(PartUpdatedEvent.create(text_part))
646
+ elif text_part is not None:
647
+ # Update the streamed text part with final timing
648
+ final_text_part = TextPart(
649
+ id=text_part.id,
650
+ message_id=assistant_msg_id,
651
+ session_id=session_id,
652
+ text=response_text,
653
+ time=TimeStartEndOptional(start=now, end=response_time),
654
+ )
655
+ # Update in parts list
656
+ for i, p in enumerate(assistant_msg_with_parts.parts):
657
+ if isinstance(p, TextPart) and p.id == text_part.id:
658
+ assistant_msg_with_parts.parts[i] = final_text_part
659
+ break
660
+
661
+ step_finish = StepFinishPart(
662
+ id=identifier.ascending("part"),
663
+ message_id=assistant_msg_id,
664
+ session_id=session_id,
665
+ tokens=StepFinishTokens(
666
+ cache=TokenCache(read=0, write=0),
667
+ input=input_tokens,
668
+ output=output_tokens,
669
+ reasoning=0,
670
+ ),
671
+ cost=total_cost,
672
+ )
673
+ assistant_msg_with_parts.parts.append(step_finish)
674
+ await state.broadcast_event(PartUpdatedEvent.create(step_finish))
675
+
676
+ print(f"Response text: {response_text[:100] if response_text else 'EMPTY'}...")
677
+
678
+ # Update assistant message with final timing and tokens
679
+ updated_assistant = assistant_message.model_copy(
680
+ update={
681
+ "time": MessageTime(created=now, completed=response_time),
682
+ "tokens": Tokens(
683
+ cache=TokensCache(read=0, write=0),
684
+ input=input_tokens,
685
+ output=output_tokens,
686
+ reasoning=0,
687
+ ),
688
+ "cost": total_cost,
689
+ }
690
+ )
691
+ assistant_msg_with_parts.info = updated_assistant
692
+
693
+ # Broadcast final message update
694
+ await state.broadcast_event(MessageUpdatedEvent.create(updated_assistant))
695
+ # Persist assistant message to storage
696
+ await persist_message_to_storage(state, assistant_msg_with_parts, session_id)
697
+ # Mark session as not running
698
+ state.session_status[session_id] = SessionStatus(type="idle")
699
+ await state.broadcast_event(SessionStatusEvent.create(session_id, SessionStatus(type="idle")))
700
+ await state.broadcast_event(SessionIdleEvent.create(session_id))
701
+
702
+ # Update session timestamp
703
+ session = state.sessions[session_id]
704
+ state.sessions[session_id] = session.model_copy(
705
+ update={"time": TimeCreatedUpdated(created=session.time.created, updated=response_time)}
706
+ )
707
+ # Title generation now handled by StorageManager signal (on_title_generated in server.py)
708
+ # Agent calls log_conversation() → _generate_title_from_prompt() → emits title_generated signal
709
+ return assistant_msg_with_parts
710
+
711
+
712
+ @router.post("/message")
713
+ async def send_message(
714
+ session_id: str,
715
+ request: MessageRequest,
716
+ state: StateDep,
717
+ ) -> MessageWithParts:
718
+ """Send a message and wait for the agent's response.
719
+
720
+ This is the synchronous version - waits for completion before returning.
721
+ For async processing, use POST /session/{id}/prompt_async instead.
722
+ """
723
+ return await _process_message(session_id, request, state)
724
+
725
+
726
+ @router.post("/prompt_async", status_code=status.HTTP_204_NO_CONTENT)
727
+ async def send_message_async(
728
+ session_id: str,
729
+ request: MessageRequest,
730
+ state: StateDep,
731
+ ) -> None:
732
+ """Send a message asynchronously without waiting for response.
733
+
734
+ Starts the agent processing in the background and returns immediately.
735
+ Client should listen to SSE events to get updates.
736
+
737
+ Returns 204 No Content immediately.
738
+ """
739
+ # Create background task to process the message
740
+ state.create_background_task(
741
+ _process_message(session_id, request, state),
742
+ name=f"process_message_{session_id}",
743
+ )
744
+
745
+
746
+ @router.get("/message/{message_id}")
747
+ async def get_message(
748
+ session_id: str,
749
+ message_id: str,
750
+ state: StateDep,
751
+ ) -> MessageWithParts:
752
+ """Get a specific message."""
753
+ session = await get_or_load_session(state, session_id)
754
+ if session is None:
755
+ raise HTTPException(status_code=404, detail="Session not found")
756
+
757
+ for msg in state.messages.get(session_id, []):
758
+ if msg.info.id == message_id:
759
+ return msg
760
+
761
+ raise HTTPException(status_code=404, detail="Message not found")