Agent frontends

Where the human sits, and what the chat window has to be able to do

2026-07-25 — 2026-07-25

Wherein the Reader Is Instructed on the Selection of Chat Frontends for Language Models, With Particular Attention to a Forgotten Sampling-Temperature Control That Alone Decides the Matter.

computers are awful together
diy
machine learning
NLP
premature optimization
slop
UI
Figure 1

The frontend is the outermost layer of the stack — the human-facing surface, and the boundary that says what a harness — the loop running underneath — is not. It is also the loosest-coupled: the model neither knows nor cares what renders its tokens, so picking one is cheap and reversible. Which is exactly why it is worth deciding once and forgetting about, rather than drifting between four of them.

Three questions decide it.

  1. Can it render what the model emits? Prose is free. Equations, proof state, diagrams and structured output are not.
  2. Can it reach the tools? Which in practice means MCP or skills. Everything below answers one of those, but only because I auditioned tool-using agents and nothing else — so this question filters the field before the table rather than deciding anything inside it.
  3. Can it set the sampling parameters? The one people forget, and the one that decides the matter.

Getting documents in looks like a fourth question and is not really one. The Anthropic bundle reads the binary natively, Open WebUI and Qwen-Agent run their own extraction-plus-RAG pipelines, and Goose, OpenCode and Jan bring no pipeline at all — but all of that is moot for anything with real notation in it, since the built-in extractors mangle display equations, dense notation and scans alike. Convert deliberately first and feed the markdown to whichever frontend; extraction quality belongs to the converter, not to the chat window.

When a hosted general agent is doing the orchestrating there is not much to decide: we read the output in whatever client we already drive. The questions bite when we move onto open models, or own the loop.

1 Sampling, which is the one that decides it

Most chat clients expose a temperature slider, some expose nothing, and a few expose the whole parameter set. That sounds like a minor convenience until the loop depends on it.

maj@k is a sampling technique: greedy decoding returns \(k\) identical answers and there is nothing to vote on, so a client that cannot set temperature cannot run the technique at all. Worse, the solver drawing the samples and the loop driving it want different temperatures while sharing one endpoint. A local server like vllm-mlx has no per-model sampling in its registry, so the value has to come from the client or not at all — and most clients cannot send it.

1.1 What each client can send

Whether we can send a value turns out to be a property of the harness rather than of the server, and the harnesses differ more than I expected.

Harness What it can send Snags
Goose temperature, nothing else its binary contains GOOSE_TEMPERATURE and not the strings top_p, min_p or repetition_penalty, so no configuration would make it send them
OpenCode arbitrary keys per model, from a models.<id>.options block its @ai-sdk/openai-compatible transport spreads into the body last models.<id>.temperature is a boolean capability flag, not a value — without it no temperature is sent at all; and defaults are injected by substring-matching the model id, so anything with qwen in the name silently acquires temperature 0.55
Open WebUI a full pinned parameter set per model preset the penalty field is misnamed against any non-Ollama server (detail)
Jan the full sampler set — but only on a custom provider; the controls are hidden for the built-in cloud providers a local endpoint is not one of Jan’s “local” options, which are its own engines; it has to be added by hand

Open WebUI is the one that actually fits the shape of the problem, which is not what I expected from its strata of Node packages and weird version requirements. The unit is a model preset: a base model plus a system prompt plus a pinned parameter set, saved as its own entry in the picker. So one base model can carry several presets — the same weights appearing twice in the picker, a Qwen3.6 at temp 1.0 for thinking beside a Qwen3.6 at 0.6 for coding — which is precisely the distinction the model tables draw and that neither a server-side registry nor Goose can express. It also reads /v1/models, so the model list cannot drift out of sync the way a hand-maintained one does.

A preset is one row in webui.db, and the part that matters is small:

{
  "id": "cascade-2-maths",
  "base_model_id": "nemotron-cascade-2",
  "name": "Cascade-2 (maths)",
  "params": { "temperature": 1, "top_p": 0.95, "min_p": 0.02, "repetition_penalty": 1.05 }
}

That preset mechanism turns out to be a better reason to run Open WebUI than the rendering I originally chose it for. What it looks like against one particular local server — including a parameter name that silently does not survive the trip — is on the Mac page.

2 The options

Documents are in the table as reference data rather than as a deciding question, per above. “Needs a system-prompt line”, in the rendering column, is the LaTeX delimiter fix below.

Rendering Tools Documents Sampling
Anthropic bundle (Desktop, Code, Cowork) out of the box MCP + skills native multimodal, drag-and-drop no knob exposed
Open WebUI out of the box, every delimiter MCP, Tools, Python interpreter built-in RAG full parameter set, per preset
Qwen-Agent WebUI delimiters registrable a plain Python function built-in RAG (files=[…]) it is a library — whatever we put in llm_cfg
Goose needs a system-prompt line MCP; skills BYO via MCP temperature only
OpenCode needs a system-prompt line MCP BYO via MCP arbitrary keys per model, once past a temperature key that is a boolean flag rather than a value
Jan needs a system-prompt line MCP BYO via MCP the full set, on a custom provider
an editor, over ACP the editor’s own diffs and buffers whatever the agent has the files already open the agent’s
our own script ours to render a plain function call ours to parse ours to set

The Anthropic bundle gets rendering and documents for free with no configuration at all, and tool calls once MCP is wired up. It is also closed, hosted, and priced at someone else’s discretion, so the interesting choosing happens among the self-hosted options — where Open WebUI and Qwen-Agent are the two that answer yes to everything. Of those two the endorsement goes, reluctantly, to Open WebUI: it is a pile of Node packages with peculiar version requirements, and it is also the only frontend here that renders mathematics correctly without being told how.

3 Driving an agent from the editor

For coding, and increasingly for anything where the artefact is a file, the frontend people actually want is the editor they already have. The Agent Client Protocol (ACP) is how the two talk. Zed wrote it, and the pitch is modelled on the Language Server Protocol: rather than every editor hand-rolling an integration for every agent — and every agent re-implementing each editor’s API — both sides speak one JSON-RPC dialect, and ideally any ACP editor can drive any ACP agent.

Mechanically, the agent runs as a subprocess of the editor and they talk JSON-RPC over stdio (or HTTP/WebSocket for a remote agent); the editor is the boss of the files and the terminal, so the agent’s edits arrive as native diffs and its shell commands run in the editor’s own terminal. It reuses MCP’s JSON types where it can and adds a few agentic-UX types of its own.

On the editor side the adopters are Zed and JetBrains, with community bridges for Neovim, Emacs, and VS Code. On the agent side, Goose ships a goose acp server, Google’s Gemini CLI likewise I think, and Claude Code and Codex have adapters, I think; Goose’s ACP-clients guide is the least stale list I have found.

4 When the model wants a different interface entirely

Everything above assumes an autoregressive model: we append a prompt, the model appends an answer, and the frontend’s job is to draw the appending nicely. A diffusion language model like DiffusionGemma instead fixes a block of text and denoises the whole thing at once, every token effectively attending to all the others. Streaming that into a chat box, we are watching a printing press pretend to be a typewriter.

It would better suit an interface built around editing text rather than chatting — much as image diffusions get inpainting in their GUIs. Rather than only appending, we’d want to regenerate selections, infill gaps, rewrite under constraints. The closest thing today is Aider’s AI! / AI? scoped edits, plus the inline-diff autocomplete most sidebar extensions ship — both of which already mutate selected text, though I’m unclear whether either explicitly supports the diffusion behaviour, and I don’t fully trust the process at arbitrary infill anyway. Also, the actual prompt window for each generation looks quite short (hundreds of tokens) which seems… not enough?

This is the one case where the frontend is not the loosest-coupled layer, and it is worth flagging because everything else on this page assumes it is.

5 The maths case

Mathematics is the domain that demands all three questions at once, which is why I ended up caring. Notation has to render, the solve() oracle has to be reachable, maj@k needs the temperature knob, and if we are proving there is a fourth thing no chat window does at all.

5.1 Rendering notation

Hosted clients render LaTeX without our help, so this only bites self-hosted frontends, and there it bites in two places: delimiters and streaming. Delimiters first. Models like to emit \(…\) and \[…\], while most chat UIs only wire $…$ and $$…$$ into their renderer, so the equation arrives as raw backslashes. The fix is a system-prompt line telling the model to emit dollar delimiters; Goose, OpenCode, and Jan all need it. Open WebUI does not: its KaTeX pass registers every delimiter style$…$, $$…$$, \(…\), \[…\], even \begin{equation} — behind guards that keep prose dollar signs out of maths mode.1 Qwen-Agent attacks it from the other end: its Gradio UI lets us register the delimiters at the renderer (gr.Chatbot(latex_delimiters=…)) instead of nagging the model. Streaming is the second fumble.2

5.2 Calling the oracle

Reaching a solve() oracle reduces to MCP versus skills, which every row in the table answers the same way, so it decides nothing between them. The one frontend-level trap is the timeout: a synchronous solve() that fans out for three minutes will exceed some clients’ tool-use time limit, which is what the async handle pattern is for.

5.3 Lean state

No chat frontend renders Lean proof state, so this question has one good answer and it is not a chat UI. The Lean 4 web editor runs a Lean server with a live goal panel, so we keep it open beside the chat rather than build our own. When we own the loop we at least see what the loop sees: the compiler’s complaints are the loop’s food, and printing them is free. A custom React app with an embedded editor buys full control for a day or two of plumbing; I’d live in Open WebUI first and defer that until the loops earn it.

5.4 Our loop behind Open WebUI

Owning the loop is one thing, but building a whole new frontend sounds tedious. Fortunately we need not do both. Open WebUI straddles two layers — it is frontend and harness — and the two come apart, so we can keep the frontend and replace the harness underneath it. That is, we can run our own orchestrator and still inherit the KaTeX rendering, the RAG, and the chat history without writing any of it. There are three extension points that seem appropriate, in order of increasing maintenance burden:

  • A Pipe Function is the basic option: a Python function that registers as a custom “model” in the chat dropdown, intercepts the prompt, and returns whatever the function produces. Enough for a single custom agent.
  • A Pipeline is similar but elaborated: a standalone Python service that Open WebUI talks to as if it were a model, so, for example, the generate-run-verify loop (routing, retries, fan-out, the oracle call) can run in a separate process. Upload a .py file, or point it at a raw GitHub URL, and it shows up as a model in the UI. Looks like enough power for the current purpose.
  • MCP (native since v0.6.31) is to let the loop itself use tools — the mathx oracle, a (wrapped) Lean server. It can compose with either of the above.

Logical escalation: keep the stock UI, put the loop in a Pipeline, integrate more needful tools via MCP. I suspect we can defer the custom React app until we hit something outside the scope of Open WebUI, such as proof-state rendering.

Footnotes

  1. Recent, though: inline $…$ was deliberately excluded into early 2026, so an older install still wants the system-prompt line.↩︎

  2. A $$ arrives before its closing pair, so the equation shows as raw text until the delimiter lands — the maths case of the flash-of-incomplete-markdown problem, fixed by buffering until the delimiters balance. Rendering a document rather than a chat stream sidesteps it entirely.↩︎