npm package: @recalio-client/sdk
Description: Recalio SDK - Memory Layer for AI Agents
Author: DcodeFrenzy
Latest version: 1.1.0

# Recalio Client SDK

Memory Layer SDK for AI Agents - TypeScript/JavaScript client library.

## Installation

```bash
npm install @recalio/client
```

## Quick Start

```typescript
import { Recalio } from '@recalio/client'

const recalio = new Recalio({ 
    apiKey: 'rec_sk_your_api_key_here',
    baseUrl: 'https://api.recalio.dev' // Optional, defaults to production
})

// Add memory
await recalio.memory.add({
    messages: [{ role: 'user', content: 'I work at Google as a Senior Engineer' }],
    contextId: 'chat-123',
    userId: 'alice'
})

// Search memory
const result = await recalio.memory.search('Where does Alice work?', {
    userId: 'alice',
    rerank: true,
    includeSummary: true
})

console.log(result.summary)    // "Alice works at Google as a Senior Engineer."
console.log(result.aiContext)  // Structured context for AI agents
```

## API Reference

### `memory.add(input)`

Add conversations or data to memory. The ingestion layer automatically extracts entities (people, organizations, etc.) and facts (relationships) from your messages.

**Parameters:**
- `messages` (any) - Messages to ingest. Accepts any format:
  - Array of chat messages: `[{ role: 'user', content: '...' }]`
  - Plain string: `"Alice works at Google"`
  - Array of strings: `["Fact 1", "Fact 2"]`
  - Any object/structured data
- `contextId` (string, **required**) - Unique identifier for this context. Examples:
  - Chat conversation: `'chat-123'` or `'thread-abc'`
  - Phone call: `'call-456'`
  - Session: `'session-789'`
  - Page/document: `'page-xyz'`
- `userId` (string, optional) - User ID for private data. Should be unique per user (e.g., `'alice_chen_123'` or `'user_${name}_${id}'`). When set, data is only accessible to that user.
- `contextType` (string, optional) - Type of context (default: `'conversation'`). Examples: `'conversation'`, `'meeting'`, `'email'`, `'call'`
- `source` (string, optional) - Source system (default: `'ingestion_api'`). Examples: `'web_app'`, `'slack'`, `'api'`

**Returns:**
``typescript
{
    success: boolean
    contextId: string
    status: 'processing'        // Indicates async queue processing
    message: string             // e.g., "Your data has been queued for processing. (2 jobs ahead)"
    jobId: string              // Queue job ID for tracking
    queuePosition: number      // Number of jobs ahead
    stats: {                   // All zeros until processing completes
        entitiesExtracted: 0
        factsExtracted: 0
        factsAdded: 0
        factsUpdated: 0
        factsIgnored: 0
        factsEscalated: 0
    }
    tokenUsage: {              // All zeros until processing completes
        promptTokens: 0
        completionTokens: 0
        totalTokens: 0
    }
}
```

> **Note:** Recalio uses **queue-based processing** for reliable ingestion. Stats and token counts are zeros initially and will be logged after processing completes (typically 2-5 seconds). Use `memory.search()` to verify completion.

**Examples:**

```typescript
// Example 1: Add a conversation
const result = await recalio.memory.add({
    messages: [
        { role: 'user', content: "I just met Alice Chen. She's a Senior Engineer at Google." },
        { role: 'assistant', content: "That's interesting! What does she work on?" },
        { role: 'user', content: "She works on AI for the Search team." }
    ],
    contextId: 'chat-123',
    userId: 'user_me_123'
})
// Returns: { success: true, contextId: 'chat-123', status: 'processing', message: '...', jobId: 'job_abc', queuePosition: 0, stats: {...}, tokenUsage: {...} }

// Example 2: Add plain text
await recalio.memory.add({
    messages: "Alice Chen works at Google as a Senior Engineer on the Search team.",
    contextId: 'note-456'
})

// Example 3: Add user-specific preferences
await recalio.memory.add({
    messages: "I prefer email notifications and my favorite color is blue.",
    contextId: 'preferences-alice',
    userId: 'alice_chen_123',  // Private to Alice
    contextType: 'preferences'
})

// Example 4: Add meeting notes
await recalio.memory.add({
    messages: [
        "Meeting with Alice Chen and Bob Smith",
        "Alice discussed her work on AI at Google",
        "Bob manages the Cloud division"
    ],
    contextId: 'meeting-789',
    contextType: 'meeting',
    source: 'calendar_integration'
})
```

### `memory.search(query, options?)`

Search memory and get AI-ready context. Returns structured facts, entities, and formatted context for your AI agent.

**Parameters:**
- `query` (string) - Search query (natural language)
- `options` (SearchOptions, optional):
  - `userId` (string) - Include user's private data in results
  - `contextId` (string) - Filter to specific context only
  - `limit` (number) - Maximum results (default: 100)
  - `onlyCurrent` (boolean) - Only return current facts (exclude historical)
  - `rerank` (boolean) - Enable LLM reranking for better relevance (default: `false`). Only applies if ≥10 facts.
  - `includeSummary` (boolean) - Include natural language summary (default: `true`)
  - `expandDepth` (number) - Graph expansion depth in hops (default: `2`). Higher values traverse more relationships but are slower.

**Returns:**
```typescript
{
    query: string
    entities: Entity[]           // Relevant entities found
    facts: Fact[]               // Relevant facts/relationships
    aiContext: string           // Structured context for prompt injection
    summary?: string            // Natural language summary (if includeSummary is true)
    retrievalTimeMs: number     // Time taken in milliseconds
}
```

**Entity Structure:**
```typescript
{
    id: string
    name: string                 // e.g., "Alice Chen"
    label: string               // e.g., "Person", "Organization"
    properties?: Record<string, any>
}
```

**Fact Structure:**
```typescript
{
    id: string
    subject: { id: string; name: string; label: string }
    predicate: string           // e.g., "works_at", "reports_to"
    object: { id: string; name: string; label: string }
    factType?: string           // e.g., "relationship", "attribute"
    confidence?: number         // 0.0 to 1.0
    properties?: Record<string, any>
    validFrom?: string         // ISO date string
    validTo?: string           // ISO date string (if historical)
    isCurrent?: boolean        // true if currently valid
}
```

**Examples:**

```typescript
// Example 1: Simple search WITH error handling
// ⚠️ IMPORTANT: You MUST handle errors - search() can throw exceptions

let result
try {
    result = await recalio.memory.search('Where does Alice work?')
    
    // Check for credit exhaustion (doesn't throw, returns in error field)
    if (result.error) {
        console.warn('Memory unavailable:', result.error.message)
        // result.entities = []
        // result.facts = []
        // result.aiContext = ''
    }
} catch (error) {
    // Handle other errors (UNAUTHORIZED, PAYLOAD_TOO_LARGE, etc.)
    console.error('Search failed:', error.message)
    // Return empty result or handle appropriately
    result = {
        query: 'Where does Alice work?',
        entities: [],
        facts: [],
        aiContext: '',
        retrievalTimeMs: 0
    }
}

// Use result safely
const aiContext = result.aiContext || 'No memory available'
console.log(result.summary)
// "Alice Chen works at Google as a Senior Engineer on the Search team."

console.log(result.aiContext)
// FACTS and ENTITIES represent relevant context to the current conversation.
// 
// <FACTS>
//   ## CURRENT FACTS (valid now):
//     ### Generals:
//     - [CURRENT] Alice Chen works at Google. (since 2024-01-15 10:30:00)
// </FACTS>
// 
// <ENTITIES>
//   - Alice Chen: Alice Chen is a Senior Engineer at Google...
//   - Google: Google is a technology company...
// </ENTITIES>

// Example 2: Search with reranking (better for complex queries)
const result = await recalio.memory.search('Who does Alice work with?', {
    rerank: true,           // Enable LLM reranking
    includeSummary: true,    // Get summary
    expandDepth: 2,          // Traverse 2 hops in graph
    limit: 50                // Max 50 results
})

