loki-mode 5.41.0 → 5.42.0
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 +186 -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.0
|
|
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.0) | 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.0 | feat: GitHub sync-back, PR creation, export (fully wired) | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.
|
|
1
|
+
5.42.0
|
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
|
@@ -1500,22 +1500,61 @@ async def get_memory_timeline():
|
|
|
1500
1500
|
|
|
1501
1501
|
|
|
1502
1502
|
# Learning/metrics endpoints
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
def _read_learning_signals(signal_type: Optional[str] = None, limit: int = 50) -> list:
|
|
1506
|
+
"""Read learning signals from .loki/learning/signals/*.json files.
|
|
1507
|
+
|
|
1508
|
+
Learning signals are written as individual JSON files by the learning emitter
|
|
1509
|
+
(learning/emitter.py). Each file contains a single signal object with fields:
|
|
1510
|
+
id, type, source, action, timestamp, confidence, outcome, data, context.
|
|
1511
|
+
"""
|
|
1512
|
+
signals_dir = _get_loki_dir() / "learning" / "signals"
|
|
1513
|
+
if not signals_dir.exists() or not signals_dir.is_dir():
|
|
1514
|
+
return []
|
|
1515
|
+
|
|
1516
|
+
signals = []
|
|
1517
|
+
try:
|
|
1518
|
+
for fpath in signals_dir.glob("*.json"):
|
|
1519
|
+
try:
|
|
1520
|
+
raw = fpath.read_text()
|
|
1521
|
+
if not raw.strip():
|
|
1522
|
+
continue
|
|
1523
|
+
sig = json.loads(raw)
|
|
1524
|
+
if signal_type and sig.get("type") != signal_type:
|
|
1525
|
+
continue
|
|
1526
|
+
signals.append(sig)
|
|
1527
|
+
except (json.JSONDecodeError, OSError):
|
|
1528
|
+
continue
|
|
1529
|
+
except OSError:
|
|
1530
|
+
return []
|
|
1531
|
+
|
|
1532
|
+
# Sort by timestamp descending (newest first)
|
|
1533
|
+
signals.sort(key=lambda s: s.get("timestamp", ""), reverse=True)
|
|
1534
|
+
return signals[:limit]
|
|
1535
|
+
|
|
1536
|
+
|
|
1503
1537
|
@app.get("/api/learning/metrics")
|
|
1504
1538
|
async def get_learning_metrics(
|
|
1505
1539
|
timeRange: str = "7d",
|
|
1506
1540
|
signalType: Optional[str] = None,
|
|
1507
1541
|
source: Optional[str] = None,
|
|
1508
1542
|
):
|
|
1509
|
-
"""Get learning metrics from events and
|
|
1543
|
+
"""Get learning metrics from events, metrics files, and learning signals."""
|
|
1510
1544
|
events = _read_events(timeRange)
|
|
1511
1545
|
|
|
1546
|
+
# Also read from learning signals directory
|
|
1547
|
+
all_signals = _read_learning_signals(limit=10000)
|
|
1548
|
+
|
|
1512
1549
|
# Filter by type and source
|
|
1513
1550
|
if signalType:
|
|
1514
1551
|
events = [e for e in events if e.get("data", {}).get("type") == signalType]
|
|
1552
|
+
all_signals = [s for s in all_signals if s.get("type") == signalType]
|
|
1515
1553
|
if source:
|
|
1516
1554
|
events = [e for e in events if e.get("data", {}).get("source") == source]
|
|
1555
|
+
all_signals = [s for s in all_signals if s.get("source") == source]
|
|
1517
1556
|
|
|
1518
|
-
# Count by type
|
|
1557
|
+
# Count by type from events.jsonl
|
|
1519
1558
|
by_type: dict = {}
|
|
1520
1559
|
by_source: dict = {}
|
|
1521
1560
|
for e in events:
|
|
@@ -1524,6 +1563,19 @@ async def get_learning_metrics(
|
|
|
1524
1563
|
s = e.get("data", {}).get("source", "unknown")
|
|
1525
1564
|
by_source[s] = by_source.get(s, 0) + 1
|
|
1526
1565
|
|
|
1566
|
+
# Merge counts from learning signals directory
|
|
1567
|
+
for s in all_signals:
|
|
1568
|
+
t = s.get("type", "unknown")
|
|
1569
|
+
by_type[t] = by_type.get(t, 0) + 1
|
|
1570
|
+
src = s.get("source", "unknown")
|
|
1571
|
+
by_source[src] = by_source.get(src, 0) + 1
|
|
1572
|
+
|
|
1573
|
+
total_count = len(events) + len(all_signals)
|
|
1574
|
+
|
|
1575
|
+
# Calculate average confidence across both sources
|
|
1576
|
+
total_conf = sum(e.get("data", {}).get("confidence", 0) for e in events)
|
|
1577
|
+
total_conf += sum(s.get("confidence", 0) for s in all_signals)
|
|
1578
|
+
|
|
1527
1579
|
# Load aggregation data from file if available
|
|
1528
1580
|
aggregation = {
|
|
1529
1581
|
"preferences": [],
|
|
@@ -1543,10 +1595,10 @@ async def get_learning_metrics(
|
|
|
1543
1595
|
pass
|
|
1544
1596
|
|
|
1545
1597
|
return {
|
|
1546
|
-
"totalSignals":
|
|
1598
|
+
"totalSignals": total_count,
|
|
1547
1599
|
"signalsByType": by_type,
|
|
1548
1600
|
"signalsBySource": by_source,
|
|
1549
|
-
"avgConfidence": round(
|
|
1601
|
+
"avgConfidence": round(total_conf / max(total_count, 1), 4),
|
|
1550
1602
|
"aggregation": aggregation,
|
|
1551
1603
|
}
|
|
1552
1604
|
|
|
@@ -1579,25 +1631,107 @@ async def get_learning_signals(
|
|
|
1579
1631
|
limit: int = 50,
|
|
1580
1632
|
offset: int = 0,
|
|
1581
1633
|
):
|
|
1582
|
-
"""Get raw learning signals."""
|
|
1634
|
+
"""Get raw learning signals from both events.jsonl and learning signals directory."""
|
|
1583
1635
|
events = _read_events(timeRange)
|
|
1584
1636
|
if signalType:
|
|
1585
1637
|
events = [e for e in events if e.get("type") == signalType]
|
|
1586
1638
|
if source:
|
|
1587
1639
|
events = [e for e in events if e.get("data", {}).get("source") == source]
|
|
1588
|
-
|
|
1640
|
+
|
|
1641
|
+
# Also read from learning signals directory
|
|
1642
|
+
file_signals = _read_learning_signals(signal_type=signalType, limit=10000)
|
|
1643
|
+
if source:
|
|
1644
|
+
file_signals = [s for s in file_signals if s.get("source") == source]
|
|
1645
|
+
|
|
1646
|
+
# Merge and sort by timestamp (newest first)
|
|
1647
|
+
combined = events + file_signals
|
|
1648
|
+
combined.sort(key=lambda s: s.get("timestamp", ""), reverse=True)
|
|
1649
|
+
return combined[offset:offset + limit]
|
|
1589
1650
|
|
|
1590
1651
|
|
|
1591
1652
|
@app.get("/api/learning/aggregation")
|
|
1592
1653
|
async def get_learning_aggregation():
|
|
1593
|
-
"""Get latest learning aggregation result."""
|
|
1654
|
+
"""Get latest learning aggregation result, merging file-based aggregation with live signals."""
|
|
1655
|
+
result = {"preferences": [], "error_patterns": [], "success_patterns": [], "tool_efficiencies": []}
|
|
1656
|
+
|
|
1657
|
+
# Load pre-computed aggregation from file if available
|
|
1594
1658
|
agg_file = _get_loki_dir() / "metrics" / "aggregation.json"
|
|
1595
1659
|
if agg_file.exists():
|
|
1596
1660
|
try:
|
|
1597
|
-
|
|
1661
|
+
result = json.loads(agg_file.read_text())
|
|
1598
1662
|
except Exception:
|
|
1599
1663
|
pass
|
|
1600
|
-
|
|
1664
|
+
|
|
1665
|
+
# Supplement with live data from learning signals directory
|
|
1666
|
+
success_signals = _read_learning_signals(signal_type="success_pattern", limit=500)
|
|
1667
|
+
tool_signals = _read_learning_signals(signal_type="tool_efficiency", limit=500)
|
|
1668
|
+
error_signals = _read_learning_signals(signal_type="error_pattern", limit=500)
|
|
1669
|
+
pref_signals = _read_learning_signals(signal_type="user_preference", limit=500)
|
|
1670
|
+
|
|
1671
|
+
# Merge success patterns from signals if aggregation file had none
|
|
1672
|
+
if not result.get("success_patterns") and success_signals:
|
|
1673
|
+
pattern_counts: dict = {}
|
|
1674
|
+
for s in success_signals:
|
|
1675
|
+
name = s.get("data", {}).get("pattern_name", s.get("action", "unknown"))
|
|
1676
|
+
pattern_counts[name] = pattern_counts.get(name, 0) + 1
|
|
1677
|
+
result["success_patterns"] = [
|
|
1678
|
+
{"pattern_name": k, "frequency": v, "confidence": min(1.0, v / 10)}
|
|
1679
|
+
for k, v in sorted(pattern_counts.items(), key=lambda x: -x[1])
|
|
1680
|
+
]
|
|
1681
|
+
|
|
1682
|
+
# Merge tool efficiencies from signals if aggregation file had none
|
|
1683
|
+
if not result.get("tool_efficiencies") and tool_signals:
|
|
1684
|
+
tool_stats: dict = {}
|
|
1685
|
+
for s in tool_signals:
|
|
1686
|
+
data = s.get("data", {})
|
|
1687
|
+
tool_name = data.get("tool_name", s.get("action", "unknown"))
|
|
1688
|
+
if tool_name not in tool_stats:
|
|
1689
|
+
tool_stats[tool_name] = {"count": 0, "total_ms": 0, "successes": 0}
|
|
1690
|
+
tool_stats[tool_name]["count"] += 1
|
|
1691
|
+
tool_stats[tool_name]["total_ms"] += data.get("duration_ms", 0)
|
|
1692
|
+
if data.get("success", s.get("outcome") == "success"):
|
|
1693
|
+
tool_stats[tool_name]["successes"] += 1
|
|
1694
|
+
result["tool_efficiencies"] = []
|
|
1695
|
+
for tname, stats in sorted(tool_stats.items(), key=lambda x: -x[1]["count"]):
|
|
1696
|
+
avg_ms = stats["total_ms"] / stats["count"] if stats["count"] else 0
|
|
1697
|
+
sr = round(stats["successes"] / stats["count"], 4) if stats["count"] else 0
|
|
1698
|
+
result["tool_efficiencies"].append({
|
|
1699
|
+
"tool_name": tname, "efficiency_score": sr,
|
|
1700
|
+
"count": stats["count"], "avg_execution_time_ms": round(avg_ms, 2),
|
|
1701
|
+
"success_rate": sr,
|
|
1702
|
+
})
|
|
1703
|
+
|
|
1704
|
+
# Merge error patterns from signals if aggregation file had none
|
|
1705
|
+
if not result.get("error_patterns") and error_signals:
|
|
1706
|
+
error_counts: dict = {}
|
|
1707
|
+
for s in error_signals:
|
|
1708
|
+
etype = s.get("data", {}).get("error_type", s.get("action", "unknown"))
|
|
1709
|
+
error_counts[etype] = error_counts.get(etype, 0) + 1
|
|
1710
|
+
result["error_patterns"] = [
|
|
1711
|
+
{"error_type": k, "resolution_rate": 0.0, "frequency": v, "confidence": min(1.0, v / 10)}
|
|
1712
|
+
for k, v in sorted(error_counts.items(), key=lambda x: -x[1])
|
|
1713
|
+
]
|
|
1714
|
+
|
|
1715
|
+
# Merge preferences from signals if aggregation file had none
|
|
1716
|
+
if not result.get("preferences") and pref_signals:
|
|
1717
|
+
pref_counts: dict = {}
|
|
1718
|
+
for s in pref_signals:
|
|
1719
|
+
key = s.get("data", {}).get("preference_key", s.get("action", "unknown"))
|
|
1720
|
+
pref_counts[key] = pref_counts.get(key, 0) + 1
|
|
1721
|
+
result["preferences"] = [
|
|
1722
|
+
{"preference_key": k, "preferred_value": k, "frequency": v, "confidence": min(1.0, v / 10)}
|
|
1723
|
+
for k, v in sorted(pref_counts.items(), key=lambda x: -x[1])
|
|
1724
|
+
]
|
|
1725
|
+
|
|
1726
|
+
# Add signal counts summary
|
|
1727
|
+
result["signal_counts"] = {
|
|
1728
|
+
"success_patterns": len(success_signals),
|
|
1729
|
+
"tool_efficiency": len(tool_signals),
|
|
1730
|
+
"error_patterns": len(error_signals),
|
|
1731
|
+
"user_preferences": len(pref_signals),
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
return result
|
|
1601
1735
|
|
|
1602
1736
|
|
|
1603
1737
|
@app.post("/api/learning/aggregate", dependencies=[Depends(auth.require_scope("control"))])
|
|
@@ -1690,34 +1824,50 @@ async def trigger_aggregation():
|
|
|
1690
1824
|
|
|
1691
1825
|
@app.get("/api/learning/preferences")
|
|
1692
1826
|
async def get_learning_preferences(limit: int = 50):
|
|
1693
|
-
"""Get aggregated user preferences."""
|
|
1827
|
+
"""Get aggregated user preferences from events and learning signals directory."""
|
|
1694
1828
|
events = _read_events("30d")
|
|
1695
1829
|
prefs = [e for e in events if e.get("type") == "user_preference"]
|
|
1696
|
-
|
|
1830
|
+
# Also read from learning signals directory
|
|
1831
|
+
file_prefs = _read_learning_signals(signal_type="user_preference", limit=limit)
|
|
1832
|
+
combined = prefs + file_prefs
|
|
1833
|
+
combined.sort(key=lambda s: s.get("timestamp", ""), reverse=True)
|
|
1834
|
+
return combined[:limit]
|
|
1697
1835
|
|
|
1698
1836
|
|
|
1699
1837
|
@app.get("/api/learning/errors")
|
|
1700
1838
|
async def get_learning_errors(limit: int = 50):
|
|
1701
|
-
"""Get aggregated error patterns."""
|
|
1839
|
+
"""Get aggregated error patterns from events and learning signals directory."""
|
|
1702
1840
|
events = _read_events("30d")
|
|
1703
1841
|
errors = [e for e in events if e.get("type") == "error_pattern"]
|
|
1704
|
-
|
|
1842
|
+
# Also read from learning signals directory
|
|
1843
|
+
file_errors = _read_learning_signals(signal_type="error_pattern", limit=limit)
|
|
1844
|
+
combined = errors + file_errors
|
|
1845
|
+
combined.sort(key=lambda s: s.get("timestamp", ""), reverse=True)
|
|
1846
|
+
return combined[:limit]
|
|
1705
1847
|
|
|
1706
1848
|
|
|
1707
1849
|
@app.get("/api/learning/success")
|
|
1708
1850
|
async def get_learning_success(limit: int = 50):
|
|
1709
|
-
"""Get aggregated success patterns."""
|
|
1851
|
+
"""Get aggregated success patterns from events and learning signals directory."""
|
|
1710
1852
|
events = _read_events("30d")
|
|
1711
1853
|
successes = [e for e in events if e.get("type") == "success_pattern"]
|
|
1712
|
-
|
|
1854
|
+
# Also read from learning signals directory
|
|
1855
|
+
file_successes = _read_learning_signals(signal_type="success_pattern", limit=limit)
|
|
1856
|
+
combined = successes + file_successes
|
|
1857
|
+
combined.sort(key=lambda s: s.get("timestamp", ""), reverse=True)
|
|
1858
|
+
return combined[:limit]
|
|
1713
1859
|
|
|
1714
1860
|
|
|
1715
1861
|
@app.get("/api/learning/tools")
|
|
1716
1862
|
async def get_tool_efficiency(limit: int = 50):
|
|
1717
|
-
"""Get tool efficiency rankings."""
|
|
1863
|
+
"""Get tool efficiency rankings from events and learning signals directory."""
|
|
1718
1864
|
events = _read_events("30d")
|
|
1719
1865
|
tools = [e for e in events if e.get("type") == "tool_efficiency"]
|
|
1720
|
-
|
|
1866
|
+
# Also read from learning signals directory
|
|
1867
|
+
file_tools = _read_learning_signals(signal_type="tool_efficiency", limit=limit)
|
|
1868
|
+
combined = tools + file_tools
|
|
1869
|
+
combined.sort(key=lambda s: s.get("timestamp", ""), reverse=True)
|
|
1870
|
+
return combined[:limit]
|
|
1721
1871
|
|
|
1722
1872
|
|
|
1723
1873
|
def _parse_time_range(time_range: str) -> Optional[datetime]:
|
|
@@ -1957,24 +2107,28 @@ async def get_cost():
|
|
|
1957
2107
|
except (json.JSONDecodeError, KeyError, TypeError):
|
|
1958
2108
|
pass
|
|
1959
2109
|
|
|
1960
|
-
#
|
|
2110
|
+
# Fallback: read from context tracking if efficiency files have no token data
|
|
1961
2111
|
if total_input == 0 and total_output == 0:
|
|
1962
|
-
|
|
1963
|
-
if
|
|
2112
|
+
ctx_file = loki_dir / "context" / "tracking.json"
|
|
2113
|
+
if ctx_file.exists():
|
|
1964
2114
|
try:
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
total_input =
|
|
1968
|
-
total_output =
|
|
1969
|
-
model = state.get("model", "sonnet").lower()
|
|
2115
|
+
ctx = json.loads(ctx_file.read_text())
|
|
2116
|
+
totals = ctx.get("totals", {})
|
|
2117
|
+
total_input = totals.get("total_input", 0)
|
|
2118
|
+
total_output = totals.get("total_output", 0)
|
|
1970
2119
|
if total_input > 0 or total_output > 0:
|
|
1971
|
-
estimated_cost =
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2120
|
+
estimated_cost = totals.get("total_cost_usd", 0.0)
|
|
2121
|
+
# Rebuild by_model and by_phase from per_iteration data
|
|
2122
|
+
for it in ctx.get("per_iteration", []):
|
|
2123
|
+
inp = it.get("input_tokens", 0)
|
|
2124
|
+
out = it.get("output_tokens", 0)
|
|
2125
|
+
cost = it.get("cost_usd", 0)
|
|
2126
|
+
model = ctx.get("provider", "sonnet").lower()
|
|
2127
|
+
if model not in by_model:
|
|
2128
|
+
by_model[model] = {"input_tokens": 0, "output_tokens": 0, "cost_usd": 0.0}
|
|
2129
|
+
by_model[model]["input_tokens"] += inp
|
|
2130
|
+
by_model[model]["output_tokens"] += out
|
|
2131
|
+
by_model[model]["cost_usd"] += cost
|
|
1978
2132
|
except (json.JSONDecodeError, KeyError):
|
|
1979
2133
|
pass
|
|
1980
2134
|
|
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED