Guide · v0.0.1
Integrate your app with Bela
This is the guide for wiring Bela into your own web app, standalone — no other context needed. It walks through install, defining entities and tools, creating the agent, handling outcomes in a chat UI, and writing good utterances.
Status. Bela is currently at v0.0.1 (P1 + P2 + P3 + P4 — core skeleton, self-trained neural fallback, dialogue safety, and CLI/packaging): the deterministic matching pipeline (templates + fuzzy entity resolution + date/number extraction) is real and tested; a small self-trained neural intent/slot model can be layered on top as a fallback for paraphrases the templates can't match; handle(text, ctx) carries per-user session state — missing-slot/ambiguous follow-ups, enforced confirm: true gating, roles-based permission checks, and an audit callback — and a real bela CLI (init/train/eval) wraps the training/eval workflow below.
On npm today as bela-ai — npm install bela-ai — see Current limitations and the CLI reference's "today vs. in-repo dev" note.
0. The CLI workflow (recommended path)
Most projects don't need to call trainModel/saveModel/loadModel directly — the bela CLI wraps the same functions with a gate check and a flag interface, and is the recommended way to train and evaluate a model:
bela init ./my-agent $EDITOR ./my-agent/agent.tools.mjs# write your real entities/toolsbela train --config ./my-agent/agent.tools.mjs --out ./my-agent/bela-model.json bela eval --config ./my-agent/agent.tools.mjs --model ./my-agent/bela-model.json
bela train runs trainModel, checks the result against meetsGate (defaults: intentAcc>=0.9, slotExact>=0.8), and only calls saveModel if the gate passes (or --force is given) — so a config change that tanks model quality can't silently ship a bad artifact. Full flag reference, exit codes, and a worked session: the CLI reference.
The rest of this guide (§7–§8) covers the programmatic API (trainModel/saveModel/loadModel/makeFallback from @bela/model directly) — reach for it if you need custom training options, a build-step integration the CLI doesn't cover, or to call createAgent(config, { fallback }) in your app, which the CLI doesn't do for you.
1. Install
Bela isn't published to npm yet. Today, @bela/core is a workspace package you pull in via the monorepo/git, not npm install @bela/core.
If your app lives alongside the Bela repo (or as a git submodule / cloned sibling), add it as a workspace dependency:
{
"dependencies": {
"@bela/core": "workspace:*"
}
}
Or, until it's published, point at the git repo/commit directly in your lockfile-friendly package manager of choice. The bela CLI (init/train/eval, see §0 above and the CLI reference) is built and tested, but a real npm release of @bela/cli — so npx bela init works outside this monorepo — is still a P5 roadmap item.
2. Define your entities
Entities are the "nouns" your app cares about — rooms, menu items, guests, whatever your tools need to resolve free text against. Each entity has:
resolve()— a function (sync or async) returning the current rows. This is where your Prisma/SQL/API call goes.match— which fields on each row to fuzzy-match user text against.
import { defineAgent } from "@bela/core"
const config = defineAgent({
entities: {
Room: {
resolve: () => prisma.room.findMany({ select: { id: true, number: true } }),
match: ["number"],
},
MenuItem: {
resolve: () => prisma.menuItem.findMany(),
match: ["name", "aliases"],
},
Guest: {
resolve: () => prisma.guest.findMany(),
match: ["name"],
},
},
tools: [ /* see below */ ],
})
Every row needs an id (string | number) — that's what gets handed to your tool's run handler once a slot resolves.
3. Define your tools
A tool is one command Bela can execute. It has:
params— a map of param name → param spec. Built with the helper functionsentity(entityName),number({ default? }), ordateRange().utterances— one or more command templates with{placeholder}markers matching your param names. Provide a few phrasings/synonyms; entity matching is script-agnostic, so Urdu/Roman-Urdu variants work too.confirm(optional boolean) — marks a sensitive action as requiring a yes/no confirmation before executing. This is enforced: a fresh match against aconfirm: truetool never runs immediately — it returns aconfirmoutcome and pauses in a per-user session until the next message from that sameuserIdis an explicit yes (yes,y,ok,okay,confirm,sure, or the Roman-Urduhaan/han/ji) or no (no,n,cancel,nahi,nah). Anything else is treated as a brand new command. There is no way to skip the confirmation step.roles(optionalstring[]) — restricts a tool to callers whosectx.role(seehandle(text, ctx)below) is in the list. Noroles(or an empty array) means anyone can call it. A disallowed or missing role returns adeniedoutcome instead of running the tool.run(args)— your own function or API call. Business logic stays in your app; Bela just gets you validated, resolved arguments.
import { tool, entity, number, dateRange } from "@bela/core"
const orderFood = tool("orderFood", {
params: { qty: number({ default: 1 }), item: entity("MenuItem"), room: entity("Room") },
utterances: [
"order {qty} {item} for room {room}",
"order {item} for room {room}",
"{item} for room {room}",
],
run: ({ item, room, qty }) => api.post("/api/orders", { itemId: item.id, roomId: room.id, qty }),
})
const cancelBooking = tool("cancelBooking", {
params: { guest: entity("Guest") },
utterances: ["cancel {guest} booking", "cancel booking for {guest}"],
confirm: true, // always pauses for an explicit yes/no before running
run: ({ guest }) => bookings.cancel(guest.id),
})
const closeDay = tool("closeDay", {
params: {},
utterances: ["close the day", "close day"],
roles: ["manager"], // only handle(text, { role: "manager" }) can run this
run: () => shift.close(),
})
defineAgent validates your config at build time and throws if:
- two tools share a name,
- a tool references an entity that isn't declared,
- an utterance has a
{placeholder}that isn't a declared param.
4. Create the agent
import { createAgent, defineAgent } from "@bela/core"
const agent = createAgent(defineAgent({ entities, tools }))
await agent.sync() // pulls fresh entity rows via each entity's resolve()
Call agent.sync() on startup and whenever your underlying data changes meaningfully (new rooms, menu changes, etc.) — it's what refreshes the in-memory knowledge base the fuzzy resolver matches against. There's no background scheduler in the engine yet; your app decides when to re-sync.
createAgent(config, opts?) takes:
now?: () => Date— inject a fixed clock (useful in tests — seefixtures/hotel/src/hotel.ts). Also stampsAuditEntry.at(see below).fallback?: FallbackFn— the P2 neural fallback, see §7.audit?: (e: AuditEntry) => void— called after everyexecuted,error,denied, andcancelledoutcome. See §8.sessionTtlMs?: number(default300_000= 5 minutes) — how long a pendingneed_slot/ambiguous/confirmsession stays alive peruserIdbefore it's treated as expired and the next message starts a fresh command.
5. Handle commands
const outcome = await agent.handle(userText, { userId: "u1", role: "manager" })
handle(text, ctx?) takes an optional HandleContext:
export type HandleContext = { userId?: string; role?: string }
userId— identifies the caller for session/dialogue state (see below). Defaults to"default"if omitted, so single-user/no-session callers can keep callingagent.handle(text)with no second argument. If your app has more than one concurrent user, always pass a realuserId— every in-flightneed_slot/ambiguous/confirmfollow-up is keyed on it, and two users sharing the default key will stomp on each other's pending dialogue.role— checked against a tool'sroleslist, if it has one (see §3).
handle returns a discriminated union (Outcome, exported from @bela/core) — branch on outcome.type in your chat UI:
export type Outcome =
| { type: "executed"; tool: string; args: Record<string, unknown>; result: unknown; reply: string }
| { type: "need_slot"; tool: string; param: string; reply: string }
| { type: "ambiguous"; tool: string; param: string; options: EntityRow[]; reply: string }
| { type: "confirm"; tool: string; args: Record<string, unknown>; reply: string }
| { type: "cancelled"; tool: string; reply: string }
| { type: "denied"; tool: string; reply: string }
| { type: "unknown"; reply: string }
| { type: "error"; tool: string; reply: string; detail: string }
What to do with each:
outcome.type | What happened | What your UI does |
|---|---|---|
executed | The tool ran. | Show outcome.reply (e.g. "Done — orderFood."). outcome.result is whatever your run handler returned, if you want more detail. |
need_slot | A required param is still missing/unresolved. | Show outcome.reply (e.g. "Which room?") as a follow-up question. Just relay the next raw message from the same userId back into handle() — the session remembers what was already filled in. |
ambiguous | The entity text matched more than one row (e.g. two guests named "John"). | Show outcome.reply, which already lists the candidates as 1) … 2) …. The user can answer with the number or with enough of the name to be unique; either resumes the same session. |
confirm | The matched tool has confirm: true. | Show outcome.reply (e.g. "Confirm cancelBooking: guest=John Smith? (yes/no)") and wait for the same userId's next message. Nothing has run yet — outcome.args are the resolved args that would run. |
cancelled | The user replied "no" (or nahi/nah/cancel/n) to a confirm prompt. | Show outcome.reply ("Okay, cancelled."). Nothing ran; an audit entry with outcome: "cancelled" was recorded if you passed an audit callback. |
denied | The tool has a roles list and ctx.role isn't in it. | Show outcome.reply ("Sorry, you're not allowed to do that."). An audit entry with outcome: "denied" was recorded. |
unknown | Nothing matched any tool template (and the fallback, if any, also missed). | Show outcome.reply verbatim — a "here's what I can do" list built from each tool's first utterance. This is the intended "no chit-chat" fallback. |
error | A tool's run handler threw. | Show outcome.reply (a safe templated message — your handler's original error never leaks to the user). Log outcome.tool/outcome.detail server-side. An audit entry with outcome: "error" was recorded. |
Sessions, follow-up turns, and the numbered-reply format
need_slot, ambiguous, and confirm all set a pending session for that userId — the next call to handle() from the same userId is checked against that pending state before anything else:
- After
need_slot, any free-text reply is used to fill the missing param and re-resolution runs (it may still come backneed_slotagain for a different param, orambiguous, or execute/confirm). - After
ambiguous, reply with the option's number ("1","2", …, as listed inoutcome.reply) or with enough of the name/value to uniquely identify one row. An unrecognized reply keeps the sameambiguoussession open rather than dropping it. - After
confirm, only an explicit yes/no resolves it (see §3). Anything else is treated as a brand new command — the pending confirm is dropped, so a user can always escape a stuck confirmation by typing a fresh command. - If the next message from that
userIdis actually a full new command that matches a template on its own, it takes priority over a pendingneed_slot/ambiguoussession (the old session is dropped) — so users aren't stuck if they change their mind mid-follow-up. - Sessions expire after
sessionTtlMs(default 5 minutes) of inactivity peruserId; an expired session is discarded and the next message is treated as fresh. - There's no cross-process session store — sessions live in the
createAgentinstance's memory. If you run multiple Node processes/instances behind a load balancer, either pin a user's requests to one instance or keep dialogue turns short enough that TTL/process lifetime aren't a concern.
6. Writing good utterances
- Keep templates short — command phrasing, not sentences ("order {item} for room {room}", not "could you please order {item} for room {room}").
- Provide a few phrasings per tool, including any shorthand your users actually type ("checkout msg room {room}" alongside "send checkout message to room {room}").
- Param kinds:
entity("EntityName")— matched fuzzily against thematchfields you declared for that entity.number({ default? })— the slot text must contain digits; the extractor is a plain\d+regex, it will not parse spelled-out numbers ("two" won't resolve, "2" will). Give it adefaultfor optional quantities.dateRange()— parsed withchrono-node, so natural phrases like "from 4th jul to 6th jul" work; single dates resolve to a same-day range.
- The last placeholder in a template is greedy (matches
.+); every other placeholder is non-greedy (.+?). Put your most "open-ended" slot (like a free-text item name) last in the template, and put anything you need to bound tightly (like a room number) earlier or usenumber()for it, which always matches only digits regardless of position. - When more than one template matches, the engine prefers the one with more literal (non-slot) characters — so a more specific phrasing beats a vaguer one automatically; you don't need to order utterances by specificity.
7. Enabling the neural fallback
The deterministic matcher (templates + fuzzy entity resolution) is the whole engine in P1. As of P2, you can train a small neural intent/slot model straight from your own defineAgent(...) config and wire it in as a fallback for the phrasings your templates don't cover:
import { defineAgent } from "@bela/core"
import { trainModel, meetsGate, saveModel } from "@bela/model"
const config = defineAgent({ entities, tools })
const { model, metrics } = await trainModel(config, { seed: 42, perTool: 200, epochs: 12 })
if (!meetsGate(metrics)) throw new Error("model below quality gate — check templates/coverage")
saveModel(model, "./bela-model.json")
import { defineAgent, createAgent } from "@bela/core"
import { loadModel, makeFallback } from "@bela/model"
const config = defineAgent({ entities, tools })
const model = loadModel("./bela-model.json")
const agent = createAgent(config, { fallback: makeFallback(model, 0.6) })
await agent.sync()
await agent.handle("get karahi in room 4") // paraphrase your templates never listed
How it fits together:
trainModel(config, opts?)synthesizes training/test examples from your entities and tool utterances (including built-in synonym augmentation — "order" also generates "get"/"bring"/"want" variants, etc.), trains a tiny from-scratch intent classifier and slot tagger, and returns{ model, metrics }.optslets you tuneseed,perTool(examples generated per tool),epochs,lr,buckets,dim— the defaults are reasonable starting points.meetsGate(metrics, min?)is a pass/fail check (defaults:intentAcc >= 0.9andslotExact >= 0.8) — use it in your training script/CI to catch a config change that made the model unusably bad before you ship it.saveModel(model, path)/loadModel(path)serialize the trained weights to/from a JSON file. The artifact is small — expect roughly a few MB depending on yourbuckets/dimsettings and vocabulary size — so it's fine to commit or ship alongside your app.loadModelvalidates the artifact's shape (version, array types, and that every embedding/weight/bias array is exactly the expected length) and throws a clearinvalid model artifact: …error on a corrupted or hand-edited file, instead of failing deep inside prediction with an opaque error.makeFallback(model, minConfidence?)(default0.7) wraps a loaded model into theFallbackFnshapecreateAgentexpects. Pass it as{ fallback }increateAgent(config, opts). It only runs when the deterministic template matcher finds no match at all — a matched template that then fails entity resolution still returnsneed_slot/ambiguous, it does not fall through to the neural model.- Retrain when your config or data changes — new tools, new utterances, or a meaningfully different set of entity rows (new menu items, rooms, etc.) all change what the model was trained to recognize. There's no online/incremental learning yet; training is a from-scratch batch step that runs in-process in seconds on a single CPU core for a fixture-sized config. Re-run it as part of your deploy/build step, gate it with
meetsGate, and ship the resulting artifact. - See the full wiring in one file: the reference hotel fixture (
fixtures/hotel/src/hotel.ts, which exposesmakeState()/hotelConfig(state)/makeHotel(now?, fallback?, state?, audit?)so the same config can be trained on and executed against) and its E2E test (fixtures/hotel/test/neural.test.ts).
8. Roles, the audit log, and teaching Bela with corrections
Roles
Add roles: string[] to a tool (see §3) to restrict who can run it. Pass the caller's role as ctx.role in handle(text, ctx). No role, or a role not in the list, gets a denied outcome instead of execution — the tool never runs and nothing is mutated.
Audit log
Pass audit: (e: AuditEntry) => void to createAgent(config, opts) to record every outcome that isn't a plain question back to the user:
export type AuditEntry = {
at: Date; userId: string; input: string; tool: string
args: Record<string, unknown>
outcome: "executed" | "error" | "denied" | "cancelled"
detail?: string // present on "error", the caught error's String()
}
- Fired for
executed,error,denied, andcancelled— never forneed_slot,ambiguous,confirm, orunknown(nothing happened yet on those, so there's nothing to log). atcomes from the samenowyou passed tocreateAgent(or realDate.now()if you didn't), so it's deterministic in tests.- If your
auditcallback throws, the pipeline swallows the error and continues — a broken logger can never break command handling. - Typical use: append to a durable log/table for compliance, or drive notifications ("manager cancelled booking for John Smith").
CorrectionStore and retraining on real phrasings
If you've wired in the P2 neural fallback (§7) and a real user's phrasing gets misrouted or falls through to unknown/the wrong tool, capture it and feed it back into the next training run instead of hand-tuning utterances forever:
import { CorrectionStore } from "@bela/model"
const corrections = new CorrectionStore("./corrections.jsonl")
// wherever you learn the right answer (support ticket, manual review, a
// "that's wrong, I meant..." UI action) — record the raw text and the tool
// it should have matched:
corrections.add({ text: "get me karahi for room four please", tool: "orderFood" })
CorrectionStore(path)appends/reads newline-delimited JSON (.jsonl) atpath.add(c)appends one line;load()reads all of them back (silently skipping malformed lines), returningCorrection[]({ text: string; tool: string }).- Feed the loaded corrections back into
trainModel:
import { trainModel, saveModel } from "@bela/model"
const corrections = new CorrectionStore("./corrections.jsonl").load()
const { model, metrics } = await trainModel(config, { seed: 42, perTool: 200, epochs: 12, corrections })
saveModel(model, "./bela-model.json")
trainModel'scorrectionsoption is filtered to tools that already exist in your config (a stale correction naming a removed tool is silently dropped) and mixed into the intent classifier's training data alongside the auto-generated examples — it does not affect the slot tagger. This nudges the model toward your real users' phrasing without hand-editing utterance templates.- Retrain periodically (e.g. nightly, or after N new corrections) as part of the same build/deploy step described in §7, and re-check
meetsGate(metrics)before shipping the new artifact.
Bootstrapping corrections with an LLM (scripts/distill.mjs)
Waiting for real misfires is slow, and a brand-new deployment has none. You can bootstrap the same correction file by asking a bigger model — one you host yourself — how your users would actually phrase each command, then training the tiny model on its answers. The big model is used once, offline; the runtime stays templates + the tiny net, with no LLM in the request path.
BELA_LLM_URL=http://127.0.0.1:18795 \ node scripts/distill.mjs --config ./agent.tools.js --out ./corrections.jsonl --n 25
tsx instead of node.For every tool the script sends one prompt containing the tool's name, its utterance templates, its parameter names, and a handful of real entity values pulled from your database via collectEntityValues. It asks for N short command-style phrasings with the slots already filled in, requests strict JSON, retries on a parse failure or a short batch, de-dups within and across tools, and appends each surviving line to the output file as a { text, tool } correction via CorrectionStore. It prints a per-tool count so you can see which tools the model struggled with.
| Flag | Default | Meaning |
|---|---|---|
| --config | — | path to the module exporting your AgentConfig (required) |
| --out | — | corrections .jsonl to append to (required) |
| --n | 25 | phrasings per tool |
| --url | $BELA_LLM_URL | OpenAI-compatible base URL; else http://127.0.0.1:18795 |
| --model | $BELA_LLM_MODEL | model name to request; else local |
| --concurrency | 3 | parallel tools in flight (capped at 4) |
| --temperature | 0.9 | high on purpose — you want variety, not the single best phrasing |
| --retries | 3 | attempts per tool before giving up |
| --tools | all | comma-separated subset, for iterating on one tool |
| --dry | off | print counts without writing the file |
Notes:
- No endpoint is baked in. Pass
--urlor setBELA_LLM_URL; nothing about your infrastructure ends up in the repo. - Read the output before you train on it. It is a plain text file; a model will occasionally invent a phrasing that belongs to a different tool, and that mislabel is worse than no correction at all.
- Small context windows are common on CPU-hosted models. The prompt is kept short deliberately; if you see truncated batches, lower
--n(the script will retry and top up) or raise the server's per-slot context. - Distilled corrections and hand-written ones live in the same file and are merged by
trainModel({ corrections })exactly the same way.
8b. Optional: thinking with a big LLM
If neither the deterministic templates nor the trained neural fallback (§7) catch a phrasing — an odd paraphrase, a rare edge case — you can layer a third, optional fallback that calls out to a real LLM over any OpenAI-compatible /v1/chat/completions endpoint (a local llama.cpp server, for example):
import { defineAgent, createAgent } from "@bela/core"
import { loadModel, makeFallback, makeLlmFallback, describeTools, chainFallbacks } from "@bela/model"
const config = defineAgent({ entities, tools })
const model = loadModel("./bela-model.json")
const neural = makeFallback(model, 0.6)
const llm = makeLlmFallback({
url: "http://127.0.0.1:8080/v1/chat/completions", // any OpenAI-compatible server
tools: describeTools(config), // derives name/params/utterances from your config
timeoutMs: 8000,
})
const agent = createAgent(config, { fallback: chainFallbacks(neural, llm) })
Be honest with yourself about what this buys you:
- Local commands never need it. If your templates and neural fallback cover real usage, this adapter never fires. It exists purely for the long tail of paraphrases/edge cases neither catches.
- It can only suggest, never act.
makeLlmFallbackreturns a{ tool, slots }guess (ornull) — the exact same shape as the neuralFallbackFn. That guess still goes through the full deterministic pipeline: entity resolution,need_slot/ambiguousfollow-ups, enforcedconfirm: truegating, androleschecks. An LLM suggesting aconfirm: truetool still stops at aconfirmoutcome — nothing runs until the same safety gate every other path goes through is satisfied. - It fails closed. Network errors, timeouts, non-200 responses, unparseable JSON, an unknown tool name, or malformed slots all resolve to
null— the adapter never throws and never blocks the pipeline beyondtimeoutMs(default 8000 ms). chainFallbacks(…)picks the first non-null suggestion, so the recommended order ischainFallbacks(neural, llm)— try the fast local model first, and only pay the network round-trip to the LLM if it misses.- Works with any OpenAI-compatible endpoint —
llama.cpp's server mode, vLLM, Ollama's OpenAI-compatible route, or a hosted API, as long as it speaks the standard/v1/chat/completionsrequest/response shape.
9. Current limitations (be honest with your users)
- Deterministic matching first, neural fallback second. Template + fuzzy-match coverage still depends entirely on the utterance templates you write; the neural fallback (P2, see above) only engages when no template matches at all, and is opt-in — you must train, gate, and wire it in yourself, it isn't automatic.
- English command structure, any-language entity names. Templates and literal words are normalized/matched as English-shaped text; entity values (room numbers, guest names, menu item names/aliases) can be in any script — match fields are just strings. Urdu digits are normalized to ASCII digits automatically.
- In-memory sessions only. Dialogue state (
need_slot/ambiguous/confirm) lives in thecreateAgentinstance's process memory, keyed byuserId, with a TTL (default 5 minutes) — see §5. There's no shared/persistent session store across multiple processes yet. - In-process only. Bela runs inside your app's Node process today; there is no HTTP sidecar mode yet (see the FAQ below).
- No incremental/online learning.
trainModel({ corrections })(§8) still runs a full from-scratch retrain each time — there's no way to nudge a live model in place without retraining and reshipping it.
FAQ
My app isn't Node (PHP / WordPress / Laravel / Django)?
Not supported yet, but it's an explicit roadmap item: an HTTP sidecar mode where Bela runs as its own small process, tools are declared as REST endpoints (URL + params + auth header) in a JSON/YAML config, and entity resolvers are read-only HTTP endpoints instead of in-process functions. Same engine, HTTP transport instead of direct function calls — no redesign required, just not built yet.
Does it call an LLM API?
No. No external AI API calls, no per-token cost. The core is deterministic matching in TypeScript, and the optional P2 fallback is a small model you train yourself, from scratch, on your own config and data — it runs and trains on your own VPS in seconds on one CPU core, not a hosted LLM.
Can it write to my database directly?
No, and it never will — Bela can only call tools you explicitly register. run handlers are your own functions or API calls; you keep full control of your business rules and data access.
What happens if a command doesn't match anything?
You get an unknown outcome with a reply listing what the agent can do (from each tool's first utterance) — never a guess, never chit-chat.
Is this production-ready?
It's v0.0.1 — the deterministic engine, the neural fallback, dialogue safety (sessions, enforced confirm: true, roles, audit), and the bela CLI (init/train/eval) all work and are tested end-to-end (see fixtures/hotel/ and packages/cli/test/). An npm release and a real production deployment are still ahead (P5).
Start Bela on day one (greenfield apps)
The cheapest moment to integrate Bela is while the app is being built. The pattern that worked in production:
- Install at project start —
bela initnext to your first migration. The starter config is youragent.tools.tsseed. - One feature, one tool. Every time you ship a user-facing action (an API route or server action), add its Bela tool in the same pull request — the tool is usually 15 lines: params, five utterances (write them in the languages your users actually type), and a
runthat calls the function you just wrote. While the feature is fresh in your head, the utterances write themselves. - Mirror your permission table. If your app has a roles/permissions module, derive each tool's
rolesfrom it (roles: rolesFor("order:create")) instead of hand-writing lists — Bela then can never allow what the app forbids, and stays in sync forever. Rule of thumb: whatever a user can do in the app with their role, they can do through Bela — no more, no less. - Retrain in CI.
bela trainafter schema/config changes; the accuracy gate fails the build if the config got confusing. Keep agolden.mjsof real user phrases and replay it (dry-run) before deploys. - Ship the memory file empty.
bela-memory.mdbelongs to the customer. The phrases and aliases they add are the difference between a demo and their agent.