loki-mode 5.41.0 → 5.42.1
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.
- package/SKILL.md +3 -3
- package/VERSION +1 -1
- package/autonomy/run.sh +33 -3
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +229 -32
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with zero human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v5.
|
|
6
|
+
# Loki Mode v5.42.1
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -258,8 +258,8 @@ The following features are documented in skill modules but not yet fully automat
|
|
|
258
258
|
|---------|--------|-------|
|
|
259
259
|
| PRE-ACT goal drift detection | Planned | Agent-level attention check before each action; no automated enforcement yet |
|
|
260
260
|
| CONTINUITY.md working memory | Implemented (v5.35.0) | Auto-managed by run.sh, updated each iteration |
|
|
261
|
-
| GitHub integration | Implemented (v5.
|
|
261
|
+
| GitHub integration | Implemented (v5.42.1) | Import, sync-back, PR creation, export. CLI: `loki github`, API: `/api/github/*` |
|
|
262
262
|
| Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
|
|
263
263
|
| Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
|
|
264
264
|
|
|
265
|
-
**v5.
|
|
265
|
+
**v5.42.1 | feat: GitHub sync-back, PR creation, export (fully wired) | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.
|
|
1
|
+
5.42.1
|
package/autonomy/run.sh
CHANGED
|
@@ -2705,6 +2705,14 @@ except: print('{\"total\":0,\"unacknowledged\":0}')
|
|
|
2705
2705
|
"council": $council_state,
|
|
2706
2706
|
"budget": $budget_json,
|
|
2707
2707
|
"context": $context_state,
|
|
2708
|
+
"tokens": $(python3 -c "
|
|
2709
|
+
import json
|
|
2710
|
+
try:
|
|
2711
|
+
t = json.load(open('.loki/context/tracking.json'))
|
|
2712
|
+
totals = t.get('totals', {})
|
|
2713
|
+
print(json.dumps({'input': totals.get('total_input', 0), 'output': totals.get('total_output', 0), 'cost_usd': totals.get('total_cost_usd', 0)}))
|
|
2714
|
+
except: print('null')
|
|
2715
|
+
" 2>/dev/null || echo "null"),
|
|
2708
2716
|
"notifications": $notification_summary
|
|
2709
2717
|
}
|
|
2710
2718
|
EOF
|
|
@@ -2857,6 +2865,9 @@ track_iteration_complete() {
|
|
|
2857
2865
|
--context "{\"iteration\":$iteration,\"exit_code\":$exit_code}"
|
|
2858
2866
|
fi
|
|
2859
2867
|
|
|
2868
|
+
# Track context window usage FIRST to get token data (v5.42.0)
|
|
2869
|
+
track_context_usage "$iteration"
|
|
2870
|
+
|
|
2860
2871
|
# Write efficiency tracking file for /api/cost endpoint
|
|
2861
2872
|
mkdir -p .loki/metrics/efficiency
|
|
2862
2873
|
local model_tier="sonnet"
|
|
@@ -2869,6 +2880,25 @@ track_iteration_complete() {
|
|
|
2869
2880
|
fi
|
|
2870
2881
|
local phase="${LAST_KNOWN_PHASE:-}"
|
|
2871
2882
|
[ -z "$phase" ] && phase=$(python3 -c "import json; print(json.load(open('.loki/state/orchestrator.json')).get('currentPhase', 'unknown'))" 2>/dev/null || echo "unknown")
|
|
2883
|
+
|
|
2884
|
+
# Read token data from context tracker output (v5.42.0)
|
|
2885
|
+
local iter_input=0 iter_output=0 iter_cost=0
|
|
2886
|
+
if [ -f ".loki/context/tracking.json" ]; then
|
|
2887
|
+
read iter_input iter_output iter_cost < <(python3 -c "
|
|
2888
|
+
import json
|
|
2889
|
+
try:
|
|
2890
|
+
t = json.load(open('.loki/context/tracking.json'))
|
|
2891
|
+
iters = t.get('per_iteration', [])
|
|
2892
|
+
match = [i for i in iters if i.get('iteration') == $iteration]
|
|
2893
|
+
if match:
|
|
2894
|
+
m = match[-1]
|
|
2895
|
+
print(m.get('input_tokens', 0), m.get('output_tokens', 0), m.get('cost_usd', 0))
|
|
2896
|
+
else:
|
|
2897
|
+
print(0, 0, 0)
|
|
2898
|
+
except: print(0, 0, 0)
|
|
2899
|
+
" 2>/dev/null || echo "0 0 0")
|
|
2900
|
+
fi
|
|
2901
|
+
|
|
2872
2902
|
cat > ".loki/metrics/efficiency/iteration-${iteration}.json" << EFF_EOF
|
|
2873
2903
|
{
|
|
2874
2904
|
"iteration": $iteration,
|
|
@@ -2877,13 +2907,13 @@ track_iteration_complete() {
|
|
|
2877
2907
|
"duration_ms": $duration_ms,
|
|
2878
2908
|
"provider": "${PROVIDER_NAME:-claude}",
|
|
2879
2909
|
"status": "$status_str",
|
|
2910
|
+
"input_tokens": ${iter_input:-0},
|
|
2911
|
+
"output_tokens": ${iter_output:-0},
|
|
2912
|
+
"cost_usd": ${iter_cost:-0},
|
|
2880
2913
|
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
2881
2914
|
}
|
|
2882
2915
|
EFF_EOF
|
|
2883
2916
|
|
|
2884
|
-
# Track context window usage (v5.40.0)
|
|
2885
|
-
track_context_usage "$iteration"
|
|
2886
|
-
|
|
2887
2917
|
# Check notification triggers (v5.40.0)
|
|
2888
2918
|
check_notification_triggers "$iteration"
|
|
2889
2919
|
|
package/dashboard/__init__.py
CHANGED
package/dashboard/server.py
CHANGED
|
@@ -259,14 +259,57 @@ manager = ConnectionManager()
|
|
|
259
259
|
start_time = datetime.now(timezone.utc)
|
|
260
260
|
|
|
261
261
|
|
|
262
|
+
async def _orphan_watchdog():
|
|
263
|
+
"""Background task that shuts down dashboard if the parent session dies.
|
|
264
|
+
|
|
265
|
+
When the session process is killed (SIGKILL), the cleanup trap never runs
|
|
266
|
+
and the dashboard is left orphaned. This watchdog checks the session PID
|
|
267
|
+
every 30 seconds and initiates shutdown if the session is gone.
|
|
268
|
+
"""
|
|
269
|
+
loki_dir = _get_loki_dir()
|
|
270
|
+
pid_file = loki_dir / "loki.pid"
|
|
271
|
+
# Wait 60s before first check to let session fully start
|
|
272
|
+
await asyncio.sleep(60)
|
|
273
|
+
while True:
|
|
274
|
+
try:
|
|
275
|
+
if pid_file.exists():
|
|
276
|
+
pid = int(pid_file.read_text().strip())
|
|
277
|
+
try:
|
|
278
|
+
os.kill(pid, 0) # Check if process exists
|
|
279
|
+
except OSError:
|
|
280
|
+
# Session PID is dead -- we're orphaned
|
|
281
|
+
logger.warning(
|
|
282
|
+
"Session PID %d is gone. Dashboard shutting down to avoid orphan.", pid
|
|
283
|
+
)
|
|
284
|
+
# Clean up our own PID file
|
|
285
|
+
dash_pid = loki_dir / "dashboard" / "dashboard.pid"
|
|
286
|
+
dash_pid.unlink(missing_ok=True)
|
|
287
|
+
# Give a moment for any in-flight requests
|
|
288
|
+
await asyncio.sleep(2)
|
|
289
|
+
os._exit(0)
|
|
290
|
+
# If PID file doesn't exist and we've been running >2 min, also shut down
|
|
291
|
+
elif time.time() - _dashboard_start_time > 120:
|
|
292
|
+
logger.warning("No session PID file found. Dashboard shutting down.")
|
|
293
|
+
os._exit(0)
|
|
294
|
+
except (ValueError, OSError):
|
|
295
|
+
pass
|
|
296
|
+
await asyncio.sleep(30)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
_dashboard_start_time = time.time()
|
|
300
|
+
|
|
301
|
+
|
|
262
302
|
@asynccontextmanager
|
|
263
303
|
async def lifespan(app: FastAPI):
|
|
264
304
|
"""Application lifespan handler."""
|
|
265
305
|
# Startup
|
|
266
306
|
await init_db()
|
|
267
307
|
_telemetry.send_telemetry("dashboard_start")
|
|
308
|
+
# Start orphan watchdog
|
|
309
|
+
watchdog_task = asyncio.create_task(_orphan_watchdog())
|
|
268
310
|
yield
|
|
269
311
|
# Shutdown
|
|
312
|
+
watchdog_task.cancel()
|
|
270
313
|
await close_db()
|
|
271
314
|
|
|
272
315
|
|
|
@@ -1500,22 +1543,61 @@ async def get_memory_timeline():
|
|
|
1500
1543
|
|
|
1501
1544
|
|
|
1502
1545
|
# Learning/metrics endpoints
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
def _read_learning_signals(signal_type: Optional[str] = None, limit: int = 50) -> list:
|
|
1549
|
+
"""Read learning signals from .loki/learning/signals/*.json files.
|
|
1550
|
+
|
|
1551
|
+
Learning signals are written as individual JSON files by the learning emitter
|
|
1552
|
+
(learning/emitter.py). Each file contains a single signal object with fields:
|
|
1553
|
+
id, type, source, action, timestamp, confidence, outcome, data, context.
|
|
1554
|
+
"""
|
|
1555
|
+
signals_dir = _get_loki_dir() / "learning" / "signals"
|
|
1556
|
+
if not signals_dir.exists() or not signals_dir.is_dir():
|
|
1557
|
+
return []
|
|
1558
|
+
|
|
1559
|
+
signals = []
|
|
1560
|
+
try:
|
|
1561
|
+
for fpath in signals_dir.glob("*.json"):
|
|
1562
|
+
try:
|
|
1563
|
+
raw = fpath.read_text()
|
|
1564
|
+
if not raw.strip():
|
|
1565
|
+
continue
|
|
1566
|
+
sig = json.loads(raw)
|
|
1567
|
+
if signal_type and sig.get("type") != signal_type:
|
|
1568
|
+
continue
|
|
1569
|
+
signals.append(sig)
|
|
1570
|
+
except (json.JSONDecodeError, OSError):
|
|
1571
|
+
continue
|
|
1572
|
+
except OSError:
|
|
1573
|
+
return []
|
|
1574
|
+
|
|
1575
|
+
# Sort by timestamp descending (newest first)
|
|
1576
|
+
signals.sort(key=lambda s: s.get("timestamp", ""), reverse=True)
|
|
1577
|
+
return signals[:limit]
|
|
1578
|
+
|
|
1579
|
+
|
|
1503
1580
|
@app.get("/api/learning/metrics")
|
|
1504
1581
|
async def get_learning_metrics(
|
|
1505
1582
|
timeRange: str = "7d",
|
|
1506
1583
|
signalType: Optional[str] = None,
|
|
1507
1584
|
source: Optional[str] = None,
|
|
1508
1585
|
):
|
|
1509
|
-
"""Get learning metrics from events and
|
|
1586
|
+
"""Get learning metrics from events, metrics files, and learning signals."""
|
|
1510
1587
|
events = _read_events(timeRange)
|
|
1511
1588
|
|
|
1589
|
+
# Also read from learning signals directory
|
|
1590
|
+
all_signals = _read_learning_signals(limit=10000)
|
|
1591
|
+
|
|
1512
1592
|
# Filter by type and source
|
|
1513
1593
|
if signalType:
|
|
1514
1594
|
events = [e for e in events if e.get("data", {}).get("type") == signalType]
|
|
1595
|
+
all_signals = [s for s in all_signals if s.get("type") == signalType]
|
|
1515
1596
|
if source:
|
|
1516
1597
|
events = [e for e in events if e.get("data", {}).get("source") == source]
|
|
1598
|
+
all_signals = [s for s in all_signals if s.get("source") == source]
|
|
1517
1599
|
|
|
1518
|
-
# Count by type
|
|
1600
|
+
# Count by type from events.jsonl
|
|
1519
1601
|
by_type: dict = {}
|
|
1520
1602
|
by_source: dict = {}
|
|
1521
1603
|
for e in events:
|
|
@@ -1524,6 +1606,19 @@ async def get_learning_metrics(
|
|
|
1524
1606
|
s = e.get("data", {}).get("source", "unknown")
|
|
1525
1607
|
by_source[s] = by_source.get(s, 0) + 1
|
|
1526
1608
|
|
|
1609
|
+
# Merge counts from learning signals directory
|
|
1610
|
+
for s in all_signals:
|
|
1611
|
+
t = s.get("type", "unknown")
|
|
1612
|
+
by_type[t] = by_type.get(t, 0) + 1
|
|
1613
|
+
src = s.get("source", "unknown")
|
|
1614
|
+
by_source[src] = by_source.get(src, 0) + 1
|
|
1615
|
+
|
|
1616
|
+
total_count = len(events) + len(all_signals)
|
|
1617
|
+
|
|
1618
|
+
# Calculate average confidence across both sources
|
|
1619
|
+
total_conf = sum(e.get("data", {}).get("confidence", 0) for e in events)
|
|
1620
|
+
total_conf += sum(s.get("confidence", 0) for s in all_signals)
|
|
1621
|
+
|
|
1527
1622
|
# Load aggregation data from file if available
|
|
1528
1623
|
aggregation = {
|
|
1529
1624
|
"preferences": [],
|
|
@@ -1543,10 +1638,10 @@ async def get_learning_metrics(
|
|
|
1543
1638
|
pass
|
|
1544
1639
|
|
|
1545
1640
|
return {
|
|
1546
|
-
"totalSignals":
|
|
1641
|
+
"totalSignals": total_count,
|
|
1547
1642
|
"signalsByType": by_type,
|
|
1548
1643
|
"signalsBySource": by_source,
|
|
1549
|
-
"avgConfidence": round(
|
|
1644
|
+
"avgConfidence": round(total_conf / max(total_count, 1), 4),
|
|
1550
1645
|
"aggregation": aggregation,
|
|
1551
1646
|
}
|
|
1552
1647
|
|
|
@@ -1579,25 +1674,107 @@ async def get_learning_signals(
|
|
|
1579
1674
|
limit: int = 50,
|
|
1580
1675
|
offset: int = 0,
|
|
1581
1676
|
):
|
|
1582
|
-
"""Get raw learning signals."""
|
|
1677
|
+
"""Get raw learning signals from both events.jsonl and learning signals directory."""
|
|
1583
1678
|
events = _read_events(timeRange)
|
|
1584
1679
|
if signalType:
|
|
1585
1680
|
events = [e for e in events if e.get("type") == signalType]
|
|
1586
1681
|
if source:
|
|
1587
1682
|
events = [e for e in events if e.get("data", {}).get("source") == source]
|
|
1588
|
-
|
|
1683
|
+
|
|
1684
|
+
# Also read from learning signals directory
|
|
1685
|
+
file_signals = _read_learning_signals(signal_type=signalType, limit=10000)
|
|
1686
|
+
if source:
|
|
1687
|
+
file_signals = [s for s in file_signals if s.get("source") == source]
|
|
1688
|
+
|
|
1689
|
+
# Merge and sort by timestamp (newest first)
|
|
1690
|
+
combined = events + file_signals
|
|
1691
|
+
combined.sort(key=lambda s: s.get("timestamp", ""), reverse=True)
|
|
1692
|
+
return combined[offset:offset + limit]
|
|
1589
1693
|
|
|
1590
1694
|
|
|
1591
1695
|
@app.get("/api/learning/aggregation")
|
|
1592
1696
|
async def get_learning_aggregation():
|
|
1593
|
-
"""Get latest learning aggregation result."""
|
|
1697
|
+
"""Get latest learning aggregation result, merging file-based aggregation with live signals."""
|
|
1698
|
+
result = {"preferences": [], "error_patterns": [], "success_patterns": [], "tool_efficiencies": []}
|
|
1699
|
+
|
|
1700
|
+
# Load pre-computed aggregation from file if available
|
|
1594
1701
|
agg_file = _get_loki_dir() / "metrics" / "aggregation.json"
|
|
1595
1702
|
if agg_file.exists():
|
|
1596
1703
|
try:
|
|
1597
|
-
|
|
1704
|
+
result = json.loads(agg_file.read_text())
|
|
1598
1705
|
except Exception:
|
|
1599
1706
|
pass
|
|
1600
|
-
|
|
1707
|
+
|
|
1708
|
+
# Supplement with live data from learning signals directory
|
|
1709
|
+
success_signals = _read_learning_signals(signal_type="success_pattern", limit=500)
|
|
1710
|
+
tool_signals = _read_learning_signals(signal_type="tool_efficiency", limit=500)
|
|
1711
|
+
error_signals = _read_learning_signals(signal_type="error_pattern", limit=500)
|
|
1712
|
+
pref_signals = _read_learning_signals(signal_type="user_preference", limit=500)
|
|
1713
|
+
|
|
1714
|
+
# Merge success patterns from signals if aggregation file had none
|
|
1715
|
+
if not result.get("success_patterns") and success_signals:
|
|
1716
|
+
pattern_counts: dict = {}
|
|
1717
|
+
for s in success_signals:
|
|
1718
|
+
name = s.get("data", {}).get("pattern_name", s.get("action", "unknown"))
|
|
1719
|
+
pattern_counts[name] = pattern_counts.get(name, 0) + 1
|
|
1720
|
+
result["success_patterns"] = [
|
|
1721
|
+
{"pattern_name": k, "frequency": v, "confidence": min(1.0, v / 10)}
|
|
1722
|
+
for k, v in sorted(pattern_counts.items(), key=lambda x: -x[1])
|
|
1723
|
+
]
|
|
1724
|
+
|
|
1725
|
+
# Merge tool efficiencies from signals if aggregation file had none
|
|
1726
|
+
if not result.get("tool_efficiencies") and tool_signals:
|
|
1727
|
+
tool_stats: dict = {}
|
|
1728
|
+
for s in tool_signals:
|
|
1729
|
+
data = s.get("data", {})
|
|
1730
|
+
tool_name = data.get("tool_name", s.get("action", "unknown"))
|
|
1731
|
+
if tool_name not in tool_stats:
|
|
1732
|
+
tool_stats[tool_name] = {"count": 0, "total_ms": 0, "successes": 0}
|
|
1733
|
+
tool_stats[tool_name]["count"] += 1
|
|
1734
|
+
tool_stats[tool_name]["total_ms"] += data.get("duration_ms", 0)
|
|
1735
|
+
if data.get("success", s.get("outcome") == "success"):
|
|
1736
|
+
tool_stats[tool_name]["successes"] += 1
|
|
1737
|
+
result["tool_efficiencies"] = []
|
|
1738
|
+
for tname, stats in sorted(tool_stats.items(), key=lambda x: -x[1]["count"]):
|
|
1739
|
+
avg_ms = stats["total_ms"] / stats["count"] if stats["count"] else 0
|
|
1740
|
+
sr = round(stats["successes"] / stats["count"], 4) if stats["count"] else 0
|
|
1741
|
+
result["tool_efficiencies"].append({
|
|
1742
|
+
"tool_name": tname, "efficiency_score": sr,
|
|
1743
|
+
"count": stats["count"], "avg_execution_time_ms": round(avg_ms, 2),
|
|
1744
|
+
"success_rate": sr,
|
|
1745
|
+
})
|
|
1746
|
+
|
|
1747
|
+
# Merge error patterns from signals if aggregation file had none
|
|
1748
|
+
if not result.get("error_patterns") and error_signals:
|
|
1749
|
+
error_counts: dict = {}
|
|
1750
|
+
for s in error_signals:
|
|
1751
|
+
etype = s.get("data", {}).get("error_type", s.get("action", "unknown"))
|
|
1752
|
+
error_counts[etype] = error_counts.get(etype, 0) + 1
|
|
1753
|
+
result["error_patterns"] = [
|
|
1754
|
+
{"error_type": k, "resolution_rate": 0.0, "frequency": v, "confidence": min(1.0, v / 10)}
|
|
1755
|
+
for k, v in sorted(error_counts.items(), key=lambda x: -x[1])
|
|
1756
|
+
]
|
|
1757
|
+
|
|
1758
|
+
# Merge preferences from signals if aggregation file had none
|
|
1759
|
+
if not result.get("preferences") and pref_signals:
|
|
1760
|
+
pref_counts: dict = {}
|
|
1761
|
+
for s in pref_signals:
|
|
1762
|
+
key = s.get("data", {}).get("preference_key", s.get("action", "unknown"))
|
|
1763
|
+
pref_counts[key] = pref_counts.get(key, 0) + 1
|
|
1764
|
+
result["preferences"] = [
|
|
1765
|
+
{"preference_key": k, "preferred_value": k, "frequency": v, "confidence": min(1.0, v / 10)}
|
|
1766
|
+
for k, v in sorted(pref_counts.items(), key=lambda x: -x[1])
|
|
1767
|
+
]
|
|
1768
|
+
|
|
1769
|
+
# Add signal counts summary
|
|
1770
|
+
result["signal_counts"] = {
|
|
1771
|
+
"success_patterns": len(success_signals),
|
|
1772
|
+
"tool_efficiency": len(tool_signals),
|
|
1773
|
+
"error_patterns": len(error_signals),
|
|
1774
|
+
"user_preferences": len(pref_signals),
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
return result
|
|
1601
1778
|
|
|
1602
1779
|
|
|
1603
1780
|
@app.post("/api/learning/aggregate", dependencies=[Depends(auth.require_scope("control"))])
|
|
@@ -1690,34 +1867,50 @@ async def trigger_aggregation():
|
|
|
1690
1867
|
|
|
1691
1868
|
@app.get("/api/learning/preferences")
|
|
1692
1869
|
async def get_learning_preferences(limit: int = 50):
|
|
1693
|
-
"""Get aggregated user preferences."""
|
|
1870
|
+
"""Get aggregated user preferences from events and learning signals directory."""
|
|
1694
1871
|
events = _read_events("30d")
|
|
1695
1872
|
prefs = [e for e in events if e.get("type") == "user_preference"]
|
|
1696
|
-
|
|
1873
|
+
# Also read from learning signals directory
|
|
1874
|
+
file_prefs = _read_learning_signals(signal_type="user_preference", limit=limit)
|
|
1875
|
+
combined = prefs + file_prefs
|
|
1876
|
+
combined.sort(key=lambda s: s.get("timestamp", ""), reverse=True)
|
|
1877
|
+
return combined[:limit]
|
|
1697
1878
|
|
|
1698
1879
|
|
|
1699
1880
|
@app.get("/api/learning/errors")
|
|
1700
1881
|
async def get_learning_errors(limit: int = 50):
|
|
1701
|
-
"""Get aggregated error patterns."""
|
|
1882
|
+
"""Get aggregated error patterns from events and learning signals directory."""
|
|
1702
1883
|
events = _read_events("30d")
|
|
1703
1884
|
errors = [e for e in events if e.get("type") == "error_pattern"]
|
|
1704
|
-
|
|
1885
|
+
# Also read from learning signals directory
|
|
1886
|
+
file_errors = _read_learning_signals(signal_type="error_pattern", limit=limit)
|
|
1887
|
+
combined = errors + file_errors
|
|
1888
|
+
combined.sort(key=lambda s: s.get("timestamp", ""), reverse=True)
|
|
1889
|
+
return combined[:limit]
|
|
1705
1890
|
|
|
1706
1891
|
|
|
1707
1892
|
@app.get("/api/learning/success")
|
|
1708
1893
|
async def get_learning_success(limit: int = 50):
|
|
1709
|
-
"""Get aggregated success patterns."""
|
|
1894
|
+
"""Get aggregated success patterns from events and learning signals directory."""
|
|
1710
1895
|
events = _read_events("30d")
|
|
1711
1896
|
successes = [e for e in events if e.get("type") == "success_pattern"]
|
|
1712
|
-
|
|
1897
|
+
# Also read from learning signals directory
|
|
1898
|
+
file_successes = _read_learning_signals(signal_type="success_pattern", limit=limit)
|
|
1899
|
+
combined = successes + file_successes
|
|
1900
|
+
combined.sort(key=lambda s: s.get("timestamp", ""), reverse=True)
|
|
1901
|
+
return combined[:limit]
|
|
1713
1902
|
|
|
1714
1903
|
|
|
1715
1904
|
@app.get("/api/learning/tools")
|
|
1716
1905
|
async def get_tool_efficiency(limit: int = 50):
|
|
1717
|
-
"""Get tool efficiency rankings."""
|
|
1906
|
+
"""Get tool efficiency rankings from events and learning signals directory."""
|
|
1718
1907
|
events = _read_events("30d")
|
|
1719
1908
|
tools = [e for e in events if e.get("type") == "tool_efficiency"]
|
|
1720
|
-
|
|
1909
|
+
# Also read from learning signals directory
|
|
1910
|
+
file_tools = _read_learning_signals(signal_type="tool_efficiency", limit=limit)
|
|
1911
|
+
combined = tools + file_tools
|
|
1912
|
+
combined.sort(key=lambda s: s.get("timestamp", ""), reverse=True)
|
|
1913
|
+
return combined[:limit]
|
|
1721
1914
|
|
|
1722
1915
|
|
|
1723
1916
|
def _parse_time_range(time_range: str) -> Optional[datetime]:
|
|
@@ -1957,24 +2150,28 @@ async def get_cost():
|
|
|
1957
2150
|
except (json.JSONDecodeError, KeyError, TypeError):
|
|
1958
2151
|
pass
|
|
1959
2152
|
|
|
1960
|
-
#
|
|
2153
|
+
# Fallback: read from context tracking if efficiency files have no token data
|
|
1961
2154
|
if total_input == 0 and total_output == 0:
|
|
1962
|
-
|
|
1963
|
-
if
|
|
2155
|
+
ctx_file = loki_dir / "context" / "tracking.json"
|
|
2156
|
+
if ctx_file.exists():
|
|
1964
2157
|
try:
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
total_input =
|
|
1968
|
-
total_output =
|
|
1969
|
-
model = state.get("model", "sonnet").lower()
|
|
2158
|
+
ctx = json.loads(ctx_file.read_text())
|
|
2159
|
+
totals = ctx.get("totals", {})
|
|
2160
|
+
total_input = totals.get("total_input", 0)
|
|
2161
|
+
total_output = totals.get("total_output", 0)
|
|
1970
2162
|
if total_input > 0 or total_output > 0:
|
|
1971
|
-
estimated_cost =
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2163
|
+
estimated_cost = totals.get("total_cost_usd", 0.0)
|
|
2164
|
+
# Rebuild by_model and by_phase from per_iteration data
|
|
2165
|
+
for it in ctx.get("per_iteration", []):
|
|
2166
|
+
inp = it.get("input_tokens", 0)
|
|
2167
|
+
out = it.get("output_tokens", 0)
|
|
2168
|
+
cost = it.get("cost_usd", 0)
|
|
2169
|
+
model = ctx.get("provider", "sonnet").lower()
|
|
2170
|
+
if model not in by_model:
|
|
2171
|
+
by_model[model] = {"input_tokens": 0, "output_tokens": 0, "cost_usd": 0.0}
|
|
2172
|
+
by_model[model]["input_tokens"] += inp
|
|
2173
|
+
by_model[model]["output_tokens"] += out
|
|
2174
|
+
by_model[model]["cost_usd"] += cost
|
|
1978
2175
|
except (json.JSONDecodeError, KeyError):
|
|
1979
2176
|
pass
|
|
1980
2177
|
|
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED