agent-observatory-cli 0.1.0__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.
@@ -0,0 +1,67 @@
1
+ """Build a portable installer from this running distribution's public code."""
2
+ from __future__ import annotations
3
+
4
+ import io
5
+ import json
6
+ import tarfile
7
+ import tomllib
8
+ from importlib import metadata
9
+ from pathlib import Path
10
+
11
+
12
+ def _manifest(package):
13
+ source = package.parent / "pyproject.toml"
14
+ if source.is_file():
15
+ content = source.read_text(encoding="utf-8")
16
+ return tomllib.loads(content)["project"]["version"], content
17
+ info = metadata.metadata("agent-observatory-cli")
18
+ version = info["Version"]
19
+ extras = info.get_all("Provides-Extra") or []
20
+ content = ('[build-system]\nrequires = ["setuptools>=77"]\nbuild-backend = "setuptools.build_meta"\n'
21
+ '[project]\nname = "agent-observatory-cli"\nversion = ' + json.dumps(version) + '\n'
22
+ 'requires-python = ' + json.dumps(info.get("Requires-Python", ">=3.11")) + '\n'
23
+ 'license = ' + json.dumps(info.get("License-Expression", "MIT")) + '\n'
24
+ 'license-files = ["LICENSE"]\n'
25
+ 'dependencies = ' + json.dumps(info.get_all("Requires-Dist") or []) + '\n'
26
+ '[project.optional-dependencies]\n' + ''.join(json.dumps(extra) + ' = []\n' for extra in extras) +
27
+ '[project.scripts]\nagent-observatory = "observatory.__main__:main"\n'
28
+ '[tool.setuptools.packages.find]\ninclude = ["observatory*"]\n'
29
+ '[tool.setuptools.package-data]\nobservatory = ["index.html", "setup.js", "setup.css", "activity.js", "activity.css"]\n')
30
+ return version, content
31
+
32
+
33
+ def installer_info():
34
+ package = Path(__file__).resolve().parent
35
+ version, _ = _manifest(package)
36
+ filename = f"agent-observatory-{version}.tar.gz"
37
+ return {"package_filename": filename,
38
+ "install_command": f'python -m pip install "./{filename}[auto]"'}
39
+
40
+
41
+ def package_archive():
42
+ package = Path(__file__).resolve().parent
43
+ version, manifest = _manifest(package)
44
+ root = f"agent-observatory-{version}"
45
+ output = io.BytesIO()
46
+ with tarfile.open(fileobj=output, mode="w:gz") as archive:
47
+ def add(name, data):
48
+ info = tarfile.TarInfo(root + "/" + name)
49
+ info.size = len(data)
50
+ info.mode = 0o644
51
+ archive.addfile(info, io.BytesIO(data))
52
+ add("pyproject.toml", manifest.encode("utf-8"))
53
+ license_file = package.parent / "LICENSE"
54
+ if not license_file.is_file():
55
+ try:
56
+ installed_license = metadata.distribution("agent-observatory-cli").read_text("licenses/LICENSE")
57
+ except (metadata.PackageNotFoundError, OSError):
58
+ installed_license = None
59
+ if installed_license:
60
+ add("LICENSE", installed_license.encode("utf-8"))
61
+ else:
62
+ add("LICENSE", license_file.read_bytes())
63
+ for path in sorted(package.rglob("*")):
64
+ if (path.is_file() and path.suffix in {".py", ".html", ".css", ".js"}
65
+ and path.resolve().is_relative_to(package)):
66
+ add("observatory/" + path.relative_to(package).as_posix(), path.read_bytes())
67
+ return output.getvalue(), root + ".tar.gz"
@@ -0,0 +1,176 @@
1
+ """Deterministic, evidence-linked observations over captured agent activity."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import re
6
+
7
+
8
+ _SENSITIVE_FIELD = re.compile(
9
+ r"(?:^|[._-])(?:api[_-]?key|access[_-]?token|refresh[_-]?token|"
10
+ r"client[_-]?secret|password|passwd|private[_-]?key|authorization|"
11
+ r"credential|secret|bearer)(?:$|[._-])", re.IGNORECASE)
12
+ _TOKEN_FIELD = re.compile(r"(?:^|[.-])(?:auth[_-]?)?token(?:$|[.-])", re.IGNORECASE)
13
+ _ASSIGNED_SECRET = re.compile(
14
+ r"(?i)\b(api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|"
15
+ r"password|passwd|private[_-]?key|authorization|credential|secret|bearer)"
16
+ r"(\s*[:=]\s*)([\"']?)([^\s\"'`,;]{4,})")
17
+ _BEARER_VALUE = re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._~+/-]{8,}={0,}")
18
+ _PRIVATE_KEY = re.compile(
19
+ r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----",
20
+ re.IGNORECASE)
21
+ _TOKEN_PATTERNS = (
22
+ ("OpenAI or compatible", re.compile(r"\bsk-[A-Za-z0-9_-]{20,}\b")),
23
+ ("GitHub", re.compile(r"\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b")),
24
+ ("AWS access key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
25
+ ("Google API key", re.compile(r"\bAIza[0-9A-Za-z_-]{30,}\b")),
26
+ ("Slack", re.compile(r"\bxox[baprs]-[A-Za-z0-9-]{20,}\b")),
27
+ )
28
+
29
+
30
+ def _safe_text(value):
31
+ """Remove recognizable credential material before it enters a finding."""
32
+ text = str(value)
33
+ text = _PRIVATE_KEY.sub("[REDACTED PRIVATE KEY]", text)
34
+ text = _BEARER_VALUE.sub("Bearer [REDACTED]", text)
35
+ text = _ASSIGNED_SECRET.sub(lambda match: match.group(1) + match.group(2) + "[REDACTED]", text)
36
+ for label, pattern in _TOKEN_PATTERNS:
37
+ text = pattern.sub(f"[REDACTED {label} token]", text)
38
+ return text
39
+
40
+
41
+ def _credential_hits(value, field_path=""):
42
+ """Return safe field/provider labels; never return the matched values."""
43
+ hits = set()
44
+ if isinstance(value, dict):
45
+ for key, child in value.items():
46
+ key = str(key)
47
+ path = f"{field_path}.{key}" if field_path else key
48
+ if (_SENSITIVE_FIELD.search(key) or _TOKEN_FIELD.search(key)) and child not in (None, "", False):
49
+ hits.add(("sensitive field", path[:160]))
50
+ hits.update(_credential_hits(child, path))
51
+ elif isinstance(value, (list, tuple)):
52
+ for child in value[:100]:
53
+ hits.update(_credential_hits(child, field_path))
54
+ elif isinstance(value, str):
55
+ if _PRIVATE_KEY.search(value):
56
+ hits.add(("private key material", field_path[:160] or "captured text"))
57
+ if _ASSIGNED_SECRET.search(value):
58
+ hits.add(("credential assignment", field_path[:160] or "captured text"))
59
+ if _BEARER_VALUE.search(value):
60
+ hits.add(("bearer token", field_path[:160] or "captured text"))
61
+ for label, pattern in _TOKEN_PATTERNS:
62
+ if pattern.search(value):
63
+ hits.add((label + " token pattern", field_path[:160] or "captured text"))
64
+ return hits
65
+
66
+
67
+ def _finding(trace_id, kind, title, severity, evidence, detail):
68
+ source = ",".join(sorted(f"{e['type']}:{e['id']}" for e in evidence))
69
+ digest = hashlib.sha256(f"{trace_id}:{kind}:{source}".encode()).hexdigest()[:16]
70
+ return {"id": digest, "trace_id": trace_id, "kind": kind,
71
+ "title": _safe_text(title), "severity": severity,
72
+ "detail": _safe_text(detail), "evidence": evidence}
73
+
74
+
75
+ def _blocked_activity(attrs):
76
+ result = str(attrs.get("agent.activity.result") or attrs.get("activity.result") or "").lower()
77
+ return result in {"blocked", "denied", "policy_denied"}
78
+
79
+
80
+ def _activity_detail(attrs):
81
+ domain = attrs.get("agent.activity.domain") or attrs.get("activity.domain") or "activity"
82
+ action = attrs.get("agent.activity.action") or attrs.get("activity.action") or "action"
83
+ subject = (attrs.get("agent.activity.subject") or attrs.get("agent.activity.target") or
84
+ attrs.get("activity.subject") or attrs.get("activity.target"))
85
+ return f"{domain}: {action}" + (f" · {subject}" if subject else "")
86
+
87
+
88
+ def analyze(spans, logs, outcome=None):
89
+ if not spans and not logs and not outcome:
90
+ return []
91
+ trace_id = spans[0]["trace_id"] if spans else logs[0]["trace_id"] if logs else outcome["trace_id"]
92
+ findings = []
93
+ failures = {}
94
+ credential_hits = set()
95
+ credential_sources = {}
96
+ for span in spans:
97
+ attrs = span["attributes"]
98
+ hits = _credential_hits(attrs)
99
+ for event in span.get("events", []):
100
+ if isinstance(event, dict):
101
+ hits.update(_credential_hits(event.get("attributes", {}), "event." + str(event.get("name") or "event")))
102
+ if hits:
103
+ credential_hits.update(label for label, _ in hits)
104
+ credential_sources.setdefault(("span", span["span_id"]), set()).update(field for _, field in hits)
105
+ reason = attrs.get("tool.error_message") or attrs.get("tool.error")
106
+ failed = span["status"] == "ERROR" or attrs.get("tool.result_status") == "error" or bool(attrs.get("tool.error"))
107
+ if failed:
108
+ title = f"Error in {attrs.get('tool.name') or span['name']}"
109
+ findings.append(_finding(trace_id, "span_error", title, "high",
110
+ [{"type": "span", "id": span["span_id"]}],
111
+ str(reason or "Span status is ERROR")[:500]))
112
+ if attrs.get("tool.name"):
113
+ key = (str(attrs["tool.name"]), str(reason or attrs.get("tool.error") or "error"))
114
+ failures.setdefault(key, []).append(span["span_id"])
115
+ if span["name"] == "agent.run":
116
+ stopped = attrs.get("agent.stopped_reason")
117
+ if stopped and stopped != "completed":
118
+ findings.append(_finding(trace_id, "incomplete_run", "Run stopped before completion", "medium",
119
+ [{"type": "span", "id": span["span_id"]}],
120
+ f"Recorded stop reason: {stopped}"))
121
+ blocked = [_activity_detail(event.get("attributes", {}))
122
+ for event in span["events"]
123
+ if isinstance(event, dict) and event.get("name") == "agent.activity" and
124
+ isinstance(event.get("attributes"), dict) and
125
+ _blocked_activity(event["attributes"])]
126
+ if blocked:
127
+ findings.append(_finding(trace_id, "blocked_activity",
128
+ f"Blocked agent activity ({len(blocked)})", "medium",
129
+ [{"type": "span", "id": span["span_id"]}],
130
+ "; ".join(blocked[:5])[:500]))
131
+ for (tool, reason), ids in failures.items():
132
+ if len(ids) >= 2:
133
+ findings.append(_finding(trace_id, "repeated_tool_error", f"Repeated errors from {tool}", "medium",
134
+ [{"type": "span", "id": value} for value in ids],
135
+ f"{len(ids)} failed calls recorded with the same error: {reason[:250]}"))
136
+ for log in logs:
137
+ hits = _credential_hits(log.get("attributes", {}))
138
+ hits.update(_credential_hits(log.get("body")))
139
+ if hits:
140
+ credential_hits.update(label for label, _ in hits)
141
+ credential_sources.setdefault(("log", log["id"]), set()).update(field for _, field in hits)
142
+ if log["severity_number"] >= 17:
143
+ findings.append(_finding(trace_id, "error_log", "Error log recorded", "medium",
144
+ [{"type": "log", "id": log["id"]}],
145
+ str(log["body"])[:500]))
146
+ if log.get("event_name") == "agent.activity" and isinstance(log.get("attributes"), dict) and \
147
+ _blocked_activity(log["attributes"]):
148
+ findings.append(_finding(trace_id, "blocked_activity", "Blocked agent activity", "medium",
149
+ [{"type": "log", "id": log["id"]}],
150
+ _activity_detail(log["attributes"])[:500]))
151
+ if outcome and not outcome["passed"]:
152
+ failed = [str(score.get("scorer") or "assertion") for score in outcome["scores"]
153
+ if score.get("passed") is False]
154
+ detail = ("Failed checks: " + ", ".join(failed)) if failed else str(outcome.get("error") or "Evaluation did not pass")
155
+ findings.append(_finding(trace_id, "failed_outcome", "Task outcome failed", "high",
156
+ [{"type": "outcome", "id": trace_id}], detail[:500]))
157
+ if outcome:
158
+ hits = _credential_hits(outcome)
159
+ if hits:
160
+ credential_hits.update(label for label, _ in hits)
161
+ credential_sources.setdefault(("outcome", outcome["trace_id"]), set()).update(
162
+ field for _, field in hits)
163
+ if credential_sources:
164
+ evidence = [{"type": source_type, "id": source_id}
165
+ for source_type, source_id in sorted(credential_sources)]
166
+ fields = sorted({field for values in credential_sources.values() for field in values})
167
+ labels = ", ".join(sorted(credential_hits))
168
+ field_summary = ", ".join(fields[:8]) or "captured content"
169
+ if len(fields) > 8:
170
+ field_summary += f" and {len(fields) - 8} more fields"
171
+ severity = "high" if any(label == "private key material" or
172
+ label.endswith("token pattern") for label in credential_hits) else "medium"
173
+ findings.append(_finding(
174
+ trace_id, "credential_exposure", "Possible credential material captured", severity,
175
+ evidence, f"Detected {labels} in {field_summary}. Values are omitted; inspect the linked source."))
176
+ return findings
observatory/index.html ADDED
@@ -0,0 +1,84 @@
1
+ <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
3
+ <title>Observatory · Agent activity</title><link rel="stylesheet" href="/setup.css"><link rel="stylesheet" href="/activity.css"><script src="/activity.js" defer></script>
4
+ <style>
5
+ :root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;color:#e9edf5;background:#0c111b;font-size:14px}*{box-sizing:border-box}body{margin:0}button,select,input{font:inherit}button{cursor:pointer}header{height:66px;border-bottom:1px solid #243044;display:flex;align-items:center;justify-content:space-between;padding:0 28px;background:#101827}header strong{letter-spacing:.02em;font-size:17px}header strong span{color:#77aaff}header small{color:#91a0b6}.shell{display:grid;grid-template-columns:320px minmax(0,1fr);min-height:calc(100vh - 66px)}aside{border-right:1px solid #243044;background:#101827}main{min-width:0;padding:30px;max-width:1500px;width:100%;margin:auto}.aside-head{padding:20px;border-bottom:1px solid #243044}.aside-head h2{font-size:13px;text-transform:uppercase;letter-spacing:.12em;color:#93a5c0;margin:0 0 15px}.controls{display:flex;gap:8px}select,.refresh,input{background:#182338;color:#dfe9f8;border:1px solid #334159;border-radius:8px;padding:9px;min-width:0}.controls input{flex:1}.controls select{flex:1}.refresh{padding:9px 11px}.run{display:block;width:100%;text-align:left;color:#dce6f5;background:transparent;border:0;border-bottom:1px solid #202b3d;padding:17px 20px}.run:hover,.run.active{background:#1b2c46}.run .top{display:flex;justify-content:space-between;gap:10px;margin-bottom:8px}.run .title{font-weight:650;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.run .sub{font-size:12px;color:#91a0b6;display:flex;justify-content:space-between}.run.endpoint{padding-top:13px;padding-bottom:13px}.run.endpoint .sub{margin-top:5px;gap:8px}.endpoint-services{font-size:11px;color:#91a0b6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.endpoint.selected{border-color:#6f9dde;background:#203451}.clear-endpoint{border:0;background:transparent;color:#9fc0f4;text-decoration:underline;cursor:pointer;font:11px sans-serif}.endpoint-filter-state{padding:0 10px 8px;overflow-wrap:anywhere}.session-summary{margin:10px 0 4px;padding:10px;border:1px solid #293b54;border-radius:9px;background:#172439}.session-summary-title{font-size:12px}.session-summary-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:8px}.session-stat{display:flex;flex-direction:column;gap:2px}.session-stat .muted{font-size:10px}.session-summary-details{margin-top:8px;color:#a7bad4;font-size:10px;overflow-wrap:anywhere}.badge{font-size:11px;border-radius:20px;padding:3px 7px;background:#233d35;color:#8ce2bc}.badge.error{background:#493037;color:#ff9d9d}.eyebrow{text-transform:uppercase;letter-spacing:.14em;color:#83a7e9;font-weight:700;font-size:11px}.heading{display:flex;align-items:flex-start;justify-content:space-between;gap:20px}.heading h1{font-size:28px;line-height:1.2;margin:9px 0 7px;overflow-wrap:anywhere}.muted{color:#98a9be}.id{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px}.metrics{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;margin:27px 0}.usage-heading{display:flex;align-items:center;justify-content:space-between;gap:16px}.usage-heading h2{margin:0}.usage-chart{display:block;width:100%;height:250px;margin:12px 0}.usage-chart text{fill:#9babc0;font-size:11px}.usage-chart line{stroke:#334159;stroke-width:1}.usage-chart path{fill:none;stroke-width:2.5}.usage-input{stroke:#77aaff}.usage-output{stroke:#73d9b0}.usage-summary{display:flex;gap:7px;flex-wrap:wrap;margin-top:12px}.metric-chart{display:block;width:100%;height:280px;margin:12px 0}.metric-chart text{fill:#9babc0;font-size:11px}.metric-chart line{stroke:#334159;stroke-width:1}.metric-chart path{fill:none;stroke-width:2}.metric-legend{display:flex;flex-wrap:wrap;gap:6px}.card,.panel{border:1px solid #29364b;background:#141f30;border-radius:12px}.card{padding:17px}.card label{display:block;color:#9babc0;font-size:12px;margin-bottom:8px}.card b{font-size:24px;font-weight:680}.grid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(280px,1fr);gap:16px}.panel{padding:19px;min-width:0}.panel h2{font-size:15px;margin:0 0 17px}.timeline{display:flex;flex-direction:column;gap:8px;max-height:68vh;overflow:auto;padding-right:4px}.span{border:1px solid #31415a;border-radius:9px;padding:10px 12px;background:#172439}.span.error{border-color:#9d5360}.span button{display:flex;align-items:center;justify-content:space-between;width:100%;border:0;background:transparent;color:#e8effa;text-align:left;padding:0}.span .meta{font-size:11px;color:#9eafc4;margin-top:7px}.bar{height:4px;background:#3d70b9;border-radius:2px;margin-top:9px;min-width:3px}.span.error .bar{background:#e57886}.details{margin-top:10px;border-top:1px solid #31415a;padding-top:10px;color:#b5c4d8;font-family:ui-monospace,monospace;font-size:11px;white-space:pre-wrap;overflow-wrap:anywhere}.progress{border-left:2px solid #4d7fca;padding:0 0 18px 15px;margin-left:5px}.progress b{display:block;margin-bottom:3px}.progress small{color:#9badc3}.empty{padding:34px;color:#9babc0;text-align:center}.hint{line-height:1.5;color:#9babc0}.pill{display:inline-block;border:1px solid #344962;color:#9db9dc;border-radius:6px;padding:5px 8px;margin:4px 5px 0 0;font-size:12px}@media(max-width:1000px){.shell{grid-template-columns:250px 1fr}.metrics{grid-template-columns:repeat(3,1fr)}.grid{grid-template-columns:1fr}}@media(max-width:680px){.shell{display:block}aside{border-right:0}.run-list{max-height:210px;overflow:auto}main{padding:20px}.metrics{grid-template-columns:repeat(2,1fr)}}
6
+ .activity-list{display:flex;flex-direction:column;gap:8px;max-height:340px;overflow:auto}.activity-row{border:1px solid #31415a;border-radius:9px;padding:10px 12px;background:#172439}.activity-head,.activity-foot{display:flex;align-items:center;justify-content:space-between;gap:8px}.activity-head{font-weight:650}.activity-subject{margin-top:7px;overflow-wrap:anywhere;color:#dce6f5}.activity-foot{margin-top:7px;font-size:11px;color:#98a9be}.activity-domain{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:#93b9f5}.activity-result{font-size:10px;border-radius:12px;padding:3px 7px;background:#26374f;color:#b6c9e6}.activity-result.error,.activity-result.blocked{background:#493037;color:#ff9d9d}.activity-source{border:0;background:transparent;color:#9fc0f4;padding:0;text-decoration:underline;font:inherit;cursor:pointer}.activity-filters{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:9px}.activity-filter{border:1px solid #344a67;border-radius:16px;background:#172439;color:#b6c9e6;padding:5px 9px;font:11px sans-serif;cursor:pointer}.activity-filter.active{background:#29476b;border-color:#5475a2;color:#fff}.interactions{display:grid;gap:10px}.interaction-card{border:1px solid #293b54;border-radius:10px;padding:10px;background:#172439}.interaction-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;overflow-wrap:anywhere}.interaction-stats{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}.interaction-stats .pill{font-size:10px;padding:4px 7px}.interaction-block{margin-top:10px}.interaction-label{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:#93b9f5;margin-bottom:4px}.interaction-content{margin:0;max-height:260px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.5 ui-monospace,SFMono-Regular,monospace;color:#dce6f5}.interaction-card .activity-source{margin-top:9px}.interaction-message{margin-top:7px;border-left:2px solid #344a67;padding-left:8px}.interaction-role{display:block;font-size:11px;color:#b6c9e6;margin-bottom:3px}#review{max-height:360px;overflow:auto}.activity-subject-heading{margin:8px 0 5px;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#93a5c0}.activity-subject-groups{display:grid;gap:5px;margin-bottom:10px}.activity-subject-group{display:flex;justify-content:space-between;gap:8px;text-align:left;border:1px solid #344a67;border-radius:7px;background:#182338;color:#dce6f5;padding:6px 8px;font:11px sans-serif;cursor:pointer}.activity-subject-group:hover,.activity-subject-group.active{background:#29476b;border-color:#5475a2}.activity-subject-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activity-subject-count{flex:none;color:#a9bed9}.span-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}.span-toolbar h2{flex:none;margin:0}.span-toolbar .controls{min-width:0;flex:1;justify-content:flex-end}.span-toolbar input{max-width:220px;flex:1}.span-toolbar select{max-width:140px}.span-count{font-size:11px;margin-bottom:8px}@media(max-width:680px){.span-toolbar{align-items:stretch;flex-direction:column}.span-toolbar .controls{justify-content:stretch}.span-toolbar input,.span-toolbar select{max-width:none}}</style></head><body><header><strong><span>◉</span> Observatory</strong><div class="header-actions"><small id="connection">Local agent telemetry</small><button id="connect-agent" class="connect-button" type="button">Connect an agent</button></div></header><div class="shell"><aside><div class="aside-head"><h2>Traces</h2><div class="controls"><select id="service"><option value="">All services</option></select><select id="status"><option value="">All states</option><option value="ERROR">Errors</option><option value="INCOMPLETE">Incomplete</option><option value="FAILED">Failed outcome</option><option value="RUNNING">Running</option><option value="OK">OK</option></select><button class="refresh" id="refresh" title="Refresh">↻</button></div><div class="controls" style="margin-top:8px"><input id="trace-search" type="search" placeholder="Search traces, tasks, models…" aria-label="Search traces, tasks, services, and models"><button id="clear-search" class="refresh" title="Clear search">×</button></div><div class="controls" style="margin-top:8px"><select id="session"><option value="">All sessions</option></select></div><div id="session-summary" class="session-summary" hidden></div></div><div class="run-list" id="runs"></div><button class="refresh" id="older" hidden>Load older traces</button><div class="aside-head"><h2>Findings</h2></div><div class="run-list" id="review"></div><div class="aside-head"><h2>Endpoints</h2><button id="clear-endpoint-filter" class="clear-endpoint" hidden>Clear host filter</button></div><div id="endpoint-filter-state" class="muted endpoint-filter-state" hidden></div><div class="run-list" id="endpoints"></div></aside><main><section class="setup-panel" id="agent-setup" aria-labelledby="setup-title">
7
+ <div class="setup-heading"><div><div class="eyebrow">Connect your agent</div><h1 id="setup-title">See your models at work</h1><p>Start your Python agent with automatic capture. Model calls, tokens, timing, errors, and supported framework activity appear here as you use it.</p></div><button id="close-agent-setup" class="setup-close" type="button" aria-label="Close connection guide">×</button></div>
8
+ <div class="setup-support"><span>OpenAI</span><span>Anthropic</span><span>LangChain / LangGraph</span><span>OpenAI-compatible local models</span></div>
9
+ <div class="setup-fields">
10
+ <label class="setup-field">How will your agent connect?<select id="setup-runtime"><option value="python">Python · automatic capture</option><option value="otel">An app already using OpenTelemetry</option></select></label>
11
+ <label class="setup-field">Name your agent<input id="setup-service" value="my-agent" maxlength="128" autocomplete="off"><small>This name groups its activity in the dashboard.</small></label>
12
+ <div class="setup-field" id="setup-target-field"><label for="setup-target">Your agent script or module</label><input id="setup-target" value="agent.py" autocomplete="off"><small><label><input type="checkbox" id="setup-module" style="width:auto"> Run as a Python module</label></small></div>
13
+ <label class="setup-field">Receiver address<input id="setup-endpoint" type="url" autocomplete="off"><small>For containers, use the host or service address the agent can reach.</small></label>
14
+ </div>
15
+ <div id="setup-capture-options"><label class="setup-capture"><input type="checkbox" id="setup-capture"> Include prompts and responses</label><div class="setup-capture-note">Default capture includes model names, token usage, timing, and errors. Enable this to also store message content locally.</div></div>
16
+ <div class="setup-steps">
17
+ <div class="setup-step" id="setup-install-step"><h3><span>1</span> Install in your agent’s environment</h3><p>Download the installer, then run this command from your download folder in the Python environment your agent uses.</p><button id="download-agent-package" class="connect-button" type="button" style="margin-bottom:12px">Download Python installer</button><div class="setup-code"><pre id="setup-install-code"></pre><button type="button" class="setup-copy" data-copy-command="setup-install-code">Copy</button></div></div>
18
+ <div class="setup-step"><h3><span id="setup-launch-number">2</span><b id="setup-launch-title">Start your agent with automatic capture</b></h3><p id="setup-launch-help"></p><div class="setup-code"><pre id="setup-launch-code"></pre><button type="button" class="setup-copy" data-copy-command="setup-launch-code">Copy</button></div></div>
19
+ </div>
20
+ <p id="setup-runtime-note" class="setup-hint"></p><p id="setup-auth-hint" class="setup-hint" hidden>This receiver requires a token. Set OBSERVATORY_TOKEN in your agent’s terminal before running the command.</p><div id="setup-feedback" class="setup-feedback" role="status" aria-live="polite"></div>
21
+ </section>
22
+ <div class="source-strip"><span id="receiver-state" class="receiver-state">Receiver listening</span><div id="connected-agents" class="source-strip" style="margin:0"></div></div>
23
+ <section class="waiting-card" id="waiting-first-call"><h2 id="waiting-title">Waiting for your first agent</h2><p id="waiting-help">Connect an agent above, then make a model call. This page updates automatically.</p></section>
24
+ <div id="investigation" hidden><section class="panel" style="margin-bottom:16px"><div class="usage-heading"><div><h2>Usage trends</h2><div class="muted" id="usage-caption">Token use and run outcomes over time</div></div><select id="usage-days" aria-label="Usage range"><option value="1">24 hours</option><option value="7" selected>7 days</option><option value="30">30 days</option><option value="90">90 days</option></select></div><div id="usage-summary" class="usage-summary"></div><svg id="usage-chart" class="usage-chart" viewBox="0 0 900 250" role="img" aria-label="Input and output tokens over time"></svg><div id="usage-legend" class="metric-legend"></div></section><div class="heading"><div><div class="eyebrow">Agent activity</div><h1 id="title">Select a trace</h1><div class="muted id" id="subtitle">Spans, progress and model usage appear here.</div></div><span class="badge" id="state" hidden></span></div><div class="metrics"><div class="card"><label>Duration</label><b id="duration">—</b></div><div class="card"><label>Spans</label><b id="spans">—</b></div><div class="card"><label>Input tokens</label><b id="input">—</b></div><div class="card"><label>Output tokens</label><b id="output">—</b></div><div class="card"><label>Known cost</label><b id="cost">—</b></div></div><section class="panel" style="margin-bottom:16px"><h2>Metric explorer</h2><div class="controls"><select id="metric"><option value="">No metrics recorded</option></select><select id="metric-field" aria-label="Metric value" hidden></select><span class="muted" id="metric-caption">Select an OTLP metric</span></div><svg id="metric-chart" class="metric-chart" viewBox="0 0 900 280" role="img" aria-label="Metric time series"></svg><div id="metric-legend" class="metric-legend"></div><h3>Recent metric points</h3><div id="measurements" class="hint">OTLP metric points will appear here.</div></section><div class="grid"><section class="panel"><div class="span-toolbar"><h2>Span timeline</h2><div class="controls"><input id="span-search" type="search" placeholder="Search spans…" aria-label="Search span names, services, models, tools, and errors"><select id="span-filter" aria-label="Filter spans"><option value="all">All spans</option><option value="errors">Errors</option><option value="models">Model calls</option><option value="tools">Tool calls</option><option value="agents">Agent spans</option></select></div></div><div id="span-count" class="muted span-count" aria-live="polite"></div><div class="timeline" id="timeline"><div class="empty">Select a run to inspect its work.</div></div></section><div><section class="panel"><h2>Model interactions</h2><div id="interactions" class="interactions"><div class="hint">Opt-in captured prompts and outputs will appear here.</div></div></section><section class="panel" style="margin-top:16px"><h2>Progress</h2><div id="progress" class="hint">Progress events linked to this trace will appear here.</div></section><section class="panel" style="margin-top:16px"><h2>Agent activity ledger</h2><div id="activity" class="activity-list"><div class="hint">Structured agent actions linked to this trace will appear here.</div></div></section><section class="panel" style="margin-top:16px"><h2>Correlated logs</h2><div id="logs" class="hint">Logs linked to this trace will appear here.</div></section><section class="panel" style="margin-top:16px"><h2>Run context</h2><div id="context" class="hint">Service and model details will appear here.</div></section><section class="panel" style="margin-top:16px"><h2>Related traces</h2><div id="related" class="hint">Span links to other traces will appear here.</div></section><section class="panel" style="margin-top:16px"><h2>Findings in this trace</h2><div id="findings" class="hint">Recorded failures and review findings will appear here.</div></section><section class="panel" style="margin-top:16px" id="outcome"><h2>Evaluation outcome</h2><div id="outcome-details" class="hint">No outcome has been joined to this trace.</div></section></div></div></div></main></div>
25
+ <script src="/setup.js"></script><script>
26
+ const $=id=>document.getElementById(id);let selected=null;let runs=[];let nextCursor=null;let activeService='';let activeStatus='';let activeSession='';let activeQuery='';let activeEndpoint='';let loadedEndpoint='';let searchTimer=null;let metricCatalog=null;let metricForChart=null;let metricPointsCache=[];let usageCacheKey='';let usageLoadedAt=0;let sessionCache=null;let sessionLoadedAt=0;let reviewItems=[];let reviewCursor=null;let reviewHasMore=false;let reviewSeverity='all';let reviewLoading=false;let reviewInitialized=false;let activeSpanQuery='';let activeSpanFilter='all';let spanSearchTimer=null;let currentTraceSpans=[];let matchedTraceSpans=[];let traceSpanById=new Map();let traceSpanDepth=new Map();let traceSpanStart=0;let traceSpanTotal=1;let renderedSpanCount=0;let spanPageCursor=null;let spanPageLoading=false;let spanPageRequest=0;
27
+ const fmt=n=>Number(n||0).toLocaleString();
28
+ const el=(tag,cls,text)=>{const x=document.createElement(tag);if(cls)x.className=cls;if(text!==undefined)x.textContent=text;return x};
29
+ let token=sessionStorage.getItem('observatoryToken')||'';
30
+ async function authorizedFetch(path){let r=await fetch(path,{headers:token?{Authorization:'Bearer '+token}:{}});if(r.status===401){token=prompt('Observatory access token')||'';sessionStorage.setItem('observatoryToken',token);r=await fetch(path,{headers:token?{Authorization:'Bearer '+token}:{}})}if(!r.ok)throw Error(`${r.status} ${r.statusText}`);return r}
31
+ async function get(path){return (await authorizedFetch(path)).json()}
32
+ function label(r){const generic=['ChatCompletion','messages.create','Completion','Responses'];return r.task||(generic.includes(r.name)?(r.models?.join(', ')||'Model call'):r.name)||r.trace_id}
33
+ function renderEndpoints(items){const host=$('endpoints');host.replaceChildren();const selected=items.find(item=>item.endpoint_id===activeEndpoint);$('clear-endpoint-filter').hidden=!activeEndpoint;$('endpoint-filter-state').hidden=!activeEndpoint;$('endpoint-filter-state').textContent=selected?'Showing traces observed on '+selected.name:'Host filter active';if(!items.length){host.append(el('div','empty','No host resource attributes received yet.'));return}for(const item of items){const b=el('button','run endpoint'+(item.endpoint_id===activeEndpoint?' selected':''));const top=el('div','top');top.append(el('span','title',item.name),el('span','badge',fmt(item.trace_count)+' traces'));b.append(top);const details=[item.os_type,item.os_version,item.architecture].filter(Boolean).join(' · ')||item.identity_key;const sub=el('div','sub');sub.append(el('span','',details),el('span','',new Date(item.last_seen_unix_nano/1e6).toLocaleDateString()));b.append(sub);if(item.services.length)b.append(el('div','endpoint-services',item.services.join(' · ')));b.title=[item.identity_value,item.user,item.environment,item.cloud_region].filter(Boolean).join(' · ');b.onclick=()=>{activeEndpoint=activeEndpoint===item.endpoint_id?'':item.endpoint_id;refresh(true)};host.append(b)}}
34
+ function renderSessionSummary(sessions,sessionId){const host=$('session-summary');host.replaceChildren();const item=sessions.find(session=>session.session_id===sessionId);host.hidden=!item;if(!item)return;host.append(el('strong','session-summary-title','Conversation summary'));const grid=el('div','session-summary-grid');for(const [label,value] of [['Runs',fmt(item.trace_count)],['Errors',fmt(item.errors)],['Failed',fmt(item.failed)],['Incomplete',fmt(item.incomplete)],['Input tokens',fmt(item.input_tokens)],['Output tokens',fmt(item.output_tokens)],['Known cost','$'+Number(item.known_cost_usd||0).toFixed(4)],['Cost gaps',fmt(item.unknown_cost_runs)]]){const stat=el('div','session-stat');stat.append(el('span','muted',label),el('b','',value));grid.append(stat)}host.append(grid);const details=[...(item.services||[]),...(item.models||[])];if(details.length)host.append(el('div','session-summary-details',details.join(' · ')));host.title='Updated '+new Date(item.last_unix_nano/1e6).toLocaleString()}
35
+ function drawRuns(){const host=$('runs');host.replaceChildren();if(!runs.length){host.append(el('div','empty','Waiting for agent activity. Use Connect an agent to start automatic capture.'));return}for(const r of runs){const b=el('button','run'+(r.trace_id===selected?' active':''));const top=el('div','top');top.append(el('span','title',label(r)),el('span','badge'+(['ERROR','FAILED','INCOMPLETE'].includes(r.status)?' error':''),r.status));b.append(top);const sub=el('div','sub');sub.append(el('span','',r.services.join(', ')),el('span','',new Date(r.end_unix_nano/1e6).toLocaleTimeString()));b.append(sub);b.onclick=()=>openTrace(r.trace_id);host.append(b)}}
36
+ function spanModel(s){const a=s.attributes||{};return a['gen_ai.request.model']||a['llm.response.model_name']||a['llm.request.model_name']||a['llm.model_name']||a['embedding.model_name']||a['llm.model']||''}function drawSpan(s,start,total,depth){const a=s.attributes||{};const model=spanModel(s);const wrap=el('div','span'+(s.status==='ERROR'?' error':''));wrap.id='span-'+s.span_id;wrap.style.marginLeft=Math.min(depth*16,96)+'px';const b=el('button');b.append(el('strong','',s.name),el('span','muted',((s.end_unix_nano-s.start_unix_nano)/1e6).toFixed(1)+' ms'));wrap.append(b);const meta=el('div','meta',(s.service||'unknown')+' · '+s.status+(a['tool.name']?' · '+a['tool.name']:'')+(model?' · '+model:''));wrap.append(meta);const bar=el('div','bar');bar.style.marginLeft=Math.max(0,(s.start_unix_nano-start)/total*70)+'%';bar.style.width=Math.max(1,(s.end_unix_nano-s.start_unix_nano)/total*70)+'%';wrap.append(bar);const detail=el('div','details',JSON.stringify({attributes:s.attributes,events:s.events,links:s.links,resource:s.resource_attributes,scope:s.scope_name},null,2));detail.hidden=true;b.onclick=()=>detail.hidden=!detail.hidden;wrap.append(detail);return wrap}
37
+ function interactionText(value){if(value===null||value===undefined)return '';if(typeof value==='string'){const trimmed=value.trim();if(trimmed.startsWith('{')||trimmed.startsWith('[')){try{return JSON.stringify(JSON.parse(trimmed),null,2)}catch{}}return value}try{return JSON.stringify(value,null,2)}catch{return String(value)}}
38
+ function parseInteraction(value){if(typeof value==='string'){const trimmed=value.trim();if(trimmed.startsWith('{')||trimmed.startsWith('[')){try{return JSON.parse(trimmed)}catch{}}}return value}
39
+ function interactionMessages(value){const parsed=parseInteraction(value);const messages=Array.isArray(parsed)?parsed:(parsed&&Array.isArray(parsed.messages)?parsed.messages:null);if(!messages||!messages.length||!messages.some(item=>item&&typeof item==='object'&&('role'in item||'speaker'in item||'content'in item)))return null;return messages}
40
+ function appendInteraction(block,label,value){block.append(el('div','interaction-label',label));const messages=interactionMessages(value);if(messages){messages.slice(0,30).forEach((message,index)=>{const item=el('div','interaction-message');const role=message.role||message.speaker||message.type||('Message '+(index+1));item.append(el('strong','interaction-role',String(role)));let body=message.content??message.text??message.output??message; if(Array.isArray(body)&&body.every(part=>part&&typeof part==='object'&&typeof part.text==='string'))body=body.map(part=>part.text).join('\n');let rendered=interactionText(body);if(rendered.length>12000)rendered=rendered.slice(0,12000)+'\n… [truncated for display]';item.append(el('pre','interaction-content',rendered));block.append(item)});if(messages.length>30)block.append(el('div','hint','Showing the first 30 messages.'));return}let rendered=interactionText(value);if(rendered.length>12000)rendered=rendered.slice(0,12000)+'\n… [truncated for display]';block.append(el('pre','interaction-content',rendered))}
41
+ function openInferenceMessages(attrs,direction){const prefix='llm.'+direction+'_messages.';const indexes=new Set();for(const key of Object.keys(attrs)){if(!key.startsWith(prefix))continue;const match=key.slice(prefix.length).match(/^(\d+)\./);if(match)indexes.add(Number(match[1]))}if(!indexes.size)return null;return [...indexes].sort((a,b)=>a-b).map(index=>{const base=prefix+index+'.';const message={};const contentParts=[];for(const [key,value] of Object.entries(attrs)){if(!key.startsWith(base))continue;const field=key.slice(base.length);if(field==='message.role')message.role=value;else if(field==='message.content')message.content=value;else{const part=field.match(/^message\.contents\.(\d+)\.message_content\.text$/);if(part)contentParts[Number(part[1])]=value}}if(message.content===undefined&&contentParts.some(value=>value!==undefined))message.content=contentParts.filter(value=>value!==undefined).join('');return message})}function openInferenceEmbeddingInputs(attrs){const found=[];for(const [key,value] of Object.entries(attrs)){const match=key.match(/^embedding\.embeddings\.(\d+)\.embedding\.text$/);if(match&&value!==undefined&&value!==null)found.push([Number(match[1]),value])}if(found.length)return found.sort((a,b)=>a[0]-b[0]).map(([index,value])=>({role:'Input '+(index+1),content:value}));return attrs['input.value']!==undefined?[{role:'Input',content:attrs['input.value']}]:null}function openInferenceVectorSummary(attrs){const vectors=new Map();let redacted=false;for(const [key,value] of Object.entries(attrs)){const match=key.match(/^embedding\.embeddings\.(\d+)\.embedding\.vector$/);if(!match)continue;vectors.set(Number(match[1]),value);if(value==='__REDACTED__')redacted=true}if(!vectors.size)return null;const dimensions=[...new Set([...vectors.values()].filter(Array.isArray).map(value=>value.length))].sort((a,b)=>a-b);return vectors.size+' vector'+(vectors.size===1?'':'s')+(dimensions.length?' · '+dimensions.join('/')+' dimensions':'')+(redacted?' · some vectors redacted':'')}function renderInteractions(spans){const host=$('interactions');host.replaceChildren();const pairs=[['Input',['gen_ai.input.messages','openai_agents.input','langchain.input','llm.input']],['Output',['gen_ai.output.messages','openai_agents.output','langchain.output','llm.output']]];let found=0;for(const span of spans){const attrs=span.attributes||{};const content=pairs.map(([label,keys])=>{const direction=label==='Input'?'input':'output';const openInference=direction==='input'&&attrs['openinference.span.kind']==='EMBEDDING'?openInferenceEmbeddingInputs(attrs):openInferenceMessages(attrs,direction);if(openInference)return [label,openInference];for(const key of keys)if(attrs[key]!==undefined&&attrs[key]!==null&&attrs[key]!=='')return [label,attrs[key]];return null}).filter(Boolean);const model=attrs['gen_ai.request.model']||attrs['llm.response.model_name']||attrs['llm.request.model_name']||attrs['llm.model_name']||attrs['embedding.model_name']||attrs['llm.model'];const hasUsage=['gen_ai.usage.input_tokens','llm.usage.input_tokens','gen_ai.usage.output_tokens','llm.usage.output_tokens','llm.token_count.prompt','llm.token_count.completion','llm.cost.total','llm.cost.prompt','llm.cost.completion','llm.cost_usd','gen_ai.usage.cost'].some(key=>attrs[key]!==undefined);const modelSpan=Boolean(model||hasUsage||content.length||attrs['gen_ai.operation.name']||['LLM','EMBEDDING'].includes(attrs['openinference.span.kind'])||/^(llm|gen_ai)\.|^(openai_agents|anthropic)\.(generation|llm)\b/i.test(span.name));if(!modelSpan)continue;if(found++>=30)break;const card=el('article','interaction-card');const head=el('div','interaction-head');head.append(el('strong','',model||span.name),el('span','muted',span.status+' · '+((span.end_unix_nano-span.start_unix_nano)/1e6).toFixed(1)+' ms'));card.append(head);const input=attrs['gen_ai.usage.input_tokens']??attrs['llm.usage.input_tokens']??attrs['llm.token_count.prompt'];const output=attrs['gen_ai.usage.output_tokens']??attrs['llm.usage.output_tokens']??attrs['llm.token_count.completion'];const totalCost=attrs['llm.cost.total']??attrs['llm.cost_usd']??attrs['gen_ai.usage.cost'];const promptCost=attrs['llm.cost.prompt'];const completionCost=attrs['llm.cost.completion'];const cost=totalCost??([promptCost,completionCost].filter(value=>value!==undefined).reduce((sum,value)=>sum+Number(value),0));const costComplete=attrs['llm.cost_known']!==false&&(totalCost!==undefined||(promptCost!==undefined&&completionCost!==undefined));const stats=el('div','interaction-stats');const isEmbedding=attrs['openinference.span.kind']==='EMBEDDING';stats.append(el('span','pill','Input · '+(input===undefined?'unknown':fmt(input)+' tokens')),el('span','pill','Output · '+(isEmbedding?'n/a':output===undefined?'unknown':fmt(output)+' tokens')));const vectorSummary=isEmbedding?openInferenceVectorSummary(attrs):null;if(vectorSummary)stats.append(el('span','pill','Embeddings · '+vectorSummary));const costValue=cost===undefined?null:Number(cost);stats.append(el('span','pill','Cost · '+(costValue===null||!Number.isFinite(costValue)?'unknown':'$'+costValue.toFixed(6)+(costComplete?'':' · partial'))));for(const [label,value] of [['Prompt cost',promptCost],['Completion cost',completionCost]])if(value!==undefined){const amount=Number(value);stats.append(el('span','pill',label+' · '+(Number.isFinite(amount)?'$'+amount.toFixed(6):'unknown')))}card.append(stats);if(content.length){for(const [label,value] of content){const block=el('div','interaction-block');appendInteraction(block,label,value);card.append(block)}}else card.append(el('div','hint','Input/output content was not captured for this call.'));const source=el('button','activity-source','View source span: '+span.name);source.type='button';source.onclick=()=>{const row=$('span-'+span.span_id);if(row){row.scrollIntoView({behavior:'smooth',block:'center'});const button=row.querySelector('button');if(button){const detail=row.querySelector('.details');if(detail&&detail.hidden)button.click()}}};card.append(source);host.append(card)}if(!found)host.append(el('div','hint','No model-call spans recorded on this trace.'));else if(found>=30)host.append(el('div','hint','Showing the first 30 model calls.'))}
42
+ function spanDepth(span){let cursor=span;const chain=[],seen=new Set();while(cursor&&!traceSpanDepth.has(cursor.span_id)&&!seen.has(cursor.span_id)){seen.add(cursor.span_id);chain.push(cursor);cursor=cursor.parent_span_id?traceSpanById.get(cursor.parent_span_id):null}let depth=cursor&&traceSpanDepth.has(cursor.span_id)?traceSpanDepth.get(cursor.span_id)+1:0;for(let index=chain.length-1;index>=0;index--)traceSpanDepth.set(chain[index].span_id,depth++);return traceSpanDepth.get(span.span_id)||0}async function appendSpanBatch(){await loadSpanPage(false)}async function loadSpanPage(reset=true,queryOverride=null){if(!selected)return;if(!reset&&spanPageLoading)return;const traceId=selected;const requestId=reset?++spanPageRequest:spanPageRequest;if(reset){spanPageCursor=null;renderedSpanCount=0;currentTraceSpans=[];traceSpanById=new Map();traceSpanDepth=new Map();$('timeline').replaceChildren(el('div','hint','Loading spans…'))}const params=new URLSearchParams({limit:'200',q:queryOverride===null?activeSpanQuery:queryOverride,filter:activeSpanFilter});if(!reset&&spanPageCursor)params.set('cursor',spanPageCursor);spanPageLoading=true;try{const page=await get('/api/traces/'+traceId+'/spans?'+params);if(selected!==traceId||requestId!==spanPageRequest)return;if(reset){traceSpanStart=page.start_unix_nano;traceSpanTotal=Math.max(1,page.end_unix_nano-page.start_unix_nano);$('timeline').replaceChildren()}if(!page.total){$('timeline').replaceChildren(el('div','empty',page.trace_total?'No spans match this search or filter.':'No spans recorded for this trace. Progress and correlated logs are shown alongside the run.'))}$('span-count').textContent=page.total?'Loading '+fmt(currentTraceSpans.length+page.spans.length)+' of '+fmt(page.total)+' matching spans':'';for(const span of page.spans){currentTraceSpans.push(span);traceSpanById.set(span.span_id,span);$('timeline').append(drawSpan(span,traceSpanStart,traceSpanTotal,spanDepth(span)))}renderedSpanCount=currentTraceSpans.length;if(!activeSpanQuery&&activeSpanFilter==='all')renderInteractions(currentTraceSpans);spanPageCursor=page.next_cursor;$('more-spans')?.remove();if(spanPageCursor){const more=el('button','refresh','Show next 200 spans · '+fmt(page.total-renderedSpanCount)+' remaining');more.id='more-spans';more.type='button';more.onclick=appendSpanBatch;$('timeline').append(more)}$('span-count').textContent=page.total?'Showing '+fmt(renderedSpanCount)+' of '+fmt(page.total)+' matching spans':''}catch(error){if(requestId===spanPageRequest)$('timeline').replaceChildren(el('div','empty','Could not load spans: '+error.message))}finally{if(requestId===spanPageRequest)spanPageLoading=false}}function renderSpanTimeline(spans,resetLimit=true,pageInfo=null){currentTraceSpans=[];spanPageCursor=pageInfo?.next_cursor||null;traceSpanStart=pageInfo?.start_unix_nano||0;traceSpanTotal=Math.max(1,(pageInfo?.end_unix_nano||0)-traceSpanStart);traceSpanById=new Map();traceSpanDepth=new Map();renderedSpanCount=0;const host=$('timeline');host.replaceChildren();if(pageInfo&&pageInfo.total===0){$('span-count').textContent='';host.append(el('div','empty',pageInfo.trace_total?'No spans match this search or filter.':'No spans recorded for this trace. Progress and correlated logs are shown alongside the run.'));return}for(const span of spans||[]){currentTraceSpans.push(span);traceSpanById.set(span.span_id,span);host.append(drawSpan(span,traceSpanStart,traceSpanTotal,spanDepth(span)))}renderedSpanCount=currentTraceSpans.length;const total=pageInfo?.total??currentTraceSpans.length;if(!total){$('span-count').textContent='';host.append(el('div','empty','No spans recorded for this trace. Progress and correlated logs are shown alongside the run.'));return}if(pageInfo?.next_cursor){const more=el('button','refresh','Show next 200 spans · '+fmt(total-renderedSpanCount)+' remaining');more.id='more-spans';more.type='button';more.onclick=appendSpanBatch;host.append(more)}$('span-count').textContent='Showing '+fmt(renderedSpanCount)+' of '+fmt(total)+' matching spans'}function render(d){const r=d.summary;if(!r)return;$('title').textContent=label(r);$('subtitle').textContent=r.trace_id;$('state').hidden=false;$('state').textContent=r.status;$('state').className='badge'+(['ERROR','FAILED','INCOMPLETE'].includes(r.status)?' error':'');$('duration').textContent=r.duration_ms.toLocaleString()+' ms';$('spans').textContent=fmt(r.span_count);$('input').textContent=fmt(r.input_tokens);$('output').textContent=fmt(r.output_tokens);$('cost').textContent='$'+r.known_cost_usd.toFixed(4)+(r.cost_complete?'':'*');const context=$('context');context.replaceChildren();if(r.session_id)context.append(el('span','pill','Session · '+r.session_id));for(const v of [...r.services,...r.models])context.append(el('span','pill',v));if(r.outcome)context.append(el('p','hint','Outcome: '+r.outcome+(r.steps===null?'':' · '+r.steps+' steps')));if(r.summary)context.append(el('p','hint',r.summary));if(!r.cost_complete)context.append(el('p','hint','* Cost is incomplete or unknown for at least one model call.'));const progress=$('progress');progress.replaceChildren();if(!d.progress.length)progress.append(el('div','hint','No progress events recorded for this trace.'));for(const p of d.progress){const x=el('div','progress');x.append(el('b','',p.stage+(p.percent==null?'':' · '+p.percent+'%')),el('div','',p.message),el('small','',new Date(p.time_ns/1e6).toLocaleTimeString()));progress.append(x)}renderInteractions(d.spans);renderActivity(d.activity||[]);renderSpanTimeline(d.spans,true,d.span_page)}
43
+ function isActivityChange(item){return /\b(create|write|modify|update|delete|remove|rename|move|commit|push|install|configure|deploy|execute|run)\b/i.test((item.domain||'')+' '+(item.action||''))}
44
+ function isActivityError(item){return /error|fail|blocked|denied|policy_denied/i.test(item.result||'')}
45
+ function renderActivity(items,filter='all',subjectFilter=''){const host=$('activity');host.replaceChildren();const bar=el('div','activity-filters');const choices=[['all','All activity',items.length],['changes','Changes',items.filter(isActivityChange).length],['errors','Errors',items.filter(isActivityError).length]];for(const [value,label,count] of choices){const button=el('button','activity-filter'+(filter===value?' active':''),label+' · '+count);button.type='button';button.setAttribute('aria-pressed',String(filter===value));button.onclick=()=>renderActivity(items,value,subjectFilter);bar.append(button)}host.append(bar);if(!items.length){host.append(el('div','hint','No agent.activity events or standard tool-call spans recorded.'));return}const filtered=items.filter(item=>filter==='all'||(filter==='changes'&&isActivityChange(item))||(filter==='errors'&&isActivityError(item)));const groups=new Map();for(const item of filtered){const key=item.subject||((item.domain||'other')+' · no subject');const group=groups.get(key)||{key,count:0,errors:0,actions:new Set()};group.count++;group.errors+=isActivityError(item)?1:0;if(item.action)group.actions.add(item.action);groups.set(key,group)}if(groups.size){const heading=el('div','activity-subject-heading','Activity by subject');host.append(heading);const subjects=el('div','activity-subject-groups');const ordered=[...groups.values()].sort((a,b)=>b.count-a.count||a.key.localeCompare(b.key));for(const group of ordered.slice(0,12)){const button=el('button','activity-subject-group'+(subjectFilter===group.key?' active':''));button.type='button';button.setAttribute('aria-pressed',String(subjectFilter===group.key));button.title=[...group.actions].join(', ')+(group.errors?' · '+group.errors+' errors':'');button.append(el('span','activity-subject-name',group.key),el('span','activity-subject-count',String(group.count)+(group.errors?' · '+group.errors+' errors':'')));button.onclick=()=>renderActivity(items,filter,subjectFilter===group.key?'':group.key);subjects.append(button)}if(ordered.length>12)subjects.append(el('div','hint','Showing 12 of '+ordered.length+' subjects.'));host.append(subjects)}const visible=filtered.filter(item=>!subjectFilter||(item.subject||((item.domain||'other')+' · no subject'))===subjectFilter);if(!visible.length){host.append(el('div','hint','No activity matches this filter.'));return}for(const item of visible){const row=el('div','activity-row');const head=el('div','activity-head');head.append(el('span','activity-domain',item.domain),el('span','',item.action));head.append(el('span','activity-result'+(isActivityError(item)?' '+String(item.result).toLowerCase():''),item.result));row.append(head);if(item.subject)row.append(el('div','activity-subject',item.subject));if(item.detail)row.append(el('div','muted',item.detail));const foot=el('div','activity-foot');const who=[item.actor,item.service].filter(Boolean).join(' · ');foot.append(el('span','',who),el('span','',new Date(item.time_unix_nano/1e6).toLocaleTimeString()));const source=el('button','activity-source',item.source==='span'?'Open span':'Open log');source.onclick=async()=>{let target=document.getElementById(item.source==='span'?'span-'+item.span_id:'log-'+item.source_id);if(!target&&item.source==='span'){activeSpanQuery=item.span_id;activeSpanFilter='all';$('span-search').value=activeSpanQuery;$('span-filter').value='all';await loadSpanPage(true);target=document.getElementById('span-'+item.span_id)}if(target){target.scrollIntoView({behavior:'smooth',block:'center'});if(item.source==='span')target.querySelector('button')?.click()}};foot.append(source);row.append(foot);host.append(row)}}
46
+ function renderLogs(items){const host=$('logs');host.replaceChildren();if(!items.length){host.append(el('div','hint','No correlated logs recorded.'));return}for(const log of items){const row=el('div','progress');row.id='log-'+log.id;row.append(el('b','',(log.severity_text||log.event_name||'LOG')+' · '+log.service),el('div','',typeof log.body==='string'?log.body:JSON.stringify(log.body)),el('small','',new Date(log.time_unix_nano/1e6).toLocaleTimeString()));host.append(row)}}
47
+ function renderOutcome(outcome){const host=$('outcome-details');host.replaceChildren();if(!outcome){host.append(el('div','hint','No outcome has been joined to this trace.'));return}host.append(el('b','',outcome.passed?'Passed':'Failed'));host.append(el('div','muted',outcome.task_id+' · '+outcome.provider_model));if(outcome.error)host.append(el('p','',outcome.error));for(const score of outcome.scores){host.append(el('div','progress',(score.passed?'✓ ':'✕ ')+(score.scorer||'check')+' · '+(score.detail||'')))}}
48
+ function renderUsage(data){
49
+ const points=data.points||[],summary=$('usage-summary'),svg=$('usage-chart'),legend=$('usage-legend');
50
+ summary.replaceChildren();svg.replaceChildren();legend.replaceChildren();
51
+ const totals={runs:0,errors:0,input_tokens:0,output_tokens:0,known_cost_usd:0,unknown_cost_runs:0};
52
+ for(const point of points)for(const key of Object.keys(totals))totals[key]+=Number(point[key]||0);
53
+ for(const [key,label] of [['runs','runs'],['errors','errors'],['input_tokens','input tokens'],['output_tokens','output tokens']])summary.append(el('span','pill',fmt(totals[key])+' '+label));
54
+ summary.append(el('span','pill','$'+totals.known_cost_usd.toFixed(4)+' known cost'));
55
+ if(totals.unknown_cost_runs)summary.append(el('span','pill',fmt(totals.unknown_cost_runs)+' runs with unknown cost'));
56
+ $('usage-caption').textContent=(data.service||'All agents')+' · '+data.days+' days · '+(data.step==='hour'?'hourly':'daily')+' totals from recorded traces';
57
+ if(!points.length){const text=svgNode('text',{x:450,y:125,'text-anchor':'middle'});text.textContent='No usage recorded in this period.';svg.append(text);return}
58
+ const width=900,height=250,left=64,right=20,top=16,bottom=38,plotW=width-left-right,plotH=height-top-bottom;
59
+ let maximum=1;for(const point of points)maximum=Math.max(maximum,Number(point.input_tokens||0),Number(point.output_tokens||0));
60
+ for(let i=0;i<=4;i++){const fraction=i/4,y=top+plotH*(1-fraction);svg.append(svgNode('line',{x1:left,y1:y,x2:width-right,y2:y}));const tick=svgNode('text',{x:left-8,y:y+4,'text-anchor':'end'});tick.textContent=fmt(Math.round(maximum*fraction));svg.append(tick)}
61
+ for(const [field,label,color] of [['input_tokens','Input tokens','#77aaff'],['output_tokens','Output tokens','#73d9b0']]){
62
+ const coordinates=points.map((point,index)=>[left+index/Math.max(1,points.length-1)*plotW,top+plotH*(1-Number(point[field]||0)/maximum)]);
63
+ svg.append(svgNode('path',{d:coordinates.map(([x,y],i)=>(i?'L':'M')+x.toFixed(1)+' '+y.toFixed(1)).join(' '),stroke:color}));
64
+ points.forEach((point,index)=>{const [x,y]=coordinates[index],dot=svgNode('circle',{cx:x,cy:y,r:2.5,fill:color}),title=svgNode('title');title.textContent=new Date(Number(point.start_unix_nano)/1e6).toLocaleString()+': '+fmt(point[field])+' '+label.toLowerCase();dot.append(title);svg.append(dot)});
65
+ const tag=el('span','pill',label);tag.style.borderColor=color;legend.append(tag);
66
+ }
67
+ for(const [point,x,anchor] of [[points[0],left,'start'],[points.at(-1),width-right,'end']]){const tick=svgNode('text',{x,y:height-12,'text-anchor':anchor});const date=new Date(Number(point.start_unix_nano)/1e6);tick.textContent=data.step==='hour'?date.toLocaleString():date.toLocaleDateString();svg.append(tick)}
68
+ }
69
+ function svgNode(name,attrs={}){const n=document.createElementNS('http://www.w3.org/2000/svg',name);for(const [key,value] of Object.entries(attrs))n.setAttribute(key,String(value));return n}
70
+ async function openExemplarTrace(exemplar){if(!exemplar||!exemplar.trace_id)return;await openTrace(exemplar.trace_id);if(!exemplar.span_id)return;let target=document.getElementById('span-'+exemplar.span_id);if(!target){activeSpanQuery=exemplar.span_id;activeSpanFilter='all';$('span-search').value=activeSpanQuery;$('span-filter').value='all';await loadSpanPage(true);target=document.getElementById('span-'+exemplar.span_id)}if(target){target.scrollIntoView({behavior:'smooth',block:'center'});const detail=target.querySelector('.details');if(detail&&detail.hidden)target.querySelector('button')?.click()}}function metricFields(metric,points){if(['gauge','sum'].includes(metric.kind))return [['value','Value']];const candidates=[['count','Count'],['sum','Sum'],['min','Minimum'],['max','Maximum']];const fields=candidates.filter(([key])=>points.some(point=>Number.isFinite(Number(point.data[key]))&&point.data[key]!=null));if(metric.kind==='histogram'&&points.some(point=>Array.isArray(point.data.bucketCounts)&&Array.isArray(point.data.explicitBounds)&&point.data.bucketCounts.length))fields.push(['buckets','Bucket distribution']);if(metric.kind==='exponentialHistogram'&&points.some(point=>point.data.positive||point.data.negative||point.data.zeroCount))fields.push(['buckets','Bucket distribution']);const quantiles=new Set();for(const point of points)for(const item of point.data.quantileValues||[])if(Number.isFinite(Number(item.quantile)))quantiles.add(Number(item.quantile));for(const q of [...quantiles].sort((a,b)=>a-b))fields.push(['quantile:'+q,'p'+Math.round(q*100)]);return fields}
71
+ function metricFieldValue(point,field){if(field.startsWith('quantile:')){const q=Number(field.slice(9));return (point.data.quantileValues||[]).find(item=>Number(item.quantile)===q)?.value}return point.data[field]}
72
+ function histogramBuckets(metric,point){const data=point.data||{};if(metric.kind==='histogram'){const bounds=data.explicitBounds||[],counts=data.bucketCounts||[];if(!counts.length)return [];if(!bounds.length)return [{label:'(-∞, +∞)',count:Number(counts[0])}].filter(item=>Number.isFinite(item.count)&&item.count>=0);if(counts.length!==bounds.length+1)return [];return counts.map((count,index)=>({label:index===0?'≤ '+bounds[0]:index<bounds.length?'('+bounds[index-1]+', '+bounds[index]+']':'('+bounds.at(-1)+', +∞)',count:Number(count)})).filter(item=>Number.isFinite(item.count)&&item.count>=0)}const scale=Number(data.scale||0),power=2**(-scale);if(!Number.isFinite(power)||power>2048)return [];const base=2**power;if(!Number.isFinite(base)||base<=1)return [];const buckets=[];for(const [sign,range] of [['negative',data.negative],['positive',data.positive]]){if(!range)continue;for(let offset=0;offset<(range.bucketCounts||[]).length;offset++){const index=Number(range.offset||0)+offset;const low=base**index,high=base**(index+1),count=Number(range.bucketCounts[offset]);buckets.push({order:sign==='negative'?-high:low,label:sign==='negative'?'[-'+high+', -'+low+')':'('+low+', '+high+']',count})}}const zero=Number(data.zeroCount||0);if(zero||Number(data.zeroThreshold||0)>0){const threshold=Number(data.zeroThreshold||0);buckets.push({order:-threshold,label:threshold?'[-'+threshold+', '+threshold+']':'zero',count:zero})}return buckets.filter(item=>Number.isFinite(item.count)&&item.count>=0).sort((a,b)=>a.order-b.order)}function renderHistogramBuckets(metric,points,svg,legend){const candidates=points.filter(point=>histogramBuckets(metric,point).length).sort((a,b)=>Number(b.time_unix_nano)-Number(a.time_unix_nano));if(!candidates.length){$('metric-caption').textContent=metric.name+' · no bucket distribution recorded';return}const point=candidates[0],buckets=histogramBuckets(metric,point),width=900,height=280,margin={left:64,right:20,top:16,bottom:50},plotW=width-margin.left-margin.right,plotH=height-margin.top-margin.bottom,max=Math.max(1,...buckets.map(item=>item.count));for(let i=0;i<=4;i++){const fraction=i/4,y=margin.top+plotH*(1-fraction);svg.append(svgNode('line',{x1:margin.left,y1:y,x2:width-margin.right,y2:y}));const label=svgNode('text',{x:margin.left-8,y:y+4,'text-anchor':'end'});label.textContent=(max*fraction).toPrecision(3);svg.append(label)}const slot=plotW/buckets.length,barW=Math.max(1,slot*.72);for(let i=0;i<buckets.length;i++){const item=buckets[i],barH=Math.max(0,item.count/max*plotH),x=margin.left+i*slot+(slot-barW)/2,y=margin.top+plotH-barH;const rect=svgNode('rect',{x,y,width:barW,height:barH,fill:'#77aaff',rx:2});const title=svgNode('title');title.textContent=item.label+': '+item.count+' observations';rect.append(title);svg.append(rect);const stride=Math.max(1,Math.ceil(buckets.length/12));if(i%stride===0||i===buckets.length-1){const tick=svgNode('text',{x:x+barW/2,y:height-16,'text-anchor':'middle'});tick.textContent=item.label.length>15?item.label.slice(0,12)+'…':item.label;svg.append(tick)}}const tag=el('span','pill',JSON.stringify({attributes:point.attributes,resource:point.resource_attributes,scope:point.scope_name}).slice(0,110));legend.append(tag);const seenExemplars=new Set();for(const exemplar of (point.data.exemplars||[]).slice(0,8)){if(!exemplar.trace_id||seenExemplars.has(exemplar.trace_id+':'+(exemplar.span_id||'')))continue;seenExemplars.add(exemplar.trace_id+':'+(exemplar.span_id||''));const link=el('button','activity-source','Open exemplar '+(exemplar.span_id?'span':'trace')+' '+(exemplar.span_id||exemplar.trace_id).slice(0,8));link.type='button';link.onclick=()=>openExemplarTrace(exemplar);legend.append(link)}$('metric-caption').textContent=metric.name+' · latest bucket distribution · '+new Date(Number(point.time_unix_nano)/1e6).toLocaleString()+' · '+(metric.temporality===2?'cumulative':metric.temporality===1?'delta':'unspecified temporality')}function renderMetricChart(metric,points,field){const svg=$('metric-chart');svg.replaceChildren();const legend=$('metric-legend');legend.replaceChildren();if(!metric){$('metric-caption').textContent='Select an OTLP metric';return}field=field||(['gauge','sum'].includes(metric.kind)?'value':'count');if(field==='buckets'){renderHistogramBuckets(metric,points,svg,legend);return}const groups=new Map();for(const point of points){const raw=metricFieldValue(point,field);const value=Number(raw);if(raw==null||!Number.isFinite(value))continue;const attrs=JSON.stringify([point.attributes,point.resource_attributes,point.scope_name]);if(!groups.has(attrs))groups.set(attrs,[]);groups.get(attrs).push({time:Number(point.time_unix_nano),value,point})}const series=[...groups.entries()].map(([key,items])=>({key,items:items.sort((a,b)=>a.time-b.time)})).filter(s=>s.items.length);const fieldLabel=metricFields(metric,points).find(item=>item[0]===field)?.[1]||field;if(!series.length){$('metric-caption').textContent=metric.name+' · no numeric '+fieldLabel.toLowerCase()+' values';svg.append(svgNode('text',{x:450,y:135,'text-anchor':'middle'}));svg.firstChild.textContent='No numeric values available for this metric field.';return}const width=900,height=280,margin={left:64,right:20,top:16,bottom:42},plotW=width-margin.left-margin.right,plotH=height-margin.top-margin.bottom;const values=series.flatMap(s=>s.items.map(p=>p.value));let low=Math.min(...values),high=Math.max(...values);if(low===high){const pad=Math.max(1,Math.abs(low)*.05);low-=pad;high+=pad}const times=series.flatMap(s=>s.items.map(p=>p.time));const minT=Math.min(...times),maxT=Math.max(...times),spanT=Math.max(1,maxT-minT);for(let i=0;i<=4;i++){const fraction=i/4,y=margin.top+plotH*fraction,value=high-(high-low)*fraction;svg.append(svgNode('line',{x1:margin.left,y1:y,x2:width-margin.right,y2:y}));const label=svgNode('text',{x:margin.left-8,y:y+4,'text-anchor':'end'});label.textContent=value.toPrecision(4);svg.append(label)}const colors=['#77aaff','#86e0b4','#f0bb65','#e681a0','#b29aff','#55c3cf','#ff8e62','#ccd56d'];series.slice(0,12).forEach((item,index)=>{const color=colors[index%colors.length];const coords=item.items.map(p=>[margin.left+((p.time-minT)/spanT)*plotW,margin.top+((high-p.value)/(high-low))*plotH]);const path=svgNode('path',{d:coords.map(([x,y],i)=>(i?'L':'M')+x.toFixed(1)+' '+y.toFixed(1)).join(' '),stroke:color});svg.append(path);item.items.forEach((entry,i)=>{const [x,y]=coords[i];const circle=svgNode('circle',{cx:x,cy:y,r:3,fill:color});const exemplar=(entry.point.data.exemplars||[]).find(item=>item.trace_id);const title=svgNode('title');title.textContent=new Date(entry.time/1e6).toLocaleString()+': '+entry.value+' '+(metric.unit||'')+(exemplar?' · click to open trace '+exemplar.trace_id:'');circle.append(title);if(exemplar){circle.style.cursor='pointer';circle.onclick=()=>openExemplarTrace(exemplar)}svg.append(circle)});const point=item.items.at(-1).point;const desc=JSON.stringify({attributes:point.attributes,resource:point.resource_attributes,scope:point.scope_name});const tag=el('span','pill',desc.slice(0,72));tag.style.borderColor=color;legend.append(tag)});const first=svgNode('text',{x:margin.left,y:height-12});first.textContent=new Date(minT/1e6).toLocaleString();svg.append(first);const last=svgNode('text',{x:width-margin.right,y:height-12,'text-anchor':'end'});last.textContent=new Date(maxT/1e6).toLocaleString();svg.append(last);const mode=metric.temporality===2?'cumulative':metric.temporality===1?'delta':'unspecified temporality';const unit=field==='count'?'observations':(metric.unit||'unitless');$('metric-caption').textContent=metric.name+' · '+fieldLabel+' ('+unit+') · '+metric.kind+' · '+mode+' · '+points.length+' points'+(points.length>=500?' (latest 500)':'')+(series.length>12?' · showing 12 of '+series.length+' attribute series':'')}
73
+ async function renderSelectedMetric(){const raw=$('metric').value;const fieldSelect=$('metric-field');if(!raw){metricForChart=null;metricPointsCache=[];fieldSelect.hidden=true;fieldSelect.replaceChildren();renderMetricChart(null,[]);return}try{const metric=JSON.parse(raw);const query=new URLSearchParams({limit:'500',service:metric.service,name:metric.name,kind:metric.kind,temporality:String(metric.temporality),scope_name:metric.scope_name,unit:metric.unit,description:metric.description});const data=await get('/api/measurements?'+query.toString());metricForChart=metric;metricPointsCache=data.points;const options=metricFields(metric,data.points);const previous=fieldSelect.value;fieldSelect.replaceChildren();for(const [value,label] of options)fieldSelect.add(new Option(label,value));fieldSelect.hidden=options.length<2;if(options.some(option=>option[0]===previous))fieldSelect.value=previous;else if(options.length)fieldSelect.value=options[0][0];renderMetricChart(metric,data.points,fieldSelect.value)}catch(e){$('metric-caption').textContent='Metric query error: '+e.message}}
74
+
75
+ function renderMeasurements(points){const host=$('measurements');host.replaceChildren();if(!points.length){host.append(el('div','hint','No OTLP metric points recorded.'));return}for(const point of points.slice(0,12)){const d=point.data;const value=d.value!==undefined?d.value:(d.count!==undefined?('count '+d.count+(d.sum!==undefined?' · sum '+d.sum:'')):'distribution');const type=point.kind+(point.temporality===2?' · cumulative':point.temporality===1?' · delta':'');const pill=el('span','pill',point.name+' · '+value+' '+point.unit+' · '+type+' · '+point.service);pill.title=JSON.stringify(point.attributes);host.append(pill);for(const exemplar of (d.exemplars||[]).slice(0,3)){if(!exemplar.trace_id)continue;const link=el('button','activity-source','Open exemplar trace '+exemplar.trace_id.slice(0,8));link.type='button';link.onclick=async()=>{await openTrace(exemplar.trace_id);if(exemplar.span_id){const target=$('span-'+exemplar.span_id);if(target){target.scrollIntoView({behavior:'smooth',block:'center'});const detail=target.querySelector('.details');if(detail&&detail.hidden)target.querySelector('button')?.click()}}};host.append(link)}}}
76
+ function renderFindings(items){const host=$('findings');host.replaceChildren();if(!items.length){host.append(el('div','hint','No findings from the current rules.'));return}for(const finding of items){const row=el('div','progress');row.append(el('b','',finding.title+' · '+finding.severity),el('div','',finding.detail));for(const source of finding.evidence){const jump=el('button','refresh',source.type+' '+source.id.slice(0,8));jump.style.marginTop='8px';jump.onclick=()=>document.getElementById(source.type==='span'?'span-'+source.id:source.type==='log'?'log-'+source.id:'outcome')?.scrollIntoView({behavior:'smooth',block:'center'});row.append(jump)}host.append(row)}}
77
+ async function refreshReview(){if(reviewLoading)return;reviewLoading=true;try{const page=await get('/api/findings?limit=100');if(!reviewInitialized){reviewItems=page.findings;reviewCursor=page.next_cursor;reviewHasMore=page.has_more;reviewInitialized=true}else{const known=new Set(reviewItems.map(item=>item.id));reviewItems=[...page.findings,...reviewItems.filter(item=>!known.has(item.id))];if(!reviewCursor&&page.has_more){reviewCursor=page.next_cursor;reviewHasMore=true}}}finally{reviewLoading=false;renderReview()}}async function loadOlderFindings(){if(!reviewCursor||reviewLoading)return;reviewLoading=true;try{const page=await get('/api/findings?'+new URLSearchParams({limit:'100',cursor:reviewCursor}));const known=new Set(reviewItems.map(item=>item.id));reviewItems.push(...page.findings.filter(item=>!known.has(item.id)));reviewCursor=page.next_cursor;reviewHasMore=page.has_more}catch(e){$('connection').textContent='Finding query error: '+e.message}finally{reviewLoading=false;renderReview()}}function renderReview(){const host=$('review');host.replaceChildren();const counts={all:reviewItems.length,high:reviewItems.filter(item=>item.severity==='high').length,medium:reviewItems.filter(item=>item.severity==='medium').length};const bar=el('div','activity-filters');for(const [value,label] of [['all','All'],['high','High'],['medium','Medium']]){const button=el('button','activity-filter'+(reviewSeverity===value?' active':''),label+' · '+counts[value]);button.type='button';button.setAttribute('aria-pressed',String(reviewSeverity===value));button.onclick=()=>{reviewSeverity=value;renderReview()};bar.append(button)}host.append(bar);if(reviewHasMore)host.append(el('div','hint','Counts cover loaded findings; load older findings to review the rest.'));const visible=reviewItems.filter(item=>reviewSeverity==='all'||item.severity===reviewSeverity);if(!visible.length)host.append(el('div','empty',reviewItems.length?'No findings at this severity.':'No findings yet.'));for(const finding of visible){const button=el('button','run',finding.title+' · '+finding.severity);button.onclick=async()=>{await openTrace(finding.trace_id);const source=finding.evidence&&finding.evidence[0];if(source&&['span','log','outcome'].includes(source.type)){const target=document.getElementById(source.type==='span'?'span-'+source.id:source.type==='log'?'log-'+source.id:'outcome');target?.scrollIntoView({behavior:'smooth',block:'center'})}};host.append(button)}if(reviewHasMore){const more=el('button','refresh',reviewLoading?'Loading…':'Load older findings');more.type='button';more.disabled=reviewLoading;more.onclick=loadOlderFindings;host.append(more)}}
78
+ function renderRelated(items){const host=$('related');host.replaceChildren();if(!items.length){host.append(el('div','hint','No linked traces recorded.'));return}for(const item of items){const button=el('button','run',item.direction+' · '+(item.summary?label(item.summary):item.trace_id)+' · '+item.edges.length+' link(s)');button.disabled=!item.summary;button.onclick=()=>openTrace(item.trace_id);host.append(button)}}
79
+ async function openTrace(id){selected=id;spanPageRequest++;spanPageLoading=false;drawRuns();try{const params=new URLSearchParams({span_limit:'200',q:'',filter:'all'});const data=await get('/api/traces/'+id+'?'+params);if(selected!==id)return;render(data);if(activeSpanQuery||activeSpanFilter!=='all')loadSpanPage(true);renderLogs(data.logs||[]);renderFindings(data.findings||[]);renderOutcome(data.outcome);const related=await get('/api/traces/'+id+'/related');if(selected===id)renderRelated(related.related)}catch(e){$('timeline').textContent=e.message}}
80
+ function clearTraceView(){selected=null;$('title').textContent='No matching traces';$('subtitle').textContent='Adjust filters or select another endpoint.';$('state').hidden=true;for(const id of ['duration','spans','input','output','cost'])$(id).textContent='—';renderSpanTimeline([]);$('timeline').replaceChildren(el('div','empty','No traces match the selected filters.'));$('span-count').textContent='';$('progress').replaceChildren(el('div','hint','Progress events will appear here.'));renderActivity([]);renderInteractions([]);renderLogs([]);$('context').replaceChildren(el('div','hint','No trace selected.'));renderFindings([]);renderOutcome(null);renderRelated([])}
81
+ async function refresh(reset=false){try{const service=$('service').value;const status=$('status').value;const session=$('session').value;const search=$('trace-search').value.trim();const endpoint=activeEndpoint;const changed=service!==activeService||status!==activeStatus||session!==activeSession||search!==activeQuery||endpoint!==loadedEndpoint;if(reset||changed){runs=[];nextCursor=null;activeService=service;activeStatus=status;activeSession=session;activeQuery=search;loadedEndpoint=endpoint}const query='limit=100&service='+encodeURIComponent(service)+'&status='+encodeURIComponent(status)+'&session_id='+encodeURIComponent(session)+'&q='+encodeURIComponent(search)+'&endpoint_id='+encodeURIComponent(endpoint);const data=await get('/api/traces?'+query);if(reset||changed){runs=data.traces;nextCursor=data.next_cursor}else{const known=new Set(data.traces.map(x=>x.trace_id));runs=[...data.traces,...runs.filter(x=>!known.has(x.trace_id))];if(!nextCursor)nextCursor=data.next_cursor}const all=await get('/api/metrics');await refreshAgentConnections(all);const usageKey=JSON.stringify({days:$('usage-days').value||'7',service});if(usageKey!==usageCacheKey||Date.now()-usageLoadedAt>30000){const usageData=await get('/api/usage-series?'+new URLSearchParams({days:$('usage-days').value||'7',step:Number($('usage-days').value||7)===1?'hour':'day',service}));renderUsage(usageData);usageCacheKey=usageKey;usageLoadedAt=Date.now()}if(!sessionCache||Date.now()-sessionLoadedAt>30000){sessionCache=await get('/api/sessions?limit=100');sessionLoadedAt=Date.now()}const sessionData=sessionCache;const endpointData=await get('/api/endpoints?limit=100');renderEndpoints(endpointData.endpoints);await refreshReview();renderMeasurements((await get('/api/measurements?limit=100')).points);if(reset||!metricCatalog)metricCatalog=(await get('/api/metric-catalog?limit=500')).metrics;const metricSelect=$('metric');const currentMetric=metricSelect.value;metricSelect.replaceChildren(new Option(metricCatalog.length?'Select a metric':'No metrics recorded',''));for(const item of metricCatalog){const key=JSON.stringify({service:item.service,name:item.name,kind:item.kind,temporality:item.temporality,scope_name:item.scope_name,unit:item.unit,description:item.description});const label=item.name+' · '+item.service+' · '+item.kind+' · '+item.point_count;metricSelect.add(new Option(label,key))}if(currentMetric&&[...metricSelect.options].some(option=>option.value===currentMetric))metricSelect.value=currentMetric;else if(metricSelect.options.length>1)metricSelect.selectedIndex=1;await renderSelectedMetric();const serviceSelect=$('service');const current=serviceSelect.value;serviceSelect.replaceChildren(new Option('All services',''));for(const name of all.services)serviceSelect.add(new Option(name,name));serviceSelect.value=current;const sessionSelect=$('session');const currentSession=sessionSelect.value;sessionSelect.replaceChildren(new Option('All sessions',''));for(const item of sessionData.sessions){const signals=[];signals.push(fmt(item.trace_count)+' traces');const tokens=Number(item.input_tokens||0)+Number(item.output_tokens||0);if(tokens)signals.push(fmt(tokens)+' tokens');if(item.issues)signals.push(fmt(item.issues)+' runs with issues');const label=item.session_id.slice(0,40)+' · '+signals.join(' · ');sessionSelect.add(new Option(label,item.session_id))}if(currentSession&&!sessionData.sessions.some(item=>item.session_id===currentSession))sessionSelect.add(new Option(currentSession.slice(0,40),currentSession));sessionSelect.value=currentSession;renderSessionSummary(sessionData.sessions,currentSession);drawRuns();$('older').hidden=!nextCursor;$('older').disabled=!nextCursor;$('connection').textContent=fmt(all.runs)+' runs · '+fmt(all.spans)+' spans · '+fmt(all.logs)+' logs · '+fmt(all.metric_points)+' metric points · '+fmt(all.failed_outcomes)+' failed outcomes';if(selected){if(runs.some(r=>r.trace_id===selected))await openTrace(selected);else if(runs.length)await openTrace(runs[0].trace_id);else clearTraceView()}else if(runs.length)await openTrace(runs[0].trace_id);else clearTraceView()}catch(e){$('connection').textContent='Connection error: '+e.message}}
82
+ async function loadOlder(){if(!nextCursor)return;try{const query='limit=100&service='+encodeURIComponent(activeService)+'&status='+encodeURIComponent(activeStatus)+'&session_id='+encodeURIComponent(activeSession)+'&q='+encodeURIComponent(activeQuery)+'&endpoint_id='+encodeURIComponent(activeEndpoint)+'&cursor='+encodeURIComponent(nextCursor);const data=await get('/api/traces?'+query);const known=new Set(runs.map(x=>x.trace_id));runs.push(...data.traces.filter(x=>!known.has(x.trace_id)));nextCursor=data.next_cursor;drawRuns();$('older').hidden=!nextCursor;$('older').disabled=!nextCursor}catch(e){$('connection').textContent='Connection error: '+e.message}}
83
+ $('span-search').oninput=()=>{clearTimeout(spanSearchTimer);spanSearchTimer=setTimeout(()=>{activeSpanQuery=$('span-search').value.trim().toLowerCase();loadSpanPage(true)},150)};$('span-filter').onchange=()=>{activeSpanFilter=$('span-filter').value;loadSpanPage(true)};$('refresh').onclick=()=>refresh(true);$('older').onclick=loadOlder;$('review').addEventListener('keydown',event=>{if(event.key==='Escape'&&reviewSeverity!=='all'){reviewSeverity='all';renderReview()}});$('clear-endpoint-filter').onclick=()=>{activeEndpoint='';refresh(true)};$('service').onchange=()=>refresh(true);$('status').onchange=()=>refresh(true);$('session').onchange=()=>refresh(true);$('trace-search').oninput=()=>{clearTimeout(searchTimer);searchTimer=setTimeout(()=>refresh(true),250)};$('clear-search').onclick=()=>{$('trace-search').value='';refresh(true)};$('metric').onchange=renderSelectedMetric;$('metric-field').onchange=()=>renderMetricChart(metricForChart,metricPointsCache,$('metric-field').value);$('usage-days').onchange=()=>refresh(true);refresh(true);setInterval(()=>refresh(false),5000);
84
+ </script></body></html>
@@ -0,0 +1 @@
1
+ """Optional adapters for agent frameworks."""