Hosted and self-hosted Ask backend
Hosted and self-hosted Ask backend
createAskServiceHandler is the production boundary for semantic questions that the deterministic plane cannot answer. It accepts one public protocol in every deployment and derives all authority server-side.
flowchart LR
Q["Exact question"] --> D{"Deterministic artifact"}
D -->|"match"| L["Local cited answer"]
D -->|"miss"| H["Trusted Ask handler"]
H --> A["Authenticate and resolve site"]
A --> R["AgentsKit RAG / Retriever"]
R --> G["AgentsKit provider / adapter"]
G --> C["Cited Ask NDJSON"]
C --> P["CAS persistence + private metrics"]
Shared production factory
import { createAskServiceHandler } from '@agentskit/chat/server'
export const ask = createAskServiceHandler({
authenticate: (request, signal) => auth.verify(request, signal),
resolveSite: (identity, signal) => siteRegistry.resolve(identity.siteId, signal),
resolveSubjectId: identity => identity.subjectId,
retrievers: {
local: { retrieve: input => localRag.retrieve(input) },
federated: { retrieve: input => federatedRag.retrieve(input) },
},
generator: providerGenerator,
sessionStore: durableCasStore,
rateLimit: input => limiter.consume(input.site.siteId, input.subjectId, input.signal),
onMetric: metric => telemetry.record(metric),
})
localRag and federatedRag are adapters around the public AgentsKit RAG/Retriever APIs. providerGenerator delegates generation and usage to an AgentsKit provider. The handler never implements embeddings, vector search, ranking, model transport, authentication, or storage.
Hosted route
Web-standard platforms mount the shared factory directly:
import { ask } from './ask-backend.js'
export const POST = ask
Self-hosted Node route
The Node bridge only translates HTTP to Web standards. It does not change the public request or response:
import { createServer } from 'node:http'
import { ask } from './ask-backend.js'
createServer(async (incoming, outgoing) => {
const disconnected = new AbortController()
incoming.once('aborted', () => disconnected.abort())
outgoing.once('close', () => disconnected.abort())
const chunks: Buffer[] = []
for await (const chunk of incoming) chunks.push(Buffer.from(chunk))
const response = await ask(new Request(`http://localhost${incoming.url ?? '/'}`, {
method: incoming.method,
headers: incoming.headers as HeadersInit,
signal: disconnected.signal,
...(chunks.length === 0 ? {} : { body: Buffer.concat(chunks) }),
}))
outgoing.writeHead(response.status, Object.fromEntries(response.headers))
if (response.body === null) return outgoing.end()
for await (const chunk of response.body) outgoing.write(chunk)
outgoing.end()
}).listen(3000)
The package integration tests execute both local and federated configurations through the same handler contract and prove identical events through a direct hosted invocation and a real self-hosted Node streaming bridge.
Site configuration
Each trusted site record declares:
- stable site and assistant identities;
- suggestions shown by the host;
- one authorized corpus and its
localorfederatedmode; - registered components and actions;
- request, retrieval, and generation deadlines;
- maximum citation count;
- required or disabled persistence.
The browser cannot select this record. Legacy corpus and persona query parameters must equal the resolved record or the server returns ASK_FORBIDDEN.
Persistence and replay
Sites using persistence.mode: required must inject a CAS store. Keys contain trusted site and subject IDs plus the validated application session ID. load returns a bounded revisioned record; save receives the expected revision. A concurrent mutation produces ASK_PERSISTENCE_CONFLICT and increments the conflict metric. Provider details and user content never enter the diagnostic.
Operational gates
- Apply rate limits before parsing the request body.
- Keep provider secrets and corpus credentials only in injected server adapters.
- Propagate
AbortSignalinto authentication, rate limiting, retrieval, generation, and persistence. - Require at least one safe citation. Unsupported schemes, credentials in URLs, and malformed sources are dropped.
- Export every backend metric, including zeros.
stream.snapshotsis zero for Ask because its public stream contains events rather than turn snapshots. - Alert on error, cancellation, conflict, and rate-limit ratios; establish latency/token/cost baselines before tuning.
- Never log request bodies, prompts, generated answers, retrieved excerpts, credentials, subject IDs, or session IDs.
See ADR-0026 for the accepted boundary.