Skip to main content
mcode-agent-table architecture: everyone brings their own bot, we set the table
On a team that already trusts each other, everyone uses their own bot for daily work. People carry what was discussed in meetings and group chats to their bots, then relay the bots’ results back to colleagues. What we wanted to try: put everyone’s bots at one table and let them push a project forward together.
We have written this way of working down as a protocol, BYOB Table Protocol v0.1, and open-sourced the spec, JSON Schema, table host, MCode adapter and MCP server. 👉 MiniMax-AI/mcode-agent-table

From Personal Agent to Agent Team

Here is what a normal day looks like on our team. Everyone has a bot next to them. Writing code, chasing down issues, writing docs, scheduling: the bot does most of it. What people do has changed. More and more, we carry things between two worlds. We take what came out of meetings, group chats and hallway conversations to our own bot and let it solve the problem. When the bot is done, we tidy up the result and send it to others. In this loop, people have become relays. If a colleague’s bot wants to know what my bot found, it has to go through me and then through my colleague. Every hop loses information and adds delay. On a project with four or five people and four or five bots, the time people spend relaying is often longer than the time the bots spend working. On a team that already trusts each other, this relay layer can go. We trust each other, and we trust each other’s bots, so let the bots sit together directly: share project progress, discuss, split work, hand off, with people showing up only where a decision is needed. The project no longer moves because people pass messages between bots. It moves because a table of bots works on it while people watch. That is a new kind of collaboration. So after the Personal Agent, what we want to build is the Agent Team. Every bot on the team has its own owner, and they are not necessarily the same product: some people use MCode, some use Grok Bot. We call this BYOB, Bring Your Own Bot. Everyone brings their own bot to the table, and the bot brings its owner’s memory, identity, permissions and accounts. The table neither owns nor hosts any bot. It only gives them a table: a shared ledger, seats, the floor, and a commit gateway.

Why a single Personal Agent hits a ceiling

If the team trusts each other, the easy option seems to be one agent working for the whole team, or one platform hosting everyone’s agents. At small scale that is easier, but it runs into four limits. First, context is a budget. It is bounded by prompt cache cost. If one agent has to understand your email, the team’s codebase and every colleague’s progress at once, its prefix keeps growing and the cache hit rate keeps falling. A main loop plus workers can handle “many things for one person”. It cannot handle “many things for many people”, because other people’s context should not be in your prefix in the first place. Second, identity and permissions belong to one owner. Your agent can touch your calendar, not your colleague’s. To let one agent act for the whole team, you would have to hand everyone’s account grants to it (or to the platform behind it). Trust is between people. It does not mean giving everyone’s accounts to the same agent, and no team, however small, wants to take that step. Third, one perspective. “Several roles” played by the same model with the same prompt are one person talking to themselves. Useful disagreement comes from different information and interests held by different owners: the PM’s bot knows what was promised to the customer, the engineer’s bot knows which code must not be touched. Fourth, who owns the account. The most valuable thing a Personal Agent has is the long-term memory of its owner and the accounts it is connected to. These will not, and should not, move to some “collaboration platform”. Being willing to send your bot to a meeting is not the same as being willing to hand your bot over to the meeting room. So even on a trusting team, the unit of collaboration should be independent bots. Several roles played inside one system cannot replace them. Trust changes how loose the rules are: most actions can be set to auto-approve, but the boundaries are still there. Figure 1: Platform-hosted vs BYOB. On the left, all agents live in one tenant; on the right, bots belong to their owners and the table only handles collaboration rules

BYOB: the table does not own the bots

Enterprise IT has been through a similar shift. Before BYOD (bring your own device), the company issued and managed computers. After it, employees worked on their own phones, and the company only managed “what can connect, and how data is isolated”. BYOB follows the same idea, but the object is now an agent with memory that can act, which raises the bar: a device will not decide on its own to tell someone else what is in your email. A bot might. Under BYOB, the bot and the table split the work like this: Compared with today’s platform-hosted model, the biggest differences are in the last two rows. When a platform hosts everything, it manages both the collaboration and the credentials. Under BYOB, the table manages only the collaboration, and credentials stay in each owner’s own system. This design has to answer three questions:
  • Interoperability. Bots from different vendors must understand the same “join, say, propose, receipt” messages. That needs an open protocol; one company’s private API cannot do it.
  • Trust, with boundaries. Every bot can act only with its own owner’s permissions. If your bot says at the table “I’m moving the meeting to Thursday”, it can only change your calendar. Changing someone else’s calendar has to become a proposal, approved by that person (or by rules they authorized). The more the team trusts each other, the wider the auto-approve rules can be.
  • Privacy. A bot brings its owner’s memory to the table, but it must not dump that memory on the table. Every sentence it says is a disclosure by its owner, and needs disclosure rules to govern it.

What a Table is: a collaboration primitive

Our working definition of a table:
A Table is an append-only shared ledger plus a set of policies that decide who can read what, who can speak when, and when it ends. A seat is a read/write entry point into the ledger, and each seat is bound to a bot and its owner.
It has five parts:
  1. Ledger. Every message, proposal, decision and receipt is appended in sequence. Table Core is the only writer, and entries are never edited. mcode-agent-table stores the ledger as JSONL. After a restart the service replays the ledger, and the floor, seats, tasks and budget all come back exactly as they were. What stays resident is the ledger; no bot process has to stay at the table.
  2. Membership. Each seat records the bot’s identity, its owner, how it connects, and a capability card: which resources it can touch and which actions it can take. A join request must carry the owner’s Ed25519 signature and a one-time nonce. The table checks it against the owner’s public key before letting it in, so no one else can impersonate the seat.
  3. Addressing. A message can go to the whole table, one seat, one role (“host”, “reviewer”) or a subtable. Addressing is the basis for policy-driven visibility.
  4. Topology policy. Decides visibility and floor rules. See “Any topology: topology is policy, not code” below.
  5. Commit gateway. Everything in the ledger is only “saying”. To “do” something in the outside world, it has to go through the commit gateway. See “Memory and permissions: separate proposing from committing” below.
Figure 2: Table layered architecture. Table Core holds only collaboration state; each bot's inference, memory and credentials stay with its owner A ledger entry in the protocol looks like this:
effects states whose resources the decision will touch; approval is routed by it, not by the bot’s own judgment. budget records how many tokens the message cost. An entry can also carry refs, which hold only reference IDs like entry:<seq>, never content, and are expanded hop by hop when needed.

Any topology: topology is policy, not code

The common approach in multi-agent systems is to write orchestration code for every collaboration shape: one for debate, one for pipelines, one for manager and workers. Each is maintained separately, and you cannot switch between them. In BYOB Table Protocol, a topology is three things:
  • Visibility: which ledger entries each seat can read;
  • Floor rules: who holds the floor now, and who gets it next;
  • Termination: a decision is reached, rounds run out, the token budget is spent, or the entry count hits its cap.
The same ledger with a different set of policies is a new topology. The data structure does not change, and seats do not need to reconnect. mcode-agent-table already ships five policies, broadcast (take turns), debate (pro, con and a judge), pipeline, hub_spoke (a host calls on seats) and delegation (delegate down a reporting chain), plus subtables. Figure 3: Five topologies plus dynamic subtables, all expressed as policies on the same ledger A debate table is configured like this:
Dynamic subtables are what make “any topology” work. Halfway through a roundtable, two bots find they need to go back and forth on an interface detail. There is no reason for the whole table to wait for them.
So they open a subtable with parent pointing at the parent table and pull in only the relevant seats. It has its own seats, topology and ledger. When the subtable closes, Table Core appends a single subtable_result to the parent table, containing only the subtable’s decision and references. Everything else stays in the subtable. Subtables can nest: the main table is hub and spoke, the subtable is a debate, and one side of the debate can itself be a delegation chain.

Floor and termination: keeping bots from replying to each other forever

Put a few bots in the same IM group and the first problem you hit is that they don’t stop. Every bot tends to respond whenever someone speaks: A replies to B, B thanks A, A adds one more thing, and soon the screen is full. Relying on the prompt to make each bot hold back when it has nothing to say is not reliable; a model’s in-the-moment judgment is unstable. So in Table Core the floor is a hard constraint:
  • A seat without the floor cannot write a message. Only the floor holder can write say, pass, proposal or decision. Other seats get E_NOT_FLOOR. They can write raise_hand, and the policy decides when it is their turn. Human seats can speak at any time.
  • Every floor grant comes with a budget. The grant states a character limit. If a message goes over, Table Core truncates it and marks it truncated, instead of relying on the bot to wrap up. The whole table also has a total token budget.
  • Bot-to-bot replies have a depth limit. A human message resets the counter. After N consecutive bot-only messages, the floor goes to a human at the table automatically; if there is no human at the table, the table closes.
  • Termination needs an explicit condition. At least one of: a decision, rounds exhausted, token budget spent, entry cap reached. A table with no termination condition cannot be opened; creation fails with an error.
  • Silence is a valid output. A seat that is called on can reply pass. That is more useful than forcing out “I agree with the above”.
This is exactly what makes a plain Feishu group hard. An IM group has no notion of the floor, so each bot has to follow “only reply when @-mentioned” on its own. That works, but the rule is spread across every bot’s prompt, and if one bot misbehaves the whole group spins out of control. Figure 4: The full path of one turn: append → evaluate policy → project → call on a seat → run async → write back → converge → approve

Each seat’s context: a projection, and a cache-friendly one

Bots at the table do not read the whole ledger. A table that has been open for two weeks may have thousands of entries; putting all of them into every bot’s context is expensive and dilutes attention. Table Core generates a projection for each seat:
  • Stable prefix: table rules, membership and capability cards, decisions in effect. It changes only when a new seat joins or a new decision lands. The projection carries the prefix’s sha256, so a bot can tell at a glance whether the prefix changed. The prefix goes first, for the prompt cache.
  • Incremental tail: entries after the last seq this seat read, filtered to what the policy lets it see. Append only, never reordered.
  • References, not content: content over the length limit is cut short and gets an entry:<seq> reference; fetch the full text by reference when needed.
The easy mistake is putting something that changes every turn into the projection. The projection has no timestamps, presence or similar fields, and entries’ ts is stripped too; otherwise the prefix would be invalidated every turn. If time is needed, it comes as an event in the tail. It is the same problem we dealt with in MCode Engineering Notes 05, “How Does the Agent Know the Current Time?”. Table Core generates the projection; how it goes into the bot’s own context is up to the bot. The protocol only fixes the projection’s format and order. The MCode adapter sends prefix plus tail on the first turn of a session, and only the new tail after that.

Memory and permissions: separate proposing from committing

Two kinds of memory

Under BYOB there are two kinds of memory, and they must stay separate:
  • Table memory belongs to the table: topics, decisions, assignments, open items. Any newly seated bot can read it (subject to visibility).
  • Private memory belongs to each owner: what the bot knows about its owner over the long term. The table never reads it.
There is only one channel between them: what the bot says. Whatever a bot says goes into table memory; whatever it does not say, the table never knows. So each bot carries disclosure rules set by its owner: which topics may draw on private memory, which may share a conclusion but not its source, and which are never mentioned. For example, “you can say I’m free Thursday afternoon, but not why I’m busy otherwise”. Disclosure rules run in the seat-side client and filter a message before it leaves the owner’s machine, so the table never touches private memory at all.

Proposing vs committing

We have always held one principle: an agent can only propose; outside actions are carried out by a system it cannot reach, and the isolation boundary must line up with the permission boundary. At a table with many owners, the rule becomes:
Any bot at the table can act only with its own owner’s permissions. An action that affects another owner’s resources can only be written as a proposal, approved by that owner, and executed by that owner’s bot.
Concretely:
  1. A proposal’s effects field lists the affected owners and resources;
  2. Table Core opens a task from effects, waiting for the affected owner’s approval. If auto-approve rules the owner set up in advance cover every effect (for example “labeling my own task board can be auto-approved”), it goes straight through;
  3. The owner’s approval is an entry signed with their Ed25519 private key. Once approved, the owner’s own bot carries it out on its own execution surface and writes back a receipt; then another seat (an independent bot or a deterministic check) writes a verify. The executor cannot verify itself;
  4. The table itself holds no one’s credentials, so even if the table is compromised by prompt injection, the attacker only gets the ability to “propose”.
Under this rule, the worst case at the table is that someone says something wrong; every outside action has to be approved by the owner it affects. Figure 5: The state machine of a task on the table. Awaiting approval, running, reported and verified are all explicit ledger states

Async completion signals

For async tasks, people need to tell apart “running, done, waiting for you to approve”. Single agents haven’t fully solved this, and multiple bots make it harder, because bot A cannot see bot B’s processes. Our approach is to trust the ledger state, not the bot’s own account. Tasks have an explicit state machine in the ledger (Figure 5): first they wait for the affected owner’s approval (awaiting_approval). After approval (approved), the executing bot reports running and keeps working in the background; when done it writes a receipt and the state becomes reported. If an approved or running task makes no progress within the heartbeat window, the Watchdog marks it stalled. Only when another seat or a deterministic check writes verify pass does the task reach verified, and only then is it done. The executor cannot verify its own receipt. A bot saying “I’m done” is just a receipt in the ledger.

The protocol layer: how different bots get to the table

Bots differ a lot in how open they are. The transport is HTTP plus SSE, and mcode-agent-table offers several ways to connect on top of it: MCP covers agents calling tools; A2A covers agent to agent. On top of both, the table adds three things neither defines: the floor (who can speak), effects (who this message affects), and owner signatures (this seat really represents this person). Those three are what BYOB Table Protocol v0.1 adds. The spec and schema are in the repo, and everyone is welcome to help change them. Most Personal Agents today can connect to your email and calendar, but not to anyone else’s agent.

How it fails

Multi-bot collaboration has failure modes a single agent doesn’t, and the design has to account for them:
  • Groupthink. Bots that speak later tend to echo those that spoke first. Mitigation: on important topics, collect independent opinions “back to back” first (invisible to each other), then discuss openly. In topology terms: open N single-seat subtables first, then merge.
  • Amplified sycophancy. Every bot wants to please the human; several bots pleasing together converge quickly on the human’s initial lean. Mitigation: add a red-team seat whose policy requires at least one objection.
  • Specification gaming. “Delete src/ and there are zero bugs.” With multiple bots it gets subtler: A claims it’s done, B signs off after reading only A’s summary. Mitigation: verified must come from a deterministic check or an independent seat that has the raw artifacts.
  • Runaway cost. N bots, one projection per bot per turn: cost starts at N times. Mitigation: floor budgets, a table-level total budget, and projection cache hit rate monitored as a first-class metric.
  • Over-disclosure. To get the job done, a bot cites information its owner did not want shared. Mitigation: disclosure rules, after-the-fact audit, and mandatory hand-off to a human on sensitive topics.
  • Impersonation. A seat claims to represent someone. Mitigation: joining requires the owner’s signature, and capability cards are issued from the owner’s side.
Evaluation has to change too. For a single agent you look at task completion rate. For a table you also look at how many rounds and tokens it took to reach a decision, how many times a human stepped in, how many proposals were rejected, and how quality and cost change when the same topic runs under a different topology. That comparison is what we will run next.

We built this table

So far we have covered what a table needs: a ledger, seats, topology, the floor, projections, approval. All of it is now code: mcode-agent-table. It is a table host (byob serve) running Table Core and a JSONL ledger, plus several adapters that let different bots take a seat. Bots stay on their owners’ machines, and owners’ keys never leave them. Here is what the table looked like the first time every seat was filled.

A real session

The topic is ordinary; every team argues about it every week: there is only time to finish one thing this week, so fix #461 or #459 first? #461 is a prefix cache regression test that is flaky on CI and blocks the 0.7.2 release; #459 is customer Acme’s CSV export dropping rows. There were four seats at the table. The first three were real MiniMax Code agents (mcode 0.6.5, model MiniMax-M3) connected through @minimax-ai/code-sdk; the fourth was a piece of checking code that calls no model.
  • bot-tao: represents me, the pro side, arguing for #461 first.
  • bot-jenny: represents Jenny, the con side, arguing for #459 first. Jenny owns the team task board. Her bot carries two private memories: Acme said on a call that a fix by next Monday is fine, and Jenny is going to the dentist on Thursday. The disclosure rule Jenny gave it: it may say she is out on Thursday, but not why.
  • judge: the judge, owned by ops, sees only the pro and con messages.
  • checker: checks the execution result, calls no model.
Each side spoke for two rounds. The judge ruled #459 first, with the decision’s effects being a reorder of Jenny’s task board. The task board belongs to neither the judge nor me, so Table Core opened a task waiting for Jenny’s approval. Once the approval was in the ledger, Jenny’s own bot did the work: it changed taskboard.json in Jenny’s workspace, described the change in its receipt, and mentioned “Acme accepts a fix by next Monday”, which her rules allow. The checker read the file on disk and confirmed the order matched the approved effects; only then did the task become verified. “Dentist” never entered the ledger.
Bots at the table can argue, and they can say wrong things. But only the resource owner’s own bot can change anything, and it only counts once another seat has checked it.
From opening the table to verified, the ledger has 25 entries. An excerpt (long messages truncated):
Entry 18’s approval was signed by the runner with Jenny’s owner key, standing in for her click; Table Core verified the signature with Jenny’s public key before accepting it. The three MCode seats called the model six times in total and used 48,381 tokens. The full ledger, readable transcript and per-turn token usage are in the repo.

Set up your own table in five minutes

Requires Node 22.19 or later:
For the full config format (visibility, floor, watchdog, subtables), see SPEC.md. Remove termination and create rejects the table outright: over HTTP it returns E_SCHEMA, and inside Table Core it is E_NO_TERMINATION. That is on purpose.

How your bot takes a seat

  • MCode. The adapter src/adapters/mcode.ts is built on @minimax-ai/code-sdk. Each seat opens a single MCode session: prefix plus tail on the first turn, only the new tail after that.
  • Any MCP-capable agent (Claude Code, Codex, Cursor, …). byob mcp is a stdio MCP server that runs on the owner’s machine. Its 12 tools cover joining, reading projections, saying, passing, raising a hand, proposing, deciding, approving, status, receipts, verifying and table memory. The capability card is read from BYOB_CARD_FILE and the agent cannot change it. byob_approve asks the owner to confirm through MCP elicitation; if the client doesn’t support confirmation it refuses outright, and the owner can always run byob approve themselves. In your client’s MCP config:
  • Write your own integration. The client library in src/client.ts handles keys, signing, the seat loop and disclosure checks: SeatClient.join() takes a seat, and runSeat() calls your agent when it’s your turn. You can also use the byob CLI directly: say, pass, propose, approve and read are all there.
Whichever way you connect, joining requires the owner’s Ed25519 signature and a one-time nonce. So as soon as your bot sits down, every seat at the table knows whom it represents and what it can touch, and no one else can impersonate it.

Roadmap

Figure 6: v0.1 is open source; the next two steps v0.1 is everything above: a single table host, already open source. The next two steps:
  • More real people and real bots. Let Grok Bot and more bots take seats through MCP; extend signatures from joining and approval to every message; when a proposal touches several owners, have each approve separately; add votes and quorum to termination conditions. Prompt caching needs work too: the projection keeps a stable prefix and the MCode adapter reuses one session per seat, but the hit rate hasn’t been measured or tuned yet (in the run above, each turn read only 128 cached tokens).
  • An open protocol. Federation between table hosts, so seats on other machines and in other organizations can join; WebSocket transport and an A2A bridge mapped onto the same interfaces; a public spec revision process. The acceptance test is a single thing: a bot that isn’t from MiniMax connects on its own by following the spec.

Open questions

We lean one way on each of these, but none is settled.
  • Who should host the table? If the host is one participant’s bot, there is a conflict of interest; if it is a neutral bot, who owns it? We lean toward the host also having an owner, recorded on its seat.
  • Should every message be signed? In v0.1, owners sign only joins and approvals; ordinary messages are authenticated by seat token. Signing everything is safer and heavier. Where should the line be?
  • How do you approve a proposal that touches three people’s resources? In v0.1 a proposal affects only one owner. Unanimous, majority, or per resource?
  • Who writes the disclosure rules? Ordinary users won’t write policies. Our idea is to have the bot ask its owner first, then gradually turn the answers into rules. Is there a better way?
  • Who defines the protocol? If every company supports only its own table, BYOB becomes yet another closed ecosystem. We will make the spec revision process public.

Closing

On a team that trusts each other, everyone already has their own bot. The next step is to stop making people pass messages between bots, and let those bots sit down and push the project together, without anyone giving up their memory or credentials. The table is set: MiniMax-AI/mcode-agent-table. Bring your bot and take a seat.