@199-bio/engram 0.8.1 → 0.10.0

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.
@@ -119,6 +119,37 @@ export interface RetrievalLog {
119
119
  processed: boolean; // Has this been used for learning?
120
120
  }
121
121
 
122
+ /**
123
+ * Consolidation checkpoint for safe backlog processing
124
+ * Enables resume capability and progress tracking
125
+ */
126
+ export interface ConsolidationCheckpoint {
127
+ id: string;
128
+ run_id: string; // Unique ID for this consolidation run
129
+ phase: "episodes" | "memories" | "decay" | "cleanup" | "complete";
130
+ batches_completed: number;
131
+ batches_total: number;
132
+ memories_processed: number;
133
+ episodes_processed: number;
134
+ digests_created: number;
135
+ contradictions_found: number;
136
+ tokens_used: number;
137
+ estimated_cost_usd: number;
138
+ started_at: Date;
139
+ updated_at: Date;
140
+ completed_at: Date | null;
141
+ error: string | null;
142
+ }
143
+
144
+ /**
145
+ * Consolidation configuration for budget and rate limits
146
+ */
147
+ export interface ConsolidationConfig {
148
+ key: string;
149
+ value: string;
150
+ updated_at: Date;
151
+ }
152
+
122
153
  export class EngramDatabase {
123
154
  private db: Database.Database;
124
155
  private stmtCache: Map<string, Database.Statement> = new Map();
@@ -284,6 +315,34 @@ export class EngramDatabase {
284
315
  CREATE INDEX IF NOT EXISTS idx_digest_sources_memory ON digest_sources(memory_id);
285
316
  `);
286
317
 
318
+ // FTS5 for digest BM25 search
319
+ this.db.exec(`
320
+ CREATE VIRTUAL TABLE IF NOT EXISTS digests_fts USING fts5(
321
+ content,
322
+ topic,
323
+ content='digests',
324
+ content_rowid='rowid'
325
+ );
326
+
327
+ -- Triggers to keep FTS in sync
328
+ CREATE TRIGGER IF NOT EXISTS digests_ai AFTER INSERT ON digests BEGIN
329
+ INSERT INTO digests_fts(rowid, content, topic) VALUES (NEW.rowid, NEW.content, COALESCE(NEW.topic, ''));
330
+ END;
331
+
332
+ CREATE TRIGGER IF NOT EXISTS digests_ad AFTER DELETE ON digests BEGIN
333
+ INSERT INTO digests_fts(digests_fts, rowid, content, topic) VALUES('delete', OLD.rowid, OLD.content, COALESCE(OLD.topic, ''));
334
+ END;
335
+
336
+ CREATE TRIGGER IF NOT EXISTS digests_au AFTER UPDATE ON digests BEGIN
337
+ INSERT INTO digests_fts(digests_fts, rowid, content, topic) VALUES('delete', OLD.rowid, OLD.content, COALESCE(OLD.topic, ''));
338
+ INSERT INTO digests_fts(rowid, content, topic) VALUES (NEW.rowid, NEW.content, COALESCE(NEW.topic, ''));
339
+ END;
340
+ `);
341
+
342
+ // Rebuild FTS5 index to sync with content table (necessary for existing databases)
343
+ // This is idempotent and fast for small tables
344
+ this.db.exec(`INSERT INTO digests_fts(digests_fts) VALUES('rebuild');`);
345
+
287
346
  // Contradictions table (detected conflicts)
288
347
  this.db.exec(`
289
348
  CREATE TABLE IF NOT EXISTS contradictions (
@@ -340,6 +399,48 @@ export class EngramDatabase {
340
399
  CREATE INDEX IF NOT EXISTS idx_retrieval_logs_recall ON retrieval_logs(recall_id);
341
400
  CREATE INDEX IF NOT EXISTS idx_retrieval_logs_processed ON retrieval_logs(processed);
342
401
  `);
402
+
403
+ // Consolidation checkpoints for safe backlog processing
404
+ this.db.exec(`
405
+ CREATE TABLE IF NOT EXISTS consolidation_checkpoints (
406
+ id TEXT PRIMARY KEY,
407
+ run_id TEXT NOT NULL UNIQUE,
408
+ phase TEXT NOT NULL CHECK(phase IN ('episodes', 'memories', 'decay', 'cleanup', 'complete')),
409
+ batches_completed INTEGER DEFAULT 0,
410
+ batches_total INTEGER DEFAULT 0,
411
+ memories_processed INTEGER DEFAULT 0,
412
+ episodes_processed INTEGER DEFAULT 0,
413
+ digests_created INTEGER DEFAULT 0,
414
+ contradictions_found INTEGER DEFAULT 0,
415
+ tokens_used INTEGER DEFAULT 0,
416
+ estimated_cost_usd REAL DEFAULT 0,
417
+ started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
418
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
419
+ completed_at DATETIME,
420
+ error TEXT
421
+ );
422
+
423
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_run ON consolidation_checkpoints(run_id);
424
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_phase ON consolidation_checkpoints(phase);
425
+ `);
426
+
427
+ // Consolidation configuration for budget and rate limits
428
+ this.db.exec(`
429
+ CREATE TABLE IF NOT EXISTS consolidation_config (
430
+ key TEXT PRIMARY KEY,
431
+ value TEXT NOT NULL,
432
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
433
+ );
434
+
435
+ -- Default configuration values
436
+ INSERT OR IGNORE INTO consolidation_config (key, value) VALUES
437
+ ('daily_budget_usd', '5.0'),
438
+ ('max_batches_per_run', '5'),
439
+ ('delay_between_calls_ms', '2000'),
440
+ ('recovery_mode_threshold', '100'),
441
+ ('error_rate_threshold', '0.3'),
442
+ ('empty_digest_threshold', '0.2');
443
+ `);
343
444
  }
344
445
 
345
446
  /**
@@ -568,6 +669,23 @@ export class EngramDatabase {
568
669
  }));
569
670
  }
570
671
 
672
+ searchDigestsBM25(query: string, limit: number = 10): Array<Digest & { score: number }> {
673
+ const escapedQuery = this.escapeFTS5Query(query);
674
+ const rows = this.stmt(`
675
+ SELECT d.*, bm25(digests_fts) as score
676
+ FROM digests_fts fts
677
+ JOIN digests d ON fts.rowid = d.rowid
678
+ WHERE digests_fts MATCH ?
679
+ ORDER BY score
680
+ LIMIT ?
681
+ `).all(escapedQuery, limit) as Array<Record<string, unknown>>;
682
+
683
+ return rows.map((row) => ({
684
+ ...this.rowToDigest(row),
685
+ score: row.score as number,
686
+ }));
687
+ }
688
+
571
689
  private escapeFTS5Query(query: string): string {
572
690
  // Simple tokenization - split on whitespace, escape special chars
573
691
  const tokens = query
@@ -1341,6 +1459,195 @@ export class EngramDatabase {
1341
1459
  return result.changes;
1342
1460
  }
1343
1461
 
1462
+ // ============ Consolidation Checkpoints ============
1463
+
1464
+ /**
1465
+ * Create a new consolidation checkpoint
1466
+ */
1467
+ createCheckpoint(runId: string, phase: ConsolidationCheckpoint["phase"], batchesTotal: number): ConsolidationCheckpoint {
1468
+ const id = randomUUID();
1469
+ this.db.prepare(`
1470
+ INSERT INTO consolidation_checkpoints (id, run_id, phase, batches_total)
1471
+ VALUES (?, ?, ?, ?)
1472
+ `).run(id, runId, phase, batchesTotal);
1473
+ return this.getCheckpoint(runId)!;
1474
+ }
1475
+
1476
+ /**
1477
+ * Get checkpoint by run_id
1478
+ */
1479
+ getCheckpoint(runId: string): ConsolidationCheckpoint | null {
1480
+ const row = this.stmt("SELECT * FROM consolidation_checkpoints WHERE run_id = ?").get(runId) as Record<string, unknown> | undefined;
1481
+ return row ? this.rowToCheckpoint(row) : null;
1482
+ }
1483
+
1484
+ /**
1485
+ * Get the latest incomplete checkpoint (for resume)
1486
+ */
1487
+ getIncompleteCheckpoint(): ConsolidationCheckpoint | null {
1488
+ const row = this.stmt(`
1489
+ SELECT * FROM consolidation_checkpoints
1490
+ WHERE completed_at IS NULL AND error IS NULL
1491
+ ORDER BY started_at DESC LIMIT 1
1492
+ `).get() as Record<string, unknown> | undefined;
1493
+ return row ? this.rowToCheckpoint(row) : null;
1494
+ }
1495
+
1496
+ /**
1497
+ * Update checkpoint progress
1498
+ */
1499
+ updateCheckpoint(
1500
+ runId: string,
1501
+ updates: Partial<Pick<ConsolidationCheckpoint,
1502
+ "phase" | "batches_completed" | "batches_total" | "memories_processed" |
1503
+ "episodes_processed" | "digests_created" | "contradictions_found" |
1504
+ "tokens_used" | "estimated_cost_usd" | "error"
1505
+ >>
1506
+ ): ConsolidationCheckpoint | null {
1507
+ const sets: string[] = ["updated_at = CURRENT_TIMESTAMP"];
1508
+ const values: unknown[] = [];
1509
+
1510
+ if (updates.phase !== undefined) {
1511
+ sets.push("phase = ?");
1512
+ values.push(updates.phase);
1513
+ }
1514
+ if (updates.batches_completed !== undefined) {
1515
+ sets.push("batches_completed = ?");
1516
+ values.push(updates.batches_completed);
1517
+ }
1518
+ if (updates.batches_total !== undefined) {
1519
+ sets.push("batches_total = ?");
1520
+ values.push(updates.batches_total);
1521
+ }
1522
+ if (updates.memories_processed !== undefined) {
1523
+ sets.push("memories_processed = ?");
1524
+ values.push(updates.memories_processed);
1525
+ }
1526
+ if (updates.episodes_processed !== undefined) {
1527
+ sets.push("episodes_processed = ?");
1528
+ values.push(updates.episodes_processed);
1529
+ }
1530
+ if (updates.digests_created !== undefined) {
1531
+ sets.push("digests_created = ?");
1532
+ values.push(updates.digests_created);
1533
+ }
1534
+ if (updates.contradictions_found !== undefined) {
1535
+ sets.push("contradictions_found = ?");
1536
+ values.push(updates.contradictions_found);
1537
+ }
1538
+ if (updates.tokens_used !== undefined) {
1539
+ sets.push("tokens_used = ?");
1540
+ values.push(updates.tokens_used);
1541
+ }
1542
+ if (updates.estimated_cost_usd !== undefined) {
1543
+ sets.push("estimated_cost_usd = ?");
1544
+ values.push(updates.estimated_cost_usd);
1545
+ }
1546
+ if (updates.error !== undefined) {
1547
+ sets.push("error = ?");
1548
+ values.push(updates.error);
1549
+ }
1550
+
1551
+ values.push(runId);
1552
+ this.db.prepare(`UPDATE consolidation_checkpoints SET ${sets.join(", ")} WHERE run_id = ?`).run(...values);
1553
+ return this.getCheckpoint(runId);
1554
+ }
1555
+
1556
+ /**
1557
+ * Mark checkpoint as complete
1558
+ */
1559
+ completeCheckpoint(runId: string): void {
1560
+ this.db.prepare(`
1561
+ UPDATE consolidation_checkpoints
1562
+ SET phase = 'complete', completed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
1563
+ WHERE run_id = ?
1564
+ `).run(runId);
1565
+ }
1566
+
1567
+ /**
1568
+ * Get recent checkpoints for reporting
1569
+ */
1570
+ getRecentCheckpoints(limit: number = 10): ConsolidationCheckpoint[] {
1571
+ const rows = this.stmt(`
1572
+ SELECT * FROM consolidation_checkpoints
1573
+ ORDER BY started_at DESC LIMIT ?
1574
+ `).all(limit) as Record<string, unknown>[];
1575
+ return rows.map(r => this.rowToCheckpoint(r));
1576
+ }
1577
+
1578
+ /**
1579
+ * Get total spending in the last 24 hours
1580
+ */
1581
+ getDailySpending(): number {
1582
+ const row = this.stmt(`
1583
+ SELECT COALESCE(SUM(estimated_cost_usd), 0) as total
1584
+ FROM consolidation_checkpoints
1585
+ WHERE started_at >= datetime('now', '-1 day')
1586
+ `).get() as { total: number };
1587
+ return row.total;
1588
+ }
1589
+
1590
+ // ============ Consolidation Config ============
1591
+
1592
+ /**
1593
+ * Get a config value
1594
+ */
1595
+ getConfig(key: string): string | null {
1596
+ const row = this.stmt("SELECT value FROM consolidation_config WHERE key = ?").get(key) as { value: string } | undefined;
1597
+ return row?.value ?? null;
1598
+ }
1599
+
1600
+ /**
1601
+ * Get a config value as number
1602
+ */
1603
+ getConfigNumber(key: string, defaultValue: number): number {
1604
+ const value = this.getConfig(key);
1605
+ return value ? parseFloat(value) : defaultValue;
1606
+ }
1607
+
1608
+ /**
1609
+ * Set a config value
1610
+ */
1611
+ setConfig(key: string, value: string): void {
1612
+ this.db.prepare(`
1613
+ INSERT INTO consolidation_config (key, value, updated_at)
1614
+ VALUES (?, ?, CURRENT_TIMESTAMP)
1615
+ ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP
1616
+ `).run(key, value, value);
1617
+ }
1618
+
1619
+ /**
1620
+ * Get all config values
1621
+ */
1622
+ getAllConfig(): Record<string, string> {
1623
+ const rows = this.stmt("SELECT key, value FROM consolidation_config").all() as Array<{ key: string; value: string }>;
1624
+ const config: Record<string, string> = {};
1625
+ for (const row of rows) {
1626
+ config[row.key] = row.value;
1627
+ }
1628
+ return config;
1629
+ }
1630
+
1631
+ private rowToCheckpoint(row: Record<string, unknown>): ConsolidationCheckpoint {
1632
+ return {
1633
+ id: row.id as string,
1634
+ run_id: row.run_id as string,
1635
+ phase: row.phase as ConsolidationCheckpoint["phase"],
1636
+ batches_completed: row.batches_completed as number,
1637
+ batches_total: row.batches_total as number,
1638
+ memories_processed: row.memories_processed as number,
1639
+ episodes_processed: row.episodes_processed as number,
1640
+ digests_created: row.digests_created as number,
1641
+ contradictions_found: row.contradictions_found as number,
1642
+ tokens_used: row.tokens_used as number,
1643
+ estimated_cost_usd: row.estimated_cost_usd as number,
1644
+ started_at: new Date(row.started_at as string),
1645
+ updated_at: new Date(row.updated_at as string),
1646
+ completed_at: row.completed_at ? new Date(row.completed_at as string) : null,
1647
+ error: row.error as string | null,
1648
+ };
1649
+ }
1650
+
1344
1651
  // ============ Statistics ============
1345
1652
 
1346
1653
  getStats(): {
@@ -0,0 +1,111 @@
1
+ /**
2
+ * HTTP Transport for Engram MCP Server
3
+ * Uses StreamableHTTPServerTransport in stateless mode for Railway deployment
4
+ */
5
+
6
+ import http from "http";
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
+
10
+ interface HttpServerOptions {
11
+ port: number;
12
+ server: Server;
13
+ }
14
+
15
+ /**
16
+ * Start HTTP server with MCP transport
17
+ * Returns a promise that resolves when server is listening
18
+ */
19
+ export async function startHttpServer(options: HttpServerOptions): Promise<http.Server> {
20
+ const { port, server } = options;
21
+
22
+ // Create stateless transport (sessionIdGenerator: undefined)
23
+ // Stateless mode is perfect for Railway - no session management needed
24
+ const transport = new StreamableHTTPServerTransport({
25
+ sessionIdGenerator: undefined,
26
+ });
27
+
28
+ // Connect MCP server to transport
29
+ await server.connect(transport);
30
+
31
+ // Create HTTP server
32
+ const httpServer = http.createServer(async (req, res) => {
33
+ // CORS headers for remote clients (ElevenLabs, etc.)
34
+ res.setHeader("Access-Control-Allow-Origin", "*");
35
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
36
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
37
+
38
+ if (req.method === "OPTIONS") {
39
+ res.writeHead(204);
40
+ res.end();
41
+ return;
42
+ }
43
+
44
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
45
+
46
+ // Health check endpoint (Railway uses this)
47
+ if (url.pathname === "/health" && req.method === "GET") {
48
+ res.writeHead(200, { "Content-Type": "application/json" });
49
+ res.end(JSON.stringify({
50
+ status: "ok",
51
+ transport: "http",
52
+ version: "0.10.0"
53
+ }));
54
+ return;
55
+ }
56
+
57
+ // Root endpoint - service info
58
+ if (url.pathname === "/" && req.method === "GET") {
59
+ res.writeHead(200, { "Content-Type": "application/json" });
60
+ res.end(JSON.stringify({
61
+ name: "engram",
62
+ description: "MCP memory server with hybrid search",
63
+ version: "0.10.0",
64
+ transport: "streamable-http",
65
+ endpoints: {
66
+ mcp: "/mcp",
67
+ health: "/health",
68
+ },
69
+ }));
70
+ return;
71
+ }
72
+
73
+ // MCP endpoint - handles both POST (messages) and GET (SSE stream)
74
+ if (url.pathname === "/mcp") {
75
+ try {
76
+ await transport.handleRequest(req, res);
77
+ } catch (error) {
78
+ console.error("[Engram HTTP] Error handling MCP request:", error);
79
+ if (!res.headersSent) {
80
+ res.writeHead(500, { "Content-Type": "application/json" });
81
+ res.end(JSON.stringify({ error: "Internal server error" }));
82
+ }
83
+ }
84
+ return;
85
+ }
86
+
87
+ // SSE alias (for clients expecting /sse endpoint)
88
+ if (url.pathname === "/sse") {
89
+ // Redirect to /mcp which handles SSE via GET
90
+ res.writeHead(307, { "Location": "/mcp" });
91
+ res.end();
92
+ return;
93
+ }
94
+
95
+ // 404 for unknown paths
96
+ res.writeHead(404, { "Content-Type": "application/json" });
97
+ res.end(JSON.stringify({ error: "Not found" }));
98
+ });
99
+
100
+ // Start listening
101
+ return new Promise((resolve, reject) => {
102
+ httpServer.once("error", reject);
103
+ httpServer.listen(port, () => {
104
+ console.error(`[Engram] MCP HTTP server running on port ${port}`);
105
+ console.error(`[Engram] Endpoints:`);
106
+ console.error(`[Engram] POST http://localhost:${port}/mcp - MCP protocol`);
107
+ console.error(`[Engram] GET http://localhost:${port}/health - Health check`);
108
+ resolve(httpServer);
109
+ });
110
+ });
111
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Transport layer for Engram MCP Server
3
+ * Supports both stdio (local) and HTTP (remote/Railway) transports
4
+ */
5
+
6
+ export type TransportMode = "stdio" | "http";
7
+
8
+ /**
9
+ * Detect transport mode from environment variables
10
+ * Default: stdio (preserves existing behavior)
11
+ */
12
+ export function getTransportMode(): TransportMode {
13
+ const mode = process.env.ENGRAM_TRANSPORT?.toLowerCase();
14
+ if (mode === "http" || mode === "sse") return "http";
15
+ return "stdio";
16
+ }
17
+
18
+ /**
19
+ * Get HTTP port from environment
20
+ * Railway provides PORT, we also support ENGRAM_MCP_PORT
21
+ */
22
+ export function getHttpPort(): number {
23
+ return parseInt(process.env.PORT || process.env.ENGRAM_MCP_PORT || "3000", 10);
24
+ }
@@ -253,16 +253,34 @@ const TOOLS: Anthropic.Tool[] = [
253
253
  },
