/** * HMEM tool implementations — each tool is a standalone async function * that receives the HmemClient and tool parameters, and returns a * tool-call result string. */ import type { HmemClient } from "./client.ts"; // ── Helpers ────────────────────────────────────────────────────── function json(data: unknown): string { return JSON.stringify(data, null, 2); } function truncate(s: string, max = 300): string { if (s.length <= max) return s; return s.slice(0, max) + "..."; } // ── Write ──────────────────────────────────────────────────────── export async function handleWrite( client: HmemClient, args: Record, ): Promise { const content = args.content as string | undefined; if (!content) return "❌ `content` is required"; const memoryType = (args.memory_type as string) || "observation"; if (memoryType === "insight" || memoryType === "mental_model") { return `❌ Cannot write type "${memoryType}" directly. Insights and mental models are only generated by the reflection engine (hmem_reflect). Use: observation or experience.`; } const result = await client.writeMemory({ content, memory_type: memoryType, mem_action: (args.mem_action as string) || "", mem_context: (args.mem_context as Record) || {}, mem_outcome: (args.mem_outcome as Record) || {}, mem_metadata: (args.mem_metadata as Record) || {}, namespace: args.namespace as string | undefined, }); if (!result.ok) return `❌ ${result.error}`; return json({ memory_id: result.data!.memory_id, content: truncate(result.data!.content, 100), namespace: result.data!.namespace, embedded: result.data!.embedded, auto_reflected: result.data!.auto_reflected, }); } // ── Search ─────────────────────────────────────────────────────── export async function handleSearch( client: HmemClient, args: Record, ): Promise { const query = args.query as string | undefined; if (!query) return "❌ `query` is required"; const result = await client.search({ query, limit: Math.min((args.limit as number) || 10, 50), use_rerank: args.use_rerank !== false, min_score: args.min_score as number | undefined, namespace: args.namespace as string | undefined, }); if (!result.ok) return `❌ ${result.error}`; const items = result.data!.results; if (items.length === 0) return "📭 No relevant memories found."; const lines = items.map((m, i) => { const score = m.score === undefined ? "" : `[${m.score.toFixed(2)}]`; const type = m.memory_type ? `(${m.memory_type})` : ""; const id = m.id ? `#${m.id}` : ""; const src = m.source ? ` 📄${m.source.title || m.source.doc_id}#${m.source.chunk_index ?? 0}` : ""; return `${i + 1}. ${score} ${type} ${id} ${truncate(m.content, 200)}${src}`; }); return `📚 Found ${items.length} memories:\n\n${lines.join("\n")}`; } // ── List ───────────────────────────────────────────────────────── export async function handleList( client: HmemClient, args: Record, ): Promise { const result = await client.listMemories({ namespace: args.namespace as string | undefined, limit: Math.min((args.limit as number) || 20, 100), memory_type: args.memory_type as string | undefined, }); if (!result.ok) return `❌ ${result.error}`; const items = result.data!.results; if (items.length === 0) return "📭 No memories."; const lines = items.map((m) => { const type = m.memory_type ?? "observation"; const id = m.id ?? "?"; const date = m.created_at ? m.created_at.slice(0, 19) : ""; const doc = m.doc_title ? ` [📄${m.doc_title}#${m.chunk_index ?? 0}]` : ""; return ` #${id} [${type}] ${date}${doc} ${truncate(m.content, 120)}`; }); return `📖 ${items.length} memories:\n${lines.join("\n")}`; } // ── Get single ─────────────────────────────────────────────────── export async function handleGet( client: HmemClient, args: Record, ): Promise { const memoryId = args.memory_id as number | undefined; if (memoryId === undefined) return "❌ `memory_id` is required"; const result = await client.getMemory( memoryId, args.namespace as string | undefined, ); if (!result.ok) return `❌ ${result.error}`; return json(result.data); } // ── Delete ─────────────────────────────────────────────────────── export async function handleDelete( client: HmemClient, args: Record, ): Promise { const memoryId = args.memory_id as number | undefined; if (memoryId === undefined) return "❌ `memory_id` is required"; const result = await client.deleteMemory( memoryId, args.namespace as string | undefined, ); if (!result.ok) return `❌ ${result.error}`; return `🗑️ Deleted memory #${memoryId}`; } // ── Stats ──────────────────────────────────────────────────────── export async function handleStats( client: HmemClient, args: Record, ): Promise { const result = await client.stats(args.namespace as string | undefined); if (!result.ok) return `❌ ${result.error}`; const s = result.data!; const byType = Object.entries(s.by_type ?? {}) .map(([k, v]) => ` ${k}: ${v}`) .join("\n"); return [ `📊 HMEM Memory Stats — namespace: \`${s.namespace}\``, ` Total: ${s.total_memories}`, ` Embeddings: ${s.embedding_enabled ? "✅ ON" : "❌ OFF"}`, byType ? `\nBy type:\n${byType}` : "", ] .filter(Boolean) .join("\n"); } // ── Reflect ────────────────────────────────────────────────────── export async function handleReflect( client: HmemClient, args: Record, ): Promise { const result = await client.triggerReflect( args.namespace as string | undefined, ); if (!result.ok) return `❌ ${result.error}`; const r = result.data!; return json({ status: r.status, stage: r.stage ?? "unknown", reflect_count: r.reflect_count, counts: r.counts ?? {}, }); } // ── Mental Models ──────────────────────────────────────────────── export async function handleMentalModels( client: HmemClient, args: Record, ): Promise { const result = await client.listMentalModels({ namespace: args.namespace as string | undefined, limit: Math.min((args.limit as number) || 20, 100), }); if (!result.ok) return `❌ ${result.error}`; const items = result.data!.results; if (items.length === 0) return "🧠 No mental models yet. Write more experiences and trigger reflection!"; const lines = items.map((m, i) => { const type = m.memory_type === "mental_model" ? "🧠 Mental Model" : "💡 Insight"; const date = m.created_at ? m.created_at.slice(0, 19) : ""; const supporting = m.supporting_experiences?.length ?? 0; return `${i + 1}. ${type} #${m.id} ${date}\n ${truncate(m.content, 200)}${supporting ? `\n ↳ ${supporting} supporting experiences` : ""}`; }); return `🧠 Mental Models & Insights (${items.length}):\n\n${lines.join("\n\n")}`; } // ── Graph ──────────────────────────────────────────────────────── export async function handleGraph( client: HmemClient, args: Record, ): Promise { const result = await client.graph(args.namespace as string | undefined); if (!result.ok) return `❌ ${result.error}`; const g = result.data!; return json({ nodes: g.nodes.length, edges: g.edges.length, sample_edges: g.edges.slice(0, 10).map((e) => ({ source: e.source, target: e.target, relation: e.relation, })), }); } // ── Namespaces ─────────────────────────────────────────────────── export async function handleNamespaces(client: HmemClient): Promise { const result = await client.listNamespaces(); if (!result.ok) return `❌ ${result.error}`; const nss = result.data!.namespaces; if (nss.length === 0) return "📭 No namespaces found."; return json(nss); } // ── Knowledge Base: documents ──────────────────────────────────── export async function handleDocImport( client: HmemClient, args: Record, ): Promise { const content = args.content as string | undefined; if (!content || !content.trim()) return "❌ `content` is required"; const result = await client.importDocument({ content, namespace: args.namespace as string | undefined, doc_id: args.doc_id as string | undefined, title: (args.title as string) || "", uri: (args.uri as string) || "", category: (args.category as string) || "", tags: (args.tags as string[]) || [], chunk_size: args.chunk_size as number | undefined, overlap: args.overlap as number | undefined, }); if (!result.ok) return `❌ ${result.error}`; const d = result.data!; return ( `📥 Imported \`${d.title || d.doc_id}\` into knowledge base \`${d.namespace}\`` + `\n doc_id: ${d.doc_id}` + `\n chunks: ${d.chunks} (embedded: ${d.embedded}/${d.chunks})` + (d.category ? `\n category: ${d.category}` : "") ); } export async function handleDocList( client: HmemClient, args: Record, ): Promise { const result = await client.listDocuments(args.namespace as string | undefined); if (!result.ok) return `❌ ${result.error}`; const docs = result.data!.documents; if (docs.length === 0) return `📭 No documents in \`${result.data!.namespace}\`.`; const lines = docs.map((doc, i) => { const cats = doc.category ? ` [${doc.category}]` : ""; return `${i + 1}. ${doc.doc_id} — ${doc.doc_title || "(untitled)"} (${doc.chunk_count} chunks)${cats}`; }); return `📄 ${docs.length} documents in \`${result.data!.namespace}\`:\n${lines.join("\n")}`; } export async function handleDocGet( client: HmemClient, args: Record, ): Promise { const docId = args.doc_id as string | undefined; if (!docId) return "❌ `doc_id` is required"; const result = await client.getDocument( docId, args.namespace as string | undefined, ); if (!result.ok) return `❌ ${result.error}`; const d = result.data!; const chunkLines = d.chunks.map( (c) => `[#${c.chunk_index}] ${truncate(c.content, 200)} (mem #${c.memory_id})`, ); return [ `📄 ${d.doc_title || d.doc_id}`, ` doc_id: ${d.doc_id} namespace: ${d.namespace} chunks: ${d.chunks.length}`, d.doc_uri ? ` uri: ${d.doc_uri}` : "", d.category ? ` category: ${d.category}` : "", "", chunkLines.join("\n"), ] .filter(Boolean) .join("\n"); } export async function handleDocDelete( client: HmemClient, args: Record, ): Promise { const docId = args.doc_id as string | undefined; if (!docId) return "❌ `doc_id` is required"; const result = await client.deleteDocument( docId, args.namespace as string | undefined, ); if (!result.ok) return `❌ ${result.error}`; const d = result.data!; return `🗑️ Deleted document ${d.doc_id} (${d.chunks_removed} chunks removed)`; } // ── Knowledge Base: single entries ─────────────────────────────── export async function handleKbPut( client: HmemClient, args: Record, ): Promise { const content = args.content as string | undefined; if (!content || !content.trim()) return "❌ `content` is required"; const result = await client.createKnowledge({ content, namespace: args.namespace as string | undefined, doc_id: args.doc_id as string | undefined, title: (args.title as string) || "", uri: (args.uri as string) || "", category: (args.category as string) || "", tags: (args.tags as string[]) || [], mem_metadata: (args.mem_metadata as Record) || {}, }); if (!result.ok) return `❌ ${result.error}`; const d = result.data!; return `📌 Knowledge entry #${d.memory_id} stored in \`${d.namespace}\``; } export async function handleKbQuery( client: HmemClient, args: Record, ): Promise { const result = await client.listKnowledge({ namespace: args.namespace as string | undefined, limit: Math.min((args.limit as number) || 50, 200), offset: args.offset as number | undefined, category: args.category as string | undefined, doc_id: args.doc_id as string | undefined, tags: args.tags as string | undefined, }); if (!result.ok) return `❌ ${result.error}`; const rows = result.data!.results; if (rows.length === 0) return "📭 No knowledge entries."; const lines = rows.map((m, i) => { const doc = m.doc_title ? ` 📄${m.doc_title}` : ""; const cat = m.doc_category ? ` [${m.doc_category}]` : ""; return `${i + 1}. #${m.id}${cat}${doc} ${truncate(m.content, 200)}`; }); return `📌 ${rows.length} knowledge entries in \`${result.data!.namespace}\`:\n${lines.join("\n")}`; } export async function handleKbCategories( client: HmemClient, args: Record, ): Promise { const result = await client.listKnowledgeCategories( args.namespace as string | undefined, ); if (!result.ok) return `❌ ${result.error}`; const r = result.data!; if (r.categories.length === 0) { return `📂 No categories in \`${r.namespace}\` (${r.count} entries total).`; } const lines = r.categories.map( (c) => `- ${c.category}: ${c.entries} entries / ${c.documents} docs` + (c.tags.length ? ` tags: ${c.tags.join(", ")}` : ""), ); return `📂 Categories in \`${r.namespace}\` (${r.count} entries):\n${lines.join("\n")}`; } // ── Knowledge Base: library-level ──────────────────────────────── export async function handleKbCreate( client: HmemClient, args: Record, ): Promise { const namespace = args.namespace as string | undefined; if (!namespace) return "❌ `namespace` is required"; const result = await client.createKnowledgeBase({ namespace, title: (args.title as string) || "", description: (args.description as string) || "", }); if (!result.ok) return `❌ ${result.error}`; const d = result.data!; return `📚 Knowledge base \`${d.namespace}\` ready (${d.db_path})`; } export async function handleKbList( client: HmemClient, ): Promise { const result = await client.listKnowledgeBases(); if (!result.ok) return `❌ ${result.error}`; const kbs = result.data!.knowledge_bases; if (kbs.length === 0) return "📭 No knowledge bases found."; const lines = kbs.map((kb, i) => { const cats = kb.category_list?.length ? ` categories: ${kb.category_list.join(", ")}` : ""; return `${i + 1}. ${kb.namespace}: ${kb.entries} entries / ${kb.documents} docs${cats}`; }); return `📚 ${kbs.length} knowledge bases:\n${lines.join("\n")}`; } export async function handleKbDelete( client: HmemClient, args: Record, ): Promise { const namespace = args.namespace as string | undefined; if (!namespace) return "❌ `namespace` is required"; const result = await client.deleteKnowledgeBase( namespace, args.hard === true || args.hard === "true", ); if (!result.ok) return `❌ ${result.error}`; const d = result.data!; return `🗑️ Knowledge base \`${d.namespace}\` deleted (hard=${d.hard}, db_removed=${d.db_removed})`; } // ── Prefetch: search and format for context injection ──────────── export async function prefetchMemories( client: HmemClient, query: string, limit = 5, minScore = 0.1, ): Promise { if (!query.trim()) return ""; const result = await client.search({ query, limit, use_rerank: true, min_score: minScore, }); if (!result.ok || !result.data?.results.length) return ""; const items = result.data.results; const lines = items.map((m) => { const score = m.score === undefined ? "" : `[${m.score.toFixed(2)}]`; const type = m.memory_type ?? "observation"; return `- ${score} (${type}) ${m.content}`; }); return `## HMEM Memory Context\n\nRelevant past experiences:\n${lines.join("\n")}`; }