Relay-E
Concepts

Context Resolver

Parallel-fetch context from many sources, sort by priority, trim to a token budget, hand it to the model.

The Context Resolver is the layer that decides what the model gets to see before each turn. It's the difference between an LLM that hallucinates and one that answers from your customer's real data.

Mental model

All sources fetch in parallel with timeouts, then are sorted and trimmed before reaching the model.

Every source has the same shape:

interface ContextSource {
  name: string;
  priority: number;  // higher survives the budget
  fetch(req: ContextRequest): Promise<ContextItem[]>;
}

What's bundled

Today's resolver ships with a single example source (skill_guardrails). The roadmap sources are:

SourceWhat it returns
user_profileTenant-scoped user attributes from your customer's DB
vector_searchTop-k document chunks from document_chunks (pgvector HNSW)
recent_messagesCompacted memory + last N turns from messages
mcp_resourcesResources exposed by registered MCP servers
realtime_apisLive values pulled via tool-side connectors

You can add your own — see Guides → Adding a context source.

Parallel fetch with fault tolerance

All sources run concurrently via Promise.allSettled. If a source throws, the resolver logs and continues — a flaky vector store or a slow API can't take down the response:

const settled = await Promise.allSettled(
  this.sources.map((s) => s.fetch(req)),
);
const items = settled.flatMap((r) =>
  r.status === "fulfilled" ? r.value : [],
);

Add timeouts at the source level (via ctx.signal) so a stuck source doesn't pin the whole turn.

Priority + budget trimming

After collection, items are sorted by priority (descending) and packed into the token budget:

let used = 0;
const kept: ContextItem[] = [];
for (const item of items) {
  if (used + item.tokenEstimate > budget) continue;
  used += item.tokenEstimate;
  kept.push(item);
}

Highest priority survives. Drop low-priority items first. Document the priority of each source explicitly so the system stays predictable as you add sources.

PriorityUse for
100+Hard constraints (skill guardrails, policy rules)
50–99User-specific facts (profile, recent activity)
10–49Retrieved knowledge (RAG hits)
0–9Nice-to-have ambient signals

How the budget is decided

The agent loop calls allocateBudget(contextWindow) based on the chosen model:

{
  systemPrompt: 10%   // skill prompts + base instructions
  context:      40%   // resolver output goes here
  history:      50%   // prior turns + compacted memory
  responseHeadroom: max(2k, 10%) // room for the model to write
}

For a 200K-token Claude Sonnet, that's ~80K tokens for context. For an 8K local Llama, it's ~2K. The resolver respects whatever fits.

What ends up in the prompt

Items are rendered into the system prompt under a # Context heading, each in a typed block:

# Context

<context source="user_profile">
{ "user_id": "u_123", "tier": "premium", "joined_at": "2025-04-01" }
</context>

<context source="vector_search">
[Document: Refund policy] Customers within 30 days of purchase...
</context>

This format is intentionally explicit so the model can cite the source if asked.

Latency hygiene

TrickWhat it buys
Parallel fetch (default)max(latency) instead of sum(latency)
Per-source timeoutsOne slow source can't pin the response
Pre-warm on session openFetch profile & recent messages while the user is typing
Lazy retrieval via toolsDon't pre-load — let the model ask via a search_* tool

The lazy retrieval pattern is especially powerful: it pushes context size down 5–10x because the model only fetches what it needs.

Observability

Every source's success/failure, item count, and latency are logged with requestId + tenantId:

{"level":30,"source":"vector_search","items":7,"ms":86,"msg":"context_source_ok"}
{"level":40,"source":"realtime_balances","err":{"message":"timeout"},"msg":"context_source_failed"}

Use these to spot regressions: a source that quietly stops returning hits will tank quality without throwing.

On this page