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.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/__main__.py +4 -0
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +176 -47
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +155 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +22 -37
- codex_autorunner/cli.py +5 -1156
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +4 -0
- codex_autorunner/core/about_car.py +49 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +26 -28
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +12 -2
- codex_autorunner/core/config.py +587 -103
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +136 -0
- codex_autorunner/core/engine.py +1531 -866
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +202 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +88 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +131 -0
- codex_autorunner/core/flows/runtime.py +382 -0
- codex_autorunner/core/flows/store.py +568 -0
- codex_autorunner/core/flows/transition.py +138 -0
- codex_autorunner/core/flows/ux_helpers.py +257 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +136 -16
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/core/ports/agent_backend.py +150 -0
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/core/ports/run_event.py +91 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/state_roots.py +57 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +201 -0
- codex_autorunner/core/ticket_manager_cli.py +432 -0
- codex_autorunner/core/update.py +24 -16
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +120 -11
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +98 -0
- codex_autorunner/integrations/agents/__init__.py +17 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +448 -0
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +598 -0
- codex_autorunner/integrations/agents/runner.py +91 -0
- codex_autorunner/integrations/agents/wiring.py +271 -0
- codex_autorunner/integrations/app_server/client.py +583 -152
- 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/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +204 -165
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +221 -0
- codex_autorunner/integrations/telegram/constants.py +17 -2
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +111 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +221 -42
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
- codex_autorunner/integrations/telegram/transport.py +39 -4
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +37 -67
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +3 -0
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -624
- codex_autorunner/routes/file_chat.py +7 -0
- codex_autorunner/routes/flows.py +7 -0
- codex_autorunner/routes/messages.py +7 -0
- 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 -188
- codex_autorunner/routes/usage.py +3 -0
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +3 -0
- codex_autorunner/server.py +3 -2
- codex_autorunner/static/agentControls.js +41 -11
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +35 -24
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +36 -8
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +344 -325
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +126 -185
- codex_autorunner/static/index.html +839 -863
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +873 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +149 -217
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/styles.css +8850 -3876
- codex_autorunner/static/tabs.js +175 -11
- codex_autorunner/static/terminal.js +32 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +844 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1988 -0
- codex_autorunner/static/utils.js +43 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +765 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +1224 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2019 -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 +78 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +277 -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 +836 -0
- codex_autorunner/surfaces/web/routes/flows.py +1164 -0
- codex_autorunner/surfaces/web/routes/messages.py +459 -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 +280 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -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 +417 -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 +27 -0
- codex_autorunner/tickets/agent_pool.py +399 -0
- codex_autorunner/tickets/files.py +89 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +97 -0
- codex_autorunner/tickets/outbox.py +244 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +881 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1771
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -587
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -396
- codex_autorunner/web/static_assets.py +4 -484
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +335 -0
- codex_autorunner-1.1.0.dist-info/METADATA +154 -0
- codex_autorunner-1.1.0.dist-info/RECORD +308 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/METADATA +0 -249
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
)
|