Multi-hop Agent
Some operations can’t be completed in a single step. “Update my last payment to Jayden to $200” requires searching for records, identifying the right one, then updating it. Recall Structured handles this with a multi-hop agent.
Why Multi-hop?
Section titled “Why Multi-hop?”Single-hop Limitations
Section titled “Single-hop Limitations”A single LLM call can detect intent and extract data, but it can’t see your actual data:
// User: "Update my last payment to Jayden to $200"
// Single-hop can detect:{ intent: 'update', schema: 'payments', matchCriteria: { field: 'recipient', value: 'Jayden' }, updateData: { amount: 200 },}
// But what if there are multiple payments to Jayden?// The LLM is guessing which one to update!Multi-hop Solution
Section titled “Multi-hop Solution”With multi-hop, the agent can query your data before acting:
Step 1: Agent calls searchRecords({ schema: 'payments', field: 'recipient', value: 'Jayden' }) → Returns: [ { id: 'abc', amount: 150, date: 'Dec 5' }, { id: 'def', amount: 100, date: 'Nov 20' } ]
Step 2: Agent identifies the most recent one (id: 'abc')
Step 3: Agent calls updateRecord({ schema: 'payments', id: 'abc', data: { amount: 200 } }) → Returns: { success: true }
Step 4: Agent responds: "Updated your payment to Jayden from $150 to $200"The agent can see the actual records and make informed decisions.
Two-Phase Architecture
Section titled “Two-Phase Architecture”Recall Structured uses a two-phase architecture to combine accurate extraction with multi-hop capability:
┌─────────────────────────────────────────────────────────────────┐│ Phase 1: EXTRACTION ││ • Dedicated LLM call for intent + data extraction ││ • High accuracy for parsing natural language ││ • Handles INSERT and QUERY directly │└─────────────────────────────────────────────────────────────────┘ │ ┌───────────────────┼───────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────┐ ┌──────────┐ ┌───────────────┐ │ INSERT │ │ QUERY │ │ UPDATE/DELETE │ │ ✓ Done │ │ ✓ Done │ │ → Phase 2 │ └─────────┘ └──────────┘ └───────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Phase 2: AGENT │ │ • Receives extracted context │ │ • Multi-hop tool calls │ │ • Search → Identify → Modify │ └─────────────────────────────────────────────┘Why Two Phases?
Section titled “Why Two Phases?”Phase 1 (Extraction) is optimized for:
- Understanding natural language
- Extracting structured field values
- Classifying intent
Phase 2 (Agent) is optimized for:
- Searching and filtering data
- Making decisions based on actual records
- Executing multi-step operations
By separating these concerns, each phase can use the most appropriate approach.
Agent Tools
Section titled “Agent Tools”The agent has access to these tools:
listSchemas
Section titled “listSchemas”See available data types:
listSchemas()// → [// { name: 'payments', description: '...', fields: ['recipient', 'amount', ...] },// { name: 'workouts', description: '...', fields: ['type', 'duration', ...] }// ]listRecords
Section titled “listRecords”Get records from a schema:
listRecords({ schema: 'payments', limit: 10 })// → { schema: 'payments', count: 3, records: [...] }searchRecords
Section titled “searchRecords”Find records by field value:
searchRecords({ schema: 'payments', field: 'recipient', value: 'Jayden' })// → { schema: 'payments', field: 'recipient', value: 'Jayden', count: 2, records: [...] }getRecord
Section titled “getRecord”Get a specific record by ID:
getRecord({ schema: 'payments', id: 'abc123' })// → { recipient: 'Jayden', amount: 150, ... }insertRecord
Section titled “insertRecord”Add a new record:
insertRecord({ schema: 'payments', data: { recipient: 'Jayden', amount: 150, description: 'training' },})// → { success: true, action: 'inserted', id: 'abc123', data: {...} }updateRecord
Section titled “updateRecord”Update an existing record:
updateRecord({ schema: 'payments', id: 'abc123', data: { amount: 200 },})// → { success: true, action: 'updated', id: 'abc123', changes: { amount: 200 } }deleteRecord
Section titled “deleteRecord”Remove a record:
deleteRecord({ schema: 'payments', id: 'abc123' })// → { success: true, action: 'deleted', id: 'abc123' }Extracted Context
Section titled “Extracted Context”When Phase 2 runs, the agent receives extracted context from Phase 1:
// Phase 1 extraction result:{ schema: 'payments', intent: 'update', confidence: 0.92, data: { amount: 200 }, // The new value}
// Agent prompt includes:`User message: "Update my last payment to Jayden to $200"
## Pre-extracted InformationSchema: paymentsIntent: updateConfidence: 92%Extracted data: { "amount": 200 }
Use this extracted information to perform the update operation.First search for the matching record, then call updateRecord with the extracted data.`This gives the agent:
- What schema to work with
- What action to take
- What data to use
- Clear instructions on the approach
Example: Complex Update
Section titled “Example: Complex Update”Let’s trace through “Update my last payment to Jayden to $200”:
Phase 1: Extraction
Section titled “Phase 1: Extraction”const extraction = await memory.process(message, { userId })
// Result:{ matched: true, schema: 'payments', action: 'update', confidence: 0.92, data: { amount: 200 }, // Note: Phase 1 detected update intent but didn't complete it // because it needs to search for the right record first}Phase 2: Agent Execution
Section titled “Phase 2: Agent Execution”The agent receives the extracted context and makes tool calls:
Tool Call 1: Search for Jayden’s payments
searchRecords({ schema: 'payments', field: 'recipient', value: 'Jayden' })// → {// records: [// { id: 'abc', recipient: 'Jayden', amount: 150, date: '2024-12-05' },// { id: 'def', recipient: 'Jayden', amount: 100, date: '2024-11-20' }// ]// }Tool Call 2: Update the most recent one
updateRecord({ schema: 'payments', id: 'abc', data: { amount: 200 } })// → { success: true, action: 'updated', id: 'abc' }Final Response:
"Updated your payment to Jayden from $150 to $200"Using the Agent Directly
Section titled “Using the Agent Directly”You can also use the agent directly for advanced use cases:
import { createStructuredMemoryAgent } from '@youcraft/recall-structured'import { openai } from '@ai-sdk/openai'
const agent = createStructuredMemoryAgent({ db: 'memory.db', schemas,})
// Process with the agent (multi-hop enabled)const result = await agent.process( openai('gpt-5-nano'), 'Update my last payment to Jayden to $200', { userId: 'user_123', maxSteps: 10, extractedContext: { schema: 'payments', intent: 'update', confidence: 0.92, data: { amount: 200 }, }, })
console.log(result)// {// text: "Updated your payment to Jayden from $150 to $200",// steps: 2,// toolCalls: [// { toolName: 'searchRecords', input: {...}, output: {...} },// { toolName: 'updateRecord', input: {...}, output: {...} }// ],// dataModified: true// }Agent Configuration
Section titled “Agent Configuration”Max Steps
Section titled “Max Steps”Control how many tool calls the agent can make:
await agent.process(model, message, { userId, maxSteps: 10, // Default: 10})More steps allow more complex operations but increase latency and cost.
System Prompt
Section titled “System Prompt”The agent’s system prompt includes:
- Available schemas and their fields
- Current date and time (for interpreting “today”, “last week”)
- Instructions for each operation type
You can access it:
const systemPrompt = agent.getSystemPrompt()// Regenerated fresh each call with current date/timeBest Practices
Section titled “Best Practices”1. Use Descriptive Field Names
Section titled “1. Use Descriptive Field Names”The agent uses field names to understand data:
// Good - clear field namesz.object({ recipient: z.string(), // Agent knows this is who was paid amount: z.number(),})
// Bad - unclear field namesz.object({ r: z.string(), a: z.number(),})2. Keep Schemas Focused
Section titled “2. Keep Schemas Focused”Each schema should represent one type of data:
// Good - separate schemasschemas: { payments: { ... }, workouts: { ... },}
// Bad - mixed data in one schemaschemas: { userStuff: { schema: z.object({ paymentRecipient: z.string().optional(), workoutType: z.string().optional(), // Confusing for the agent }), },}3. Handle Agent Results
Section titled “3. Handle Agent Results”Check if data was modified:
const result = await agent.process(model, message, options)
if (result.dataModified) { // Trigger side effects, notifications, etc. await syncToExternalDB(result.toolCalls)}Next Steps
Section titled “Next Steps”- API Reference — Full API documentation
- Core Concepts — Deep dive into extraction and validation