Building messages
ChatInterface serves three purposes:
- Prepare — build the message history the agent will see (system prompt, user messages, seeded assistant/tool messages)
- Record — after
service.send(), read back what the agent produced viachat.messages() - React — observe the stream in real time via hooks
System prompt
The system prompt is built from a tree of components via chat.system(), which
returns the root PromptContainer. To bypass the tree entirely and send a
single flat string, set ChatServiceConfiguration.systemPrompt — see
Flat system prompt.
Two standard child containers are available for organisation:
chat.system().child('general')— for persona, rules, domain, behavior, and other general system prompt componentschat.system().child('tutorials')— populated automatically byToolSuitewhen tool packages with tutorial content are registered
General prompt files can be loaded automatically from a directory via
ChatServiceConfiguration.systemPromptDir (env: LLM_CHAT_SYSTEM_PROMPT_DIR).
All .md and .txt files in the directory are loaded as components under
general, with the filename (without extension) as the prompt ID.
Files with any other extension are ignored.
When the directory is first accessed (on the first send()), a set of
default empty files is created. You can add your own .md or .txt
files and they will be loaded as additional general > <filename> components.
The default categories are derived from real-world system prompts collected at https://github.com/dontriskit/awesome-ai-system-prompts.
| File | Prompt ID | Purpose |
|---|---|---|
persona.md |
general > persona |
Role & identity — who the AI is, who created it, its purpose |
rules.md |
general > rules |
General instructions & constraints — behavioral rules, meta-instructions, dos/donts |
behavior.md |
general > behavior |
Tone & interaction style — how to communicate, adapt to the user |
domain.md |
general > domain |
Domain-specific knowledge — tech stack, libraries, conventions |
environment.md |
general > environment |
System context — OS, platform, IDE, sandbox, capabilities |
safety.md |
general > safety |
Refusal protocols & alignment — what to refuse, how to refuse |
reasoning.md |
general > reasoning |
Planning & thinking — step-by-step reasoning, agent loops |
capabilities.md |
general > capabilities |
Task definitions — what the AI excels at, broad task categories |
Tutorial container
The tutorials child container is populated automatically by ToolSuite.
When a ToolPackage with a non-null tutorial() return value is registered,
the suite creates a sub-container titled Tool Package <ClassName> with two
children:
- Applicability — comma-separated list of tool names in the package
- Tutorial — the value returned by
tutorial()
For example, registering a WeatherPackage whose tutorial() returns
"Use greet to say hello and weather to check conditions." produces:
tutorials
Tool Package WeatherPackage
Applicability
greet, weather
Tutorial
Use greet to say hello and weather to check conditions.
This is injected into the system prompt tree under chat.system().child('tutorials'),
so it appears in every conversation alongside the general prompts.
The system prompt is stored separately from the regular message list:
chat.messages()— returns only user, assistant, tool, and reasoning messages. The system message is not included.chat.getSystem()— returns the system message, ornullif none has been set.
When service.send() sends messages to the provider, the system prompt is always the first element.
Every message is automatically timestamped with a createdAt: Date set at creation time and tagged with an origin field (ChatMessageOrigin) indicating whether it came from the user, the model, a tool, a hook, or the system prompt.
Adding messages
chat.system().child('general').prompt('persona').setContent("You are a pirate.");
chat.user("Hello!");
// seed assistant responses or fake tool calls
chat.assistant("Let me check...", [{ id: "call_1", type: "function", function: { name: "get_weather", arguments: '{"city":"Berlin"}' } }]);
chat.tool('{"temp":22}', "call_1");
chat.assistant("It's 22°C.");
Direct chat vs message queue
Messages can be added in four ways — user(), assistant(), tool(), and reasoning() — either directly on the chat handle or via the message queue. Choose based on your timing and concurrency needs.
Direct chat (chat.xxx())
Use chat.user(), chat.assistant(), chat.tool() when you want to:
- Build history before sending — prepare the conversation that will be sent to the LLM on the next
service.send(). - Record model output — after
service.send()returns, the service uses direct chat internally to record assistant responses and tool results. - Fire message hooks immediately — appending triggers any registered message hooks in real time.
Messages are appended immediately under the chat's own mutex.
Message queue (service.queue().xxx())
Use service.queue().user(), service.queue().tool() when you need to:
- Enqueue from concurrent contexts — timers, interrupt handlers, or any code that runs while an in-flight
send()is executing. The queue has its own mutex and won't block the send. - Stage messages without triggering hooks — queued messages sit in a buffer. No hooks fire during enqueue.
- Inject from JSON hooks — hook definitions with the
queue-messageaction write to the same queue, tagged withorigin: Hook.
Messages are drained into the chat at the start of the next service.send() call. At that point they fire message hooks like any other added message. See ChatService message queue for details.
Hook-specific variant
Hooks use a separate HookMessageWriter interface (backed by the same MessageQueue) to enqueue messages with an explicit origin parameter. See HookMessageWriter.
Quick reference
| Use this | When you need ... |
|---|---|
chat.user() / chat.assistant() / chat.tool() |
Immediate appending, hook firing, building history before send() |
service.queue().user() / service.queue().tool() |
Concurrent staging, deferred processing, hook injection |
Clearing
chat.clear() removes all conversation messages, clears the system prompt
tree (children are removed, title is reset), and unregisters any message hook
listeners. The same PromptContainer root stays in place — children are
cleared rather than replaced:
chat.system().child('general').prompt('persona').setContent("You are a pirate.");
chat.user("Hello!");
chat.clear();
chat.messages(); // []
chat.getSystem(); // null
Pass retainHooks: true to preserve message hooks across the reset:
const hook = chat.hook().message(ChatRole.User).do(cb);
chat.clear(true); // hook survives
If tool packages with tutorials were registered, call service.resetTutorials()
after chat.clear() to re-attach and re-populate the tutorials child container
in the fresh system prompt tree. See ChatService.
To reset the entire service (chat, stream, tools, and JSON hooks) in one call,
use service.clear() — see ChatService.