> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oyester.metaphy.live/llms.txt
> Use this file to discover all available pages before exploring further.

# Best Practices

> Guidelines for building robust applications with the Blue Oyster API

# Best Practices

This guide provides comprehensive best practices for integrating with the Blue Oyster Personal Companion API, covering performance, reliability, security, and user experience considerations.

## Conversation Management

### Thread Continuity

<Tip>
  **Always use thread IDs** for conversation continuity and context preservation.
</Tip>

```javascript theme={null}
class ConversationManager {
  constructor() {
    this.activeThreads = new Map();
  }

  async startConversation(userId, initialMessage, personality = 'friend') {
    // Create a new thread
    const thread = await this.createThread(userId, personality);

    // Send initial message
    const response = await this.sendMessage(initialMessage, {
      threadId: thread.id,
      resourceId: userId,
      personality
    });

    return {
      threadId: thread.id,
      response,
      personality
    };
  }

  async continueConversation(threadId, message, personality) {
    return await this.sendMessage(message, {
      threadId,
      personality
    });
  }

  async createThread(userId, personality, title = null) {
    const threadData = {
      agentId: 'companionAgent',
      resourceId: userId,
      title: title || `Conversation ${new Date().toLocaleDateString()}`,
      metadata: {
        personality,
        createdAt: new Date().toISOString(),
        userId
      }
    };

    const response = await fetch('/api/memory/threads', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(threadData)
    });

    return await response.json();
  }

  async sendMessage(message, options) {
    const requestBody = {
      messages: [{ role: 'user', content: message }],
      resourceId: options.resourceId,
      ...options
    };

    const response = await fetch('/api/agents/companionAgent/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody)
    });

    return await response.json();
  }
}
```

### Context Window Management

<Warning>
  **Monitor token usage** to avoid hitting context limits and manage costs.
</Warning>

```javascript theme={null}
class TokenManager {
  constructor(maxTokens = 4000) {
    this.maxTokens = maxTokens;
    this.currentTokens = 0;
  }

  trackUsage(response) {
    this.currentTokens = response.totalUsage?.totalTokens || 0;
    return this.currentTokens;
  }

  shouldSummarize() {
    return this.currentTokens > this.maxTokens * 0.8;
  }

  async summarizeConversation(threadId, messages) {
    const summaryRequest = {
      messages: [
        {
          role: 'system',
          content: 'Summarize the key points of this conversation concisely.'
        },
        ...messages.slice(-10) // Last 10 messages
      ],
      threadId: `summary-${threadId}`,
      resourceId: 'system'
    };

    const response = await fetch('/api/agents/companionAgent/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(summaryRequest)
    });

    return await response.json();
  }
}
```

## Performance Optimization

### Streaming vs Non-Streaming

<Tip>
  **Use streaming** for real-time UI updates and better user experience.
</Tip>

```javascript theme={null}
class StreamingChatManager {
  constructor() {
    this.activeStreams = new Map();
  }

  async sendStreamingMessage(message, options, callbacks) {
    const requestBody = {
      messages: [{ role: 'user', content: message }],
      ...options
    };

    const response = await fetch('/api/agents/companionAgent/stream', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody)
    });

    if (!response.ok) {
      throw new Error(`Stream request failed: ${response.status}`);
    }

    const streamId = Date.now().toString();
    this.activeStreams.set(streamId, response);

    await this.processStream(response, callbacks);
    this.activeStreams.delete(streamId);

    return streamId;
  }

  async processStream(response, callbacks) {
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let fullText = '';

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value);
        const lines = chunk.split('\n');

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            try {
              const data = JSON.parse(line.slice(6));
              await this.handleStreamEvent(data, callbacks);

              if (data.type === 'text') {
                fullText += data.text;
              }
            } catch (e) {
              console.warn('Failed to parse stream event:', e);
            }
          }
        }
      }

      callbacks?.onComplete?.(fullText);
    } catch (error) {
      callbacks?.onError?.(error);
    } finally {
      reader.releaseLock();
    }
  }

  async handleStreamEvent(event, callbacks) {
    switch (event.type) {
      case 'text':
        callbacks?.onText?.(event.text);
        break;
      case 'tool-call':
        callbacks?.onToolCall?.(event.payload);
        break;
      case 'tool-result':
        callbacks?.onToolResult?.(event.payload);
        break;
      case 'error':
        callbacks?.onError?.(new Error(event.error.message));
        break;
    }
  }

  cancelStream(streamId) {
    const response = this.activeStreams.get(streamId);
    if (response) {
      response.body.cancel();
      this.activeStreams.delete(streamId);
    }
  }
}
```

