@townco/debugger 0.1.32 → 0.1.34

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.
@@ -114,7 +114,9 @@ export function FindSessions() {
114
114
  </CardHeader>
115
115
  <CardContent className="space-y-4">
116
116
  <div className="space-y-2">
117
- <label className="text-sm font-medium">Select Session</label>
117
+ <label htmlFor="session-select" className="text-sm font-medium">
118
+ Select Session
119
+ </label>
118
120
  <Select
119
121
  value={selectedSessionId}
120
122
  onValueChange={setSelectedSessionId}
@@ -19,13 +19,6 @@ import type {
19
19
  Session,
20
20
  } from "../types";
21
21
 
22
- function formatDuration(startNano: number, endNano: number): string {
23
- const ms = (endNano - startNano) / 1_000_000;
24
- if (ms < 1000) return `${ms.toFixed(0)}ms`;
25
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
26
- return `${(ms / 60000).toFixed(1)}m`;
27
- }
28
-
29
22
  function formatRelativeTime(nanoseconds: number): string {
30
23
  const ms = Date.now() - nanoseconds / 1_000_000;
31
24
  const seconds = Math.floor(ms / 1000);
@@ -47,9 +40,9 @@ interface SessionWithMessage extends Session {
47
40
  export function TownHall() {
48
41
  const [sessions, setSessions] = useState<SessionWithMessage[]>([]);
49
42
  const [comparisonRuns, setComparisonRuns] = useState<ComparisonRun[]>([]);
50
- const [comparisonSessionIds, setComparisonSessionIds] = useState<Set<string>>(
51
- new Set(),
52
- );
43
+ const [_comparisonSessionIds, setComparisonSessionIds] = useState<
44
+ Set<string>
45
+ >(new Set());
53
46
  const [loading, setLoading] = useState(true);
54
47
  const [error, setError] = useState<string | null>(null);
55
48
 
@@ -64,6 +57,7 @@ export function TownHall() {
64
57
  const [variantModel, setVariantModel] = useState<string | null>(null);
65
58
  const [variantSystemPrompt, setVariantSystemPrompt] = useState<string>("");
66
59
  const [variantTools, setVariantTools] = useState<string[]>([]);
60
+ const [hypothesis, setHypothesis] = useState<string>("");
67
61
 
68
62
  // Running comparison state
69
63
  const [runningComparison, setRunningComparison] = useState<string | null>(
@@ -175,13 +169,14 @@ export function TownHall() {
175
169
  variantSystemPrompt !== "" &&
176
170
  variantSystemPrompt !== agentConfig?.systemPrompt
177
171
  );
178
- case "tools":
172
+ case "tools": {
179
173
  const currentToolNames =
180
174
  agentConfig?.tools?.map((t) => t.name).sort() || [];
181
175
  const variantToolNames = [...variantTools].sort();
182
176
  return (
183
177
  JSON.stringify(currentToolNames) !== JSON.stringify(variantToolNames)
184
178
  );
179
+ }
185
180
  default:
186
181
  return false;
187
182
  }
@@ -224,6 +219,8 @@ export function TownHall() {
224
219
  // Include tools if selected
225
220
  ...(selectedDimensions.has("tools") &&
226
221
  variantTools.length > 0 && { variantTools }),
222
+ // Include hypothesis if provided
223
+ ...(hypothesis && { hypothesis }),
227
224
  };
228
225
 
229
226
  // Save config
@@ -477,6 +474,25 @@ export function TownHall() {
477
474
  )}
478
475
  </div>
479
476
  )}
477
+
478
+ {/* Hypothesis input - shown when any dimension is selected */}
479
+ {selectedDimensions.size > 0 && (
480
+ <div className="space-y-1 pt-2 border-t">
481
+ <Label className="text-xs text-muted-foreground">
482
+ Hypothesis (optional)
483
+ </Label>
484
+ <Textarea
485
+ value={hypothesis}
486
+ onChange={(e) => setHypothesis(e.target.value)}
487
+ className="min-h-[60px] text-xs"
488
+ placeholder="I expect this change to... (e.g., 'reduce token usage by caching file reads' or 'improve answer accuracy by adding web search')"
489
+ />
490
+ <p className="text-xs text-muted-foreground">
491
+ Describe what you expect the change to achieve. This helps
492
+ the analysis evaluate if your hypothesis was correct.
493
+ </p>
494
+ </div>
495
+ )}
480
496
  </CardContent>
481
497
  </Card>
482
498
  {/* Status indicator - inline below card */}
@@ -527,9 +543,9 @@ export function TownHall() {
527
543
  variant="outline"
528
544
  size="sm"
529
545
  className="h-7 px-2 text-xs"
530
- onClick={() =>
531
- (window.location.href = `/sessions/${session.session_id}`)
532
- }
546
+ onClick={() => {
547
+ window.location.href = `/sessions/${session.session_id}`;
548
+ }}
533
549
  >
534
550
  View
535
551
  </Button>
package/src/server.ts CHANGED
@@ -12,7 +12,6 @@ import type {
12
12
  ComparisonConfig,
13
13
  ConversationTrace,
14
14
  SessionMetrics,
15
- Span,
16
15
  } from "./types";
17
16
 
18
17
  export const DEFAULT_DEBUGGER_PORT = 4000;
@@ -113,6 +112,7 @@ export function startDebuggerServer(
113
112
  // Start debugger UI server
114
113
  const server = serve({
115
114
  port,
115
+ idleTimeout: 120, // 2 minutes for long-running LLM analysis requests
116
116
  routes: {
117
117
  "/api/config": {
118
118
  GET() {
@@ -140,8 +140,12 @@ export function startDebuggerServer(
140
140
  const url = new URL(req.url);
141
141
  const limit = Number.parseInt(
142
142
  url.searchParams.get("limit") || "1000",
143
+ 10,
144
+ );
145
+ const offset = Number.parseInt(
146
+ url.searchParams.get("offset") || "0",
147
+ 10,
143
148
  );
144
- const offset = Number.parseInt(url.searchParams.get("offset") || "0");
145
149
  const sessions = db.listSessions(limit, offset);
146
150
  return Response.json(sessions);
147
151
  },
@@ -150,8 +154,14 @@ export function startDebuggerServer(
150
154
  "/api/traces": {
151
155
  GET(req) {
152
156
  const url = new URL(req.url);
153
- const limit = Number.parseInt(url.searchParams.get("limit") || "50");
154
- const offset = Number.parseInt(url.searchParams.get("offset") || "0");
157
+ const limit = Number.parseInt(
158
+ url.searchParams.get("limit") || "50",
159
+ 10,
160
+ );
161
+ const offset = Number.parseInt(
162
+ url.searchParams.get("offset") || "0",
163
+ 10,
164
+ );
155
165
  const sessionId = url.searchParams.get("sessionId") || undefined;
156
166
  const traces = db.listTraces(limit, offset, sessionId);
157
167
  return Response.json(traces);
@@ -309,8 +319,14 @@ export function startDebuggerServer(
309
319
  "/api/comparison-runs": {
310
320
  GET(req) {
311
321
  const url = new URL(req.url);
312
- const limit = Number.parseInt(url.searchParams.get("limit") || "50");
313
- const offset = Number.parseInt(url.searchParams.get("offset") || "0");
322
+ const limit = Number.parseInt(
323
+ url.searchParams.get("limit") || "50",
324
+ 10,
325
+ );
326
+ const offset = Number.parseInt(
327
+ url.searchParams.get("offset") || "0",
328
+ 10,
329
+ );
314
330
  const sourceSessionId = url.searchParams.get("sourceSessionId");
315
331
 
316
332
  if (sourceSessionId) {
@@ -481,7 +497,7 @@ export function startDebuggerServer(
481
497
  });
482
498
 
483
499
  return Response.json({ success: true });
484
- } catch (error) {
500
+ } catch (_error) {
485
501
  return Response.json(
486
502
  { error: "Failed to update comparison run" },
487
503
  { status: 500 },
@@ -799,9 +815,11 @@ export function startDebuggerServer(
799
815
  // List all analyses
800
816
  const limit = Number.parseInt(
801
817
  url.searchParams.get("limit") || "50",
818
+ 10,
802
819
  );
803
820
  const offset = Number.parseInt(
804
821
  url.searchParams.get("offset") || "0",
822
+ 10,
805
823
  );
806
824
 
807
825
  const analyses = analysisDb.listAnalyses(limit, offset);
@@ -828,6 +846,7 @@ export function startDebuggerServer(
828
846
  const url = new URL(req.url);
829
847
  const limit = Number.parseInt(
830
848
  url.searchParams.get("limit") || "10",
849
+ 10,
831
850
  );
832
851
 
833
852
  // Get embedding for this session
@@ -862,6 +881,157 @@ export function startDebuggerServer(
862
881
  },
863
882
  },
864
883
 
884
+ // Comparison analysis endpoints
885
+ "/api/analyze-comparison/:runId": {
886
+ async POST(req) {
887
+ const runId = req.params.runId;
888
+
889
+ try {
890
+ // Import analyzer dynamically
891
+ const { analyzeComparison } = await import(
892
+ "./analysis/comparison-analyzer.js"
893
+ );
894
+
895
+ // Get the comparison run
896
+ const run = comparisonDb.getRun(runId);
897
+ if (!run) {
898
+ return Response.json(
899
+ { error: "Comparison run not found" },
900
+ { status: 404 },
901
+ );
902
+ }
903
+
904
+ // Get the comparison config
905
+ const config = comparisonDb.getConfig(run.configId);
906
+ if (!config) {
907
+ return Response.json(
908
+ { error: "Comparison config not found" },
909
+ { status: 404 },
910
+ );
911
+ }
912
+
913
+ // Verify all sessions exist
914
+ if (!run.controlSessionId || !run.variantSessionId) {
915
+ return Response.json(
916
+ { error: "Comparison run is incomplete - missing session IDs" },
917
+ { status: 400 },
918
+ );
919
+ }
920
+
921
+ // Fetch all three sessions from agent server
922
+ const [originalRes, controlRes, variantRes] = await Promise.all([
923
+ fetch(`${agentServerUrl}/sessions/${run.sourceSessionId}`),
924
+ fetch(`${agentServerUrl}/sessions/${run.controlSessionId}`),
925
+ fetch(`${agentServerUrl}/sessions/${run.variantSessionId}`),
926
+ ]);
927
+
928
+ if (!originalRes.ok || !controlRes.ok || !variantRes.ok) {
929
+ return Response.json(
930
+ { error: "Failed to fetch one or more sessions" },
931
+ { status: 500 },
932
+ );
933
+ }
934
+
935
+ const [originalSession, controlSession, variantSession] =
936
+ await Promise.all([
937
+ originalRes.json(),
938
+ controlRes.json(),
939
+ variantRes.json(),
940
+ ]);
941
+
942
+ // Get agent config for original tools and system prompt
943
+ const agentConfig = await fetchAgentConfig();
944
+
945
+ // Get metrics for each session
946
+ const getMetrics = (sessionId: string) => {
947
+ const spans = db.getSpansBySessionAttribute(sessionId);
948
+ const traces = db.listTraces(100, 0, sessionId);
949
+ return extractSessionMetrics(
950
+ traces,
951
+ spans,
952
+ agentConfig?.model || "unknown",
953
+ );
954
+ };
955
+
956
+ const originalMetrics = getMetrics(run.sourceSessionId);
957
+ const controlMetrics = getMetrics(run.controlSessionId);
958
+ const variantMetrics = getMetrics(run.variantSessionId);
959
+
960
+ // Run the comparison analysis
961
+ const analysis = await analyzeComparison({
962
+ runId,
963
+ hypothesis: config.hypothesis || "",
964
+ config,
965
+ originalSession,
966
+ controlSession,
967
+ variantSession,
968
+ originalMetrics,
969
+ controlMetrics,
970
+ variantMetrics,
971
+ originalSystemPrompt: agentConfig?.systemPrompt || undefined,
972
+ originalTools: agentConfig?.tools?.map((t) => t.name) || [],
973
+ });
974
+
975
+ // Save to database
976
+ comparisonDb.saveComparisonAnalysis(runId, analysis);
977
+
978
+ return Response.json(analysis);
979
+ } catch (error) {
980
+ console.error("Comparison analysis error:", error);
981
+ return Response.json(
982
+ {
983
+ error:
984
+ error instanceof Error
985
+ ? error.message
986
+ : "Comparison analysis failed",
987
+ },
988
+ { status: 500 },
989
+ );
990
+ }
991
+ },
992
+ },
993
+
994
+ "/api/comparison-analysis/:runId": {
995
+ async GET(req) {
996
+ try {
997
+ const runId = req.params.runId;
998
+ const analysis = comparisonDb.getComparisonAnalysis(runId);
999
+
1000
+ if (!analysis) {
1001
+ return Response.json(
1002
+ { error: "Comparison analysis not found" },
1003
+ { status: 404 },
1004
+ );
1005
+ }
1006
+
1007
+ return Response.json(analysis);
1008
+ } catch (error) {
1009
+ console.error("Error fetching comparison analysis:", error);
1010
+ return Response.json(
1011
+ {
1012
+ error:
1013
+ error instanceof Error
1014
+ ? error.message
1015
+ : "Failed to fetch comparison analysis",
1016
+ },
1017
+ { status: 500 },
1018
+ );
1019
+ }
1020
+ },
1021
+ },
1022
+
1023
+ "/api/comparison-analysis/:runId/exists": {
1024
+ async GET(req) {
1025
+ try {
1026
+ const runId = req.params.runId;
1027
+ const exists = comparisonDb.hasComparisonAnalysis(runId);
1028
+ return Response.json({ exists });
1029
+ } catch (_error) {
1030
+ return Response.json({ exists: false });
1031
+ }
1032
+ },
1033
+ },
1034
+
865
1035
  // Serve index.html for all unmatched routes (SPA routing)
866
1036
  "/*": index,
867
1037
  },
package/src/types.ts CHANGED
@@ -54,6 +54,8 @@ export interface AgentMessage {
54
54
  timestamp: number; // end_time_unix_nano of the chat span
55
55
  type: "chat" | "tool_call";
56
56
  toolName?: string; // Only for tool_call type
57
+ toolInput?: unknown; // Only for tool_call type - the input/args to the tool
58
+ toolOutput?: unknown; // Only for tool_call type - the result from the tool
57
59
  }
58
60
 
59
61
  export interface TraceDetail extends TraceDetailRaw {
@@ -90,6 +92,7 @@ export interface ComparisonConfig {
90
92
  variantModel?: string | undefined;
91
93
  variantSystemPrompt?: string | undefined;
92
94
  variantTools?: string[] | undefined; // JSON array of tool names
95
+ hypothesis?: string | undefined; // User's expected outcome: "I expect this change to..."
93
96
  createdAt: string;
94
97
  updatedAt: string;
95
98
  }
@@ -101,6 +104,7 @@ export interface ComparisonConfigRow {
101
104
  variant_model: string | null;
102
105
  variant_system_prompt: string | null;
103
106
  variant_tools: string | null; // JSON string
107
+ hypothesis: string | null;
104
108
  created_at: string;
105
109
  updated_at: string;
106
110
  }
@@ -0,0 +1,120 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @custom-variant dark (&:is(.dark *));
5
+
6
+ @theme inline {
7
+ --color-background: var(--background);
8
+ --color-foreground: var(--foreground);
9
+ --color-sidebar-ring: var(--sidebar-ring);
10
+ --color-sidebar-border: var(--sidebar-border);
11
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
12
+ --color-sidebar-accent: var(--sidebar-accent);
13
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
14
+ --color-sidebar-primary: var(--sidebar-primary);
15
+ --color-sidebar-foreground: var(--sidebar-foreground);
16
+ --color-sidebar: var(--sidebar);
17
+ --color-chart-5: var(--chart-5);
18
+ --color-chart-4: var(--chart-4);
19
+ --color-chart-3: var(--chart-3);
20
+ --color-chart-2: var(--chart-2);
21
+ --color-chart-1: var(--chart-1);
22
+ --color-ring: var(--ring);
23
+ --color-input: var(--input);
24
+ --color-border: var(--border);
25
+ --color-destructive: var(--destructive);
26
+ --color-accent-foreground: var(--accent-foreground);
27
+ --color-accent: var(--accent);
28
+ --color-muted-foreground: var(--muted-foreground);
29
+ --color-muted: var(--muted);
30
+ --color-secondary-foreground: var(--secondary-foreground);
31
+ --color-secondary: var(--secondary);
32
+ --color-primary-foreground: var(--primary-foreground);
33
+ --color-primary: var(--primary);
34
+ --color-popover-foreground: var(--popover-foreground);
35
+ --color-popover: var(--popover);
36
+ --color-card-foreground: var(--card-foreground);
37
+ --color-card: var(--card);
38
+ --radius-sm: calc(var(--radius) - 4px);
39
+ --radius-md: calc(var(--radius) - 2px);
40
+ --radius-lg: var(--radius);
41
+ --radius-xl: calc(var(--radius) + 4px);
42
+ }
43
+
44
+ :root {
45
+ --radius: 0.625rem;
46
+ --background: oklch(1 0 0);
47
+ --foreground: oklch(0.145 0 0);
48
+ --card: oklch(1 0 0);
49
+ --card-foreground: oklch(0.145 0 0);
50
+ --popover: oklch(1 0 0);
51
+ --popover-foreground: oklch(0.145 0 0);
52
+ --primary: oklch(0.205 0 0);
53
+ --primary-foreground: oklch(0.985 0 0);
54
+ --secondary: oklch(0.97 0 0);
55
+ --secondary-foreground: oklch(0.205 0 0);
56
+ --muted: oklch(0.97 0 0);
57
+ --muted-foreground: oklch(0.556 0 0);
58
+ --accent: oklch(0.97 0 0);
59
+ --accent-foreground: oklch(0.205 0 0);
60
+ --destructive: oklch(0.577 0.245 27.325);
61
+ --border: oklch(0.922 0 0);
62
+ --input: oklch(0.922 0 0);
63
+ --ring: oklch(0.708 0 0);
64
+ --chart-1: oklch(0.646 0.222 41.116);
65
+ --chart-2: oklch(0.6 0.118 184.704);
66
+ --chart-3: oklch(0.398 0.07 227.392);
67
+ --chart-4: oklch(0.828 0.189 84.429);
68
+ --chart-5: oklch(0.769 0.188 70.08);
69
+ --sidebar: oklch(0.985 0 0);
70
+ --sidebar-foreground: oklch(0.145 0 0);
71
+ --sidebar-primary: oklch(0.205 0 0);
72
+ --sidebar-primary-foreground: oklch(0.985 0 0);
73
+ --sidebar-accent: oklch(0.97 0 0);
74
+ --sidebar-accent-foreground: oklch(0.205 0 0);
75
+ --sidebar-border: oklch(0.922 0 0);
76
+ --sidebar-ring: oklch(0.708 0 0);
77
+ }
78
+
79
+ .dark {
80
+ --background: oklch(0.145 0 0);
81
+ --foreground: oklch(0.985 0 0);
82
+ --card: oklch(0.205 0 0);
83
+ --card-foreground: oklch(0.985 0 0);
84
+ --popover: oklch(0.205 0 0);
85
+ --popover-foreground: oklch(0.985 0 0);
86
+ --primary: oklch(0.922 0 0);
87
+ --primary-foreground: oklch(0.205 0 0);
88
+ --secondary: oklch(0.269 0 0);
89
+ --secondary-foreground: oklch(0.985 0 0);
90
+ --muted: oklch(0.269 0 0);
91
+ --muted-foreground: oklch(0.708 0 0);
92
+ --accent: oklch(0.269 0 0);
93
+ --accent-foreground: oklch(0.985 0 0);
94
+ --destructive: oklch(0.704 0.191 22.216);
95
+ --border: oklch(1 0 0 / 10%);
96
+ --input: oklch(1 0 0 / 15%);
97
+ --ring: oklch(0.556 0 0);
98
+ --chart-1: oklch(0.488 0.243 264.376);
99
+ --chart-2: oklch(0.696 0.17 162.48);
100
+ --chart-3: oklch(0.769 0.188 70.08);
101
+ --chart-4: oklch(0.627 0.265 303.9);
102
+ --chart-5: oklch(0.645 0.246 16.439);
103
+ --sidebar: oklch(0.205 0 0);
104
+ --sidebar-foreground: oklch(0.985 0 0);
105
+ --sidebar-primary: oklch(0.488 0.243 264.376);
106
+ --sidebar-primary-foreground: oklch(0.985 0 0);
107
+ --sidebar-accent: oklch(0.269 0 0);
108
+ --sidebar-accent-foreground: oklch(0.985 0 0);
109
+ --sidebar-border: oklch(1 0 0 / 10%);
110
+ --sidebar-ring: oklch(0.556 0 0);
111
+ }
112
+
113
+ @layer base {
114
+ * {
115
+ @apply border-border outline-ring/50;
116
+ }
117
+ body {
118
+ @apply bg-background text-foreground;
119
+ }
120
+ }
package/tsconfig.json CHANGED
@@ -3,10 +3,10 @@
3
3
  "compilerOptions": {
4
4
  "composite": false,
5
5
  "declaration": false,
6
- "baseUrl": ".",
7
6
  "outDir": "dist",
8
7
  "paths": {
9
- "@/*": ["./src/*"]
8
+ "@/*": ["./src/*"],
9
+ "*": ["./*"]
10
10
  }
11
11
  },
12
12
  "include": ["src/**/*"],