Manch (मंच) — the stage. The place where the puppets perform and the audience connects. An open, framework-agnostic substrate for hosting AI agents.
Manch is reusable plumbing for hosting AI agents. It exists so the products built on top of it spend their effort on domain logic, not infrastructure. It is domain-free by rule: nothing in Manch knows what a contract, a case, or a patient is.
Status: design + skeleton. Core APIs are not yet stabilised. See Status & first milestone.
Is:
- A set of Rust crates you embed as a library to host AI agents.
- An extension surface: implement a trait, register it, done — no forking.
- A bridge to external agents over ACP (Agent Client Protocol) and tools over MCP.
- Optionally, a thin self-hostable server (chat + Telegram engine) built on the same core.
Is not:
- A web service you must run (the server is one optional consumer, not the center).
- A model reseller or billing system.
- A product with domain knowledge (legal/health logic lives in consumers, never here).
- Library-centric, not server-centric. The core is headless and framework-free. A Tauri app, a CLI, a server, and any domain product all embed the same core. The application is a thin, disposable consumer.
- Functional / DI-free leaning. Capabilities are passed explicitly and composed,
not injected through a runtime container.
dyn Traitis used only at genuine extension boundaries (the rim), not throughout (the core). FP core, thin polymorphic rim. - Follow standards, own the ergonomics. ACP is already an open standard (authored by Zed, Apache-licensed, community-governed). Manch does not reinvent it — it builds the reusable, framework-agnostic host that the ecosystem lacks.
Nothing below the application layer may name a domain. No "legal", no "health", no "contract", no "patient".
Domain products are consumers that implement Manch's traits in their own repositories and register them at startup. The test:
Can a consumer depend on
manch-corewithout pulling in Tauri or anything domain-specific? If yes, it's a library. If no, logic has leaked into the shell — pull it back down.
The moment a domain noun appears inside a manch-* crate, it has stopped being substrate.
┌─────────────────────────────────────────────────────────────┐
│ CONSUMERS (own repos · domain-specific) │
│ │
│ domain products manch-server manch-app │
│ (own Tools) (open, self- (Tauri reference) │
│ hostable) │
└───────────────┬───────────────────────────────────┬──────────┘
│ register Agents / Tools / Channels via builder()
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ manch-core — runtime: registries + prompt/tool loop │
│ framework-free · domain-free · the seam gate │
└───────┬───────────────┬────────────────┬───────────────┬─────┘
│ implements │ │ │
▼ ▼ ▼ ▼
┌───────────────┐ ┌────────────┐ ┌──────────────┐ ┌──────────────┐
│ manch-protocol│ │ manch-acp │ │manch-channels│ │ manch-memory │
│ THE CONTRACTS │ │ ACP host │ │ CLI/Telegram │ │ SQLite store │
│ Agent Tool │ │ (on official│ │ /webhook │ │ + context │
│ Channel │ │ acp crate)│ │ (Channel) │ │ assembly │
│ MemoryStore │ │ │ │ │ │ (the seam) │
└───────────────┘ └────────────┘ └──────────────┘ └──────────────┘
│ │
└───────────────────┬────────────────────────────┘
▼
┌────────────────────────────────┐
│ kathputli (actors over tokio) │ ← already published,
│ katha (event sourcing) │ now with a home
└────────────────────────────────┘
Dependencies flow strictly downward. Lower layers never depend on upper layers.
katha and kathputli have zero internal dependencies.
| Crate | Responsibility | crates.io |
|---|---|---|
katha |
Event sourcing — the append-only story of what happened. | v0.1.1 |
kathputli |
Thin actor framework over Tokio — addressable, message-driven puppets. | v0.1.1 |
| Crate | Responsibility |
|---|---|
manch-protocol |
The contracts: Agent, Tool, Channel, MemoryStore traits + shared message/event types. The single source of truth for how you extend Manch. |
manch-core |
The runtime. Holds the agent/tool/channel registries, owns the MemoryStore, and runs the prompt → agent → tool → stream loop. No web framework, no domain. |
| Crate | Responsibility |
|---|---|
manch-acp |
Reusable, framework-agnostic ACP host. Wraps an external ACP agent (Claude Code, Gemini CLI, Codex) as a Manch Agent. Built on the official agent-client-protocol crate for wire types. |
manch-channels |
Inbound/outbound surfaces behind the Channel trait: CLI, Telegram, webhook. Each channel is an opt-in Cargo feature and a separately published crate, so a consumer (e.g. a future domain product) can depend on just manch-channel-telegram and wire it in as-is — no fork, no all-or-nothing dependency. |
manch-memory |
Default MemoryStore (SQLite, local-first). Context assembly lives here — the hard retrieval/summarisation/compaction problem is isolated behind one method so it can be iterated without touching anything else. |
| Consumer | Responsibility |
|---|---|
manch-app |
Thin Tauri reference app. One consumer among many; proves the core stands alone. |
manch-server |
Optional, open, domain-free self-hostable server: chat + Telegram engine (personal-agent surface). Secure-by-default (see Security model). |
| Domain products | Implement Tool (and friends) with their own logic, in their own repos. |
Everything an external developer extends is a trait in manch-protocol.
| Trait | What it abstracts | Example impls |
|---|---|---|
Agent |
How a model/agent is invoked and streams events back. | BYOK provider (Claude/GPT/Gemini), ACP child process, local Ollama model. |
Tool |
What an agent can do. This is where domain products plug in. | docx generation, a workflow, an external API call. |
Channel |
How the outside world reaches an agent. | CLI, Telegram, webhook. |
MemoryStore |
How sessions persist and how context is assembled. | SQLite default; swap for Postgres or a retrieval-backed strategy. |
PermissionPolicy |
Whether a Draft tool's proposed action reaches a human, and with what options. |
ask-once (built in), a consumer's remembered-decision policy, deny-by-default. |
- In-process (Rust): implement a trait, register with
Manch::builder(). Typed, fast, compile-time. Best for Rust contributors and for domain products. - Out-of-process (any language): speak ACP;
manch-acphosts you. No recompile, no source access. This is the primary, language-agnostic "build on it" path.
Manch follows Zed's external-agents model:
a session runs against either a key you bring or an agent CLI already on
your machine. Either way it's just a registered Agent — one interface, two
implementations. A BYOK provider and an external CLI agent both implement the
same [Agent] trait and both stream back ACP's own event vocabulary, so the core
and the UI never branch on which kind they're talking to. (Zed proves this works:
its native agent and its external ACP agents share one connection abstraction.)
- BYOK (bring your own key) — a direct provider connection (Claude / GPT /
Gemini / local Ollama).
manch-coreowns theprompt → tool → re-promptloop and supplies host-registeredTools. You hold the key; Manch never resells. - BYOC (bring your own CLI) — an external agent (Claude Code, Gemini CLI,
Codex, …) launched as a subprocess and driven over ACP by
manch-acp. The external agent owns its own auth, model selection, and tools; Manch is the ACP client and streams its events through. No recompile, no source access.
Every host-registered Tool declares a tier(): Read (safe to auto-execute)
or Draft (a proposed mutation). Three things about this are deliberate:
tier()is required, with no default. A default ofTier::Readwould hand auto-execution to any tool author who never stopped to consider the question. Declaring risk is not optional.- There is no
Nevervariant. A tool that must never run is a tool that is not registered — absence is not something a runtime needs to check for, so encoding it as a tier would only add a state nothing can reach. - A
Draftcall suspends the turn; it does not block a thread. When aPermissionPolicywants a human,manch-corereturnsTurnOutcome::AwaitingApprovaland the caller resumes later with a decision — the suspension can outlive the request, cross a process boundary, or wait on a Telegram reply. Consumers who just want the linear "ask, then run" shape can useManch::handle_with_approver, a thin loop overhandle/approvefor exactly that case.
Found during implementation; documented rather than hidden.
- A discarded batch can leave a UI showing a call that has no stored
record. When a turn issues several tool calls and a later one in the same
batch errors, the whole batch is dropped from memory —
MemoryStoreis append-only with no compensating delete, so atomicity comes from not writing yet, not from rolling back. But a call earlier in the batch that already succeeded has already emittedToolCallStatus::Completedto the event sink before the batch's fate is known. A UI driven off that stream can therefore show a call as completed that, moments later, corresponds to nothing in the persisted transcript. - Gemini tool-call ids are synthesised, and repeat calls are paired by
order, not identity. Gemini's wire format supplies no call id, so Manch
synthesises one from the part's position in the response. Gemini also keys
functionResponse(the result) by tool name, not by id — so when one turn calls the same tool twice, which result belongs to which call is genuinely ambiguous on Gemini's own wire. Manch preserves issue order (never reordering or merging), which is the best available reading, not a guaranteed-correct one.
Manch's BYOK path does not sit on a Rust LLM framework (e.g. rig). Every
serious agent host we surveyed — Zed, ZeroClaw, and AionUi's aionrs engine —
hand-rolls thin per-provider clients over reqwest, and Manch does the same. The
reason is control: the provider features that matter (prompt-caching betas,
OAuth / subscription auth, reasoning params, the Responses API, streaming quirks)
land in a framework — if ever — long after they ship in the raw API, and auth
schemes like Anthropic OAuth, Bedrock SigV4, or Vertex service accounts don't fit
a one-size abstraction at all.
The work is smaller than it looks, because providers cluster into a few wire dialects, not N bespoke integrations:
| Wire dialect | Build cost | Covers |
|---|---|---|
| Anthropic Messages | one client | Claude |
| OpenAI Chat / Responses | one client | OpenAI |
OpenAI-compatible (same client, different base_url + key) |
~zero extra | xAI/Grok, Groq, Together, Fireworks, DeepSeek, vLLM, LiteLLM, Ollama's /v1 |
Gemini generateContent |
one client | Gemini |
Bedrock (official aws-sdk) |
later | Claude/others on AWS |
So ~three hand-rolled clients plus one OpenAI-compatible config layer cover the entire list. Per-provider quirks (max-tokens field name, base URL, schema sanitisation) are data, not branches.
Pointing a client elsewhere. Every BYOK agent resolves its endpoint as
explicit argument → MANCH_{PROVIDER}_BASE_URL → vendor default, and the
catalog endpoint is derived from the same base as the completion endpoint:
// an OpenAI-compatible third party, or a self-hosted gateway
OpenAiAgent::new(key, None).base_url("https://api.fireworks.ai/inference/v1");# same effect for every OpenAI-dialect agent in the process
export MANCH_OPENAI_BASE_URL=https://api.fireworks.ai/inference/v1The explicit argument wins deliberately: a server routing several tenants through different proxies needs per-agent endpoints in one process, which an environment variable alone cannot express.
Listing models, though, is a free function — there is no instance
list_models, so an agent built with .base_url(proxy) does not redirect the
catalog on its own. A caller that overrode the base per agent (rather than via
the env var, which list_models does read) must thread that base through
itself, reading it back with base():
let agent = OpenAiAgent::new(key, None).base_url("https://api.fireworks.ai/inference/v1");
let models = manch_llm::list_models_at("openai", &key, Some(agent.base())).await?;Token usage. Providers report token counts differently — Anthropic splits
input and output across two frames, Gemini repeats a running total per chunk,
OpenAI sends one final block (and only when asked). Manch normalises them to
AgentEvent::Usage and forwards them as they arrive without accumulating. This
is for display, never for billing: a managed tier meters at its own proxy rather
than trusting a client-side total.
Provider roadmap: Anthropic → OpenAI → OpenAI-compatible (lights up xAI / Groq / Together / Fireworks / vLLM / LiteLLM at once) → Gemini, Ollama → Bedrock / Vertex on demand.
rigis not banned. If it ever saves real work for one provider it can be dropped in as a singleimplbehind the same trait — never as the load-bearing interface.
How a CLI agent gets discovered (a superset of Zed — registry + config + detection):
| Source | How |
|---|---|
| Built-in catalog | Known agents ship with default launch commands; offered when their binary resolves. |
| PATH detection | If a known agent binary (e.g. claude-code, gemini, codex) is found on PATH, it's offered automatically — this is the "whatever CLI is installed" path. |
| Explicit config | A Zed-style agent_servers block — { command, args, env } — registers any custom ACP agent the catalog doesn't know. |
Prompt flow
Channel/UI → manch-core.prompt(agent_id, session, message, sink)
→ MemoryStore.assemble_context(...) // pluggable strategy
→ Agent.prompt(context, tool_schemas, sink)
→ [stream] AgentEvent::ToolCall
→ manch-core dispatches Tool.call(args)
→ feeds ToolResult back, re-prompts
→ AgentEvent::Done
→ events stream back out to the originating Channel/UI
Extension flow
A domain product NEVER edits Manch source. It:
1. implements Tool (and/or Agent/Channel/MemoryStore) in its OWN repo
2. registers via Manch::builder().tool(Arc::new(MyDomainTool))
3. ships its own binary that embeds manch-core
That single .tool(...) call is the entire open-substrate / closed-moat boundary,
expressed in code.
An internet-reachable agent with shell/file tools and a Telegram command path is a real RCE surface. The self-hostable server ships secure-by-default:
- Auth on by default (token-based); bound to
localhostunless explicitly opened. - Telegram bot tokens scoped; inbound commands authenticated to a known user.
- Shell / file-system tools off or sandboxed unless explicitly opted in.
- Loud README warning about the exposure footgun.
- Secrets never hardcoded; user-supplied API keys encrypted at rest, never logged.
- Model billing / gateway — a separate project. End-users connect directly to Gemini / Claude / OpenAI with their own credentials and pay the provider. Keeping it out of Manch avoids the reseller-liability and account-coupling traps.
- Domain logic — see The one rule.
- A mandatory server — the server is optional; the library is the product.
| Name | Meaning | Role |
|---|---|---|
| Katha (कथा) | story / narrative | event sourcing |
| Kathputli (कठपुतली) | puppet on strings | actor framework |
| Manch (मंच) | the stage / platform | the agent substrate |
Each name maps to its function. Katha records, Kathputli performs, Manch presents.
just setup # install JS deps, generate proto bindings, install git hooks
just ci # run everything CI runs: fmt, clippy, tests, JS lint/test/build
just test # all tests (Rust + JS)
just --list # see all recipesGit hooks (via Lefthook) run fmt + lint on commit and clippy + tests on push.
manch-protocol will be published to crates.io by release-plz. Nothing is published yet: the workflow is parked (manual trigger only) until a consumer has exercised the crate surface at runtime.
Tagging vX.Y.Z builds desktop installers and the server Docker image
(ghcr.io/manchhq/manch-server).
Using an AI coding agent? See AGENTS.md — the tool-agnostic guide
(commands, repo map, conventions). Claude Code, Gemini CLI, and Copilot read their
own pointer files (CLAUDE.md, GEMINI.md, .github/copilot-instructions.md), all
of which defer to AGENTS.md.
Status: design + skeleton. Core APIs not yet stabilised.
First milestone (proves the whole thesis):
One prompt — "What is the capital of India?" — answered correctly through both agent paths behind the single [
Agent] interface:
- BYOK Anthropic, via Manch's own hand-rolled Messages-API client (no
rig).- Claude Code, launched as a subprocess and driven over ACP.
Same question, same interface, two implementations. No streaming, no tools yet — just proof that the unified seam holds across a raw provider and an external CLI.
If that works, every other consumer (the server, any domain product) and every
other provider is just a different Agent/Tool registered on the same spine.