aline-ai 0.6.5__py3-none-any.whl → 0.6.7__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.
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/METADATA +1 -1
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/RECORD +41 -34
- realign/__init__.py +1 -1
- realign/agent_names.py +79 -0
- realign/claude_hooks/stop_hook.py +3 -0
- realign/claude_hooks/terminal_state.py +43 -1
- realign/claude_hooks/user_prompt_submit_hook.py +3 -0
- realign/cli.py +62 -0
- realign/codex_detector.py +18 -3
- realign/codex_home.py +65 -16
- realign/codex_terminal_linker.py +18 -7
- realign/commands/agent.py +109 -0
- realign/commands/doctor.py +74 -1
- realign/commands/export_shares.py +448 -0
- realign/commands/import_shares.py +203 -1
- realign/commands/search.py +58 -29
- realign/commands/sync_agent.py +347 -0
- realign/dashboard/app.py +9 -9
- realign/dashboard/clipboard.py +54 -0
- realign/dashboard/screens/__init__.py +4 -0
- realign/dashboard/screens/agent_detail.py +333 -0
- realign/dashboard/screens/create_agent_info.py +244 -0
- realign/dashboard/screens/event_detail.py +6 -27
- realign/dashboard/styles/dashboard.tcss +22 -28
- realign/dashboard/tmux_manager.py +36 -10
- realign/dashboard/widgets/__init__.py +2 -2
- realign/dashboard/widgets/agents_panel.py +1248 -0
- realign/dashboard/widgets/events_table.py +4 -27
- realign/dashboard/widgets/sessions_table.py +4 -27
- realign/db/base.py +69 -0
- realign/db/locks.py +4 -0
- realign/db/schema.py +111 -2
- realign/db/sqlite_db.py +360 -2
- realign/events/agent_summarizer.py +157 -0
- realign/events/session_summarizer.py +25 -0
- realign/watcher_core.py +193 -5
- realign/worker_core.py +59 -1
- realign/dashboard/widgets/terminal_panel.py +0 -1653
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/top_level.txt +0 -0
realign/db/sqlite_db.py
CHANGED
|
@@ -21,7 +21,9 @@ from .base import (
|
|
|
21
21
|
EventRecord,
|
|
22
22
|
LockRecord,
|
|
23
23
|
AgentRecord,
|
|
24
|
+
AgentInfoRecord,
|
|
24
25
|
AgentContextRecord,
|
|
26
|
+
WindowLinkRecord,
|
|
25
27
|
UserRecord,
|
|
26
28
|
)
|
|
27
29
|
from .schema import (
|
|
@@ -293,6 +295,7 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
293
295
|
started_at: datetime,
|
|
294
296
|
workspace_path: Optional[str] = None,
|
|
295
297
|
metadata: Optional[Dict[str, Any]] = None,
|
|
298
|
+
agent_id: Optional[str] = None,
|
|
296
299
|
) -> SessionRecord:
|
|
297
300
|
"""Get existing session or create new one."""
|
|
298
301
|
conn = self._get_connection()
|
|
@@ -317,8 +320,8 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
317
320
|
INSERT INTO sessions (
|
|
318
321
|
id, session_file_path, session_type, workspace_path,
|
|
319
322
|
started_at, last_activity_at, created_at, updated_at, metadata,
|
|
320
|
-
created_by
|
|
321
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
323
|
+
created_by, agent_id
|
|
324
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
322
325
|
""",
|
|
323
326
|
(
|
|
324
327
|
session_id,
|
|
@@ -331,6 +334,7 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
331
334
|
now,
|
|
332
335
|
metadata_json,
|
|
333
336
|
config.uid,
|
|
337
|
+
agent_id,
|
|
334
338
|
),
|
|
335
339
|
)
|
|
336
340
|
conn.commit()
|
|
@@ -353,7 +357,17 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
353
357
|
metadata=metadata or {},
|
|
354
358
|
workspace_path=workspace_path,
|
|
355
359
|
created_by=config.uid,
|
|
360
|
+
agent_id=agent_id,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def update_session_agent_id(self, session_id: str, agent_id: Optional[str]) -> None:
|
|
364
|
+
"""Set or update the agent_id on an existing session (V19)."""
|
|
365
|
+
conn = self._get_connection()
|
|
366
|
+
conn.execute(
|
|
367
|
+
"UPDATE sessions SET agent_id = ?, updated_at = ? WHERE id = ?",
|
|
368
|
+
(agent_id, datetime.now(), session_id),
|
|
356
369
|
)
|
|
370
|
+
conn.commit()
|
|
357
371
|
|
|
358
372
|
def update_session_activity(self, session_id: str, last_activity_at: datetime) -> None:
|
|
359
373
|
"""Update last activity timestamp."""
|
|
@@ -557,6 +571,30 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
557
571
|
)
|
|
558
572
|
return [self._row_to_session(row) for row in cursor.fetchall()]
|
|
559
573
|
|
|
574
|
+
def get_sessions_by_agent_id(self, agent_id: str, limit: int = 1000) -> List[SessionRecord]:
|
|
575
|
+
"""Get all sessions linked to an agent.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
agent_id: The agent_info ID
|
|
579
|
+
limit: Maximum number of sessions to return
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
List of SessionRecord objects for this agent
|
|
583
|
+
"""
|
|
584
|
+
conn = self._get_connection()
|
|
585
|
+
cursor = conn.cursor()
|
|
586
|
+
try:
|
|
587
|
+
cursor.execute(
|
|
588
|
+
"""SELECT * FROM sessions
|
|
589
|
+
WHERE agent_id = ?
|
|
590
|
+
ORDER BY last_activity_at DESC
|
|
591
|
+
LIMIT ?""",
|
|
592
|
+
(agent_id, limit),
|
|
593
|
+
)
|
|
594
|
+
return [self._row_to_session(row) for row in cursor.fetchall()]
|
|
595
|
+
except sqlite3.OperationalError:
|
|
596
|
+
return []
|
|
597
|
+
|
|
560
598
|
def get_turn_content(self, turn_id: str) -> Optional[str]:
|
|
561
599
|
"""Get the JSONL content for a turn."""
|
|
562
600
|
conn = self._get_connection()
|
|
@@ -973,6 +1011,7 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
973
1011
|
expected_turns: Optional[int] = None,
|
|
974
1012
|
skip_dedup: bool = False,
|
|
975
1013
|
no_track: bool = False,
|
|
1014
|
+
agent_id: Optional[str] = None,
|
|
976
1015
|
) -> str:
|
|
977
1016
|
session_id = session_file_path.stem
|
|
978
1017
|
dedupe_key = f"turn:{session_id}:{int(turn_number)}"
|
|
@@ -991,6 +1030,8 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
991
1030
|
payload["skip_dedup"] = True
|
|
992
1031
|
if no_track:
|
|
993
1032
|
payload["no_track"] = True
|
|
1033
|
+
if agent_id:
|
|
1034
|
+
payload["agent_id"] = agent_id
|
|
994
1035
|
|
|
995
1036
|
# For append-only session formats (Claude/Codex/Gemini), a turn is immutable once completed.
|
|
996
1037
|
# Avoid re-running already-done turn jobs on repeated enqueue attempts.
|
|
@@ -1033,6 +1074,17 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
1033
1074
|
requeue_done=True,
|
|
1034
1075
|
)
|
|
1035
1076
|
|
|
1077
|
+
def enqueue_agent_description_job(self, *, agent_id: str) -> str:
|
|
1078
|
+
dedupe_key = f"agent_desc:{agent_id}"
|
|
1079
|
+
payload: Dict[str, Any] = {"agent_id": agent_id}
|
|
1080
|
+
return self.enqueue_job(
|
|
1081
|
+
kind="agent_description",
|
|
1082
|
+
dedupe_key=dedupe_key,
|
|
1083
|
+
payload=payload,
|
|
1084
|
+
priority=12,
|
|
1085
|
+
requeue_done=True,
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1036
1088
|
def claim_next_job(
|
|
1037
1089
|
self,
|
|
1038
1090
|
*,
|
|
@@ -2344,6 +2396,92 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
2344
2396
|
except sqlite3.OperationalError:
|
|
2345
2397
|
return []
|
|
2346
2398
|
|
|
2399
|
+
def insert_window_link(
|
|
2400
|
+
self,
|
|
2401
|
+
*,
|
|
2402
|
+
terminal_id: str,
|
|
2403
|
+
agent_id: Optional[str],
|
|
2404
|
+
session_id: Optional[str],
|
|
2405
|
+
provider: Optional[str],
|
|
2406
|
+
source: Optional[str],
|
|
2407
|
+
ts: Optional[float] = None,
|
|
2408
|
+
) -> None:
|
|
2409
|
+
conn = self._get_connection()
|
|
2410
|
+
cursor = conn.cursor()
|
|
2411
|
+
ts_value = ts if ts is not None else time.time()
|
|
2412
|
+
try:
|
|
2413
|
+
cursor.execute(
|
|
2414
|
+
"""
|
|
2415
|
+
INSERT INTO windowlink (
|
|
2416
|
+
terminal_id, agent_id, session_id, provider, source, ts
|
|
2417
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
2418
|
+
""",
|
|
2419
|
+
(
|
|
2420
|
+
terminal_id,
|
|
2421
|
+
agent_id,
|
|
2422
|
+
session_id,
|
|
2423
|
+
provider,
|
|
2424
|
+
source,
|
|
2425
|
+
float(ts_value),
|
|
2426
|
+
),
|
|
2427
|
+
)
|
|
2428
|
+
conn.commit()
|
|
2429
|
+
except sqlite3.OperationalError:
|
|
2430
|
+
conn.rollback()
|
|
2431
|
+
|
|
2432
|
+
def list_latest_window_links(
|
|
2433
|
+
self, *, agent_id: Optional[str] = None, limit: int = 1000
|
|
2434
|
+
) -> List[WindowLinkRecord]:
|
|
2435
|
+
conn = self._get_connection()
|
|
2436
|
+
cursor = conn.cursor()
|
|
2437
|
+
params: List[Any] = []
|
|
2438
|
+
where_clause = ""
|
|
2439
|
+
if agent_id:
|
|
2440
|
+
where_clause = "WHERE agent_id = ?"
|
|
2441
|
+
params.append(agent_id)
|
|
2442
|
+
params.append(limit)
|
|
2443
|
+
try:
|
|
2444
|
+
cursor.execute(
|
|
2445
|
+
f"""
|
|
2446
|
+
SELECT terminal_id, agent_id, session_id, provider, source, ts, created_at
|
|
2447
|
+
FROM (
|
|
2448
|
+
SELECT terminal_id,
|
|
2449
|
+
agent_id,
|
|
2450
|
+
session_id,
|
|
2451
|
+
provider,
|
|
2452
|
+
source,
|
|
2453
|
+
ts,
|
|
2454
|
+
created_at,
|
|
2455
|
+
ROW_NUMBER() OVER (
|
|
2456
|
+
PARTITION BY terminal_id
|
|
2457
|
+
ORDER BY ts DESC, id DESC
|
|
2458
|
+
) AS rn
|
|
2459
|
+
FROM windowlink
|
|
2460
|
+
{where_clause}
|
|
2461
|
+
) AS ranked
|
|
2462
|
+
WHERE rn = 1
|
|
2463
|
+
LIMIT ?
|
|
2464
|
+
""",
|
|
2465
|
+
params,
|
|
2466
|
+
)
|
|
2467
|
+
rows = cursor.fetchall()
|
|
2468
|
+
out: List[WindowLinkRecord] = []
|
|
2469
|
+
for row in rows:
|
|
2470
|
+
out.append(
|
|
2471
|
+
WindowLinkRecord(
|
|
2472
|
+
terminal_id=row[0],
|
|
2473
|
+
agent_id=row[1],
|
|
2474
|
+
session_id=row[2],
|
|
2475
|
+
provider=row[3],
|
|
2476
|
+
source=row[4],
|
|
2477
|
+
ts=row[5],
|
|
2478
|
+
created_at=self._parse_datetime(row[6]) if row[6] else None,
|
|
2479
|
+
)
|
|
2480
|
+
)
|
|
2481
|
+
return out
|
|
2482
|
+
except sqlite3.OperationalError:
|
|
2483
|
+
return []
|
|
2484
|
+
|
|
2347
2485
|
def delete_agent(self, agent_id: str) -> bool:
|
|
2348
2486
|
"""Delete an agent by ID."""
|
|
2349
2487
|
conn = self._get_connection()
|
|
@@ -2770,6 +2908,13 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
2770
2908
|
except (IndexError, KeyError):
|
|
2771
2909
|
pass
|
|
2772
2910
|
|
|
2911
|
+
# V19: agent association
|
|
2912
|
+
agent_id = None
|
|
2913
|
+
try:
|
|
2914
|
+
agent_id = row["agent_id"]
|
|
2915
|
+
except (IndexError, KeyError):
|
|
2916
|
+
pass
|
|
2917
|
+
|
|
2773
2918
|
return SessionRecord(
|
|
2774
2919
|
id=row["id"],
|
|
2775
2920
|
session_file_path=Path(row["session_file_path"]),
|
|
@@ -2790,6 +2935,7 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
2790
2935
|
shared_by=shared_by,
|
|
2791
2936
|
total_turns=total_turns,
|
|
2792
2937
|
total_turns_mtime=total_turns_mtime,
|
|
2938
|
+
agent_id=agent_id,
|
|
2793
2939
|
)
|
|
2794
2940
|
|
|
2795
2941
|
def _row_to_turn(self, row: sqlite3.Row) -> TurnRecord:
|
|
@@ -3015,3 +3161,215 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
3015
3161
|
except sqlite3.OperationalError:
|
|
3016
3162
|
pass
|
|
3017
3163
|
return None
|
|
3164
|
+
|
|
3165
|
+
# -------------------------------------------------------------------------
|
|
3166
|
+
# Agent info table methods (Schema V20)
|
|
3167
|
+
# -------------------------------------------------------------------------
|
|
3168
|
+
|
|
3169
|
+
def _row_to_agent_info(self, row: sqlite3.Row) -> AgentInfoRecord:
|
|
3170
|
+
"""Convert a database row to an AgentInfoRecord."""
|
|
3171
|
+
keys = row.keys()
|
|
3172
|
+
visibility = "visible"
|
|
3173
|
+
try:
|
|
3174
|
+
if "visibility" in keys:
|
|
3175
|
+
visibility = row["visibility"] or "visible"
|
|
3176
|
+
except Exception:
|
|
3177
|
+
visibility = "visible"
|
|
3178
|
+
|
|
3179
|
+
def _safe_get(key: str, default=None):
|
|
3180
|
+
try:
|
|
3181
|
+
return row[key] if key in keys else default
|
|
3182
|
+
except Exception:
|
|
3183
|
+
return default
|
|
3184
|
+
|
|
3185
|
+
return AgentInfoRecord(
|
|
3186
|
+
id=row["id"],
|
|
3187
|
+
name=row["name"],
|
|
3188
|
+
description=row["description"] or "",
|
|
3189
|
+
created_at=self._parse_datetime(row["created_at"]),
|
|
3190
|
+
updated_at=self._parse_datetime(row["updated_at"]),
|
|
3191
|
+
visibility=visibility,
|
|
3192
|
+
share_id=_safe_get("share_id"),
|
|
3193
|
+
share_url=_safe_get("share_url"),
|
|
3194
|
+
share_admin_token=_safe_get("share_admin_token"),
|
|
3195
|
+
share_contributor_token=_safe_get("share_contributor_token"),
|
|
3196
|
+
share_expiry_at=_safe_get("share_expiry_at"),
|
|
3197
|
+
last_synced_at=_safe_get("last_synced_at"),
|
|
3198
|
+
sync_version=_safe_get("sync_version", 0) or 0,
|
|
3199
|
+
)
|
|
3200
|
+
|
|
3201
|
+
def get_or_create_agent_info(
|
|
3202
|
+
self, agent_id: str, name: Optional[str] = None
|
|
3203
|
+
) -> AgentInfoRecord:
|
|
3204
|
+
"""Get existing agent info or create a new one.
|
|
3205
|
+
|
|
3206
|
+
Args:
|
|
3207
|
+
agent_id: UUID for the agent.
|
|
3208
|
+
name: Display name (if None, caller should provide a generated name).
|
|
3209
|
+
"""
|
|
3210
|
+
conn = self._get_connection()
|
|
3211
|
+
cursor = conn.cursor()
|
|
3212
|
+
|
|
3213
|
+
try:
|
|
3214
|
+
cursor.execute("SELECT * FROM agent_info WHERE id = ?", (agent_id,))
|
|
3215
|
+
row = cursor.fetchone()
|
|
3216
|
+
if row:
|
|
3217
|
+
return self._row_to_agent_info(row)
|
|
3218
|
+
except sqlite3.OperationalError:
|
|
3219
|
+
pass
|
|
3220
|
+
|
|
3221
|
+
display_name = name or agent_id[:8]
|
|
3222
|
+
cursor.execute(
|
|
3223
|
+
"""
|
|
3224
|
+
INSERT INTO agent_info (id, name, description, visibility, created_at, updated_at)
|
|
3225
|
+
VALUES (?, ?, '', 'visible', datetime('now'), datetime('now'))
|
|
3226
|
+
""",
|
|
3227
|
+
(agent_id, display_name),
|
|
3228
|
+
)
|
|
3229
|
+
conn.commit()
|
|
3230
|
+
|
|
3231
|
+
cursor.execute("SELECT * FROM agent_info WHERE id = ?", (agent_id,))
|
|
3232
|
+
row = cursor.fetchone()
|
|
3233
|
+
return self._row_to_agent_info(row)
|
|
3234
|
+
|
|
3235
|
+
def get_agent_info(self, agent_id: str) -> Optional[AgentInfoRecord]:
|
|
3236
|
+
"""Get agent info by ID, or None if not found."""
|
|
3237
|
+
conn = self._get_connection()
|
|
3238
|
+
try:
|
|
3239
|
+
cursor = conn.execute("SELECT * FROM agent_info WHERE id = ?", (agent_id,))
|
|
3240
|
+
row = cursor.fetchone()
|
|
3241
|
+
if row:
|
|
3242
|
+
return self._row_to_agent_info(row)
|
|
3243
|
+
except sqlite3.OperationalError:
|
|
3244
|
+
pass
|
|
3245
|
+
return None
|
|
3246
|
+
|
|
3247
|
+
def update_agent_info(
|
|
3248
|
+
self,
|
|
3249
|
+
agent_id: str,
|
|
3250
|
+
*,
|
|
3251
|
+
name: Optional[str] = None,
|
|
3252
|
+
description: Optional[str] = None,
|
|
3253
|
+
visibility: Optional[str] = None,
|
|
3254
|
+
) -> Optional[AgentInfoRecord]:
|
|
3255
|
+
"""Update agent info fields. Returns updated record or None if not found."""
|
|
3256
|
+
conn = self._get_connection()
|
|
3257
|
+
sets: list[str] = []
|
|
3258
|
+
params: list[Any] = []
|
|
3259
|
+
|
|
3260
|
+
if name is not None:
|
|
3261
|
+
sets.append("name = ?")
|
|
3262
|
+
params.append(name)
|
|
3263
|
+
if description is not None:
|
|
3264
|
+
sets.append("description = ?")
|
|
3265
|
+
params.append(description)
|
|
3266
|
+
if visibility is not None:
|
|
3267
|
+
sets.append("visibility = ?")
|
|
3268
|
+
params.append(visibility)
|
|
3269
|
+
|
|
3270
|
+
if not sets:
|
|
3271
|
+
return self.get_agent_info(agent_id)
|
|
3272
|
+
|
|
3273
|
+
sets.append("updated_at = datetime('now')")
|
|
3274
|
+
params.append(agent_id)
|
|
3275
|
+
|
|
3276
|
+
try:
|
|
3277
|
+
conn.execute(
|
|
3278
|
+
f"UPDATE agent_info SET {', '.join(sets)} WHERE id = ?",
|
|
3279
|
+
params,
|
|
3280
|
+
)
|
|
3281
|
+
conn.commit()
|
|
3282
|
+
except sqlite3.OperationalError:
|
|
3283
|
+
return None
|
|
3284
|
+
|
|
3285
|
+
return self.get_agent_info(agent_id)
|
|
3286
|
+
|
|
3287
|
+
def list_agent_info(self, *, include_invisible: bool = False) -> list[AgentInfoRecord]:
|
|
3288
|
+
"""List agent info records, ordered by created_at descending."""
|
|
3289
|
+
conn = self._get_connection()
|
|
3290
|
+
try:
|
|
3291
|
+
if include_invisible:
|
|
3292
|
+
cursor = conn.execute("SELECT * FROM agent_info ORDER BY created_at DESC")
|
|
3293
|
+
else:
|
|
3294
|
+
try:
|
|
3295
|
+
cursor = conn.execute(
|
|
3296
|
+
"SELECT * FROM agent_info WHERE visibility = 'visible' ORDER BY created_at DESC"
|
|
3297
|
+
)
|
|
3298
|
+
except sqlite3.OperationalError:
|
|
3299
|
+
cursor = conn.execute("SELECT * FROM agent_info ORDER BY created_at DESC")
|
|
3300
|
+
return [self._row_to_agent_info(row) for row in cursor.fetchall()]
|
|
3301
|
+
except sqlite3.OperationalError:
|
|
3302
|
+
return []
|
|
3303
|
+
|
|
3304
|
+
def update_agent_sync_metadata(
|
|
3305
|
+
self,
|
|
3306
|
+
agent_id: str,
|
|
3307
|
+
*,
|
|
3308
|
+
share_id: Optional[str] = None,
|
|
3309
|
+
share_url: Optional[str] = None,
|
|
3310
|
+
share_admin_token: Optional[str] = None,
|
|
3311
|
+
share_contributor_token: Optional[str] = None,
|
|
3312
|
+
share_expiry_at: Optional[str] = None,
|
|
3313
|
+
last_synced_at: Optional[str] = None,
|
|
3314
|
+
sync_version: Optional[int] = None,
|
|
3315
|
+
) -> Optional[AgentInfoRecord]:
|
|
3316
|
+
"""Update sync-related metadata on an agent_info record."""
|
|
3317
|
+
conn = self._get_connection()
|
|
3318
|
+
sets: list[str] = []
|
|
3319
|
+
params: list[Any] = []
|
|
3320
|
+
|
|
3321
|
+
if share_id is not None:
|
|
3322
|
+
sets.append("share_id = ?")
|
|
3323
|
+
params.append(share_id)
|
|
3324
|
+
if share_url is not None:
|
|
3325
|
+
sets.append("share_url = ?")
|
|
3326
|
+
params.append(share_url)
|
|
3327
|
+
if share_admin_token is not None:
|
|
3328
|
+
sets.append("share_admin_token = ?")
|
|
3329
|
+
params.append(share_admin_token)
|
|
3330
|
+
if share_contributor_token is not None:
|
|
3331
|
+
sets.append("share_contributor_token = ?")
|
|
3332
|
+
params.append(share_contributor_token)
|
|
3333
|
+
if share_expiry_at is not None:
|
|
3334
|
+
sets.append("share_expiry_at = ?")
|
|
3335
|
+
params.append(share_expiry_at)
|
|
3336
|
+
if last_synced_at is not None:
|
|
3337
|
+
sets.append("last_synced_at = ?")
|
|
3338
|
+
params.append(last_synced_at)
|
|
3339
|
+
if sync_version is not None:
|
|
3340
|
+
sets.append("sync_version = ?")
|
|
3341
|
+
params.append(sync_version)
|
|
3342
|
+
|
|
3343
|
+
if not sets:
|
|
3344
|
+
return self.get_agent_info(agent_id)
|
|
3345
|
+
|
|
3346
|
+
sets.append("updated_at = datetime('now')")
|
|
3347
|
+
params.append(agent_id)
|
|
3348
|
+
|
|
3349
|
+
try:
|
|
3350
|
+
conn.execute(
|
|
3351
|
+
f"UPDATE agent_info SET {', '.join(sets)} WHERE id = ?",
|
|
3352
|
+
params,
|
|
3353
|
+
)
|
|
3354
|
+
conn.commit()
|
|
3355
|
+
except sqlite3.OperationalError:
|
|
3356
|
+
return None
|
|
3357
|
+
|
|
3358
|
+
return self.get_agent_info(agent_id)
|
|
3359
|
+
|
|
3360
|
+
def get_agent_content_hashes(self, agent_id: str) -> set[str]:
|
|
3361
|
+
"""Return all content_hash values for turns in this agent's sessions."""
|
|
3362
|
+
conn = self._get_connection()
|
|
3363
|
+
try:
|
|
3364
|
+
cursor = conn.execute(
|
|
3365
|
+
"""
|
|
3366
|
+
SELECT DISTINCT t.content_hash
|
|
3367
|
+
FROM turns t
|
|
3368
|
+
JOIN sessions s ON t.session_id = s.id
|
|
3369
|
+
WHERE s.agent_id = ?
|
|
3370
|
+
""",
|
|
3371
|
+
(agent_id,),
|
|
3372
|
+
)
|
|
3373
|
+
return {row["content_hash"] for row in cursor.fetchall()}
|
|
3374
|
+
except sqlite3.OperationalError:
|
|
3375
|
+
return set()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Agent description generation from session summaries using LLM."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from ..db.sqlite_db import SQLiteDatabase
|
|
8
|
+
from ..db.base import SessionRecord
|
|
9
|
+
from ..db.locks import lease_lock, lock_key_for_agent_description, make_lock_owner
|
|
10
|
+
from ..llm_client import call_llm_cloud
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def schedule_agent_description_update(db: SQLiteDatabase, agent_id: str) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Enqueue an agent description update job (durable).
|
|
18
|
+
|
|
19
|
+
Call this after a session summary is updated for an agent-linked session.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
db: Database instance
|
|
23
|
+
agent_id: ID of the agent to update
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
db.enqueue_agent_description_job(agent_id=agent_id)
|
|
27
|
+
logger.debug(f"Enqueued agent description job for agent_id={agent_id}")
|
|
28
|
+
except Exception as e:
|
|
29
|
+
logger.debug(f"Failed to enqueue agent description job, falling back: {e}")
|
|
30
|
+
force_update_agent_description(db, agent_id)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def force_update_agent_description(db: SQLiteDatabase, agent_id: str) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Immediately update agent description, bypassing the job queue.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
db: Database instance
|
|
39
|
+
agent_id: ID of the agent to update
|
|
40
|
+
"""
|
|
41
|
+
_update_agent_description(db, agent_id)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _update_agent_description(db: SQLiteDatabase, agent_id: str) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Actually perform the agent description update.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
db: Database instance
|
|
50
|
+
agent_id: ID of the agent to update
|
|
51
|
+
"""
|
|
52
|
+
logger.info(f"Updating agent description for agent_id={agent_id}")
|
|
53
|
+
|
|
54
|
+
owner = make_lock_owner("agent_description")
|
|
55
|
+
lock_key = lock_key_for_agent_description(agent_id)
|
|
56
|
+
|
|
57
|
+
with lease_lock(
|
|
58
|
+
db,
|
|
59
|
+
lock_key,
|
|
60
|
+
owner=owner,
|
|
61
|
+
ttl_seconds=10 * 60, # 10 minutes
|
|
62
|
+
wait_timeout_seconds=0.0,
|
|
63
|
+
) as acquired:
|
|
64
|
+
if not acquired:
|
|
65
|
+
logger.debug(f"Agent description lock held by another process: agent_id={agent_id}")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
sessions = db.get_sessions_by_agent_id(agent_id)
|
|
69
|
+
if not sessions:
|
|
70
|
+
logger.warning(f"No sessions found for agent_id={agent_id}")
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# Filter to sessions with non-empty title or summary
|
|
74
|
+
sessions_with_content = [
|
|
75
|
+
s for s in sessions if s.session_title or s.session_summary
|
|
76
|
+
]
|
|
77
|
+
if not sessions_with_content:
|
|
78
|
+
logger.warning(f"No sessions with summaries for agent_id={agent_id}")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
description = _generate_agent_description_llm(sessions_with_content)
|
|
82
|
+
db.update_agent_info(agent_id, description=description)
|
|
83
|
+
logger.info(
|
|
84
|
+
f"Agent description updated: '{description[:60]}...' for agent_id={agent_id}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _generate_agent_description_llm(sessions: List[SessionRecord]) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Use LLM to generate an agent description from all its sessions.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
sessions: List of sessions linked to the agent
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Description string
|
|
97
|
+
"""
|
|
98
|
+
if not sessions:
|
|
99
|
+
return ""
|
|
100
|
+
|
|
101
|
+
# Build sessions payload
|
|
102
|
+
sessions_data = []
|
|
103
|
+
for i, session in enumerate(sessions):
|
|
104
|
+
sessions_data.append(
|
|
105
|
+
{
|
|
106
|
+
"session_number": i + 1,
|
|
107
|
+
"title": session.session_title or f"Session {session.id[:8]}",
|
|
108
|
+
"summary": session.session_summary or "(no summary)",
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Try cloud provider
|
|
113
|
+
try:
|
|
114
|
+
from ..auth import is_logged_in
|
|
115
|
+
|
|
116
|
+
if is_logged_in():
|
|
117
|
+
logger.debug("Attempting cloud LLM for agent description")
|
|
118
|
+
custom_prompt = None
|
|
119
|
+
user_prompt_path = Path.home() / ".aline" / "prompts" / "agent_description.md"
|
|
120
|
+
try:
|
|
121
|
+
if user_prompt_path.exists():
|
|
122
|
+
custom_prompt = user_prompt_path.read_text(encoding="utf-8").strip()
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
_, result = call_llm_cloud(
|
|
127
|
+
task="agent_description",
|
|
128
|
+
payload={"sessions": sessions_data},
|
|
129
|
+
custom_prompt=custom_prompt,
|
|
130
|
+
silent=True,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if result:
|
|
134
|
+
description = result.get("description", "")
|
|
135
|
+
logger.info(f"Cloud LLM agent description success: {description[:60]}...")
|
|
136
|
+
return description
|
|
137
|
+
else:
|
|
138
|
+
logger.warning("Cloud LLM agent description failed, using fallback")
|
|
139
|
+
return _fallback_agent_description(sessions)
|
|
140
|
+
except ImportError:
|
|
141
|
+
logger.debug("Auth module not available, skipping cloud LLM")
|
|
142
|
+
|
|
143
|
+
logger.warning("Not logged in, cannot use cloud LLM for agent description")
|
|
144
|
+
return _fallback_agent_description(sessions)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _fallback_agent_description(sessions: List[SessionRecord]) -> str:
|
|
148
|
+
"""Fallback when LLM fails: simple concatenation of recent session summaries."""
|
|
149
|
+
summaries = [s.session_summary for s in sessions if s.session_summary]
|
|
150
|
+
if not summaries:
|
|
151
|
+
titles = [s.session_title for s in sessions if s.session_title]
|
|
152
|
+
if titles:
|
|
153
|
+
return f"Agent with {len(sessions)} sessions. Recent work: {'; '.join(titles[-3:])}"
|
|
154
|
+
return f"Agent with {len(sessions)} sessions."
|
|
155
|
+
|
|
156
|
+
recent = summaries[:3] # sessions already ordered by last_activity_at DESC
|
|
157
|
+
return f"Agent with {len(sessions)} sessions. " + " ".join(recent)
|
|
@@ -145,6 +145,9 @@ def update_session_summary_now(db: SQLiteDatabase, session_id: str) -> bool:
|
|
|
145
145
|
|
|
146
146
|
# Trigger event summary updates for all parent events (if any)
|
|
147
147
|
_trigger_parent_event_updates(db, session_id)
|
|
148
|
+
|
|
149
|
+
# Trigger agent description update if session is linked to an agent
|
|
150
|
+
_trigger_agent_description_update(db, session_id)
|
|
148
151
|
return True
|
|
149
152
|
|
|
150
153
|
|
|
@@ -168,6 +171,28 @@ def _trigger_parent_event_updates(db: SQLiteDatabase, session_id: str) -> None:
|
|
|
168
171
|
logger.warning(f"Failed to trigger event updates: {e}")
|
|
169
172
|
|
|
170
173
|
|
|
174
|
+
def _trigger_agent_description_update(db: SQLiteDatabase, session_id: str) -> None:
|
|
175
|
+
"""
|
|
176
|
+
Trigger agent description update if the session is linked to an agent.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
db: Database instance
|
|
180
|
+
session_id: ID of the session whose agent should be updated
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
session = db.get_session_by_id(session_id)
|
|
184
|
+
if session and getattr(session, "agent_id", None):
|
|
185
|
+
from .agent_summarizer import schedule_agent_description_update
|
|
186
|
+
|
|
187
|
+
schedule_agent_description_update(db, session.agent_id)
|
|
188
|
+
logger.debug(
|
|
189
|
+
f"Triggered agent description update for agent_id={session.agent_id}"
|
|
190
|
+
)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
# Don't fail session summary update if agent trigger fails
|
|
193
|
+
logger.warning(f"Failed to trigger agent description update: {e}")
|
|
194
|
+
|
|
195
|
+
|
|
171
196
|
def _get_session_summary_prompt() -> str:
|
|
172
197
|
"""
|
|
173
198
|
Load session summary prompt with user customization support.
|