@199-bio/engram 0.8.0 → 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.
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Chat Handler for Engram Web Interface
3
- * Uses Claude Haiku 4.5 with tools for entity/memory management
3
+ * Uses Claude Opus 4.5 with tools for entity/memory management
4
4
  */
5
5
 
6
6
  import Anthropic from "@anthropic-ai/sdk";
@@ -9,22 +9,22 @@ import { KnowledgeGraph } from "../graph/knowledge-graph.js";
9
9
  import { HybridSearch } from "../retrieval/hybrid.js";
10
10
  import { getAnthropicApiKey } from "../settings.js";
11
11
 
12
- // Tool definitions for Claude
12
+ // Tool definitions for Claude - optimized for LLM consumption
13
13
  const TOOLS: Anthropic.Tool[] = [
14
14
  {
15
15
  name: "list_entities",
16
- description: "List all entities in the knowledge graph. Use this to see what people, organizations, and places are stored.",
16
+ description: "Returns array of {name, type, id}. Filters: type (person|organization|place), limit. Default limit=50.",
17
17
  input_schema: {
18
18
  type: "object" as const,
19
19
  properties: {
20
20
  type: {
21
21
  type: "string",
22
22
  enum: ["person", "organization", "place"],
23
- description: "Filter by entity type (optional)",
23
+ description: "Filter: person, organization, or place",
24
24
  },
25
25
  limit: {
26
- type: "number",
27
- description: "Maximum number of entities to return (default: 50)",
26
+ type: "integer",
27
+ description: "Max results (default: 50)",
28
28
  },
29
29
  },
30
30
  required: [],
@@ -32,13 +32,13 @@ const TOOLS: Anthropic.Tool[] = [
32
32
  },
33
33
  {
34
34
  name: "get_entity",
35
- description: "Get detailed information about an entity including its observations and relationships.",
35
+ description: "Returns {name, type, observations[], relationships_from[], relationships_to[]} or {error}.",
36
36
  input_schema: {
37
37
  type: "object" as const,
38
38
  properties: {
39
39
  name: {
40
40
  type: "string",
41
- description: "The entity name to look up",
41
+ description: "Exact entity name (case-sensitive)",
42
42
  },
43
43
  },
44
44
  required: ["name"],
@@ -46,13 +46,13 @@ const TOOLS: Anthropic.Tool[] = [
46
46
  },
47
47
  {
48
48
  name: "delete_entity",
49
- description: "Delete an entity and all its observations and relationships. Use this to remove incorrect or duplicate entities.",
49
+ description: "Permanently removes entity + all observations + all relationships. Returns {success, deleted} or {error}.",
50
50
  input_schema: {
51
51
  type: "object" as const,
52
52
  properties: {
53
53
  name: {
54
54
  type: "string",
55
- description: "The entity name to delete",
55
+ description: "Exact entity name to delete",
56
56
  },
57
57
  },
58
58
  required: ["name"],
@@ -60,17 +60,17 @@ const TOOLS: Anthropic.Tool[] = [
60
60
  },
61
61
  {
62
62
  name: "merge_entities",
63
- description: "Merge one entity into another. All observations and relationships from the source will be moved to the target, then the source is deleted. Use this to fix duplicates.",
63
+ description: "Moves all data from 'merge' into 'keep', then deletes 'merge'. Use for deduplication. Returns {success, kept, merged, observations_moved, relations_moved} or {error}.",
64
64
  input_schema: {
65
65
  type: "object" as const,
66
66
  properties: {
67
67
  keep: {
68
68
  type: "string",
69
- description: "The entity name to keep (target)",
69
+ description: "Entity name to preserve (target)",
70
70
  },
71
71
  merge: {
72
72
  type: "string",
73
- description: "The entity name to merge and delete (source)",
73
+ description: "Entity name to merge then delete (source)",
74
74
  },
75
75
  },
76
76
  required: ["keep", "merge"],
@@ -78,17 +78,17 @@ const TOOLS: Anthropic.Tool[] = [
78
78
  },
79
79
  {
80
80
  name: "rename_entity",
81
- description: "Rename an entity to a new name.",
81
+ description: "Changes entity name. Returns {success, old_name, new_name} or {error}.",
82
82
  input_schema: {
83
83
  type: "object" as const,
84
84
  properties: {
85
85
  old_name: {
86
86
  type: "string",
87
- description: "Current entity name",
87
+ description: "Current exact name",
88
88
  },
89
89
  new_name: {
90
90
  type: "string",
91
- description: "New entity name",
91
+ description: "New name",
92
92
  },
93
93
  },
94
94
  required: ["old_name", "new_name"],
@@ -96,7 +96,7 @@ const TOOLS: Anthropic.Tool[] = [
96
96
  },
97
97
  {
98
98
  name: "delete_relationship",
99
- description: "Delete a relationship between two entities.",
99
+ description: "Removes specific relationship. All 3 params must match exactly. Returns {success, deleted} or {error}.",
100
100
  input_schema: {
101
101
  type: "object" as const,
102
102
  properties: {
@@ -110,7 +110,7 @@ const TOOLS: Anthropic.Tool[] = [
110
110
  },
111
111
  type: {
112
112
  type: "string",
113
- description: "Relationship type (e.g., 'works_at', 'knows')",
113
+ description: "Relationship type (e.g., works_at, knows, lives_in)",
114
114
  },
115
115
  },
116
116
  required: ["from", "to", "type"],
@@ -118,17 +118,17 @@ const TOOLS: Anthropic.Tool[] = [
118
118
  },
119
119
  {
120
120
  name: "search_memories",
121
- description: "Search through stored memories.",
121
+ description: "Hybrid BM25+semantic search. Returns {results[{id, content, timestamp, score}], count}.",
122
122
  input_schema: {
123
123
  type: "object" as const,
124
124
  properties: {
125
125
  query: {
126
126
  type: "string",
127
- description: "Search query",
127
+ description: "Search query (keywords or natural language)",
128
128
  },
129
129
  limit: {
130
- type: "number",
131
- description: "Maximum results (default: 10)",
130
+ type: "integer",
131
+ description: "Max results (default: 10)",
132
132
  },
133
133
  },
134
134
  required: ["query"],
@@ -136,13 +136,13 @@ const TOOLS: Anthropic.Tool[] = [
136
136
  },
137
137
  {
138
138
  name: "delete_memory",
139
- description: "Delete a memory by its ID (soft-delete, can be recovered).",
139
+ description: "Soft-delete (recoverable). Returns {success, disabled_id} or {error}.",
140
140
  input_schema: {
141
141
  type: "object" as const,
142
142
  properties: {
143
143
  id: {
144
144
  type: "string",
145
- description: "The memory ID to delete",
145
+ description: "Memory UUID",
146
146
  },
147
147
  },
148
148
  required: ["id"],
@@ -150,13 +150,13 @@ const TOOLS: Anthropic.Tool[] = [
150
150
  },
151
151
  {
152
152
  name: "edit_memory",
153
- description: "Edit an existing memory's content or importance.",
153
+ description: "Updates content and/or importance. Returns {success, memory_id, updated_fields[]} or {error}.",
154
154
  input_schema: {
155
155
  type: "object" as const,
156
156
  properties: {
157
157
  id: {
158
158
  type: "string",
159
- description: "The memory ID to edit",
159
+ description: "Memory UUID",
160
160
  },
161
161
  content: {
162
162
  type: "string",
@@ -164,7 +164,9 @@ const TOOLS: Anthropic.Tool[] = [
164
164
  },
165
165
  importance: {
166
166
  type: "number",
167
- description: "New importance (0-1): 0.9=core identity, 0.8=major, 0.5=normal, 0.3=minor",
167
+ minimum: 0,
168
+ maximum: 1,
169
+ description: "0-1: 0.9=identity, 0.8=major, 0.5=normal, 0.3=minor",
168
170
  },
169
171
  },
170
172
  required: ["id"],
@@ -172,17 +174,19 @@ const TOOLS: Anthropic.Tool[] = [
172
174
  },
173
175
  {
174
176
  name: "create_memory",
175
- description: "Create a new memory. Use for storing user information, preferences, or facts.",
177
+ description: "Stores new memory. Returns {success, memory_id, content}.",
176
178
  input_schema: {
177
179
  type: "object" as const,
178
180
  properties: {
179
181
  content: {
180
182
  type: "string",
181
- description: "The information to store",
183
+ description: "Information to store",
182
184
  },
183
185
  importance: {
184
186
  type: "number",
185
- description: "0-1 score: 0.9=core identity, 0.8=major, 0.5=normal (default), 0.3=minor",
187
+ minimum: 0,
188
+ maximum: 1,
189
+ description: "0-1: 0.9=identity, 0.8=major, 0.5=normal (default), 0.3=minor",
186
190
  },
187
191
  },
188
192
  required: ["content"],
@@ -190,13 +194,13 @@ const TOOLS: Anthropic.Tool[] = [
190
194
  },
191
195
  {
192
196
  name: "create_entity",
193
- description: "Create a new entity (person, organization, or place).",
197
+ description: "Creates new entity. Returns {success, entity_id, name, type} or {error} if exists.",
194
198
  input_schema: {
195
199
  type: "object" as const,
196
200
  properties: {
197
201
  name: {
198
202
  type: "string",
199
- description: "The entity name",
203
+ description: "Entity name",
200
204
  },
201
205
  type: {
202
206
  type: "string",
@@ -209,7 +213,7 @@ const TOOLS: Anthropic.Tool[] = [
209
213
  },
210
214
  {
211
215
  name: "create_relationship",
212
- description: "Create a relationship between two entities.",
216
+ description: "Links two entities. Auto-creates entities as 'person' if missing. Returns {success, relationship}.",
213
217
  input_schema: {
214
218
  type: "object" as const,
215
219
  properties: {
@@ -223,7 +227,7 @@ const TOOLS: Anthropic.Tool[] = [
223
227
  },
224
228
  type: {
225
229
  type: "string",
226
- description: "Relationship type (e.g., 'works_at', 'lives_in', 'knows', 'sibling_of')",
230
+ description: "Relationship type (e.g., works_at, lives_in, knows, sibling_of, parent_of)",
227
231
  },
228
232
  },
229
233
  required: ["from", "to", "type"],
@@ -231,7 +235,7 @@ const TOOLS: Anthropic.Tool[] = [
231
235
  },
232
236
  {
233
237
  name: "find_duplicates",
234
- description: "Find potential duplicate entities that could be merged.",
238
+ description: "Detects similar entity names. Returns {groups[{keep, duplicates[]}], total_duplicates}.",
235
239
  input_schema: {
236
240
  type: "object" as const,
237
241
  properties: {},
@@ -240,7 +244,7 @@ const TOOLS: Anthropic.Tool[] = [
240
244
  },
241
245
  {
242
246
  name: "auto_tidy",
243
- description: "Automatically merge all detected duplicate entities.",
247
+ description: "Auto-merges all detected duplicates. Returns {entities_merged, observations_moved, relations_moved}.",
244
248
  input_schema: {
245
249
  type: "object" as const,
246
250
  properties: {},
@@ -249,16 +253,34 @@ const TOOLS: Anthropic.Tool[] = [
249
253
  },
250
254
  ];
251
255
 
252
- const SYSTEM_PROMPT = `You are a helpful assistant for managing Engram, a personal memory system. You help users:
253
- - View and search their memories
254
- - Manage entities (people, organizations, places)
255
- - Fix incorrect relationships
256
- - Merge duplicate entities
257
- - Delete incorrect data
258
-
259
- Be concise and helpful. When making changes, confirm what you did. If asked to do something destructive, confirm first unless the user is explicit.
260
-
261
- 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`;
262
284
 
263
285
  interface ChatMessage {
264
286
  role: "user" | "assistant";
@@ -267,7 +289,7 @@ interface ChatMessage {
267
289
 
268
290
  // Stream event types for SSE
269
291
  export interface StreamEvent {
270
- type: "text" | "tool_start" | "tool_end" | "error" | "done";
292
+ type: "text" | "thinking" | "tool_start" | "tool_end" | "error" | "done";
271
293
  content?: string;
272
294
  tool?: string;
273
295
  result?: unknown;
@@ -301,13 +323,23 @@ export class ChatHandler {
301
323
  refreshClient(): void {
302
324
  const apiKey = getAnthropicApiKey();
303
325
  if (apiKey) {
304
- this.client = new Anthropic({ apiKey });
326
+ if (!this.client) {
327
+ console.error("[Engram] ChatHandler: API key configured");
328
+ }
329
+ this.client = new Anthropic({
330
+ apiKey,
331
+ defaultHeaders: {
332
+ "anthropic-beta": "interleaved-thinking-2025-05-14",
333
+ },
334
+ });
305
335
  } else {
306
336
  this.client = null;
307
337
  }
308
338
  }
309
339
 
310
340
  isConfigured(): boolean {
341
+ // Re-check API key in case it was added after startup
342
+ this.refreshClient();
311
343
  return this.client !== null;
312
344
  }
313
345
 
@@ -341,6 +373,7 @@ export class ChatHandler {
341
373
 
342
374
  // Queue-aware chat method
343
375
  async chat(userMessage: string): Promise<string> {
376
+ this.refreshClient();
344
377
  if (!this.client) {
345
378
  return "Chat is not configured. Set ANTHROPIC_API_KEY environment variable.";
346
379
  }
@@ -357,6 +390,7 @@ export class ChatHandler {
357
390
 
358
391
  // Streaming chat with callbacks for real-time updates
359
392
  async *chatStream(userMessage: string): AsyncGenerator<StreamEvent> {
393
+ this.refreshClient();
360
394
  if (!this.client) {
361
395
  yield { type: "error", content: "Chat is not configured. Set ANTHROPIC_API_KEY environment variable." };
362
396
  return;
@@ -377,18 +411,22 @@ export class ChatHandler {
377
411
  }
378
412
 
379
413
  let continueLoop = true;
380
- let fullResponse = "";
381
414
 
382
415
  while (continueLoop) {
383
416
  const stream = this.client.messages.stream({
384
- model: "claude-haiku-4-5-20241022",
385
- max_tokens: 1024,
417
+ model: "claude-opus-4-5-20251101",
418
+ max_tokens: 16000,
386
419
  system: SYSTEM_PROMPT,
387
420
  tools: TOOLS,
388
421
  messages: this.conversationHistory,
422
+ thinking: {
423
+ type: "enabled",
424
+ budget_tokens: 8000,
425
+ },
389
426
  });
390
427
 
391
428
  let currentToolUse: { id: string; name: string; input: string } | null = null;
429
+ let isThinking = false;
392
430
 
393
431
  for await (const event of stream) {
394
432
  if (event.type === "content_block_start") {
@@ -399,28 +437,23 @@ export class ChatHandler {
399
437
  input: "",
400
438
  };
401
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: "" };
402
443
  }
403
444
  } else if (event.type === "content_block_delta") {
404
445
  if (event.delta.type === "text_delta") {
405
- fullResponse += event.delta.text;
406
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 };
407
450
  } else if (event.delta.type === "input_json_delta" && currentToolUse) {
408
451
  currentToolUse.input += event.delta.partial_json;
409
452
  }
410
453
  } else if (event.type === "content_block_stop") {
411
- if (currentToolUse) {
412
- // Execute the tool
413
- let toolInput: Record<string, unknown> = {};
414
- try {
415
- toolInput = JSON.parse(currentToolUse.input || "{}");
416
- } catch {
417
- toolInput = {};
418
- }
419
-
420
- const result = await this.executeTool(currentToolUse.name, toolInput);
421
- yield { type: "tool_end", tool: currentToolUse.name, result };
422
- currentToolUse = null;
423
- }
454
+ // Don't execute tools here - wait for finalMessage to avoid double execution
455
+ currentToolUse = null;
456
+ isThinking = false;
424
457
  }
425
458
  }
426
459
 
@@ -428,7 +461,7 @@ export class ChatHandler {
428
461
  const finalMessage = await stream.finalMessage();
429
462
 
430
463
  if (finalMessage.stop_reason === "tool_use") {
431
- // Process tool results and continue
464
+ // Process tool results (execute only once, here)
432
465
  const toolUseBlocks = finalMessage.content.filter(
433
466
  (block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
434
467
  );
@@ -436,10 +469,13 @@ export class ChatHandler {
436
469
  const toolResults: Anthropic.ToolResultBlockParam[] = [];
437
470
  for (const toolUse of toolUseBlocks) {
438
471
  const result = await this.executeTool(toolUse.name, toolUse.input as Record<string, unknown>);
472
+ const isError = typeof result === "object" && result !== null && "error" in result;
473
+ yield { type: "tool_end", tool: toolUse.name, result };
439
474
  toolResults.push({
440
475
  type: "tool_result",
441
476
  tool_use_id: toolUse.id,
442
477
  content: JSON.stringify(result),
478
+ is_error: isError,
443
479
  });
444
480
  }
445
481
 
@@ -492,11 +528,15 @@ export class ChatHandler {
492
528
  }
493
529
 
494
530
  let response = await this.client.messages.create({
495
- model: "claude-haiku-4-5-20241022",
496
- max_tokens: 1024,
531
+ model: "claude-opus-4-5-20251101",
532
+ max_tokens: 16000,
497
533
  system: SYSTEM_PROMPT,
498
534
  tools: TOOLS,
499
535
  messages: this.conversationHistory,
536
+ thinking: {
537
+ type: "enabled",
538
+ budget_tokens: 8000,
539
+ },
500
540
  });
501
541
 
502
542
  // Handle tool use loop
@@ -508,10 +548,12 @@ export class ChatHandler {
508
548
  const toolResults: Anthropic.ToolResultBlockParam[] = [];
509
549
  for (const toolUse of toolUseBlocks) {
510
550
  const result = await this.executeTool(toolUse.name, toolUse.input as Record<string, unknown>);
551
+ const isError = typeof result === "object" && result !== null && "error" in result;
511
552
  toolResults.push({
512
553
  type: "tool_result",
513
554
  tool_use_id: toolUse.id,
514
555
  content: JSON.stringify(result),
556
+ is_error: isError,
515
557
  });
516
558
  }
517
559
 
@@ -527,11 +569,15 @@ export class ChatHandler {
527
569
 
528
570
  // Continue the conversation
529
571
  response = await this.client.messages.create({
530
- model: "claude-haiku-4-5-20241022",
531
- max_tokens: 1024,
572
+ model: "claude-opus-4-5-20251101",
573
+ max_tokens: 16000,
532
574
  system: SYSTEM_PROMPT,
533
575
  tools: TOOLS,
534
576
  messages: this.conversationHistory,
577
+ thinking: {
578
+ type: "enabled",
579
+ budget_tokens: 8000,
580
+ },
535
581
  });
536
582
  }
537
583