Agent sessions

agentd runs coding agents as Bus sessions. Anything on the Bus can start one: Workshop, a terminal, a Mix script, or another agent. Each running agent's output is published as Bus events, so any number of watchers can follow a turn. When the agent asks permission to do something, the question goes to the human on the desk they are looking at, through notifyd.

agentd speaks the Agent Client Protocol (ACP) to the agent. ACP is the open standard that most coding agents already support, so one integration covers them all. agentd adds what ACP lacks: addressing across the mesh, many watchers, and a human in the loop. Every session also gets mixos-mcp, so the agent can use the Bus in turn.

 Bus callers ── agent.* commands, agent.session.* topics ──► agentd
                                                              │ ACP over the agent's stdio
                                                              ▼
                                                     agent (claude-agent-acp, …)
                                                              │ MCP
                                                              ▼
                                                 mixos-mcp ──► back onto the Bus

Run it as a session service: mix --serve agentd.mix --name agent. It needs a mix with process_watch stdin pipes (stdin: "pipe", process_write).

Configuration

agentd reads $MIXOS_AGENTD_CONF, else <etc>/agent.conf.mix. The file is strict data:

max_live: 4
agents: {
  claude: {argv: ["claude-agent-acp"], roots: ["/home/user/src"]},
  gemini: {argv: ["gemini", "--acp"], roots: ["/home/user/src"]},
  opencode: {argv: ["opencode", "acp"], roots: ["/home/user/src"], policy: "ask"}
}
  • argv: the agent's ACP command, run directly (no shell).

  • roots: the directories a session's cwd and additional_dirs must lie under. Paths are resolved first, so a symlink cannot escape. This confines only what the agent asks agentd for. The agent's own tools, such as a shell, are not sandboxed by agentd.

  • policy:

    • ask (the default) sends every permission request to the human.
    • allow_once lets agentd answer allow_once by itself.

    agentd never answers allow_always.

  • permission_timeout_s (default 300): with no answer by then, the request is rejected. No answer is never yes.

  • max_live (default 4): live sessions at once. It is a hard cap, and agent-started sessions count against it.

  • log_keep_days (default 30): how long the session log keeps a session after it ends.

Commands

Bodies are JSON objects.

CommandBodyReply
agent.agentsthe configured agents, live count and cap
agent.session.new{agent, cwd, mode?, additional_dirs?}{session, status} at once; ready follows as an event
agent.session.prompt{session, content}{session, turn}; the turn streams as events
agent.session.cancel{session}cancels the running turn
agent.session.close{session}ends the agent
agent.session.set{session, mode} or {session, option, value}sets a mode, or a config option such as the model
agent.session.listlive and recently ended sessions
agent.session.read{session, since?, limit?}the session and its logged events after seq: {session, events, more, next}
agent.permission.respond{session, request, option}answers a permission request

content is a string, or a list of ACP content blocks.

A read returns at most limit events (default and maximum 2000) and about 32 MiB. When more is true, read again with since set to next.

The acting commands (new, prompt, cancel, close, set, respond) take an optional id of 1-128 characters from [A-Za-z0-9._:-]. Ids are scoped to the session (to new for agent.session.new). A retry with the same id gets the first reply and never runs again, so a caller that lost a reply can safely send the command again. If agentd stopped after logging a command but before replying, the retry gets COMMAND_INTERRUPTED; the command is not run again.

Refusals carry {error_code, message}:

rcerror_code
10USAGE, OUTSIDE_ROOTS or UNKNOWN_AGENT
14UNKNOWN_SESSION
15AGENT_RUNNING (a turn is already running), AGENT_NOT_READY or COMMAND_INTERRUPTED
16AGENT_LIMIT or AGENT_DEPTH
17AGENT_SELF

Events

TopicBody
agent.session.updated{session, seq, kind, …}
agent.session.ended{session, exit_code, signal}
agent.permission.requested{session, request, title, options}

kind on agent.session.updated is one of:

  • initialized or ready;
  • an ACP update kind (agent_message_chunk, tool_call, plan, …), with the update unchanged in update;
  • stop, with stop set to end_turn, max_tokens, max_turn_requests, refusal or cancelled;
  • permission_answered;
  • error;
  • ended, logged (not published) when a restart ends a session.

seq rises by one per event in a session. A watcher that missed events reads them back with agent.session.read {since}.

Session log

agentd writes every event and every acting command to a SQLite log before it publishes the event or runs the command: $MIXOS_AGENTD_DB, else <var>/agentd/sessions.db. The log outlives agentd. After a restart:

  • ended sessions and their events can still be listed and read;
  • command receipts still answer retries;
  • the agents that were running are gone (agentd owned them), so their sessions are ended with a last ended event whose reason is agentd_restart.

Sessions are dropped from the log log_keep_days after they end.

If the log cannot be written (a full disk), commands are refused, but the agent's events are still published and the gap is reported on stderr. A watcher that follows seq should therefore also check the session's status in a read: after such a gap and a restart, the ended event can carry a seq the watcher has already seen.

Agents calling back

agentd runs every agent with MIXOS_AGENT_SESSION set to its session id, and that agent's mixos-mcp registers on the Bus as mcp-s-<session>-<suffix>. The broker stamps that name as the sender of everything the bridge sends, so agentd recognises a session's own agent:

  • It cannot act on its own session (AGENT_SELF): no answering its own permission requests, no prompting or closing itself.
  • A session it starts is a child, and a child cannot start sessions (AGENT_DEPTH).
  • The max_live cap holds for everyone.

This catches the agent's MCP path. An agent with a shell can still open a Bus client of its own, which is why the cap and the human answer matter.

This page as Markdown · This page describes the current main branch of MixOS.