// Example 3: Search user-specific memory
const userResult = await recalio.memory.search('What are my preferences?', {
    userId: 'alice_chen_123',  // Only search Alice's private data
    includeSummary: true
})

// Example 4: Search specific context
const contextResult = await recalio.memory.search('What was discussed?', {
    contextId: 'meeting-789',  // Only search this meeting
    includeSummary: true
})

// Example 5: Search only current facts (exclude historical)
const currentResult = await recalio.memory.search('Where does Alice work now?', {
    onlyCurrent: true,     // Exclude historical facts
    expandDepth: 1         // 1 hop only (faster)
})
```

## Using Memory in AI Agents

### Best Practice: Memory Context at Bottom of Prompt

When using `aiContext` in your AI agent prompts, place it at the **bottom** of your system prompt, after instructions but before user messages:

```typescript
// ✅ CORRECT: Memory context at bottom
const systemPrompt = `
You are a helpful assistant.

Instructions:
- Be concise and factual
- Use the provided context to answer questions
- If context is insufficient, say so

${searchResult.aiContext}  // Memory context at bottom
`

// ❌ WRONG: Don't put memory at the top
const badPrompt = `
${searchResult.aiContext}  // Don't do this

You are a helpful assistant...
`
```

### Complete Workflow Example

```typescript
import { Recalio } from '@recalio/client'
import OpenAI from 'openai'

const recalio = new Recalio({ apiKey: 'rec_sk_xxx' })
const openai = new OpenAI({ apiKey: 'sk-xxx' })

// Step 1: Add conversation to memory
await recalio.memory.add({
    messages: [
        { role: 'user', content: "I just met Alice Chen. She's a Senior Engineer at Google." },
        { role: 'assistant', content: "That's interesting! What does she work on?" },
        { role: 'user', content: "She works on AI for the Search team." }
    ],
    contextId: 'chat-123',
    userId: 'user_me_123'
})

// Step 2: When user asks a question, search memory
// IMPORTANT: Handle errors - search() can throw exceptions
let searchResult
try {
    searchResult = await recalio.memory.search('Tell me about Alice Chen', {
        userId: 'user_me_123',
        rerank: true,
        includeSummary: true,
        expandDepth: 2
    })
    
    // Check for credit exhaustion (doesn't throw, returns in error field)
    if (searchResult.error) {
        console.warn('Memory unavailable:', searchResult.error.message)
        // Continue without memory - AI agent can still work
    }
} catch (error) {
    // Handle other errors (UNAUTHORIZED, PAYLOAD_TOO_LARGE, etc.)
    if (error instanceof RecalioError) {
        console.error('Search failed:', error.message)
        // Return empty result or handle appropriately
    }
    searchResult = {
        query: 'Tell me about Alice Chen',
        entities: [],
        facts: [],
        aiContext: '',
        retrievalTimeMs: 0
    }
}

// Step 3: Build prompt with memory at bottom
const messages = [
    {
        role: 'system',
        content: `You are a helpful assistant.

Instructions:
- Use the provided context to answer questions accurately
- Be concise and factual
- If you don't know something, say so

${searchResult.aiContext}`  // Memory context at bottom
    },
    {
        role: 'user',
        content: 'Tell me about Alice Chen'
    }
]

// Step 4: Send to your LLM
const completion = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: messages
})

console.log(completion.choices[0].message.content)
```

### Customer Support Example

```typescript
async function handleCustomerQuery(customerId: string, query: string) {
    // Search customer's history
    const memory = await recalio.memory.search(query, {
        userId: `customer_${customerId}`,
        includeSummary: true,
        rerank: true
    })
    
    // Build prompt with memory at bottom
    const systemPrompt = `
You are a customer support agent.

Guidelines:
- Be friendly and helpful
- Use customer history to provide personalized responses
- Escalate if you cannot resolve the issue

${memory.aiContext}  // Customer history at bottom
`
    
    // Send to your LLM
    return await yourLLM.generate(systemPrompt, query)
}
```

## Error Handling

Recalio SDK handles errors in two ways:

1. **Thrown Exceptions** - For most errors (UNAUTHORIZED, PAYLOAD_TOO_LARGE, etc.)
2. **In-Response Errors** - For credit exhaustion (returns empty data with error field)

### Error Types

```typescript
import { RecalioError, RecalioErrorCode } from '@recalio/client'

