codex-autorunner 0.1.2__py3-none-any.whl → 1.1.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 (276) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/__main__.py +4 -0
  3. codex_autorunner/agents/codex/harness.py +1 -1
  4. codex_autorunner/agents/opencode/client.py +68 -35
  5. codex_autorunner/agents/opencode/constants.py +3 -0
  6. codex_autorunner/agents/opencode/harness.py +6 -1
  7. codex_autorunner/agents/opencode/logging.py +21 -5
  8. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  9. codex_autorunner/agents/opencode/runtime.py +176 -47
  10. codex_autorunner/agents/opencode/supervisor.py +36 -48
  11. codex_autorunner/agents/registry.py +155 -8
  12. codex_autorunner/api.py +25 -0
  13. codex_autorunner/bootstrap.py +22 -37
  14. codex_autorunner/cli.py +5 -1156
  15. codex_autorunner/codex_cli.py +20 -84
  16. codex_autorunner/core/__init__.py +4 -0
  17. codex_autorunner/core/about_car.py +49 -32
  18. codex_autorunner/core/adapter_utils.py +21 -0
  19. codex_autorunner/core/app_server_ids.py +59 -0
  20. codex_autorunner/core/app_server_logging.py +7 -3
  21. codex_autorunner/core/app_server_prompts.py +27 -260
  22. codex_autorunner/core/app_server_threads.py +26 -28
  23. codex_autorunner/core/app_server_utils.py +165 -0
  24. codex_autorunner/core/archive.py +349 -0
  25. codex_autorunner/core/codex_runner.py +12 -2
  26. codex_autorunner/core/config.py +587 -103
  27. codex_autorunner/core/docs.py +10 -2
  28. codex_autorunner/core/drafts.py +136 -0
  29. codex_autorunner/core/engine.py +1531 -866
  30. codex_autorunner/core/exceptions.py +4 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +202 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +88 -0
  35. codex_autorunner/core/flows/reasons.py +52 -0
  36. codex_autorunner/core/flows/reconciler.py +131 -0
  37. codex_autorunner/core/flows/runtime.py +382 -0
  38. codex_autorunner/core/flows/store.py +568 -0
  39. codex_autorunner/core/flows/transition.py +138 -0
  40. codex_autorunner/core/flows/ux_helpers.py +257 -0
  41. codex_autorunner/core/flows/worker_process.py +242 -0
  42. codex_autorunner/core/git_utils.py +62 -0
  43. codex_autorunner/core/hub.py +136 -16
  44. codex_autorunner/core/locks.py +4 -0
  45. codex_autorunner/core/notifications.py +14 -2
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/core/ports/agent_backend.py +150 -0
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/core/ports/run_event.py +91 -0
  50. codex_autorunner/core/prompt.py +15 -7
  51. codex_autorunner/core/redaction.py +29 -0
  52. codex_autorunner/core/review_context.py +5 -8
  53. codex_autorunner/core/run_index.py +6 -0
  54. codex_autorunner/core/runner_process.py +5 -2
  55. codex_autorunner/core/state.py +0 -88
  56. codex_autorunner/core/state_roots.py +57 -0
  57. codex_autorunner/core/supervisor_protocol.py +15 -0
  58. codex_autorunner/core/supervisor_utils.py +67 -0
  59. codex_autorunner/core/text_delta_coalescer.py +54 -0
  60. codex_autorunner/core/ticket_linter_cli.py +201 -0
  61. codex_autorunner/core/ticket_manager_cli.py +432 -0
  62. codex_autorunner/core/update.py +24 -16
  63. codex_autorunner/core/update_paths.py +28 -0
  64. codex_autorunner/core/update_runner.py +2 -0
  65. codex_autorunner/core/usage.py +164 -12
  66. codex_autorunner/core/utils.py +120 -11
  67. codex_autorunner/discovery.py +2 -4
  68. codex_autorunner/flows/review/__init__.py +17 -0
  69. codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
  70. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  71. codex_autorunner/flows/ticket_flow/definition.py +98 -0
  72. codex_autorunner/integrations/agents/__init__.py +17 -0
  73. codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
  74. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  75. codex_autorunner/integrations/agents/codex_backend.py +448 -0
  76. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  77. codex_autorunner/integrations/agents/opencode_backend.py +598 -0
  78. codex_autorunner/integrations/agents/runner.py +91 -0
  79. codex_autorunner/integrations/agents/wiring.py +271 -0
  80. codex_autorunner/integrations/app_server/client.py +583 -152
  81. codex_autorunner/integrations/app_server/env.py +2 -107
  82. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  83. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  84. codex_autorunner/integrations/telegram/adapter.py +204 -165
  85. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  86. codex_autorunner/integrations/telegram/config.py +221 -0
  87. codex_autorunner/integrations/telegram/constants.py +17 -2
  88. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  89. codex_autorunner/integrations/telegram/doctor.py +47 -0
  90. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
  91. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  92. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  93. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  94. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
  95. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  96. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  97. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  98. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
  99. codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
  100. codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
  101. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  102. codex_autorunner/integrations/telegram/helpers.py +111 -16
  103. codex_autorunner/integrations/telegram/outbox.py +208 -37
  104. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  105. codex_autorunner/integrations/telegram/service.py +221 -42
  106. codex_autorunner/integrations/telegram/state.py +100 -2
  107. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
  108. codex_autorunner/integrations/telegram/transport.py +39 -4
  109. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  110. codex_autorunner/manifest.py +2 -0
  111. codex_autorunner/plugin_api.py +22 -0
  112. codex_autorunner/routes/__init__.py +37 -67
  113. codex_autorunner/routes/agents.py +2 -137
  114. codex_autorunner/routes/analytics.py +3 -0
  115. codex_autorunner/routes/app_server.py +2 -131
  116. codex_autorunner/routes/base.py +2 -624
  117. codex_autorunner/routes/file_chat.py +7 -0
  118. codex_autorunner/routes/flows.py +7 -0
  119. codex_autorunner/routes/messages.py +7 -0
  120. codex_autorunner/routes/repos.py +2 -196
  121. codex_autorunner/routes/review.py +2 -147
  122. codex_autorunner/routes/sessions.py +2 -175
  123. codex_autorunner/routes/settings.py +2 -168
  124. codex_autorunner/routes/shared.py +2 -275
  125. codex_autorunner/routes/system.py +4 -188
  126. codex_autorunner/routes/usage.py +3 -0
  127. codex_autorunner/routes/voice.py +2 -119
  128. codex_autorunner/routes/workspace.py +3 -0
  129. codex_autorunner/server.py +3 -2
  130. codex_autorunner/static/agentControls.js +41 -11
  131. codex_autorunner/static/agentEvents.js +248 -0
  132. codex_autorunner/static/app.js +35 -24
  133. codex_autorunner/static/archive.js +826 -0
  134. codex_autorunner/static/archiveApi.js +37 -0
  135. codex_autorunner/static/autoRefresh.js +36 -8
  136. codex_autorunner/static/bootstrap.js +1 -0
  137. codex_autorunner/static/bus.js +1 -0
  138. codex_autorunner/static/cache.js +1 -0
  139. codex_autorunner/static/constants.js +20 -4
  140. codex_autorunner/static/dashboard.js +344 -325
  141. codex_autorunner/static/diffRenderer.js +37 -0
  142. codex_autorunner/static/docChatCore.js +324 -0
  143. codex_autorunner/static/docChatStorage.js +65 -0
  144. codex_autorunner/static/docChatVoice.js +65 -0
  145. codex_autorunner/static/docEditor.js +133 -0
  146. codex_autorunner/static/env.js +1 -0
  147. codex_autorunner/static/eventSummarizer.js +166 -0
  148. codex_autorunner/static/fileChat.js +182 -0
  149. codex_autorunner/static/health.js +155 -0
  150. codex_autorunner/static/hub.js +126 -185
  151. codex_autorunner/static/index.html +839 -863
  152. codex_autorunner/static/liveUpdates.js +1 -0
  153. codex_autorunner/static/loader.js +1 -0
  154. codex_autorunner/static/messages.js +873 -0
  155. codex_autorunner/static/mobileCompact.js +2 -1
  156. codex_autorunner/static/preserve.js +17 -0
  157. codex_autorunner/static/settings.js +149 -217
  158. codex_autorunner/static/smartRefresh.js +52 -0
  159. codex_autorunner/static/styles.css +8850 -3876
  160. codex_autorunner/static/tabs.js +175 -11
  161. codex_autorunner/static/terminal.js +32 -0
  162. codex_autorunner/static/terminalManager.js +34 -59
  163. codex_autorunner/static/ticketChatActions.js +333 -0
  164. codex_autorunner/static/ticketChatEvents.js +16 -0
  165. codex_autorunner/static/ticketChatStorage.js +16 -0
  166. codex_autorunner/static/ticketChatStream.js +264 -0
  167. codex_autorunner/static/ticketEditor.js +844 -0
  168. codex_autorunner/static/ticketVoice.js +9 -0
  169. codex_autorunner/static/tickets.js +1988 -0
  170. codex_autorunner/static/utils.js +43 -3
  171. codex_autorunner/static/voice.js +1 -0
  172. codex_autorunner/static/workspace.js +765 -0
  173. codex_autorunner/static/workspaceApi.js +53 -0
  174. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  175. codex_autorunner/surfaces/__init__.py +5 -0
  176. codex_autorunner/surfaces/cli/__init__.py +6 -0
  177. codex_autorunner/surfaces/cli/cli.py +1224 -0
  178. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  179. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  180. codex_autorunner/surfaces/web/__init__.py +1 -0
  181. codex_autorunner/surfaces/web/app.py +2019 -0
  182. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  183. codex_autorunner/surfaces/web/middleware.py +587 -0
  184. codex_autorunner/surfaces/web/pty_session.py +370 -0
  185. codex_autorunner/surfaces/web/review.py +6 -0
  186. codex_autorunner/surfaces/web/routes/__init__.py +78 -0
  187. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  188. codex_autorunner/surfaces/web/routes/analytics.py +277 -0
  189. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  190. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  191. codex_autorunner/surfaces/web/routes/base.py +615 -0
  192. codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
  193. codex_autorunner/surfaces/web/routes/flows.py +1164 -0
  194. codex_autorunner/surfaces/web/routes/messages.py +459 -0
  195. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  196. codex_autorunner/surfaces/web/routes/review.py +148 -0
  197. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  198. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  199. codex_autorunner/surfaces/web/routes/shared.py +280 -0
  200. codex_autorunner/surfaces/web/routes/system.py +196 -0
  201. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  202. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  203. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  204. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  205. codex_autorunner/surfaces/web/schemas.py +417 -0
  206. codex_autorunner/surfaces/web/static_assets.py +490 -0
  207. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  208. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  209. codex_autorunner/tickets/__init__.py +27 -0
  210. codex_autorunner/tickets/agent_pool.py +399 -0
  211. codex_autorunner/tickets/files.py +89 -0
  212. codex_autorunner/tickets/frontmatter.py +55 -0
  213. codex_autorunner/tickets/lint.py +102 -0
  214. codex_autorunner/tickets/models.py +97 -0
  215. codex_autorunner/tickets/outbox.py +244 -0
  216. codex_autorunner/tickets/replies.py +179 -0
  217. codex_autorunner/tickets/runner.py +881 -0
  218. codex_autorunner/tickets/spec_ingest.py +77 -0
  219. codex_autorunner/web/__init__.py +5 -1
  220. codex_autorunner/web/app.py +2 -1771
  221. codex_autorunner/web/hub_jobs.py +2 -191
  222. codex_autorunner/web/middleware.py +2 -587
  223. codex_autorunner/web/pty_session.py +2 -369
  224. codex_autorunner/web/runner_manager.py +2 -24
  225. codex_autorunner/web/schemas.py +2 -396
  226. codex_autorunner/web/static_assets.py +4 -484
  227. codex_autorunner/web/static_refresh.py +2 -85
  228. codex_autorunner/web/terminal_sessions.py +2 -77
  229. codex_autorunner/workspace/__init__.py +40 -0
  230. codex_autorunner/workspace/paths.py +335 -0
  231. codex_autorunner-1.1.0.dist-info/METADATA +154 -0
  232. codex_autorunner-1.1.0.dist-info/RECORD +308 -0
  233. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
  234. codex_autorunner/agents/execution/policy.py +0 -292
  235. codex_autorunner/agents/factory.py +0 -52
  236. codex_autorunner/agents/orchestrator.py +0 -358
  237. codex_autorunner/core/doc_chat.py +0 -1446
  238. codex_autorunner/core/snapshot.py +0 -580
  239. codex_autorunner/integrations/github/chatops.py +0 -268
  240. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  241. codex_autorunner/routes/docs.py +0 -381
  242. codex_autorunner/routes/github.py +0 -327
  243. codex_autorunner/routes/runs.py +0 -250
  244. codex_autorunner/spec_ingest.py +0 -812
  245. codex_autorunner/static/docChatActions.js +0 -287
  246. codex_autorunner/static/docChatEvents.js +0 -300
  247. codex_autorunner/static/docChatRender.js +0 -205
  248. codex_autorunner/static/docChatStream.js +0 -361
  249. codex_autorunner/static/docs.js +0 -20
  250. codex_autorunner/static/docsClipboard.js +0 -69
  251. codex_autorunner/static/docsCrud.js +0 -257
  252. codex_autorunner/static/docsDocUpdates.js +0 -62
  253. codex_autorunner/static/docsDrafts.js +0 -16
  254. codex_autorunner/static/docsElements.js +0 -69
  255. codex_autorunner/static/docsInit.js +0 -285
  256. codex_autorunner/static/docsParse.js +0 -160
  257. codex_autorunner/static/docsSnapshot.js +0 -87
  258. codex_autorunner/static/docsSpecIngest.js +0 -263
  259. codex_autorunner/static/docsState.js +0 -127
  260. codex_autorunner/static/docsThreadRegistry.js +0 -44
  261. codex_autorunner/static/docsUi.js +0 -153
  262. codex_autorunner/static/docsVoice.js +0 -56
  263. codex_autorunner/static/github.js +0 -504
  264. codex_autorunner/static/logs.js +0 -678
  265. codex_autorunner/static/review.js +0 -157
  266. codex_autorunner/static/runs.js +0 -418
  267. codex_autorunner/static/snapshot.js +0 -124
  268. codex_autorunner/static/state.js +0 -94
  269. codex_autorunner/static/todoPreview.js +0 -27
  270. codex_autorunner/workspace.py +0 -16
  271. codex_autorunner-0.1.2.dist-info/METADATA +0 -249
  272. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  273. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  274. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
  275. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
  276. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,448 @@
1
+ import asyncio
2
+ import logging
3
+ from pathlib import Path
4
+ from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, Optional, Union
5
+
6
+ from ...core.circuit_breaker import CircuitBreaker
7
+ from ...core.ports.agent_backend import AgentBackend, AgentEvent, now_iso
8
+ from ...core.ports.run_event import (
9
+ ApprovalRequested,
10
+ Completed,
11
+ Failed,
12
+ OutputDelta,
13
+ RunEvent,
14
+ Started,
15
+ ToolCall,
16
+ )
17
+ from ...integrations.app_server.client import CodexAppServerClient, CodexAppServerError
18
+
19
+ _logger = logging.getLogger(__name__)
20
+
21
+ ApprovalDecision = Union[str, Dict[str, Any]]
22
+ NotificationHandler = Callable[[Dict[str, Any]], Awaitable[None]]
23
+
24
+
25
+ class CodexAppServerBackend(AgentBackend):
26
+ def __init__(
27
+ self,
28
+ command: list[str],
29
+ *,
30
+ cwd: Optional[Path] = None,
31
+ env: Optional[Dict[str, str]] = None,
32
+ approval_policy: Optional[str] = None,
33
+ sandbox_policy: Optional[str] = None,
34
+ model: Optional[str] = None,
35
+ reasoning_effort: Optional[str] = None,
36
+ turn_timeout_seconds: Optional[float] = None,
37
+ auto_restart: Optional[bool] = None,
38
+ request_timeout: Optional[float] = None,
39
+ turn_stall_timeout_seconds: Optional[float] = None,
40
+ turn_stall_poll_interval_seconds: Optional[float] = None,
41
+ turn_stall_recovery_min_interval_seconds: Optional[float] = None,
42
+ max_message_bytes: Optional[int] = None,
43
+ oversize_preview_bytes: Optional[int] = None,
44
+ max_oversize_drain_bytes: Optional[int] = None,
45
+ restart_backoff_initial_seconds: Optional[float] = None,
46
+ restart_backoff_max_seconds: Optional[float] = None,
47
+ restart_backoff_jitter_ratio: Optional[float] = None,
48
+ notification_handler: Optional[NotificationHandler] = None,
49
+ logger: Optional[logging.Logger] = None,
50
+ ):
51
+ self._command = command
52
+ self._cwd = cwd
53
+ self._env = env
54
+ self._approval_policy = approval_policy
55
+ self._sandbox_policy = sandbox_policy
56
+ self._model = model
57
+ self._reasoning_effort = reasoning_effort
58
+ self._turn_timeout_seconds = turn_timeout_seconds
59
+ self._auto_restart = auto_restart
60
+ self._request_timeout = request_timeout
61
+ self._turn_stall_timeout_seconds = turn_stall_timeout_seconds
62
+ self._turn_stall_poll_interval_seconds = turn_stall_poll_interval_seconds
63
+ self._turn_stall_recovery_min_interval_seconds = (
64
+ turn_stall_recovery_min_interval_seconds
65
+ )
66
+ self._max_message_bytes = max_message_bytes
67
+ self._oversize_preview_bytes = oversize_preview_bytes
68
+ self._max_oversize_drain_bytes = max_oversize_drain_bytes
69
+ self._restart_backoff_initial_seconds = restart_backoff_initial_seconds
70
+ self._restart_backoff_max_seconds = restart_backoff_max_seconds
71
+ self._restart_backoff_jitter_ratio = restart_backoff_jitter_ratio
72
+ self._notification_handler = notification_handler
73
+ self._logger = logger or _logger
74
+
75
+ self._client: Optional[CodexAppServerClient] = None
76
+ self._session_id: Optional[str] = None
77
+ self._thread_id: Optional[str] = None
78
+ self._turn_id: Optional[str] = None
79
+ self._thread_info: Optional[Dict[str, Any]] = None
80
+
81
+ self._circuit_breaker = CircuitBreaker("CodexAppServer", logger=_logger)
82
+ self._event_queue: asyncio.Queue[RunEvent] = asyncio.Queue()
83
+
84
+ async def _ensure_client(self) -> CodexAppServerClient:
85
+ if self._client is None:
86
+ self._client = CodexAppServerClient(
87
+ self._command,
88
+ cwd=self._cwd,
89
+ env=self._env,
90
+ approval_handler=self._handle_approval_request,
91
+ notification_handler=self._handle_notification,
92
+ auto_restart=self._auto_restart,
93
+ request_timeout=self._request_timeout,
94
+ turn_stall_timeout_seconds=self._turn_stall_timeout_seconds,
95
+ turn_stall_poll_interval_seconds=self._turn_stall_poll_interval_seconds,
96
+ turn_stall_recovery_min_interval_seconds=self._turn_stall_recovery_min_interval_seconds,
97
+ max_message_bytes=self._max_message_bytes,
98
+ oversize_preview_bytes=self._oversize_preview_bytes,
99
+ max_oversize_drain_bytes=self._max_oversize_drain_bytes,
100
+ restart_backoff_initial_seconds=self._restart_backoff_initial_seconds,
101
+ restart_backoff_max_seconds=self._restart_backoff_max_seconds,
102
+ restart_backoff_jitter_ratio=self._restart_backoff_jitter_ratio,
103
+ logger=self._logger,
104
+ )
105
+ await self._client.start()
106
+ return self._client
107
+
108
+ def configure(
109
+ self,
110
+ *,
111
+ approval_policy: Optional[str],
112
+ sandbox_policy: Optional[str],
113
+ model: Optional[str],
114
+ reasoning_effort: Optional[str],
115
+ turn_timeout_seconds: Optional[float],
116
+ notification_handler: Optional[NotificationHandler],
117
+ ) -> None:
118
+ self._approval_policy = approval_policy
119
+ self._sandbox_policy = sandbox_policy
120
+ self._model = model
121
+ self._reasoning_effort = reasoning_effort
122
+ self._turn_timeout_seconds = turn_timeout_seconds
123
+ self._notification_handler = notification_handler
124
+
125
+ async def start_session(self, target: dict, context: dict) -> str:
126
+ client = await self._ensure_client()
127
+
128
+ repo_root = Path(context.get("workspace") or self._cwd or Path.cwd())
129
+ resume_session = context.get("session_id") or context.get("thread_id")
130
+ # Ensure we don't reuse a stale turn id when a new session begins.
131
+ self._turn_id = None
132
+ if isinstance(resume_session, str) and resume_session:
133
+ try:
134
+ resume_result = await client.thread_resume(resume_session)
135
+ if isinstance(resume_result, dict):
136
+ self._thread_info = resume_result
137
+ resumed_id = (
138
+ resume_result.get("id")
139
+ if isinstance(resume_result, dict)
140
+ else resume_session
141
+ )
142
+ self._thread_id = (
143
+ resumed_id if isinstance(resumed_id, str) else resume_session
144
+ )
145
+ except CodexAppServerError:
146
+ self._thread_id = None
147
+ self._thread_info = None
148
+
149
+ if not self._thread_id:
150
+ result = await client.thread_start(str(repo_root))
151
+ self._thread_info = result if isinstance(result, dict) else None
152
+ self._thread_id = result.get("id") if isinstance(result, dict) else None
153
+
154
+ if not self._thread_id:
155
+ raise RuntimeError("Failed to start thread: missing thread ID")
156
+
157
+ self._session_id = self._thread_id
158
+ _logger.info("Started Codex app-server session: %s", self._session_id)
159
+
160
+ return self._session_id
161
+
162
+ async def run_turn(
163
+ self, session_id: str, message: str
164
+ ) -> AsyncGenerator[AgentEvent, None]:
165
+ client = await self._ensure_client()
166
+
167
+ if session_id:
168
+ self._thread_id = session_id
169
+ # Reset last turn to avoid interrupting the wrong turn when reusing backends.
170
+ self._turn_id = None
171
+
172
+ if not self._thread_id:
173
+ await self.start_session(target={}, context={})
174
+
175
+ _logger.info(
176
+ "Running turn on thread %s with message: %s",
177
+ self._thread_id or "unknown",
178
+ message[:100],
179
+ )
180
+
181
+ turn_kwargs: Dict[str, Any] = {}
182
+ if self._model:
183
+ turn_kwargs["model"] = self._model
184
+ if self._reasoning_effort:
185
+ turn_kwargs["effort"] = self._reasoning_effort
186
+ handle = await client.turn_start(
187
+ self._thread_id if self._thread_id else "default",
188
+ text=message,
189
+ approval_policy=self._approval_policy,
190
+ sandbox_policy=self._sandbox_policy,
191
+ **turn_kwargs,
192
+ )
193
+ self._turn_id = handle.turn_id
194
+
195
+ yield AgentEvent.stream_delta(content=message, delta_type="user_message")
196
+
197
+ result = await handle.wait(timeout=self._turn_timeout_seconds)
198
+
199
+ for msg in result.agent_messages:
200
+ yield AgentEvent.stream_delta(content=msg, delta_type="assistant_message")
201
+
202
+ for event_data in result.raw_events:
203
+ yield self._parse_raw_event(event_data)
204
+
205
+ yield AgentEvent.message_complete(
206
+ final_message="\n".join(result.agent_messages)
207
+ )
208
+
209
+ async def run_turn_events(
210
+ self, session_id: str, message: str
211
+ ) -> AsyncGenerator[RunEvent, None]:
212
+ client = await self._ensure_client()
213
+
214
+ if session_id:
215
+ self._thread_id = session_id
216
+ self._turn_id = None
217
+
218
+ if not self._thread_id:
219
+ actual_session_id = await self.start_session(target={}, context={})
220
+ else:
221
+ actual_session_id = self._thread_id
222
+
223
+ _logger.info(
224
+ "Running turn events on thread %s with message: %s",
225
+ actual_session_id or "unknown",
226
+ message[:100],
227
+ )
228
+
229
+ yield Started(
230
+ timestamp=now_iso(),
231
+ session_id=actual_session_id,
232
+ thread_id=self._thread_id,
233
+ turn_id=self._turn_id,
234
+ )
235
+
236
+ yield OutputDelta(
237
+ timestamp=now_iso(), content=message, delta_type="user_message"
238
+ )
239
+
240
+ self._event_queue = asyncio.Queue()
241
+
242
+ turn_kwargs: dict[str, Any] = {}
243
+ if self._model:
244
+ turn_kwargs["model"] = self._model
245
+ if self._reasoning_effort:
246
+ turn_kwargs["effort"] = self._reasoning_effort
247
+ handle = await client.turn_start(
248
+ actual_session_id if actual_session_id else "default",
249
+ text=message,
250
+ approval_policy=self._approval_policy,
251
+ sandbox_policy=self._sandbox_policy,
252
+ **turn_kwargs,
253
+ )
254
+ self._turn_id = handle.turn_id
255
+
256
+ wait_task = asyncio.create_task(handle.wait(timeout=self._turn_timeout_seconds))
257
+
258
+ try:
259
+ while True:
260
+ if not self._event_queue.empty():
261
+ run_event = self._event_queue.get_nowait()
262
+ if run_event:
263
+ yield run_event
264
+ continue
265
+
266
+ get_task = asyncio.create_task(self._event_queue.get())
267
+ done_set, pending_set = await asyncio.wait(
268
+ {wait_task, get_task}, return_when=asyncio.FIRST_COMPLETED
269
+ )
270
+
271
+ if wait_task in done_set:
272
+ if get_task in pending_set:
273
+ get_task.cancel()
274
+ result = wait_task.result()
275
+ for msg in result.agent_messages:
276
+ yield OutputDelta(
277
+ timestamp=now_iso(),
278
+ content=msg,
279
+ delta_type="assistant_message",
280
+ )
281
+ # raw_events already contain the same notifications we streamed
282
+ # through _event_queue; skipping here avoids double-emitting.
283
+ while not self._event_queue.empty():
284
+ extra = self._event_queue.get_nowait()
285
+ if extra:
286
+ yield extra
287
+ yield Completed(
288
+ timestamp=now_iso(),
289
+ final_message="\n".join(result.agent_messages),
290
+ )
291
+ break
292
+
293
+ if get_task in done_set:
294
+ run_event = get_task.result()
295
+ if run_event:
296
+ yield run_event
297
+ for task in pending_set:
298
+ task.cancel()
299
+ except Exception as e:
300
+ _logger.error("Error during turn execution: %s", e)
301
+ if not wait_task.done():
302
+ wait_task.cancel()
303
+ yield Failed(timestamp=now_iso(), error_message=str(e))
304
+
305
+ async def stream_events(self, session_id: str) -> AsyncGenerator[AgentEvent, None]:
306
+ if False:
307
+ yield AgentEvent.stream_delta(content="", delta_type="noop")
308
+
309
+ async def interrupt(self, session_id: str) -> None:
310
+ target_thread = session_id or self._thread_id
311
+ target_turn = self._turn_id
312
+ if self._client and target_turn:
313
+ try:
314
+ await self._client.turn_interrupt(target_turn, thread_id=target_thread)
315
+ _logger.info(
316
+ "Interrupted turn %s on thread %s",
317
+ target_turn,
318
+ target_thread or "unknown",
319
+ )
320
+ return
321
+ except Exception as e:
322
+ _logger.warning("Failed to interrupt turn: %s", e)
323
+ return
324
+ if self._client and target_thread:
325
+ _logger.warning(
326
+ "Cannot interrupt turn for thread %s: missing turn id",
327
+ target_thread,
328
+ )
329
+
330
+ async def final_messages(self, session_id: str) -> list[str]:
331
+ return []
332
+
333
+ async def request_approval(
334
+ self, description: str, context: Optional[Dict[str, Any]] = None
335
+ ) -> bool:
336
+ raise NotImplementedError(
337
+ "Approvals are handled via approval_handler in CodexAppServerBackend"
338
+ )
339
+
340
+ async def close(self) -> None:
341
+ if self._client is None:
342
+ return
343
+ try:
344
+ await self._client.close()
345
+ finally:
346
+ self._client = None
347
+
348
+ async def _handle_approval_request(
349
+ self, request: Dict[str, Any]
350
+ ) -> ApprovalDecision:
351
+ method = request.get("method", "")
352
+ item_type = request.get("params", {}).get("type", "")
353
+
354
+ _logger.info("Received approval request: %s (type=%s)", method, item_type)
355
+ request_id = str(request.get("id") or "")
356
+ # Surface the approval request to consumers (e.g., Telegram) while defaulting to approve
357
+ await self._event_queue.put(
358
+ ApprovalRequested(
359
+ timestamp=now_iso(),
360
+ request_id=request_id,
361
+ description=method or "approval requested",
362
+ context=request.get("params", {}),
363
+ )
364
+ )
365
+
366
+ return {"approve": True}
367
+
368
+ async def _handle_notification(self, notification: Dict[str, Any]) -> None:
369
+ if self._notification_handler is not None:
370
+ try:
371
+ await self._notification_handler(notification)
372
+ except Exception as exc:
373
+ self._logger.debug("Notification handler failed: %s", exc)
374
+ method = notification.get("method", "")
375
+ params = notification.get("params", {}) or {}
376
+ thread_id = params.get("threadId") or params.get("thread_id")
377
+ if self._thread_id and thread_id and thread_id != self._thread_id:
378
+ return
379
+ _logger.debug("Received notification: %s", method)
380
+ run_event = self._map_to_run_event(notification)
381
+ if run_event:
382
+ await self._event_queue.put(run_event)
383
+
384
+ def _map_to_run_event(self, event_data: Dict[str, Any]) -> Optional[RunEvent]:
385
+ method = event_data.get("method", "")
386
+
387
+ if method == "turn/streamDelta":
388
+ content = event_data.get("params", {}).get("delta", "")
389
+ return OutputDelta(
390
+ timestamp=now_iso(), content=content, delta_type="assistant_stream"
391
+ )
392
+
393
+ if method == "item/toolCall/start":
394
+ params = event_data.get("params", {})
395
+ return ToolCall(
396
+ timestamp=now_iso(),
397
+ tool_name=params.get("name", ""),
398
+ tool_input=params.get("input", {}),
399
+ )
400
+
401
+ if method == "item/toolCall/end":
402
+ return None
403
+
404
+ if method == "turn/error":
405
+ params = event_data.get("params", {})
406
+ error_message = params.get("message", "Unknown error")
407
+ return Failed(timestamp=now_iso(), error_message=error_message)
408
+
409
+ return None
410
+
411
+ def _parse_raw_event(self, event_data: Dict[str, Any]) -> AgentEvent:
412
+ method = event_data.get("method", "")
413
+
414
+ if method == "turn/streamDelta":
415
+ content = event_data.get("params", {}).get("delta", "")
416
+ return AgentEvent.stream_delta(
417
+ content=content, delta_type="assistant_stream"
418
+ )
419
+
420
+ if method == "item/toolCall/start":
421
+ params = event_data.get("params", {})
422
+ return AgentEvent.tool_call(
423
+ tool_name=params.get("name", ""),
424
+ tool_input=params.get("input", {}),
425
+ )
426
+
427
+ if method == "item/toolCall/end":
428
+ params = event_data.get("params", {})
429
+ return AgentEvent.tool_result(
430
+ tool_name=params.get("name", ""),
431
+ result=params.get("result"),
432
+ error=params.get("error"),
433
+ )
434
+
435
+ if method == "turn/error":
436
+ params = event_data.get("params", {})
437
+ error_message = params.get("message", "Unknown error")
438
+ return AgentEvent.error(error_message=error_message)
439
+
440
+ return AgentEvent.stream_delta(content="", delta_type="unknown_event")
441
+
442
+ @property
443
+ def last_turn_id(self) -> Optional[str]:
444
+ return self._turn_id
445
+
446
+ @property
447
+ def last_thread_info(self) -> Optional[Dict[str, Any]]:
448
+ return self._thread_info
@@ -0,0 +1,108 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import MutableMapping, Optional
4
+
5
+ from ...agents.opencode.client import OpenCodeClient
6
+ from ...agents.opencode.supervisor import OpenCodeSupervisor
7
+
8
+ _logger = logging.getLogger(__name__)
9
+
10
+
11
+ class OpenCodeAdapterOrchestrator:
12
+ """
13
+ Orchestrates OpenCode backend sessions using OpenCodeSupervisor.
14
+
15
+ This adapter wraps the OpenCodeSupervisor to provide an AgentBackend-compatible
16
+ interface for use by the Engine.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ opencode_command: Optional[list[str]] = None,
23
+ opencode_binary: Optional[str] = None,
24
+ workspace_root: Optional[Path] = None,
25
+ logger: Optional[logging.Logger] = None,
26
+ request_timeout: Optional[float] = None,
27
+ max_handles: Optional[int] = None,
28
+ idle_ttl_seconds: Optional[float] = None,
29
+ session_stall_timeout_seconds: Optional[float] = None,
30
+ base_env: Optional[MutableMapping[str, str]] = None,
31
+ subagent_models: Optional[dict[str, str]] = None,
32
+ ):
33
+ self._opencode_command = opencode_command
34
+ self._opencode_binary = opencode_binary
35
+ self._workspace_root = workspace_root
36
+ self._logger = logger or _logger
37
+ self._request_timeout = request_timeout
38
+ self._max_handles = max_handles
39
+ self._idle_ttl_seconds = idle_ttl_seconds
40
+ self._session_stall_timeout_seconds = session_stall_timeout_seconds
41
+ self._base_env = base_env
42
+ self._subagent_models = subagent_models
43
+
44
+ self._supervisor: Optional[OpenCodeSupervisor] = None
45
+ self._client: Optional[OpenCodeClient] = None
46
+ self._session_id: Optional[str] = None
47
+
48
+ async def ensure_supervisor(self) -> Optional[OpenCodeSupervisor]:
49
+ """Ensure the OpenCode supervisor is initialized."""
50
+ if self._supervisor is None:
51
+ self._supervisor = self._build_supervisor()
52
+ return self._supervisor
53
+
54
+ async def get_client(self, workspace_root: Path) -> OpenCodeClient:
55
+ """Get or create an OpenCode client for the given workspace."""
56
+ supervisor = await self.ensure_supervisor()
57
+ if supervisor is None:
58
+ raise RuntimeError(
59
+ "OpenCode is not configured: neither opencode_command nor opencode_binary is set"
60
+ )
61
+ if self._client is None:
62
+ self._client = await supervisor.get_client(workspace_root)
63
+ return self._client
64
+
65
+ async def close_all(self) -> None:
66
+ """Close the supervisor and clean up resources."""
67
+ if self._supervisor is not None:
68
+ await self._supervisor.close_all()
69
+ self._supervisor = None
70
+ self._client = None
71
+ self._session_id = None
72
+
73
+ def _build_supervisor(self) -> Optional[OpenCodeSupervisor]:
74
+ """Build the OpenCodeSupervisor instance."""
75
+ command = list(self._opencode_command or [])
76
+ if not command and self._opencode_binary:
77
+ command = [
78
+ self._opencode_binary,
79
+ "serve",
80
+ "--hostname",
81
+ "127.0.0.1",
82
+ "--port",
83
+ "0",
84
+ ]
85
+
86
+ if not command:
87
+ return None
88
+
89
+ username = None
90
+ password = None
91
+ if self._base_env is not None:
92
+ username = self._base_env.get("OPENCODE_SERVER_USERNAME")
93
+ password = self._base_env.get("OPENCODE_SERVER_PASSWORD")
94
+ if password and not username:
95
+ username = "opencode"
96
+
97
+ return OpenCodeSupervisor(
98
+ command,
99
+ logger=self._logger,
100
+ request_timeout=self._request_timeout,
101
+ max_handles=self._max_handles,
102
+ idle_ttl_seconds=self._idle_ttl_seconds,
103
+ session_stall_timeout_seconds=self._session_stall_timeout_seconds,
104
+ username=username if password else None,
105
+ password=password if password else None,
106
+ base_env=self._base_env,
107
+ subagent_models=self._subagent_models,
108
+ )