@townco/debugger 0.1.27 → 0.1.29

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/src/schemas.ts CHANGED
@@ -1,15 +1,34 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const VariantToolsSchema = z.array(z.string());
3
+ const numberWithDefault = z
4
+ .number()
5
+ .optional()
6
+ .transform((val) => val ?? 0);
4
7
 
5
- export const SessionMetricsSchema = z.object({
6
- durationMs: z.number(),
7
- inputTokens: z.number(),
8
- outputTokens: z.number(),
9
- totalTokens: z.number(),
10
- estimatedCost: z.number(),
11
- toolCallCount: z.number(),
8
+ const ToolCallSchema = z.object({
9
+ name: z.string(),
10
+ input: z.unknown(),
11
+ output: z.unknown(),
12
+ startTimeUnixNano: z.number().optional(),
13
+ endTimeUnixNano: z.number().optional(),
12
14
  });
13
15
 
16
+ export const VariantToolsSchema = z.array(z.string());
17
+
18
+ export const SessionMetricsSchema = z
19
+ .object({
20
+ durationMs: numberWithDefault,
21
+ inputTokens: numberWithDefault,
22
+ outputTokens: numberWithDefault,
23
+ totalTokens: numberWithDefault,
24
+ estimatedCost: z.number().catch(0),
25
+ toolCallCount: numberWithDefault,
26
+ toolCalls: z.array(ToolCallSchema).optional().default([]),
27
+ })
28
+ .transform((metrics) => ({
29
+ ...metrics,
30
+ toolCalls: metrics.toolCalls ?? [],
31
+ }));
32
+
14
33
  export type VariantTools = z.infer<typeof VariantToolsSchema>;
15
34
  export type SessionMetrics = z.infer<typeof SessionMetricsSchema>;
package/src/server.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { resetDb } from "@townco/otlp-server/db";
2
2
  import { createOtlpServer } from "@townco/otlp-server/http";
3
3
  import { serve } from "bun";
4
+ import { AnalysisDb } from "./analysis-db";
4
5
  import { ComparisonDb } from "./comparison-db";
5
6
  import { DebuggerDb } from "./db";
6
7
  import index from "./index.html";
@@ -10,6 +11,7 @@ import type {
10
11
  AgentConfig,
11
12
  ComparisonConfig,
12
13
  ConversationTrace,
14
+ SessionMetrics,
13
15
  Span,
14
16
  } from "./types";
15
17
 
@@ -56,6 +58,9 @@ export function startDebuggerServer(
56
58
  const comparisonDbPath = dbPath.replace(/\.db$/, "-comparison.db");
57
59
  const comparisonDb = new ComparisonDb(comparisonDbPath);
58
60
 
61
+ // Create analysis database - uses main debugger database
62
+ const analysisDb = new AnalysisDb(dbPath);
63
+
59
64
  // Helper to fetch agent config from agent server
60
65
  async function fetchAgentConfig(): Promise<AgentConfig | null> {
61
66
  try {
@@ -133,7 +138,9 @@ export function startDebuggerServer(
133
138
  "/api/sessions": {
134
139
  GET(req) {
135
140
  const url = new URL(req.url);
136
- const limit = Number.parseInt(url.searchParams.get("limit") || "50");
141
+ const limit = Number.parseInt(
142
+ url.searchParams.get("limit") || "1000",
143
+ );
137
144
  const offset = Number.parseInt(url.searchParams.get("offset") || "0");
138
145
  const sessions = db.listSessions(limit, offset);
139
146
  return Response.json(sessions);
@@ -258,7 +265,7 @@ export function startDebuggerServer(
258
265
  const body = await req.json();
259
266
  const config: ComparisonConfig = {
260
267
  id: body.id || crypto.randomUUID(),
261
- dimension: body.dimension,
268
+ dimensions: body.dimensions || [],
262
269
  controlModel: body.controlModel,
263
270
  variantModel: body.variantModel,
264
271
  variantSystemPrompt: body.variantSystemPrompt,
@@ -269,6 +276,7 @@ export function startDebuggerServer(
269
276
  comparisonDb.saveConfig(config);
270
277
  return Response.json({ id: config.id });
271
278
  } catch (error) {
279
+ console.error("Error saving comparison config:", error);
272
280
  return Response.json(
273
281
  { error: "Invalid request body" },
274
282
  { status: 400 },
@@ -311,7 +319,53 @@ export function startDebuggerServer(
311
319
  { status: 404 },
312
320
  );
313
321
  }
314
- return Response.json(run);
322
+
323
+ const config = comparisonDb.getConfig(run.configId);
324
+ const controlModel =
325
+ config?.controlModel ??
326
+ config?.variantModel ??
327
+ "claude-sonnet-4-5-20250929";
328
+ const variantModel =
329
+ config?.variantModel ??
330
+ config?.controlModel ??
331
+ "claude-sonnet-4-5-20250929";
332
+
333
+ const maybeRefreshMetrics = (
334
+ sessionId: string | null,
335
+ cached: SessionMetrics | null,
336
+ model: string,
337
+ ): SessionMetrics | null => {
338
+ if (!sessionId) return cached;
339
+ const needsRefresh =
340
+ !cached ||
341
+ cached.totalTokens === 0 ||
342
+ cached.toolCallCount === 0 ||
343
+ !cached.toolCalls ||
344
+ cached.toolCalls.length === 0;
345
+ if (!needsRefresh) return cached;
346
+
347
+ const spans = db.getSpansBySessionAttribute(sessionId);
348
+ if (spans.length === 0) return cached;
349
+ const traces = db.listTraces(100, 0, sessionId);
350
+ return extractSessionMetrics(traces, spans, model);
351
+ };
352
+
353
+ const controlMetrics = maybeRefreshMetrics(
354
+ run.controlSessionId,
355
+ run.controlMetrics,
356
+ controlModel,
357
+ );
358
+ const variantMetrics = maybeRefreshMetrics(
359
+ run.variantSessionId,
360
+ run.variantMetrics,
361
+ variantModel,
362
+ );
363
+
364
+ return Response.json({
365
+ ...run,
366
+ controlMetrics,
367
+ variantMetrics,
368
+ });
315
369
  },
316
370
  },
317
371
 
@@ -449,6 +503,279 @@ export function startDebuggerServer(
449
503
  },
450
504
  },
451
505
 
506
+ "/api/analyze-session/:sessionId": {
507
+ async POST(req) {
508
+ const sessionId = req.params.sessionId;
509
+
510
+ try {
511
+ // Import analyzer dynamically to avoid loading at startup
512
+ const { analyzeSession } = await import("./analysis/analyzer.js");
513
+
514
+ // Fetch session from agent server via ACP HTTP API
515
+ const sessionResponse = await fetch(
516
+ `${agentServerUrl}/sessions/${sessionId}`,
517
+ );
518
+
519
+ if (!sessionResponse.ok) {
520
+ if (sessionResponse.status === 404) {
521
+ return Response.json(
522
+ { error: "Session not found" },
523
+ { status: 404 },
524
+ );
525
+ }
526
+ throw new Error(
527
+ `Failed to fetch session: ${sessionResponse.statusText}`,
528
+ );
529
+ }
530
+
531
+ const sessionData = await sessionResponse.json();
532
+
533
+ // Analyze with LLM
534
+ const analysis = await analyzeSession(sessionData);
535
+
536
+ // Persist to database
537
+ analysisDb.saveAnalysis(analysis);
538
+
539
+ // Generate and save embedding
540
+ try {
541
+ const { embedAnalysis } = await import(
542
+ "./analysis/embeddings.js"
543
+ );
544
+ const embedding = await embedAnalysis(analysis);
545
+ await analysisDb.saveEmbedding(analysis.session_id, embedding);
546
+ } catch (error) {
547
+ console.error(
548
+ `Failed to generate embedding for ${sessionId}:`,
549
+ error,
550
+ );
551
+ // Continue - don't fail entire analysis
552
+ }
553
+
554
+ return Response.json(analysis);
555
+ } catch (error) {
556
+ console.error("Session analysis error:", error);
557
+ return Response.json(
558
+ {
559
+ error:
560
+ error instanceof Error ? error.message : "Analysis failed",
561
+ },
562
+ { status: 500 },
563
+ );
564
+ }
565
+ },
566
+ },
567
+
568
+ "/api/analyze-all-sessions": {
569
+ async POST(req) {
570
+ try {
571
+ const body = await req.json();
572
+ const { sessionIds } = body as { sessionIds: string[] };
573
+
574
+ if (!Array.isArray(sessionIds)) {
575
+ return Response.json(
576
+ { error: "sessionIds must be an array" },
577
+ { status: 400 },
578
+ );
579
+ }
580
+
581
+ // Import analyzer dynamically
582
+ const { analyzeSession } = await import("./analysis/analyzer.js");
583
+
584
+ // Process in batches of 25
585
+ const BATCH_SIZE = 25;
586
+ const results: Array<{
587
+ session_id: string;
588
+ success: boolean;
589
+ error?: string;
590
+ }> = [];
591
+
592
+ const totalBatches = Math.ceil(sessionIds.length / BATCH_SIZE);
593
+ console.log(
594
+ `✨ Starting batch analysis of ${sessionIds.length} sessions (${totalBatches} batches)...`,
595
+ );
596
+
597
+ for (let i = 0; i < sessionIds.length; i += BATCH_SIZE) {
598
+ const batch = sessionIds.slice(i, i + BATCH_SIZE);
599
+ const batchNum = Math.floor(i / BATCH_SIZE) + 1;
600
+
601
+ console.log(
602
+ `📊 Processing batch ${batchNum}/${totalBatches} (${batch.length} sessions)...`,
603
+ );
604
+
605
+ // Run batch in parallel
606
+ const batchResults = await Promise.allSettled(
607
+ batch.map(async (sessionId) => {
608
+ // Fetch session data
609
+ const sessionResponse = await fetch(
610
+ `${agentServerUrl}/sessions/${sessionId}`,
611
+ );
612
+
613
+ if (!sessionResponse.ok) {
614
+ throw new Error(`Failed to fetch session ${sessionId}`);
615
+ }
616
+
617
+ const sessionData = await sessionResponse.json();
618
+
619
+ // Analyze
620
+ const analysis = await analyzeSession(sessionData);
621
+
622
+ // Persist
623
+ analysisDb.saveAnalysis(analysis);
624
+
625
+ // Generate and save embedding
626
+ try {
627
+ const { embedAnalysis } = await import(
628
+ "./analysis/embeddings.js"
629
+ );
630
+ const embedding = await embedAnalysis(analysis);
631
+ await analysisDb.saveEmbedding(sessionId, embedding);
632
+ } catch (error) {
633
+ console.error(
634
+ `Failed to generate embedding for ${sessionId}:`,
635
+ error,
636
+ );
637
+ // Continue - batch processing continues
638
+ }
639
+
640
+ return { session_id: sessionId, success: true };
641
+ }),
642
+ );
643
+
644
+ // Collect results
645
+ for (let j = 0; j < batchResults.length; j++) {
646
+ const result = batchResults[j];
647
+ const sessionId = batch[j];
648
+ if (!sessionId) continue;
649
+
650
+ if (result && result.status === "fulfilled") {
651
+ results.push(result.value);
652
+ } else if (result && result.status === "rejected") {
653
+ results.push({
654
+ session_id: sessionId,
655
+ success: false,
656
+ error:
657
+ result.reason instanceof Error
658
+ ? result.reason.message
659
+ : String(result.reason || "Unknown error"),
660
+ });
661
+ }
662
+ }
663
+
664
+ const batchSuccesses = batchResults.filter(
665
+ (r) => r.status === "fulfilled",
666
+ ).length;
667
+ const batchErrors = batchResults.filter(
668
+ (r) => r.status === "rejected",
669
+ ).length;
670
+ console.log(
671
+ `✅ Batch ${batchNum}/${totalBatches} complete: ${batchSuccesses} successful, ${batchErrors} failed`,
672
+ );
673
+ }
674
+
675
+ const totalSuccesses = results.filter((r) => r.success).length;
676
+ const totalErrors = results.filter((r) => !r.success).length;
677
+ console.log(
678
+ `🎉 Batch analysis complete: ${totalSuccesses} successful, ${totalErrors} failed`,
679
+ );
680
+
681
+ return Response.json({ results });
682
+ } catch (error) {
683
+ console.error("Batch analysis error:", error);
684
+ return Response.json(
685
+ {
686
+ error:
687
+ error instanceof Error ? error.message : "Analysis failed",
688
+ },
689
+ { status: 500 },
690
+ );
691
+ }
692
+ },
693
+ },
694
+
695
+ "/api/session-analyses": {
696
+ async GET(req) {
697
+ try {
698
+ const url = new URL(req.url);
699
+ const sessionId = url.searchParams.get("sessionId");
700
+
701
+ if (sessionId) {
702
+ // Get single analysis
703
+ const analysis = analysisDb.getAnalysis(sessionId);
704
+ if (!analysis) {
705
+ return Response.json(
706
+ { error: "Analysis not found" },
707
+ { status: 404 },
708
+ );
709
+ }
710
+ return Response.json(analysis);
711
+ }
712
+
713
+ // List all analyses
714
+ const limit = Number.parseInt(
715
+ url.searchParams.get("limit") || "50",
716
+ );
717
+ const offset = Number.parseInt(
718
+ url.searchParams.get("offset") || "0",
719
+ );
720
+
721
+ const analyses = analysisDb.listAnalyses(limit, offset);
722
+ return Response.json({ analyses });
723
+ } catch (error) {
724
+ console.error("Error retrieving analyses:", error);
725
+ return Response.json(
726
+ {
727
+ error:
728
+ error instanceof Error
729
+ ? error.message
730
+ : "Failed to retrieve analyses",
731
+ },
732
+ { status: 500 },
733
+ );
734
+ }
735
+ },
736
+ },
737
+
738
+ "/api/session-analyses/:sessionId/similar": {
739
+ async GET(req) {
740
+ try {
741
+ const sessionId = req.params.sessionId;
742
+ const url = new URL(req.url);
743
+ const limit = Number.parseInt(
744
+ url.searchParams.get("limit") || "10",
745
+ );
746
+
747
+ // Get embedding for this session
748
+ const embedding = await analysisDb.getEmbedding(sessionId);
749
+ if (!embedding) {
750
+ return Response.json(
751
+ { error: "No embedding found for this session" },
752
+ { status: 404 },
753
+ );
754
+ }
755
+
756
+ // Search for similar sessions
757
+ const similar = (
758
+ await analysisDb.searchSimilarSessions(embedding, limit + 1)
759
+ )
760
+ .filter((s) => s.session_id !== sessionId)
761
+ .slice(0, limit);
762
+
763
+ return Response.json({ similar });
764
+ } catch (error) {
765
+ console.error("Error finding similar sessions:", error);
766
+ return Response.json(
767
+ {
768
+ error:
769
+ error instanceof Error
770
+ ? error.message
771
+ : "Failed to find similar sessions",
772
+ },
773
+ { status: 500 },
774
+ );
775
+ }
776
+ },
777
+ },
778
+
452
779
  // Serve index.html for all unmatched routes (SPA routing)
453
780
  "/*": index,
454
781
  },
@@ -464,5 +791,12 @@ export function startDebuggerServer(
464
791
  otlpServer.stop();
465
792
  };
466
793
 
794
+ console.log(`🔍 Debugger UI: http://${server.hostname}:${server.port}`);
795
+ console.log(
796
+ `📊 OTLP endpoint: http://${otlpServer.hostname}:${otlpServer.port}`,
797
+ );
798
+ console.log(`📁 Database: ${dbPath}`);
799
+ console.log(`🤖 Agent server: ${agentServerUrl}`);
800
+
467
801
  return { server, otlpServer, stop };
468
802
  }
package/src/types.ts CHANGED
@@ -85,7 +85,7 @@ export type ComparisonDimension = "model" | "system_prompt" | "tools";
85
85
 
86
86
  export interface ComparisonConfig {
87
87
  id: string;
88
- dimension: ComparisonDimension;
88
+ dimensions: ComparisonDimension[]; // Now supports multiple dimensions
89
89
  controlModel?: string | undefined; // Original model for comparison
90
90
  variantModel?: string | undefined;
91
91
  variantSystemPrompt?: string | undefined;
@@ -96,7 +96,7 @@ export interface ComparisonConfig {
96
96
 
97
97
  export interface ComparisonConfigRow {
98
98
  id: string;
99
- dimension: string;
99
+ dimensions: string; // JSON array of dimensions
100
100
  control_model: string | null;
101
101
  variant_model: string | null;
102
102
  variant_system_prompt: string | null;
@@ -112,6 +112,15 @@ export interface SessionMetrics {
112
112
  totalTokens: number;
113
113
  estimatedCost: number;
114
114
  toolCallCount: number;
115
+ toolCalls?: ToolCall[];
116
+ }
117
+
118
+ export interface ToolCall {
119
+ name: string;
120
+ input: unknown;
121
+ output: unknown;
122
+ startTimeUnixNano?: number | undefined;
123
+ endTimeUnixNano?: number | undefined;
115
124
  }
116
125
 
117
126
  export interface ComparisonRun {