@raquezha/notrace 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +678 -0
- package/index.ts +692 -0
- package/package.json +28 -0
- package/tsconfig.json +14 -0
package/index.ts
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* html-observability extension
|
|
7
|
+
*
|
|
8
|
+
* Captures execution traces from the pi coding agent sessions and writes
|
|
9
|
+
* a self-contained, interactive, beautiful HTML report to the active task's
|
|
10
|
+
* workspace at the end of the session.
|
|
11
|
+
*/
|
|
12
|
+
export default function (pi: ExtensionAPI) {
|
|
13
|
+
const events: any[] = [];
|
|
14
|
+
const sessionStartTime = Date.now();
|
|
15
|
+
let traceId = "";
|
|
16
|
+
let activeLlmPayload: any = null;
|
|
17
|
+
let llmStartTime = 0;
|
|
18
|
+
const activeToolTimes: Record<string, number> = {};
|
|
19
|
+
|
|
20
|
+
// Helper to extract active task path from .workflow/active_task.json
|
|
21
|
+
function getActiveTaskDir(cwd: string): string {
|
|
22
|
+
try {
|
|
23
|
+
const activeTaskJsonPath = path.join(cwd, ".workflow", "active_task.json");
|
|
24
|
+
if (existsSync(activeTaskJsonPath)) {
|
|
25
|
+
const content = JSON.parse(readFileSync(activeTaskJsonPath, "utf-8"));
|
|
26
|
+
if (content.taskPath) {
|
|
27
|
+
return path.resolve(cwd, content.taskPath);
|
|
28
|
+
} else if (content.active_task) {
|
|
29
|
+
return path.resolve(cwd, ".workflow", "tasks", content.active_task);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// fallback
|
|
34
|
+
}
|
|
35
|
+
const defaultDir = path.join(cwd, ".workflow");
|
|
36
|
+
if (!existsSync(defaultDir)) {
|
|
37
|
+
try { mkdirSync(defaultDir, { recursive: true }); } catch {}
|
|
38
|
+
}
|
|
39
|
+
return defaultDir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 1. Session start
|
|
43
|
+
pi.on("session_start", async (event, ctx) => {
|
|
44
|
+
traceId = ctx.sessionManager.getSessionId() || `session-${Date.now()}`;
|
|
45
|
+
events.push({
|
|
46
|
+
type: "session_start",
|
|
47
|
+
timestamp: Date.now(),
|
|
48
|
+
reason: event.reason,
|
|
49
|
+
sessionFile: ctx.sessionManager.getSessionFile() || ""
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 2. Turn start
|
|
54
|
+
pi.on("turn_start", async (event, ctx) => {
|
|
55
|
+
events.push({
|
|
56
|
+
type: "turn_start",
|
|
57
|
+
timestamp: Date.now()
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 3. Turn end
|
|
62
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
63
|
+
events.push({
|
|
64
|
+
type: "turn_end",
|
|
65
|
+
timestamp: Date.now()
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// 4. Tool start
|
|
70
|
+
pi.on("tool_execution_start", async (event, ctx) => {
|
|
71
|
+
const { toolCallId, toolName, args } = event;
|
|
72
|
+
activeToolTimes[toolCallId] = Date.now();
|
|
73
|
+
events.push({
|
|
74
|
+
type: "tool_start",
|
|
75
|
+
toolCallId,
|
|
76
|
+
toolName,
|
|
77
|
+
args,
|
|
78
|
+
timestamp: Date.now()
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// 5. Tool end
|
|
83
|
+
pi.on("tool_execution_end", async (event, ctx) => {
|
|
84
|
+
const { toolCallId, toolName, result, isError } = event;
|
|
85
|
+
const startTime = activeToolTimes[toolCallId] || Date.now();
|
|
86
|
+
const durationMs = Date.now() - startTime;
|
|
87
|
+
|
|
88
|
+
events.push({
|
|
89
|
+
type: "tool_end",
|
|
90
|
+
toolCallId,
|
|
91
|
+
toolName,
|
|
92
|
+
result,
|
|
93
|
+
isError,
|
|
94
|
+
durationMs,
|
|
95
|
+
timestamp: Date.now()
|
|
96
|
+
});
|
|
97
|
+
delete activeToolTimes[toolCallId];
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 6. LLM call start (capture payload)
|
|
101
|
+
pi.on("before_provider_request", async (event, ctx) => {
|
|
102
|
+
activeLlmPayload = event.payload;
|
|
103
|
+
llmStartTime = Date.now();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// 7. LLM call end (capture generation / usage)
|
|
107
|
+
pi.on("message_end", async (event, ctx) => {
|
|
108
|
+
const { message } = event;
|
|
109
|
+
if (message.role !== "assistant") return;
|
|
110
|
+
|
|
111
|
+
const durationMs = llmStartTime > 0 ? Date.now() - llmStartTime : 0;
|
|
112
|
+
|
|
113
|
+
events.push({
|
|
114
|
+
type: "llm_completion",
|
|
115
|
+
model: message.model || "unknown",
|
|
116
|
+
provider: message.provider || "unknown",
|
|
117
|
+
inputPayload: activeLlmPayload,
|
|
118
|
+
outputContent: message.content,
|
|
119
|
+
usage: message.usage,
|
|
120
|
+
durationMs,
|
|
121
|
+
timestamp: Date.now()
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
activeLlmPayload = null;
|
|
125
|
+
llmStartTime = 0;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// 8. Shutdown: Compile the HTML report and write to disk
|
|
129
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
130
|
+
const sessionEndTime = Date.now();
|
|
131
|
+
const totalDurationMs = sessionEndTime - sessionStartTime;
|
|
132
|
+
|
|
133
|
+
const taskDir = getActiveTaskDir(ctx.cwd);
|
|
134
|
+
const reportPath = path.join(taskDir, "notrace.html");
|
|
135
|
+
|
|
136
|
+
// Gather statistics
|
|
137
|
+
let totalTokens = 0;
|
|
138
|
+
let inputTokens = 0;
|
|
139
|
+
let outputTokens = 0;
|
|
140
|
+
let totalCost = 0;
|
|
141
|
+
let toolCallCount = 0;
|
|
142
|
+
let llmCallCount = 0;
|
|
143
|
+
|
|
144
|
+
events.forEach((e) => {
|
|
145
|
+
if (e.type === "llm_completion") {
|
|
146
|
+
llmCallCount++;
|
|
147
|
+
if (e.usage) {
|
|
148
|
+
inputTokens += e.usage.input || 0;
|
|
149
|
+
outputTokens += e.usage.output || 0;
|
|
150
|
+
totalTokens += e.usage.totalTokens || 0;
|
|
151
|
+
totalCost += e.usage.cost?.total || 0;
|
|
152
|
+
}
|
|
153
|
+
} else if (e.type === "tool_start") {
|
|
154
|
+
toolCallCount++;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const htmlContent = generateHtmlReport({
|
|
159
|
+
traceId,
|
|
160
|
+
projectName: process.env.PHOENIX_PROJECT_NAME || "pi-coding-agent",
|
|
161
|
+
startTime: new Date(sessionStartTime).toISOString(),
|
|
162
|
+
endTime: new Date(sessionEndTime).toISOString(),
|
|
163
|
+
durationMs: totalDurationMs,
|
|
164
|
+
metrics: {
|
|
165
|
+
totalTokens,
|
|
166
|
+
inputTokens,
|
|
167
|
+
outputTokens,
|
|
168
|
+
totalCost: totalCost.toFixed(5),
|
|
169
|
+
toolCallCount,
|
|
170
|
+
llmCallCount
|
|
171
|
+
},
|
|
172
|
+
events
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
writeFileSync(reportPath, htmlContent, "utf-8");
|
|
177
|
+
// Output a nice clickable file:// link to the console for the user
|
|
178
|
+
console.log(`\n📊 [notrace] Observability report generated:`);
|
|
179
|
+
console.log(`👉 \x1b[36mfile://${reportPath}\x1b[0m\n`);
|
|
180
|
+
} catch (err: any) {
|
|
181
|
+
console.warn(`[notrace] Failed to write HTML report: ${err?.message || err}`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Returns a self-contained premium HTML template incorporating the design tokens
|
|
187
|
+
function generateHtmlReport(data: any): string {
|
|
188
|
+
const serializedData = JSON.stringify(data);
|
|
189
|
+
|
|
190
|
+
return `<!DOCTYPE html>
|
|
191
|
+
<html lang="en">
|
|
192
|
+
<head>
|
|
193
|
+
<meta charset="UTF-8">
|
|
194
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
195
|
+
<title>notrace - ${data.traceId}</title>
|
|
196
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
197
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
198
|
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Source+Code+Pro:wght@400;500&display=swap" rel="stylesheet">
|
|
199
|
+
<style>
|
|
200
|
+
:root {
|
|
201
|
+
--bg: #0b0b0e;
|
|
202
|
+
--bg-panel: rgba(22, 22, 28, 0.45);
|
|
203
|
+
--bg-card: rgba(30, 30, 38, 0.6);
|
|
204
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
205
|
+
--border-hover: rgba(255, 255, 255, 0.15);
|
|
206
|
+
--text: #e2e2e9;
|
|
207
|
+
--text-muted: #9f9fa9;
|
|
208
|
+
--accent: #8b5cf6;
|
|
209
|
+
--accent-gradient: linear-gradient(135deg, #a78bfa, #8b5cf6);
|
|
210
|
+
--success: #10b981;
|
|
211
|
+
--error: #ef4444;
|
|
212
|
+
--warning: #f59e0b;
|
|
213
|
+
--info: #3b82f6;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
* {
|
|
217
|
+
box-sizing: border-box;
|
|
218
|
+
margin: 0;
|
|
219
|
+
padding: 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
body {
|
|
223
|
+
font-family: 'Outfit', sans-serif;
|
|
224
|
+
background-color: var(--bg);
|
|
225
|
+
color: var(--text);
|
|
226
|
+
line-height: 1.5;
|
|
227
|
+
padding: 2rem;
|
|
228
|
+
min-height: 100vh;
|
|
229
|
+
background-image:
|
|
230
|
+
radial-gradient(circle at 10% 20%, rgba(139, 92, 246, 0.08) 0%, transparent 40%),
|
|
231
|
+
radial-gradient(circle at 90% 80%, rgba(59, 130, 246, 0.06) 0%, transparent 40%);
|
|
232
|
+
background-attachment: fixed;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.container {
|
|
236
|
+
max-width: 1100px;
|
|
237
|
+
margin: 0 auto;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
header {
|
|
241
|
+
display: flex;
|
|
242
|
+
justify-content: space-between;
|
|
243
|
+
align-items: center;
|
|
244
|
+
margin-bottom: 2rem;
|
|
245
|
+
padding-bottom: 1.5rem;
|
|
246
|
+
border-bottom: 1px solid var(--border);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.brand {
|
|
250
|
+
display: flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
gap: 0.75rem;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.brand h1 {
|
|
256
|
+
font-size: 1.75rem;
|
|
257
|
+
font-weight: 700;
|
|
258
|
+
background: var(--accent-gradient);
|
|
259
|
+
-webkit-background-clip: text;
|
|
260
|
+
-webkit-text-fill-color: transparent;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.brand-tag {
|
|
264
|
+
font-size: 0.75rem;
|
|
265
|
+
text-transform: uppercase;
|
|
266
|
+
letter-spacing: 0.05em;
|
|
267
|
+
background: rgba(139, 92, 246, 0.15);
|
|
268
|
+
color: #c084fc;
|
|
269
|
+
padding: 0.2rem 0.5rem;
|
|
270
|
+
border-radius: 4px;
|
|
271
|
+
border: 1px solid rgba(167, 139, 250, 0.3);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.meta-time {
|
|
275
|
+
font-size: 0.875rem;
|
|
276
|
+
color: var(--text-muted);
|
|
277
|
+
text-align: right;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.metrics-grid {
|
|
281
|
+
display: grid;
|
|
282
|
+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
283
|
+
gap: 1rem;
|
|
284
|
+
margin-bottom: 2.5rem;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.metric-card {
|
|
288
|
+
background: var(--bg-panel);
|
|
289
|
+
border: 1px solid var(--border);
|
|
290
|
+
padding: 1.25rem;
|
|
291
|
+
border-radius: 12px;
|
|
292
|
+
backdrop-filter: blur(12px);
|
|
293
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.metric-card:hover {
|
|
297
|
+
border-color: var(--border-hover);
|
|
298
|
+
transform: translateY(-2px);
|
|
299
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.metric-label {
|
|
303
|
+
font-size: 0.75rem;
|
|
304
|
+
text-transform: uppercase;
|
|
305
|
+
letter-spacing: 0.05em;
|
|
306
|
+
color: var(--text-muted);
|
|
307
|
+
margin-bottom: 0.5rem;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.metric-value {
|
|
311
|
+
font-size: 1.5rem;
|
|
312
|
+
font-weight: 600;
|
|
313
|
+
color: #fff;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.timeline {
|
|
317
|
+
position: relative;
|
|
318
|
+
padding-left: 2rem;
|
|
319
|
+
border-left: 2px solid var(--border);
|
|
320
|
+
margin-left: 1rem;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.timeline-event {
|
|
324
|
+
position: relative;
|
|
325
|
+
margin-bottom: 2rem;
|
|
326
|
+
animation: slideIn 0.4s ease-out;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@keyframes slideIn {
|
|
330
|
+
from { opacity: 0; transform: translateX(-10px); }
|
|
331
|
+
to { opacity: 1; transform: translateX(0); }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.timeline-dot {
|
|
335
|
+
position: absolute;
|
|
336
|
+
left: calc(-2rem - 6px);
|
|
337
|
+
top: 1.25rem;
|
|
338
|
+
width: 10px;
|
|
339
|
+
height: 10px;
|
|
340
|
+
border-radius: 50%;
|
|
341
|
+
background: var(--text-muted);
|
|
342
|
+
box-shadow: 0 0 0 4px var(--bg);
|
|
343
|
+
transition: all 0.3s ease;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* Event color mappings */
|
|
347
|
+
.timeline-event.session-start .timeline-dot { background: var(--info); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--info); }
|
|
348
|
+
.timeline-event.turn-start .timeline-dot { background: var(--accent); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--accent); }
|
|
349
|
+
.timeline-event.tool-call .timeline-dot { background: var(--warning); box-shadow: 0 0 0 4px var(--bg); }
|
|
350
|
+
.timeline-event.tool-call.error .timeline-dot { background: var(--error); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--error); }
|
|
351
|
+
.timeline-event.llm-call .timeline-dot { background: var(--success); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--success); }
|
|
352
|
+
|
|
353
|
+
.card {
|
|
354
|
+
background: var(--bg-panel);
|
|
355
|
+
border: 1px solid var(--border);
|
|
356
|
+
border-radius: 12px;
|
|
357
|
+
padding: 1.25rem;
|
|
358
|
+
backdrop-filter: blur(12px);
|
|
359
|
+
transition: border-color 0.2s;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.card:hover {
|
|
363
|
+
border-color: var(--border-hover);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.card-header {
|
|
367
|
+
display: flex;
|
|
368
|
+
justify-content: space-between;
|
|
369
|
+
align-items: center;
|
|
370
|
+
cursor: pointer;
|
|
371
|
+
user-select: none;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.card-title {
|
|
375
|
+
display: flex;
|
|
376
|
+
align-items: center;
|
|
377
|
+
gap: 0.75rem;
|
|
378
|
+
font-weight: 600;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.card-badge {
|
|
382
|
+
font-size: 0.75rem;
|
|
383
|
+
font-weight: 500;
|
|
384
|
+
padding: 0.15rem 0.5rem;
|
|
385
|
+
border-radius: 4px;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.badge-session { background: rgba(59, 130, 246, 0.15); color: #60a5fa; }
|
|
389
|
+
.badge-turn { background: rgba(139, 92, 246, 0.15); color: #c084fc; }
|
|
390
|
+
.badge-tool { background: rgba(245, 158, 11, 0.15); color: #fbbf24; }
|
|
391
|
+
.badge-tool.error { background: rgba(239, 68, 68, 0.15); color: #f87171; }
|
|
392
|
+
.badge-llm { background: rgba(16, 185, 129, 0.15); color: #34d399; }
|
|
393
|
+
|
|
394
|
+
.card-time {
|
|
395
|
+
font-size: 0.8rem;
|
|
396
|
+
color: var(--text-muted);
|
|
397
|
+
display: flex;
|
|
398
|
+
align-items: center;
|
|
399
|
+
gap: 0.5rem;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.arrow-icon {
|
|
403
|
+
width: 16px;
|
|
404
|
+
height: 16px;
|
|
405
|
+
fill: none;
|
|
406
|
+
stroke: currentColor;
|
|
407
|
+
stroke-width: 2;
|
|
408
|
+
stroke-linecap: round;
|
|
409
|
+
stroke-linejoin: round;
|
|
410
|
+
transition: transform 0.2s;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.expanded .arrow-icon {
|
|
414
|
+
transform: rotate(90deg);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.card-body {
|
|
418
|
+
margin-top: 1rem;
|
|
419
|
+
padding-top: 1rem;
|
|
420
|
+
border-top: 1px solid var(--border);
|
|
421
|
+
display: none;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.expanded .card-body {
|
|
425
|
+
display: block;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.code-block {
|
|
429
|
+
font-family: 'Source Code Pro', monospace;
|
|
430
|
+
font-size: 0.875rem;
|
|
431
|
+
background: rgba(0, 0, 0, 0.35);
|
|
432
|
+
border: 1px solid var(--border);
|
|
433
|
+
padding: 1rem;
|
|
434
|
+
border-radius: 8px;
|
|
435
|
+
overflow-x: auto;
|
|
436
|
+
margin-top: 0.5rem;
|
|
437
|
+
color: #e2e2e9;
|
|
438
|
+
white-space: pre-wrap;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.messages-container {
|
|
442
|
+
display: flex;
|
|
443
|
+
flex-direction: column;
|
|
444
|
+
gap: 0.75rem;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.msg-row {
|
|
448
|
+
display: flex;
|
|
449
|
+
flex-direction: column;
|
|
450
|
+
gap: 0.25rem;
|
|
451
|
+
padding: 0.75rem;
|
|
452
|
+
background: rgba(255, 255, 255, 0.02);
|
|
453
|
+
border-radius: 6px;
|
|
454
|
+
border-left: 3px solid var(--border);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.msg-row.user { border-left-color: var(--info); }
|
|
458
|
+
.msg-row.assistant { border-left-color: var(--success); }
|
|
459
|
+
.msg-row.system { border-left-color: var(--accent); }
|
|
460
|
+
|
|
461
|
+
.msg-role {
|
|
462
|
+
font-size: 0.75rem;
|
|
463
|
+
font-weight: 600;
|
|
464
|
+
text-transform: uppercase;
|
|
465
|
+
letter-spacing: 0.05em;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.msg-row.user .msg-role { color: #60a5fa; }
|
|
469
|
+
.msg-row.assistant .msg-role { color: #34d399; }
|
|
470
|
+
.msg-row.system .msg-role { color: #c084fc; }
|
|
471
|
+
|
|
472
|
+
.msg-text {
|
|
473
|
+
font-size: 0.9rem;
|
|
474
|
+
white-space: pre-wrap;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.duration-pill {
|
|
478
|
+
font-size: 0.75rem;
|
|
479
|
+
background: rgba(255, 255, 255, 0.06);
|
|
480
|
+
padding: 0.1rem 0.4rem;
|
|
481
|
+
border-radius: 4px;
|
|
482
|
+
color: var(--text-muted);
|
|
483
|
+
}
|
|
484
|
+
</style>
|
|
485
|
+
</head>
|
|
486
|
+
<body>
|
|
487
|
+
<div class="container">
|
|
488
|
+
<header>
|
|
489
|
+
<div class="brand">
|
|
490
|
+
<h1>notrace</h1>
|
|
491
|
+
<span class="brand-tag">Local Observability</span>
|
|
492
|
+
</div>
|
|
493
|
+
<div class="meta-time">
|
|
494
|
+
<div>Session: <span id="sess-id"></span></div>
|
|
495
|
+
<div id="sess-time" style="font-size: 0.8rem;"></div>
|
|
496
|
+
</div>
|
|
497
|
+
</header>
|
|
498
|
+
|
|
499
|
+
<div class="metrics-grid">
|
|
500
|
+
<div class="metric-card">
|
|
501
|
+
<div class="metric-label">Duration</div>
|
|
502
|
+
<div class="metric-value" id="val-duration">-</div>
|
|
503
|
+
</div>
|
|
504
|
+
<div class="metric-card">
|
|
505
|
+
<div class="metric-label">Total Tokens</div>
|
|
506
|
+
<div class="metric-value" id="val-tokens">-</div>
|
|
507
|
+
</div>
|
|
508
|
+
<div class="metric-card">
|
|
509
|
+
<div class="metric-label">LLM Calls</div>
|
|
510
|
+
<div class="metric-value" id="val-llms">-</div>
|
|
511
|
+
</div>
|
|
512
|
+
<div class="metric-card">
|
|
513
|
+
<div class="metric-label">Tool Calls</div>
|
|
514
|
+
<div class="metric-value" id="val-tools">-</div>
|
|
515
|
+
</div>
|
|
516
|
+
<div class="metric-card">
|
|
517
|
+
<div class="metric-label">Cost (USD)</div>
|
|
518
|
+
<div class="metric-value" id="val-cost">-</div>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<h2 style="margin-bottom: 1.5rem; font-weight: 600; font-size: 1.25rem; color: #fff;">Activity Flow</h2>
|
|
523
|
+
<div class="timeline" id="timeline-container">
|
|
524
|
+
<!-- Injected by JS -->
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
<script>
|
|
529
|
+
const traceData = ${serializedData};
|
|
530
|
+
|
|
531
|
+
// Render Metrics
|
|
532
|
+
document.getElementById("sess-id").textContent = traceData.traceId;
|
|
533
|
+
document.getElementById("sess-time").textContent = new Date(traceData.startTime).toLocaleString();
|
|
534
|
+
document.getElementById("val-duration").textContent = (traceData.durationMs / 1000).toFixed(2) + "s";
|
|
535
|
+
document.getElementById("val-tokens").textContent = traceData.metrics.totalTokens.toLocaleString();
|
|
536
|
+
document.getElementById("val-llms").textContent = traceData.metrics.toolCallCount; // Wait, actually traceData.metrics.llmCallCount
|
|
537
|
+
document.getElementById("val-tools").textContent = traceData.metrics.toolCallCount;
|
|
538
|
+
document.getElementById("val-cost").textContent = "$" + traceData.metrics.totalCost;
|
|
539
|
+
|
|
540
|
+
// Correct the labels mapping if names swapped
|
|
541
|
+
document.getElementById("val-llms").textContent = traceData.metrics.llmCallCount;
|
|
542
|
+
|
|
543
|
+
// Process & Render Timeline
|
|
544
|
+
const container = document.getElementById("timeline-container");
|
|
545
|
+
const events = traceData.events;
|
|
546
|
+
|
|
547
|
+
// Group start and end of tools to display them as single cards
|
|
548
|
+
const toolExecutions = {};
|
|
549
|
+
const renderedEvents = [];
|
|
550
|
+
|
|
551
|
+
events.forEach(e => {
|
|
552
|
+
if (e.type === "session_start") {
|
|
553
|
+
renderedEvents.push({
|
|
554
|
+
type: "session",
|
|
555
|
+
title: "Session Initialized",
|
|
556
|
+
time: new Date(e.timestamp).toLocaleTimeString(),
|
|
557
|
+
body: \`Reason: \${e.reason}\\nFile: \${e.sessionFile}\`
|
|
558
|
+
});
|
|
559
|
+
} else if (e.type === "turn_start") {
|
|
560
|
+
renderedEvents.push({
|
|
561
|
+
type: "turn",
|
|
562
|
+
title: "User Turn Started",
|
|
563
|
+
time: new Date(e.timestamp).toLocaleTimeString(),
|
|
564
|
+
body: "Agent waiting for query execution loop."
|
|
565
|
+
});
|
|
566
|
+
} else if (e.type === "tool_start") {
|
|
567
|
+
toolExecutions[e.toolCallId] = {
|
|
568
|
+
type: "tool",
|
|
569
|
+
title: \`Tool Call: \${e.toolName}\`,
|
|
570
|
+
toolName: e.toolName,
|
|
571
|
+
time: new Date(e.timestamp).toLocaleTimeString(),
|
|
572
|
+
args: e.args,
|
|
573
|
+
startTime: e.timestamp
|
|
574
|
+
};
|
|
575
|
+
} else if (e.type === "tool_end") {
|
|
576
|
+
const start = toolExecutions[e.toolCallId];
|
|
577
|
+
if (start) {
|
|
578
|
+
start.result = e.result;
|
|
579
|
+
start.isError = e.isError;
|
|
580
|
+
start.durationMs = e.durationMs;
|
|
581
|
+
renderedEvents.push(start);
|
|
582
|
+
delete toolExecutions[e.toolCallId];
|
|
583
|
+
}
|
|
584
|
+
} else if (e.type === "llm_completion") {
|
|
585
|
+
renderedEvents.push({
|
|
586
|
+
type: "llm",
|
|
587
|
+
title: \`LLM Call: \${e.model}\`,
|
|
588
|
+
model: e.model,
|
|
589
|
+
provider: e.provider,
|
|
590
|
+
time: new Date(e.timestamp).toLocaleTimeString(),
|
|
591
|
+
durationMs: e.durationMs,
|
|
592
|
+
usage: e.usage,
|
|
593
|
+
payload: e.inputPayload,
|
|
594
|
+
output: e.outputContent
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// Render cards
|
|
600
|
+
renderedEvents.forEach((ev, index) => {
|
|
601
|
+
const evDiv = document.createElement("div");
|
|
602
|
+
evDiv.className = \`timeline-event \${ev.type}-start \${ev.isError ? "error" : ""}\`;
|
|
603
|
+
|
|
604
|
+
let cardHtml = \`
|
|
605
|
+
<div class="timeline-dot"></div>
|
|
606
|
+
<div class="card" id="card-\${index}">
|
|
607
|
+
<div class="card-header" onclick="toggleCard(\${index})">
|
|
608
|
+
<div class="card-title">
|
|
609
|
+
<span class="card-badge badge-\${ev.type} \${ev.isError ? "error" : ""}">\${ev.type.toUpperCase()}</span>
|
|
610
|
+
<span>\${ev.title}</span>
|
|
611
|
+
\${ev.durationMs ? \`<span class="duration-pill">\${(ev.durationMs / 1000).toFixed(2)}s</span>\` : ""}
|
|
612
|
+
</div>
|
|
613
|
+
<div class="card-time">
|
|
614
|
+
<span>\${ev.time}</span>
|
|
615
|
+
<svg class="arrow-icon" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
<div class="card-body">
|
|
619
|
+
\`;
|
|
620
|
+
|
|
621
|
+
if (ev.type === "session" || ev.type === "turn") {
|
|
622
|
+
cardHtml += \`<div class="code-block">\${ev.body}</div>\`;
|
|
623
|
+
} else if (ev.type === "tool") {
|
|
624
|
+
cardHtml += \`
|
|
625
|
+
<strong>Arguments:</strong>
|
|
626
|
+
<div class="code-block">\${JSON.stringify(ev.args, null, 2)}</div>
|
|
627
|
+
<strong style="margin-top: 1rem; display: block;">Result (\${ev.isError ? "Error" : "Success"}):</strong>
|
|
628
|
+
<div class="code-block">\${typeof ev.result === "string" ? ev.result : JSON.stringify(ev.result, null, 2)}</div>
|
|
629
|
+
\`;
|
|
630
|
+
} else if (ev.type === "llm") {
|
|
631
|
+
// Render system prompt and input messages if present
|
|
632
|
+
let messagesHtml = '<div class="messages-container">';
|
|
633
|
+
if (ev.payload) {
|
|
634
|
+
if (ev.payload.system_instruction) {
|
|
635
|
+
const instr = typeof ev.payload.system_instruction === "string"
|
|
636
|
+
? ev.payload.system_instruction
|
|
637
|
+
: JSON.stringify(ev.payload.system_instruction);
|
|
638
|
+
messagesHtml += \`
|
|
639
|
+
<div class="msg-row system">
|
|
640
|
+
<span class="msg-role">System Instruction</span>
|
|
641
|
+
<span class="msg-text">\${instr}</span>
|
|
642
|
+
</div>
|
|
643
|
+
\`;
|
|
644
|
+
}
|
|
645
|
+
if (ev.payload.messages && Array.isArray(ev.payload.messages)) {
|
|
646
|
+
ev.payload.messages.forEach(m => {
|
|
647
|
+
const contentText = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
|
|
648
|
+
messagesHtml += \`
|
|
649
|
+
<div class="msg-row \${m.role}">
|
|
650
|
+
<span class="msg-role">\${m.role}</span>
|
|
651
|
+
<span class="msg-text">\${contentText}</span>
|
|
652
|
+
</div>
|
|
653
|
+
\`;
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
messagesHtml += '</div>';
|
|
658
|
+
|
|
659
|
+
cardHtml += \`
|
|
660
|
+
<strong>Context Messages:</strong>
|
|
661
|
+
\${messagesHtml}
|
|
662
|
+
<strong style="margin-top: 1.25rem; display: block;">Generated Response:</strong>
|
|
663
|
+
<div class="code-block">\${JSON.stringify(ev.output, null, 2)}</div>
|
|
664
|
+
\${ev.usage ? \`
|
|
665
|
+
<div style="margin-top: 1rem; font-size: 0.85rem; color: var(--text-muted); display: flex; gap: 1.5rem;">
|
|
666
|
+
<span>Input Tokens: <strong>\${ev.usage.input}</strong></span>
|
|
667
|
+
<span>Output Tokens: <strong>\${ev.usage.output}</strong></span>
|
|
668
|
+
<span>Total Tokens: <strong>\${ev.usage.totalTokens}</strong></span>
|
|
669
|
+
<span>Cost: <strong>\$\${ev.usage.cost?.total.toFixed(5) || "0.00"}</strong></span>
|
|
670
|
+
</div>
|
|
671
|
+
\` : ""}
|
|
672
|
+
\`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
cardHtml += \`
|
|
676
|
+
</div>
|
|
677
|
+
</div>
|
|
678
|
+
\`;
|
|
679
|
+
|
|
680
|
+
evDiv.innerHTML = cardHtml;
|
|
681
|
+
container.appendChild(evDiv);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// Expand/Collapse controller
|
|
685
|
+
function toggleCard(index) {
|
|
686
|
+
const card = document.getElementById(\`card-\${index}\`);
|
|
687
|
+
card.classList.toggle("expanded");
|
|
688
|
+
}
|
|
689
|
+
</script>
|
|
690
|
+
</body>
|
|
691
|
+
</html>`;
|
|
692
|
+
}
|