// Error codes
RecalioErrorCode.UNAUTHORIZED           // Invalid API key
RecalioErrorCode.INSUFFICIENT_CREDITS  // Credits exhausted (in response.error)
RecalioErrorCode.PAYLOAD_TOO_LARGE      // Request too large
RecalioErrorCode.TOO_MANY_REQUESTS      // Rate limit exceeded
RecalioErrorCode.NOT_FOUND              // Resource not found
RecalioErrorCode.BAD_REQUEST            // Invalid request
RecalioErrorCode.CONFLICT               // Resource conflict
RecalioErrorCode.INTERNAL_SERVER_ERROR // Server error
```

### Handling Credit Exhaustion (In-Response Error)

Credit exhaustion doesn't throw - it returns empty data with an error field:

```typescript
import { Recalio, isCreditExhaustionError, extractCreditResetDate } from '@recalio/client'

const result = await recalio.memory.search('Where does Alice work?')

// Check for credit exhaustion (doesn't throw exception)
if (result.error) {
    if (isCreditExhaustionError(result.error)) {
        const resetDate = extractCreditResetDate(result.error)
        console.warn(`Credits exhausted. Resets at: ${resetDate}`)
        
        // Continue without memory - AI agent can still work
        // result.entities = []
        // result.facts = []
        // result.aiContext = ''
    }
}

// Use result normally - empty arrays if credits exhausted
const aiPrompt = result.aiContext || 'No memory available'
```

### Handling Thrown Exceptions

Most errors throw exceptions:

```typescript
import { Recalio, RecalioError, RecalioErrorCode } from '@recalio/client'

try {
    const result = await recalio.memory.add({
        messages: 'Test message',
        contextId: 'test-123'
    })
} catch (error) {
    if (error instanceof RecalioError) {
        // Check error code
        switch (error.code) {
            case RecalioErrorCode.UNAUTHORIZED:
                console.error('Invalid API key - regenerate your key')
                break
                
            case RecalioErrorCode.PAYLOAD_TOO_LARGE:
                console.error('Data too large - split into smaller chunks')
                // Split data and retry
                break
                
            case RecalioErrorCode.TOO_MANY_REQUESTS:
                console.error('Rate limit exceeded - wait and retry')
                // Implement exponential backoff
                await new Promise(resolve => setTimeout(resolve, 1000))
                // Retry request
                break
                
            case RecalioErrorCode.NOT_FOUND:
                console.error('Resource not found')
                break
                
            case RecalioErrorCode.BAD_REQUEST:
                console.error('Invalid request:', error.message)
                break
                
            default:
                console.error('Error:', error.getUserMessage())
        }
        
        // Check if retryable
        if (error.isRetryable()) {
            // Implement retry logic
        }
        
        // Check if user action required
        if (error.requiresUserAction()) {
            // Show user-friendly message
            console.error(error.getUserMessage())
        }
    } else {
        console.error('Unknown error:', error)
    }
}
```

### Complete Error Handling Example

```typescript
import { 
    Recalio, 
    RecalioError, 
    RecalioErrorCode,
    isCreditExhaustionError,
    getErrorMessage
} from '@recalio/client'

