Table of Contents
chat_llm: LLM-backed chat engine
chat_llm is the JavaScript chat engine behind Synchronet's LLM-powered
Guru. It turns a large language model (running locally under Ollama, or any
OpenAI-compatible endpoint) into a conversational Guru that can answer
questions, search your message and file bases for grounding (RAG), call
tools, and remember each caller across sessions.
It is transport-agnostic and serves three callers from one engine:
- private — the classic 1:1 Guru page from a terminal session.
- multinode — Guru participation in multi-node channel chat.
- irc — the IRC bot adapter.
The engine lives in exec/chat_llm.js. Its companions are
chat_llm.ini (configuration), the tool
registry, and the RAG index builder. For a
start-to-finish setup walk-through, see Setting up the LLM
Guru.
Enabling it
Two steps, no recompile:
- In SCFG → Chat Features → Artificial Gurus, edit your Guru and set the Module field to
chat_llm. An empty Module field keeps the legacy ELIZA-style pattern engine driven by the Guru's.datanswer file. - Configure the backend (endpoint, model, prompts) in chat_llm.ini.
That is all that is required for a working LLM Guru. Retrieval (RAG) and the bundled tools are optional layers configured separately.
Entry points
A chat module is a JavaScript file under exec/ that exposes the functions
the BBS calls. chat_llm exposes two high-level entries — these are
the contract every chat module must satisfy:
chat_session(input, ctx)— process one caller turn; returns the reply string (ornullto stay silent). Loads the caller's memory, dispatches to the model, persists the updated memory.open_session(ctx)— produce an opening greeting at the start of a session (ornullfor none).
It also exposes lower-level helpers callers may use directly:
llm_chat(input, ctx, opts)/llm_open(ctx, opts)— pure dispatch, no memory load/save. For unusual flows that manage their own history.ctx_from_user(useron, persona_code, persona_name, supports_utf8)— the standard way to build actx(below) from a SynchronetUser.
The ctx object
Every entry point takes a ctx (chat context) object. It carries who is
talking, the transport mode, the conversation so far, and a few output knobs.
A custom module receives this same object.
There are three modes: private (1:1 Guru page), multinode (channel
chat), and irc (IRC bot). Most fields are shared; a few only matter in
channel modes.
Caller-supplied fields
| Field | Type | Meaning |
|---|---|---|
persona | {code, name} | Bot identity. code selects the chat_llm.ini section and the memory-file namespace; name is the display name. |
speaker | {id, alias, attrs} | Who is talking. id is namespaced ("user:42", "irc:vert/frosty") and becomes the memory filename. alias is the display name. attrs holds {real_name, level, location, lang} for BBS users, or {}. |
mode | string | "private", "multinode", or "irc". Channel modes prefix each turn with the speaker's name so the model can attribute multi-party lines. |
transcript | array | Conversation history, newest last; each turn is {who, text, ts, bot} (bot:true marks the bot's own turns). chat_session/open_session auto-load this from memory, so callers normally pass []. |
participants | array | {id, alias} roster for multinode; empty otherwise. |
addressed | boolean | In channel modes, false makes the engine return null (stay silent). Always true in private mode. |
supports_utf8 | boolean | Gates the language directive and output charset; forces English to non-UTF-8 terminals for non-Latin scripts. |
channel | string | Channel name (channel modes); used to validate relay recipients. |
channel_context | string | Recent room chatter, injected so the model can follow cross-conversations. |
channel_members | object | IRC: current channel roster, used to validate relay recipients. |
seen_members | object | IRC: nicks seen before, used for deferred (“tell them when you see them”) relays. |
typing_speed_factor | number | Per-character output speed for the terminal typing animation; 0 disables it (IRC uses 0). |
simulate_typos | boolean | Whether the terminal typing animation includes fat-finger/transposition typos. |
Engine-set fields (read-only)
The engine writes a few fields back onto ctx during a call. A module
author treats these as outputs — do not set them:
ctx._profile— a one-line diagnostic summary (persona, mode, retrieval stats), suitable for a chat log. Read it afterchat_session()returns.ctx._streamed—truewhen the engine already streamed its reply to the terminal itself, so the caller should not re-emit the returned string.- The engine also attaches retrieval diagnostics (
ctx._rag_*) and other internal scratch fields; treat anything beginning with_as read-only engine state.
Examples
Private (1:1 Guru page) — the standard BBS path:
var ctx = ctx_from_user(user, "guru", "The Guru", console.term_supports(USER_UTF8)); var greeting = open_session(ctx); // optional opening line, or null var reply = chat_session("what subs cover C programming?", ctx); log(ctx._profile); // read AFTER the call
Multinode channel turn:
var ctx = ctx_from_user(user, "guru", "The Guru", true); ctx.mode = "multinode"; ctx.addressed = true; ctx.participants = [{id:"user:42", alias:"Frosty"}, {id:"user:7", alias:"Digital Man"}]; ctx.channel_context = "...recent channel lines..."; var reply = chat_session("anyone know the FidoNet zone for Europe?", ctx);
IRC (built by hand, no User object):
var ctx = { persona: { code: "guru:irc", name: "The Guru" }, speaker: { id: "irc:vert/frosty", alias: "Frosty", attrs: {} }, participants: [], transcript: [], mode: "irc", supports_utf8: true, addressed: true, typing_speed_factor: 0, // no terminal: no per-character animation simulate_typos: false, channel: "#synchronet", channel_context: format_channel_context("#synchronet"), channel_members: chan_members("#synchronet"), seen_members: chan_seen("#synchronet") }; var reply = chat_session("Frosty: what's new?", ctx);
To test from the command line without a live session, run the engine under
jsexec and build a ctx from a stored user — pass new User(N)
to ctx_from_user, or null for an anonymous tester context.
How a turn is processed
For each caller turn, chat_session:
- Checks for a “forget me” command (e.g.
/forget me,forget everything) and, if matched, wipes that caller's stored memory and replies with a canned confirmation. - Classifies intent to decide cheaply whether retrieval and tools are even worth invoking for this input.
- Retrieves grounding (RAG) from the configured BM25 index, injecting the top hits — but only when they clear a relevance gate, so off-topic questions don't pull in noise.
- Runs the tool loop, offering the model the registered tools and feeding their results back until it produces a final answer.
- Post-processes the reply: strips control codes that could trigger hang-up/quit, and appends a verified wiki URL when the answer was grounded in a wiki page.
Persistent memory
chat_session/open_session keep a per-(speaker, persona) memory file
under data/chat/. Each file holds a rolling window of recent turns plus
an LLM-compressed long-term summary, and survives BBS recycles. A caller can
erase their own memory at any time by saying /forget me.
Memory behavior is tuned in chat_llm.ini (memory_persist,
history_window, summarize_threshold, memory_max_age_days, and
related knobs). Set memory_persist = false for public contexts where you
don't want to retain anything about strangers.
Writing your own chat module
You are not limited to chat_llm. Any JavaScript file under exec/ that
defines chat_session(input, ctx) and open_session(ctx) — and accepts
the ctx object above — can be named in the Guru Module field. This
lets you back a Guru with a different engine, a scripted persona, or an
entirely custom integration, without touching Synchronet's C/C++ code.
See Also
- chat_llm.ini — engine and chat configuration
- llm_tools — the tool registry and bundled tools
- llm_index — building the RAG index
- chat_llm_irc — the IRC bot adapter
- llm-guru — start-to-finish setup
- Artificial Gurus — the SCFG menu