### Caching Strategies

<Tip>
  **Cache tool results** when appropriate to reduce API calls and improve performance.
</Tip>

```javascript theme={null}
class CacheManager {
  constructor(ttl = 300000) { // 5 minutes default
    this.cache = new Map();
    this.ttl = ttl;
  }

  set(key, value) {
    const expiry = Date.now() + this.ttl;
    this.cache.set(key, { value, expiry });
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;

    if (Date.now() > item.expiry) {
      this.cache.delete(key);
      return null;
    }

    return item.value;
  }

  clear() {
    this.cache.clear();
  }

  cleanup() {
    const now = Date.now();
    for (const [key, item] of this.cache.entries()) {
      if (now > item.expiry) {
        this.cache.delete(key);
      }
    }
  }
}

class WeatherCache {
  constructor() {
    this.cache = new CacheManager(1800000); // 30 minutes for weather
  }

  getCachedWeather(location) {
    return this.cache.get(`weather_${location.toLowerCase()}`);
  }

  setCachedWeather(location, weatherData) {
    this.cache.set(`weather_${location.toLowerCase()}`, weatherData);
  }

  async getWeatherWithCache(location) {
    const cached = this.getCachedWeather(location);
    if (cached) {
      return { ...cached, cached: true };
    }

    // Fetch from API
    const response = await fetch('/api/agents/companionAgent/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        messages: [{ role: 'user', content: `What's the weather like in ${location}?` }],
        resourceId: 'system'
      })
    });

    const result = await response.json();

    // Extract weather data from tool results
    const weatherData = this.extractWeatherFromResponse(result);
    if (weatherData) {
      this.setCachedWeather(location, weatherData);
    }

    return { ...weatherData, cached: false };
  }

  extractWeatherFromResponse(response) {
    const toolResults = response.toolResults || [];
    const weatherResult = toolResults.find(r => r.toolName === 'weatherTool');
    return weatherResult?.result || null;
  }
}
```

## Error Handling and Resilience

### Comprehensive Error Recovery

<Tip>
  **Implement exponential backoff** and circuit breaker patterns for reliability.
</Tip>

```javascript theme={null}
class ResilientAPIClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.retryHandler = new RetryHandler();
    this.circuitBreaker = new CircuitBreaker();
    this.errorTracker = new ErrorTracker();
  }

  async makeRequest(endpoint, data) {
    return await this.circuitBreaker.execute(async () => {
      return await this.retryHandler.executeWithRetry(async () => {
        const response = await fetch(`${this.baseUrl}${endpoint}`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data)
        });

        if (!response.ok) {
          const errorData = await response.json();
          throw new APIError(errorData.error, response.status, errorData.code);
        }

        return await response.json();
      });
    });
  }

  async sendMessage(message, options = {}) {
    try {
      const requestData = {
        messages: [{ role: 'user', content: message }],
        ...options
      };

      return await this.makeRequest('/api/agents/companionAgent/generate', requestData);
    } catch (error) {
      this.errorTracker.trackError(error, {
        action: 'send_message',
        messageLength: message.length,
        personality: options.personality
      });

      // Return fallback response
      return await this.generateFallbackResponse(message, options);
    }
  }

  async generateFallbackResponse(message, options) {
    // Provide basic fallback when API is unavailable
    const fallbacks = {
      friend: "I'm here to support you. Could you tell me more about what's on your mind?",
      guru: "In times of uncertainty, remember that the eternal truth resides within.",
      wanderer: "Every journey has its challenges. What path are you walking today?"
    };

    return {
      text: fallbacks[options.personality] || fallbacks.friend,
      fallback: true,
      timestamp: new Date().toISOString()
    };
  }
}
```

## Security Considerations

### Input Validation

<Warning>
  **Always validate and sanitize user inputs** to prevent injection attacks.
</Warning>

```javascript theme={null}
class InputValidator {
  static validateMessage(message) {
    if (!message || typeof message !== 'string') {
      throw new ValidationError('Message must be a non-empty string');
    }

    if (message.length > 10000) {
      throw new ValidationError('Message too long (max 10000 characters)');
    }

    // Check for potentially harmful content
    if (this.containsMaliciousContent(message)) {
      throw new ValidationError('Message contains inappropriate content');
    }

    return message.trim();
  }

