/** * Engram Toolkit * * Registers four Engram tools with a {@link ToolRegistry} so any agent can * commit facts, query shared memory, audit conflict resolutions, and override * auto-resolutions. * * Run: * npx tsx examples/integrations/with-engram/research-team.ts * * Prerequisites: * - Engram server running at http://localhost:7474 (or custom baseUrl) * - ENGRAM_INVITE_KEY env var (or passed via constructor) */ import { z } from 'zod' import { defineTool, ToolRegistry } from '../../../src/index.js' // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- export interface EngramToolkitOptions { /** Engram server URL. Defaults to `http://localhost:7474`. */ baseUrl?: string /** Workspace invite key. Falls back to `ENGRAM_INVITE_KEY` env var. */ inviteKey?: string } // --------------------------------------------------------------------------- // EngramToolkit // --------------------------------------------------------------------------- export class EngramToolkit { private readonly baseUrl: string private readonly inviteKey: string constructor(options: EngramToolkitOptions = {}) { this.baseUrl = (options.baseUrl ?? 'http://localhost:7474').replace(/\/+$/, '') this.inviteKey = options.inviteKey ?? process.env.ENGRAM_INVITE_KEY ?? '' } /** * Register all four Engram tools with the given registry. */ registerAll(registry: ToolRegistry): void { for (const tool of this.getTools()) { registry.register(tool) } } /** * Returns all four Engram tool definitions as an array. * Use this with `AgentConfig.customTools` so the orchestrator's per-agent * registry picks them up automatically (instead of a shared outer registry * that `runTeam` / `buildPool` never sees). */ getTools() { return [this.commitTool(), this.queryTool(), this.conflictsTool(), this.resolveTool()] } // --------------------------------------------------------------------------- // Tool definitions // --------------------------------------------------------------------------- private commitTool() { return defineTool({ name: 'engram_commit', description: 'Commit a verified fact to Engram shared team memory. ' + 'Use this to record discoveries, decisions, or corrections that other agents should see.', inputSchema: z.object({ content: z.string().describe('The fact to commit'), scope: z.string().describe('Context scope (e.g. "research", "architecture")'), confidence: z.number().min(0).max(1).describe('Confidence level 0-1'), operation: z .enum(['add', 'update', 'delete', 'none']) .optional() .describe('Memory operation. Use "update" when correcting a prior fact. Default: add.'), fact_type: z .enum(['observation', 'decision', 'constraint', 'warning', 'inference']) .optional() .describe('Category of the fact'), agent_id: z.string().optional().describe('Identifier of the committing agent'), ttl_days: z.number().optional().describe('Auto-expire after N days'), }), execute: async (input) => { const res = await fetch(`${this.baseUrl}/api/commit`, { method: 'POST', headers: this.headers(), body: JSON.stringify(input), }) const data = await res.text() return { data, isError: !res.ok } }, }) } private queryTool() { return defineTool({ name: 'engram_query', description: 'Query Engram shared memory for facts about a topic. ' + 'Call this before starting any task to see what the team already knows.', inputSchema: z.object({ topic: z.string().describe('What to search for'), scope: z.string().optional().describe('Filter by scope'), limit: z.number().optional().describe('Max results (default 10)'), fact_type: z .enum(['observation', 'decision', 'constraint', 'warning', 'inference']) .optional() .describe('Filter by fact type'), }), execute: async (input) => { const res = await fetch(`${this.baseUrl}/api/query`, { method: 'POST', headers: this.headers(), body: JSON.stringify(input), }) const data = await res.text() return { data, isError: !res.ok } }, }) } private conflictsTool() { return defineTool({ name: 'engram_conflicts', description: 'List conflicts between facts in Engram shared memory. ' + 'Conflicts are auto-resolved by Claude (with ANTHROPIC_API_KEY) or heuristic — ' + 'this tool is for auditing resolutions, not triggering them.', inputSchema: z.object({ scope: z.string().optional().describe('Filter by scope'), status: z .enum(['open', 'resolved', 'dismissed']) .optional() .describe('Filter by status (default: open)'), }), execute: async (input) => { const params = new URLSearchParams() if (input.scope) params.set('scope', input.scope) if (input.status) params.set('status', input.status) const qs = params.toString() const url = `${this.baseUrl}/api/conflicts${qs ? `?${qs}` : ''}` const res = await fetch(url, { headers: this.headers() }) const data = await res.text() return { data, isError: !res.ok } }, }) } private resolveTool() { return defineTool({ name: 'engram_resolve', description: 'Override an auto-resolution for a conflict between facts. ' + 'Use this when the automatic resolution was incorrect and you need to pick a different winner or merge.', inputSchema: z.object({ conflict_id: z.string().describe('ID of the conflict to resolve'), resolution_type: z .enum(['winner', 'merge', 'dismissed']) .describe('How to resolve: pick a winner, merge both, or dismiss'), resolution: z.string().describe('Explanation of the resolution'), winning_claim_id: z .string() .optional() .describe('fact_id of the correct fact (required for winner type)'), }), execute: async (input) => { const res = await fetch(`${this.baseUrl}/api/resolve`, { method: 'POST', headers: this.headers(), body: JSON.stringify(input), }) const data = await res.text() return { data, isError: !res.ok } }, }) } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- private headers(): Record { return { Authorization: `Bearer ${this.inviteKey}`, 'Content-Type': 'application/json', } } }