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.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +469 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {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 ...
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
self.
|
|
89
|
-
|
|
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=
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
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=
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
)
|