A local chat app where every chat belongs to an agent: a persona with its own purpose, instructions, tools and (optionally) model. Every agent knows the rest of the team and can give any teammate a task or ask it a question. You watch every step live, and each agent's work for another agent shows up as its own chat you can open.
An agent can also start subagents: copies of itself with a fresh context that each do one part of a job and report back, so the agent's own context holds the results, not every file and page they read. MCP servers plug in more tools (reverse engineering, databases, GitHub, ...).
Agents share a memory about you that grows as you chat. A Mail agent reads, sorts, drafts and (with your approval) sends email; a Planner reads your calendars; a Browser agent drives a real browser. Routines run agents on a schedule. Long chats compact themselves before they run out of context.
Use it from the terminal (agent-chat, a Rust client, like running claude) or the
browser (agent-chat web); both show the same chats live, with the same slash
commands.
It talks to any OpenAI-compatible server. It's set up for llama.cpp's llama-server on
http://127.0.0.1:8080/v1; Ollama, LM Studio and vLLM work too. Everything stays on your
machine: chats, memory, email and calendar credentials.
Every reply is a trace: a line in the agent's color with its steps hanging off it, so you can read what the agent thought, which tools it called, and whom it handed work to. The sidebar lists the agents with what each one is doing right now, then your chats.
A handoff, live. The Orchestrator hands the task to the Coder, whose trace nests inside in its own color; its shell command waits for your approval. "Approve all for this run" lets the rest of the run go through without asking again.
Thinking opens inline on the trace, every reply shows its speed, token counts and thinking time, and the context bar in the header updates live.
The workspace drawer shows what the agent's file and shell tools can see; any file opens in a viewer. The command palette finds chats by what was said in them.
A shared memory about you grows as you chat; pin the facts every agent should always see.
Light theme, if you prefer it.
curl -fsSL https://agent-chat-i4sv.onrender.com/install.sh | bashThat installs Agent Chat for your user (no sudo): the app in ~/.local/share/agent-chat/app,
your chats, memory and settings in ~/.local/share/agent-chat/data (never touched by an
update), and two commands in ~/.local/bin. It installs uv if
you don't have it. On Linux and macOS, x86_64 or ARM, it downloads the terminal client prebuilt
(its SHA-256 is checked); elsewhere it builds it with cargo if you have Rust, or you use the
web UI. On Windows, use WSL. To read the script first:
curl -fsSL https://agent-chat-i4sv.onrender.com/install.sh -o install.sh.
Put options after the pipe (… | AGENT_CHAT_NO_TUI=1 bash): AGENT_CHAT_VERSION=0.3.4
installs that release, AGENT_CHAT_NO_TUI=1 skips the terminal client, AGENT_CHAT_BUILD_TUI=1
builds it with cargo even where a prebuilt one exists, and AGENT_CHAT_HOME /
AGENT_CHAT_BIN move the install. The script's header lists the rest.
Then start your model server and run agent-chat, like you'd run claude: it starts the app
in the background if it isn't running and opens the terminal client.
agent-chat the terminal client (starts the app first if needed)
agent-chat web the web UI in your browser
agent-chat status | stop | restart | logs [-f]
agent-chat update [VER] the latest release (or VER); keeps your data
agent-chat uninstall removes the app and commands; keeps your data unless --purge
AGENT_CHAT_PORT picks the port (default 8765). The launcher also works straight from a git
checkout (bin/agent-chat), using the checkout's code and data/.
# 1. start your model (this app never starts it for you), e.g.
llama-server --jinja -m your-model.gguf # tool calling needs --jinja
# 2. start the app
./run.sh
# 3. open http://127.0.0.1:8765On first run, uv creates the virtualenv and installs dependencies. Point
Settings → Model server URL at your server if it isn't on http://127.0.0.1:8080/v1.
To have it start by itself when you log in (needed for routines to fire on time), run
./install-service.sh once (--remove undoes it; PORT=… ./install-service.sh picks a
port). It starts only this app, never the model.
- Agents sit at the top of the sidebar, each with what it is doing right now: working, needs you (an approval is waiting; the tab's favicon gets a dot too), or → Coder when it is waiting on another agent. Click an agent to see its chats; All chats shows the ones you started. Drag agents to reorder them; right-click for options.
- The chat list is grouped by day, with pinned chats on top. A dot marks a chat that got a reply while you were elsewhere. Right-click a chat (or click ⋯) to rename, pin, export or delete it; the ⋯ next to the list's title can delete every idle chat of that agent.
- Ctrl+K opens the command palette: type to find chats by title or by what was said in them, start a chat with any agent, or run any command (settings, memory, theme, export, branch, …). ? lists every shortcut.
- New chat starts a chat with the selected agent (or asks which one). n does the same.
- The pencil edits an agent: name, icon, purpose, instructions, tools, who it can talk to, memory, model, temperature and workspace folder. Preview prompt shows the exact instructions a chat with it starts with (team, memory and date included). Duplicate, Export and Import move agents around as JSON. New on the rail creates one, from a template, a blank sheet or a file.
- Memory, Routines, Settings and the theme switch sit at the bottom of the rail.
- Attach files by dropping or pasting them, or with 📎. Text files go into the message, and
every file is saved to the workspace's
uploads/folder. Images go to the model as images (vision models like Qwen2.5-VL or Gemma 3 with an mmproj); a model that can't take images gets the text alone and the chat says so. - The folder button in the header (Ctrl+.) opens the workspace drawer: the files the agent's tools can see, refreshed as it writes them. Click a file to view it (code is highlighted, images show) or download it.
- Hover a message to edit and resend it, copy it, regenerate a reply, or branch from that point: a new chat with the conversation up to there, leaving the original as it is. "Branch from the end" is in the header's ⋯ menu.
- Stop cancels a reply mid-stream; the partial text is kept. An error card has Try again, which re-runs that turn.
- Message queue (like Claude Code's): keep typing while an agent works. Enter queues the message (the button reads Queue), and it shows above the message box. The agent gets it at its next step: after the tool calls in flight finish and before it writes again, so it can change course mid-task. If the agent answers first, the queued messages start the next turn right away. ✎ or ✕ takes one message back out, and ↑ in an empty message box takes them all back to edit. If the run ends any other way (Stop, an error, the step limit), queued messages go back into the message box unsent.
- Ctrl+O (or the expand button) shows every thinking block and tool call in full, including inside delegated work, and new ones open as they stream. The choice is remembered.
- Live tool calls: every tool call is a step on the trace. While an agent writes one you watch it being written. A file being created shows its code line by line with syntax highlighting; a shell command shows its output while it runs; a browser screenshot shows the picture.
- Permissions: the chip under the message box shows the mode. "Asks before acting" (the default) asks you before every shell command and every email send. An approval card offers Approve, Approve all for this run (everything else this run asks for goes through, marked "auto-approved") and Deny. Click the chip, or tick "Bypass permissions" in Settings, to let agents act without asking at all. The chip turns red while bypass is on. Anything an agent reads (web pages, emails) could try to trick it, so only bypass when you trust the task.
- The compact button in the header summarizes older messages on demand (
/compact keep the API decisionssays what the summary must keep). - The end of every chat says how many times it was compacted and how long it has been going
(
compacted 2× · going for 3h 12m);/usageand/contextrepeat it. - The header shows a context bar (
ctx 12k / 85k) and the last reply's speed. Each reply's header shows its speed, tokens in and out, and how long it thought. - Chat titles are written by the model after the first reply (Settings → Agents turns it off). Click a title to rename it; a name you chose is never replaced.
- Export (the download button) saves a chat as Markdown or as JSON with the agent-to-agent conversations it started.
- Notifications: when a reply finishes or needs your approval while the tab is in the background (allow notifications when the browser asks).
- Runs continue in the background. Switch chats or reload the page, and the live view picks up where it is. When you scroll up during a reply, a Newest pill takes you back down.
- Ctrl+B hides the chat list to give the conversation the width.
Settings (the gear on the rail, /settings, Alt+S in the terminal) has these tabs:
- Model: the server's URL and API key, the default model (each agent can pick its own), the context size (0 asks llama-server; set it for other servers), and when chats compact (70% of the context by default). Test connection says what kind of server answered.
- Agents: Max steps per turn (rounds of tool use per message, 30 by default, 0 = no limit), the shell command timeout (180 s), Subagents at once (1), model-written chat titles, automatic memory, and Bypass permissions.
- Web & browser: the SearXNG URL for
web_search, and the browser's executable and headless switch. - Email and Calendars: see Email and Calendars.
- MCP: servers agents can use as tools; see MCP servers.
Settings live in data/settings.json; passwords and private calendar links in
data/secrets.json, readable by your user only.
Type / at the start of the message box (web or terminal) and a menu of commands opens above
it, like Claude Code's. Typing filters it, ↑/↓ choose, Tab completes, Enter runs, Esc closes.
Commands run right away, even while an agent works; they are never sent to the agent.
//text sends /text as a message, and a first word with another / in it (a path like
/etc/hosts) is sent as a message too.
| Command | Does |
|---|---|
/help |
Every command and the keyboard shortcuts |
/new [agent] (/clear) |
A new chat with this chat's agent, or the one you name |
/resume [search] (/chats) |
Find an earlier chat |
/rename [title], /pin, /delete |
This chat's name, pin, and deleting it (asks first) |
/branch (/fork) |
A new chat that continues from the end of this one |
/retry (/regenerate), /edit, /stop |
Regenerate the last reply, edit and resend your last message, stop the reply |
/compact [instructions] |
Summarize older messages now; the instructions say what to keep |
/export [md|json], /copy |
Save the chat (the terminal client writes it to the current folder), copy the last reply |
/agents, /model [name] |
Edit this chat's agent; show the models or switch the agent's model (default clears it) |
/memory, /remember <fact> |
The memory panel; save a fact every agent shares |
/permissions [ask|bypass] |
Whether agents ask before running commands and sending email |
/settings (/config), /routines, /files, /verbose, /theme [name] |
Panels and display |
/context, /usage (/cost, /stats), /status |
Context use, tokens and speed for this chat, the model server |
/quit (/exit) |
Terminal client only |
Commands that would change what a running agent is doing (/retry, /edit, /compact,
/rename, /pin, /delete) wait: they say the agent is working and keep your text.
tui/ holds a Rust terminal client that talks to the same app, so the web UI and the terminal
share every chat, agent, memory and setting. It draws the same things the web UI does: the
sidebar with agents and their live status, chats grouped by day, and every reply as a trace.
The installer puts it on your PATH (agent-chat starts it). From a checkout:
cd tui && cargo build --release # needs a Rust toolchain (rustup.rs); ~1 min, 8 MB binary
./target/release/agent-chat-tui # or --url http://host:8765 (or AGENT_CHAT_URL), --light, --plain, --chat ID, --quiet- Everything is on the keyboard: F1 lists it. Ctrl+K is the same palette as the web UI
(chats by content, agents, commands);
nor a number starts a chat; Enter sends, or queues while the agent works;/opens the slash commands; Alt+Y / Alt+A / Alt+N approve, approve all for the run, or deny from anywhere (plainy/a/nwhen the trace has the focus); Ctrl+S stops; Alt+T shows or hides thinking (shown in full by default, as it streams and after); Alt+O opens every thinking block and tool result; Alt+F is the workspace drawer (Enter opens a file in a viewer); Alt+M memory, Alt+R routines, Alt+S settings, Alt+A the agent editor; F2 rename, Alt+P pin, Alt+D delete, Alt+X export, Ctrl+G branch, Ctrl+L compact, Ctrl+E edit and resend your last message, Ctrl+R regenerate or retry, Ctrl+U attach a file by path, Ctrl+Y bypass permissions, Alt+B hide the sidebar. - In the trace,
[and]select a reply:ccopies it,rregenerates it,bbranches from it, andoopens the chat of its first handoff or subagent. In the sidebar,/filters,ddeletes,rrenames,ppins,eedits the agent andmmarks everything read. - Mouse works too: click an agent or a chat, scroll the trace. Drag in the trace to select
text; letting go copies it, without the trace's bars and markers (Shift+drag gives the
terminal's own selection instead). Whole replies copy with
con a selected turn ([and]select). Copies go through the terminal (OSC 52) and the desktop's clipboard too: wl-copy, xclip, xsel or pbcopy if there, else KDE's Klipper, so terminals without OSC 52 work. - A running tool or command shows how long it has been going (
running · 12 s), and a finished one what it took (done · 1 min 4 s; time spent waiting for your approval left out). The web UI shows the same on its tool cards. - Settings, agents, routines, email and calendars are edited in forms (Tab between fields, Space toggles, Ctrl+S saves). Chat titles, unread dots, model-written titles, live shell output, nested handoffs and compaction markers all arrive over the same event stream the browser uses.
- Pasting works in any terminal: line endings and tabs are kept as text, color codes and
other control characters are dropped, and a paste over 10 lines or 1,000 characters goes
into the box as one
[Pasted text #1 +120 lines]token that is sent in full. Terminals without bracketed paste are detected by timing, so a pasted newline never sends early. - Ctrl+C closes a dialog, then clears the filter or a non-empty message box, and only then quits; Ctrl+Q always quits.
- Light terminals:
--light(or/theme light, remembered); to keep your terminal's own colors:--plain. It rings the terminal bell when an approval comes in;--quietstops that. - What you toggle (thinking, verbose, sidebar, drawer, filter, theme) is kept in
agent-chat-tui/state.jsonin your config folder (~/.configon Linux).
| Agent | Purpose | Tools |
|---|---|---|
| 💬 Assistant | General chat | team |
| Reads, sorts and answers your email | team, email | |
| 📅 Planner | Knows your schedule and plans your days | team, calendar, read/write files |
| 🧭 Orchestrator | Plans tasks and hands them out | team, list/read files |
| 💻 Coder | Writes, runs and fixes code | team, subagents, files, shell |
| 🔎 Researcher | Web research with sources | team, subagents, web search, fetch, write files |
| 🌐 Browser | Drives a real Brave browser to open, read and act on web pages | team, browser, read/write files |
| ✍️ Writer | Drafts and edits text | team, read/write files |
| 🧐 Reviewer | Code review | team, subagents, list/read files |
"team" = the ask_agent tool, which reaches every other agent by default. Every agent also
has memory unless you switch it off for that agent. New starter agents added in later versions
appear automatically; ones you deleted stay deleted. The agent editor's templates add a
Personal Assistant, Tutor, Translator, Brainstormer and Data Analyst.
| Tool | What it does |
|---|---|
list_dir, read_file, write_file, edit_file |
Work on files inside the agent's workspace folder only. Paths that escape it are refused. read_file numbers lines like cat -n and can read a range; edit_file replaces exact text, and when the text matches nowhere it tries again without copied line numbers or one extra level of indentation (and says so). |
run_shell |
Runs a command with the workspace as its working directory. Not sandboxed. By default every command needs your Approve/Deny in the chat. Output streams into the chat while it runs; background=true starts a long-running process and returns its log path. Commands get your environment, not Agent Chat's own virtualenv, so python3 and pytest are yours. |
web_search |
Top 8 results (titles, URLs, snippets) from your SearXNG instance, see Web search. Without one it scrapes DuckDuckGo, which often blocks it. |
web_fetch |
Downloads a page (up to 2 MB) and returns its readable text. |
browser_navigate, browser_read, browser_links, browser_click, browser_type, browser_screenshot, browser_close |
Drive a real Brave browser for pages that need JavaScript, a login, or clicking/typing through steps. One shared session persists across the calls. See Browser control. |
ask_agent |
Sends a task or a question to another agent. The other agent runs its own tool loop. Its work shows nested in the card and as its own chat. Maximum depth is 3. |
subagent |
Hands one self-contained task to a fresh copy of the agent (its instructions and tools, an empty context) and returns its report. Several in one reply run side by side. See Subagents. |
remember, recall_memory, forget_memory |
Manage the shared memory about you. |
email_list, email_search, email_read |
Read your mailbox (reading doesn't mark mail as read). |
email_draft |
Saves a draft in your mailbox without sending. |
email_send |
Sends an email. Shows you the exact email to Approve or Deny first, unless you turned on bypass permissions. |
calendar_events |
Lists events from your connected calendars in local time (default: the next 7 days; a search looks a year ahead). Read-only. |
| MCP servers | Tools from other programs over the Model Context Protocol. A small server's tools are offered by name (<server>__<tool>); a big one is searched with find_tools and run with call_tool. See MCP servers. |
- Everyone knows the team. Each agent's system prompt gets its own name and job, plus the name and purpose of every other agent.
- Everyone can reach everyone by default ("Every other agent, including ones added later" in the editor). Untick it to choose specific agents, or untick the "Talk to other agents" tool to make an agent work alone.
- Each conversation between two agents is its own chat. It's listed under the agent doing the work, marked "↳ … for Orchestrator", and shown nested in the caller's trace. Open it (or click the busy agent in the sidebar) to watch live. These chats are read-only; you reply in the chat that started them.
- Conversations are remembered. Asking Coder again continues where it left off,
including in later turns of the same chat. The caller can pass
new_conversation: trueto start fresh. Editing, regenerating or branching also drops what the agents said to each other in the removed turns (a branch starts its agent conversations fresh). - Questions go back up. A helper that needs information ends its reply with a question. The
caller answers with another
ask_agentcall, and the helper continues with full memory. If the caller doesn't know either, it asks its own caller, and finally the top agent asks you. - No loops. An agent can't call an agent that is already waiting on it (it replies by ending its turn instead), and chains stop at depth 3.
All agents share ~/agent-chat/workspace/ unless you give one its own folder, so one
agent can pick up files another one wrote.
An agent with the subagent tool ("Start subagents" in the editor) can hand one task to a
copy of itself: the same instructions and tools, a fresh, empty context, and a report back.
Use it the way Claude Code uses its subagents: to search a codebase, read through a pile of
pages, or do one separate part of a bigger job, without filling the agent's own context with
everything that was looked at. Only the report comes back.
- The task must stand alone. A subagent can't see the conversation or ask anything back,
so the calling agent writes the whole task: goal, known details and paths, and what the
report should contain. It gets a short label (
description) that the trace shows. - One at a time, or side by side. Subagents started in one reply run one after the other by
default, which keeps each one's prompt in the server's cache. Settings → Agents →
Subagents at once runs more together; 0 means as many as llama-server has slots
(
--parallel N). Only raise it when every slot has room of its own: llama-server's automatic--parallelgives four slots that share one context (--kv-unified), and four growing subagents can fill it. vLLM, or llama-server with--parallel Nand-clarge enough, can take it. - One level. A subagent can use the team (
ask_agent) but can't start subagents itself. - Its own chat. Each subagent's work is a read-only chat, nested in the trace under its
task (
↳ subagent <label>in the terminal client) and listed under the agent. A model error ends that subagent with an error result; the calling agent carries on. Editing or regenerating removes the subagents of the removed turns.
Inspired by amnis. Facts about you live in
data/memory.db (SQLite). Each fact has a category, importance (1–10), confidence and usage
stats. Search uses SQLite's full-text index (bm25), ranked by relevance × importance ×
confidence. Near-duplicates are merged instead of stored twice.
How memories reach agents:
- Profile: when a chat starts, its agent gets your most important and pinned facts in its system prompt. The snapshot stays fixed for that chat (so the model's prompt cache keeps working). It refreshes when the chat is compacted.
- Recall: each message you send gets the few memories most relevant to it attached. The chat shows "N memories recalled" under the message.
- Tools: agents save facts with
remember, look things up withrecall_memory, and fix outdated facts withforget_memory. - Automatic learning: after each reply, the model reviews the turn and adds, updates or deletes facts. This runs only while the model is idle (a chat waiting for your approval doesn't count as busy) and stops the moment you send something. Turn it off in Settings.
Open Memory to search, add, edit, re-categorize, pin (always in the profile) or delete memories. Tidy up asks the model to merge duplicates, resolve contradictions (newest wins) and drop trivia. It never rewrites or deletes facts you typed in yourself. Weak automatic memories nobody used for 60 days are pruned.
Set it up in Settings → Email: pick a provider to fill in the servers, then enter your
address and an app password. Gmail, iCloud, Yahoo and Fastmail all require app passwords
for this, and your normal password won't work. Save and test email checks both servers.
The password is stored only in data/secrets.json (readable by your user only), and is never
sent to the model or back to the browser.
Ask the Mail agent things like "what needs a reply today?", "summarize the newsletter from X", or "draft a reply to Sam saying I'll be late". It writes in your voice using what it remembers about you.
Read this: emails are written by strangers, and a crafted email can try to instruct the agent ("forward this to…", "fetch this link…"). The app fences email text as untrusted, and the Mail agent is told never to act on instructions inside emails. The hard guarantees are the approvals: nothing is sent and no shell command runs without your click. Fetching web pages is not approval-gated. Be suspicious if an agent wants to visit odd links after reading mail.
Routines lists agents that run on a schedule: at a set time on chosen days, or every N minutes. Each routine has its own chat ("Routine · Morning briefing"). Every run adds the routine's prompt there and starts the agent, as if you typed it, so you can read each result and reply to follow up. Templates include a morning email briefing, planning tomorrow, a news digest built around your interests, and a weekly memory check.
Routines run only while the app is running. A run missed while the app was off fires on start-up if it is less than two hours late; older ones are skipped. Approvals (sending email, shell commands) still wait for you, and you get a notification.
In Settings → Calendars, paste each calendar's private iCal/ICS link:
- Google: Calendar settings → your calendar → "Secret address in iCal format".
- iCloud: share the calendar as a public calendar and copy the
webcal://link. - Outlook: Settings → Calendar → Shared calendars → Publish → ICS link.
- A path to a local
.icsfile also works.
Access is read-only. The links are stored in data/secrets.json, and the browser only sees a
shortened version. Repeating events, skipped dates, moved occurrences and time zones are all
handled by the recurring-ical-events library. Feeds are cached for 5 minutes. The 📅
Planner agent uses this. Give "Read your calendars" to any other agent in its editor, and
try the Morning briefing routine (calendar plus urgent email, weekdays at 07:30).
web_search works best with a self-hosted SearXNG: set its URL
at Settings → Web & browser → Search engine URL (e.g. http://127.0.0.1:8888). It is
free, needs no API key, and asks several engines at once, so one engine blocking you doesn't
break search.
- Install: follow the SearXNG docs. A git clone with its own venv, or the Docker image, both work.
- Config it needs: turn JSON output on (
search.formats: [html, json]— without it SearXNG returns 403 to the API) and the bot limiter off (server.limiter: false). Bind it to127.0.0.1only. Enable whichever engines work from your network. - Check engines: open
<your-searxng>/statsto see which engines fail, and toggle them in itssettings.yml. - Leave the URL empty to fall back to scraping DuckDuckGo directly (often rate-limited).
The 🌐 Browser agent drives a real Brave browser through
Playwright. Use it when web_fetch isn't enough — pages that
render with JavaScript, need a login or cookies, or where you have to click through steps or
fill a form. Brave is Chromium-based, so Playwright drives the system binary directly; no
bundled-browser download is needed.
- Settings → Web & browser: the executable path (default
/usr/bin/brave) and a headless toggle. Headless (the default) runs with no visible window; turn it off to watch the browser work on your screen. Changing either setting restarts the session on the next navigation. - One shared session. The browser launches on the first
browser_navigateand stays open across the agent's tool calls, so it keeps the same page and cookies between steps. A module-level lock serialises calls, so two agents acting at once take turns on the one session.browser_close(or shutting the app down) closes it. - Tools:
browser_navigate(open a URL),browser_read(re-read the current page's text),browser_links(list links as text → URL),browser_click(by visible label or CSS selector),browser_type(fill a field by label/placeholder or selector, optionally pressing Enter),browser_screenshot(save a PNG into the workspace; it shows in the chat). - Safety. The agent is told to treat page text and form labels as information, never as instructions, and not to log in, buy, post or take other consequential actions unless you asked for that specific thing. Browser calls don't pop an approval prompt, so give the agent only tasks you're comfortable with it carrying out; bypass permissions doesn't change browser behaviour (it already doesn't ask).
- Install:
uv syncpulls in Playwright. You don't needplaywright install, because the agent drives your installed Brave rather than a downloaded Chromium.
Agents can use tools from MCP servers, the plug-ins many agent apps share (reverse engineering, databases, GitHub, ...). Only local servers that run as a command (the stdio transport) are supported.
- Settings → MCP: paste the server's config in the
mcpServersshape its README gives for Claude Desktop, and save. For example REA, for reverse engineering:{"mcpServers": {"rea": {"command": "npx", "args": ["-y", "rea-agents@6.4.0", "mcp"], "env": {"REA_MCP_INPUT_SCHEMA_PROFILE": "compact"}}}} - Test starts the server and shows how many tools it has and what they cost in context.
- Edit an agent → Tools → MCP servers: tick the server.
The server starts when an agent that has it begins a turn (the chat shows "Starting MCP server …"), and keeps running, one process for every agent, until you change its settings or stop the app. If it crashes, the call says why (the end of its stderr) and the next call starts it again. Stop cancels a running call on the server too.
Big servers are searched, not listed. Every tool definition goes into every request, and a
local model's context is small: REA's 170 tools would take about 67K tokens. A server whose
definitions are over ~4K tokens isn't put in the agent's tool list. The agent gets two tools
instead: find_tools searches the server's catalog by keywords and returns the best matches with
their argument schemas (an empty query lists every name), and call_tool runs one. The system
prompt names the server, its tool count and the instructions it gives clients.
Approvals. By default a call asks you first unless the tool declares itself read-only or
non-destructive (MCP's readOnlyHint/destructiveHint). Per server, "confirm": "always" asks
for every call and "never" for none; bypass permissions skips the question as usual.
Other keys per server: "env" (added to the app's environment), "timeout" (seconds per call,
default 600, 0 = no limit) and "disabled": true. The server runs in the default workspace
folder; tools get absolute paths. Text results go to the model as they are (cut to fit the
context like other tool outputs), images and files a tool returns are saved under mcp/ in the
agent's workspace, and a result with only structured content is shown as JSON.
Before every model call the app estimates the prompt size. It calibrates itself against the token counts the server reports. Past 70% of the context window (adjustable in Settings), the same model writes a summary of the older messages:
- The model sees the summary plus the recent messages word for word.
- Your chat still shows everything, with an expandable "Earlier messages summarized" marker.
- Before it comes to that, old tool outputs are cleared: past 70% of the compaction point (49% of the window by default) the oldest file reads, command outputs and pages are replaced, in what the model sees, by a one-line note saying the tool can be run again. One batch takes the context down to 40% of the compaction point, so llama-server's prompt cache is broken rarely, and no model call is needed. The newest three outputs, the current step's, teammates' answers and memory results stay. The cards keep their output, tagged "cleared"; editing or regenerating an earlier message brings the outputs back. One tool output may take at most a quarter of the window.
- Compaction aims for half the trigger point, so a chat has room to grow before the next one. The system prompt, the tools, the request being worked on and the summary itself can't be summarized away, so the word-for-word part shrinks to make room for them. The summary is asked to stay short (about 6% of the window). A compaction that would free less than 10% of the window is skipped rather than repeated at every step.
- If the server still rejects a prompt as too long, the app compacts and retries once.
- If a reply fills the context window before it finishes (a very long think, say), the agent keeps going instead of stopping. The next request leaves the cut-off reply out and passes the end of its thinking (the last 10% of the window, at most 24,000 characters) plus any answer it had started, with "Continue from where you stopped." If the prompt itself took more than half the window, the app compacts first. After four cut-offs in a row the turn stops with an error. This also works in delegated sub-chats, so a teammate never hands back an empty answer.
- Thinking is never cut short.
- After you press Stop, the next message you send carries the end of what the stopped reply had thought (the same amount a cut-off reply hands back), so "continue" picks up from there instead of starting over.
Local models sometimes go round in circles: Qwen has been seen thinking the same plan out 19 times in a row (115,000 characters) until someone pressed Stop, and an agent can call the same tool with the same arguments over and over. The loop guard (on by default; Settings → Agents) catches both:
- A reply that repeats itself. While the thinking and the answer stream, the app checks every 500 characters whether the newest 200 have already appeared three times, over at least 2,000 characters. If so the reply is stopped, and the next request hands back the reply up to the loop, with the repeated passage once, and tells the model to act on it instead of writing it again. The trace says "Loop guard: the thinking kept repeating the same passage…". After three stopped replies in a row the turn ends with an error. Only repetition stops a reply, never length: thinking that keeps saying new things is left alone however long it runs. Tool-call arguments (a file being written) aren't watched.
- The same call, the same result. The third time a call with the same arguments returns the same result in one task, the result the model sees gets a note saying that calling it again won't change anything. At the fifth, the turn ends. A different result (a test run after an edit, say) is progress and resets nothing; an output cleared to save context doesn't count, since running the tool again is what the model was told to do.
- Start
llama-serverwith--jinja; tool calling needs it. - Summaries, chat titles and memory notes ask llama-server to skip thinking
(
chat_template_kwargs: {enable_thinking: false}). With Qwen3.5-4B a compaction took 100–196 s with thinking (and once returned nothing) and 12–22 s without. Other servers aren't sent it. - Thinking (
reasoning_content) streams into a collapsible "Thinking" block that follows the newest text. It is never capped. - With
--parallel 1, chats that run at the same time wait in line at the server. Switching between agents is cheap anyway: llama-server's RAM prompt cache (--cache-ram, default 8 GB) restores the previous agent's prompt instead of recomputing it (measured: ~1–2 s). That cache uses system RAM. Add--cache-ram 4096if memory gets tight. - System prompts contain the date but not the time, and the memory profile is fixed per chat, so prompts stay identical between turns and the server's prompt cache keeps working.
- The app asks llama-server for per-token timings (the live context meter); other servers aren't sent that field and get an estimate instead.
- For images, load a vision model with its projector (
--mmproj). Without one the app sends the text alone and tells you.
Measured on a laptop (2026-10-12, Qwen3.5-4B Q4_K_M on an RTX 3050 with 4 GB, llama.cpp
b11564, -np 1): with a 16k context, fully offloaded, it ran about 29 tok/s, and the Orchestrator
had the Coder build a todo app with 7 passing tests in 2 min 47 s. With 8k it ran about
45 tok/s, but multi-file coding hit the 30-step limit before it finished. For agent work give a
small model at least 16k.
app/main.py HTTP API + SSE streams, workspace browser, exports
app/runner.py agent loop, delegation + sub-chats, subagents, approvals, background runs, titles, memory extraction
app/history.py what the model sees; auto-compaction; image parts
app/memory.py long-term memory (SQLite + FTS5)
app/mail.py IMAP/SMTP email tools
app/agenda.py read-only calendars from iCal feeds
app/routines.py scheduled agent runs
app/llm.py streaming OpenAI-compatible client
app/tools.py file, shell (with live output) and web tools
app/browser.py Playwright + Brave browser tools
app/mcp.py MCP servers (stdio): processes, tool lists, find_tools/call_tool
app/store.py JSON storage: agents, chats (pin, fork), settings
app/defaults.py starter agents
static/ the web UI: index.html, style.css, js/ (ES modules, no build step)
tui/ the Rust terminal client (ratatui); talks to the same API
bin/agent-chat the launcher (start the app, open a client, update, uninstall)
site/ the install page and install.sh, served on Render
scripts/ build-release.sh (the terminal client for this machine), publish-release.sh
.github/ CI on every push and PR; release builds of the terminal client
data/ agents, chats, settings (JSON), memory.db, secrets.json
tests/ mock model server, unit tests, end-to-end tests (web, browser, TUI), run_all.sh
Everything the UI does goes through /api/… (see app/main.py). State-changing requests
need the header X-Agent-Chat: 1 (so another website can't make them from your browser), and
the server only answers requests addressed to 127.0.0.1/localhost unless you add hosts
with AGENT_CHAT_HOSTS. A few handy ones:
GET /api/health ok, version, running runs
GET /api/state agents, chats, settings, tools
POST /api/chats {agent_id} new chat; POST /api/chats/{id}/run {content, images, from_index}
GET /api/chats/{id}/stream SSE: snapshot, then every event of the run
POST /api/chats/{id}/fork {upto} branch; PATCH /api/chats/{id} {title, pinned, agent_id}
GET /api/chats/{id}/export.md|.json exports; POST /api/chats/delete {ids}
POST /api/chats/{id}/approvals/{aid} {approve, all}
POST /api/chats/{id}/compact {instructions?} summarize older messages now
GET /api/workspace?agent_id&path folder listing; GET /api/workspace/file?agent_id&path[&download]
GET /api/agents/{id}/prompt the composed system prompt; PUT /api/agents/order {ids}
GET /api/search?q= chats whose messages contain q
GET /api/mcp MCP servers: running, tools, last error; POST /api/mcp/{name}/test
tests/run_all.sh # starts the mock model + the app on scratch ports, runs everything
tests/run_all.sh --no-ui # skip the real-browser suites (no Brave on this machine)That runs, in order: tests/test_unit.py (history, compaction, routines, memory, store),
tests/e2e.py (chat, tools, approvals, delegation, stop, edit), tests/e2e_features.py
(sub-chats, statuses, compaction), tests/e2e_cutoff.py, tests/e2e_loops.py (the loop
guard), tests/e2e_ctx.py,
tests/e2e_queue.py, tests/e2e_memory.py, tests/e2e_routines.py, tests/e2e_v2.py
(pinning, branching, exports, approve-all, titles, workspace, images, live shell output,
agent order), tests/e2e_calendar.py, tests/e2e_mcp.py (MCP servers, against the fake
server in tests/fake_mcp.py), tests/e2e_subagents.py (subagents: their tools, results in
call order, side by side or one at a time, Stop part way, errors, nesting), tests/e2e_browser.py (real headless Brave over a
local page), tests/e2e_ui.py (the web UI itself in headless Brave: streaming,
approvals, files, palette, branching, images, unread, errors, keyboard, mobile) and, when
cargo is installed, the Rust unit tests (cargo test in tui/) and the terminal client in a
pseudo-terminal, read back from an emulated screen: tests/e2e_tui.py (the main flows, pastes,
slash commands), tests/e2e_tui_keys.py (every key in every pane and dialog, server outages,
CLI flags), tests/e2e_tui_render.py (hostile text, emoji, every size from 1×1 up, resize
storms; each step checks the screen matches a full repaint) and tests/e2e_tui_slash.py
(every command and alias in every state). tests/e2e_ui_more.py does the same for the web
slash menu, the chat footer and huge pastes. TEST_PORT_BASE=8900 tests/run_all.sh moves every
port, so two runs can share a machine; SUITES=unit,subagents runs just those; SKIP_TUI=1
skips the terminal client.
CI (.github/workflows/ci.yml) runs all of it on every push and pull request: the app and
API suites, the web UI in headless Chrome, the Rust tests on Linux and macOS, the four
terminal-client suites side by side, and the installer end to end on Linux and macOS
(tests/install_smoke.sh). A failing suite's logs and screenshots are kept as an artifact.
To run one suite by hand, start the mocks and the app yourself:
uv run uvicorn tests.mock_llm:app --port 8766 & # fake model
MOCK_NCTX=3000 uv run uvicorn tests.mock_llm:app --port 8768 & # tiny context
MOCK_OVERFLOW_CHARS=6000 uv run uvicorn tests.mock_llm:app --port 8769 &
MOCK_NO_VISION=1 uv run uvicorn tests.mock_llm:app --port 8770 & # rejects images
AGENT_CHAT_DATA=/tmp/ac-test AGENT_CHAT_ROUTINE_TICK=1 uv run uvicorn app.main:app --port 8767 &
export AC_URL=http://127.0.0.1:8767
curl -X PUT $AC_URL/api/settings -H 'content-type: application/json' -H 'X-Agent-Chat: 1' \
-d '{"base_url":"http://127.0.0.1:8766/v1"}'
uv run python tests/e2e_v2.py
# web search needs SearXNG on :8888 and the live web:
uv run python tests/e2e_search.py
# email: run a test IMAP + SMTP server, point Settings → Email at them, then:
# uv run --with pymap pymap --port 1143 --no-tls dict --demo-data &
# uv run --with aiosmtpd python -m aiosmtpd -n -l 127.0.0.1:1025 &
uv run python tests/e2e_mail.pyEnvironment variables: AGENT_CHAT_DATA (data folder), AGENT_CHAT_WORKSPACE (default
workspace), AGENT_CHAT_HOSTS (extra allowed host names, for example to reach it from your
phone over the LAN; you also need to start uvicorn with --host 0.0.0.0), PORT (for
run.sh and install-service.sh).
- Set the version in
app/main.py,pyproject.toml,tui/Cargo.tomlandsite/install.sh(PINNED_VERSION), and give it a section inCHANGELOG.md. git tag vX.Y.Z && git push origin vX.Y.Z:.github/workflows/release.ymlbuildsagent-chat-tuifor linux-x86_64, linux-aarch64, macos-aarch64 and macos-x86_64.scripts/publish-release.sh vX.Y.Zwaits for that build, checks the checksums and publishes the release with the CHANGELOG section as its notes.agent-chat updateand the installer take the newest release.
MIT.