  static validatePersonality(personality) {
    const validPersonalities = ['friend', 'guru', 'wanderer', 'philosopher'];
    if (!validPersonalities.includes(personality)) {
      throw new ValidationError(`Invalid personality: ${personality}`);
    }
    return personality;
  }

  static validateResourceId(resourceId) {
    if (!resourceId || typeof resourceId !== 'string') {
      throw new ValidationError('ResourceId must be a non-empty string');
    }

    // Basic sanitization
    const sanitized = resourceId.replace(/[^a-zA-Z0-9_-]/g, '');
    if (sanitized !== resourceId) {
      throw new ValidationError('ResourceId contains invalid characters');
    }

    return sanitized;
  }

  static containsMaliciousContent(text) {
    const maliciousPatterns = [
      /<script/i,
      /javascript:/i,
      /on\w+\s*=/i,
      /<iframe/i,
      /<object/i
    ];

    return maliciousPatterns.some(pattern => pattern.test(text));
  }
}

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
  }
}
```

### Rate Limiting

<Tip>
  **Implement client-side rate limiting** to prevent abuse and manage costs.
</Tip>

```javascript theme={null}
class RateLimiter {
  constructor(requestsPerMinute = 60) {
    this.requestsPerMinute = requestsPerMinute;
    this.requests = [];
  }

  async acquire() {
    const now = Date.now();
    const oneMinuteAgo = now - 60000;

    // Remove old requests
    this.requests = this.requests.filter(time => time > oneMinuteAgo);

    if (this.requests.length >= this.requestsPerMinute) {
      const waitTime = 60000 - (now - this.requests[0]);
      await this.sleep(waitTime);
    }

    this.requests.push(now);
  }

  async sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Usage
const rateLimiter = new RateLimiter(30); // 30 requests per minute

async function sendRateLimitedMessage(message, options) {
  await rateLimiter.acquire();

  return await chat.sendMessage(message, options);
}
```

## User Experience

### Loading States and Feedback

<Tip>
  **Provide clear feedback** during API calls and tool executions.
</Tip>

```javascript theme={null}
class UIManager {
  constructor() {
    this.loadingStates = new Map();
  }

  showLoading(elementId, message = 'Thinking...') {
    const element = document.getElementById(elementId);
    if (!element) return;

    const loadingId = Date.now().toString();
    this.loadingStates.set(loadingId, elementId);

    element.innerHTML = `
      <div class="loading-container">
        <div class="loading-spinner"></div>
        <div class="loading-text">${message}</div>
      </div>
    `;

    element.classList.add('loading');

    return loadingId;
  }

  updateLoading(loadingId, message) {
    const elementId = this.loadingStates.get(loadingId);
    if (!elementId) return;

    const element = document.getElementById(elementId);
    const textElement = element.querySelector('.loading-text');
    if (textElement) {
      textElement.textContent = message;
    }
  }

  hideLoading(loadingId) {
    const elementId = this.loadingStates.get(loadingId);
    if (!elementId) return;

    const element = document.getElementById(elementId);
    element.classList.remove('loading');
    element.innerHTML = '';

    this.loadingStates.delete(loadingId);
  }