254
254
  ];
255
255
 
256
- const SYSTEM_PROMPT = `You are a helpful assistant for managing Engram, a personal memory system. You help users:
257
- - View and search their memories
258
- - Manage entities (people, organizations, places)
259
- - Fix incorrect relationships
260
- - Merge duplicate entities
261
- - Delete incorrect data
262
-
263
- Be concise and helpful. When making changes, confirm what you did. If asked to do something destructive, confirm first unless the user is explicit.
264
-
265
- When listing entities or memories, format them clearly. Use the tools available to you.`;
256
+ const SYSTEM_PROMPT = `You are a helpful assistant for managing Engram, a personal memory system. You have extended thinking capabilities - use them to reason carefully about complex requests.
257
+
258
+ ## Your Capabilities
259
+ - Search and retrieve memories using semantic + keyword hybrid search
260
+ - Manage entities (people, organizations, places) - create, rename, merge, delete
261
+ - Manage relationships between entities
262
+ - Create, edit, and delete memories
263
+ - Find and auto-merge duplicate entities
264
+
265
+ ## Critical Behaviors
266
+ 1. **Always search first**: When asked about anything that might be in memory, use search_memories FIRST before answering. Don't assume you know the answer.
267
+ 2. **Multi-step reasoning**: For complex requests, break them into steps. Search, analyze results, then act.
268
+ 3. **Confirm destructive actions**: Unless the user is explicit, ask before deleting or merging data.
269
+ 4. **Be precise**: Use exact entity names when making changes. Check spelling.
270
+ 5. **Context awareness**: Remember what the user discussed earlier in this conversation.
271
+
272
+ ## Response Style
273
+ - Be concise but thorough
274
+ - Format lists and results clearly using markdown
275
+ - When you find relevant memories, quote the key parts
276
+ - If you're uncertain, say so and explain your reasoning
277
+
278
+ ## Tool Usage
279
+ - search_memories: Use liberally - hybrid search is fast and effective
280
+ - list_entities: Good for getting an overview before specific operations
281
+ - get_entity: Get full details including observations and relationships
282
+ - find_duplicates: Run this when asked about data quality or cleanup
283
+ - auto_tidy: Only use when user explicitly wants automatic cleanup`;
266
284
 
267
285
  interface ChatMessage {
268
286
  role: "user" | "assistant";
@@ -271,7 +289,7 @@ interface ChatMessage {
271
289
 
272
290
  // Stream event types for SSE
273
291
  export interface StreamEvent {
274
- type: "text" | "tool_start" | "tool_end" | "error" | "done";
292
+ type: "text" | "thinking" | "tool_start" | "tool_end" | "error" | "done";
275
293
  content?: string;
276
294
  tool?: string;
277
295
  result?: unknown;
@@ -308,7 +326,12 @@ export class ChatHandler {
308
326
  if (!this.client) {
309
327
  console.error("[Engram] ChatHandler: API key configured");
310
328
  }
311
- this.client = new Anthropic({ apiKey });
329
+ this.client = new Anthropic({
330
+ apiKey,
331
+ defaultHeaders: {
332
+ "anthropic-beta": "interleaved-thinking-2025-05-14",
333
+ },
334
+ });
312
335
  } else {
313
336
  this.client = null;
314
337
  }
@@ -392,13 +415,18 @@ export class ChatHandler {
392
415
  while (continueLoop) {
393
416
  const stream = this.client.messages.stream({
394
417
  model: "claude-opus-4-5-20251101",
395
- max_tokens: 1024,
418
+ max_tokens: 16000,
396
419
  system: SYSTEM_PROMPT,
397
420
  tools: TOOLS,
398
421
  messages: this.conversationHistory,
422
+ thinking: {
423
+ type: "enabled",
424
+ budget_tokens: 8000,
425
+ },
399
426
  });
400
427
 
401
428
  let currentToolUse: { id: string; name: string; input: string } | null = null;
429
+ let isThinking = false;
402
430
 
403
431
  for await (const event of stream) {
404
432
  if (event.type === "content_block_start") {
@@ -409,16 +437,23 @@ export class ChatHandler {
409
437
  input: "",
410
438
  };
411
439
  yield { type: "tool_start", tool: event.content_block.name };
440
+ } else if (event.content_block.type === "thinking") {
441
+ isThinking = true;
442
+ yield { type: "thinking", content: "" };
412
443
  }
413
444
  } else if (event.type === "content_block_delta") {
414
445
  if (event.delta.type === "text_delta") {
415
446
  yield { type: "text", content: event.delta.text };
447
+ } else if (event.delta.type === "thinking_delta") {
448
+ // Stream thinking content for transparency
449
+ yield { type: "thinking", content: event.delta.thinking };
416
450
  } else if (event.delta.type === "input_json_delta" && currentToolUse) {
417
451
  currentToolUse.input += event.delta.partial_json;
418
452
  }
419
453
  } else if (event.type === "content_block_stop") {
420
454
  // Don't execute tools here - wait for finalMessage to avoid double execution
421
455
  currentToolUse = null;
456
+ isThinking = false;
422
457
  }
423
458
  }
424
459
 
@@ -494,10 +529,14 @@ export class ChatHandler {
494
529
 
495
530
  let response = await this.client.messages.create({
496
531
  model: "claude-opus-4-5-20251101",
497
- max_tokens: 1024,
532
+ max_tokens: 16000,
498
533
  system: SYSTEM_PROMPT,
499
534
  tools: TOOLS,
500
535
  messages: this.conversationHistory,
536
+ thinking: {
537
+ type: "enabled",
538
+ budget_tokens: 8000,
539
+ },
501
540
  });
502
541
 
503
542
  // Handle tool use loop
@@ -531,10 +570,14 @@ export class ChatHandler {
531
570
  // Continue the conversation
532
571
  response = await this.client.messages.create({
533
572
  model: "claude-opus-4-5-20251101",
534
- max_tokens: 1024,
573
+ max_tokens: 16000,
535
574
  system: SYSTEM_PROMPT,
536
575
  tools: TOOLS,
537
576
  messages: this.conversationHistory,
577
+ thinking: {
578
+ type: "enabled",
579
+ budget_tokens: 8000,
580
+ },
538
581
  });
539
582
  }
540
583