async function searchWithMemory(query: string) {
    try {
        const result = await recalio.memory.search(query)
        
        // Check for in-response errors (credit exhaustion)
        if (result.error) {
            if (isCreditExhaustionError(result.error)) {
                console.warn('Memory unavailable: Credits exhausted')
                // Continue without memory - don't break AI agent
                return {
                    aiContext: '',
                    hasMemory: false,
                    error: result.error
                }
            }
        }
        
        // Normal response
        return {
            aiContext: result.aiContext,
            summary: result.summary,
            hasMemory: true,
            entities: result.entities,
            facts: result.facts
        }
        
    } catch (error) {
        if (error instanceof RecalioError) {
            // Handle thrown exceptions
            if (error.code === RecalioErrorCode.UNAUTHORIZED) {
                throw new Error('API key invalid - please regenerate')
            }
            
            // Log other errors but don't break
            console.error('Memory search error:', getErrorMessage({
                code: error.code,
                message: error.message
            }))
            
            // Return empty result - AI agent can continue
            return {
                aiContext: '',
                hasMemory: false,
                error: {
                    code: error.code,
                    message: error.message
                }
            }
        }
        
        // Unknown error - still return empty to not break AI agent
        return {
            aiContext: '',
            hasMemory: false,
            error: {
                code: 'UNKNOWN',
                message: 'Unknown error occurred'
            }
        }
    }
}

// Use in AI agent
const memory = await searchWithMemory('user query')
const prompt = memory.hasMemory 
    ? `Use this context: ${memory.aiContext}` 
    : 'No memory available'
```

### Error Helper Functions

```typescript
import {
    isCreditExhaustionError,    // Check if credit exhaustion
    isRetryableError,            // Check if error is retryable
    requiresUserAction,          // Check if user action needed
    getErrorMessage,              // Get user-friendly message
    extractCreditResetDate       // Extract credit reset date
} from '@recalio/client'

// Check error type
if (isCreditExhaustionError(result.error)) {
    const resetDate = extractCreditResetDate(result.error)
    console.log(`Credits reset at: ${resetDate}`)
}

// Get user-friendly message
const message = getErrorMessage({
    code: RecalioErrorCode.INSUFFICIENT_CREDITS,
    message: 'Credit limit exceeded...'
})
```

### Safe Methods (Never Throw)

For AI agents that should never crash, use the `*Safe` methods:

```typescript
import { Recalio } from '@recalio/client'

const recalio = new Recalio({ apiKey: 'rec_xxx' })

// searchSafe - never throws, returns empty data on error
const result = await recalio.memory.searchSafe('query')
if (result.error) {
    console.warn('Memory unavailable:', result.error.message)
    // Continue without memory - AI agent still works
}
const aiContext = result.aiContext || 'No memory available'

// addSafe - never throws, returns error in result
const addResult = await recalio.memory.addSafe({
    messages: 'Test',
    contextId: 'chat-123'
})
if (addResult.error) {
    console.warn('Failed to save memory:', addResult.error.message)
    // Continue - memory not saved but agent still works
}
```

**When to use Safe methods:**
- Production AI agents that must never crash
- When you want to gracefully degrade without memory
- When you prefer checking `result.error` over try/catch

**When to use regular methods:**
- When you want to handle errors explicitly
- When errors should stop execution
- When you need detailed error information

### Best Practices

1. **Always check `error` field** in responses (for credit exhaustion)
2. **Catch exceptions** for all other errors
3. **Don't break AI agents** - return empty data when memory unavailable
4. **Log errors** for debugging but continue gracefully
5. **Use helper functions** for error checking and messages
6. **Use `*Safe` methods** for production AI agents that must never crash

## User Management

### `createUser(input)`

Create a new user in your organization.

```typescript
await recalio.createUser({
    userId: 'alice_chen_123',
    email: 'alice@example.com',
    properties: { name: 'Alice Chen', role: 'engineer' }
})
```

### `getUser(input)`

Get user by ID or userId.

```typescript
const { user } = await recalio.getUser({ userId: 'alice_chen_123' })
```

### `updateUser(input)`

Update user information.

```typescript
await recalio.updateUser({
    userId: 'alice_chen_123',
    email: 'alice.new@example.com',
    properties: { role: 'senior_engineer' }
})
```

### `listUsers()`

List all users in organization.

```typescript
const { users } = await recalio.listUsers()
```

### `deleteUser(userId)`

Delete a user.

```typescript
await recalio.deleteUser('alice_chen_123')
```

## License

MIT