  showToolExecution(toolName, toolCall) {
    const messages = {
      weatherTool: 'Checking the weather...',
      'search-destinations': 'Finding spiritual destinations...',
      'search-meditation-audio': 'Searching for meditation tracks...'
    };

    return this.showLoading('response-area', messages[toolName] || 'Working...');
  }

  showStreamingText(text) {
    const element = document.getElementById('response-area');
    if (!element) return;

    // Append text progressively for streaming effect
    element.innerHTML += text;
  }

  showError(message, retryable = false) {
    const element = document.getElementById('response-area');
    if (!element) return;

    element.innerHTML = `
      <div class="error-message">
        <div class="error-icon">⚠️</div>
        <div class="error-text">${message}</div>
        ${retryable ? '<button onclick="retryLastRequest()">Retry</button>' : ''}
      </div>
    `;
  }
}
```

### Personality Selection Guidelines

<Tip>
  **Match personality to user needs** and conversation context.
</Tip>

```javascript theme={null}
class PersonalityRecommender {
  static recommendPersonality(message, userHistory = []) {
    const lowerMessage = message.toLowerCase();

    // Deep spiritual questions → Guru
    if (this.containsKeywords(lowerMessage, ['enlightenment', 'truth', 'wisdom', 'ancient'])) {
      return 'guru';
    }

    // Travel and exploration → Wanderer
    if (this.containsKeywords(lowerMessage, ['travel', 'journey', 'visit', 'explore'])) {
      return 'wanderer';
    }

    // Philosophical inquiry → Philosopher
    if (this.containsKeywords(lowerMessage, ['why', 'meaning', 'existence', 'reality'])) {
      return 'philosopher';
    }

    // Emotional support → Friend
    if (this.containsKeywords(lowerMessage, ['help', 'feel', 'struggling', 'support'])) {
      return 'friend';
    }

    // Default based on user history
    const personalityUsage = this.analyzeHistory(userHistory);
    return personalityUsage.mostUsed || 'friend';
  }

  static containsKeywords(text, keywords) {
    return keywords.some(keyword => text.includes(keyword));
  }

  static analyzeHistory(history) {
    const personalityCount = {};

    history.forEach(item => {
      const personality = item.personality || 'friend';
      personalityCount[personality] = (personalityCount[personality] || 0) + 1;
    });

    const mostUsed = Object.keys(personalityCount).reduce((a, b) =>
      personalityCount[a] > personalityCount[b] ? a : b
    );

    return {
      mostUsed,
      totalInteractions: history.length,
      distribution: personalityCount
    };
  }
}
```

## Production Deployment

### Monitoring and Analytics

<Tip>
  **Implement comprehensive monitoring** for performance and error tracking.
</Tip>

```javascript theme={null}
class APIMonitor {
  constructor() {
    this.metrics = {
      requests: 0,
      errors: 0,
      averageResponseTime: 0,
      tokenUsage: 0
    };

    this.responseTimes = [];
  }

  trackRequest(response, startTime) {
    this.metrics.requests++;
    const responseTime = Date.now() - startTime;
    this.responseTimes.push(responseTime);

    // Keep only last 100 response times
    if (this.responseTimes.length > 100) {
      this.responseTimes.shift();
    }

    this.metrics.averageResponseTime = this.responseTimes.reduce((a, b) => a + b, 0) / this.responseTimes.length;

    if (response.totalUsage) {
      this.metrics.tokenUsage += response.totalUsage.totalTokens;
    }
  }

  trackError(error) {
    this.metrics.errors++;
    console.error('API Error tracked:', error);
  }

  getMetrics() {
    return {
      ...this.metrics,
      errorRate: this.metrics.requests > 0 ? (this.metrics.errors / this.metrics.requests) * 100 : 0
    };
  }

  reset() {
    this.metrics = {
      requests: 0,
      errors: 0,
      averageResponseTime: 0,
      tokenUsage: 0
    };
    this.responseTimes = [];
  }
}
```

These best practices will help you build robust, performant, and user-friendly applications with the Blue Oyster Personal Companion API.
