aline-ai 0.5.9__py3-none-any.whl → 0.5.10__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.
realign/db/sqlite_db.py CHANGED
@@ -20,6 +20,8 @@ from .base import (
20
20
  TurnRecord,
21
21
  EventRecord,
22
22
  LockRecord,
23
+ AgentRecord,
24
+ AgentContextRecord,
23
25
  )
24
26
  from .schema import (
25
27
  INIT_SCRIPTS,
@@ -2024,6 +2026,572 @@ class SQLiteDatabase(DatabaseInterface):
2024
2026
  self._connection.close()
2025
2027
  self._connection = None
2026
2028
 
2029
+ # -------------------------------------------------------------------------
2030
+ # Agent methods (Schema V15 - replaces terminal.json)
2031
+ # -------------------------------------------------------------------------
2032
+
2033
+ def get_or_create_agent(
2034
+ self,
2035
+ agent_id: str,
2036
+ provider: str,
2037
+ session_type: str,
2038
+ *,
2039
+ session_id: Optional[str] = None,
2040
+ context_id: Optional[str] = None,
2041
+ transcript_path: Optional[str] = None,
2042
+ cwd: Optional[str] = None,
2043
+ project_dir: Optional[str] = None,
2044
+ source: Optional[str] = None,
2045
+ status: str = "active",
2046
+ attention: Optional[str] = None,
2047
+ ) -> AgentRecord:
2048
+ """Get existing agent or create new one."""
2049
+ conn = self._get_connection()
2050
+ cursor = conn.cursor()
2051
+
2052
+ try:
2053
+ cursor.execute("SELECT * FROM agents WHERE id = ?", (agent_id,))
2054
+ row = cursor.fetchone()
2055
+ if row:
2056
+ return self._row_to_agent(row)
2057
+ except sqlite3.OperationalError:
2058
+ # Table may not exist in older schema
2059
+ raise
2060
+
2061
+ # Get user identity from config
2062
+ try:
2063
+ from ..config import ReAlignConfig
2064
+
2065
+ config = ReAlignConfig.load()
2066
+ creator_name = config.user_name
2067
+ creator_id = config.user_id
2068
+ except Exception:
2069
+ creator_name = None
2070
+ creator_id = None
2071
+
2072
+ now = datetime.now()
2073
+ cursor.execute(
2074
+ """
2075
+ INSERT INTO agents (
2076
+ id, provider, session_type, session_id, context_id,
2077
+ transcript_path, cwd, project_dir, status, attention, source,
2078
+ created_at, updated_at, creator_name, creator_id
2079
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2080
+ """,
2081
+ (
2082
+ agent_id,
2083
+ provider,
2084
+ session_type,
2085
+ session_id,
2086
+ context_id,
2087
+ transcript_path,
2088
+ cwd,
2089
+ project_dir,
2090
+ status,
2091
+ attention,
2092
+ source,
2093
+ now,
2094
+ now,
2095
+ creator_name,
2096
+ creator_id,
2097
+ ),
2098
+ )
2099
+ conn.commit()
2100
+
2101
+ return AgentRecord(
2102
+ id=agent_id,
2103
+ provider=provider,
2104
+ session_type=session_type,
2105
+ session_id=session_id,
2106
+ context_id=context_id,
2107
+ transcript_path=transcript_path,
2108
+ cwd=cwd,
2109
+ project_dir=project_dir,
2110
+ status=status,
2111
+ attention=attention,
2112
+ source=source,
2113
+ created_at=now,
2114
+ updated_at=now,
2115
+ creator_name=creator_name,
2116
+ creator_id=creator_id,
2117
+ )
2118
+
2119
+ def update_agent(
2120
+ self,
2121
+ agent_id: str,
2122
+ *,
2123
+ provider: Optional[str] = None,
2124
+ session_type: Optional[str] = None,
2125
+ session_id: Optional[str] = None,
2126
+ context_id: Optional[str] = None,
2127
+ transcript_path: Optional[str] = None,
2128
+ cwd: Optional[str] = None,
2129
+ project_dir: Optional[str] = None,
2130
+ status: Optional[str] = None,
2131
+ attention: Optional[str] = None,
2132
+ source: Optional[str] = None,
2133
+ ) -> Optional[AgentRecord]:
2134
+ """Update an existing agent. Returns the updated record or None if not found."""
2135
+ conn = self._get_connection()
2136
+ cursor = conn.cursor()
2137
+
2138
+ # Build dynamic UPDATE statement
2139
+ updates: List[str] = []
2140
+ params: List[Any] = []
2141
+
2142
+ if provider is not None:
2143
+ updates.append("provider = ?")
2144
+ params.append(provider)
2145
+ if session_type is not None:
2146
+ updates.append("session_type = ?")
2147
+ params.append(session_type)
2148
+ if session_id is not None:
2149
+ updates.append("session_id = ?")
2150
+ params.append(session_id)
2151
+ if context_id is not None:
2152
+ updates.append("context_id = ?")
2153
+ params.append(context_id)
2154
+ if transcript_path is not None:
2155
+ updates.append("transcript_path = ?")
2156
+ params.append(transcript_path)
2157
+ if cwd is not None:
2158
+ updates.append("cwd = ?")
2159
+ params.append(cwd)
2160
+ if project_dir is not None:
2161
+ updates.append("project_dir = ?")
2162
+ params.append(project_dir)
2163
+ if status is not None:
2164
+ updates.append("status = ?")
2165
+ params.append(status)
2166
+ if attention is not None:
2167
+ updates.append("attention = ?")
2168
+ params.append(attention if attention != "" else None)
2169
+ if source is not None:
2170
+ updates.append("source = ?")
2171
+ params.append(source)
2172
+
2173
+ if not updates:
2174
+ # Nothing to update, just return existing
2175
+ return self.get_agent_by_id(agent_id)
2176
+
2177
+ updates.append("updated_at = datetime('now')")
2178
+ params.append(agent_id)
2179
+
2180
+ try:
2181
+ cursor.execute(
2182
+ f"UPDATE agents SET {', '.join(updates)} WHERE id = ?",
2183
+ params,
2184
+ )
2185
+ conn.commit()
2186
+ return self.get_agent_by_id(agent_id)
2187
+ except sqlite3.OperationalError:
2188
+ conn.rollback()
2189
+ return None
2190
+
2191
+ def get_agent_by_id(self, agent_id: str) -> Optional[AgentRecord]:
2192
+ """Get an agent by its ID."""
2193
+ conn = self._get_connection()
2194
+ cursor = conn.cursor()
2195
+ try:
2196
+ cursor.execute("SELECT * FROM agents WHERE id = ?", (agent_id,))
2197
+ row = cursor.fetchone()
2198
+ if row:
2199
+ return self._row_to_agent(row)
2200
+ except sqlite3.OperationalError:
2201
+ pass
2202
+ return None
2203
+
2204
+ def list_agents(
2205
+ self,
2206
+ *,
2207
+ status: Optional[str] = None,
2208
+ context_id: Optional[str] = None,
2209
+ limit: int = 100,
2210
+ ) -> List[AgentRecord]:
2211
+ """List agents, optionally filtered by status or context."""
2212
+ conn = self._get_connection()
2213
+ cursor = conn.cursor()
2214
+
2215
+ where_clauses: List[str] = []
2216
+ params: List[Any] = []
2217
+
2218
+ if status:
2219
+ where_clauses.append("status = ?")
2220
+ params.append(status)
2221
+ if context_id:
2222
+ where_clauses.append("context_id = ?")
2223
+ params.append(context_id)
2224
+
2225
+ where_sql = ""
2226
+ if where_clauses:
2227
+ where_sql = "WHERE " + " AND ".join(where_clauses)
2228
+
2229
+ params.append(limit)
2230
+
2231
+ try:
2232
+ cursor.execute(
2233
+ f"""
2234
+ SELECT * FROM agents
2235
+ {where_sql}
2236
+ ORDER BY updated_at DESC
2237
+ LIMIT ?
2238
+ """,
2239
+ params,
2240
+ )
2241
+ return [self._row_to_agent(row) for row in cursor.fetchall()]
2242
+ except sqlite3.OperationalError:
2243
+ return []
2244
+
2245
+ def delete_agent(self, agent_id: str) -> bool:
2246
+ """Delete an agent by ID."""
2247
+ conn = self._get_connection()
2248
+ try:
2249
+ cursor = conn.execute("DELETE FROM agents WHERE id = ?", (agent_id,))
2250
+ conn.commit()
2251
+ return cursor.rowcount > 0
2252
+ except sqlite3.OperationalError:
2253
+ conn.rollback()
2254
+ return False
2255
+
2256
+ # -------------------------------------------------------------------------
2257
+ # Agent Context methods (Schema V15 - replaces load.json)
2258
+ # -------------------------------------------------------------------------
2259
+
2260
+ def get_or_create_agent_context(
2261
+ self,
2262
+ context_id: str,
2263
+ *,
2264
+ workspace: Optional[str] = None,
2265
+ loaded_at: Optional[str] = None,
2266
+ metadata: Optional[Dict[str, Any]] = None,
2267
+ ) -> AgentContextRecord:
2268
+ """Get existing agent context or create new one."""
2269
+ conn = self._get_connection()
2270
+ cursor = conn.cursor()
2271
+
2272
+ try:
2273
+ cursor.execute("SELECT * FROM agent_contexts WHERE id = ?", (context_id,))
2274
+ row = cursor.fetchone()
2275
+ if row:
2276
+ return self._row_to_agent_context(row)
2277
+ except sqlite3.OperationalError:
2278
+ raise
2279
+
2280
+ now = datetime.now()
2281
+ metadata_json = json.dumps(metadata or {}, ensure_ascii=False)
2282
+
2283
+ cursor.execute(
2284
+ """
2285
+ INSERT INTO agent_contexts (id, workspace, loaded_at, created_at, updated_at, metadata)
2286
+ VALUES (?, ?, ?, ?, ?, ?)
2287
+ """,
2288
+ (context_id, workspace, loaded_at, now, now, metadata_json),
2289
+ )
2290
+ conn.commit()
2291
+
2292
+ return AgentContextRecord(
2293
+ id=context_id,
2294
+ workspace=workspace,
2295
+ loaded_at=loaded_at,
2296
+ created_at=now,
2297
+ updated_at=now,
2298
+ metadata=metadata,
2299
+ session_ids=[],
2300
+ event_ids=[],
2301
+ )
2302
+
2303
+ def update_agent_context(
2304
+ self,
2305
+ context_id: str,
2306
+ *,
2307
+ workspace: Optional[str] = None,
2308
+ loaded_at: Optional[str] = None,
2309
+ metadata: Optional[Dict[str, Any]] = None,
2310
+ ) -> Optional[AgentContextRecord]:
2311
+ """Update an existing agent context."""
2312
+ conn = self._get_connection()
2313
+ cursor = conn.cursor()
2314
+
2315
+ updates: List[str] = []
2316
+ params: List[Any] = []
2317
+
2318
+ if workspace is not None:
2319
+ updates.append("workspace = ?")
2320
+ params.append(workspace)
2321
+ if loaded_at is not None:
2322
+ updates.append("loaded_at = ?")
2323
+ params.append(loaded_at)
2324
+ if metadata is not None:
2325
+ updates.append("metadata = ?")
2326
+ params.append(json.dumps(metadata, ensure_ascii=False))
2327
+
2328
+ if not updates:
2329
+ return self.get_agent_context_by_id(context_id)
2330
+
2331
+ updates.append("updated_at = datetime('now')")
2332
+ params.append(context_id)
2333
+
2334
+ try:
2335
+ cursor.execute(
2336
+ f"UPDATE agent_contexts SET {', '.join(updates)} WHERE id = ?",
2337
+ params,
2338
+ )
2339
+ conn.commit()
2340
+ return self.get_agent_context_by_id(context_id)
2341
+ except sqlite3.OperationalError:
2342
+ conn.rollback()
2343
+ return None
2344
+
2345
+ def get_agent_context_by_id(self, context_id: str) -> Optional[AgentContextRecord]:
2346
+ """Get an agent context by its ID, including linked sessions and events."""
2347
+ conn = self._get_connection()
2348
+ cursor = conn.cursor()
2349
+
2350
+ try:
2351
+ cursor.execute("SELECT * FROM agent_contexts WHERE id = ?", (context_id,))
2352
+ row = cursor.fetchone()
2353
+ if not row:
2354
+ return None
2355
+
2356
+ record = self._row_to_agent_context(row)
2357
+
2358
+ # Fetch linked session IDs
2359
+ cursor.execute(
2360
+ "SELECT session_id FROM agent_context_sessions WHERE context_id = ?",
2361
+ (context_id,),
2362
+ )
2363
+ record.session_ids = [r[0] for r in cursor.fetchall()]
2364
+
2365
+ # Fetch linked event IDs
2366
+ cursor.execute(
2367
+ "SELECT event_id FROM agent_context_events WHERE context_id = ?",
2368
+ (context_id,),
2369
+ )
2370
+ record.event_ids = [r[0] for r in cursor.fetchall()]
2371
+
2372
+ return record
2373
+ except sqlite3.OperationalError:
2374
+ return None
2375
+
2376
+ def get_agent_context_by_workspace(
2377
+ self, workspace: str
2378
+ ) -> Optional[AgentContextRecord]:
2379
+ """Get an agent context by workspace path."""
2380
+ conn = self._get_connection()
2381
+ cursor = conn.cursor()
2382
+
2383
+ try:
2384
+ cursor.execute(
2385
+ "SELECT * FROM agent_contexts WHERE workspace = ?", (workspace,)
2386
+ )
2387
+ row = cursor.fetchone()
2388
+ if not row:
2389
+ return None
2390
+
2391
+ record = self._row_to_agent_context(row)
2392
+ context_id = record.id
2393
+
2394
+ # Fetch linked session IDs
2395
+ cursor.execute(
2396
+ "SELECT session_id FROM agent_context_sessions WHERE context_id = ?",
2397
+ (context_id,),
2398
+ )
2399
+ record.session_ids = [r[0] for r in cursor.fetchall()]
2400
+
2401
+ # Fetch linked event IDs
2402
+ cursor.execute(
2403
+ "SELECT event_id FROM agent_context_events WHERE context_id = ?",
2404
+ (context_id,),
2405
+ )
2406
+ record.event_ids = [r[0] for r in cursor.fetchall()]
2407
+
2408
+ return record
2409
+ except sqlite3.OperationalError:
2410
+ return None
2411
+
2412
+ def list_agent_contexts(self, limit: int = 100) -> List[AgentContextRecord]:
2413
+ """List all agent contexts."""
2414
+ conn = self._get_connection()
2415
+ cursor = conn.cursor()
2416
+
2417
+ try:
2418
+ cursor.execute(
2419
+ """
2420
+ SELECT * FROM agent_contexts
2421
+ ORDER BY updated_at DESC
2422
+ LIMIT ?
2423
+ """,
2424
+ (limit,),
2425
+ )
2426
+ contexts = []
2427
+ for row in cursor.fetchall():
2428
+ record = self._row_to_agent_context(row)
2429
+ context_id = record.id
2430
+
2431
+ # Fetch linked session IDs
2432
+ cursor.execute(
2433
+ "SELECT session_id FROM agent_context_sessions WHERE context_id = ?",
2434
+ (context_id,),
2435
+ )
2436
+ record.session_ids = [r[0] for r in cursor.fetchall()]
2437
+
2438
+ # Fetch linked event IDs
2439
+ cursor.execute(
2440
+ "SELECT event_id FROM agent_context_events WHERE context_id = ?",
2441
+ (context_id,),
2442
+ )
2443
+ record.event_ids = [r[0] for r in cursor.fetchall()]
2444
+
2445
+ contexts.append(record)
2446
+ return contexts
2447
+ except sqlite3.OperationalError:
2448
+ return []
2449
+
2450
+ def delete_agent_context(self, context_id: str) -> bool:
2451
+ """Delete an agent context and all its links."""
2452
+ conn = self._get_connection()
2453
+ try:
2454
+ conn.execute(
2455
+ "DELETE FROM agent_context_sessions WHERE context_id = ?", (context_id,)
2456
+ )
2457
+ conn.execute(
2458
+ "DELETE FROM agent_context_events WHERE context_id = ?", (context_id,)
2459
+ )
2460
+ cursor = conn.execute(
2461
+ "DELETE FROM agent_contexts WHERE id = ?", (context_id,)
2462
+ )
2463
+ conn.commit()
2464
+ return cursor.rowcount > 0
2465
+ except sqlite3.OperationalError:
2466
+ conn.rollback()
2467
+ return False
2468
+
2469
+ def link_session_to_agent_context(self, context_id: str, session_id: str) -> bool:
2470
+ """Link a session to an agent context.
2471
+
2472
+ Silently skips if session doesn't exist (FK constraint).
2473
+ """
2474
+ conn = self._get_connection()
2475
+ try:
2476
+ conn.execute(
2477
+ """
2478
+ INSERT OR IGNORE INTO agent_context_sessions (context_id, session_id)
2479
+ VALUES (?, ?)
2480
+ """,
2481
+ (context_id, session_id),
2482
+ )
2483
+ conn.commit()
2484
+ return True
2485
+ except (sqlite3.OperationalError, sqlite3.IntegrityError):
2486
+ conn.rollback()
2487
+ return False
2488
+
2489
+ def unlink_session_from_agent_context(
2490
+ self, context_id: str, session_id: str
2491
+ ) -> bool:
2492
+ """Unlink a session from an agent context."""
2493
+ conn = self._get_connection()
2494
+ try:
2495
+ conn.execute(
2496
+ "DELETE FROM agent_context_sessions WHERE context_id = ? AND session_id = ?",
2497
+ (context_id, session_id),
2498
+ )
2499
+ conn.commit()
2500
+ return True
2501
+ except (sqlite3.OperationalError, sqlite3.IntegrityError):
2502
+ conn.rollback()
2503
+ return False
2504
+
2505
+ def link_event_to_agent_context(self, context_id: str, event_id: str) -> bool:
2506
+ """Link an event to an agent context.
2507
+
2508
+ Silently skips if event doesn't exist (FK constraint).
2509
+ """
2510
+ conn = self._get_connection()
2511
+ try:
2512
+ conn.execute(
2513
+ """
2514
+ INSERT OR IGNORE INTO agent_context_events (context_id, event_id)
2515
+ VALUES (?, ?)
2516
+ """,
2517
+ (context_id, event_id),
2518
+ )
2519
+ conn.commit()
2520
+ return True
2521
+ except (sqlite3.OperationalError, sqlite3.IntegrityError):
2522
+ conn.rollback()
2523
+ return False
2524
+
2525
+ def unlink_event_from_agent_context(self, context_id: str, event_id: str) -> bool:
2526
+ """Unlink an event from an agent context."""
2527
+ conn = self._get_connection()
2528
+ try:
2529
+ conn.execute(
2530
+ "DELETE FROM agent_context_events WHERE context_id = ? AND event_id = ?",
2531
+ (context_id, event_id),
2532
+ )
2533
+ conn.commit()
2534
+ return True
2535
+ except (sqlite3.OperationalError, sqlite3.IntegrityError):
2536
+ conn.rollback()
2537
+ return False
2538
+
2539
+ def set_agent_context_sessions(
2540
+ self, context_id: str, session_ids: List[str]
2541
+ ) -> bool:
2542
+ """Replace all sessions linked to a context.
2543
+
2544
+ Silently skips sessions that don't exist in DB (FK constraint).
2545
+ """
2546
+ conn = self._get_connection()
2547
+ try:
2548
+ conn.execute(
2549
+ "DELETE FROM agent_context_sessions WHERE context_id = ?", (context_id,)
2550
+ )
2551
+ if session_ids:
2552
+ # Insert one at a time to skip FK failures
2553
+ for sid in session_ids:
2554
+ try:
2555
+ conn.execute(
2556
+ "INSERT OR IGNORE INTO agent_context_sessions (context_id, session_id) VALUES (?, ?)",
2557
+ (context_id, sid),
2558
+ )
2559
+ except sqlite3.IntegrityError:
2560
+ # Session doesn't exist in DB, skip
2561
+ pass
2562
+ conn.commit()
2563
+ return True
2564
+ except (sqlite3.OperationalError, sqlite3.IntegrityError):
2565
+ conn.rollback()
2566
+ return False
2567
+
2568
+ def set_agent_context_events(self, context_id: str, event_ids: List[str]) -> bool:
2569
+ """Replace all events linked to a context.
2570
+
2571
+ Silently skips events that don't exist in DB (FK constraint).
2572
+ """
2573
+ conn = self._get_connection()
2574
+ try:
2575
+ conn.execute(
2576
+ "DELETE FROM agent_context_events WHERE context_id = ?", (context_id,)
2577
+ )
2578
+ if event_ids:
2579
+ # Insert one at a time to skip FK failures
2580
+ for eid in event_ids:
2581
+ try:
2582
+ conn.execute(
2583
+ "INSERT OR IGNORE INTO agent_context_events (context_id, event_id) VALUES (?, ?)",
2584
+ (context_id, eid),
2585
+ )
2586
+ except sqlite3.IntegrityError:
2587
+ # Event doesn't exist in DB, skip
2588
+ pass
2589
+ conn.commit()
2590
+ return True
2591
+ except (sqlite3.OperationalError, sqlite3.IntegrityError):
2592
+ conn.rollback()
2593
+ return False
2594
+
2027
2595
  # Helper methods for row mapping
2028
2596
  def _row_to_project(self, row: sqlite3.Row) -> ProjectRecord:
2029
2597
  return ProjectRecord(
@@ -2255,3 +2823,52 @@ class SQLiteDatabase(DatabaseInterface):
2255
2823
  creator_name=creator_name,
2256
2824
  creator_id=creator_id,
2257
2825
  )
2826
+
2827
+ def _row_to_agent(self, row: sqlite3.Row) -> AgentRecord:
2828
+ """Convert a database row to an AgentRecord."""
2829
+ creator_name = None
2830
+ creator_id = None
2831
+ try:
2832
+ creator_name = row["creator_name"]
2833
+ creator_id = row["creator_id"]
2834
+ except (IndexError, KeyError):
2835
+ pass
2836
+
2837
+ return AgentRecord(
2838
+ id=row["id"],
2839
+ provider=row["provider"],
2840
+ session_type=row["session_type"],
2841
+ session_id=row["session_id"],
2842
+ context_id=row["context_id"],
2843
+ transcript_path=row["transcript_path"],
2844
+ cwd=row["cwd"],
2845
+ project_dir=row["project_dir"],
2846
+ status=row["status"] or "active",
2847
+ attention=row["attention"],
2848
+ source=row["source"],
2849
+ created_at=self._parse_datetime(row["created_at"]),
2850
+ updated_at=self._parse_datetime(row["updated_at"]),
2851
+ creator_name=creator_name,
2852
+ creator_id=creator_id,
2853
+ )
2854
+
2855
+ def _row_to_agent_context(self, row: sqlite3.Row) -> AgentContextRecord:
2856
+ """Convert a database row to an AgentContextRecord."""
2857
+ metadata = None
2858
+ try:
2859
+ metadata_raw = row["metadata"]
2860
+ if metadata_raw:
2861
+ metadata = json.loads(metadata_raw)
2862
+ except (IndexError, KeyError, json.JSONDecodeError):
2863
+ pass
2864
+
2865
+ return AgentContextRecord(
2866
+ id=row["id"],
2867
+ workspace=row["workspace"],
2868
+ loaded_at=row["loaded_at"],
2869
+ created_at=self._parse_datetime(row["created_at"]),
2870
+ updated_at=self._parse_datetime(row["updated_at"]),
2871
+ metadata=metadata,
2872
+ session_ids=None, # Populated separately
2873
+ event_ids=None, # Populated separately
2874
+ )