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,295 @@
|
|
|
1
|
+
"""Base repository class for database operations."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
import threading
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
import aiosqlite
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseRepository:
|
|
15
|
+
"""Base class for all repositories.
|
|
16
|
+
|
|
17
|
+
Provides common database utilities and connection management.
|
|
18
|
+
Each repository handles a specific domain (projects, issues, tasks, etc.).
|
|
19
|
+
|
|
20
|
+
Supports both synchronous (sqlite3) and asynchronous (aiosqlite) operations.
|
|
21
|
+
Thread-safe synchronous operations are provided via a shared lock.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
sync_conn: Optional[sqlite3.Connection] = None,
|
|
27
|
+
async_conn: Optional[aiosqlite.Connection] = None,
|
|
28
|
+
database: Optional[Any] = None,
|
|
29
|
+
sync_lock: Optional[threading.Lock] = None
|
|
30
|
+
):
|
|
31
|
+
"""Initialize repository with database connections.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
sync_conn: Synchronous sqlite3.Connection
|
|
35
|
+
async_conn: Asynchronous aiosqlite.Connection
|
|
36
|
+
database: Reference to parent Database instance (for cross-repository operations)
|
|
37
|
+
sync_lock: Threading lock for thread-safe synchronous operations
|
|
38
|
+
|
|
39
|
+
Note:
|
|
40
|
+
At least one connection must be provided. Both can be provided
|
|
41
|
+
to support repositories with both sync and async methods.
|
|
42
|
+
If sync_conn is provided without sync_lock, operations will not be thread-safe.
|
|
43
|
+
"""
|
|
44
|
+
if sync_conn is None and async_conn is None:
|
|
45
|
+
raise ValueError("At least one connection (sync or async) must be provided")
|
|
46
|
+
|
|
47
|
+
self.conn = sync_conn # For backward compatibility
|
|
48
|
+
self._async_conn = async_conn
|
|
49
|
+
self._database = database # Reference to parent Database instance
|
|
50
|
+
self._sync_lock = sync_lock # Threading lock for thread-safe sync operations
|
|
51
|
+
|
|
52
|
+
def _execute(self, query: str, params: tuple = ()) -> sqlite3.Cursor:
|
|
53
|
+
"""Execute a query synchronously with thread-safe locking.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
query: SQL query string
|
|
57
|
+
params: Query parameters
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Cursor with query results
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
RuntimeError: If sync connection is not available
|
|
64
|
+
|
|
65
|
+
Note:
|
|
66
|
+
Uses threading lock if available to ensure thread-safe access
|
|
67
|
+
to the shared connection object.
|
|
68
|
+
"""
|
|
69
|
+
if self.conn is None:
|
|
70
|
+
raise RuntimeError("Sync connection not available, use async methods")
|
|
71
|
+
|
|
72
|
+
if self._sync_lock is not None:
|
|
73
|
+
with self._sync_lock:
|
|
74
|
+
return self.conn.execute(query, params)
|
|
75
|
+
else:
|
|
76
|
+
return self.conn.execute(query, params)
|
|
77
|
+
|
|
78
|
+
def _fetchone(self, query: str, params: tuple = ()) -> Optional[sqlite3.Row]:
|
|
79
|
+
"""Fetch a single row synchronously.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
query: SQL query string
|
|
83
|
+
params: Query parameters
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Row or None if no results
|
|
87
|
+
"""
|
|
88
|
+
cursor = self._execute(query, params)
|
|
89
|
+
return cursor.fetchone()
|
|
90
|
+
|
|
91
|
+
def _fetchall(self, query: str, params: tuple = ()) -> List[sqlite3.Row]:
|
|
92
|
+
"""Fetch all rows synchronously.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
query: SQL query string
|
|
96
|
+
params: Query parameters
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
List of rows
|
|
100
|
+
"""
|
|
101
|
+
cursor = self._execute(query, params)
|
|
102
|
+
return cursor.fetchall()
|
|
103
|
+
|
|
104
|
+
def _commit(self) -> None:
|
|
105
|
+
"""Commit the current transaction synchronously with thread-safe locking.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
RuntimeError: If sync connection is not available
|
|
109
|
+
|
|
110
|
+
Note:
|
|
111
|
+
Uses threading lock if available to ensure thread-safe access
|
|
112
|
+
to the shared connection object.
|
|
113
|
+
"""
|
|
114
|
+
if self.conn is None:
|
|
115
|
+
raise RuntimeError("Sync connection not available, use async methods")
|
|
116
|
+
|
|
117
|
+
if self._sync_lock is not None:
|
|
118
|
+
with self._sync_lock:
|
|
119
|
+
self.conn.commit()
|
|
120
|
+
else:
|
|
121
|
+
self.conn.commit()
|
|
122
|
+
|
|
123
|
+
async def _execute_async(self, query: str, params: tuple = ()) -> aiosqlite.Cursor:
|
|
124
|
+
"""Execute a query asynchronously.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
query: SQL query string
|
|
128
|
+
params: Query parameters
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Cursor with query results
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
RuntimeError: If async connection is not available
|
|
135
|
+
"""
|
|
136
|
+
if self._async_conn is None:
|
|
137
|
+
raise RuntimeError("Async connection not available, use sync methods")
|
|
138
|
+
return await self._async_conn.execute(query, params)
|
|
139
|
+
|
|
140
|
+
async def _fetchone_async(self, query: str, params: tuple = ()) -> Optional[aiosqlite.Row]:
|
|
141
|
+
"""Fetch a single row asynchronously.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
query: SQL query string
|
|
145
|
+
params: Query parameters
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Row or None if no results
|
|
149
|
+
"""
|
|
150
|
+
cursor = await self._execute_async(query, params)
|
|
151
|
+
return await cursor.fetchone()
|
|
152
|
+
|
|
153
|
+
async def _fetchall_async(self, query: str, params: tuple = ()) -> List[aiosqlite.Row]:
|
|
154
|
+
"""Fetch all rows asynchronously.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
query: SQL query string
|
|
158
|
+
params: Query parameters
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
List of rows
|
|
162
|
+
"""
|
|
163
|
+
cursor = await self._execute_async(query, params)
|
|
164
|
+
return await cursor.fetchall()
|
|
165
|
+
|
|
166
|
+
async def _commit_async(self) -> None:
|
|
167
|
+
"""Commit the current transaction asynchronously.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
RuntimeError: If async connection is not available
|
|
171
|
+
"""
|
|
172
|
+
if self._async_conn is None:
|
|
173
|
+
raise RuntimeError("Async connection not available, use sync methods")
|
|
174
|
+
await self._async_conn.commit()
|
|
175
|
+
|
|
176
|
+
def _row_to_dict(self, row: Union[sqlite3.Row, aiosqlite.Row]) -> Dict[str, Any]:
|
|
177
|
+
"""Convert a database row to a dictionary.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
row: SQLite Row object (sync or async)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Dictionary with column names as keys
|
|
184
|
+
|
|
185
|
+
Note:
|
|
186
|
+
Both sqlite3.Row and aiosqlite.Row support dictionary-style access
|
|
187
|
+
and keys() method for column names.
|
|
188
|
+
"""
|
|
189
|
+
if row is None:
|
|
190
|
+
return {}
|
|
191
|
+
return {key: row[key] for key in row.keys()}
|
|
192
|
+
|
|
193
|
+
def _parse_datetime(
|
|
194
|
+
self,
|
|
195
|
+
dt_str: Optional[str],
|
|
196
|
+
field_name: str = "",
|
|
197
|
+
row_id: Optional[int] = None
|
|
198
|
+
) -> Optional[datetime]:
|
|
199
|
+
"""Parse datetime string to datetime object.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
dt_str: ISO format datetime string or None
|
|
203
|
+
field_name: Field name for logging (optional)
|
|
204
|
+
row_id: Row ID for logging (optional)
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
datetime object or None if input is None
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
ValueError: If datetime string is malformed
|
|
211
|
+
"""
|
|
212
|
+
if dt_str is None:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# Parse ISO format: "2024-11-23T10:30:00" or "2024-11-23 10:30:00"
|
|
217
|
+
# Handle both 'T' and space separators
|
|
218
|
+
dt_str_normalized = dt_str.replace("T", " ")
|
|
219
|
+
|
|
220
|
+
# Try with microseconds first
|
|
221
|
+
try:
|
|
222
|
+
return datetime.fromisoformat(dt_str_normalized)
|
|
223
|
+
except ValueError:
|
|
224
|
+
# Try without microseconds
|
|
225
|
+
return datetime.strptime(dt_str_normalized, "%Y-%m-%d %H:%M:%S")
|
|
226
|
+
except (ValueError, AttributeError) as e:
|
|
227
|
+
context = f" for {field_name}" if field_name else ""
|
|
228
|
+
row_context = f" (row {row_id})" if row_id else ""
|
|
229
|
+
logger.warning(
|
|
230
|
+
f"Failed to parse datetime '{dt_str}'{context}{row_context}: {e}"
|
|
231
|
+
)
|
|
232
|
+
raise ValueError(f"Invalid datetime format: {dt_str}") from e
|
|
233
|
+
|
|
234
|
+
def _format_datetime(self, dt: Optional[datetime]) -> Optional[str]:
|
|
235
|
+
"""Format datetime object to ISO string for database storage.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
dt: datetime object or None
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
ISO format datetime string or None
|
|
242
|
+
"""
|
|
243
|
+
if dt is None:
|
|
244
|
+
return None
|
|
245
|
+
return dt.isoformat()
|
|
246
|
+
|
|
247
|
+
async def _get_async_conn(self) -> aiosqlite.Connection:
|
|
248
|
+
"""Get async connection with health check and automatic reconnection.
|
|
249
|
+
|
|
250
|
+
This method delegates to the parent Database instance which:
|
|
251
|
+
1. Creates connection if none exists (lazy initialization)
|
|
252
|
+
2. Checks connection health via simple query
|
|
253
|
+
3. Reconnects automatically if connection is dead
|
|
254
|
+
4. Uses a lock to prevent race conditions
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Active aiosqlite connection
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
RuntimeError: If database reference is not available
|
|
261
|
+
aiosqlite.Error: If connection cannot be established
|
|
262
|
+
|
|
263
|
+
Note:
|
|
264
|
+
Uses async connection with automatic health check and reconnection.
|
|
265
|
+
Call close_async() when done to release database resources.
|
|
266
|
+
"""
|
|
267
|
+
if self._database is None:
|
|
268
|
+
raise RuntimeError("Database reference not available for async connection")
|
|
269
|
+
return await self._database._get_async_conn()
|
|
270
|
+
|
|
271
|
+
def _ensure_rfc3339(self, timestamp_str: Optional[str]) -> Optional[str]:
|
|
272
|
+
"""Ensure timestamp is in RFC 3339 format with timezone.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
timestamp_str: Timestamp string (may be SQLite format or RFC 3339)
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
RFC 3339 formatted timestamp (with 'Z' suffix for UTC) or None
|
|
279
|
+
|
|
280
|
+
Note:
|
|
281
|
+
Converts SQLite timestamps like "2025-10-17 22:01:56" to "2025-10-17T22:01:56Z".
|
|
282
|
+
Timestamps already in RFC 3339 format are returned as-is.
|
|
283
|
+
"""
|
|
284
|
+
if not timestamp_str:
|
|
285
|
+
return timestamp_str
|
|
286
|
+
# If already has 'Z' or timezone, return as-is
|
|
287
|
+
if "Z" in timestamp_str or "+" in timestamp_str:
|
|
288
|
+
return timestamp_str
|
|
289
|
+
# Parse and add Z suffix for UTC
|
|
290
|
+
try:
|
|
291
|
+
# SQLite format: "2025-10-17 22:01:56"
|
|
292
|
+
dt = datetime.fromisoformat(timestamp_str)
|
|
293
|
+
return dt.isoformat() + "Z"
|
|
294
|
+
except ValueError:
|
|
295
|
+
return timestamp_str
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Repository for interactive agent session operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import uuid
|
|
7
|
+
from datetime import datetime, UTC
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from codeframe.platform_store.repositories.base import BaseRepository
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InteractiveSessionRepository(BaseRepository):
|
|
14
|
+
"""Repository for interactive_sessions and session_messages tables."""
|
|
15
|
+
|
|
16
|
+
# -------------------------------------------------------------------------
|
|
17
|
+
# Sessions
|
|
18
|
+
# -------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
def create(
|
|
21
|
+
self,
|
|
22
|
+
workspace_path: str,
|
|
23
|
+
task_id: Optional[str] = None,
|
|
24
|
+
agent_type: str = "claude",
|
|
25
|
+
model: Optional[str] = None,
|
|
26
|
+
) -> dict:
|
|
27
|
+
now = datetime.now(UTC).isoformat()
|
|
28
|
+
session_id = str(uuid.uuid4())
|
|
29
|
+
self._execute(
|
|
30
|
+
"""
|
|
31
|
+
INSERT INTO interactive_sessions
|
|
32
|
+
(id, workspace_path, task_id, state, agent_type, model,
|
|
33
|
+
cost_usd, input_tokens, output_tokens, created_at, updated_at, ended_at)
|
|
34
|
+
VALUES (?, ?, ?, 'active', ?, ?, 0.0, 0, 0, ?, ?, NULL)
|
|
35
|
+
""",
|
|
36
|
+
(session_id, workspace_path, task_id, agent_type, model, now, now),
|
|
37
|
+
)
|
|
38
|
+
self._commit()
|
|
39
|
+
return self.get(session_id)
|
|
40
|
+
|
|
41
|
+
def get(self, session_id: str) -> Optional[dict]:
|
|
42
|
+
row = self._fetchone(
|
|
43
|
+
"SELECT * FROM interactive_sessions WHERE id = ?", (session_id,)
|
|
44
|
+
)
|
|
45
|
+
return self._row_to_dict(row) if row else None
|
|
46
|
+
|
|
47
|
+
def list(
|
|
48
|
+
self,
|
|
49
|
+
workspace_path: Optional[str] = None,
|
|
50
|
+
state: Optional[str] = None,
|
|
51
|
+
limit: int = 50,
|
|
52
|
+
) -> list[dict]:
|
|
53
|
+
query = "SELECT * FROM interactive_sessions WHERE 1=1"
|
|
54
|
+
params: list = []
|
|
55
|
+
if workspace_path is not None:
|
|
56
|
+
query += " AND workspace_path = ?"
|
|
57
|
+
params.append(workspace_path)
|
|
58
|
+
if state is not None:
|
|
59
|
+
query += " AND state = ?"
|
|
60
|
+
params.append(state)
|
|
61
|
+
query += " ORDER BY created_at DESC LIMIT ?"
|
|
62
|
+
params.append(limit)
|
|
63
|
+
rows = self._fetchall(query, tuple(params))
|
|
64
|
+
return [self._row_to_dict(r) for r in rows]
|
|
65
|
+
|
|
66
|
+
def update_state(self, session_id: str, state: str) -> None:
|
|
67
|
+
"""Update session state. Called internally by the agent runtime, not via REST API.
|
|
68
|
+
|
|
69
|
+
Callers are responsible for validating state against VALID_STATES before calling.
|
|
70
|
+
"""
|
|
71
|
+
now = datetime.now(UTC).isoformat()
|
|
72
|
+
self._execute(
|
|
73
|
+
"UPDATE interactive_sessions SET state = ?, updated_at = ? WHERE id = ?",
|
|
74
|
+
(state, now, session_id),
|
|
75
|
+
)
|
|
76
|
+
self._commit()
|
|
77
|
+
|
|
78
|
+
def update_cost(
|
|
79
|
+
self, session_id: str, cost_usd: float, input_tokens: int, output_tokens: int
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Accumulate cost and token counts. Called internally by the agent runtime, not via REST API.
|
|
82
|
+
|
|
83
|
+
The increment is applied atomically at the DB level to prevent lost-update races.
|
|
84
|
+
"""
|
|
85
|
+
now = datetime.now(UTC).isoformat()
|
|
86
|
+
self._execute(
|
|
87
|
+
"""
|
|
88
|
+
UPDATE interactive_sessions
|
|
89
|
+
SET cost_usd = cost_usd + ?, input_tokens = input_tokens + ?,
|
|
90
|
+
output_tokens = output_tokens + ?, updated_at = ?
|
|
91
|
+
WHERE id = ?
|
|
92
|
+
""",
|
|
93
|
+
(cost_usd, input_tokens, output_tokens, now, session_id),
|
|
94
|
+
)
|
|
95
|
+
self._commit()
|
|
96
|
+
|
|
97
|
+
def end(self, session_id: str) -> Optional[dict]:
|
|
98
|
+
"""End a session. Returns the updated row, or None if session_id not found."""
|
|
99
|
+
now = datetime.now(UTC).isoformat()
|
|
100
|
+
cursor = self._execute(
|
|
101
|
+
"""
|
|
102
|
+
UPDATE interactive_sessions
|
|
103
|
+
SET state = 'ended', ended_at = ?, updated_at = ?
|
|
104
|
+
WHERE id = ?
|
|
105
|
+
""",
|
|
106
|
+
(now, now, session_id),
|
|
107
|
+
)
|
|
108
|
+
self._commit()
|
|
109
|
+
if cursor.rowcount == 0:
|
|
110
|
+
return None
|
|
111
|
+
return self.get(session_id)
|
|
112
|
+
|
|
113
|
+
# -------------------------------------------------------------------------
|
|
114
|
+
# Messages
|
|
115
|
+
# -------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def add_message(
|
|
118
|
+
self,
|
|
119
|
+
session_id: str,
|
|
120
|
+
role: str,
|
|
121
|
+
content: str,
|
|
122
|
+
metadata: Optional[dict] = None,
|
|
123
|
+
) -> dict:
|
|
124
|
+
now = datetime.now(UTC).isoformat()
|
|
125
|
+
message_id = str(uuid.uuid4())
|
|
126
|
+
metadata_json = json.dumps(metadata) if metadata is not None else None
|
|
127
|
+
self._execute(
|
|
128
|
+
"""
|
|
129
|
+
INSERT INTO session_messages (id, session_id, role, content, metadata, created_at)
|
|
130
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
131
|
+
""",
|
|
132
|
+
(message_id, session_id, role, content, metadata_json, now),
|
|
133
|
+
)
|
|
134
|
+
self._commit()
|
|
135
|
+
return {
|
|
136
|
+
"id": message_id,
|
|
137
|
+
"session_id": session_id,
|
|
138
|
+
"role": role,
|
|
139
|
+
"content": content,
|
|
140
|
+
"metadata": metadata,
|
|
141
|
+
"created_at": now,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
def get_messages(
|
|
145
|
+
self, session_id: str, limit: int = 100, offset: int = 0
|
|
146
|
+
) -> list[dict]:
|
|
147
|
+
rows = self._fetchall(
|
|
148
|
+
"""
|
|
149
|
+
SELECT * FROM session_messages
|
|
150
|
+
WHERE session_id = ?
|
|
151
|
+
ORDER BY created_at
|
|
152
|
+
LIMIT ? OFFSET ?
|
|
153
|
+
""",
|
|
154
|
+
(session_id, limit, offset),
|
|
155
|
+
)
|
|
156
|
+
result = []
|
|
157
|
+
for row in rows:
|
|
158
|
+
d = self._row_to_dict(row)
|
|
159
|
+
if d.get("metadata"):
|
|
160
|
+
try:
|
|
161
|
+
d["metadata"] = json.loads(d["metadata"])
|
|
162
|
+
except (json.JSONDecodeError, TypeError):
|
|
163
|
+
d["metadata"] = None
|
|
164
|
+
result.append(d)
|
|
165
|
+
return result
|