Build an Ask-the-docs chat
Recipe for a grounded RAG docs chat using AgentsKit Chat on the AgentsKit foundation. For the product chat framework home, start at chat.agentskit.io.
Ownership. Product chat applications and the shared Ask shell live in AgentsKit Chat. This cookbook is a foundation-side recipe that dogfoods Chat on top of
@agentskit/*(adapters, memory, RAG). Prefer the Chat docs when you want the versioned multi-surface application layer.
This is the recipe for the chat you're using right now: a grounded RAG assistant over your own docs that streams concise, cited answers β running at $0 on the OpenRouter free pool, composed with AgentsKit Chat over the AgentsKit foundation.
The production widget dogfoods the public AgentsKit Chat framework. AgentChat owns the canonical message timeline, streaming lifecycle, cancellation, retry/edit/regenerate behavior, persistence through AgentsKit memory, component validation, and accessible rendering diagnostics. The docs host keeps only its corpus branding, Markdown slot, generated local knowledge, and a small adapter that converts the existing Ask NDJSON boundary into ordered assistant content. Citations use the standard source-list contract rather than a site-private citation component. Known exact questions now use the deterministic local answer plane before this RAG path, so they require no backend request.
import { createAssistantContentEncoder } from '@agentskit/chat/protocol'
const content = createAssistantContentEncoder()
yield { type: 'text', content: content.encode({ kind: 'text', text: answerChunk }) }
yield { type: 'text', content: content.encode({
kind: 'component',
frame: { componentKey: 'source-list', /* validated sources + fallback */ },
}) }Every text delta passes through the encoder, so model output cannot manufacture component framing. Unknown tools stay inert, and malformed sources never reach the standard renderer.
Two ways in: scaffold it with the CLI, or wire it by hand.
#Fastest path β the CLI
npx agentskit add docs-chatdocs-chat is a UI component, not just an agent file β agentskit add now installs it via the component registry flow defined in RFC-0006.
#What the command actually does
Scan β if .agentskit/components.json is absent, the CLI scans your project: UI binding (react, svelte, vue, β¦), meta-framework (next-app, next-pages, sveltekit, nuxt, remix, tanstack-start, vite, and more), package manager, TypeScript vs JavaScript, monorepo root. Ambiguous signals surface as validation errors β never a silent guess. With components.json committed (written once by agentskit init), the scan is skipped entirely and every add runs non-interactively.
Validate β before anything is written the CLI checks compatibility and refuses with a concrete error plus a pointer to a supported alternative if the detected environment can't satisfy the component's runtimeRequirement or embeddingBackend (for example, onnx-node is blocked on edge or Expo targets). Peer-range conflicts across all resolved dependencies are surfaced in aggregate, not on first hit. Pass --dry-run to see the full plan β files, targets, deps, env, conflicts β without writing anything.
Place β files land in framework-correct locations. The server handler is placed per serverTargetByMeta[metaFramework]: a Next.js App Router route handler (app/api/ask/route.ts), a SvelteKit +server.ts, a Nuxt server/api/*.post.ts, a Remix resource route, and so on. The client component composes the matching @agentskit/* binding. You own every file β edit guardrails, styling tokens, and adapter choices freely.
Record β the installer appends a tamper-evident entry to .agentskit/install-log.jsonl and updates the installed marker in components.json with per-file SHA-256 and the pinned registry ref, enabling agentskit diff docs-chat and agentskit update docs-chat later.
#Safety guarantees
- Per-file SHA-256 verification β every fetched file is verified against the signed manifest before any write. A mismatch aborts the entire install.
- Path-containment guard β
path.resolve(dest)is asserted to stay inside the target directory on every file, both on the write path and ondiffreads. A../../.env-style path escape is anIntegrityError. - Transactional (all-or-nothing) β files are staged in a sibling temp directory, verified, then moved atomically. Any pre-commit failure rolls back all partial writes and reports "rolled back N files." Your tree is never left dirty.
- Append-only audit log β
.agentskit/install-log.jsonlchains entries viaprevEntryHash(SHA-256 of the prior entry); a futureagentskit auditcommand walks the chain and fails on any gap or mismatch.
#Framework support
The first shipping port is React Γ Next.js (app router). Other frameworks (sveltekit, nuxt, remix, tanstack-start, angular, expo, ink) are rolling out port-by-port, each gated by the binding stability requirements in RFC-0004. The CLI will refuse to install a port that has not shipped β it will not copy broken source into an unsupported framework.
#Zero-prompts via agentskit init
npx agentskit init # writes .agentskit/components.json β commit this file
npx agentskit add docs-chatAfter init, every subsequent add reads the committed config and runs non-interactively. In CI pass --yes to exit non-zero on any blocker instead of hanging.
#After install
The ready output prints a per-framework usage snippet wiring createAskHandler to your retriever and adapter, the required env vars (written to .env.example), and a "run the indexer before first use" step. The installer copies an indexer (agentskit ask index ./docs) and an empty index stub β it never ships AgentsKit's own corpus.
Point the handler at your retriever and adapter as shown in the sections below, then run the indexer. A full runnable version lives in apps/example-rag-chat β swap the sample docs for yours.
#1. Index your docs (RAG)
Chunk + embed your docs once, into any @agentskit/memory vector store. Embedding stays free + local with an ONNX model:
import { createRAG } from '@agentskit/rag'
import { fileVectorMemory } from '@agentskit/memory'
import { pipeline } from '@huggingface/transformers'
// Local ONNX embedder β $0, no API key.
let extractor: Awaited<ReturnType<typeof pipeline>> | null = null
const embed = async (text: string): Promise<number[]> => {
extractor ??= await pipeline('feature-extraction', 'Xenova/bge-small-en-v1.5')
const out = await extractor(text, { pooling: 'mean', normalize: true })
return Array.from(out.data as Float32Array)
}
const rag = createRAG({ embed, store: fileVectorMemory({ path: './docs-index' }), topK: 6 })
await rag.ingest(docs) // docs: { id, content, metadata: { path, title } }[]For a committed, read-only index (great for serverless), generate it at build time and ship the JSON β see the docs-site's scripts/gen-ask-index.mjs.
#2. Stream grounded, cited answers
createRAG returns a Retriever β drop it straight into an AgentsKit adapter consumed by AgentChat (or into useChat / createRuntime for a lower-level surface). Two design choices make it reliable on free models:
- Co-locate the context with the question (free models attend to recent tokens far better than a long system prompt).
- Emit citations from what you retrieved β don't depend on a weak model to call a
citetool.
import { useChat } from '@agentskit/react'
import { openrouter, createFallbackAdapter } from '@agentskit/adapters'
const FREE = ['meta-llama/llama-3.3-70b-instruct:free', 'qwen/qwen3-next-80b-a3b-instruct:free']
const adapter = createFallbackAdapter(
FREE.map((model) => ({ id: model, adapter: openrouter({ apiKey, model }) })),
)
const chat = useChat({
adapter,
retriever: rag,
systemPrompt: 'Answer ONLY from the provided docs. Be concise. Decline + name the nearest page when uncovered. Never mention other frameworks.',
})createFallbackAdapter cascades across the free pool when one is rate-limited (429). Keep answers short and AgentsKit-specific with a tight system prompt.
#3. The widget (optional, fancy)
The floating chat is a headless, slotted component β open by default, branded logo, animated loading, and a "build this" link, all overridable:
<AskDocsWidget
logo={<YourMark />} // header logo slot
loadingState={<YourSpinner />} // loading slot
title="Ask our docs"
docsHref="/docs/cookbook/ask-the-docs"
/>Generative UI (option buttons, forms, runnable code) activates with ASK_RICH_UI=1 and a capable model β free models stay on reliable markdown text.
#Guardrails (built in)
Before any model call, a cheap triage runs β it saves your free-tier quota on trivia and blocks the obvious attacks:
- Greetings ("hi", "oi") and noise ("test", empty, gibberish) β an instant canned reply, no LLM.
- Prompt injection ("ignore previous instructions", "reveal your system prompt", "you are nowβ¦", "jailbreak") β a firm decline that never changes role or leaks the prompt β no LLM.
- Real questions (even one word like "memory") fall through.
It's additive and extensible β keep the defenses, add your own:
import { triageMessage } from './lib/ask/guard'
const triage = triageMessage(userText, {
greetings: ['salut', 'ciao'],
noise: ['blah'],
injectionPatterns: [/give me the raw config/i],
replies: { greeting: 'Hey! Ask me about our product docs.' },
})
if (triage.kind === 'canned') return stream(triage.reply) // skip the modelThis sits on top of the other layers: client system messages are stripped, the
retrieved context is fenced as untrusted data, and the grounded prompt keeps
answers on-topic β defense-in-depth.
#Going further
- Durable rate limit β front the route with Upstash (in-memory fallback for dev).
- Eval it β measure retrieval
recall@k/MRRdeterministically + an LLM judge; gate in CI. - Run code in-browser β
@agentskit/sandbox/webwebWorkerBackend(zero-vendor) powers runnable snippets.
That's the whole thing: index β ground β stream β cite. Grounding quality scales with model quality β start free, bring your own key when you want the rich UI.
Explore nearby
- PeerCookbook
Copy-paste recipes for the things every agent app needs. Each recipe stands on its own.
- PeerStreaming chat
useChat + abort + back-pressure. The minimum viable streaming chat, production-ready.
- PeerTools + memory together
The "chat with state and actions" loop β persistent memory plus tool execution.