codex-autorunner 0.1.1__py3-none-any.whl → 1.0.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/__main__.py +4 -0
- codex_autorunner/agents/__init__.py +20 -0
- codex_autorunner/agents/base.py +2 -2
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/__init__.py +4 -0
- codex_autorunner/agents/opencode/agent_config.py +104 -0
- codex_autorunner/agents/opencode/client.py +305 -28
- codex_autorunner/agents/opencode/harness.py +71 -20
- codex_autorunner/agents/opencode/logging.py +225 -0
- codex_autorunner/agents/opencode/run_prompt.py +261 -0
- codex_autorunner/agents/opencode/runtime.py +1202 -132
- codex_autorunner/agents/opencode/supervisor.py +194 -68
- codex_autorunner/agents/registry.py +258 -0
- codex_autorunner/agents/types.py +2 -2
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +19 -40
- codex_autorunner/cli.py +234 -151
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_events.py +15 -6
- codex_autorunner/core/app_server_logging.py +55 -15
- codex_autorunner/core/app_server_prompts.py +28 -259
- codex_autorunner/core/app_server_threads.py +15 -26
- codex_autorunner/core/circuit_breaker.py +183 -0
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +555 -133
- codex_autorunner/core/docs.py +54 -9
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +828 -274
- codex_autorunner/core/exceptions.py +60 -0
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +178 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +75 -0
- codex_autorunner/core/flows/runtime.py +351 -0
- codex_autorunner/core/flows/store.py +485 -0
- codex_autorunner/core/flows/transition.py +133 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/hub.py +21 -13
- codex_autorunner/core/locks.py +118 -1
- codex_autorunner/core/logging_utils.py +9 -6
- codex_autorunner/core/path_utils.py +123 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/retry.py +61 -0
- codex_autorunner/core/review.py +888 -0
- codex_autorunner/core/review_context.py +161 -0
- codex_autorunner/core/run_index.py +223 -0
- codex_autorunner/core/runner_controller.py +44 -1
- codex_autorunner/core/runner_process.py +30 -1
- codex_autorunner/core/sqlite_utils.py +32 -0
- codex_autorunner/core/state.py +273 -44
- codex_autorunner/core/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +43 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +107 -75
- codex_autorunner/core/utils.py +167 -3
- codex_autorunner/discovery.py +3 -3
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +91 -0
- codex_autorunner/integrations/agents/__init__.py +27 -0
- codex_autorunner/integrations/agents/agent_backend.py +142 -0
- codex_autorunner/integrations/agents/codex_backend.py +307 -0
- codex_autorunner/integrations/agents/opencode_backend.py +325 -0
- codex_autorunner/integrations/agents/run_event.py +71 -0
- codex_autorunner/integrations/app_server/client.py +708 -153
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +474 -185
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +239 -1
- codex_autorunner/integrations/telegram/constants.py +19 -1
- codex_autorunner/integrations/telegram/dispatch.py +44 -8
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/approvals.py +12 -10
- codex_autorunner/integrations/telegram/handlers/callbacks.py +15 -1
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +29 -0
- codex_autorunner/integrations/telegram/handlers/commands/approvals.py +173 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +2595 -0
- codex_autorunner/integrations/telegram/handlers/commands/files.py +1408 -0
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +81 -0
- codex_autorunner/integrations/telegram/handlers/commands/github.py +1688 -0
- codex_autorunner/integrations/telegram/handlers/commands/shared.py +190 -0
- codex_autorunner/integrations/telegram/handlers/commands/voice.py +112 -0
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +2043 -0
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +954 -5689
- codex_autorunner/integrations/telegram/handlers/{commands.py → commands_spec.py} +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +374 -49
- codex_autorunner/integrations/telegram/handlers/questions.py +389 -0
- codex_autorunner/integrations/telegram/handlers/selections.py +6 -4
- codex_autorunner/integrations/telegram/handlers/utils.py +171 -0
- codex_autorunner/integrations/telegram/helpers.py +90 -18
- codex_autorunner/integrations/telegram/notifications.py +126 -35
- codex_autorunner/integrations/telegram/outbox.py +214 -43
- codex_autorunner/integrations/telegram/progress_stream.py +42 -19
- codex_autorunner/integrations/telegram/runtime.py +24 -13
- codex_autorunner/integrations/telegram/service.py +500 -129
- codex_autorunner/integrations/telegram/state.py +1278 -330
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +37 -4
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/integrations/telegram/types.py +22 -2
- codex_autorunner/integrations/telegram/voice.py +14 -15
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +25 -14
- codex_autorunner/routes/agents.py +18 -78
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +142 -113
- codex_autorunner/routes/file_chat.py +836 -0
- codex_autorunner/routes/flows.py +980 -0
- codex_autorunner/routes/messages.py +459 -0
- codex_autorunner/routes/repos.py +17 -0
- codex_autorunner/routes/review.py +148 -0
- codex_autorunner/routes/sessions.py +16 -8
- codex_autorunner/routes/settings.py +22 -0
- codex_autorunner/routes/shared.py +33 -3
- codex_autorunner/routes/system.py +22 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/voice.py +5 -13
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +9 -1
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +27 -22
- codex_autorunner/static/autoRefresh.js +29 -1
- 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 +162 -150
- 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 +67 -126
- codex_autorunner/static/index.html +788 -807
- codex_autorunner/static/liveUpdates.js +59 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +470 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/settings.js +24 -205
- codex_autorunner/static/styles.css +7577 -3758
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -0
- codex_autorunner/static/terminalManager.js +53 -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 +750 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1315 -0
- codex_autorunner/static/utils.js +32 -3
- codex_autorunner/static/voice.js +21 -7
- codex_autorunner/static/workspace.js +672 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/tickets/__init__.py +20 -0
- codex_autorunner/tickets/agent_pool.py +377 -0
- codex_autorunner/tickets/files.py +85 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +95 -0
- codex_autorunner/tickets/outbox.py +232 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +823 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/voice/capture.py +7 -7
- codex_autorunner/voice/service.py +51 -9
- codex_autorunner/web/app.py +419 -199
- codex_autorunner/web/hub_jobs.py +13 -2
- codex_autorunner/web/middleware.py +47 -13
- codex_autorunner/web/pty_session.py +26 -13
- codex_autorunner/web/schemas.py +114 -109
- codex_autorunner/web/static_assets.py +55 -42
- codex_autorunner/web/static_refresh.py +86 -0
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +20 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
- codex_autorunner/core/doc_chat.py +0 -1415
- 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 -118
- codex_autorunner/spec_ingest.py +0 -788
- codex_autorunner/static/docChatActions.js +0 -279
- 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 -274
- 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 -442
- codex_autorunner/static/logs.js +0 -640
- codex_autorunner/static/runs.js +0 -409
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -86
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.1.dist-info/RECORD +0 -191
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Modular API routes for the codex-autorunner server.
|
|
3
3
|
|
|
4
|
-
This package splits
|
|
5
|
-
- base: Index,
|
|
4
|
+
This package splits monolithic api_routes.py into focused modules:
|
|
5
|
+
- base: Index, WebSocket terminal, and general endpoints
|
|
6
6
|
- agents: Agent harness models and event streaming
|
|
7
7
|
- app_server: App-server thread registry endpoints
|
|
8
|
-
-
|
|
9
|
-
-
|
|
8
|
+
- workspace: Optional workspace docs (active_context/decisions/spec)
|
|
9
|
+
- flows: Flow runtime management (start/stop/resume/status/events/artifacts)
|
|
10
|
+
- messages: Inbox/message wrappers over ticket_flow dispatch + reply histories
|
|
10
11
|
- repos: Run control (start/stop/resume/reset)
|
|
11
|
-
- runs: Run telemetry and artifacts
|
|
12
12
|
- sessions: Terminal session registry endpoints
|
|
13
13
|
- settings: Session settings for autorunner overrides
|
|
14
|
+
- file_chat: Unified file chat (tickets + workspace docs)
|
|
14
15
|
- voice: Voice transcription and config
|
|
15
16
|
- terminal_images: Terminal image uploads
|
|
16
17
|
"""
|
|
@@ -20,25 +21,29 @@ from pathlib import Path
|
|
|
20
21
|
from fastapi import APIRouter
|
|
21
22
|
|
|
22
23
|
from .agents import build_agents_routes
|
|
24
|
+
from .analytics import build_analytics_routes
|
|
23
25
|
from .app_server import build_app_server_routes
|
|
24
|
-
from .base import build_base_routes
|
|
25
|
-
from .
|
|
26
|
-
from .
|
|
26
|
+
from .base import build_base_routes, build_frontend_routes
|
|
27
|
+
from .file_chat import build_file_chat_routes
|
|
28
|
+
from .flows import build_flow_routes
|
|
29
|
+
from .messages import build_messages_routes
|
|
27
30
|
from .repos import build_repos_routes
|
|
28
|
-
from .
|
|
31
|
+
from .review import build_review_routes
|
|
29
32
|
from .sessions import build_sessions_routes
|
|
30
33
|
from .settings import build_settings_routes
|
|
31
34
|
from .system import build_system_routes
|
|
32
35
|
from .terminal_images import build_terminal_image_routes
|
|
36
|
+
from .usage import build_usage_routes
|
|
33
37
|
from .voice import build_voice_routes
|
|
38
|
+
from .workspace import build_workspace_routes
|
|
34
39
|
|
|
35
40
|
|
|
36
41
|
def build_repo_router(static_dir: Path) -> APIRouter:
|
|
37
42
|
"""
|
|
38
|
-
Build
|
|
43
|
+
Build complete API router by combining all route modules.
|
|
39
44
|
|
|
40
45
|
Args:
|
|
41
|
-
static_dir: Path to
|
|
46
|
+
static_dir: Path to static assets directory
|
|
42
47
|
|
|
43
48
|
Returns:
|
|
44
49
|
Combined APIRouter with all endpoints
|
|
@@ -47,17 +52,23 @@ def build_repo_router(static_dir: Path) -> APIRouter:
|
|
|
47
52
|
|
|
48
53
|
# Include all route modules
|
|
49
54
|
router.include_router(build_base_routes(static_dir))
|
|
55
|
+
router.include_router(build_analytics_routes())
|
|
50
56
|
router.include_router(build_agents_routes())
|
|
51
57
|
router.include_router(build_app_server_routes())
|
|
52
|
-
router.include_router(
|
|
53
|
-
router.include_router(
|
|
58
|
+
router.include_router(build_workspace_routes())
|
|
59
|
+
router.include_router(build_flow_routes())
|
|
60
|
+
router.include_router(build_file_chat_routes())
|
|
61
|
+
router.include_router(build_messages_routes())
|
|
54
62
|
router.include_router(build_repos_routes())
|
|
55
|
-
router.include_router(
|
|
63
|
+
router.include_router(build_review_routes())
|
|
56
64
|
router.include_router(build_sessions_routes())
|
|
57
65
|
router.include_router(build_settings_routes())
|
|
58
66
|
router.include_router(build_system_routes())
|
|
59
67
|
router.include_router(build_terminal_image_routes())
|
|
68
|
+
router.include_router(build_usage_routes())
|
|
60
69
|
router.include_router(build_voice_routes())
|
|
70
|
+
# Include frontend routes last to avoid shadowing API routes
|
|
71
|
+
router.include_router(build_frontend_routes(static_dir))
|
|
61
72
|
|
|
62
73
|
return router
|
|
63
74
|
|
|
@@ -21,16 +21,27 @@ def _available_agents(request: Request) -> tuple[list[dict[str, str]], str]:
|
|
|
21
21
|
default_agent: Optional[str] = None
|
|
22
22
|
|
|
23
23
|
if getattr(request.app.state, "app_server_supervisor", None) is not None:
|
|
24
|
-
agents.append({"id": "codex", "name": "Codex"})
|
|
24
|
+
agents.append({"id": "codex", "name": "Codex", "protocol_version": "2.0"})
|
|
25
25
|
default_agent = "codex"
|
|
26
26
|
|
|
27
27
|
if getattr(request.app.state, "opencode_supervisor", None) is not None:
|
|
28
|
-
|
|
28
|
+
supervisor = getattr(request.app.state, "opencode_supervisor", None)
|
|
29
|
+
version = None
|
|
30
|
+
if supervisor and hasattr(supervisor, "_handles"):
|
|
31
|
+
handles = supervisor._handles
|
|
32
|
+
if handles:
|
|
33
|
+
first_handle = next(iter(handles.values()), None)
|
|
34
|
+
if first_handle:
|
|
35
|
+
version = getattr(first_handle, "version", None)
|
|
36
|
+
agent_data = {"id": "opencode", "name": "OpenCode"}
|
|
37
|
+
if version:
|
|
38
|
+
agent_data["version"] = str(version)
|
|
39
|
+
agents.append(agent_data)
|
|
29
40
|
if default_agent is None:
|
|
30
41
|
default_agent = "opencode"
|
|
31
42
|
|
|
32
43
|
if not agents:
|
|
33
|
-
agents = [{"id": "codex", "name": "Codex"}]
|
|
44
|
+
agents = [{"id": "codex", "name": "Codex", "protocol_version": "2.0"}]
|
|
34
45
|
default_agent = "codex"
|
|
35
46
|
|
|
36
47
|
return agents, default_agent or "codex"
|
|
@@ -51,76 +62,6 @@ def _serialize_model_catalog(catalog: ModelCatalog) -> dict[str, Any]:
|
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
|
|
54
|
-
def _coerce_opencode_providers(payload: Any) -> list[dict[str, Any]]:
|
|
55
|
-
if isinstance(payload, dict):
|
|
56
|
-
providers = payload.get("providers")
|
|
57
|
-
if isinstance(providers, list):
|
|
58
|
-
return [entry for entry in providers if isinstance(entry, dict)]
|
|
59
|
-
if isinstance(payload, list):
|
|
60
|
-
return [entry for entry in payload if isinstance(entry, dict)]
|
|
61
|
-
return []
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _build_opencode_model_catalog(payload: Any) -> ModelCatalog:
|
|
65
|
-
from ..agents.types import ModelSpec
|
|
66
|
-
|
|
67
|
-
providers = _coerce_opencode_providers(payload)
|
|
68
|
-
models: list[ModelSpec] = []
|
|
69
|
-
default_model = ""
|
|
70
|
-
if isinstance(payload, dict):
|
|
71
|
-
raw_default = payload.get("default")
|
|
72
|
-
if isinstance(raw_default, dict):
|
|
73
|
-
for provider in providers:
|
|
74
|
-
provider_id = provider.get("id") or provider.get("providerID")
|
|
75
|
-
if (
|
|
76
|
-
isinstance(provider_id, str)
|
|
77
|
-
and provider_id
|
|
78
|
-
and provider_id in raw_default
|
|
79
|
-
):
|
|
80
|
-
default_model_id = raw_default[provider_id]
|
|
81
|
-
if isinstance(default_model_id, str) and default_model_id:
|
|
82
|
-
default_model = f"{provider_id}/{default_model_id}"
|
|
83
|
-
break
|
|
84
|
-
for provider in providers:
|
|
85
|
-
provider_id = provider.get("id") or provider.get("providerID")
|
|
86
|
-
if not isinstance(provider_id, str) or not provider_id:
|
|
87
|
-
continue
|
|
88
|
-
models_map = provider.get("models")
|
|
89
|
-
if not isinstance(models_map, dict):
|
|
90
|
-
continue
|
|
91
|
-
for model_id, model in models_map.items():
|
|
92
|
-
if not isinstance(model_id, str) or not isinstance(model, dict):
|
|
93
|
-
continue
|
|
94
|
-
model_name = model.get("name") or model.get("id") or model_id
|
|
95
|
-
display_name = (
|
|
96
|
-
model_name if isinstance(model_name, str) and model_name else model_id
|
|
97
|
-
)
|
|
98
|
-
capabilities = model.get("capabilities")
|
|
99
|
-
supports_reasoning = False
|
|
100
|
-
if isinstance(capabilities, dict):
|
|
101
|
-
supports_reasoning = bool(capabilities.get("reasoning"))
|
|
102
|
-
variants = model.get("variants")
|
|
103
|
-
reasoning_options: list[str] = []
|
|
104
|
-
if isinstance(variants, dict):
|
|
105
|
-
reasoning_options = [
|
|
106
|
-
key for key in variants.keys() if isinstance(key, str)
|
|
107
|
-
]
|
|
108
|
-
if reasoning_options:
|
|
109
|
-
supports_reasoning = True
|
|
110
|
-
full_id = f"{provider_id}/{model_id}"
|
|
111
|
-
models.append(
|
|
112
|
-
ModelSpec(
|
|
113
|
-
id=full_id,
|
|
114
|
-
display_name=display_name,
|
|
115
|
-
supports_reasoning=supports_reasoning,
|
|
116
|
-
reasoning_options=reasoning_options,
|
|
117
|
-
)
|
|
118
|
-
)
|
|
119
|
-
if not default_model and models:
|
|
120
|
-
default_model = models[0].id
|
|
121
|
-
return ModelCatalog(default_model=default_model, models=models)
|
|
122
|
-
|
|
123
|
-
|
|
124
65
|
def build_agents_routes() -> APIRouter:
|
|
125
66
|
router = APIRouter()
|
|
126
67
|
|
|
@@ -138,8 +79,8 @@ def build_agents_routes() -> APIRouter:
|
|
|
138
79
|
events = request.app.state.app_server_events
|
|
139
80
|
if supervisor is None:
|
|
140
81
|
raise HTTPException(status_code=404, detail="Codex harness unavailable")
|
|
141
|
-
|
|
142
|
-
catalog = await
|
|
82
|
+
codex_harness = CodexHarness(supervisor, events)
|
|
83
|
+
catalog = await codex_harness.model_catalog(engine.repo_root)
|
|
143
84
|
return _serialize_model_catalog(catalog)
|
|
144
85
|
if agent_id == "opencode":
|
|
145
86
|
supervisor = getattr(request.app.state, "opencode_supervisor", None)
|
|
@@ -148,9 +89,8 @@ def build_agents_routes() -> APIRouter:
|
|
|
148
89
|
status_code=404, detail="OpenCode harness unavailable"
|
|
149
90
|
)
|
|
150
91
|
try:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
catalog = _build_opencode_model_catalog(payload)
|
|
92
|
+
opencode_harness = OpenCodeHarness(supervisor)
|
|
93
|
+
catalog = await opencode_harness.model_catalog(engine.repo_root)
|
|
154
94
|
return _serialize_model_catalog(catalog)
|
|
155
95
|
except OpenCodeSupervisorError as exc:
|
|
156
96
|
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Analytics summary routes.
|
|
2
|
+
|
|
3
|
+
This module aggregates run/ticket/message metadata for the analytics dashboard
|
|
4
|
+
without relying on legacy autorunner endpoints. It intentionally reads from the
|
|
5
|
+
filesystem-backed ticket_flow store and ticket files to keep the UI consistent
|
|
6
|
+
with the rest of the app.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
from fastapi import APIRouter
|
|
16
|
+
|
|
17
|
+
from ..core.flows.models import FlowRunRecord, FlowRunStatus
|
|
18
|
+
from ..core.flows.store import FlowStore
|
|
19
|
+
from ..core.utils import find_repo_root
|
|
20
|
+
from ..tickets.files import list_ticket_paths, read_ticket, ticket_is_done
|
|
21
|
+
from ..tickets.outbox import resolve_outbox_paths
|
|
22
|
+
from ..tickets.replies import resolve_reply_paths
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _flows_db_path(repo_root: Path) -> Path:
|
|
26
|
+
return repo_root / ".codex-autorunner" / "flows.db"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_flow_store(repo_root: Path) -> Optional[FlowStore]:
|
|
30
|
+
db_path = _flows_db_path(repo_root)
|
|
31
|
+
if not db_path.exists():
|
|
32
|
+
return None
|
|
33
|
+
store = FlowStore(db_path)
|
|
34
|
+
try:
|
|
35
|
+
store.initialize()
|
|
36
|
+
except Exception:
|
|
37
|
+
return None
|
|
38
|
+
return store
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _select_primary_run(records: list[FlowRunRecord]) -> Optional[FlowRunRecord]:
|
|
42
|
+
"""Select the primary run for analytics display.
|
|
43
|
+
|
|
44
|
+
Only considers the newest run (records[0]). If it's active or paused, return it.
|
|
45
|
+
If the newest run is terminal (completed/stopped/failed), return None to show idle.
|
|
46
|
+
This matches the backend's _active_or_paused_run() logic and prevents showing
|
|
47
|
+
stale data from old paused runs when newer runs have completed.
|
|
48
|
+
"""
|
|
49
|
+
if not records:
|
|
50
|
+
return None
|
|
51
|
+
newest = records[0]
|
|
52
|
+
if (
|
|
53
|
+
FlowRunStatus(newest.status).is_active()
|
|
54
|
+
or FlowRunStatus(newest.status).is_paused()
|
|
55
|
+
):
|
|
56
|
+
return newest
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_timestamp(value: Optional[str]) -> Optional[datetime]:
|
|
61
|
+
if not value:
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
if value.endswith("Z"):
|
|
65
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
66
|
+
return datetime.fromisoformat(value)
|
|
67
|
+
except ValueError:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _duration_seconds(
|
|
72
|
+
started_at: Optional[str], finished_at: Optional[str], status: str
|
|
73
|
+
) -> Optional[float]:
|
|
74
|
+
start_dt = _parse_timestamp(started_at)
|
|
75
|
+
if not start_dt:
|
|
76
|
+
return None
|
|
77
|
+
end_dt = _parse_timestamp(finished_at)
|
|
78
|
+
if not end_dt and status in {
|
|
79
|
+
FlowRunStatus.RUNNING.value,
|
|
80
|
+
FlowRunStatus.PAUSED.value,
|
|
81
|
+
FlowRunStatus.PENDING.value,
|
|
82
|
+
}:
|
|
83
|
+
end_dt = datetime.now(timezone.utc)
|
|
84
|
+
if not end_dt:
|
|
85
|
+
return None
|
|
86
|
+
return (end_dt - start_dt).total_seconds()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _ticket_counts(ticket_dir: Path) -> dict[str, int]:
|
|
90
|
+
total = 0
|
|
91
|
+
done = 0
|
|
92
|
+
for path in list_ticket_paths(ticket_dir):
|
|
93
|
+
total += 1
|
|
94
|
+
try:
|
|
95
|
+
if ticket_is_done(path):
|
|
96
|
+
done += 1
|
|
97
|
+
except Exception:
|
|
98
|
+
# Treat unreadable/invalid tickets as not-done but still count them.
|
|
99
|
+
continue
|
|
100
|
+
todo = max(total - done, 0)
|
|
101
|
+
return {"todo": todo, "done": done, "total": total}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _count_history_dirs(history_dir: Path) -> int:
|
|
105
|
+
if not history_dir.exists() or not history_dir.is_dir():
|
|
106
|
+
return 0
|
|
107
|
+
count = 0
|
|
108
|
+
try:
|
|
109
|
+
for child in history_dir.iterdir():
|
|
110
|
+
try:
|
|
111
|
+
if child.is_dir() and len(child.name) == 4 and child.name.isdigit():
|
|
112
|
+
count += 1
|
|
113
|
+
except OSError:
|
|
114
|
+
continue
|
|
115
|
+
except OSError:
|
|
116
|
+
return count
|
|
117
|
+
return count
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _build_summary(repo_root: Path) -> Dict[str, Any]:
|
|
121
|
+
ticket_dir = repo_root / ".codex-autorunner" / "tickets"
|
|
122
|
+
store = _load_flow_store(repo_root)
|
|
123
|
+
records: list[FlowRunRecord] = []
|
|
124
|
+
if store:
|
|
125
|
+
try:
|
|
126
|
+
records = store.list_flow_runs(flow_type="ticket_flow")
|
|
127
|
+
except Exception:
|
|
128
|
+
records = []
|
|
129
|
+
finally:
|
|
130
|
+
try:
|
|
131
|
+
store.close()
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
run_record = _select_primary_run(records)
|
|
136
|
+
|
|
137
|
+
default_run = {
|
|
138
|
+
"id": None,
|
|
139
|
+
"short_id": None,
|
|
140
|
+
"status": "idle",
|
|
141
|
+
"started_at": None,
|
|
142
|
+
"finished_at": None,
|
|
143
|
+
"duration_seconds": None,
|
|
144
|
+
"current_step": None,
|
|
145
|
+
"created_at": None,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
run_data: Dict[str, Any] = default_run
|
|
149
|
+
turns: Dict[str, Optional[int]] = {
|
|
150
|
+
"total": None,
|
|
151
|
+
"current_ticket": None,
|
|
152
|
+
"dispatches": 0,
|
|
153
|
+
"replies": 0,
|
|
154
|
+
}
|
|
155
|
+
current_ticket: Optional[str] = None
|
|
156
|
+
agent_id: Optional[str] = None
|
|
157
|
+
|
|
158
|
+
if run_record:
|
|
159
|
+
run_data = {
|
|
160
|
+
"id": run_record.id,
|
|
161
|
+
"short_id": run_record.id.split("-")[0] if run_record.id else None,
|
|
162
|
+
"status": run_record.status.value,
|
|
163
|
+
"started_at": run_record.started_at,
|
|
164
|
+
"finished_at": run_record.finished_at,
|
|
165
|
+
"duration_seconds": _duration_seconds(
|
|
166
|
+
run_record.started_at, run_record.finished_at, run_record.status.value
|
|
167
|
+
),
|
|
168
|
+
"current_step": run_record.current_step,
|
|
169
|
+
"created_at": run_record.created_at,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
state = run_record.state if isinstance(run_record.state, dict) else {}
|
|
173
|
+
ticket_state = state.get("ticket_engine") if isinstance(state, dict) else {}
|
|
174
|
+
if isinstance(ticket_state, dict):
|
|
175
|
+
turns["total"] = ticket_state.get("total_turns") # type: ignore[index]
|
|
176
|
+
turns["current_ticket"] = ticket_state.get("ticket_turns") # type: ignore[index]
|
|
177
|
+
current_ticket = ticket_state.get("current_ticket") # type: ignore[assignment]
|
|
178
|
+
agent_id = ticket_state.get("last_agent_id") # type: ignore[assignment]
|
|
179
|
+
|
|
180
|
+
workspace_value = run_record.input_data.get("workspace_root")
|
|
181
|
+
workspace_root = Path(workspace_value) if workspace_value else repo_root
|
|
182
|
+
runs_dir = Path(
|
|
183
|
+
run_record.input_data.get("runs_dir") or ".codex-autorunner/runs"
|
|
184
|
+
)
|
|
185
|
+
outbox_paths = resolve_outbox_paths(
|
|
186
|
+
workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_record.id
|
|
187
|
+
)
|
|
188
|
+
reply_paths = resolve_reply_paths(
|
|
189
|
+
workspace_root=workspace_root, runs_dir=runs_dir, run_id=run_record.id
|
|
190
|
+
)
|
|
191
|
+
turns["dispatches"] = _count_history_dirs(outbox_paths.dispatch_history_dir)
|
|
192
|
+
turns["replies"] = _count_history_dirs(reply_paths.reply_history_dir)
|
|
193
|
+
|
|
194
|
+
# If current ticket is known, read its frontmatter to pick agent id when available.
|
|
195
|
+
if current_ticket:
|
|
196
|
+
current_path = (workspace_root / current_ticket).resolve()
|
|
197
|
+
try:
|
|
198
|
+
doc, _errors = read_ticket(current_path)
|
|
199
|
+
if doc and doc.frontmatter and getattr(doc.frontmatter, "agent", None):
|
|
200
|
+
agent_id = doc.frontmatter.agent
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
ticket_counts = _ticket_counts(ticket_dir)
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
"run": run_data,
|
|
208
|
+
"tickets": {
|
|
209
|
+
"todo_count": ticket_counts["todo"],
|
|
210
|
+
"done_count": ticket_counts["done"],
|
|
211
|
+
"total_count": ticket_counts["total"],
|
|
212
|
+
"current_ticket": current_ticket,
|
|
213
|
+
},
|
|
214
|
+
"turns": {
|
|
215
|
+
"total": turns.get("total"),
|
|
216
|
+
"current_ticket": turns.get("current_ticket"),
|
|
217
|
+
"dispatches": turns.get("dispatches"),
|
|
218
|
+
"replies": turns.get("replies"),
|
|
219
|
+
},
|
|
220
|
+
"agent": {
|
|
221
|
+
"id": agent_id,
|
|
222
|
+
"model": None,
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def build_analytics_routes() -> APIRouter:
|
|
228
|
+
router = APIRouter(prefix="/api/analytics", tags=["analytics"])
|
|
229
|
+
|
|
230
|
+
@router.get("/summary")
|
|
231
|
+
def get_analytics_summary():
|
|
232
|
+
repo_root = find_repo_root()
|
|
233
|
+
data = _build_summary(repo_root)
|
|
234
|
+
return data
|
|
235
|
+
|
|
236
|
+
return router
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
__all__ = ["build_analytics_routes"]
|