AgentsKit Chat Alpha

Hosted and self-hosted Ask backend

View canonical Markdown

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 local or federated mode;
  • 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 AbortSignal into 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.snapshots is 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.