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
|
@@ -11,6 +11,7 @@ from typing import Any, Mapping, Optional, Sequence
|
|
|
11
11
|
import httpx
|
|
12
12
|
|
|
13
13
|
from ...core.logging_utils import log_event
|
|
14
|
+
from ...core.supervisor_utils import evict_lru_handle_locked, pop_idle_handles_locked
|
|
14
15
|
from ...core.utils import infer_home_from_workspace, subprocess_env
|
|
15
16
|
from ...workspace import canonical_workspace_root, workspace_id_for_path
|
|
16
17
|
from .client import OpenCodeClient
|
|
@@ -53,20 +54,28 @@ class OpenCodeSupervisor:
|
|
|
53
54
|
base_env: Optional[Mapping[str, str]] = None,
|
|
54
55
|
base_url: Optional[str] = None,
|
|
55
56
|
subagent_models: Optional[Mapping[str, str]] = None,
|
|
57
|
+
session_stall_timeout_seconds: Optional[float] = None,
|
|
56
58
|
) -> None:
|
|
57
59
|
self._command = [str(arg) for arg in command]
|
|
58
60
|
self._logger = logger or logging.getLogger(__name__)
|
|
59
61
|
self._request_timeout = request_timeout
|
|
60
62
|
self._max_handles = max_handles
|
|
61
63
|
self._idle_ttl_seconds = idle_ttl_seconds
|
|
64
|
+
self._session_stall_timeout_seconds = session_stall_timeout_seconds
|
|
62
65
|
if password and not username:
|
|
63
66
|
username = "opencode"
|
|
64
|
-
self._auth
|
|
67
|
+
self._auth: Optional[tuple[str, str]] = (
|
|
68
|
+
(username, password) if password and username else None
|
|
69
|
+
)
|
|
65
70
|
self._base_env = base_env
|
|
66
71
|
self._base_url = base_url
|
|
67
72
|
self._subagent_models = subagent_models or {}
|
|
68
73
|
self._handles: dict[str, OpenCodeHandle] = {}
|
|
69
|
-
self._lock
|
|
74
|
+
self._lock: Optional[asyncio.Lock] = None
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def session_stall_timeout_seconds(self) -> Optional[float]:
|
|
78
|
+
return self._session_stall_timeout_seconds
|
|
70
79
|
|
|
71
80
|
async def get_client(self, workspace_root: Path) -> OpenCodeClient:
|
|
72
81
|
canonical_root = canonical_workspace_root(workspace_root)
|
|
@@ -79,7 +88,7 @@ class OpenCodeSupervisor:
|
|
|
79
88
|
return handle.client
|
|
80
89
|
|
|
81
90
|
async def close_all(self) -> None:
|
|
82
|
-
async with self.
|
|
91
|
+
async with self._get_lock():
|
|
83
92
|
handles = list(self._handles.values())
|
|
84
93
|
self._handles = {}
|
|
85
94
|
for handle in handles:
|
|
@@ -98,7 +107,7 @@ class OpenCodeSupervisor:
|
|
|
98
107
|
async def mark_turn_started(self, workspace_root: Path) -> None:
|
|
99
108
|
canonical_root = canonical_workspace_root(workspace_root)
|
|
100
109
|
workspace_id = workspace_id_for_path(canonical_root)
|
|
101
|
-
async with self.
|
|
110
|
+
async with self._get_lock():
|
|
102
111
|
handle = self._handles.get(workspace_id)
|
|
103
112
|
if handle is None:
|
|
104
113
|
return
|
|
@@ -108,7 +117,7 @@ class OpenCodeSupervisor:
|
|
|
108
117
|
async def mark_turn_finished(self, workspace_root: Path) -> None:
|
|
109
118
|
canonical_root = canonical_workspace_root(workspace_root)
|
|
110
119
|
workspace_id = workspace_id_for_path(canonical_root)
|
|
111
|
-
async with self.
|
|
120
|
+
async with self._get_lock():
|
|
112
121
|
handle = self._handles.get(workspace_id)
|
|
113
122
|
if handle is None:
|
|
114
123
|
return
|
|
@@ -187,7 +196,7 @@ class OpenCodeSupervisor:
|
|
|
187
196
|
) -> OpenCodeHandle:
|
|
188
197
|
handles_to_close: list[OpenCodeHandle] = []
|
|
189
198
|
evicted_id: Optional[str] = None
|
|
190
|
-
async with self.
|
|
199
|
+
async with self._get_lock():
|
|
191
200
|
existing = self._handles.get(workspace_id)
|
|
192
201
|
if existing is not None:
|
|
193
202
|
existing.last_used_at = time.monotonic()
|
|
@@ -287,7 +296,7 @@ class OpenCodeSupervisor:
|
|
|
287
296
|
logging.WARNING,
|
|
288
297
|
"opencode.openapi.fetch_failed",
|
|
289
298
|
base_url=base_url,
|
|
290
|
-
exc=
|
|
299
|
+
exc=exc,
|
|
291
300
|
)
|
|
292
301
|
handle.openapi_spec = {}
|
|
293
302
|
handle.started = True
|
|
@@ -356,7 +365,7 @@ class OpenCodeSupervisor:
|
|
|
356
365
|
logging.WARNING,
|
|
357
366
|
"opencode.openapi.fetch_failed",
|
|
358
367
|
base_url=base_url,
|
|
359
|
-
exc=
|
|
368
|
+
exc=exc,
|
|
360
369
|
)
|
|
361
370
|
handle.openapi_spec = {}
|
|
362
371
|
self._start_stdout_drain(handle)
|
|
@@ -468,53 +477,32 @@ class OpenCodeSupervisor:
|
|
|
468
477
|
return match.group(1)
|
|
469
478
|
|
|
470
479
|
async def _pop_idle_handles(self) -> list[OpenCodeHandle]:
|
|
471
|
-
async with self.
|
|
480
|
+
async with self._get_lock():
|
|
472
481
|
return self._pop_idle_handles_locked()
|
|
473
482
|
|
|
483
|
+
def _get_lock(self) -> asyncio.Lock:
|
|
484
|
+
if self._lock is None:
|
|
485
|
+
self._lock = asyncio.Lock()
|
|
486
|
+
return self._lock
|
|
487
|
+
|
|
474
488
|
def _pop_idle_handles_locked(self) -> list[OpenCodeHandle]:
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
logging.INFO,
|
|
484
|
-
"opencode.handle.prune.skipped",
|
|
485
|
-
reason="active_turns",
|
|
486
|
-
workspace_id=handle.workspace_id,
|
|
487
|
-
workspace_root=str(handle.workspace_root),
|
|
488
|
-
active_turns=handle.active_turns,
|
|
489
|
-
)
|
|
490
|
-
continue
|
|
491
|
-
if handle.last_used_at and handle.last_used_at < cutoff:
|
|
492
|
-
self._handles.pop(handle.workspace_id, None)
|
|
493
|
-
stale.append(handle)
|
|
494
|
-
return stale
|
|
489
|
+
return pop_idle_handles_locked(
|
|
490
|
+
self._handles,
|
|
491
|
+
self._idle_ttl_seconds,
|
|
492
|
+
self._logger,
|
|
493
|
+
"opencode",
|
|
494
|
+
last_used_at_getter=lambda h: h.last_used_at,
|
|
495
|
+
should_skip_prune=lambda h: h.active_turns > 0,
|
|
496
|
+
)
|
|
495
497
|
|
|
496
498
|
def _evict_lru_handle_locked(self) -> Optional[OpenCodeHandle]:
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
return None
|
|
501
|
-
lru_handle = min(
|
|
502
|
-
self._handles.values(),
|
|
503
|
-
key=lambda handle: handle.last_used_at or 0.0,
|
|
504
|
-
)
|
|
505
|
-
log_event(
|
|
499
|
+
return evict_lru_handle_locked(
|
|
500
|
+
self._handles,
|
|
501
|
+
self._max_handles,
|
|
506
502
|
self._logger,
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
reason="max_handles",
|
|
510
|
-
workspace_id=lru_handle.workspace_id,
|
|
511
|
-
workspace_root=str(lru_handle.workspace_root),
|
|
512
|
-
max_handles=self._max_handles,
|
|
513
|
-
handle_count=len(self._handles),
|
|
514
|
-
last_used_at=lru_handle.last_used_at,
|
|
503
|
+
"opencode",
|
|
504
|
+
last_used_at_getter=lambda h: h.last_used_at or 0.0,
|
|
515
505
|
)
|
|
516
|
-
self._handles.pop(lru_handle.workspace_id, None)
|
|
517
|
-
return lru_handle
|
|
518
506
|
|
|
519
507
|
|
|
520
508
|
__all__ = ["OpenCodeHandle", "OpenCodeSupervisor", "OpenCodeSupervisorError"]
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import importlib.metadata
|
|
3
4
|
import logging
|
|
4
5
|
from dataclasses import dataclass
|
|
5
|
-
from typing import Any, Callable, Literal, Optional
|
|
6
|
+
from typing import Any, Callable, Iterable, Literal, Optional
|
|
6
7
|
|
|
8
|
+
from ..plugin_api import CAR_AGENT_ENTRYPOINT_GROUP, CAR_PLUGIN_API_VERSION
|
|
7
9
|
from .base import AgentHarness
|
|
8
10
|
from .codex.harness import CodexHarness
|
|
9
11
|
from .opencode.harness import OpenCodeHarness
|
|
@@ -22,11 +24,20 @@ AgentCapability = Literal[
|
|
|
22
24
|
|
|
23
25
|
@dataclass(frozen=True)
|
|
24
26
|
class AgentDescriptor:
|
|
27
|
+
"""A registered agent backend.
|
|
28
|
+
|
|
29
|
+
Built-in backends live in `_BUILTIN_AGENTS`. Additional backends MAY be loaded
|
|
30
|
+
via Python entry points (see `CAR_AGENT_ENTRYPOINT_GROUP`).
|
|
31
|
+
|
|
32
|
+
Plugins SHOULD set `plugin_api_version` to `CAR_PLUGIN_API_VERSION`.
|
|
33
|
+
"""
|
|
34
|
+
|
|
25
35
|
id: str
|
|
26
36
|
name: str
|
|
27
37
|
capabilities: frozenset[AgentCapability]
|
|
28
38
|
make_harness: Callable[[Any], AgentHarness]
|
|
29
39
|
healthcheck: Optional[Callable[[Any], bool]] = None
|
|
40
|
+
plugin_api_version: int = CAR_PLUGIN_API_VERSION
|
|
30
41
|
|
|
31
42
|
|
|
32
43
|
def _make_codex_harness(ctx: Any) -> AgentHarness:
|
|
@@ -54,7 +65,7 @@ def _check_opencode_health(ctx: Any) -> bool:
|
|
|
54
65
|
return supervisor is not None
|
|
55
66
|
|
|
56
67
|
|
|
57
|
-
|
|
68
|
+
_BUILTIN_AGENTS: dict[str, AgentDescriptor] = {
|
|
58
69
|
"codex": AgentDescriptor(
|
|
59
70
|
id="codex",
|
|
60
71
|
name="Codex",
|
|
@@ -88,32 +99,165 @@ _REGISTERED_AGENTS: dict[str, AgentDescriptor] = {
|
|
|
88
99
|
),
|
|
89
100
|
}
|
|
90
101
|
|
|
102
|
+
# Lazy-loaded cache of built-in + plugin agents.
|
|
103
|
+
_AGENT_CACHE: Optional[dict[str, AgentDescriptor]] = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _select_entry_points(group: str) -> Iterable[importlib.metadata.EntryPoint]:
|
|
107
|
+
"""Compatibility wrapper for `importlib.metadata.entry_points()` across py versions."""
|
|
108
|
+
|
|
109
|
+
eps = importlib.metadata.entry_points()
|
|
110
|
+
# Python 3.9: may return a dict
|
|
111
|
+
if isinstance(eps, dict):
|
|
112
|
+
return eps.get(group, [])
|
|
113
|
+
if hasattr(eps, "select"):
|
|
114
|
+
return list(eps.select(group=group))
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _load_agent_plugins() -> dict[str, AgentDescriptor]:
|
|
119
|
+
loaded: dict[str, AgentDescriptor] = {}
|
|
120
|
+
for ep in _select_entry_points(CAR_AGENT_ENTRYPOINT_GROUP):
|
|
121
|
+
try:
|
|
122
|
+
obj = ep.load()
|
|
123
|
+
except Exception as exc: # noqa: BLE001
|
|
124
|
+
_logger.warning(
|
|
125
|
+
"Failed to load agent plugin entry point %s:%s: %s",
|
|
126
|
+
ep.group,
|
|
127
|
+
ep.name,
|
|
128
|
+
exc,
|
|
129
|
+
)
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
descriptor: Optional[AgentDescriptor] = None
|
|
133
|
+
if isinstance(obj, AgentDescriptor):
|
|
134
|
+
descriptor = obj
|
|
135
|
+
elif callable(obj):
|
|
136
|
+
try:
|
|
137
|
+
maybe = obj()
|
|
138
|
+
except Exception as exc: # noqa: BLE001
|
|
139
|
+
_logger.warning(
|
|
140
|
+
"Agent plugin entry point %s:%s factory failed: %s",
|
|
141
|
+
ep.group,
|
|
142
|
+
ep.name,
|
|
143
|
+
exc,
|
|
144
|
+
)
|
|
145
|
+
continue
|
|
146
|
+
if isinstance(maybe, AgentDescriptor):
|
|
147
|
+
descriptor = maybe
|
|
148
|
+
|
|
149
|
+
if descriptor is None:
|
|
150
|
+
_logger.warning(
|
|
151
|
+
"Ignoring agent plugin entry point %s:%s: expected AgentDescriptor or factory",
|
|
152
|
+
ep.group,
|
|
153
|
+
ep.name,
|
|
154
|
+
)
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
agent_id = (descriptor.id or "").strip().lower()
|
|
158
|
+
if not agent_id:
|
|
159
|
+
_logger.warning(
|
|
160
|
+
"Ignoring agent plugin entry point %s:%s: missing id",
|
|
161
|
+
ep.group,
|
|
162
|
+
ep.name,
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
api_version_raw = getattr(descriptor, "plugin_api_version", None)
|
|
167
|
+
try:
|
|
168
|
+
api_version = int(api_version_raw)
|
|
169
|
+
except Exception:
|
|
170
|
+
api_version = None
|
|
171
|
+
if api_version is None:
|
|
172
|
+
_logger.warning(
|
|
173
|
+
"Ignoring agent plugin %s: invalid api_version %s",
|
|
174
|
+
agent_id,
|
|
175
|
+
api_version_raw,
|
|
176
|
+
)
|
|
177
|
+
continue
|
|
178
|
+
if api_version > CAR_PLUGIN_API_VERSION:
|
|
179
|
+
_logger.warning(
|
|
180
|
+
"Ignoring agent plugin %s (api_version=%s) requires newer core (%s)",
|
|
181
|
+
agent_id,
|
|
182
|
+
api_version,
|
|
183
|
+
CAR_PLUGIN_API_VERSION,
|
|
184
|
+
)
|
|
185
|
+
continue
|
|
186
|
+
if api_version < CAR_PLUGIN_API_VERSION:
|
|
187
|
+
_logger.info(
|
|
188
|
+
"Loaded agent plugin %s with older api_version=%s (current=%s)",
|
|
189
|
+
agent_id,
|
|
190
|
+
api_version,
|
|
191
|
+
CAR_PLUGIN_API_VERSION,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if agent_id in _BUILTIN_AGENTS:
|
|
195
|
+
_logger.warning(
|
|
196
|
+
"Ignoring agent plugin %s: conflicts with built-in agent id",
|
|
197
|
+
agent_id,
|
|
198
|
+
)
|
|
199
|
+
continue
|
|
200
|
+
if agent_id in loaded:
|
|
201
|
+
_logger.warning(
|
|
202
|
+
"Ignoring duplicate agent plugin id %s from entry point %s:%s",
|
|
203
|
+
agent_id,
|
|
204
|
+
ep.group,
|
|
205
|
+
ep.name,
|
|
206
|
+
)
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
loaded[agent_id] = descriptor
|
|
210
|
+
_logger.info("Loaded agent plugin: %s (%s)", agent_id, descriptor.name)
|
|
211
|
+
|
|
212
|
+
return loaded
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _all_agents() -> dict[str, AgentDescriptor]:
|
|
216
|
+
global _AGENT_CACHE
|
|
217
|
+
if _AGENT_CACHE is None:
|
|
218
|
+
agents = _BUILTIN_AGENTS.copy()
|
|
219
|
+
agents.update(_load_agent_plugins())
|
|
220
|
+
_AGENT_CACHE = agents
|
|
221
|
+
return _AGENT_CACHE
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def reload_agents() -> dict[str, AgentDescriptor]:
|
|
225
|
+
"""Clear the plugin cache and reload agent backends.
|
|
226
|
+
|
|
227
|
+
This is primarily useful for tests and local development.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
global _AGENT_CACHE
|
|
231
|
+
_AGENT_CACHE = None
|
|
232
|
+
return get_registered_agents()
|
|
233
|
+
|
|
91
234
|
|
|
92
235
|
def get_registered_agents() -> dict[str, AgentDescriptor]:
|
|
93
|
-
return
|
|
236
|
+
return _all_agents().copy()
|
|
94
237
|
|
|
95
238
|
|
|
96
239
|
def get_available_agents(app_ctx: Any) -> dict[str, AgentDescriptor]:
|
|
97
|
-
available = {}
|
|
98
|
-
for agent_id, descriptor in
|
|
240
|
+
available: dict[str, AgentDescriptor] = {}
|
|
241
|
+
for agent_id, descriptor in _all_agents().items():
|
|
99
242
|
if descriptor.healthcheck is None or descriptor.healthcheck(app_ctx):
|
|
100
243
|
available[agent_id] = descriptor
|
|
101
244
|
return available
|
|
102
245
|
|
|
103
246
|
|
|
104
247
|
def get_agent_descriptor(agent_id: str) -> Optional[AgentDescriptor]:
|
|
105
|
-
|
|
248
|
+
normalized = (agent_id or "").strip().lower()
|
|
249
|
+
return _all_agents().get(normalized)
|
|
106
250
|
|
|
107
251
|
|
|
108
252
|
def validate_agent_id(agent_id: str) -> str:
|
|
109
253
|
normalized = (agent_id or "").strip().lower()
|
|
110
|
-
if normalized not in
|
|
254
|
+
if normalized not in _all_agents():
|
|
111
255
|
raise ValueError(f"Unknown agent: {agent_id!r}")
|
|
112
256
|
return normalized
|
|
113
257
|
|
|
114
258
|
|
|
115
259
|
def has_capability(agent_id: str, capability: AgentCapability) -> bool:
|
|
116
|
-
descriptor =
|
|
260
|
+
descriptor = get_agent_descriptor(agent_id)
|
|
117
261
|
if descriptor is None:
|
|
118
262
|
return False
|
|
119
263
|
return capability in descriptor.capabilities
|
|
@@ -122,9 +266,12 @@ def has_capability(agent_id: str, capability: AgentCapability) -> bool:
|
|
|
122
266
|
__all__ = [
|
|
123
267
|
"AgentCapability",
|
|
124
268
|
"AgentDescriptor",
|
|
269
|
+
"CAR_PLUGIN_API_VERSION",
|
|
270
|
+
"CAR_AGENT_ENTRYPOINT_GROUP",
|
|
125
271
|
"get_registered_agents",
|
|
126
272
|
"get_available_agents",
|
|
127
273
|
"get_agent_descriptor",
|
|
128
274
|
"validate_agent_id",
|
|
129
275
|
"has_capability",
|
|
276
|
+
"reload_agents",
|
|
130
277
|
]
|
codex_autorunner/api.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Stable public API for Codex Autorunner plugins.
|
|
2
|
+
|
|
3
|
+
Everything else in the codebase should be treated as internal unless documented.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from .agents.base import AgentHarness
|
|
9
|
+
from .agents.registry import AgentCapability, AgentDescriptor, reload_agents
|
|
10
|
+
from .agents.types import AgentId, ConversationRef, ModelCatalog, ModelSpec, TurnRef
|
|
11
|
+
from .plugin_api import CAR_AGENT_ENTRYPOINT_GROUP, CAR_PLUGIN_API_VERSION
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AgentCapability",
|
|
15
|
+
"AgentDescriptor",
|
|
16
|
+
"AgentHarness",
|
|
17
|
+
"AgentId",
|
|
18
|
+
"ConversationRef",
|
|
19
|
+
"ModelCatalog",
|
|
20
|
+
"ModelSpec",
|
|
21
|
+
"TurnRef",
|
|
22
|
+
"CAR_AGENT_ENTRYPOINT_GROUP",
|
|
23
|
+
"CAR_PLUGIN_API_VERSION",
|
|
24
|
+
"reload_agents",
|
|
25
|
+
]
|
codex_autorunner/bootstrap.py
CHANGED
|
@@ -11,43 +11,21 @@ from .core.config import (
|
|
|
11
11
|
resolve_hub_config_data,
|
|
12
12
|
)
|
|
13
13
|
from .core.state import RunnerState, save_state
|
|
14
|
+
from .core.ticket_linter_cli import ensure_ticket_linter
|
|
15
|
+
from .core.ticket_manager_cli import ensure_ticket_manager
|
|
14
16
|
from .core.utils import atomic_write
|
|
15
17
|
from .manifest import load_manifest
|
|
16
18
|
|
|
17
19
|
GITIGNORE_CONTENT = "*"
|
|
20
|
+
GENERATED_CONFIG_HEADER = "# GENERATED by CAR - DO NOT EDIT\n"
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
def sample_todo() -> str:
|
|
21
|
-
return ""
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def sample_opinions() -> str:
|
|
25
|
-
return """# Opinions\n\n- Prefer small, well-tested changes.\n- Keep docs in sync with code.\n- Avoid unnecessary dependencies.\n"""
|
|
24
|
+
return ""
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
def sample_spec() -> str:
|
|
29
|
-
return ""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def sample_summary() -> str:
|
|
33
|
-
return """# Summary
|
|
34
|
-
|
|
35
|
-
This doc is the **user-facing report and handoff** for work done by CAR agents.
|
|
36
|
-
|
|
37
|
-
Use it for:
|
|
38
|
-
- Anything that requires **user action** or an **external party** (not agents).
|
|
39
|
-
- Unresolved decisions or blockers that agents can’t finish autonomously.
|
|
40
|
-
- A final condensed report once TODO is complete.
|
|
41
|
-
|
|
42
|
-
## External/user actions
|
|
43
|
-
- (none)
|
|
44
|
-
|
|
45
|
-
## Open questions / blockers
|
|
46
|
-
- (none)
|
|
47
|
-
|
|
48
|
-
## Final report
|
|
49
|
-
- (pending)
|
|
50
|
-
"""
|
|
28
|
+
return ""
|
|
51
29
|
|
|
52
30
|
|
|
53
31
|
def _seed_doc(path: Path, force: bool, content: str) -> None:
|
|
@@ -82,6 +60,7 @@ def write_hub_config(hub_root: Path, force: bool = False) -> Path:
|
|
|
82
60
|
return config_path
|
|
83
61
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
84
62
|
with config_path.open("w", encoding="utf-8") as f:
|
|
63
|
+
f.write(GENERATED_CONFIG_HEADER)
|
|
85
64
|
yaml.safe_dump(
|
|
86
65
|
resolve_hub_config_data(hub_root),
|
|
87
66
|
f,
|
|
@@ -116,24 +95,30 @@ def seed_repo_files(
|
|
|
116
95
|
if not log_path.exists() or force:
|
|
117
96
|
log_path.write_text("", encoding="utf-8")
|
|
118
97
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
98
|
+
tickets_dir = ca_dir / "tickets"
|
|
99
|
+
if not tickets_dir.exists() or force:
|
|
100
|
+
tickets_dir.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
|
|
102
|
+
workspace_dir = ca_dir / "workspace"
|
|
103
|
+
if not workspace_dir.exists() or force:
|
|
104
|
+
workspace_dir.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
|
|
106
|
+
_seed_doc(workspace_dir / "active_context.md", force, sample_todo())
|
|
107
|
+
_seed_doc(workspace_dir / "decisions.md", force, "")
|
|
108
|
+
_seed_doc(workspace_dir / "spec.md", force, sample_spec())
|
|
124
109
|
|
|
125
110
|
# Seed an always-available briefing doc for interactive Codex sessions.
|
|
126
111
|
ensure_about_car_file_for_repo(
|
|
127
112
|
repo_root,
|
|
128
113
|
doc_paths={
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"spec": ca_dir / "SPEC.md",
|
|
133
|
-
"summary": ca_dir / "SUMMARY.md",
|
|
114
|
+
"active_context": workspace_dir / "active_context.md",
|
|
115
|
+
"decisions": workspace_dir / "decisions.md",
|
|
116
|
+
"spec": workspace_dir / "spec.md",
|
|
134
117
|
},
|
|
135
118
|
force=force,
|
|
136
119
|
)
|
|
120
|
+
ensure_ticket_linter(repo_root, force=force)
|
|
121
|
+
ensure_ticket_manager(repo_root, force=force)
|
|
137
122
|
|
|
138
123
|
|
|
139
124
|
def seed_hub_files(hub_root: Path, force: bool = False) -> None:
|