Overview
Every LLM call in the Grid should go through the brain - not directly to Anthropic, Ollama, or any provider. The brain is the routing layer that selects the best available model, enforces tenant entitlements, meters usage, and handles failover. @arcgrid/brain-client is the only entry point your code needs.
The SDK lives in agent-space-co/brain on branch feat/brain-sdk (PR #35). It is not yet published to a registry. Install from the repo directly until #35 merges and is published.
Install
Until the package is published to GitHub Packages, install from the brain repo directly:
npm install git+ssh://git@github.com/agent-space-co/brain.git#feat/brain-sdk:sdkFor local development inside the brain monorepo:
npm install ../../brain/sdkOnce PR #35 merges and the package is published:
npm install @arcgrid/brain-clientQuick start
import { BrainClient } from '@arcgrid/brain-client';
const brain = new BrainClient({
apiKey: process.env.BRAIN_API_KEY!,
// baseUrl defaults to process.env.BRAIN_API_URL or http://arcrouter:4000
});
// Non-streaming
const reply = await brain.chat({
messages: [{ role: 'user', content: 'Summarize this in one sentence.' }],
});
console.log(reply.choices[0].message.content);
// Streaming
for await (const chunk of brain.stream({
messages: [{ role: 'user', content: 'Walk me through this step by step.' }],
model: 'deep',
})) {
process.stdout.write(chunk.choices[0]?.delta.content ?? '');
}Chat
Non-streaming completion. Returns a ChatCompletion when the model finishes.
const result = await brain.chat({
model: 'standard', // effort alias: trivial | minimal | standard | deep
messages: [
{ role: 'system', content: 'You are a concise assistant.' },
{ role: 'user', content: 'What is ArcCycle?' },
],
module_id: 'core.chat', // entitlement gate - required for licensed modules
max_tokens: 512,
temperature: 0.3,
});
console.log(result.choices[0].message.content);
console.log(result.usage);
// { prompt_tokens: 24, completion_tokens: 91, total_tokens: 115 }Constructor defaults
Set defaultModuleId and defaultAttribution once on the client instance rather than per-call:
const brain = new BrainClient({
apiKey: process.env.BRAIN_API_KEY!,
defaultModuleId: 'surface.assistant',
defaultAttribution: { agent_id: 'surface', flow_id: 'ticket-triage' },
});Streaming
Returns an async generator that yields ChatCompletionChunk objects as SSE frames arrive. Use when you need to display output as it generates.
let full = '';
for await (const chunk of brain.stream({ messages, model: 'deep' })) {
const delta = chunk.choices[0]?.delta.content ?? '';
full += delta;
process.stdout.write(delta);
}
console.log('final:', full);Streams internally, returns the full ChatCompletion when done. Use when you want streaming latency (time-to-first-token) but a single awaitable result.
const result = await brain.streamCollect({ messages });
console.log(result.choices[0].message.content);Entitlement check
Check whether the current tenant is entitled to a module before making a call. Useful to gate UI features without attempting a full request.
const check = await brain.checkEntitlement('vision.hair.transfer');
if (!check.allowed) {
// check.reason: 'not_entitled' | 'tenant_frozen' | 'expired' | ...
console.log('module not available:', check.reason);
}ArcRouter also enforces this on every chat() call when module_id is set - checkEntitlement() is for pre-flight checks in UI or routing logic.
User memory
brain.memory.* reads and writes per-user profile state within the tenant namespace. The userId is always an opaque token your app controls - never an email, phone number, or any PII. The brain stores and returns the profile without interpreting its shape.
Profiles with data_class: 'BIOMETRIC' require a valid consent record. Call brain.consent.grant() first and pass the returned id as consent_reference. A missing or revoked consent returns 403.
getProfile
const profile = await brain.memory.getProfile('u_a1b2c3');
if (profile) {
console.log(profile.data_class); // STANDARD | HEALTH | BIOMETRIC
console.log(profile.profile); // your app's stored object
console.log(profile.updated_at);
}upsertProfile
// STANDARD profile - no consent needed
await brain.memory.upsertProfile('u_a1b2c3', {
data_class: 'STANDARD',
profile: {
preferred_language: 'en',
onboarding_complete: true,
last_session_id: 'sess_xyz',
},
});
// BIOMETRIC profile - consent reference required
const consent = await brain.consent.grant({
user_id: 'u_a1b2c3',
data_class: 'BIOMETRIC',
purpose: 'hair_visualization',
consent_text_hash: sha256(disclosureText),
});
await brain.memory.upsertProfile('u_a1b2c3', {
data_class: 'BIOMETRIC',
consent_reference: consent.id,
profile: {
hair_type: '4C',
last_scan_at: new Date().toISOString(),
},
});deleteProfile
Deletes the profile and all history, then writes a deletion attestation. Required for right-to-deletion requests under CCPA/CPRA.
const result = await brain.memory.deleteProfile('u_a1b2c3');
console.log(result.deleted); // true
console.log(result.data_class); // what class was deletedexportProfile
Returns the current profile plus immutable audit history. Use for GDPR/CCPA data subject access requests.
const export_ = await brain.memory.exportProfile('u_a1b2c3');
console.log(export_.profile); // current state
console.log(export_.history);
// [ { snapshot: {...}, recorded_at: '2026-09-23T...' }, ... ]Consent
brain.consent.* manages consent records required before writing BIOMETRIC or HEALTH data. This is the BIPA (Illinois) and CPRA (California) compliance layer. The consent_text_hash must be the SHA-256 of the exact disclosure text shown to the user - this proves the user saw a specific version of the disclosure.
grant
import crypto from 'node:crypto';
const disclosureText =
'By continuing, you agree to allow ArcGrid to collect and store ' +
'biometric data (hair imagery) to personalize your experience. ' +
'Data is retained for up to 3 years and deleted on request.';
const hash = crypto
.createHash('sha256')
.update(disclosureText)
.digest('hex');
const record = await brain.consent.grant({
user_id: 'u_a1b2c3',
data_class: 'BIOMETRIC',
purpose: 'hair_visualization',
consent_text_hash: hash,
expires_at: new Date(
Date.now() + 3 * 365 * 24 * 60 * 60 * 1000 // 3 years - BIPA max
).toISOString(),
evidence: {
ui_version: '2.1.0',
locale: 'en-US',
screen: 'onboarding/consent',
},
});
// record.id is the consent_reference you pass to upsertProfile
console.log(record.id, record.status);get
Returns all consent records for a user - granted, revoked, and expired.
const records = await brain.consent.get('u_a1b2c3');
const active = records.filter(r => r.status === 'granted');
console.log(`${active.length} active consent(s)`);revoke
await brain.consent.revoke('u_a1b2c3', 'hair_visualization');
// Status changes to 'revoked'. Future upsertProfile(BIOMETRIC) with
// this consent_reference will return 403.Model aliases
Pass an effort alias as model. ArcRouter resolves it to the best available descriptor given the tenant tier, license class, and current rig availability.
| Alias | Intended use | Current mapping |
|---|---|---|
trivial | Classification, tagging, yes/no decisions | llama3.1:8b (ArcRig1/2) |
minimal | Short answers, formatting, extraction | qwen2.5-coder or llama |
standard | General tasks - default when model is omitted | qwen3-fast (ArcRig3) |
deep | Complex reasoning, long-form, architecture | Claude (frontier fallback) |
To pin to a specific descriptor, pass its full id instead of an alias: model: 'arcrig3/qwen3-fast'.
Attribution
Pass ledger fields to track which agent, flow, or pack generated each request. ArcRouter records these in the usage ledger for cost attribution, debugging, and billing. All fields are optional - the router fills from the key record when omitted.
await brain.chat({
messages,
attribution: {
agent_id: 'trebek',
flow_id: 'ticket-triage-v2',
node_id: 'classify',
client_id: 'beyond-sleep',
matter_id: 'ticket-4821',
},
});Errors
All errors from the brain surface as BrainError instances with status (HTTP status), type (error code), and message.
import { BrainError } from '@arcgrid/brain-client';
try {
await brain.chat({ messages, module_id: 'vision.hair.transfer' });
} catch (err) {
if (err instanceof BrainError) {
console.log(err.status); // 403
console.log(err.type); // 'not_entitled'
console.log(err.message);
}
}| Type | Status | Cause |
|---|---|---|
not_entitled | 403 | Tenant not entitled to the requested module_id |
license_class_denied | 403 | All candidate models are research_only for this tenant tier |
consent_required | 422 | BIOMETRIC upsert attempted without consent_reference |
consent_denied | 403 | Consent record not found or revoked |
rate_limited | 429 | Request rate or token budget exceeded |
no_candidates | 503 | No models match routing criteria or all rigs down |
unauthorized | 401 | Invalid or missing API key |
Internal wiring
The brain is the language layer for every intelligent feature in the Grid. Two wiring targets are pending:
Surface backend
Create a shared brain client instance used across all Surface API routes that need LLM calls. Wire it once at startup:
// surface/src/lib/brain.ts
import { BrainClient } from '@arcgrid/brain-client';
export const brain = new BrainClient({
apiKey: process.env.BRAIN_API_KEY!,
defaultAttribution: { agent_id: 'surface' },
defaultModuleId: 'surface.assistant',
});
// In any API route:
const result = await brain.chat({
messages: buildMessages(context),
model: 'standard',
});Trebek
Replace Trebek's direct Anthropic call with brain.chat(). The neo4j MCP connection stays unchanged - only the language layer changes. This means Trebek calls are metered, logged in the ledger, and can be rerouted to local models for client privacy sessions.
// Before: direct Anthropic call
const res = await anthropic.messages.create({ model: 'claude-opus-...', messages });
// After: through the brain (same interface, all routing handled by ArcRouter)
const res = await brain.chat({ model: 'standard', messages, module_id: 'trebek.route' });ArcRouter's routing table controls which provider serves each alias. Switching between on-prem ArcRig, cloud self-hosted, and frontier API requires only a routing-table config change - no application code changes needed. This is how we protect client data: T2/T3 client calls never hit an external API unless explicitly configured.
Environment variables
| Variable | Default | Purpose |
|---|---|---|
BRAIN_API_KEY | - | Set on the BrainClient constructor. Required. |
BRAIN_API_URL | http://arcrouter:4000 | Override for local dev: http://localhost:4000 |
Type reference
// All exports from @arcgrid/brain-client
import {
BrainClient,
BrainError,
} from '@arcgrid/brain-client';
import type {
// LLM
ChatParams,
ChatCompletion,
ChatCompletionChunk,
StreamDelta,
ChatChoice,
Message,
ContentPart,
TextPart,
ImageUrlPart,
ToolCall,
ToolDefinition,
ToolChoice,
Usage,
EffortAlias,
Role,
Attribution,
EntitlementCheck,
// Memory
UserProfile,
UpsertProfileInput,
ProfileExport,
DataClass,
// Consent
ConsentRecord,
GrantConsentInput,
// Client
BrainClientOptions,
} from '@arcgrid/brain-client';