From exa-pack
Implements Exa search reliability patterns: fallback query chains across search types, exponential backoff retries, circuit breakers for fault-tolerant integrations.
How this skill is triggered — by the user, by Claude, or both
Slash command
/exa-pack:exa-reliability-patternsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Production reliability patterns for Exa neural search. Exa-specific failure modes include: empty result sets (query too narrow), content retrieval failures (sites block crawling), variable latency by search type, and 429 rate limits at 10 QPS default.
Production reliability patterns for Exa neural search. Exa-specific failure modes include: empty result sets (query too narrow), content retrieval failures (sites block crawling), variable latency by search type, and 429 rate limits at 10 QPS default.
import Exa from "exa-js";
const exa = new Exa(process.env.EXA_API_KEY);
// If neural search returns too few results, fall back through search types
async function resilientSearch(
query: string,
minResults = 3,
opts: any = {}
) {
// Try 1: Neural search (best quality)
let results = await exa.searchAndContents(query, {
type: "neural",
numResults: 10,
...opts,
});
if (results.results.length >= minResults) return results;
// Try 2: Auto search (Exa picks best approach)
results = await exa.searchAndContents(query, {
type: "auto",
numResults: 10,
...opts,
});
if (results.results.length >= minResults) return results;
// Try 3: Keyword search (different index)
results = await exa.searchAndContents(query, {
type: "keyword",
numResults: 10,
...opts,
});
if (results.results.length >= minResults) return results;
// Try 4: Remove filters and broaden
const broadOpts = { ...opts };
delete broadOpts.startPublishedDate;
delete broadOpts.endPublishedDate;
delete broadOpts.includeDomains;
delete broadOpts.includeText;
return exa.searchAndContents(query, {
type: "auto",
numResults: 10,
...broadOpts,
});
}
async function searchWithRetry(
query: string,
opts: any,
maxRetries = 3
) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await exa.searchAndContents(query, opts);
} catch (err: any) {
const status = err.status || 0;
// Only retry on rate limits (429) and server errors (5xx)
if (status !== 429 && (status < 500 || status >= 600)) throw err;
if (attempt === maxRetries) throw err;
const delay = 1000 * Math.pow(2, attempt) + Math.random() * 500;
console.log(`[Exa] ${status} retry ${attempt + 1}/${maxRetries} in ${delay.toFixed(0)}ms`);
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error("Unreachable");
}
class ExaCircuitBreaker {
private failures = 0;
private lastFailure = 0;
private state: "closed" | "open" | "half-open" = "closed";
private readonly threshold = 5; // failures before opening
private readonly resetTimeMs = 30000; // 30s before half-open
async execute<T>(fn: () => Promise<T>, fallback?: () => T): Promise<T> {
// Check if circuit should reset
if (this.state === "open") {
if (Date.now() - this.lastFailure > this.resetTimeMs) {
this.state = "half-open";
} else if (fallback) {
return fallback();
} else {
throw new Error("Exa circuit breaker is open");
}
}
try {
const result = await fn();
if (this.state === "half-open") {
this.state = "closed";
this.failures = 0;
}
return result;
} catch (err: any) {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.threshold) {
this.state = "open";
console.warn(`[Exa] Circuit breaker OPEN after ${this.failures} failures`);
}
if (fallback && this.state === "open") return fallback();
throw err;
}
}
getState() {
return { state: this.state, failures: this.failures };
}
}
const circuitBreaker = new ExaCircuitBreaker();
// Usage with fallback to cached results
const result = await circuitBreaker.execute(
() => exa.searchAndContents("query", { numResults: 5, text: true }),
() => getCachedResults("query") // fallback when circuit is open
);
interface SearchResultWithMeta {
results: any[];
degraded: boolean;
source: "live" | "cache" | "fallback";
searchType: string;
}
async function degradableSearch(
query: string,
opts: any = {}
): Promise<SearchResultWithMeta> {
// Level 1: Full search with contents
try {
const results = await searchWithRetry(query, {
type: "neural",
numResults: 10,
text: { maxCharacters: 2000 },
highlights: { maxCharacters: 500 },
...opts,
}, 2);
return { results: results.results, degraded: false, source: "live", searchType: "neural" };
} catch {}
// Level 2: Fast search without content (less expensive)
try {
const results = await exa.search(query, {
type: "fast",
numResults: 5,
});
return { results: results.results, degraded: true, source: "live", searchType: "fast" };
} catch {}
// Level 3: Return cached results
const cached = getCachedResults(query);
if (cached) {
return { results: cached, degraded: true, source: "cache", searchType: "cached" };
}
// Level 4: Return empty with degradation flag
return { results: [], degraded: true, source: "fallback", searchType: "none" };
}
class SearchQualityMonitor {
private stats = { total: 0, empty: 0, lowScore: 0 };
record(results: any[]) {
this.stats.total++;
if (results.length === 0) this.stats.empty++;
if (results[0]?.score < 0.5) this.stats.lowScore++;
}
isHealthy(): boolean {
if (this.stats.total < 10) return true; // not enough data
const emptyRate = this.stats.empty / this.stats.total;
const lowScoreRate = this.stats.lowScore / this.stats.total;
return emptyRate < 0.2 && lowScoreRate < 0.3;
}
getReport() {
return {
...this.stats,
emptyRate: `${((this.stats.empty / this.stats.total) * 100).toFixed(1)}%`,
lowScoreRate: `${((this.stats.lowScore / this.stats.total) * 100).toFixed(1)}%`,
healthy: this.isHealthy(),
};
}
}
| Issue | Cause | Solution |
|---|---|---|
| Empty results | Query too specific | Use fallback chain with broader query |
| Slow responses | Neural on complex query | Degrade to fast type |
| 429 rate limit | Burst traffic | Circuit breaker + backoff |
| Content retrieval fails | Site blocks crawling | Fall back to highlights or summary |
| Quality degradation | Query drift | Monitor empty/low-score rates |
For policy guardrails, see exa-policy-guardrails. For architecture variants, see exa-architecture-variants.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin exa-packApplies production-ready exa-js SDK patterns: client singletons, typed search wrappers, error handling for TypeScript Exa API integrations.
Implements Perplexity Sonar API reliability patterns: model fallback, circuit breaker, streaming timeout, citation validation. For production AI search resilience.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.