@townco/debugger 0.1.67 → 0.1.68
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/package.json +3 -3
- package/src/App.tsx +9 -2
- package/src/components/ConversationPanel.tsx +392 -0
- package/src/components/DebuggerHeader.tsx +1 -1
- package/src/components/DiagnosticDetailsPanel.tsx +1095 -0
- package/src/components/DiagnosticPanel.tsx +111 -0
- package/src/components/NavBar.tsx +130 -0
- package/src/components/SpanDetailsPanel.tsx +2 -2
- package/src/components/SpanIcon.tsx +75 -24
- package/src/components/SpanTimeline.tsx +2 -3
- package/src/components/SpansLogsList.tsx +477 -0
- package/src/components/UnifiedTimeline.tsx +1 -2
- package/src/components/ViewControlBar.tsx +79 -0
- package/src/lib/segmentation.ts +391 -0
- package/src/lib/toolRegistry.ts +424 -0
- package/src/pages/SessionLogsView.tsx +585 -0
- package/src/pages/SessionView.tsx +24 -13
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import type { ConversationTrace, Log, Span } from "../types";
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Types
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export interface ConversationItem {
|
|
8
|
+
id: string;
|
|
9
|
+
type: "user" | "agent" | "thinking" | "tool_call";
|
|
10
|
+
content: string;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
spanId?: string;
|
|
13
|
+
toolName?: string;
|
|
14
|
+
toolInput?: unknown;
|
|
15
|
+
toolOutput?: unknown;
|
|
16
|
+
/** Label shown above the content (e.g., "Agent", "Thinking stream") */
|
|
17
|
+
label?: string;
|
|
18
|
+
/** If this item is from a subagent, the span_id of the parent Task span */
|
|
19
|
+
subagentSpanId?: string;
|
|
20
|
+
/** Short identifier for the subagent (last 4 chars of span_id) */
|
|
21
|
+
subagentId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Segment {
|
|
25
|
+
id: string;
|
|
26
|
+
traceId: string;
|
|
27
|
+
startTime: number;
|
|
28
|
+
endTime: number;
|
|
29
|
+
conversationItems: ConversationItem[];
|
|
30
|
+
spans: Span[];
|
|
31
|
+
logs: Log[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TraceData {
|
|
35
|
+
trace: ConversationTrace;
|
|
36
|
+
spans: Span[];
|
|
37
|
+
logs: Log[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Utility Functions
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
function parseAttributes(attrs: string | null): Record<string, unknown> {
|
|
45
|
+
if (!attrs) return {};
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(attrs);
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatTimestamp(timestampNano: number): string {
|
|
54
|
+
const date = new Date(timestampNano / 1_000_000);
|
|
55
|
+
return date.toLocaleTimeString("en-US", {
|
|
56
|
+
hour: "2-digit",
|
|
57
|
+
minute: "2-digit",
|
|
58
|
+
second: "2-digit",
|
|
59
|
+
hour12: false,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extract thinking/reasoning content from a chat span if available
|
|
65
|
+
*/
|
|
66
|
+
function extractThinkingContent(span: Span): string | null {
|
|
67
|
+
const attrs = parseAttributes(span.attributes);
|
|
68
|
+
const outputMessages = attrs["gen_ai.output.messages"];
|
|
69
|
+
|
|
70
|
+
if (!outputMessages) return null;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const messages =
|
|
74
|
+
typeof outputMessages === "string"
|
|
75
|
+
? JSON.parse(outputMessages)
|
|
76
|
+
: outputMessages;
|
|
77
|
+
|
|
78
|
+
if (!Array.isArray(messages)) return null;
|
|
79
|
+
|
|
80
|
+
// Look for thinking blocks in the content
|
|
81
|
+
for (const msg of messages) {
|
|
82
|
+
if (msg.role === "assistant" || msg.role === "ai") {
|
|
83
|
+
const content = msg.content;
|
|
84
|
+
if (Array.isArray(content)) {
|
|
85
|
+
for (const block of content) {
|
|
86
|
+
if (block.type === "thinking" && block.thinking) {
|
|
87
|
+
return block.thinking;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Ignore parse errors
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if a span is a chat/LLM call span
|
|
102
|
+
*/
|
|
103
|
+
function isChatSpan(span: Span): boolean {
|
|
104
|
+
const attrs = parseAttributes(span.attributes);
|
|
105
|
+
return span.name.startsWith("chat") && "gen_ai.input.messages" in attrs;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if a span is a subagent (Task) span
|
|
110
|
+
*
|
|
111
|
+
* A span is considered a subagent if:
|
|
112
|
+
* - It's a tool call span (agent.tool_call)
|
|
113
|
+
* - AND the tool name matches one of these patterns:
|
|
114
|
+
* - Exactly "Task" (the built-in subagent tool)
|
|
115
|
+
* - Contains "subagent" (case-insensitive)
|
|
116
|
+
*/
|
|
117
|
+
function isSubagentSpan(span: Span): boolean {
|
|
118
|
+
if (span.name !== "agent.tool_call") return false;
|
|
119
|
+
const attrs = parseAttributes(span.attributes);
|
|
120
|
+
const toolName = attrs["tool.name"];
|
|
121
|
+
|
|
122
|
+
if (typeof toolName !== "string") return false;
|
|
123
|
+
|
|
124
|
+
// Check for Task or any tool name containing "subagent" (case-insensitive)
|
|
125
|
+
const isSubagent =
|
|
126
|
+
toolName === "Task" || toolName.toLowerCase().includes("subagent");
|
|
127
|
+
|
|
128
|
+
return isSubagent;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get a short identifier from a span_id (last 4 characters)
|
|
133
|
+
*/
|
|
134
|
+
function getShortId(spanId: string): string {
|
|
135
|
+
return spanId.slice(-4);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface SubagentInfo {
|
|
139
|
+
spanId: string;
|
|
140
|
+
shortId: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Build a map of span_id -> subagent info for all spans that are descendants of a Task span
|
|
145
|
+
*/
|
|
146
|
+
function buildSubagentMap(spans: Span[]): Map<string, SubagentInfo> {
|
|
147
|
+
const subagentMap = new Map<string, SubagentInfo>();
|
|
148
|
+
const spanById = new Map(spans.map((s) => [s.span_id, s]));
|
|
149
|
+
|
|
150
|
+
// Find all Task (subagent) spans
|
|
151
|
+
const taskSpans = spans.filter(isSubagentSpan);
|
|
152
|
+
|
|
153
|
+
console.log("[DEBUG] Found subagent tool call spans:", {
|
|
154
|
+
count: taskSpans.length,
|
|
155
|
+
spans: taskSpans.map((s) => {
|
|
156
|
+
const attrs = parseAttributes(s.attributes);
|
|
157
|
+
return {
|
|
158
|
+
spanId: s.span_id.slice(-4),
|
|
159
|
+
toolName: attrs["tool.name"],
|
|
160
|
+
spanName: s.name,
|
|
161
|
+
};
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// For each Task span, mark it and all its descendants
|
|
166
|
+
for (const taskSpan of taskSpans) {
|
|
167
|
+
const subagentInfo: SubagentInfo = {
|
|
168
|
+
spanId: taskSpan.span_id,
|
|
169
|
+
shortId: getShortId(taskSpan.span_id),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Mark the Task span itself
|
|
173
|
+
subagentMap.set(taskSpan.span_id, subagentInfo);
|
|
174
|
+
|
|
175
|
+
// Track descendants for logging
|
|
176
|
+
const descendants: string[] = [];
|
|
177
|
+
|
|
178
|
+
// Find all descendants using BFS
|
|
179
|
+
const queue: string[] = [taskSpan.span_id];
|
|
180
|
+
while (queue.length > 0) {
|
|
181
|
+
const currentId = queue.shift();
|
|
182
|
+
if (!currentId) break;
|
|
183
|
+
// Find children of current span
|
|
184
|
+
for (const span of spans) {
|
|
185
|
+
if (span.parent_span_id === currentId) {
|
|
186
|
+
subagentMap.set(span.span_id, subagentInfo);
|
|
187
|
+
queue.push(span.span_id);
|
|
188
|
+
descendants.push(span.span_id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log("[DEBUG] Subagent descendant tree:", {
|
|
194
|
+
rootSpanId: taskSpan.span_id.slice(-4),
|
|
195
|
+
shortId: subagentInfo.shortId,
|
|
196
|
+
descendantCount: descendants.length,
|
|
197
|
+
descendants: descendants.map((id) => ({
|
|
198
|
+
spanId: id.slice(-4),
|
|
199
|
+
spanName: spanById.get(id)?.name || "unknown",
|
|
200
|
+
})),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return subagentMap;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// Segmentation Function
|
|
209
|
+
// ============================================================================
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Per-Turn Segmentation: Each user message + full agent response = one segment
|
|
213
|
+
* Spans are grouped by their trace_id, preserving full hierarchy
|
|
214
|
+
*/
|
|
215
|
+
export function segmentByTurn(
|
|
216
|
+
traces: ConversationTrace[],
|
|
217
|
+
traceDataMap: Map<string, TraceData>,
|
|
218
|
+
): Segment[] {
|
|
219
|
+
const segments: Segment[] = [];
|
|
220
|
+
|
|
221
|
+
for (const trace of traces) {
|
|
222
|
+
const data = traceDataMap.get(trace.trace_id);
|
|
223
|
+
if (!data) continue;
|
|
224
|
+
|
|
225
|
+
// Build subagent map for this trace's spans
|
|
226
|
+
const subagentMap = buildSubagentMap(data.spans);
|
|
227
|
+
|
|
228
|
+
// Debug: Log subagent map contents
|
|
229
|
+
if (subagentMap.size > 0) {
|
|
230
|
+
console.log("[DEBUG] Subagent map for trace:", trace.trace_id.slice(-4), {
|
|
231
|
+
subagentCount: subagentMap.size,
|
|
232
|
+
subagents: Array.from(subagentMap.entries()).map(([spanId, info]) => ({
|
|
233
|
+
spanId: spanId.slice(-4),
|
|
234
|
+
subagentSpanId: info.spanId.slice(-4),
|
|
235
|
+
shortId: info.shortId,
|
|
236
|
+
})),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const conversationItems: ConversationItem[] = [];
|
|
241
|
+
|
|
242
|
+
// Add user message if available
|
|
243
|
+
if (trace.userInput) {
|
|
244
|
+
conversationItems.push({
|
|
245
|
+
id: `${trace.trace_id}-user`,
|
|
246
|
+
type: "user",
|
|
247
|
+
content: trace.userInput,
|
|
248
|
+
timestamp: trace.start_time_unix_nano,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Add agent messages (chat and tool calls)
|
|
253
|
+
for (const msg of trace.agentMessages) {
|
|
254
|
+
// Check if this message is from a subagent
|
|
255
|
+
const subagentInfo = msg.spanId ? subagentMap.get(msg.spanId) : undefined;
|
|
256
|
+
|
|
257
|
+
// Debug: Log message processing
|
|
258
|
+
if (msg.spanId) {
|
|
259
|
+
console.log("[DEBUG] Processing agent message:", {
|
|
260
|
+
type: msg.type,
|
|
261
|
+
spanId: msg.spanId.slice(-4),
|
|
262
|
+
hasSubagentInfo: !!subagentInfo,
|
|
263
|
+
subagentShortId: subagentInfo?.shortId,
|
|
264
|
+
contentPreview: msg.content.slice(0, 50),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (msg.type === "chat") {
|
|
269
|
+
const item: ConversationItem = {
|
|
270
|
+
id: `${trace.trace_id}-agent-${msg.spanId}`,
|
|
271
|
+
type: "agent",
|
|
272
|
+
content: msg.content,
|
|
273
|
+
timestamp: msg.timestamp,
|
|
274
|
+
spanId: msg.spanId,
|
|
275
|
+
label: subagentInfo ? `Subagent .${subagentInfo.shortId}` : "Agent",
|
|
276
|
+
};
|
|
277
|
+
if (subagentInfo) {
|
|
278
|
+
item.subagentSpanId = subagentInfo.spanId;
|
|
279
|
+
item.subagentId = subagentInfo.shortId;
|
|
280
|
+
}
|
|
281
|
+
conversationItems.push(item);
|
|
282
|
+
} else if (msg.type === "tool_call" && msg.toolName) {
|
|
283
|
+
const item: ConversationItem = {
|
|
284
|
+
id: `${trace.trace_id}-tool-${msg.spanId}`,
|
|
285
|
+
type: "tool_call",
|
|
286
|
+
content: msg.toolName || msg.content,
|
|
287
|
+
timestamp: msg.timestamp,
|
|
288
|
+
spanId: msg.spanId,
|
|
289
|
+
toolName: msg.toolName,
|
|
290
|
+
toolInput: msg.toolInput,
|
|
291
|
+
toolOutput: msg.toolOutput,
|
|
292
|
+
label: "Tool Call",
|
|
293
|
+
};
|
|
294
|
+
if (subagentInfo) {
|
|
295
|
+
item.subagentSpanId = subagentInfo.spanId;
|
|
296
|
+
item.subagentId = subagentInfo.shortId;
|
|
297
|
+
}
|
|
298
|
+
conversationItems.push(item);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check for thinking content in chat spans
|
|
303
|
+
for (const span of data.spans) {
|
|
304
|
+
if (isChatSpan(span)) {
|
|
305
|
+
const thinking = extractThinkingContent(span);
|
|
306
|
+
if (thinking) {
|
|
307
|
+
// Insert thinking before the corresponding chat message
|
|
308
|
+
const chatItemIndex = conversationItems.findIndex(
|
|
309
|
+
(item) => item.spanId === span.span_id && item.type === "agent",
|
|
310
|
+
);
|
|
311
|
+
if (chatItemIndex > 0) {
|
|
312
|
+
const subagentInfo = subagentMap.get(span.span_id);
|
|
313
|
+
const item: ConversationItem = {
|
|
314
|
+
id: `${trace.trace_id}-thinking-${span.span_id}`,
|
|
315
|
+
type: "thinking",
|
|
316
|
+
content: thinking,
|
|
317
|
+
timestamp: span.start_time_unix_nano,
|
|
318
|
+
spanId: span.span_id,
|
|
319
|
+
label: "Thinking stream",
|
|
320
|
+
};
|
|
321
|
+
if (subagentInfo) {
|
|
322
|
+
item.subagentSpanId = subagentInfo.spanId;
|
|
323
|
+
item.subagentId = subagentInfo.shortId;
|
|
324
|
+
}
|
|
325
|
+
conversationItems.splice(chatItemIndex, 0, item);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Sort by timestamp
|
|
332
|
+
conversationItems.sort((a, b) => a.timestamp - b.timestamp);
|
|
333
|
+
|
|
334
|
+
const startTime =
|
|
335
|
+
data.spans.length > 0
|
|
336
|
+
? Math.min(...data.spans.map((s) => s.start_time_unix_nano))
|
|
337
|
+
: trace.start_time_unix_nano;
|
|
338
|
+
const endTime =
|
|
339
|
+
data.spans.length > 0
|
|
340
|
+
? Math.max(...data.spans.map((s) => s.end_time_unix_nano))
|
|
341
|
+
: trace.start_time_unix_nano;
|
|
342
|
+
|
|
343
|
+
segments.push({
|
|
344
|
+
id: trace.trace_id,
|
|
345
|
+
traceId: trace.trace_id,
|
|
346
|
+
startTime,
|
|
347
|
+
endTime,
|
|
348
|
+
conversationItems,
|
|
349
|
+
spans: data.spans,
|
|
350
|
+
logs: data.logs,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return segments;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ============================================================================
|
|
358
|
+
// Span Utilities
|
|
359
|
+
// ============================================================================
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Get token count for a span
|
|
363
|
+
*/
|
|
364
|
+
export function getSpanTokens(span: Span): number {
|
|
365
|
+
const attrs = parseAttributes(span.attributes);
|
|
366
|
+
const inputTokens = (attrs["gen_ai.usage.input_tokens"] as number) || 0;
|
|
367
|
+
const outputTokens = (attrs["gen_ai.usage.output_tokens"] as number) || 0;
|
|
368
|
+
return inputTokens + outputTokens;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Get duration in milliseconds for a span
|
|
373
|
+
*/
|
|
374
|
+
export function getSpanDurationMs(span: Span): number {
|
|
375
|
+
return (span.end_time_unix_nano - span.start_time_unix_nano) / 1_000_000;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Format duration for display
|
|
380
|
+
*/
|
|
381
|
+
export function formatDuration(durationMs: number): string {
|
|
382
|
+
if (durationMs < 1) {
|
|
383
|
+
return `${(durationMs * 1000).toFixed(0)}μs`;
|
|
384
|
+
}
|
|
385
|
+
if (durationMs < 1000) {
|
|
386
|
+
return `${durationMs.toFixed(0)}ms`;
|
|
387
|
+
}
|
|
388
|
+
return `${(durationMs / 1000).toFixed(1)}s`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export { formatTimestamp };
|