feat: unify history tools and improve execution reliability

This commit is contained in:
somew1nd committed 2026-09-03 16:05:47 -07:00
1 parent d23a4f6472
commit 2fea9865ee
116 files changed
+8291 -2503

No files matched your search

@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
import os
import sqlite3
@@ -301,6 +302,89 @@ class SessionRepository:
result.setdefault(session_id, []).append(name)
return result
def get_session_usage(self, session_id: str) -> dict[str, Any]:
"""Aggregate measured LLM usage for one session.
Sums every ``llm_usage`` trace the token meter recorded (all agents,
Flash and Pro alike) and reports the prompt size of the latest
Operator/FlashRunner call as the live executor context. Also echoes
the profile and Pro tuning the run was started with, read from the
session's stored ``device_info``.
"""
from artemis.services.token_meter import (
OPERATOR_CONTEXT_WINDOW_TOKENS,
OPERATOR_NODE_NAMES,
)
def _int(value: Any) -> int:
try:
return int(value or 0)
except (TypeError, ValueError):
return 0
llm_calls = prompt = completion = total = cached = 0
operator_context: int | None = None
operator_context_at: float | None = None
with db_session(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT payload, timestamp FROM traces WHERE session_id = ?"
" AND type = 'llm_call' AND name = 'llm_usage' ORDER BY timestamp ASC",
(session_id,),
)
for row in cursor.fetchall():
raw = row["payload"]
try:
payload = json.loads(raw) if isinstance(raw, str) else raw
except (ValueError, TypeError):
continue
if not isinstance(payload, dict):
continue
p = _int(payload.get("prompt_tokens"))
c = _int(payload.get("completion_tokens"))
t = _int(payload.get("total_tokens")) or (p + c)
if t <= 0:
continue
llm_calls += 1
prompt += p
completion += c
total += t
cached += _int(payload.get("cached_tokens"))
node = str(payload.get("node") or "").strip().lower()
if node in OPERATOR_NODE_NAMES:
operator_context = _int(payload.get("context_base_tokens")) or p
operator_context_at = row["timestamp"]
cursor.execute("SELECT device_info FROM sessions WHERE session_id = ?", (session_id,))
session_row = cursor.fetchone()
profile: str | None = None
run_tuning: dict[str, Any] | None = None
if session_row and session_row["device_info"]:
try:
d_info = json.loads(session_row["device_info"])
except (ValueError, TypeError):
d_info = None
if isinstance(d_info, dict):
profile = d_info.get("profile") or None
tuning = d_info.get("run_tuning")
if isinstance(tuning, dict) and tuning:
run_tuning = dict(tuning)
return {
"session_id": session_id,
"llm_calls": llm_calls,
"prompt_tokens": prompt,
"completion_tokens": completion,
"total_tokens": total,
"cached_tokens": cached,
"operator_context_tokens": operator_context,
"operator_context_window_tokens": OPERATOR_CONTEXT_WINDOW_TOKENS,
"operator_context_updated_at": operator_context_at,
"profile": profile,
"run_tuning": run_tuning,
}
def get_session_by_id(self, session_id: str) -> dict[str, Any] | None:
try:
with db_session(self.db_path) as conn:
+9
View File
@@ -176,6 +176,15 @@ async def get_session_details(session_id: str):
return dict(row)
@router.get("/api/sessions/{session_id}/usage")
async def get_session_usage(session_id: str):
"""Session-wide LLM token totals, live executor context size and run tuning."""
try:
return await asyncio.to_thread(session_repo.get_session_usage, session_id)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/sessions/{session_id}/tree")
async def get_tree(session_id: str):
try:
+46 -29
View File
@@ -397,15 +397,19 @@ class MediaService:
or not segment_path.is_file()
):
continue
segments.append(
{
"url": cls.path_to_video_url(segment_path),
"start": float(item.get("start", 0)),
"duration": float(item.get("duration", 0)),
"width": int(item.get("width", 0)),
"height": int(item.get("height", 0)),
}
)
segment = {
"url": cls.path_to_video_url(segment_path),
"start": float(item.get("start", 0)),
"duration": float(item.get("duration", 0)),
"width": int(item.get("width", 0)),
"height": int(item.get("height", 0)),
}
# Manifest v2 carries the real session-relative timeline so the
# UI can seek by step time across scrcpy restart gaps.
if item.get("offset_ms") is not None and item.get("duration_ms") is not None:
segment["offset_ms"] = int(item["offset_ms"])
segment["duration_ms"] = int(item["duration_ms"])
segments.append(segment)
return segments
except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError):
return []
@@ -535,30 +539,43 @@ class MediaService:
return f"Error reading task plan: {e}"
return "No task plan created yet."
@staticmethod
def _read_jsonl_records(path: Path, label: str, session_id: str) -> list[dict[str, Any]]:
records: list[dict[str, Any]] = []
if not path.is_file():
return records
try:
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
rec = json.loads(line)
except ValueError:
continue
if isinstance(rec, dict):
records.append(rec)
except (OSError, ValueError) as e:
logger.warning(f"Failed to read {label} for {session_id}: {e}")
return records
@staticmethod
def get_session_checks(session_id: str) -> dict[str, Any]:
"""Checker material of one session for UI backfill: the append-only
verdict ledger (``check_ledger.jsonl``) and the machine-readable run
outcome (``run_outcome.json``), both written by
``artemis.graph.checkpoints``. Missing files mean "no checks ran".
verdict ledger (``check_ledger.jsonl``), the per-attempt transcripts of
the Checker's streamed reasoning (``check_streams.jsonl``: one record
per attempt with timestamped ``segments``) and the machine-readable run
outcome (``run_outcome.json``), all written by the Checker /
``artemis.graph.checkpoints``. Missing files mean "no checks ran"
(sessions recorded before transcripts existed simply have no streams).
"""
session_dir = TRACES_PATH / session_id
records: list[dict[str, Any]] = []
ledger_path = session_dir / "check_ledger.jsonl"
if ledger_path.is_file():
try:
for line in ledger_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
rec = json.loads(line)
except ValueError:
continue
if isinstance(rec, dict):
records.append(rec)
except (OSError, ValueError) as e:
logger.warning(f"Failed to read check ledger for {session_id}: {e}")
records = MediaService._read_jsonl_records(
session_dir / "check_ledger.jsonl", "check ledger", session_id
)
streams = MediaService._read_jsonl_records(
session_dir / "check_streams.jsonl", "check streams", session_id
)
run_outcome: dict[str, Any] | None = None
outcome_path = session_dir / "run_outcome.json"
@@ -570,7 +587,7 @@ class MediaService:
except (OSError, ValueError) as e:
logger.warning(f"Failed to read run outcome for {session_id}: {e}")
return {"records": records, "run_outcome": run_outcome}
return {"records": records, "streams": streams, "run_outcome": run_outcome}
@staticmethod
def get_session_notes_content(session_id: str) -> dict[str, str]: