mew is a multi-frontend agent server. The canonical runtime architecture is Frontend → Daemon → Agent → Provider. A frontend can be the built-in TUI, the React web UI, or any other client that speaks the daemon wire protocol. The TUI can also run standalone (embedding the agent directly), but in daemon mode it is just another frontend.
This doc walks through how input reaches a provider stream, how the daemon owns sessions, how tool calls are collected and executed, and how the agent relates to the rest of the system. For the ratatui event loop, display store, and streaming markdown implementation, see TUI Architecture.
The pipeline
Section titled “The pipeline”Browser/Frontend → WebSocket → mew-daemon (Unix socket or TCP) │ SessionManager::create / attach │ Session { agent, clients, turn_lock } │ agent.run_with_parts(...) → AgentEvent │ translate_event → ServerMessage │ session.broadcast → all attached clientsThe web UI reaches the daemon through mew-web-bridge, a small TCP+WS
bridge that also serves the built React app:
Browser ──ws/http──▶ mew-web-bridge (127.0.0.1:9847) │ └── unix ws ──▶ mew-daemonFor mobile/remote access, the daemon can also listen over iroh (P2P
QUIC with holepunching and relays) using mew daemon --iroh. The
mew pair command generates a QR code that the iOS app scans to
connect. See iOS App and Mobile Core
Development for details.
Crate map
Section titled “Crate map”| Crate | Purpose | Key Types |
|---|---|---|
mew |
Binary entry point, CLI parsing | Cli, Commands, build_provider |
mew-tui |
Event loop, ratatui UI, App state | Event, EventLoop, App, Action |
mew-agent |
Conversation state, tool execution loop | Agent, AgentEvent, turn_loop |
mew-provider |
Provider trait + event stream | Provider, ProviderEvent, EventStream |
mew-tools |
Tool trait + built-ins | Tool, ToolCtx, ToolOutput, Sensitivity |
mew-hashline |
Line-anchored edits with hash staleness detection | Patcher, SnapshotStore, Patch |
mew-protocol |
Wire message types | ClientMessage, ServerMessage |
mew-daemon |
WebSocket server, session ownership | DaemonServer, Session, SessionManager |
mew-mobile-core |
Rust core for iOS/Android (UniFFI) | MobileCore, CoreEvent, SessionState |
mew-web-bridge |
TCP+WS bridge + static UI server | handle_connection, proxy |
mew-web-client |
TypeScript client for the wire protocol | MewClient, MewClientEvents |
mew-web-ui |
React chat UI | App.tsx, SessionState, ChatSurface |
mew-config |
config.toml + credentials + permissions | Config, PermissionEngine |
mew-session |
JSONL session persistence | Writer, Reader, Meta |
mew-catalog |
models.dev catalog with 24h cache | Catalog, Model, ThinkingVariant |
mew-mcp |
MCP server client + McpTool wrapper | McpClient, McpTool |
mew-hooks-runtime |
Subprocess plugin dispatcher | SubprocessDispatcher, PluginHost |
mew-context |
Discover AGENTS.md / CLAUDE.md from cwd up to git root |
ContextResolver |
mew-skills |
Skill discovery + loading from .mew/skills, .opencode/skills, etc. |
Skill, SkillRegistry |
mew-personas |
Switchable system prompts + model pinning + tool allowlists | Persona, PersonaLoader |
ratatui-mdstream |
Streaming markdown to ratatui Lines | MdStream, DocumentState |
Startup: building the agent
Section titled “Startup: building the agent”In standalone mode the run_tui function in main.rs builds the agent
locally. In daemon mode the agent is built once per session by the daemon’s
AgentBuilder closure. Frontends never construct an agent directly; they
connect to a session and send Prompt messages.
The builder pipeline is the same in both cases:
-
Resolve model from CLI flags, config, session state, or the persona pin. Falls back to
"deepseek-v4-flash". -
Build provider via
build_provider(cfg, cat, provider_id, model_id, raw). Matches the provider’sshapestring to an adapter:
match shape.as_str() { "openai" => Ok(Arc::new(OpenAIAdapter::new(...))), "anthropic" => Ok(Arc::new(AnthropicAdapter::new(...))), _ => anyhow::bail!("unsupported shape"),}For router providers, wraps small + big models behind Routed.
- Build tools via
build_tools(). Returns aVec<Arc<dyn Tool>>:
let mut tools: Vec<Arc<dyn Tool>> = vec![ Arc::new(Read), Arc::new(Write), Arc::new(Edit), Arc::new(Bash), Arc::new(Glob), Arc::new(Grep), Arc::new(Echo), Arc::new(ExitTool), Arc::new(ProgressUpdate), Arc::new(AskUser), Arc::new(ShellBackground), Arc::new(ShellMonitor), Arc::new(JobStatus), Arc::new(JobBlock), Arc::new(JobCancel), Arc::new(TodoCreate), Arc::new(TodoUpdate), Arc::new(TodoComplete), Arc::new(TodoDelete), Arc::new(TodoListTool),];The Skill tool registers only when skills are discovered. The
SwitchPersonaTool registers only when personas exist.
-
Load MCP servers via
connect_mcp_servers(). Each server’s tools are wrapped asMcpTooland appended viatools.extend(mcp_tools). -
Build permission engine from config + permission mode. Applies deny rules, ask rules, workspace escape tier, and permissive short-circuit.
-
Construct agent via
Agent::new(provider, dispatcher, writer, tools, session_id), then set catalog-derived fields: pricing, context window, vision support, workspace roots, subagent runner, flagged files, secrets.
The daemon passes a fresh mew_session::Writer for the session so every
message is persisted to that session’s JSONL log as it is produced.
How input becomes a provider stream
Section titled “How input becomes a provider stream”Each frontend has its own event loop and display store. The TUI’s is covered in TUI Architecture; here is the daemon-side path shared by all frontends.
- User submits a prompt in the web UI (or TUI connected via
--connect). - The frontend sends
ClientMessage::Prompt { text, attachments }over the WebSocket. - The daemon’s
handle_connectionlooks up the attachedSessionand acquiressession.turn_lockto serialize turns. - It broadcasts
ServerMessage::UserMessageto all attached clients so every frontend shows the prompt immediately. - It calls
agent.run_with_parts(text, vec![], Some(token))and pumps the resultingAgentEvents throughforward_events. translate_eventconverts eachAgentEventto one or moreServerMessages.Providerevents become wire provider events; channel-bearing events (PermissionRequest,AskUser) are assigned a request ID and stashed insession.pending_permissions/pending_ask_user.session.broadcast(msg)sends the wire event to every attached client.- Each frontend updates its local display store and re-renders.
Web UI
Section titled “Web UI”The web UI is a React app served by mew-web-bridge. It uses the
mew-web-client TypeScript library to speak the daemon wire protocol and
Zustand for local state. Key pieces:
mew-web-bridge(Rust): listens on TCP for browser HTTP/WebSocket connections, proxies WebSocket frames to the daemon’s Unix socket, and serves the built React app from embeddedmew-web-ui/dist/assets.mew-web-client(TypeScript): typedMewClientclass that manages the WebSocket, dispatches events, and provides promise-based request/response helpers.mew-web-ui(TypeScript/React): React app with TanStack Router. TheSessionStateZustand store holds messages, streaming state, model selection, and the session list;bridgeClientToStorewires client events to store actions.
The web UI supports session switching through a sidebar rail, model switching
via the ModelPill component, and permission/ask-user modals that any attached
client can answer. See Web UI Development for
build commands, component inventory, and the end-to-end event checklist.
The turn loop
Section titled “The turn loop”turn_loop (turn.rs) is the core agent loop:
- Build tool definitions from the agent’s tool map.
- Clone messages, apply
on_chat_messagehook, strip empty text parts. - Check if context compaction is needed (estimated tokens vs threshold).
- Build the request: system prompt, messages, tools, reasoning config.
- Call
provider.stream(req)to get anEventStream. - Stream events until the stream ends or cancellation:
PartStart: create a new assistant message part.PartDelta: append content to the current part.PartEnd: finalize the part.MessageEnd: record finish reason + usage.
- After the stream ends: check for pending tool calls.
- If no tool calls: end the turn.
- If tool calls: execute them sequentially, then loop back for another provider turn.
Tool execution happens in execute_pending_tool_calls. Each tool runs
sequentially. Permission requests are emitted as AgentEvent::PermissionRequest
with an oneshot::Sender. The agent blocks until the user responds.
Hashline edits
Section titled “Hashline edits”mew implements line-anchored file edits in crates/mew-hashline. The
high-level flow is:
readrecords a snapshot of the file content and returns a[path#hash]header plus numbered lines.- The model calls
edit_hashlinewith one or more[path#hash]sections and operations likeSWAP,DEL,INS.*,SWAP.BLK,REM, orMV. mew-hashline::Patcherpreflights every section in memory: it validates the hash, resolves block ops, checks seen-line bounds, and applies the edits to LF-normalized text.- If the live file has drifted, the patcher tries 3-way-merge recovery from
the in-memory
SnapshotStore. - Only when every section prepares successfully does the patcher commit writes, deletes, and moves.
The crate is filesystem-agnostic: it calls a small HashlineFs trait. The
edit_hashline tool provides a tokio-fs implementation. See
Hashline Internals for the full architecture and
extension points.
AgentEvent variants
Section titled “AgentEvent variants”The agent communicates with the active frontend through AgentEvent. In
standalone TUI mode this is delivered directly to App. In daemon mode it is
translated to ServerMessage and broadcast to every attached client:
| Variant | Purpose |
|---|---|
Provider(ProviderEvent) |
Raw streaming event from the provider |
ToolStart { call_id } |
Tool execution started |
ToolEnd { call_id, success } |
Tool execution finished |
PartUpdated { part_id, part } |
A part’s content/state changed |
PermissionRequest { call, tx } |
Request user approval (oneshot channel) |
SubagentStart { ... } |
A subagent was spawned |
SubagentStatus { ... } |
Subagent progress update |
SubagentEnd { ... } |
Subagent finished |
TodosUpdated { todos } |
Todo list changed |
PersonaSwitchRequested { name } |
switch_persona tool was called |
JobUpdate { ... } |
Background shell job state changed |
Error(String) |
Terminal error |
AskUser { questions, tx } |
Ask the user free-text questions |
Channel-bearing variants (PermissionRequest, AskUser) use oneshot::Sender
to receive the user’s response. In daemon mode, these become ID-paired wire
requests via ServerMessage::PermissionRequest / AskUserRequest.
Sessions and multisession
Section titled “Sessions and multisession”The daemon owns every session. Frontends connect over WebSocket and ask to
NewSession, AttachSession, or ListSessions. A session ID is a ULID
prefixed with sess_, generated by SessionManager::create and persisted in
the session directory.
Multi-session support is implemented today: SessionManager maintains an
active map of in-memory sessions and can resume idle sessions from disk on
demand. The web UI exposes this through a session rail (SessionRail.tsx) that
lists sessions, attaches to them, renames them, and deletes them.
One session, many clients
Section titled “One session, many clients”A WebSocket connection is bound to exactly one session, but a session can have
many attached clients at the same time. Session::attach_client assigns each
client a monotonically increasing client_id. Session::broadcast sends every
server message to all attached clients, dropping any that have disconnected.
This means you can have the web UI and another frontend viewing the same
conversation simultaneously. All clients see UserMessage, tool progress, and
RequestResolved. SessionHistory is sent only to the client that just
attached; other clients already have the state.
Active vs idle sessions
Section titled “Active vs idle sessions”| State | Where it lives | How it is reached |
|---|---|---|
| Active | SessionManager.active |
In memory, has at least one attached client |
| Idle | ~/.local/share/mew/sessions/<id>/ |
On disk; loaded into active on AttachSession |
| Empty | Same as idle, but with a zero-byte session.jsonl |
Auto-deleted on ListSessions |
SessionManager::list() returns both active and idle top-level sessions as
SessionInfo records, including model, provider, created/last-message times,
summary, and attached client count.
Session switching
Section titled “Session switching”A single frontend connection can switch sessions by sending a new
NewSession or AttachSession. The web UI keeps sessionId in localStorage
and re-attaches on reload; the session rail lets the user jump between
sessions without reconnecting the WebSocket.
Wire messages that change or inspect sessions:
| Message | Purpose |
|---|---|
NewSession { cwd } |
Create a fresh session |
AttachSession { session_id } |
Attach to an active or idle session |
ListSessions |
List all top-level sessions |
DeleteSession { session_id } |
Delete a session from disk and memory |
RenameSession { session_id, title } |
Set a custom title |
State ownership
Section titled “State ownership”| State | Owner |
|---|---|
| Canonical message history | Daemon Agent.messages + mew_session::Writer JSONL |
| Session metadata (title, summary, timestamps) | meta.json on disk |
| Active session registry | SessionManager.active in the daemon |
| Per-session load lock | SessionManager.loading |
| Attached client list | Session.clients in the daemon |
| Pending permission / ask-user requests | Session.pending_permissions / pending_ask_user |
| Display/render state | Each frontend (TUI App, web UI store) |
| Model/provider per session | Session.model / Session.provider |
| Permission mode per session | Agent.permission_mode |
Lifecycle
Section titled “Lifecycle”Client connects → NewSession { cwd } or AttachSession { session_id } → SessionManager creates or resumes the session → SessionReady + SessionHistory (to attaching client) → Prompt and streaming events broadcast to all clientsClient disconnects → detach_client → if last client: cancel any in-flight turn, remove session from activeIdle sessions are loaded from disk on AttachSession. The JSONL log is replayed
through Agent::load_messages, and the writer is reopened so new messages
append to the same file. Subagent sessions (meta.depth != 0) are excluded from
ListSessions and cannot be attached directly.
Multitenancy and isolation
Section titled “Multitenancy and isolation”mew is single-user, multi-session. The daemon runs as the local user and all sessions share the same OS identity, config file, credential resolution, and MCP server pool. There is no authentication or per-user sandboxing.
Session-level isolation exists for the things that matter to a conversation:
- Message history: each session has its own JSONL log and
Agent.messages. - Model/provider:
Session.model/Session.providerare per-session, andSwitchModelonly affects the attached session. - Permission mode:
SetPermissionModeis per-session. - Workspace roots: each session’s agent gets its own
workspace_roots(defaulting to thecwdsupplied inNewSessionor the current directory). - Turn lock:
session.turn_lockserializes turns within a session, but different sessions run independently.
What is not isolated:
- Config and credentials: all sessions read the same
config.toml, credentials, and provider defaults. - MCP servers: MCP tools are registered once per daemon startup and shared across sessions.
- Hooks/dispatcher: the same dispatcher instance is used for every session.
This is enough for one person to run many independent conversations, but it is not multi-tenant in the SaaS sense. A shared daemon would need user accounts, namespaces, and per-session config overrides before it could safely serve multiple people.
Current limitations
Section titled “Current limitations”Handover between frontends is partially supported but not seamless:
- The TUI in daemon mode (
mew chat --connect) always callsNewSession; it cannot attach to an existing daemon session, so/resumeis rejected. - The web UI stores the last session ID in
localStorageand callsAttachSessionon reload. Two tabs with the same ID will share the session. - There is no wire event for “another client attached/detached”, so a frontend cannot show presence or claim input focus.
- The last client to disconnect cancels the current turn and unloads the session from memory. There is no way to keep a session warm while switching frontends.
See Daemon Protocol for the wire-level message types and Session Handover for the target design.