Skip to content

About

Local multi-agent chat harness for OpenAI-compatible servers (llama.cpp, Ollama, ...): agents that delegate to and question each other, a shared memory, email + calendar + browser agents, scheduled routines, and self-compacting long chats.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

Agent Chat

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.

Screenshots

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.

The welcome page: nine starter agents, each with its own job and tools

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.

Orchestrator handing off to Coder, with a shell command awaiting approval

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.

An assistant reply with an expanded thinking block and per-reply stats

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.

The workspace drawer next to a finished delegation

The command palette searching message text

A shared memory about you grows as you chat; pin the facts every agent should always see.

The memory panel with pinned and categorized facts

Light theme, if you prefer it.

The Planner in the light theme

Install

curl -fsSL https://agent-chat-i4sv.onrender.com/install.sh | bash

That 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/.

Run from a checkout

# 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:8765

On 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.

Using it

  • 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 decisions says 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); /usage and /context repeat 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

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.

Slash commands

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.

Terminal client

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

The terminal client: the same handoff, approvals included

  • Everything is on the keyboard: F1 lists it. Ctrl+K is the same palette as the web UI (chats by content, agents, commands); n or 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 (plain y / a / n when 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: c copies it, r regenerates it, b branches from it, and o opens the chat of its first handoff or subagent. In the sidebar, / filters, d deletes, r renames, p pins, e edits the agent and m marks 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 c on 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; --quiet stops that.
  • What you toggle (thinking, verbose, sidebar, drawer, filter, theme) is kept in agent-chat-tui/state.json in your config folder (~/.config on Linux).

Starter agents

Agent Purpose Tools
💬 Assistant General chat team
📧 Mail 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.

Tools

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.

How agents talk to each other

  • 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: true to 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_agent call, 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.

Subagents

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 --parallel gives four slots that share one context (--kv-unified), and four growing subagents can fill it. vLLM, or llama-server with --parallel N and -c large 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.

Memory

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 with recall_memory, and fix outdated facts with forget_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.

Email

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

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.

Calendars

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 .ics file 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

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 to 127.0.0.1 only. Enable whichever engines work from your network.
  • Check engines: open <your-searxng>/stats to see which engines fail, and toggle them in its settings.yml.
  • Leave the URL empty to fall back to scraping DuckDuckGo directly (often rate-limited).

Browser control

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_navigate and 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 sync pulls in Playwright. You don't need playwright install, because the agent drives your installed Brave rather than a downloaded Chromium.

MCP servers

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.

  1. Settings → MCP: paste the server's config in the mcpServers shape 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"}}}}
  2. Test starts the server and shows how many tools it has and what they cost in context.
  3. 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.

Long chats: auto-compaction

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.

Loop guard

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.

Notes for llama.cpp

  • Start llama-server with --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 4096 if 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.

Files

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

API

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 (no GPU needed)

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.py

Environment 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).

Releasing

  1. Set the version in app/main.py, pyproject.toml, tui/Cargo.toml and site/install.sh (PINNED_VERSION), and give it a section in CHANGELOG.md.
  2. git tag vX.Y.Z && git push origin vX.Y.Z: .github/workflows/release.yml builds agent-chat-tui for linux-x86_64, linux-aarch64, macos-aarch64 and macos-x86_64.
  3. scripts/publish-release.sh vX.Y.Z waits for that build, checks the checksums and publishes the release with the CHANGELOG section as its notes. agent-chat update and the installer take the newest release.

License

MIT.

About

Local multi-agent chat harness for OpenAI-compatible servers (llama.cpp, Ollama, ...): agents that delegate to and question each other, a shared memory, email + calendar + browser agents, scheduled routines, and self-compacting long chats.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages