codeframe-ai 0.9.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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Engine registry for adapter lookup and resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from codeframe.core.adapters.agent_adapter import AgentAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# All valid engine names
|
|
12
|
+
VALID_ENGINES = frozenset({
|
|
13
|
+
"react",
|
|
14
|
+
"plan",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"codex",
|
|
17
|
+
"opencode",
|
|
18
|
+
"kilocode",
|
|
19
|
+
"cloud",
|
|
20
|
+
"built-in", # Alias for "react"
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
# External engines that use subprocess adapters (no LLM provider needed)
|
|
24
|
+
EXTERNAL_ENGINES = frozenset({
|
|
25
|
+
"claude-code",
|
|
26
|
+
"codex",
|
|
27
|
+
"opencode",
|
|
28
|
+
"kilocode",
|
|
29
|
+
"cloud",
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
# Builtin engines that need workspace + LLM provider
|
|
33
|
+
BUILTIN_ENGINES = frozenset({
|
|
34
|
+
"react",
|
|
35
|
+
"plan",
|
|
36
|
+
"built-in",
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_engine(cli_engine: str | None = None) -> str:
|
|
41
|
+
"""Resolve which engine to use.
|
|
42
|
+
|
|
43
|
+
Priority:
|
|
44
|
+
1. CLI --engine flag (explicit wins)
|
|
45
|
+
2. CODEFRAME_ENGINE environment variable
|
|
46
|
+
3. Default: "react"
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
cli_engine: Engine name from CLI flag, or None.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Resolved engine name.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If resolved engine is not valid.
|
|
56
|
+
"""
|
|
57
|
+
engine = cli_engine or os.environ.get("CODEFRAME_ENGINE") or "react"
|
|
58
|
+
|
|
59
|
+
# Normalize alias
|
|
60
|
+
if engine == "built-in":
|
|
61
|
+
engine = "react"
|
|
62
|
+
|
|
63
|
+
if engine not in VALID_ENGINES:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"Invalid engine '{engine}'. "
|
|
66
|
+
f"Must be one of: {', '.join(sorted(VALID_ENGINES))}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return engine
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_external_engine(engine: str) -> bool:
|
|
73
|
+
"""Check if an engine uses an external subprocess adapter."""
|
|
74
|
+
return engine in EXTERNAL_ENGINES
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_external_adapter(engine: str, **kwargs: Any) -> AgentAdapter:
|
|
78
|
+
"""Get an adapter instance for an external engine.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
engine: Engine name (must be in EXTERNAL_ENGINES).
|
|
82
|
+
**kwargs: Adapter-specific keyword arguments.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
AgentAdapter instance.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ValueError: If engine is not an external engine.
|
|
89
|
+
EnvironmentError: If the required binary is not installed.
|
|
90
|
+
"""
|
|
91
|
+
if engine == "claude-code":
|
|
92
|
+
from codeframe.core.adapters.claude_code import ClaudeCodeAdapter
|
|
93
|
+
|
|
94
|
+
return ClaudeCodeAdapter(**kwargs)
|
|
95
|
+
elif engine == "codex":
|
|
96
|
+
from codeframe.core.adapters.codex import CodexAdapter
|
|
97
|
+
|
|
98
|
+
return CodexAdapter(**kwargs)
|
|
99
|
+
elif engine == "opencode":
|
|
100
|
+
from codeframe.core.adapters.opencode import OpenCodeAdapter
|
|
101
|
+
|
|
102
|
+
return OpenCodeAdapter()
|
|
103
|
+
elif engine == "kilocode":
|
|
104
|
+
from codeframe.core.adapters.kilocode import KilocodeAdapter
|
|
105
|
+
|
|
106
|
+
return KilocodeAdapter(**kwargs)
|
|
107
|
+
elif engine == "cloud":
|
|
108
|
+
from codeframe.adapters.e2b.adapter import E2BAgentAdapter
|
|
109
|
+
|
|
110
|
+
timeout_minutes = kwargs.get("timeout_minutes", 30)
|
|
111
|
+
return E2BAgentAdapter(timeout_minutes=timeout_minutes)
|
|
112
|
+
else:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Unknown external engine '{engine}'. "
|
|
115
|
+
f"Valid external engines: {', '.join(sorted(EXTERNAL_ENGINES))}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_builtin_adapter(
|
|
120
|
+
engine: str,
|
|
121
|
+
workspace: Any,
|
|
122
|
+
llm_provider: Any,
|
|
123
|
+
**kwargs: Any,
|
|
124
|
+
) -> AgentAdapter:
|
|
125
|
+
"""Get an adapter instance for a builtin engine.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
engine: Engine name (must be in BUILTIN_ENGINES).
|
|
129
|
+
workspace: Workspace instance.
|
|
130
|
+
llm_provider: LLM provider instance.
|
|
131
|
+
**kwargs: Additional keyword arguments passed to the adapter.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
AgentAdapter instance.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ValueError: If engine is not a builtin engine.
|
|
138
|
+
"""
|
|
139
|
+
resolved = "react" if engine == "built-in" else engine
|
|
140
|
+
|
|
141
|
+
if resolved == "react":
|
|
142
|
+
from codeframe.core.adapters.builtin import BuiltinReactAdapter
|
|
143
|
+
|
|
144
|
+
return BuiltinReactAdapter(workspace, llm_provider, **kwargs)
|
|
145
|
+
elif resolved == "plan":
|
|
146
|
+
from codeframe.core.adapters.builtin import BuiltinPlanAdapter
|
|
147
|
+
|
|
148
|
+
return BuiltinPlanAdapter(workspace, llm_provider, **kwargs)
|
|
149
|
+
else:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"Unknown builtin engine '{engine}'. "
|
|
152
|
+
f"Valid builtin engines: {', '.join(sorted(BUILTIN_ENGINES))}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def check_requirements(engine: str) -> dict[str, bool]:
|
|
157
|
+
"""Check if an engine's requirements are met.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
engine: Engine name to check.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dict of requirement-name → bool (True if satisfied).
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
ValueError: If engine is not valid.
|
|
167
|
+
"""
|
|
168
|
+
if engine not in VALID_ENGINES:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"Invalid engine '{engine}'. "
|
|
171
|
+
f"Must be one of: {', '.join(sorted(VALID_ENGINES))}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Resolve alias
|
|
175
|
+
resolved = "react" if engine == "built-in" else engine
|
|
176
|
+
|
|
177
|
+
# Get the adapter class and check for a requirements() classmethod
|
|
178
|
+
adapter_cls = _get_adapter_class(resolved)
|
|
179
|
+
if adapter_cls is None:
|
|
180
|
+
return {}
|
|
181
|
+
|
|
182
|
+
req_method = getattr(adapter_cls, "requirements", None)
|
|
183
|
+
if req_method is None or not callable(req_method):
|
|
184
|
+
return {}
|
|
185
|
+
|
|
186
|
+
reqs = req_method()
|
|
187
|
+
result: dict[str, bool] = {}
|
|
188
|
+
for key in reqs:
|
|
189
|
+
# Check environment variables for builtin engines
|
|
190
|
+
result[key] = bool(os.getenv(key))
|
|
191
|
+
|
|
192
|
+
# External adapters may also have a check_ready() classmethod
|
|
193
|
+
check_ready = getattr(adapter_cls, "check_ready", None)
|
|
194
|
+
if check_ready and callable(check_ready):
|
|
195
|
+
result.update(check_ready())
|
|
196
|
+
|
|
197
|
+
return result
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _get_adapter_class(engine: str) -> type | None:
|
|
201
|
+
"""Get the adapter class for an engine (without instantiating)."""
|
|
202
|
+
if engine == "react":
|
|
203
|
+
from codeframe.core.adapters.builtin import BuiltinReactAdapter
|
|
204
|
+
return BuiltinReactAdapter
|
|
205
|
+
elif engine == "plan":
|
|
206
|
+
from codeframe.core.adapters.builtin import BuiltinPlanAdapter
|
|
207
|
+
return BuiltinPlanAdapter
|
|
208
|
+
elif engine == "claude-code":
|
|
209
|
+
from codeframe.core.adapters.claude_code import ClaudeCodeAdapter
|
|
210
|
+
return ClaudeCodeAdapter
|
|
211
|
+
elif engine == "codex":
|
|
212
|
+
from codeframe.core.adapters.codex import CodexAdapter
|
|
213
|
+
return CodexAdapter
|
|
214
|
+
elif engine == "opencode":
|
|
215
|
+
from codeframe.core.adapters.opencode import OpenCodeAdapter
|
|
216
|
+
return OpenCodeAdapter
|
|
217
|
+
elif engine == "kilocode":
|
|
218
|
+
from codeframe.core.adapters.kilocode import KilocodeAdapter
|
|
219
|
+
return KilocodeAdapter
|
|
220
|
+
elif engine == "cloud":
|
|
221
|
+
from codeframe.adapters.e2b.adapter import E2BAgentAdapter
|
|
222
|
+
return E2BAgentAdapter
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_adapter(
|
|
227
|
+
engine: str,
|
|
228
|
+
workspace: Any = None,
|
|
229
|
+
llm_provider: Any = None,
|
|
230
|
+
**kwargs: Any,
|
|
231
|
+
) -> AgentAdapter:
|
|
232
|
+
"""Get an adapter for any engine type.
|
|
233
|
+
|
|
234
|
+
Unified factory that handles both external and builtin engines.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
engine: Engine name.
|
|
238
|
+
workspace: Workspace (required for builtin engines).
|
|
239
|
+
llm_provider: LLM provider (required for builtin engines).
|
|
240
|
+
**kwargs: Additional adapter-specific arguments.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
AgentAdapter instance.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
ValueError: If engine is invalid or builtin engine missing required args.
|
|
247
|
+
EnvironmentError: If external engine binary is not installed.
|
|
248
|
+
"""
|
|
249
|
+
if is_external_engine(engine):
|
|
250
|
+
return get_external_adapter(engine, **kwargs)
|
|
251
|
+
else:
|
|
252
|
+
if workspace is None or llm_provider is None:
|
|
253
|
+
raise ValueError(
|
|
254
|
+
f"Builtin engine '{engine}' requires workspace and llm_provider"
|
|
255
|
+
)
|
|
256
|
+
return get_builtin_adapter(engine, workspace, llm_provider, **kwargs)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Engine performance tracking for CodeFRAME.
|
|
2
|
+
|
|
3
|
+
Records per-run engine metrics and computes aggregate statistics
|
|
4
|
+
for comparing engine performance (react vs plan vs external adapters).
|
|
5
|
+
|
|
6
|
+
This module is headless - no FastAPI or HTTP dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from codeframe.core.workspace import Workspace, get_db_connection, _utc_now
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def record_run(
|
|
15
|
+
workspace: Workspace,
|
|
16
|
+
run_id: str,
|
|
17
|
+
engine: str,
|
|
18
|
+
task_id: str,
|
|
19
|
+
status: str,
|
|
20
|
+
duration_ms: Optional[int] = None,
|
|
21
|
+
tokens_used: int = 0,
|
|
22
|
+
gates_passed: Optional[int] = None,
|
|
23
|
+
self_corrections: int = 0,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Record an engine run in the run_engine_log table.
|
|
26
|
+
|
|
27
|
+
After inserting, recomputes aggregate stats for the engine.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
workspace: Active workspace.
|
|
31
|
+
run_id: Unique run identifier.
|
|
32
|
+
engine: Engine name (e.g. "react", "plan").
|
|
33
|
+
task_id: Task that was executed.
|
|
34
|
+
status: Final run status (COMPLETED, FAILED, BLOCKED).
|
|
35
|
+
duration_ms: Execution duration in milliseconds.
|
|
36
|
+
tokens_used: Total LLM tokens consumed.
|
|
37
|
+
gates_passed: 1 if all gates passed, 0 if not, None if no gate data.
|
|
38
|
+
self_corrections: Number of self-correction attempts.
|
|
39
|
+
"""
|
|
40
|
+
now = _utc_now().isoformat()
|
|
41
|
+
|
|
42
|
+
conn = get_db_connection(workspace)
|
|
43
|
+
try:
|
|
44
|
+
conn.execute(
|
|
45
|
+
"INSERT INTO run_engine_log "
|
|
46
|
+
"(run_id, engine, task_id, workspace_id, status, duration_ms, "
|
|
47
|
+
"tokens_used, gates_passed, self_corrections, created_at) "
|
|
48
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
49
|
+
(
|
|
50
|
+
run_id,
|
|
51
|
+
engine,
|
|
52
|
+
task_id,
|
|
53
|
+
workspace.id,
|
|
54
|
+
status,
|
|
55
|
+
duration_ms,
|
|
56
|
+
tokens_used,
|
|
57
|
+
gates_passed,
|
|
58
|
+
self_corrections,
|
|
59
|
+
now,
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
# Recompute aggregates in the same connection to avoid TOCTOU issues
|
|
63
|
+
_update_aggregate_stats_conn(conn, workspace.id, engine)
|
|
64
|
+
conn.commit()
|
|
65
|
+
finally:
|
|
66
|
+
conn.close()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _update_aggregate_stats(workspace: Workspace, engine: str) -> None:
|
|
70
|
+
"""Recompute aggregate metrics for an engine (opens its own connection).
|
|
71
|
+
|
|
72
|
+
Convenience wrapper for external callers (e.g., data seeding).
|
|
73
|
+
"""
|
|
74
|
+
conn = get_db_connection(workspace)
|
|
75
|
+
try:
|
|
76
|
+
_update_aggregate_stats_conn(conn, workspace.id, engine)
|
|
77
|
+
conn.commit()
|
|
78
|
+
finally:
|
|
79
|
+
conn.close()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _update_aggregate_stats_conn(conn, ws_id: str, engine: str) -> None:
|
|
83
|
+
"""Recompute aggregate metrics using an existing connection.
|
|
84
|
+
|
|
85
|
+
Does NOT commit — caller is responsible for committing.
|
|
86
|
+
"""
|
|
87
|
+
now = _utc_now().isoformat()
|
|
88
|
+
|
|
89
|
+
cur = conn.cursor()
|
|
90
|
+
|
|
91
|
+
# Compute all metrics in one pass where possible
|
|
92
|
+
row = cur.execute(
|
|
93
|
+
"SELECT "
|
|
94
|
+
" COUNT(*), "
|
|
95
|
+
" COUNT(CASE WHEN status = 'COMPLETED' THEN 1 END), "
|
|
96
|
+
" COUNT(CASE WHEN status = 'FAILED' THEN 1 END), "
|
|
97
|
+
" COUNT(CASE WHEN gates_passed = 1 THEN 1 END), "
|
|
98
|
+
" COUNT(CASE WHEN gates_passed IS NOT NULL THEN 1 END), "
|
|
99
|
+
" COUNT(CASE WHEN self_corrections > 0 THEN 1 END), "
|
|
100
|
+
" AVG(CASE WHEN duration_ms IS NOT NULL THEN duration_ms END), "
|
|
101
|
+
" SUM(tokens_used), "
|
|
102
|
+
" SUM(CASE WHEN status = 'COMPLETED' THEN tokens_used ELSE 0 END), "
|
|
103
|
+
" COUNT(CASE WHEN status = 'COMPLETED' THEN 1 END) "
|
|
104
|
+
"FROM run_engine_log "
|
|
105
|
+
"WHERE engine = ? AND workspace_id = ?",
|
|
106
|
+
(engine, ws_id),
|
|
107
|
+
).fetchone()
|
|
108
|
+
|
|
109
|
+
total = row[0]
|
|
110
|
+
completed = row[1]
|
|
111
|
+
failed = row[2]
|
|
112
|
+
gates_pass_count = row[3]
|
|
113
|
+
gates_total = row[4]
|
|
114
|
+
self_corr_count = row[5]
|
|
115
|
+
avg_duration = row[6]
|
|
116
|
+
total_tokens = row[7] or 0
|
|
117
|
+
completed_tokens = row[8] or 0
|
|
118
|
+
completed_count = row[9]
|
|
119
|
+
|
|
120
|
+
gate_pass_rate = (
|
|
121
|
+
100.0 * gates_pass_count / gates_total if gates_total > 0 else 0.0
|
|
122
|
+
)
|
|
123
|
+
self_correction_rate = (
|
|
124
|
+
100.0 * self_corr_count / total if total > 0 else 0.0
|
|
125
|
+
)
|
|
126
|
+
avg_tokens_per_task = (
|
|
127
|
+
completed_tokens / completed_count if completed_count > 0 else 0.0
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
metrics = {
|
|
131
|
+
"tasks_attempted": float(total),
|
|
132
|
+
"tasks_completed": float(completed),
|
|
133
|
+
"tasks_failed": float(failed),
|
|
134
|
+
"gate_pass_rate": round(gate_pass_rate, 2),
|
|
135
|
+
"self_correction_rate": round(self_correction_rate, 2),
|
|
136
|
+
"avg_duration_ms": round(avg_duration, 2) if avg_duration is not None else 0.0,
|
|
137
|
+
"total_tokens": float(total_tokens),
|
|
138
|
+
"avg_tokens_per_task": round(avg_tokens_per_task, 2),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for metric, value in metrics.items():
|
|
142
|
+
cur.execute(
|
|
143
|
+
"INSERT OR REPLACE INTO engine_stats "
|
|
144
|
+
"(workspace_id, engine, metric, value, updated_at) "
|
|
145
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
146
|
+
(ws_id, engine, metric, value, now),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_engine_stats(
|
|
151
|
+
workspace: Workspace, engine: Optional[str] = None
|
|
152
|
+
) -> dict[str, dict[str, float]]:
|
|
153
|
+
"""Get aggregate engine statistics.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
workspace: Active workspace.
|
|
157
|
+
engine: Optional engine filter. If None, returns all engines.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Dict keyed by engine name, each value is a dict of metric -> value.
|
|
161
|
+
Empty dict if no stats exist.
|
|
162
|
+
"""
|
|
163
|
+
conn = get_db_connection(workspace)
|
|
164
|
+
try:
|
|
165
|
+
if engine is not None:
|
|
166
|
+
rows = conn.execute(
|
|
167
|
+
"SELECT engine, metric, value FROM engine_stats "
|
|
168
|
+
"WHERE workspace_id = ? AND engine = ?",
|
|
169
|
+
(workspace.id, engine),
|
|
170
|
+
).fetchall()
|
|
171
|
+
else:
|
|
172
|
+
rows = conn.execute(
|
|
173
|
+
"SELECT engine, metric, value FROM engine_stats "
|
|
174
|
+
"WHERE workspace_id = ?",
|
|
175
|
+
(workspace.id,),
|
|
176
|
+
).fetchall()
|
|
177
|
+
finally:
|
|
178
|
+
conn.close()
|
|
179
|
+
|
|
180
|
+
result: dict[str, dict[str, float]] = {}
|
|
181
|
+
for eng, metric, value in rows:
|
|
182
|
+
if eng not in result:
|
|
183
|
+
result[eng] = {}
|
|
184
|
+
result[eng][metric] = value
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def get_run_log(
|
|
190
|
+
workspace: Workspace, engine: Optional[str] = None, limit: int = 100
|
|
191
|
+
) -> list[dict]:
|
|
192
|
+
"""Get raw per-run records from the run_engine_log table.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
workspace: Active workspace.
|
|
196
|
+
engine: Optional engine filter.
|
|
197
|
+
limit: Maximum records to return (default 100).
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of dicts, each representing a run record.
|
|
201
|
+
Ordered by created_at DESC.
|
|
202
|
+
"""
|
|
203
|
+
conn = get_db_connection(workspace)
|
|
204
|
+
try:
|
|
205
|
+
if engine is not None:
|
|
206
|
+
rows = conn.execute(
|
|
207
|
+
"SELECT run_id, engine, task_id, workspace_id, status, "
|
|
208
|
+
"duration_ms, tokens_used, gates_passed, self_corrections, "
|
|
209
|
+
"created_at FROM run_engine_log "
|
|
210
|
+
"WHERE workspace_id = ? AND engine = ? "
|
|
211
|
+
"ORDER BY created_at DESC LIMIT ?",
|
|
212
|
+
(workspace.id, engine, limit),
|
|
213
|
+
).fetchall()
|
|
214
|
+
else:
|
|
215
|
+
rows = conn.execute(
|
|
216
|
+
"SELECT run_id, engine, task_id, workspace_id, status, "
|
|
217
|
+
"duration_ms, tokens_used, gates_passed, self_corrections, "
|
|
218
|
+
"created_at FROM run_engine_log "
|
|
219
|
+
"WHERE workspace_id = ? "
|
|
220
|
+
"ORDER BY created_at DESC LIMIT ?",
|
|
221
|
+
(workspace.id, limit),
|
|
222
|
+
).fetchall()
|
|
223
|
+
finally:
|
|
224
|
+
conn.close()
|
|
225
|
+
|
|
226
|
+
columns = [
|
|
227
|
+
"run_id", "engine", "task_id", "workspace_id", "status",
|
|
228
|
+
"duration_ms", "tokens_used", "gates_passed", "self_corrections",
|
|
229
|
+
"created_at",
|
|
230
|
+
]
|
|
231
|
+
return [dict(zip(columns, row)) for row in rows]
|