loki-mode 9.8.0 → 9.12.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/README.md +19 -14
- package/SKILL.md +3 -2
- package/VERSION +1 -1
- package/autonomy/loki +122 -1
- package/autonomy/run.sh +49 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_evidence.py +411 -0
- package/dashboard/api_operator.py +283 -0
- package/dashboard/api_phases.py +262 -0
- package/dashboard/api_releases.py +242 -0
- package/dashboard/api_runs.py +477 -0
- package/dashboard/api_tests.py +444 -0
- package/dashboard/api_v2.py +47 -1
- package/dashboard/server.py +54 -0
- package/dashboard/static/index.html +246 -135
- package/docs/ARCHITECTURE-OVERVIEW.md +5 -3
- package/docs/CAPABILITY-BACKLOG.md +53 -0
- package/docs/COMPARISON.md +2 -2
- package/docs/COMPETITIVE-ANALYSIS.md +1 -1
- package/docs/COMPETITIVE-SCORECARD.md +422 -0
- package/docs/DASHBOARD-9.12-EVIDENCE.md +97 -0
- package/docs/DASHBOARD-ARCHITECTURE.md +423 -0
- package/docs/DEMOS.md +21 -23
- package/docs/HANDOFF-2026-08-03.md +439 -0
- package/docs/INSTALLATION.md +17 -10
- package/docs/OUTCOME-FRONTIER.md +536 -0
- package/docs/PROMPT-ABLATION-RESULT.md +97 -0
- package/docs/TOOLS.md +800 -0
- package/docs/alternative-installations.md +2 -3
- package/docs/audit-logging.md +44 -35
- package/docs/authentication.md +13 -2
- package/docs/authorization.md +87 -81
- package/docs/git-workflow.md +6 -3
- package/docs/metrics.md +15 -16
- package/docs/network-security.md +16 -13
- package/docs/openclaw-integration.md +36 -556
- package/docs/show-hn-post.md +2 -2
- package/docs/siem-integration.md +39 -36
- package/loki-ts/dist/loki.js +18 -18
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/references/confidence-routing.md +18 -1
- package/references/invariant-checks.md +13 -8
- package/references/magic-rarv-integration.md +0 -1
- package/references/multi-provider.md +27 -5
- package/skills/healing.md +4 -2
- package/tools/audit-docs.py +488 -0
- package/tools/baseline-pin.py +19 -1
- package/tools/calibration-audit.py +523 -0
- package/tools/ci-gate.py +19 -1
- package/tools/cost-forecast.py +344 -0
- package/tools/cost-guard.py +19 -1
- package/tools/cost-history.py +19 -1
- package/tools/cost-per-outcome.py +394 -0
- package/tools/estimate-run.py +19 -1
- package/tools/evidence-freshness.py +307 -0
- package/tools/gate-init.py +19 -1
- package/tools/gate-report.py +19 -1
- package/tools/gate-simulate.py +570 -0
- package/tools/gate-trend.py +354 -0
- package/tools/model-advisor.py +52 -1
- package/tools/policy-load.py +19 -1
- package/tools/prompt-cost.py +363 -0
- package/tools/prompt-diff.py +448 -0
- package/tools/prompt-lint.py +448 -0
- package/tools/receipt-bundle.py +72 -2
- package/tools/receipt-diff.py +19 -1
- package/tools/receipt-find.py +19 -1
- package/tools/receipt-stats.py +380 -0
- package/tools/receipt-timeline.py +478 -0
- package/tools/receipt-verify-batch.py +291 -0
- package/tools/run-replay.py +19 -1
- package/tools/signing-status.py +19 -1
- package/tools/token-guard.py +19 -1
- package/tools/token-tax.py +375 -0
- package/tools/tool-index.py +19 -1
- package/tools/verification-tax.py +277 -0
- package/tools/verify-chain.py +361 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""Read-only "runs" view over the filesystem .loki/ directory.
|
|
2
|
+
|
|
3
|
+
NOT dashboard/runs.py. That module is the SQLAlchemy Run/RunEvent control
|
|
4
|
+
plane (a database row per run, created by the dashboard). This module reads
|
|
5
|
+
the engine's own on-disk state under .loki/ -- what the runner actually wrote
|
|
6
|
+
-- and never touches a database. Same word, two different objects; do not
|
|
7
|
+
merge them.
|
|
8
|
+
|
|
9
|
+
WHY THIS EXISTS. The dashboard's primary object is a run, and it had zero
|
|
10
|
+
routes able to list one from engine state. These are pure functions taking a
|
|
11
|
+
loki_dir, so they are testable without FastAPI and can be mounted by whoever
|
|
12
|
+
owns server.py.
|
|
13
|
+
|
|
14
|
+
THE SOURCES, and what each can and cannot tell us:
|
|
15
|
+
|
|
16
|
+
.loki/metrics/trust-events.jsonl append-only, one record per trust event,
|
|
17
|
+
each carrying run_id (minted per run as run-<ts>-<pid>-<rand> by
|
|
18
|
+
_loki_trust_run_id in autonomy/run.sh). THE ONLY source that survives
|
|
19
|
+
across runs, and therefore the only reason list_runs can be plural.
|
|
20
|
+
.loki/state/trust-run-id the CURRENT run's minted id.
|
|
21
|
+
.loki/metrics/efficiency/iteration-*.json per-iteration cost and tokens.
|
|
22
|
+
WIPED at run start (autonomy/run.sh:6212), so these describe the current
|
|
23
|
+
run ONLY. A historical run's row therefore reports cost UNKNOWN -- not
|
|
24
|
+
zero -- because the evidence was deleted, which is a real absence of
|
|
25
|
+
measurement and must read as one.
|
|
26
|
+
.loki/state/completion.json terminal outcome of the last run.
|
|
27
|
+
.loki/loki-run.json run manifest (schema loki-run-manifest/v1).
|
|
28
|
+
.loki/PAUSE, .loki/STOP, .loki/session.json live status signals.
|
|
29
|
+
|
|
30
|
+
THE HONESTY RULE. An unmeasured cost is None, never 0.0. That predicate is
|
|
31
|
+
record_is_measured() in autonomy/lib/efficiency_cost.py and it is IMPORTED
|
|
32
|
+
here, not restated. Its own docstring explains why: "A second copy of this
|
|
33
|
+
predicate is how the honesty rule drifts: the four surfaces that once rendered
|
|
34
|
+
an unmeasured run as $0.00 each had their own idea of what counted as
|
|
35
|
+
measured." dashboard/server.py keeps a deliberate mirror; this module does not
|
|
36
|
+
add a third. If autonomy/lib is unreachable the cost fields read UNKNOWN and
|
|
37
|
+
the envelope says so -- a degraded read never invents a number.
|
|
38
|
+
|
|
39
|
+
Every returned envelope states `source` (the real paths read) and
|
|
40
|
+
`freshness_s` (age in seconds of the newest file that actually contributed,
|
|
41
|
+
None when nothing did), and carries an explicit `reason` when empty.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import json
|
|
47
|
+
import os
|
|
48
|
+
import time
|
|
49
|
+
from typing import Any, Optional
|
|
50
|
+
|
|
51
|
+
__all__ = ["list_runs", "get_run", "UNKNOWN"]
|
|
52
|
+
|
|
53
|
+
# What an unmeasured value reads as. Kept as a name so callers can render it
|
|
54
|
+
# without hardcoding None-means-unknown at each call site.
|
|
55
|
+
UNKNOWN = None
|
|
56
|
+
|
|
57
|
+
_RUN_ID_FILE = ("state", "trust-run-id")
|
|
58
|
+
_TRUST_EVENTS = ("metrics", "trust-events.jsonl")
|
|
59
|
+
_EFFICIENCY_DIR = ("metrics", "efficiency")
|
|
60
|
+
_COMPLETION = ("state", "completion.json")
|
|
61
|
+
_MANIFEST = "loki-run.json"
|
|
62
|
+
_SESSION = "session.json"
|
|
63
|
+
|
|
64
|
+
# Declared for the envelope so a caller can see exactly what was read, in the
|
|
65
|
+
# style already used at autonomy/loki:21677 (a real path, not a label).
|
|
66
|
+
_SOURCE_PATHS = (
|
|
67
|
+
".loki/metrics/trust-events.jsonl",
|
|
68
|
+
".loki/state/trust-run-id",
|
|
69
|
+
".loki/metrics/efficiency/iteration-*.json",
|
|
70
|
+
".loki/state/completion.json",
|
|
71
|
+
".loki/loki-run.json",
|
|
72
|
+
".loki/session.json",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# tiny io helpers -- every one of them returns a default rather than raising,
|
|
78
|
+
# because a dashboard read must degrade, never 500 on a missing file.
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def _p(loki_dir: str, *parts: str) -> str:
|
|
82
|
+
return os.path.join(loki_dir, *parts)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _read_json(path: str, default: Any = None) -> Any:
|
|
86
|
+
try:
|
|
87
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
88
|
+
return json.load(fh)
|
|
89
|
+
except Exception:
|
|
90
|
+
return default
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _read_text(path: str) -> str:
|
|
94
|
+
try:
|
|
95
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
96
|
+
return fh.read().strip()
|
|
97
|
+
except Exception:
|
|
98
|
+
return ""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _mtime(path: str) -> Optional[float]:
|
|
102
|
+
try:
|
|
103
|
+
return os.path.getmtime(path)
|
|
104
|
+
except OSError:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _freshness(mtimes: list, now: Optional[float] = None) -> Optional[int]:
|
|
109
|
+
"""Age in seconds of the NEWEST file that contributed. None if none did.
|
|
110
|
+
|
|
111
|
+
None is load-bearing: "no file contributed" is not "contributed zero
|
|
112
|
+
seconds ago". Clamped at 0 because a file written during this call (or by
|
|
113
|
+
a clock that stepped) must not report a negative age.
|
|
114
|
+
"""
|
|
115
|
+
real = [m for m in mtimes if m is not None]
|
|
116
|
+
if not real:
|
|
117
|
+
return None
|
|
118
|
+
return max(0, int((now if now is not None else time.time()) - max(real)))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# the canonical measured-ness predicate, imported not copied
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
_EFF_MOD: Any = None
|
|
126
|
+
_EFF_TRIED = False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _efficiency_module():
|
|
130
|
+
"""Load autonomy/lib/efficiency_cost.py by path. None if unreachable.
|
|
131
|
+
|
|
132
|
+
autonomy/lib is not an importable package (proof-generator.py has a hyphen
|
|
133
|
+
in its name), so server.py already loads siblings this way -- see
|
|
134
|
+
_trust_module() at dashboard/server.py:7967. Cached, including the failure,
|
|
135
|
+
so a missing file costs one stat per process rather than one per request.
|
|
136
|
+
"""
|
|
137
|
+
global _EFF_MOD, _EFF_TRIED
|
|
138
|
+
if _EFF_TRIED:
|
|
139
|
+
return _EFF_MOD
|
|
140
|
+
_EFF_TRIED = True
|
|
141
|
+
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
142
|
+
mod_path = os.path.join(repo_root, "autonomy", "lib", "efficiency_cost.py")
|
|
143
|
+
try:
|
|
144
|
+
import importlib.util as ilu
|
|
145
|
+
spec = ilu.spec_from_file_location("loki_efficiency_cost", mod_path)
|
|
146
|
+
if spec is None or spec.loader is None:
|
|
147
|
+
return None
|
|
148
|
+
mod = ilu.module_from_spec(spec)
|
|
149
|
+
spec.loader.exec_module(mod)
|
|
150
|
+
_EFF_MOD = mod
|
|
151
|
+
except Exception:
|
|
152
|
+
_EFF_MOD = None
|
|
153
|
+
return _EFF_MOD
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# per-iteration cost, from the current run's efficiency records
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def _iteration_records(loki_dir: str) -> list:
|
|
161
|
+
"""[(iteration_int, record_dict, path)] sorted by iteration.
|
|
162
|
+
|
|
163
|
+
Sorted NUMERICALLY. A lexical sort puts iteration-10 before iteration-2,
|
|
164
|
+
which silently misorders any run past nine iterations.
|
|
165
|
+
"""
|
|
166
|
+
eff_dir = _p(loki_dir, *_EFFICIENCY_DIR)
|
|
167
|
+
try:
|
|
168
|
+
names = os.listdir(eff_dir)
|
|
169
|
+
except OSError:
|
|
170
|
+
return []
|
|
171
|
+
out = []
|
|
172
|
+
for name in names:
|
|
173
|
+
if not (name.startswith("iteration-") and name.endswith(".json")):
|
|
174
|
+
continue
|
|
175
|
+
path = os.path.join(eff_dir, name)
|
|
176
|
+
rec = _read_json(path)
|
|
177
|
+
if not isinstance(rec, dict):
|
|
178
|
+
continue
|
|
179
|
+
try:
|
|
180
|
+
num = int(name[len("iteration-"):-len(".json")])
|
|
181
|
+
except ValueError:
|
|
182
|
+
continue
|
|
183
|
+
out.append((num, rec, path))
|
|
184
|
+
out.sort(key=lambda t: t[0])
|
|
185
|
+
return out
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _cost_for_current_run(loki_dir: str):
|
|
189
|
+
"""(cost_usd, measured, note) for the run owning the efficiency dir.
|
|
190
|
+
|
|
191
|
+
cost_usd is None whenever we did not measure -- no records, all-zero
|
|
192
|
+
records, or the canonical module being unreachable. A genuine measured
|
|
193
|
+
zero (records that carry data and sum to 0.0) stays 0.0; that distinction
|
|
194
|
+
is the whole point of the predicate and is why collect_efficiency is
|
|
195
|
+
called rather than re-summed here.
|
|
196
|
+
"""
|
|
197
|
+
mod = _efficiency_module()
|
|
198
|
+
if mod is None:
|
|
199
|
+
return UNKNOWN, False, "efficiency_cost module unreachable"
|
|
200
|
+
try:
|
|
201
|
+
cost, _model = mod.collect_efficiency(loki_dir)
|
|
202
|
+
except Exception as exc:
|
|
203
|
+
return UNKNOWN, False, "efficiency read failed: %s" % (exc,)
|
|
204
|
+
if not isinstance(cost, dict) or not cost.get("available"):
|
|
205
|
+
return UNKNOWN, False, "no measured efficiency record"
|
|
206
|
+
return cost.get("usd"), True, None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
# status
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
def _current_status(loki_dir: str) -> str:
|
|
214
|
+
"""Live status of the run occupying .loki/ right now.
|
|
215
|
+
|
|
216
|
+
Precedence copied from the CLI's own status reader (autonomy/loki:4981):
|
|
217
|
+
PAUSE file, then STOP file, then session.json status, then unknown. A
|
|
218
|
+
missing file yields "unknown" and never "completed" -- absence of a signal
|
|
219
|
+
is not evidence of success.
|
|
220
|
+
"""
|
|
221
|
+
if os.path.isfile(_p(loki_dir, "PAUSE")):
|
|
222
|
+
return "paused"
|
|
223
|
+
if os.path.isfile(_p(loki_dir, "STOP")):
|
|
224
|
+
return "stopped"
|
|
225
|
+
session = _read_json(_p(loki_dir, _SESSION))
|
|
226
|
+
if isinstance(session, dict):
|
|
227
|
+
status = session.get("status")
|
|
228
|
+
if status:
|
|
229
|
+
return str(status)
|
|
230
|
+
return "unknown"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _started_at(loki_dir: str, run_id: str, events: dict) -> Optional[str]:
|
|
234
|
+
"""Earliest trust-event ts for this run. None when never recorded."""
|
|
235
|
+
stamps = [e.get("ts") for e in events.get(run_id, []) if e.get("ts")]
|
|
236
|
+
return min(stamps) if stamps else None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
# trust events: the only cross-run source
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
def _events_by_run(loki_dir: str) -> dict:
|
|
244
|
+
"""{run_id: [event, ...]} from trust-events.jsonl. Missing file -> {}.
|
|
245
|
+
|
|
246
|
+
Malformed lines are skipped rather than failing the whole read: the file
|
|
247
|
+
is appended to by a best-effort writer that can be killed mid-line, and one
|
|
248
|
+
torn tail must not blank every run before it.
|
|
249
|
+
"""
|
|
250
|
+
path = _p(loki_dir, *_TRUST_EVENTS)
|
|
251
|
+
by_run: dict = {}
|
|
252
|
+
try:
|
|
253
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
254
|
+
for raw in fh:
|
|
255
|
+
raw = raw.strip()
|
|
256
|
+
if not raw:
|
|
257
|
+
continue
|
|
258
|
+
try:
|
|
259
|
+
rec = json.loads(raw)
|
|
260
|
+
except Exception:
|
|
261
|
+
continue
|
|
262
|
+
if not isinstance(rec, dict):
|
|
263
|
+
continue
|
|
264
|
+
rid = rec.get("run_id")
|
|
265
|
+
if not rid:
|
|
266
|
+
continue
|
|
267
|
+
by_run.setdefault(str(rid), []).append(rec)
|
|
268
|
+
except OSError:
|
|
269
|
+
return {}
|
|
270
|
+
return by_run
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _iterations_from_events(events: list) -> Optional[int]:
|
|
274
|
+
"""Highest iteration seen in this run's events, or None if never recorded.
|
|
275
|
+
|
|
276
|
+
A LOWER BOUND, not a count. Trust events fire on trust events only, so a
|
|
277
|
+
run that did ten iterations and emitted an event on three reports 3. It is
|
|
278
|
+
the floor of what the run demonstrably did, which is why it is combined
|
|
279
|
+
with max() against the other sources rather than trusted alone.
|
|
280
|
+
|
|
281
|
+
Events carry iteration as an int (trust_metrics.record_trust_event coerces
|
|
282
|
+
it). A run whose events all report iteration 0 has no iteration evidence,
|
|
283
|
+
so this reports None rather than claiming the run did zero iterations.
|
|
284
|
+
"""
|
|
285
|
+
seen = [e.get("iteration") for e in events
|
|
286
|
+
if isinstance(e.get("iteration"), int) and not isinstance(e.get("iteration"), bool)]
|
|
287
|
+
nonzero = [i for i in seen if i > 0]
|
|
288
|
+
return max(nonzero) if nonzero else None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
# public API
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
def _row(loki_dir: str, run_id: str, is_current: bool, events: dict,
|
|
296
|
+
now: Optional[float] = None) -> dict:
|
|
297
|
+
"""One run row. Cost is only ever attached to the CURRENT run.
|
|
298
|
+
|
|
299
|
+
The efficiency directory is wiped at run start, so its records belong to
|
|
300
|
+
whichever run holds .loki/ now. Attributing them to a historical run would
|
|
301
|
+
be a fabricated fact; that run reads cost UNKNOWN with a stated reason.
|
|
302
|
+
"""
|
|
303
|
+
run_events = events.get(run_id, [])
|
|
304
|
+
contributing = [_mtime(_p(loki_dir, *_TRUST_EVENTS))]
|
|
305
|
+
|
|
306
|
+
completion = _read_json(_p(loki_dir, *_COMPLETION))
|
|
307
|
+
manifest = _read_json(_p(loki_dir, _MANIFEST))
|
|
308
|
+
|
|
309
|
+
iterations = _iterations_from_events(run_events)
|
|
310
|
+
cost_usd: Any = UNKNOWN
|
|
311
|
+
measured = False
|
|
312
|
+
note = "cost records are wiped at run start; only the current run has them"
|
|
313
|
+
|
|
314
|
+
if is_current:
|
|
315
|
+
cost_usd, measured, cost_note = _cost_for_current_run(loki_dir)
|
|
316
|
+
note = cost_note
|
|
317
|
+
recs = _iteration_records(loki_dir)
|
|
318
|
+
if recs:
|
|
319
|
+
contributing.extend(_mtime(path) for _n, _r, path in recs)
|
|
320
|
+
# The efficiency records are the sharper iteration evidence for the
|
|
321
|
+
# live run; events only see iterations that emitted a trust event.
|
|
322
|
+
#
|
|
323
|
+
# `or None` for the same reason cost is None when unmeasured: a run
|
|
324
|
+
# whose only record is iteration-0 has produced no evidence that any
|
|
325
|
+
# iteration completed, and a rendered "0 iterations" is a plausible
|
|
326
|
+
# -looking fact we did not measure. Every path into `iterations`
|
|
327
|
+
# applies this rule, so the field is never a fabricated zero.
|
|
328
|
+
iterations = max(iterations or 0, max(n for n, _r, _p2 in recs)) or None
|
|
329
|
+
if isinstance(manifest, dict) and isinstance(manifest.get("iterations"), int):
|
|
330
|
+
contributing.append(_mtime(_p(loki_dir, _MANIFEST)))
|
|
331
|
+
iterations = max(iterations or 0, manifest["iterations"]) or None
|
|
332
|
+
|
|
333
|
+
status = _current_status(loki_dir) if is_current else "unknown"
|
|
334
|
+
if isinstance(completion, dict) and completion.get("outcome"):
|
|
335
|
+
# A terminal outcome is the authoritative end state. Only the current
|
|
336
|
+
# run's .loki/ wrote it, so it never labels a historical run.
|
|
337
|
+
if is_current:
|
|
338
|
+
status = str(completion["outcome"])
|
|
339
|
+
contributing.append(_mtime(_p(loki_dir, *_COMPLETION)))
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
"id": run_id,
|
|
343
|
+
"status": status,
|
|
344
|
+
"started_at": _started_at(loki_dir, run_id, events),
|
|
345
|
+
"iterations": iterations,
|
|
346
|
+
"cost_usd": cost_usd,
|
|
347
|
+
"measured": measured,
|
|
348
|
+
"cost_note": note,
|
|
349
|
+
"current": is_current,
|
|
350
|
+
"source": list(_SOURCE_PATHS),
|
|
351
|
+
"freshness_s": _freshness(contributing, now=now),
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def list_runs(loki_dir: str, now: Optional[float] = None) -> dict:
|
|
356
|
+
"""Every run discoverable from .loki/, newest first.
|
|
357
|
+
|
|
358
|
+
Returns an ENVELOPE, not a bare list, because the contract requires an
|
|
359
|
+
explicit reason when the result is empty and a list cannot carry one:
|
|
360
|
+
|
|
361
|
+
{"runs": [...], "source": [...], "freshness_s": int|None,
|
|
362
|
+
"reason": None|str}
|
|
363
|
+
|
|
364
|
+
reason is None when runs is non-empty. When runs is empty it states why in
|
|
365
|
+
words -- no .loki, no run id ever minted, and so on. An empty list is never
|
|
366
|
+
padded with a placeholder row.
|
|
367
|
+
"""
|
|
368
|
+
envelope = {
|
|
369
|
+
"runs": [],
|
|
370
|
+
"source": list(_SOURCE_PATHS),
|
|
371
|
+
"freshness_s": None,
|
|
372
|
+
"reason": None,
|
|
373
|
+
}
|
|
374
|
+
if not loki_dir or not os.path.isdir(loki_dir):
|
|
375
|
+
envelope["reason"] = "no .loki directory at %s" % (loki_dir,)
|
|
376
|
+
return envelope
|
|
377
|
+
|
|
378
|
+
events = _events_by_run(loki_dir)
|
|
379
|
+
current_id = _read_text(_p(loki_dir, *_RUN_ID_FILE))
|
|
380
|
+
|
|
381
|
+
ids = list(events.keys())
|
|
382
|
+
if current_id and current_id not in ids:
|
|
383
|
+
ids.append(current_id)
|
|
384
|
+
|
|
385
|
+
if not ids:
|
|
386
|
+
envelope["reason"] = (
|
|
387
|
+
"no run id found: .loki/state/trust-run-id is absent and "
|
|
388
|
+
".loki/metrics/trust-events.jsonl recorded no run_id"
|
|
389
|
+
)
|
|
390
|
+
return envelope
|
|
391
|
+
|
|
392
|
+
rows = [_row(loki_dir, rid, rid == current_id, events, now=now) for rid in ids]
|
|
393
|
+
# Newest first. started_at is None for a run that never recorded one, and
|
|
394
|
+
# those sort last rather than being dropped or dated.
|
|
395
|
+
rows.sort(key=lambda r: (r["started_at"] is not None, r["started_at"] or ""),
|
|
396
|
+
reverse=True)
|
|
397
|
+
envelope["runs"] = rows
|
|
398
|
+
envelope["freshness_s"] = _freshness(
|
|
399
|
+
[_mtime(_p(loki_dir, *_TRUST_EVENTS)), _mtime(_p(loki_dir, *_RUN_ID_FILE))],
|
|
400
|
+
now=now,
|
|
401
|
+
)
|
|
402
|
+
return envelope
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def get_run(loki_dir: str, run_id: str, now: Optional[float] = None) -> dict:
|
|
406
|
+
"""One run, plus per-iteration detail. Same envelope discipline.
|
|
407
|
+
|
|
408
|
+
{"run": {...}|None, "iterations": [...], "source": [...],
|
|
409
|
+
"freshness_s": int|None, "reason": None|str}
|
|
410
|
+
|
|
411
|
+
Per-iteration detail exists only for the CURRENT run (the records are
|
|
412
|
+
wiped at run start). For any other run `iterations` is empty and `reason`
|
|
413
|
+
says why, rather than returning invented rows.
|
|
414
|
+
"""
|
|
415
|
+
envelope = {
|
|
416
|
+
"run": None,
|
|
417
|
+
"iterations": [],
|
|
418
|
+
"source": list(_SOURCE_PATHS),
|
|
419
|
+
"freshness_s": None,
|
|
420
|
+
"reason": None,
|
|
421
|
+
}
|
|
422
|
+
if not loki_dir or not os.path.isdir(loki_dir):
|
|
423
|
+
envelope["reason"] = "no .loki directory at %s" % (loki_dir,)
|
|
424
|
+
return envelope
|
|
425
|
+
if not run_id:
|
|
426
|
+
envelope["reason"] = "no run_id given"
|
|
427
|
+
return envelope
|
|
428
|
+
|
|
429
|
+
events = _events_by_run(loki_dir)
|
|
430
|
+
current_id = _read_text(_p(loki_dir, *_RUN_ID_FILE))
|
|
431
|
+
if run_id not in events and run_id != current_id:
|
|
432
|
+
envelope["reason"] = "run %s not found in .loki" % (run_id,)
|
|
433
|
+
return envelope
|
|
434
|
+
|
|
435
|
+
is_current = run_id == current_id
|
|
436
|
+
row = _row(loki_dir, run_id, is_current, events, now=now)
|
|
437
|
+
envelope["run"] = row
|
|
438
|
+
envelope["freshness_s"] = row["freshness_s"]
|
|
439
|
+
|
|
440
|
+
if not is_current:
|
|
441
|
+
envelope["reason"] = (
|
|
442
|
+
"per-iteration detail unavailable: .loki/metrics/efficiency is "
|
|
443
|
+
"wiped at run start and now belongs to run %s" % (current_id or "unknown",)
|
|
444
|
+
)
|
|
445
|
+
return envelope
|
|
446
|
+
|
|
447
|
+
mod = _efficiency_module()
|
|
448
|
+
recs = _iteration_records(loki_dir)
|
|
449
|
+
if not recs:
|
|
450
|
+
envelope["reason"] = (
|
|
451
|
+
"no per-iteration records at .loki/metrics/efficiency/iteration-*.json"
|
|
452
|
+
)
|
|
453
|
+
return envelope
|
|
454
|
+
|
|
455
|
+
for num, rec, path in recs:
|
|
456
|
+
# Per-iteration measured-ness uses the SAME canonical predicate as the
|
|
457
|
+
# aggregate, so one iteration cannot be called measured by a rule the
|
|
458
|
+
# total disagrees with.
|
|
459
|
+
if mod is None:
|
|
460
|
+
measured = False
|
|
461
|
+
else:
|
|
462
|
+
measured = bool(mod.record_is_measured(rec))
|
|
463
|
+
envelope["iterations"].append({
|
|
464
|
+
"iteration": num,
|
|
465
|
+
"measured": measured,
|
|
466
|
+
"cost_usd": rec.get("cost_usd") if measured else UNKNOWN,
|
|
467
|
+
"input_tokens": rec.get("input_tokens") if measured else UNKNOWN,
|
|
468
|
+
"output_tokens": rec.get("output_tokens") if measured else UNKNOWN,
|
|
469
|
+
"model": rec.get("model") or UNKNOWN,
|
|
470
|
+
"phase": rec.get("phase") or UNKNOWN,
|
|
471
|
+
"status": rec.get("status") or UNKNOWN,
|
|
472
|
+
"duration_ms": rec.get("duration_ms"),
|
|
473
|
+
"timestamp": rec.get("timestamp") or UNKNOWN,
|
|
474
|
+
"source": ".loki/metrics/efficiency/%s" % os.path.basename(path),
|
|
475
|
+
"freshness_s": _freshness([_mtime(path)], now=now),
|
|
476
|
+
})
|
|
477
|
+
return envelope
|