codex-autorunner 1.0.0__py3-none-any.whl → 1.2.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 (227) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/agents/codex/harness.py +1 -1
  3. codex_autorunner/agents/opencode/client.py +113 -4
  4. codex_autorunner/agents/opencode/constants.py +3 -0
  5. codex_autorunner/agents/opencode/harness.py +6 -1
  6. codex_autorunner/agents/opencode/runtime.py +59 -18
  7. codex_autorunner/agents/opencode/supervisor.py +4 -0
  8. codex_autorunner/agents/registry.py +36 -7
  9. codex_autorunner/bootstrap.py +226 -4
  10. codex_autorunner/cli.py +5 -1174
  11. codex_autorunner/codex_cli.py +20 -84
  12. codex_autorunner/core/__init__.py +20 -0
  13. codex_autorunner/core/about_car.py +119 -1
  14. codex_autorunner/core/app_server_ids.py +59 -0
  15. codex_autorunner/core/app_server_threads.py +17 -2
  16. codex_autorunner/core/app_server_utils.py +165 -0
  17. codex_autorunner/core/archive.py +349 -0
  18. codex_autorunner/core/codex_runner.py +6 -2
  19. codex_autorunner/core/config.py +433 -4
  20. codex_autorunner/core/context_awareness.py +38 -0
  21. codex_autorunner/core/docs.py +0 -122
  22. codex_autorunner/core/drafts.py +58 -4
  23. codex_autorunner/core/exceptions.py +4 -0
  24. codex_autorunner/core/filebox.py +265 -0
  25. codex_autorunner/core/flows/controller.py +96 -2
  26. codex_autorunner/core/flows/models.py +13 -0
  27. codex_autorunner/core/flows/reasons.py +52 -0
  28. codex_autorunner/core/flows/reconciler.py +134 -0
  29. codex_autorunner/core/flows/runtime.py +57 -4
  30. codex_autorunner/core/flows/store.py +142 -7
  31. codex_autorunner/core/flows/transition.py +27 -15
  32. codex_autorunner/core/flows/ux_helpers.py +272 -0
  33. codex_autorunner/core/flows/worker_process.py +32 -6
  34. codex_autorunner/core/git_utils.py +62 -0
  35. codex_autorunner/core/hub.py +291 -20
  36. codex_autorunner/core/lifecycle_events.py +253 -0
  37. codex_autorunner/core/notifications.py +14 -2
  38. codex_autorunner/core/path_utils.py +2 -1
  39. codex_autorunner/core/pma_audit.py +224 -0
  40. codex_autorunner/core/pma_context.py +496 -0
  41. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  42. codex_autorunner/core/pma_lifecycle.py +527 -0
  43. codex_autorunner/core/pma_queue.py +367 -0
  44. codex_autorunner/core/pma_safety.py +221 -0
  45. codex_autorunner/core/pma_state.py +115 -0
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
  50. codex_autorunner/core/prompt.py +0 -80
  51. codex_autorunner/core/prompts.py +56 -172
  52. codex_autorunner/core/redaction.py +0 -4
  53. codex_autorunner/core/review_context.py +11 -9
  54. codex_autorunner/core/runner_controller.py +35 -33
  55. codex_autorunner/core/runner_state.py +147 -0
  56. codex_autorunner/core/runtime.py +829 -0
  57. codex_autorunner/core/sqlite_utils.py +13 -4
  58. codex_autorunner/core/state.py +7 -10
  59. codex_autorunner/core/state_roots.py +62 -0
  60. codex_autorunner/core/supervisor_protocol.py +15 -0
  61. codex_autorunner/core/templates/__init__.py +39 -0
  62. codex_autorunner/core/templates/git_mirror.py +234 -0
  63. codex_autorunner/core/templates/provenance.py +56 -0
  64. codex_autorunner/core/templates/scan_cache.py +120 -0
  65. codex_autorunner/core/text_delta_coalescer.py +54 -0
  66. codex_autorunner/core/ticket_linter_cli.py +218 -0
  67. codex_autorunner/core/ticket_manager_cli.py +494 -0
  68. codex_autorunner/core/time_utils.py +11 -0
  69. codex_autorunner/core/types.py +18 -0
  70. codex_autorunner/core/update.py +4 -5
  71. codex_autorunner/core/update_paths.py +28 -0
  72. codex_autorunner/core/usage.py +164 -12
  73. codex_autorunner/core/utils.py +125 -15
  74. codex_autorunner/flows/review/__init__.py +17 -0
  75. codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
  76. codex_autorunner/flows/ticket_flow/definition.py +52 -3
  77. codex_autorunner/integrations/agents/__init__.py +11 -19
  78. codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
  79. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  80. codex_autorunner/integrations/agents/codex_backend.py +177 -25
  81. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  82. codex_autorunner/integrations/agents/opencode_backend.py +305 -32
  83. codex_autorunner/integrations/agents/runner.py +86 -0
  84. codex_autorunner/integrations/agents/wiring.py +279 -0
  85. codex_autorunner/integrations/app_server/client.py +7 -60
  86. codex_autorunner/integrations/app_server/env.py +2 -107
  87. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  88. codex_autorunner/integrations/telegram/adapter.py +65 -0
  89. codex_autorunner/integrations/telegram/config.py +46 -0
  90. codex_autorunner/integrations/telegram/constants.py +1 -1
  91. codex_autorunner/integrations/telegram/doctor.py +228 -6
  92. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
  93. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  94. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  95. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
  96. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  97. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
  98. codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
  99. codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
  100. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  101. codex_autorunner/integrations/telegram/helpers.py +22 -1
  102. codex_autorunner/integrations/telegram/runtime.py +9 -4
  103. codex_autorunner/integrations/telegram/service.py +45 -10
  104. codex_autorunner/integrations/telegram/state.py +38 -0
  105. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
  106. codex_autorunner/integrations/telegram/transport.py +13 -4
  107. codex_autorunner/integrations/templates/__init__.py +27 -0
  108. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  109. codex_autorunner/routes/__init__.py +37 -76
  110. codex_autorunner/routes/agents.py +2 -137
  111. codex_autorunner/routes/analytics.py +2 -238
  112. codex_autorunner/routes/app_server.py +2 -131
  113. codex_autorunner/routes/base.py +2 -596
  114. codex_autorunner/routes/file_chat.py +4 -833
  115. codex_autorunner/routes/flows.py +4 -977
  116. codex_autorunner/routes/messages.py +4 -456
  117. codex_autorunner/routes/repos.py +2 -196
  118. codex_autorunner/routes/review.py +2 -147
  119. codex_autorunner/routes/sessions.py +2 -175
  120. codex_autorunner/routes/settings.py +2 -168
  121. codex_autorunner/routes/shared.py +2 -275
  122. codex_autorunner/routes/system.py +4 -193
  123. codex_autorunner/routes/usage.py +2 -86
  124. codex_autorunner/routes/voice.py +2 -119
  125. codex_autorunner/routes/workspace.py +2 -270
  126. codex_autorunner/server.py +4 -4
  127. codex_autorunner/static/agentControls.js +61 -16
  128. codex_autorunner/static/app.js +126 -14
  129. codex_autorunner/static/archive.js +826 -0
  130. codex_autorunner/static/archiveApi.js +37 -0
  131. codex_autorunner/static/autoRefresh.js +7 -7
  132. codex_autorunner/static/chatUploads.js +137 -0
  133. codex_autorunner/static/dashboard.js +224 -171
  134. codex_autorunner/static/docChatCore.js +185 -13
  135. codex_autorunner/static/fileChat.js +68 -40
  136. codex_autorunner/static/fileboxUi.js +159 -0
  137. codex_autorunner/static/hub.js +114 -131
  138. codex_autorunner/static/index.html +375 -49
  139. codex_autorunner/static/messages.js +568 -87
  140. codex_autorunner/static/notifications.js +255 -0
  141. codex_autorunner/static/pma.js +1167 -0
  142. codex_autorunner/static/preserve.js +17 -0
  143. codex_autorunner/static/settings.js +128 -6
  144. codex_autorunner/static/smartRefresh.js +52 -0
  145. codex_autorunner/static/streamUtils.js +57 -0
  146. codex_autorunner/static/styles.css +9798 -6143
  147. codex_autorunner/static/tabs.js +152 -11
  148. codex_autorunner/static/templateReposSettings.js +225 -0
  149. codex_autorunner/static/terminal.js +18 -0
  150. codex_autorunner/static/ticketChatActions.js +165 -3
  151. codex_autorunner/static/ticketChatStream.js +17 -119
  152. codex_autorunner/static/ticketEditor.js +137 -15
  153. codex_autorunner/static/ticketTemplates.js +798 -0
  154. codex_autorunner/static/tickets.js +821 -98
  155. codex_autorunner/static/turnEvents.js +27 -0
  156. codex_autorunner/static/turnResume.js +33 -0
  157. codex_autorunner/static/utils.js +39 -0
  158. codex_autorunner/static/workspace.js +389 -82
  159. codex_autorunner/static/workspaceFileBrowser.js +15 -13
  160. codex_autorunner/surfaces/__init__.py +5 -0
  161. codex_autorunner/surfaces/cli/__init__.py +6 -0
  162. codex_autorunner/surfaces/cli/cli.py +2534 -0
  163. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  164. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  165. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  166. codex_autorunner/surfaces/web/__init__.py +1 -0
  167. codex_autorunner/surfaces/web/app.py +2223 -0
  168. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  169. codex_autorunner/surfaces/web/middleware.py +587 -0
  170. codex_autorunner/surfaces/web/pty_session.py +370 -0
  171. codex_autorunner/surfaces/web/review.py +6 -0
  172. codex_autorunner/surfaces/web/routes/__init__.py +82 -0
  173. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  174. codex_autorunner/surfaces/web/routes/analytics.py +284 -0
  175. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  176. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  177. codex_autorunner/surfaces/web/routes/base.py +615 -0
  178. codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
  179. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  180. codex_autorunner/surfaces/web/routes/flows.py +1354 -0
  181. codex_autorunner/surfaces/web/routes/messages.py +490 -0
  182. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  183. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  184. codex_autorunner/surfaces/web/routes/review.py +148 -0
  185. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  186. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  187. codex_autorunner/surfaces/web/routes/shared.py +277 -0
  188. codex_autorunner/surfaces/web/routes/system.py +196 -0
  189. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  190. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  191. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  192. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  193. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  194. codex_autorunner/surfaces/web/schemas.py +469 -0
  195. codex_autorunner/surfaces/web/static_assets.py +490 -0
  196. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  197. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  198. codex_autorunner/tickets/__init__.py +8 -1
  199. codex_autorunner/tickets/agent_pool.py +53 -4
  200. codex_autorunner/tickets/files.py +37 -16
  201. codex_autorunner/tickets/lint.py +50 -0
  202. codex_autorunner/tickets/models.py +6 -1
  203. codex_autorunner/tickets/outbox.py +50 -2
  204. codex_autorunner/tickets/runner.py +396 -57
  205. codex_autorunner/web/__init__.py +5 -1
  206. codex_autorunner/web/app.py +2 -1949
  207. codex_autorunner/web/hub_jobs.py +2 -191
  208. codex_autorunner/web/middleware.py +2 -586
  209. codex_autorunner/web/pty_session.py +2 -369
  210. codex_autorunner/web/runner_manager.py +2 -24
  211. codex_autorunner/web/schemas.py +2 -376
  212. codex_autorunner/web/static_assets.py +4 -441
  213. codex_autorunner/web/static_refresh.py +2 -85
  214. codex_autorunner/web/terminal_sessions.py +2 -77
  215. codex_autorunner/workspace/paths.py +49 -33
  216. codex_autorunner-1.2.0.dist-info/METADATA +150 -0
  217. codex_autorunner-1.2.0.dist-info/RECORD +339 -0
  218. codex_autorunner/core/adapter_utils.py +0 -21
  219. codex_autorunner/core/engine.py +0 -2653
  220. codex_autorunner/core/static_assets.py +0 -55
  221. codex_autorunner-1.0.0.dist-info/METADATA +0 -246
  222. codex_autorunner-1.0.0.dist-info/RECORD +0 -251
  223. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  224. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  225. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  226. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  227. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,90 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Any, Callable, Dict, Optional, Sequence
4
+
5
+ from ...integrations.app_server.client import CodexAppServerClient
6
+ from ...integrations.app_server.supervisor import WorkspaceAppServerSupervisor
7
+
8
+ _logger = logging.getLogger(__name__)
9
+
10
+ EnvBuilder = Callable[[Path, str, Path], Dict[str, str]]
11
+
12
+
13
+ class CodexAdapterOrchestrator:
14
+ """
15
+ Orchestrates Codex app-server backend sessions using WorkspaceAppServerSupervisor.
16
+
17
+ This adapter wraps the WorkspaceAppServerSupervisor to provide an AgentBackend-compatible
18
+ interface for use by the Engine.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ command: Sequence[str],
24
+ *,
25
+ state_root: Path,
26
+ env_builder: EnvBuilder,
27
+ approval_handler: Optional[Any] = None,
28
+ notification_handler: Optional[Any] = None,
29
+ logger: Optional[logging.Logger] = None,
30
+ auto_restart: bool = True,
31
+ request_timeout: Optional[float] = None,
32
+ turn_stall_timeout_seconds: Optional[float] = None,
33
+ turn_stall_poll_interval_seconds: Optional[float] = None,
34
+ turn_stall_recovery_min_interval_seconds: Optional[float] = None,
35
+ default_approval_decision: str = "cancel",
36
+ max_handles: Optional[int] = None,
37
+ idle_ttl_seconds: Optional[float] = None,
38
+ ):
39
+ self._command = command
40
+ self._state_root = state_root
41
+ self._env_builder = env_builder
42
+ self._approval_handler = approval_handler
43
+ self._notification_handler = notification_handler
44
+ self._logger = logger or _logger
45
+ self._auto_restart = auto_restart
46
+ self._request_timeout = request_timeout
47
+ self._turn_stall_timeout_seconds = turn_stall_timeout_seconds
48
+ self._turn_stall_poll_interval_seconds = turn_stall_poll_interval_seconds
49
+ self._turn_stall_recovery_min_interval_seconds = (
50
+ turn_stall_recovery_min_interval_seconds
51
+ )
52
+ self._default_approval_decision = default_approval_decision
53
+ self._max_handles = max_handles
54
+ self._idle_ttl_seconds = idle_ttl_seconds
55
+
56
+ self._supervisor: Optional[WorkspaceAppServerSupervisor] = None
57
+ self._client: Optional[CodexAppServerClient] = None
58
+
59
+ async def ensure_supervisor(self) -> WorkspaceAppServerSupervisor:
60
+ """Ensure the Codex app-server supervisor is initialized."""
61
+ if self._supervisor is None:
62
+ self._supervisor = WorkspaceAppServerSupervisor(
63
+ self._command,
64
+ state_root=self._state_root,
65
+ env_builder=self._env_builder,
66
+ approval_handler=self._approval_handler,
67
+ notification_handler=self._notification_handler,
68
+ logger=self._logger,
69
+ auto_restart=self._auto_restart,
70
+ request_timeout=self._request_timeout,
71
+ turn_stall_timeout_seconds=self._turn_stall_timeout_seconds,
72
+ turn_stall_poll_interval_seconds=self._turn_stall_poll_interval_seconds,
73
+ turn_stall_recovery_min_interval_seconds=self._turn_stall_recovery_min_interval_seconds,
74
+ default_approval_decision=self._default_approval_decision,
75
+ max_handles=self._max_handles,
76
+ idle_ttl_seconds=self._idle_ttl_seconds,
77
+ )
78
+ return self._supervisor
79
+
80
+ async def get_client(self, workspace_root: Path) -> CodexAppServerClient:
81
+ """Get or create a Codex app-server client for the given workspace."""
82
+ supervisor = await self.ensure_supervisor()
83
+ return await supervisor.get_client(workspace_root)
84
+
85
+ async def close_all(self) -> None:
86
+ """Close the supervisor and clean up resources."""
87
+ if self._supervisor is not None:
88
+ await self._supervisor.close_all()
89
+ self._supervisor = None
90
+ self._client = None
@@ -1,12 +1,13 @@
1
1
  import asyncio
2
+ import hashlib
2
3
  import logging
3
4
  from pathlib import Path
4
- from typing import Any, AsyncGenerator, Dict, Optional, Union
5
+ from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, Optional, Union
5
6
 
6
7
  from ...core.circuit_breaker import CircuitBreaker
7
- from ...integrations.app_server.client import CodexAppServerClient
8
- from .agent_backend import AgentBackend, AgentEvent, now_iso
9
- from .run_event import (
8
+ from ...core.logging_utils import log_event
9
+ from ...core.ports.agent_backend import AgentBackend, AgentEvent, now_iso
10
+ from ...core.ports.run_event import (
10
11
  ApprovalRequested,
11
12
  Completed,
12
13
  Failed,
@@ -15,10 +16,12 @@ from .run_event import (
15
16
  Started,
16
17
  ToolCall,
17
18
  )
19
+ from ...integrations.app_server.client import CodexAppServerClient, CodexAppServerError
18
20
 
19
21
  _logger = logging.getLogger(__name__)
20
22
 
21
23
  ApprovalDecision = Union[str, Dict[str, Any]]
24
+ NotificationHandler = Callable[[Dict[str, Any]], Awaitable[None]]
22
25
 
23
26
 
24
27
  class CodexAppServerBackend(AgentBackend):
@@ -30,16 +33,52 @@ class CodexAppServerBackend(AgentBackend):
30
33
  env: Optional[Dict[str, str]] = None,
31
34
  approval_policy: Optional[str] = None,
32
35
  sandbox_policy: Optional[str] = None,
36
+ model: Optional[str] = None,
37
+ reasoning_effort: Optional[str] = None,
38
+ turn_timeout_seconds: Optional[float] = None,
39
+ auto_restart: Optional[bool] = None,
40
+ request_timeout: Optional[float] = None,
41
+ turn_stall_timeout_seconds: Optional[float] = None,
42
+ turn_stall_poll_interval_seconds: Optional[float] = None,
43
+ turn_stall_recovery_min_interval_seconds: Optional[float] = None,
44
+ max_message_bytes: Optional[int] = None,
45
+ oversize_preview_bytes: Optional[int] = None,
46
+ max_oversize_drain_bytes: Optional[int] = None,
47
+ restart_backoff_initial_seconds: Optional[float] = None,
48
+ restart_backoff_max_seconds: Optional[float] = None,
49
+ restart_backoff_jitter_ratio: Optional[float] = None,
50
+ notification_handler: Optional[NotificationHandler] = None,
51
+ logger: Optional[logging.Logger] = None,
33
52
  ):
34
53
  self._command = command
35
54
  self._cwd = cwd
36
55
  self._env = env
37
56
  self._approval_policy = approval_policy
38
57
  self._sandbox_policy = sandbox_policy
58
+ self._model = model
59
+ self._reasoning_effort = reasoning_effort
60
+ self._turn_timeout_seconds = turn_timeout_seconds
61
+ self._auto_restart = auto_restart
62
+ self._request_timeout = request_timeout
63
+ self._turn_stall_timeout_seconds = turn_stall_timeout_seconds
64
+ self._turn_stall_poll_interval_seconds = turn_stall_poll_interval_seconds
65
+ self._turn_stall_recovery_min_interval_seconds = (
66
+ turn_stall_recovery_min_interval_seconds
67
+ )
68
+ self._max_message_bytes = max_message_bytes
69
+ self._oversize_preview_bytes = oversize_preview_bytes
70
+ self._max_oversize_drain_bytes = max_oversize_drain_bytes
71
+ self._restart_backoff_initial_seconds = restart_backoff_initial_seconds
72
+ self._restart_backoff_max_seconds = restart_backoff_max_seconds
73
+ self._restart_backoff_jitter_ratio = restart_backoff_jitter_ratio
74
+ self._notification_handler = notification_handler
75
+ self._logger = logger or _logger
39
76
 
40
77
  self._client: Optional[CodexAppServerClient] = None
41
78
  self._session_id: Optional[str] = None
42
79
  self._thread_id: Optional[str] = None
80
+ self._turn_id: Optional[str] = None
81
+ self._thread_info: Optional[Dict[str, Any]] = None
43
82
 
44
83
  self._circuit_breaker = CircuitBreaker("CodexAppServer", logger=_logger)
45
84
  self._event_queue: asyncio.Queue[RunEvent] = asyncio.Queue()
@@ -52,17 +91,67 @@ class CodexAppServerBackend(AgentBackend):
52
91
  env=self._env,
53
92
  approval_handler=self._handle_approval_request,
54
93
  notification_handler=self._handle_notification,
94
+ auto_restart=self._auto_restart,
95
+ request_timeout=self._request_timeout,
96
+ turn_stall_timeout_seconds=self._turn_stall_timeout_seconds,
97
+ turn_stall_poll_interval_seconds=self._turn_stall_poll_interval_seconds,
98
+ turn_stall_recovery_min_interval_seconds=self._turn_stall_recovery_min_interval_seconds,
99
+ max_message_bytes=self._max_message_bytes,
100
+ oversize_preview_bytes=self._oversize_preview_bytes,
101
+ max_oversize_drain_bytes=self._max_oversize_drain_bytes,
102
+ restart_backoff_initial_seconds=self._restart_backoff_initial_seconds,
103
+ restart_backoff_max_seconds=self._restart_backoff_max_seconds,
104
+ restart_backoff_jitter_ratio=self._restart_backoff_jitter_ratio,
105
+ logger=self._logger,
55
106
  )
56
107
  await self._client.start()
57
108
  return self._client
58
109
 
110
+ def configure(
111
+ self,
112
+ *,
113
+ approval_policy: Optional[str],
114
+ sandbox_policy: Optional[str],
115
+ model: Optional[str],
116
+ reasoning_effort: Optional[str],
117
+ turn_timeout_seconds: Optional[float],
118
+ notification_handler: Optional[NotificationHandler],
119
+ ) -> None:
120
+ self._approval_policy = approval_policy
121
+ self._sandbox_policy = sandbox_policy
122
+ self._model = model
123
+ self._reasoning_effort = reasoning_effort
124
+ self._turn_timeout_seconds = turn_timeout_seconds
125
+ self._notification_handler = notification_handler
126
+
59
127
  async def start_session(self, target: dict, context: dict) -> str:
60
128
  client = await self._ensure_client()
61
129
 
62
130
  repo_root = Path(context.get("workspace") or self._cwd or Path.cwd())
131
+ resume_session = context.get("session_id") or context.get("thread_id")
132
+ # Ensure we don't reuse a stale turn id when a new session begins.
133
+ self._turn_id = None
134
+ if isinstance(resume_session, str) and resume_session:
135
+ try:
136
+ resume_result = await client.thread_resume(resume_session)
137
+ if isinstance(resume_result, dict):
138
+ self._thread_info = resume_result
139
+ resumed_id = (
140
+ resume_result.get("id")
141
+ if isinstance(resume_result, dict)
142
+ else resume_session
143
+ )
144
+ self._thread_id = (
145
+ resumed_id if isinstance(resumed_id, str) else resume_session
146
+ )
147
+ except CodexAppServerError:
148
+ self._thread_id = None
149
+ self._thread_info = None
63
150
 
64
- result = await client.thread_start(str(repo_root))
65
- self._thread_id = result.get("id")
151
+ if not self._thread_id:
152
+ result = await client.thread_start(str(repo_root))
153
+ self._thread_info = result if isinstance(result, dict) else None
154
+ self._thread_id = result.get("id") if isinstance(result, dict) else None
66
155
 
67
156
  if not self._thread_id:
68
157
  raise RuntimeError("Failed to start thread: missing thread ID")
@@ -79,26 +168,39 @@ class CodexAppServerBackend(AgentBackend):
79
168
 
80
169
  if session_id:
81
170
  self._thread_id = session_id
171
+ # Reset last turn to avoid interrupting the wrong turn when reusing backends.
172
+ self._turn_id = None
82
173
 
83
174
  if not self._thread_id:
84
175
  await self.start_session(target={}, context={})
85
176
 
86
- _logger.info(
87
- "Running turn on thread %s with message: %s",
88
- self._thread_id or "unknown",
89
- message[:100],
177
+ message_hash = hashlib.sha256(message.encode()).hexdigest()[:16]
178
+ log_event(
179
+ self._logger,
180
+ logging.INFO,
181
+ "agent.turn_started",
182
+ thread_id=self._thread_id,
183
+ message_length=len(message),
184
+ message_hash=message_hash,
90
185
  )
91
186
 
187
+ turn_kwargs: Dict[str, Any] = {}
188
+ if self._model:
189
+ turn_kwargs["model"] = self._model
190
+ if self._reasoning_effort:
191
+ turn_kwargs["effort"] = self._reasoning_effort
92
192
  handle = await client.turn_start(
93
193
  self._thread_id if self._thread_id else "default",
94
194
  text=message,
95
195
  approval_policy=self._approval_policy,
96
196
  sandbox_policy=self._sandbox_policy,
197
+ **turn_kwargs,
97
198
  )
199
+ self._turn_id = handle.turn_id
98
200
 
99
201
  yield AgentEvent.stream_delta(content=message, delta_type="user_message")
100
202
 
101
- result = await handle.wait(timeout=600.0)
203
+ result = await handle.wait(timeout=self._turn_timeout_seconds)
102
204
 
103
205
  for msg in result.agent_messages:
104
206
  yield AgentEvent.stream_delta(content=msg, delta_type="assistant_message")
@@ -117,19 +219,30 @@ class CodexAppServerBackend(AgentBackend):
117
219
 
118
220
  if session_id:
119
221
  self._thread_id = session_id
222
+ self._turn_id = None
120
223
 
121
224
  if not self._thread_id:
122
225
  actual_session_id = await self.start_session(target={}, context={})
123
226
  else:
124
227
  actual_session_id = self._thread_id
125
228
 
126
- _logger.info(
127
- "Running turn events on thread %s with message: %s",
128
- actual_session_id or "unknown",
129
- message[:100],
229
+ message_hash = hashlib.sha256(message.encode()).hexdigest()[:16]
230
+ log_event(
231
+ self._logger,
232
+ logging.INFO,
233
+ "agent.turn_events_started",
234
+ thread_id=actual_session_id,
235
+ turn_id=self._turn_id,
236
+ message_length=len(message),
237
+ message_hash=message_hash,
130
238
  )
131
239
 
132
- yield Started(timestamp=now_iso(), session_id=actual_session_id)
240
+ yield Started(
241
+ timestamp=now_iso(),
242
+ session_id=actual_session_id,
243
+ thread_id=self._thread_id,
244
+ turn_id=self._turn_id,
245
+ )
133
246
 
134
247
  yield OutputDelta(
135
248
  timestamp=now_iso(), content=message, delta_type="user_message"
@@ -137,14 +250,21 @@ class CodexAppServerBackend(AgentBackend):
137
250
 
138
251
  self._event_queue = asyncio.Queue()
139
252
 
253
+ turn_kwargs: dict[str, Any] = {}
254
+ if self._model:
255
+ turn_kwargs["model"] = self._model
256
+ if self._reasoning_effort:
257
+ turn_kwargs["effort"] = self._reasoning_effort
140
258
  handle = await client.turn_start(
141
259
  actual_session_id if actual_session_id else "default",
142
260
  text=message,
143
261
  approval_policy=self._approval_policy,
144
262
  sandbox_policy=self._sandbox_policy,
263
+ **turn_kwargs,
145
264
  )
265
+ self._turn_id = handle.turn_id
146
266
 
147
- wait_task = asyncio.create_task(handle.wait(timeout=600.0))
267
+ wait_task = asyncio.create_task(handle.wait(timeout=self._turn_timeout_seconds))
148
268
 
149
269
  try:
150
270
  while True:
@@ -181,11 +301,10 @@ class CodexAppServerBackend(AgentBackend):
181
301
  )
182
302
  break
183
303
 
184
- for task in done_set:
185
- if task is not wait_task:
186
- run_event = task.result()
187
- if run_event:
188
- yield run_event
304
+ if get_task in done_set:
305
+ run_event = get_task.result()
306
+ if run_event:
307
+ yield run_event
189
308
  for task in pending_set:
190
309
  task.cancel()
191
310
  except Exception as e:
@@ -200,12 +319,24 @@ class CodexAppServerBackend(AgentBackend):
200
319
 
201
320
  async def interrupt(self, session_id: str) -> None:
202
321
  target_thread = session_id or self._thread_id
203
- if self._client and target_thread:
322
+ target_turn = self._turn_id
323
+ if self._client and target_turn:
204
324
  try:
205
- await self._client.turn_interrupt(target_thread)
206
- _logger.info("Interrupted turn on thread %s", target_thread)
325
+ await self._client.turn_interrupt(target_turn, thread_id=target_thread)
326
+ _logger.info(
327
+ "Interrupted turn %s on thread %s",
328
+ target_turn,
329
+ target_thread or "unknown",
330
+ )
331
+ return
207
332
  except Exception as e:
208
333
  _logger.warning("Failed to interrupt turn: %s", e)
334
+ return
335
+ if self._client and target_thread:
336
+ _logger.warning(
337
+ "Cannot interrupt turn for thread %s: missing turn id",
338
+ target_thread,
339
+ )
209
340
 
210
341
  async def final_messages(self, session_id: str) -> list[str]:
211
342
  return []
@@ -217,6 +348,14 @@ class CodexAppServerBackend(AgentBackend):
217
348
  "Approvals are handled via approval_handler in CodexAppServerBackend"
218
349
  )
219
350
 
351
+ async def close(self) -> None:
352
+ if self._client is None:
353
+ return
354
+ try:
355
+ await self._client.close()
356
+ finally:
357
+ self._client = None
358
+
220
359
  async def _handle_approval_request(
221
360
  self, request: Dict[str, Any]
222
361
  ) -> ApprovalDecision:
@@ -238,6 +377,11 @@ class CodexAppServerBackend(AgentBackend):
238
377
  return {"approve": True}
239
378
 
240
379
  async def _handle_notification(self, notification: Dict[str, Any]) -> None:
380
+ if self._notification_handler is not None:
381
+ try:
382
+ await self._notification_handler(notification)
383
+ except Exception as exc:
384
+ self._logger.debug("Notification handler failed: %s", exc)
241
385
  method = notification.get("method", "")
242
386
  params = notification.get("params", {}) or {}
243
387
  thread_id = params.get("threadId") or params.get("thread_id")
@@ -305,3 +449,11 @@ class CodexAppServerBackend(AgentBackend):
305
449
  return AgentEvent.error(error_message=error_message)
306
450
 
307
451
  return AgentEvent.stream_delta(content="", delta_type="unknown_event")
452
+
453
+ @property
454
+ def last_turn_id(self) -> Optional[str]:
455
+ return self._turn_id
456
+
457
+ @property
458
+ def last_thread_info(self) -> Optional[Dict[str, Any]]:
459
+ 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
+ )