Assistant¶
The built-in assistant gives a served model tools and a long-term memory.
It runs a tool loop over MCP tools, keeps facts in a memory store, and
works in three places from one assistant block of the configuration
file:
| Surface | How to start it | Where tools run |
|---|---|---|
| Voice | Run gmlx talk with talk.brain: assistant, as The assistant by voice shows. |
Tools run on your Mac, as you. |
| Text | Run gmlx chat --assistant. |
Tools run on your Mac, as you. |
| API | Choose a served assistant id that server.assistants defines. |
Tools run on the server host. |
Short tasks suit the assistant, such as looking something up, chaining a
few tool calls, writing a note or remembering a fact. A turn ends when the
model answers, and no work continues afterwards. The coding agents that
gmlx launch connects have loops of their own and use the server only for
the model.
The tool loop¶
A turn is the standard OpenAI tool loop, sent to the server's chat completions endpoint. Your message goes out with the tools attached. When the model answers with tool calls instead of text, the assistant runs them, sends the results back and asks again. This repeats until the model answers in text. A failed call returns to the model as an error message, so the model can try again. Tool calls run one at a time.
assistant.max_tool_rounds limits
the rounds. After that many rounds of tool calls, the assistant sends a
last request with no tools, so the model must answer. A tool call that
takes longer than
assistant.tool_timeout_s returns a
timeout error to the model, and the tool keeps running in the background.
Each round is an ordinary chat completion request, so the server's profiles, speculative decoding and prompt cache apply to it. A round costs a model reply plus the tool call, so an answer that needs several tools takes longer than a plain reply. Choose a model that handles tool calls well, such as Qwen3.8-27B, because smaller models make more mistakes in their tool calls.
Tools¶
Tools come from MCP servers, which are separate programs
that offer tools to a model through the Model Context Protocol. The
assistant.mcp list names them, and tools need
the assistant extra, which
Optional features shows how to
install. This block connects one server that runs as a local command and
one that answers over HTTP:
assistant:
mcp:
- name: files
command: [npx, -y, "@modelcontextprotocol/server-filesystem", "/Users/me/notes"]
- name: search
url: http://127.0.0.1:8931/mcp
A server that fails to start, or that has not connected after 20 seconds,
gives a warning, and the assistant runs without its tools. For a command
server, the warning names its log file. gmlx doctor checks that the
command servers' programs exist.
Command servers get only a few variables from your environment, so pass
a token that one needs with env. ~ is not
expanded in the command or in env values, so write full paths. An HTTP
server gets no custom headers.
Tool examples¶
Every stdio or streamable HTTP MCP server is configured in the same way.
Each example is a complete assistant block, and the first two need no
account or key.
Web search without an API key¶
This community server searches with DuckDuckGo, which needs no key, and also fetches pages:
Web search on your own SearXNG¶
A SearXNG instance that you run gives results
from several search engines and keeps the queries local. The instance must
allow the JSON format, so add json to search.formats in its settings:
assistant:
mcp:
- name: search
command: [npx, -y, mcp-searxng]
env: {SEARXNG_URL: "http://127.0.0.1:8888"}
Web search with an API key¶
Brave's official server gives richer results with a free API key, which
reaches it through env:
assistant:
mcp:
- name: brave
command: [npx, -y, "@brave/brave-search-mcp-server"]
env: {BRAVE_API_KEY: your-key-here}
Document search as a tool¶
The official Qdrant server, in local mode, gives the assistant tools to
store and find documents in a vector collection on disk, with no database
process to run. It embeds documents with its own small model, so it does
not need server.embeddings:
assistant:
mcp:
- name: docs
command: [uvx, mcp-server-qdrant]
env: {QDRANT_LOCAL_PATH: /Users/me/vectors, COLLECTION_NAME: notes}
A search tool differs from memory. Memory is automatic and holds short facts about you, recalled on every turn. A search tool works on documents that you load, and the model decides when to use it. For document search in a chat app instead, see RAG pipelines.
Memory¶
The assistant remembers facts across conversations. After each turn, a background request asks the chat model to reduce the exchange to at most three lasting facts, such as "sister Ana, birthday March 12". Small talk gives none, and nothing is stored. A new fact that repeats a stored one closely replaces it.
With assistant.memory.extract: false,
the assistant stores a short form of each exchange instead. When extraction
fails, it stores the exchange and prints one warning. Cancelled turns and
very short messages are not stored.
Before each turn, the assistant finds the facts closest to your message
through the server's /v1/embeddings endpoint. It keeps up to
top_k of them, and when more facts
match, the /v1/rerank endpoint chooses the best ones if the server runs
a reranker. The facts go into that request only, so the conversation does
not grow because of them.
Memory therefore needs server.embeddings.
Without it, the assistant prints a memory disabled warning the first
time that it uses memory, and continues without memory.
Facts older than ttl_days are
removed at startup. When the store holds more than
max_items facts, those recalled
the fewest times go first, and the oldest go first among equals.
gmlx talk and gmlx chat --assistant share one store, so a fact that
you tell the assistant by voice is known in text chat too. The store is
$XDG_DATA_HOME/gmlx/assistant-memory.db, which is
~/.local/share/gmlx/assistant-memory.db by default, and
assistant.memory.path moves it. In
either client, these commands manage the store:
| Command | Effect |
|---|---|
/memory |
It lists the newest 20 facts, with their ids. |
/memory forget ID |
It removes one fact. |
/memory clear yes |
It removes every fact. Without yes, it asks you to confirm. |
Text chat¶
gmlx chat --assistant sends each turn to the assistant through the
server, and starts the server if it is down. Name a served model id, or no
model for the server's default model. A file path is refused, because
the server owns the model:
The client works as Chat describes, with the server-mode limits
listed in Where the model runs. /retry
and /undo go back over whole tool rounds, and /memory is added. While
the model uses a tool, a status line shows [assistant] using NAME....
The sampling that you set is sent with every round, as are --thinking and
--reasoning-effort. /model <id> switches to another served model and
keeps the conversation.
Served assistants¶
A served assistant is a model id that runs the tool loop in the server.
Clients that cannot run a loop, such as curl, Open WebUI or a phone app,
choose the id as their model and get the tools. The
server.assistants block defines each id:
server:
model_dirs: [~/models]
assistants:
helper: # The served id, listed in /v1/models.
model: qwen3.8-27b-ud-q6 # Required. The configured model that answers.
memory: false # With true, the id gets its own store.
mcp: null # null uses assistant.mcp. [] gives no tools.
assistant_allow_remote: false # See Security.
models:
qwen3.8-27b-ud-q6:
path: Qwen3.8-27B-UD-Q6_K.gguf
assistant:
mcp:
- name: clock
command: [uvx, mcp-server-time]
curl localhost:8080/v1/chat/completions -H 'content-type: application/json' -d '{
"model": "helper",
"messages": [{"role": "user", "content": "What time is it?"}]
}'
At startup, the server connects the tools and prints a line such as
[server] assistant 'helper' -> qwen3.8-27b-ud-q6 tools: ... for each
id. /v1/models lists each id with "assistant": true and alias_of,
which names the model. An id may not contain @ or match a model id or
alias.
Routing a request to /v1/chat/completions depends on its model and on
whether it carries tools of its own, where an empty tools list counts as
none:
| Request | Result |
|---|---|
An assistant id with no tools |
The server runs the loop and returns the answer under the assistant id. |
An assistant id with tools |
The client runs its own loop, so the server sends the request to the underlying model unchanged. |
| Any other id | The request is not changed. |
An assistant id on /v1/responses or /v1/messages |
The server returns a 400, because assistants work only through chat completions. |
The server builds each turn from the messages that the client sends, so
tool calls from earlier turns are not kept. The last message must be a
user message with text only, and earlier system messages are kept. The
answer joins every round's text. A stream carries the model's reasoning
and a comment line such as : assistant using NAME for each
tool, which also keeps the connection alive.
Every round uses the request's sampling fields and stop, but not
response_format, because it would stop the model from calling tools.
max_tokens limits each round and defaults to 4096, and
max_completion_tokens wins over it. The reported usage adds up the
completion tokens from all rounds and gives the last round's prompt
tokens.
A round that fails returns a 502 with the code assistant_upstream_error,
or an error object in a stream. The server runs at most 4 assistant turns
at a time, and a request over that limit gets an immediate 429. When a
streaming client disconnects, the turn stops at the next reply chunk or
tool call. A request without streaming runs to the end.
Memory on a served assistant is one store for each id, in
assistant-<id>.db beside the default memory file, and every client of
the id shares it. The store uses the top_k, extract, ttl_days and
max_items settings, and ignores assistant.memory.enabled and
assistant.memory.path. A shared store suits a personal server, not one
with several users.
Security¶
gmlx talk and gmlx chat --assistant run tools on your Mac, as you,
like any other command-line tool. A served assistant runs tools on the
server host, for anyone who can reach the server. On the default loopback
address, that is still only your Mac. On any other address, anyone with
the API key can make the host run tools.
These rules limit what a served assistant can do:
- Tools come only from the configuration file. A request cannot add MCP
servers or change tools, and a request with its own
toolsbypasses the loop. - On an address other than loopback, a server with
server.assistantsrefuses to start unlessserver.assistant_allow_remoteistrue. Such an address also needs an API key, unlessserver.no_authis set. - With
assistant_allow_remote: trueand a non-emptyassistant.mcp, each assistant must list its ownmcp, with[]for no tools. Each assistant's tools are therefore a choice that you make in the file. gmlx doctorwarns when assistants are served beyond loopback. For each one, it says whether the assistant inheritsassistant.mcpor how many servers its ownmcplist names.