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,279 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Awaitable, Callable, Optional
|
|
8
|
+
|
|
9
|
+
from ...core.config import RepoConfig
|
|
10
|
+
from ...core.ports.agent_backend import AgentBackend
|
|
11
|
+
from ...core.state import RunnerState
|
|
12
|
+
from ...core.utils import build_opencode_supervisor
|
|
13
|
+
from ...workspace import canonical_workspace_root, workspace_id_for_path
|
|
14
|
+
from ..app_server.env import build_app_server_env
|
|
15
|
+
from ..app_server.supervisor import WorkspaceAppServerSupervisor
|
|
16
|
+
from .codex_backend import CodexAppServerBackend
|
|
17
|
+
from .opencode_backend import OpenCodeBackend
|
|
18
|
+
|
|
19
|
+
NotificationHandler = Callable[[dict[str, Any]], Awaitable[None]]
|
|
20
|
+
BackendFactory = Callable[
|
|
21
|
+
[str, RunnerState, Optional[NotificationHandler]], AgentBackend
|
|
22
|
+
]
|
|
23
|
+
SupervisorFactory = Callable[[str, Optional[NotificationHandler]], Any]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_workspace_env(
|
|
27
|
+
repo_root: Path,
|
|
28
|
+
config: RepoConfig,
|
|
29
|
+
*,
|
|
30
|
+
event_prefix: str,
|
|
31
|
+
logger: logging.Logger,
|
|
32
|
+
) -> dict[str, str]:
|
|
33
|
+
workspace_root = canonical_workspace_root(repo_root)
|
|
34
|
+
workspace_id = workspace_id_for_path(workspace_root)
|
|
35
|
+
state_dir = config.app_server.state_root / workspace_id
|
|
36
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
return build_app_server_env(
|
|
38
|
+
config.app_server.command,
|
|
39
|
+
workspace_root,
|
|
40
|
+
state_dir,
|
|
41
|
+
logger=logger,
|
|
42
|
+
event_prefix=event_prefix,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AgentBackendFactory:
|
|
47
|
+
def __init__(self, repo_root: Path, config: RepoConfig) -> None:
|
|
48
|
+
self._repo_root = repo_root
|
|
49
|
+
self._config = config
|
|
50
|
+
self._logger = logging.getLogger("codex_autorunner.app_server")
|
|
51
|
+
self._backend_cache: dict[str, AgentBackend] = {}
|
|
52
|
+
self._opencode_supervisor: Optional[Any] = None
|
|
53
|
+
|
|
54
|
+
def __call__(
|
|
55
|
+
self,
|
|
56
|
+
agent_id: str,
|
|
57
|
+
state: RunnerState,
|
|
58
|
+
notification_handler: Optional[NotificationHandler],
|
|
59
|
+
) -> AgentBackend:
|
|
60
|
+
if agent_id == "codex":
|
|
61
|
+
if not self._config.app_server.command:
|
|
62
|
+
raise ValueError("app_server.command is required for codex backend")
|
|
63
|
+
|
|
64
|
+
approval_policy = state.autorunner_approval_policy or "never"
|
|
65
|
+
sandbox_mode = state.autorunner_sandbox_mode or "dangerFullAccess"
|
|
66
|
+
if sandbox_mode == "workspaceWrite":
|
|
67
|
+
sandbox_policy: Any = {
|
|
68
|
+
"type": "workspaceWrite",
|
|
69
|
+
"writableRoots": [str(self._repo_root)],
|
|
70
|
+
"networkAccess": bool(state.autorunner_workspace_write_network),
|
|
71
|
+
}
|
|
72
|
+
else:
|
|
73
|
+
sandbox_policy = sandbox_mode
|
|
74
|
+
|
|
75
|
+
model = state.autorunner_model_override or self._config.codex_model
|
|
76
|
+
reasoning_effort = (
|
|
77
|
+
state.autorunner_effort_override or self._config.codex_reasoning
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
env = _build_workspace_env(
|
|
81
|
+
self._repo_root,
|
|
82
|
+
self._config,
|
|
83
|
+
event_prefix="autorunner",
|
|
84
|
+
logger=self._logger,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
cached = self._backend_cache.get(agent_id)
|
|
88
|
+
if cached is None:
|
|
89
|
+
cached = CodexAppServerBackend(
|
|
90
|
+
command=self._config.app_server.command,
|
|
91
|
+
cwd=self._repo_root,
|
|
92
|
+
env=env,
|
|
93
|
+
approval_policy=approval_policy,
|
|
94
|
+
sandbox_policy=sandbox_policy,
|
|
95
|
+
model=model,
|
|
96
|
+
reasoning_effort=reasoning_effort,
|
|
97
|
+
turn_timeout_seconds=None,
|
|
98
|
+
auto_restart=self._config.app_server.auto_restart,
|
|
99
|
+
request_timeout=self._config.app_server.request_timeout,
|
|
100
|
+
turn_stall_timeout_seconds=self._config.app_server.turn_stall_timeout_seconds,
|
|
101
|
+
turn_stall_poll_interval_seconds=self._config.app_server.turn_stall_poll_interval_seconds,
|
|
102
|
+
turn_stall_recovery_min_interval_seconds=self._config.app_server.turn_stall_recovery_min_interval_seconds,
|
|
103
|
+
max_message_bytes=self._config.app_server.client.max_message_bytes,
|
|
104
|
+
oversize_preview_bytes=self._config.app_server.client.oversize_preview_bytes,
|
|
105
|
+
max_oversize_drain_bytes=self._config.app_server.client.max_oversize_drain_bytes,
|
|
106
|
+
restart_backoff_initial_seconds=self._config.app_server.client.restart_backoff_initial_seconds,
|
|
107
|
+
restart_backoff_max_seconds=self._config.app_server.client.restart_backoff_max_seconds,
|
|
108
|
+
restart_backoff_jitter_ratio=self._config.app_server.client.restart_backoff_jitter_ratio,
|
|
109
|
+
notification_handler=notification_handler,
|
|
110
|
+
logger=self._logger,
|
|
111
|
+
)
|
|
112
|
+
self._backend_cache[agent_id] = cached
|
|
113
|
+
else:
|
|
114
|
+
if isinstance(cached, CodexAppServerBackend):
|
|
115
|
+
cached.configure(
|
|
116
|
+
approval_policy=approval_policy,
|
|
117
|
+
sandbox_policy=sandbox_policy,
|
|
118
|
+
model=model,
|
|
119
|
+
reasoning_effort=reasoning_effort,
|
|
120
|
+
turn_timeout_seconds=None,
|
|
121
|
+
notification_handler=notification_handler,
|
|
122
|
+
)
|
|
123
|
+
return cached
|
|
124
|
+
|
|
125
|
+
if agent_id == "opencode":
|
|
126
|
+
agent_cfg = self._config.agents.get("opencode")
|
|
127
|
+
base_url = agent_cfg.base_url if agent_cfg else None
|
|
128
|
+
username = os.environ.get("OPENCODE_SERVER_USERNAME")
|
|
129
|
+
password = os.environ.get("OPENCODE_SERVER_PASSWORD")
|
|
130
|
+
if password and not username:
|
|
131
|
+
username = "opencode"
|
|
132
|
+
auth = (username, password) if username and password else None
|
|
133
|
+
|
|
134
|
+
cached = self._backend_cache.get(agent_id)
|
|
135
|
+
if cached is None:
|
|
136
|
+
if not base_url:
|
|
137
|
+
supervisor = self._ensure_opencode_supervisor()
|
|
138
|
+
if supervisor is None:
|
|
139
|
+
raise ValueError("opencode backend is not configured")
|
|
140
|
+
cached = OpenCodeBackend(
|
|
141
|
+
supervisor=supervisor,
|
|
142
|
+
workspace_root=self._repo_root,
|
|
143
|
+
auth=auth,
|
|
144
|
+
timeout=self._config.app_server.request_timeout,
|
|
145
|
+
model=state.autorunner_model_override,
|
|
146
|
+
reasoning=state.autorunner_effort_override,
|
|
147
|
+
approval_policy=state.autorunner_approval_policy,
|
|
148
|
+
session_stall_timeout_seconds=self._config.opencode.session_stall_timeout_seconds,
|
|
149
|
+
logger=self._logger,
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
cached = OpenCodeBackend(
|
|
153
|
+
base_url=base_url,
|
|
154
|
+
workspace_root=self._repo_root,
|
|
155
|
+
auth=auth,
|
|
156
|
+
timeout=self._config.app_server.request_timeout,
|
|
157
|
+
model=state.autorunner_model_override,
|
|
158
|
+
reasoning=state.autorunner_effort_override,
|
|
159
|
+
approval_policy=state.autorunner_approval_policy,
|
|
160
|
+
session_stall_timeout_seconds=self._config.opencode.session_stall_timeout_seconds,
|
|
161
|
+
logger=self._logger,
|
|
162
|
+
)
|
|
163
|
+
self._backend_cache[agent_id] = cached
|
|
164
|
+
else:
|
|
165
|
+
if isinstance(cached, OpenCodeBackend):
|
|
166
|
+
cached.configure(
|
|
167
|
+
model=state.autorunner_model_override,
|
|
168
|
+
reasoning=state.autorunner_effort_override,
|
|
169
|
+
approval_policy=state.autorunner_approval_policy,
|
|
170
|
+
)
|
|
171
|
+
return cached
|
|
172
|
+
|
|
173
|
+
raise ValueError(f"Unsupported agent backend: {agent_id}")
|
|
174
|
+
|
|
175
|
+
def _ensure_opencode_supervisor(self) -> Optional[Any]:
|
|
176
|
+
if self._opencode_supervisor is not None:
|
|
177
|
+
return self._opencode_supervisor
|
|
178
|
+
opencode_command = self._config.agent_serve_command("opencode")
|
|
179
|
+
opencode_binary = None
|
|
180
|
+
try:
|
|
181
|
+
opencode_binary = self._config.agent_binary("opencode")
|
|
182
|
+
except Exception:
|
|
183
|
+
opencode_binary = None
|
|
184
|
+
agent_config = self._config.agents.get("opencode")
|
|
185
|
+
subagent_models = agent_config.subagent_models if agent_config else None
|
|
186
|
+
supervisor = build_opencode_supervisor(
|
|
187
|
+
opencode_command=opencode_command,
|
|
188
|
+
opencode_binary=opencode_binary,
|
|
189
|
+
workspace_root=self._repo_root,
|
|
190
|
+
logger=self._logger,
|
|
191
|
+
request_timeout=self._config.app_server.request_timeout,
|
|
192
|
+
max_handles=self._config.app_server.max_handles,
|
|
193
|
+
idle_ttl_seconds=self._config.app_server.idle_ttl_seconds,
|
|
194
|
+
session_stall_timeout_seconds=self._config.opencode.session_stall_timeout_seconds,
|
|
195
|
+
max_text_chars=self._config.opencode.max_text_chars,
|
|
196
|
+
base_env=None,
|
|
197
|
+
subagent_models=subagent_models,
|
|
198
|
+
)
|
|
199
|
+
self._opencode_supervisor = supervisor
|
|
200
|
+
return supervisor
|
|
201
|
+
|
|
202
|
+
async def close_all(self) -> None:
|
|
203
|
+
backends = list(self._backend_cache.values())
|
|
204
|
+
self._backend_cache = {}
|
|
205
|
+
for backend in backends:
|
|
206
|
+
close = getattr(backend, "close", None)
|
|
207
|
+
if close is None:
|
|
208
|
+
continue
|
|
209
|
+
result = close()
|
|
210
|
+
if inspect.isawaitable(result):
|
|
211
|
+
await result
|
|
212
|
+
if self._opencode_supervisor is not None:
|
|
213
|
+
try:
|
|
214
|
+
await self._opencode_supervisor.close_all()
|
|
215
|
+
except Exception:
|
|
216
|
+
self._logger.warning(
|
|
217
|
+
"Failed closing opencode supervisor", exc_info=True
|
|
218
|
+
)
|
|
219
|
+
self._opencode_supervisor = None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def build_agent_backend_factory(repo_root: Path, config: RepoConfig) -> BackendFactory:
|
|
223
|
+
return AgentBackendFactory(repo_root, config)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def build_app_server_supervisor_factory(
|
|
227
|
+
config: RepoConfig,
|
|
228
|
+
*,
|
|
229
|
+
logger: Optional[logging.Logger] = None,
|
|
230
|
+
) -> SupervisorFactory:
|
|
231
|
+
app_logger = logger or logging.getLogger("codex_autorunner.app_server")
|
|
232
|
+
|
|
233
|
+
def factory(
|
|
234
|
+
event_prefix: str, notification_handler: Optional[NotificationHandler]
|
|
235
|
+
) -> WorkspaceAppServerSupervisor:
|
|
236
|
+
if not config.app_server.command:
|
|
237
|
+
raise ValueError("app_server.command is required for supervisor")
|
|
238
|
+
|
|
239
|
+
def _env_builder(
|
|
240
|
+
workspace_root: Path, _workspace_id: str, state_dir: Path
|
|
241
|
+
) -> dict[str, str]:
|
|
242
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
return build_app_server_env(
|
|
244
|
+
config.app_server.command,
|
|
245
|
+
workspace_root,
|
|
246
|
+
state_dir,
|
|
247
|
+
logger=app_logger,
|
|
248
|
+
event_prefix=event_prefix,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return WorkspaceAppServerSupervisor(
|
|
252
|
+
config.app_server.command,
|
|
253
|
+
state_root=config.app_server.state_root,
|
|
254
|
+
env_builder=_env_builder,
|
|
255
|
+
logger=app_logger,
|
|
256
|
+
notification_handler=notification_handler,
|
|
257
|
+
auto_restart=config.app_server.auto_restart,
|
|
258
|
+
max_handles=config.app_server.max_handles,
|
|
259
|
+
idle_ttl_seconds=config.app_server.idle_ttl_seconds,
|
|
260
|
+
request_timeout=config.app_server.request_timeout,
|
|
261
|
+
turn_stall_timeout_seconds=config.app_server.turn_stall_timeout_seconds,
|
|
262
|
+
turn_stall_poll_interval_seconds=config.app_server.turn_stall_poll_interval_seconds,
|
|
263
|
+
turn_stall_recovery_min_interval_seconds=config.app_server.turn_stall_recovery_min_interval_seconds,
|
|
264
|
+
max_message_bytes=config.app_server.client.max_message_bytes,
|
|
265
|
+
oversize_preview_bytes=config.app_server.client.oversize_preview_bytes,
|
|
266
|
+
max_oversize_drain_bytes=config.app_server.client.max_oversize_drain_bytes,
|
|
267
|
+
restart_backoff_initial_seconds=config.app_server.client.restart_backoff_initial_seconds,
|
|
268
|
+
restart_backoff_max_seconds=config.app_server.client.restart_backoff_max_seconds,
|
|
269
|
+
restart_backoff_jitter_ratio=config.app_server.client.restart_backoff_jitter_ratio,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return factory
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
__all__ = [
|
|
276
|
+
"AgentBackendFactory",
|
|
277
|
+
"build_agent_backend_factory",
|
|
278
|
+
"build_app_server_supervisor_factory",
|
|
279
|
+
]
|
|
@@ -23,10 +23,15 @@ from typing import (
|
|
|
23
23
|
no_type_check,
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
+
from ...core.app_server_utils import (
|
|
27
|
+
_extract_thread_id,
|
|
28
|
+
_extract_thread_id_for_turn,
|
|
29
|
+
_extract_turn_id,
|
|
30
|
+
)
|
|
26
31
|
from ...core.circuit_breaker import CircuitBreaker
|
|
27
32
|
from ...core.exceptions import (
|
|
33
|
+
AppServerError,
|
|
28
34
|
CircuitOpenError,
|
|
29
|
-
CodexError,
|
|
30
35
|
PermanentError,
|
|
31
36
|
TransientError,
|
|
32
37
|
)
|
|
@@ -62,7 +67,7 @@ _INVALID_JSON_PREVIEW_BYTES = 200
|
|
|
62
67
|
_CLIENT_INSTANCES: weakref.WeakSet = weakref.WeakSet()
|
|
63
68
|
|
|
64
69
|
|
|
65
|
-
class CodexAppServerError(
|
|
70
|
+
class CodexAppServerError(AppServerError):
|
|
66
71
|
"""Base error for app-server client failures."""
|
|
67
72
|
|
|
68
73
|
|
|
@@ -1579,54 +1584,12 @@ def _preview_excerpt(text: str, limit: int = 256) -> str:
|
|
|
1579
1584
|
return f"{normalized[:limit].rstrip()}..."
|
|
1580
1585
|
|
|
1581
1586
|
|
|
1582
|
-
def _extract_turn_id(payload: Any) -> Optional[str]:
|
|
1583
|
-
if not isinstance(payload, dict):
|
|
1584
|
-
return None
|
|
1585
|
-
for key in ("turnId", "turn_id", "id"):
|
|
1586
|
-
value = payload.get(key)
|
|
1587
|
-
if isinstance(value, str):
|
|
1588
|
-
return value
|
|
1589
|
-
turn = payload.get("turn")
|
|
1590
|
-
if isinstance(turn, dict):
|
|
1591
|
-
for key in ("id", "turnId", "turn_id"):
|
|
1592
|
-
value = turn.get(key)
|
|
1593
|
-
if isinstance(value, str):
|
|
1594
|
-
return value
|
|
1595
|
-
return None
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
1587
|
def _turn_key(thread_id: Optional[str], turn_id: Optional[str]) -> Optional[TurnKey]:
|
|
1599
1588
|
if not thread_id or not turn_id:
|
|
1600
1589
|
return None
|
|
1601
1590
|
return (thread_id, turn_id)
|
|
1602
1591
|
|
|
1603
1592
|
|
|
1604
|
-
def _extract_thread_id_for_turn(payload: Any) -> Optional[str]:
|
|
1605
|
-
if not isinstance(payload, dict):
|
|
1606
|
-
return None
|
|
1607
|
-
for candidate in (payload, payload.get("turn"), payload.get("item")):
|
|
1608
|
-
thread_id = _extract_thread_id_from_container(candidate)
|
|
1609
|
-
if thread_id:
|
|
1610
|
-
return thread_id
|
|
1611
|
-
return None
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
def _extract_thread_id_from_container(payload: Any) -> Optional[str]:
|
|
1615
|
-
if not isinstance(payload, dict):
|
|
1616
|
-
return None
|
|
1617
|
-
for key in ("threadId", "thread_id"):
|
|
1618
|
-
value = payload.get(key)
|
|
1619
|
-
if isinstance(value, str):
|
|
1620
|
-
return value
|
|
1621
|
-
thread = payload.get("thread")
|
|
1622
|
-
if isinstance(thread, dict):
|
|
1623
|
-
for key in ("id", "threadId", "thread_id"):
|
|
1624
|
-
value = thread.get(key)
|
|
1625
|
-
if isinstance(value, str):
|
|
1626
|
-
return value
|
|
1627
|
-
return None
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
1593
|
def _extract_review_text(item: Any) -> Optional[str]:
|
|
1631
1594
|
if not isinstance(item, dict):
|
|
1632
1595
|
return None
|
|
@@ -1671,22 +1634,6 @@ def _extract_error_message(payload: Any) -> Optional[str]:
|
|
|
1671
1634
|
return message
|
|
1672
1635
|
|
|
1673
1636
|
|
|
1674
|
-
def _extract_thread_id(payload: Any) -> Optional[str]:
|
|
1675
|
-
if not isinstance(payload, dict):
|
|
1676
|
-
return None
|
|
1677
|
-
for key in ("threadId", "thread_id", "id"):
|
|
1678
|
-
value = payload.get(key)
|
|
1679
|
-
if isinstance(value, str):
|
|
1680
|
-
return value
|
|
1681
|
-
thread = payload.get("thread")
|
|
1682
|
-
if isinstance(thread, dict):
|
|
1683
|
-
for key in ("id", "threadId", "thread_id"):
|
|
1684
|
-
value = thread.get(key)
|
|
1685
|
-
if isinstance(value, str):
|
|
1686
|
-
return value
|
|
1687
|
-
return None
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
1637
|
_SANDBOX_POLICY_CANONICAL = {
|
|
1691
1638
|
"dangerfullaccess": "dangerFullAccess",
|
|
1692
1639
|
"readonly": "readOnly",
|
|
@@ -1,110 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import os
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Mapping, Optional, Sequence
|
|
3
|
+
from ...core.app_server_utils import build_app_server_env
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
from ...core.utils import resolve_executable, subprocess_env
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def app_server_env(
|
|
13
|
-
command: Sequence[str],
|
|
14
|
-
cwd: Path,
|
|
15
|
-
*,
|
|
16
|
-
base_env: Optional[Mapping[str, str]] = None,
|
|
17
|
-
) -> dict[str, str]:
|
|
18
|
-
extra_paths: list[str] = []
|
|
19
|
-
if command:
|
|
20
|
-
binary = command[0]
|
|
21
|
-
resolved = resolve_executable(binary, env=base_env)
|
|
22
|
-
candidate: Optional[Path] = Path(resolved) if resolved else None
|
|
23
|
-
if candidate is None:
|
|
24
|
-
candidate = Path(binary).expanduser()
|
|
25
|
-
if not candidate.is_absolute():
|
|
26
|
-
candidate = (cwd / candidate).resolve()
|
|
27
|
-
if candidate.exists():
|
|
28
|
-
extra_paths.append(str(candidate.parent))
|
|
29
|
-
return subprocess_env(extra_paths=extra_paths, base_env=base_env)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def seed_codex_home(
|
|
33
|
-
codex_home: Path,
|
|
34
|
-
*,
|
|
35
|
-
logger: Optional[logging.Logger] = None,
|
|
36
|
-
event_prefix: str = "app_server",
|
|
37
|
-
) -> None:
|
|
38
|
-
logger = logger or logging.getLogger(__name__)
|
|
39
|
-
auth_path = codex_home / "auth.json"
|
|
40
|
-
source_root = Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser()
|
|
41
|
-
if source_root.resolve() == codex_home.resolve():
|
|
42
|
-
return
|
|
43
|
-
source_auth = source_root / "auth.json"
|
|
44
|
-
if auth_path.exists():
|
|
45
|
-
if auth_path.is_symlink() and auth_path.resolve() == source_auth.resolve():
|
|
46
|
-
return
|
|
47
|
-
log_event(
|
|
48
|
-
logger,
|
|
49
|
-
logging.INFO,
|
|
50
|
-
f"{event_prefix}.codex_home.seed.skipped",
|
|
51
|
-
reason="auth_exists",
|
|
52
|
-
source=str(source_root),
|
|
53
|
-
target=str(codex_home),
|
|
54
|
-
)
|
|
55
|
-
return
|
|
56
|
-
if not source_root.exists():
|
|
57
|
-
log_event(
|
|
58
|
-
logger,
|
|
59
|
-
logging.WARNING,
|
|
60
|
-
f"{event_prefix}.codex_home.seed.skipped",
|
|
61
|
-
reason="source_missing",
|
|
62
|
-
source=str(source_root),
|
|
63
|
-
target=str(codex_home),
|
|
64
|
-
)
|
|
65
|
-
return
|
|
66
|
-
if not source_auth.exists():
|
|
67
|
-
log_event(
|
|
68
|
-
logger,
|
|
69
|
-
logging.WARNING,
|
|
70
|
-
f"{event_prefix}.codex_home.seed.skipped",
|
|
71
|
-
reason="auth_missing",
|
|
72
|
-
source=str(source_root),
|
|
73
|
-
target=str(codex_home),
|
|
74
|
-
)
|
|
75
|
-
return
|
|
76
|
-
try:
|
|
77
|
-
auth_path.symlink_to(source_auth)
|
|
78
|
-
log_event(
|
|
79
|
-
logger,
|
|
80
|
-
logging.INFO,
|
|
81
|
-
f"{event_prefix}.codex_home.seeded",
|
|
82
|
-
source=str(source_root),
|
|
83
|
-
target=str(codex_home),
|
|
84
|
-
)
|
|
85
|
-
except OSError as exc:
|
|
86
|
-
log_event(
|
|
87
|
-
logger,
|
|
88
|
-
logging.WARNING,
|
|
89
|
-
f"{event_prefix}.codex_home.seed.failed",
|
|
90
|
-
exc=exc,
|
|
91
|
-
source=str(source_root),
|
|
92
|
-
target=str(codex_home),
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def build_app_server_env(
|
|
97
|
-
command: Sequence[str],
|
|
98
|
-
workspace_root: Path,
|
|
99
|
-
state_dir: Path,
|
|
100
|
-
*,
|
|
101
|
-
logger: Optional[logging.Logger] = None,
|
|
102
|
-
event_prefix: str = "app_server",
|
|
103
|
-
base_env: Optional[Mapping[str, str]] = None,
|
|
104
|
-
) -> dict[str, str]:
|
|
105
|
-
env = app_server_env(command, workspace_root, base_env=base_env)
|
|
106
|
-
codex_home = state_dir / "codex_home"
|
|
107
|
-
codex_home.mkdir(parents=True, exist_ok=True)
|
|
108
|
-
seed_codex_home(codex_home, logger=logger, event_prefix=event_prefix)
|
|
109
|
-
env["CODEX_HOME"] = str(codex_home)
|
|
110
|
-
return env
|
|
5
|
+
__all__ = ["build_app_server_env"]
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
+
import logging
|
|
3
4
|
import threading
|
|
4
5
|
import time
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from typing import Any, AsyncIterator, Dict, Optional
|
|
7
8
|
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
from ...core.app_server_ids import (
|
|
10
|
+
extract_thread_id,
|
|
11
|
+
extract_thread_id_for_turn,
|
|
12
|
+
extract_turn_id,
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
TurnKey = tuple[str, str]
|
|
16
|
+
LOGGER = logging.getLogger("codex_autorunner.app_server")
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
def format_sse(event: str, data: object) -> str:
|
|
@@ -143,11 +145,11 @@ class AppServerEventBuffer:
|
|
|
143
145
|
) -> tuple[Optional[str], Optional[str]]:
|
|
144
146
|
params_raw = message.get("params")
|
|
145
147
|
params: Dict[str, Any] = params_raw if isinstance(params_raw, dict) else {}
|
|
146
|
-
turn_id =
|
|
148
|
+
turn_id = extract_turn_id(params) or extract_turn_id(message)
|
|
147
149
|
thread_id = (
|
|
148
|
-
|
|
149
|
-
or
|
|
150
|
-
or
|
|
150
|
+
extract_thread_id_for_turn(params)
|
|
151
|
+
or extract_thread_id(params)
|
|
152
|
+
or extract_thread_id(message)
|
|
151
153
|
)
|
|
152
154
|
if not thread_id and turn_id:
|
|
153
155
|
thread_id = self._turn_index.get(turn_id)
|
|
@@ -184,9 +186,14 @@ class AppServerEventBuffer:
|
|
|
184
186
|
try:
|
|
185
187
|
lines = formatter.format_event(message)
|
|
186
188
|
except Exception:
|
|
189
|
+
LOGGER.warning("Failed to format app server event log line.", exc_info=True)
|
|
187
190
|
return
|
|
188
191
|
for line in lines:
|
|
189
192
|
try:
|
|
190
193
|
emit(line)
|
|
191
194
|
except Exception:
|
|
195
|
+
LOGGER.warning(
|
|
196
|
+
"Failed to emit app server event log line.",
|
|
197
|
+
exc_info=True,
|
|
198
|
+
)
|
|
192
199
|
continue
|
|
@@ -259,6 +259,17 @@ class PageCallback:
|
|
|
259
259
|
page: int
|
|
260
260
|
|
|
261
261
|
|
|
262
|
+
@dataclass(frozen=True)
|
|
263
|
+
class FlowCallback:
|
|
264
|
+
action: str
|
|
265
|
+
run_id: Optional[str] = None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dataclass(frozen=True)
|
|
269
|
+
class FlowRunCallback:
|
|
270
|
+
run_id: str
|
|
271
|
+
|
|
272
|
+
|
|
262
273
|
def parse_command(
|
|
263
274
|
text: Optional[str],
|
|
264
275
|
*,
|
|
@@ -741,6 +752,27 @@ def encode_compact_callback(action: str) -> str:
|
|
|
741
752
|
return data
|
|
742
753
|
|
|
743
754
|
|
|
755
|
+
def encode_flow_callback(action: str, run_id: Optional[str] = None) -> str:
|
|
756
|
+
action = str(action or "").strip()
|
|
757
|
+
if not action:
|
|
758
|
+
raise ValueError("flow action required")
|
|
759
|
+
if run_id:
|
|
760
|
+
data = f"flow:{action}:{run_id}"
|
|
761
|
+
else:
|
|
762
|
+
data = f"flow:{action}"
|
|
763
|
+
_validate_callback_data(data)
|
|
764
|
+
return data
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def encode_flow_run_callback(run_id: str) -> str:
|
|
768
|
+
run_id = str(run_id or "").strip()
|
|
769
|
+
if not run_id:
|
|
770
|
+
raise ValueError("flow run id required")
|
|
771
|
+
data = f"flow_run:{run_id}"
|
|
772
|
+
_validate_callback_data(data)
|
|
773
|
+
return data
|
|
774
|
+
|
|
775
|
+
|
|
744
776
|
def parse_callback_data(
|
|
745
777
|
data: Optional[str],
|
|
746
778
|
) -> Optional[
|
|
@@ -760,6 +792,8 @@ def parse_callback_data(
|
|
|
760
792
|
ReviewCommitCallback,
|
|
761
793
|
CancelCallback,
|
|
762
794
|
CompactCallback,
|
|
795
|
+
FlowCallback,
|
|
796
|
+
FlowRunCallback,
|
|
763
797
|
PageCallback,
|
|
764
798
|
]
|
|
765
799
|
]:
|
|
@@ -857,6 +891,19 @@ def parse_callback_data(
|
|
|
857
891
|
if not page.isdigit():
|
|
858
892
|
return None
|
|
859
893
|
return PageCallback(kind=kind, page=int(page))
|
|
894
|
+
if data.startswith("flow:"):
|
|
895
|
+
_, _, rest = data.partition(":")
|
|
896
|
+
action, sep, run_id = rest.partition(":")
|
|
897
|
+
if not action:
|
|
898
|
+
return None
|
|
899
|
+
if sep and not run_id:
|
|
900
|
+
return None
|
|
901
|
+
return FlowCallback(action=action, run_id=run_id or None)
|
|
902
|
+
if data.startswith("flow_run:"):
|
|
903
|
+
_, _, run_id = data.partition(":")
|
|
904
|
+
if not run_id:
|
|
905
|
+
return None
|
|
906
|
+
return FlowRunCallback(run_id=run_id)
|
|
860
907
|
return None
|
|
861
908
|
|
|
862
909
|
|
|
@@ -1062,6 +1109,24 @@ def build_bind_keyboard(
|
|
|
1062
1109
|
return build_inline_keyboard(rows)
|
|
1063
1110
|
|
|
1064
1111
|
|
|
1112
|
+
def build_flow_runs_keyboard(
|
|
1113
|
+
options: Sequence[tuple[str, str]],
|
|
1114
|
+
*,
|
|
1115
|
+
page_button: Optional[tuple[str, str]] = None,
|
|
1116
|
+
include_cancel: bool = False,
|
|
1117
|
+
) -> dict[str, Any]:
|
|
1118
|
+
rows = [
|
|
1119
|
+
[InlineButton(label, encode_flow_run_callback(run_id))]
|
|
1120
|
+
for run_id, label in options
|
|
1121
|
+
]
|
|
1122
|
+
if page_button:
|
|
1123
|
+
label, callback_data = page_button
|
|
1124
|
+
rows.append([InlineButton(label, callback_data)])
|
|
1125
|
+
if include_cancel:
|
|
1126
|
+
rows.append([InlineButton("Cancel", encode_cancel_callback("flow-runs"))])
|
|
1127
|
+
return build_inline_keyboard(rows)
|
|
1128
|
+
|
|
1129
|
+
|
|
1065
1130
|
def _validate_callback_data(data: str) -> None:
|
|
1066
1131
|
if len(data.encode("utf-8")) > TELEGRAM_CALLBACK_DATA_LIMIT:
|
|
1067
1132
|
raise ValueError("callback_data exceeds Telegram limit")
|