@townco/debugger 0.1.23 → 0.1.25

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/server.ts CHANGED
@@ -1,9 +1,17 @@
1
+ import { resetDb } from "@townco/otlp-server/db";
1
2
  import { createOtlpServer } from "@townco/otlp-server/http";
2
3
  import { serve } from "bun";
4
+ import { ComparisonDb } from "./comparison-db";
3
5
  import { DebuggerDb } from "./db";
4
6
  import index from "./index.html";
7
+ import { extractSessionMetrics } from "./lib/metrics";
5
8
  import { extractTurnMessages } from "./lib/turnExtractor";
6
- import type { ConversationTrace } from "./types";
9
+ import type {
10
+ AgentConfig,
11
+ ComparisonConfig,
12
+ ConversationTrace,
13
+ Span,
14
+ } from "./types";
7
15
 
8
16
  export const DEFAULT_DEBUGGER_PORT = 4000;
9
17
  export const DEFAULT_OTLP_PORT = 4318;
@@ -13,6 +21,7 @@ export interface DebuggerServerOptions {
13
21
  otlpPort?: number;
14
22
  dbPath: string;
15
23
  agentName?: string;
24
+ agentServerUrl?: string;
16
25
  }
17
26
 
18
27
  export interface DebuggerServerResult {
@@ -29,6 +38,7 @@ export function startDebuggerServer(
29
38
  otlpPort = DEFAULT_OTLP_PORT,
30
39
  dbPath,
31
40
  agentName = "Agent",
41
+ agentServerUrl = "http://localhost:3100",
32
42
  } = options;
33
43
 
34
44
  // Start OTLP server (initializes database internally)
@@ -41,6 +51,59 @@ export function startDebuggerServer(
41
51
  // Create debugger database connection for reading
42
52
  const db = new DebuggerDb(dbPath);
43
53
 
54
+ // Create comparison database for Town Hall feature
55
+ const comparisonDbPath = dbPath.replace(/\.db$/, "-comparison.db");
56
+ const comparisonDb = new ComparisonDb(comparisonDbPath);
57
+
58
+ // Helper to fetch agent config from agent server
59
+ async function fetchAgentConfig(): Promise<AgentConfig | null> {
60
+ try {
61
+ // Call agent's initialize RPC to get config
62
+ const response = await fetch(`${agentServerUrl}/rpc`, {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/json" },
65
+ body: JSON.stringify({
66
+ jsonrpc: "2.0",
67
+ id: "debugger-config",
68
+ method: "initialize",
69
+ params: {
70
+ protocolVersion: 1, // ACP protocol version as number
71
+ clientCapabilities: {},
72
+ },
73
+ }),
74
+ });
75
+
76
+ if (!response.ok) {
77
+ console.error("Failed to fetch agent config:", response.statusText);
78
+ return null;
79
+ }
80
+
81
+ const data = await response.json();
82
+
83
+ // Check for JSON-RPC error
84
+ if (data.error) {
85
+ console.error("Agent RPC error:", data.error);
86
+ return null;
87
+ }
88
+
89
+ const result = data.result;
90
+ if (!result) {
91
+ console.error("No result in agent response");
92
+ return null;
93
+ }
94
+
95
+ // Extract config from initialize response
96
+ return {
97
+ model: result._meta?.model || "unknown",
98
+ systemPrompt: result._meta?.systemPrompt || null,
99
+ tools: result._meta?.tools || [],
100
+ };
101
+ } catch (error) {
102
+ console.error("Error fetching agent config:", error);
103
+ return null;
104
+ }
105
+ }
106
+
44
107
  // Start debugger UI server
45
108
  const server = serve({
46
109
  port,
@@ -51,6 +114,21 @@ export function startDebuggerServer(
51
114
  },
52
115
  },
53
116
 
117
+ "/api/reset-database": {
118
+ POST() {
119
+ try {
120
+ resetDb();
121
+ return new Response("Database reset successfully", { status: 200 });
122
+ } catch (error) {
123
+ console.error("Error resetting database:", error);
124
+ return new Response(
125
+ `Failed to reset database: ${error instanceof Error ? error.message : String(error)}`,
126
+ { status: 500 },
127
+ );
128
+ }
129
+ },
130
+ },
131
+
54
132
  "/api/sessions": {
55
133
  GET(req) {
56
134
  const url = new URL(req.url);
@@ -96,22 +174,277 @@ export function startDebuggerServer(
96
174
  );
97
175
  }
98
176
 
99
- // Get all traces for the session (already sorted ASC)
100
- const traces = db.listTraces(50, 0, sessionId);
177
+ // Query traces by session attribute to avoid race conditions
178
+ const traceIds = db.getTraceIdsBySessionAttribute(sessionId);
101
179
 
102
180
  // Extract messages for each trace
103
- const conversation: ConversationTrace[] = traces.map((trace) => {
181
+ const conversation: ConversationTrace[] = traceIds.map(
182
+ (traceInfo) => {
183
+ const data = db.getTraceById(traceInfo.trace_id);
184
+ const messages = extractTurnMessages(data.spans, data.logs);
185
+ return {
186
+ trace_id: traceInfo.trace_id,
187
+ start_time_unix_nano: traceInfo.start_time_unix_nano,
188
+ userInput: messages.userInput,
189
+ llmOutput: messages.llmOutput,
190
+ agentMessages: messages.agentMessages,
191
+ };
192
+ },
193
+ );
194
+
195
+ return Response.json(conversation);
196
+ },
197
+ },
198
+
199
+ // Town Hall API endpoints
200
+
201
+ "/api/agent-config": {
202
+ async GET() {
203
+ const config = await fetchAgentConfig();
204
+ if (!config) {
205
+ return Response.json(
206
+ { error: "Failed to fetch agent config" },
207
+ { status: 503 },
208
+ );
209
+ }
210
+ return Response.json(config);
211
+ },
212
+ },
213
+
214
+ "/api/available-models": {
215
+ GET() {
216
+ // List of supported models for comparison
217
+ const models = [
218
+ // Anthropic models
219
+ "claude-sonnet-4-5-20250929",
220
+ "claude-3-5-haiku-20241022",
221
+ "claude-opus-4-5-20251101",
222
+ // Google Gemini models
223
+ "gemini-2.0-flash",
224
+ "gemini-1.5-pro",
225
+ "gemini-1.5-flash",
226
+ ];
227
+ return Response.json({ models });
228
+ },
229
+ },
230
+
231
+ "/api/session-first-message/:sessionId": {
232
+ GET(req) {
233
+ const sessionId = req.params.sessionId;
234
+
235
+ // Query logs directly by session attribute to avoid race conditions
236
+ // with trace.session_id association during concurrent sessions
237
+ const message = db.getFirstUserMessageBySession(sessionId);
238
+
239
+ if (!message) {
240
+ return Response.json(
241
+ { error: "Session not found or has no user message" },
242
+ { status: 404 },
243
+ );
244
+ }
245
+
246
+ return Response.json({ message });
247
+ },
248
+ },
249
+
250
+ "/api/comparison-config": {
251
+ GET() {
252
+ const config = comparisonDb.getLatestConfig();
253
+ return Response.json(config);
254
+ },
255
+ async POST(req) {
256
+ try {
257
+ const body = await req.json();
258
+ const config: ComparisonConfig = {
259
+ id: body.id || crypto.randomUUID(),
260
+ dimension: body.dimension,
261
+ controlModel: body.controlModel,
262
+ variantModel: body.variantModel,
263
+ variantSystemPrompt: body.variantSystemPrompt,
264
+ variantTools: body.variantTools,
265
+ createdAt: body.createdAt || new Date().toISOString(),
266
+ updatedAt: new Date().toISOString(),
267
+ };
268
+ comparisonDb.saveConfig(config);
269
+ return Response.json({ id: config.id });
270
+ } catch (error) {
271
+ return Response.json(
272
+ { error: "Invalid request body" },
273
+ { status: 400 },
274
+ );
275
+ }
276
+ },
277
+ },
278
+
279
+ "/api/comparison-session-ids": {
280
+ GET() {
281
+ const sessionIds = comparisonDb.getComparisonSessionIds();
282
+ return Response.json({ sessionIds });
283
+ },
284
+ },
285
+
286
+ "/api/comparison-runs": {
287
+ GET(req) {
288
+ const url = new URL(req.url);
289
+ const limit = Number.parseInt(url.searchParams.get("limit") || "50");
290
+ const offset = Number.parseInt(url.searchParams.get("offset") || "0");
291
+ const sourceSessionId = url.searchParams.get("sourceSessionId");
292
+
293
+ if (sourceSessionId) {
294
+ const runs = comparisonDb.listRunsBySourceSession(sourceSessionId);
295
+ return Response.json(runs);
296
+ }
297
+
298
+ const runs = comparisonDb.listRuns(limit, offset);
299
+ return Response.json(runs);
300
+ },
301
+ },
302
+
303
+ "/api/comparison-run/:runId": {
304
+ GET(req) {
305
+ const runId = req.params.runId;
306
+ const run = comparisonDb.getRun(runId);
307
+ if (!run) {
308
+ return Response.json(
309
+ { error: "Comparison run not found" },
310
+ { status: 404 },
311
+ );
312
+ }
313
+ return Response.json(run);
314
+ },
315
+ },
316
+
317
+ "/api/run-comparison": {
318
+ async POST(req) {
319
+ try {
320
+ const body = await req.json();
321
+ const { sessionId, configId } = body;
322
+
323
+ if (!sessionId || !configId) {
324
+ return Response.json(
325
+ { error: "sessionId and configId are required" },
326
+ { status: 400 },
327
+ );
328
+ }
329
+
330
+ // Get the comparison config
331
+ const config = comparisonDb.getConfig(configId);
332
+ if (!config) {
333
+ return Response.json(
334
+ { error: "Comparison config not found" },
335
+ { status: 404 },
336
+ );
337
+ }
338
+
339
+ // Get the first user message from the source session
340
+ const traces = db.listTraces(1, 0, sessionId);
341
+ if (traces.length === 0) {
342
+ return Response.json(
343
+ { error: "Source session not found" },
344
+ { status: 404 },
345
+ );
346
+ }
347
+
348
+ const trace = traces[0];
349
+ if (!trace) {
350
+ return Response.json(
351
+ { error: "Source session not found" },
352
+ { status: 404 },
353
+ );
354
+ }
355
+
104
356
  const data = db.getTraceById(trace.trace_id);
105
357
  const messages = extractTurnMessages(data.spans, data.logs);
106
- return {
107
- trace_id: trace.trace_id,
108
- start_time_unix_nano: trace.start_time_unix_nano,
109
- userInput: messages.userInput,
110
- llmOutput: messages.llmOutput,
111
- };
112
- });
113
358
 
114
- return Response.json(conversation);
359
+ if (!messages.userInput) {
360
+ return Response.json(
361
+ { error: "No user message found in source session" },
362
+ { status: 400 },
363
+ );
364
+ }
365
+
366
+ // Create the comparison run
367
+ const run = comparisonDb.createRun(
368
+ configId,
369
+ sessionId,
370
+ messages.userInput,
371
+ );
372
+
373
+ // Return the run info - actual execution will be handled by the frontend
374
+ // which will create two ACP sessions and run them in parallel
375
+ return Response.json({
376
+ runId: run.id,
377
+ firstUserMessage: run.firstUserMessage,
378
+ config,
379
+ });
380
+ } catch (error) {
381
+ console.error("Error starting comparison:", error);
382
+ return Response.json(
383
+ { error: "Failed to start comparison" },
384
+ { status: 500 },
385
+ );
386
+ }
387
+ },
388
+ },
389
+
390
+ "/api/comparison-run/:runId/update": {
391
+ async POST(req) {
392
+ try {
393
+ const runId = req.params.runId;
394
+ const body = await req.json();
395
+ const {
396
+ status,
397
+ controlSessionId,
398
+ variantSessionId,
399
+ controlMetrics,
400
+ variantMetrics,
401
+ controlResponse,
402
+ variantResponse,
403
+ } = body;
404
+
405
+ comparisonDb.updateRunStatus(runId, status, {
406
+ controlSessionId,
407
+ variantSessionId,
408
+ controlMetrics,
409
+ variantMetrics,
410
+ controlResponse,
411
+ variantResponse,
412
+ });
413
+
414
+ return Response.json({ success: true });
415
+ } catch (error) {
416
+ return Response.json(
417
+ { error: "Failed to update comparison run" },
418
+ { status: 500 },
419
+ );
420
+ }
421
+ },
422
+ },
423
+
424
+ "/api/session-metrics/:sessionId": {
425
+ async GET(req) {
426
+ const sessionId = req.params.sessionId;
427
+ const url = new URL(req.url);
428
+ const model = url.searchParams.get("model") || "unknown";
429
+
430
+ // Query spans by their agent.session_id attribute directly
431
+ // This is more reliable than trace-based lookup because concurrent
432
+ // sessions can cause race conditions in trace association
433
+ const allSpans = db.getSpansBySessionAttribute(sessionId);
434
+
435
+ if (allSpans.length === 0) {
436
+ return Response.json(
437
+ { error: "Session not found or has no traces" },
438
+ { status: 404 },
439
+ );
440
+ }
441
+
442
+ // Get traces for duration calculation (use empty array if not found)
443
+ const traces = db.listTraces(100, 0, sessionId);
444
+
445
+ // Extract metrics
446
+ const metrics = extractSessionMetrics(traces, allSpans, model);
447
+ return Response.json(metrics);
115
448
  },
116
449
  },
117
450
 
package/src/types.ts CHANGED
@@ -48,10 +48,19 @@ export interface TraceDetailRaw {
48
48
  logs: Log[];
49
49
  }
50
50
 
51
+ export interface AgentMessage {
52
+ content: string;
53
+ spanId: string;
54
+ timestamp: number; // end_time_unix_nano of the chat span
55
+ type: "chat" | "tool_call";
56
+ toolName?: string; // Only for tool_call type
57
+ }
58
+
51
59
  export interface TraceDetail extends TraceDetailRaw {
52
60
  messages: {
53
61
  userInput: string | null;
54
62
  llmOutput: string | null;
63
+ agentMessages: AgentMessage[];
55
64
  };
56
65
  }
57
66
 
@@ -60,6 +69,7 @@ export interface ConversationTrace {
60
69
  start_time_unix_nano: number;
61
70
  userInput: string | null;
62
71
  llmOutput: string | null;
72
+ agentMessages: AgentMessage[];
63
73
  }
64
74
 
65
75
  export interface Session {
@@ -68,3 +78,80 @@ export interface Session {
68
78
  first_trace_time: number;
69
79
  last_trace_time: number;
70
80
  }
81
+
82
+ // Town Hall comparison types
83
+
84
+ export type ComparisonDimension = "model" | "system_prompt" | "tools";
85
+
86
+ export interface ComparisonConfig {
87
+ id: string;
88
+ dimension: ComparisonDimension;
89
+ controlModel?: string | undefined; // Original model for comparison
90
+ variantModel?: string | undefined;
91
+ variantSystemPrompt?: string | undefined;
92
+ variantTools?: string[] | undefined; // JSON array of tool names
93
+ createdAt: string;
94
+ updatedAt: string;
95
+ }
96
+
97
+ export interface ComparisonConfigRow {
98
+ id: string;
99
+ dimension: string;
100
+ control_model: string | null;
101
+ variant_model: string | null;
102
+ variant_system_prompt: string | null;
103
+ variant_tools: string | null; // JSON string
104
+ created_at: string;
105
+ updated_at: string;
106
+ }
107
+
108
+ export interface SessionMetrics {
109
+ durationMs: number;
110
+ inputTokens: number;
111
+ outputTokens: number;
112
+ totalTokens: number;
113
+ estimatedCost: number;
114
+ toolCallCount: number;
115
+ }
116
+
117
+ export interface ComparisonRun {
118
+ id: string;
119
+ configId: string;
120
+ sourceSessionId: string;
121
+ firstUserMessage: string;
122
+ startMessageIndex: number;
123
+ turnCount: number;
124
+ controlSessionId: string | null;
125
+ variantSessionId: string | null;
126
+ status: "pending" | "running" | "completed" | "failed";
127
+ startedAt: string;
128
+ completedAt: string | null;
129
+ controlMetrics: SessionMetrics | null;
130
+ variantMetrics: SessionMetrics | null;
131
+ controlResponse: string | null;
132
+ variantResponse: string | null;
133
+ }
134
+
135
+ export interface ComparisonRunRow {
136
+ id: string;
137
+ config_id: string;
138
+ source_session_id: string;
139
+ first_user_message: string;
140
+ start_message_index: number;
141
+ turn_count: number;
142
+ control_session_id: string | null;
143
+ variant_session_id: string | null;
144
+ status: string;
145
+ started_at: string;
146
+ completed_at: string | null;
147
+ control_metrics: string | null; // JSON string
148
+ variant_metrics: string | null; // JSON string
149
+ control_response: string | null;
150
+ variant_response: string | null;
151
+ }
152
+
153
+ export interface AgentConfig {
154
+ model: string;
155
+ systemPrompt: string | null;
156
+ tools: Array<{ name: string; description?: string }>;
157
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "@townco/tsconfig",
3
+ "compilerOptions": {
4
+ "composite": false,
5
+ "declaration": false,
6
+ "baseUrl": ".",
7
+ "outDir": "dist",
8
+ "paths": {
9
+ "@/*": ["./src/*"]
10
+ }
11
+ },
12
+ "include": ["src/**/*"],
13
+ "exclude": ["dist", "node_modules"]
14
+ }