The Ensemble

Conduct and manage multiple agent sessions with precision.

Terminal multiplexers have recently experienced something of a renaissance as a key tool in driving multi-agent workflows. Steve Yegge even mentions learning tmux as a key skill in agentic coding and the ability to capture agent output and inject “typing” programmatically is indeed extremely useful when trying to drive agent coding harnesses. This is particularly the case as the various harness providers start to limit certain plans to only using their designated harness. Even if they weren’t these providers are tuning their LLMs to work best with their agent harness so there are distinct advantages to staying withihn their respective ecosystems.

In Ragtime rather than relying on external muxers like tmux we implement our own dedicated muxer that is focused largely on agent integration. These other muxers were developed for a different sort of application and for example largely don’t have programmatic APIs that you can access and things like that. By building that entirely into Ragtime we are able to orchestrate agents directly and integrate them into the Starlark rule system in a harness-agnostic way. Using our own muxer also lets us introduce environment variables to enhance The Bridge as rt hook and rt statusline can capture shell information for better integration. You can still use outside agents of course, but you get more capabilities when working inside the Ensemble.

Correlated Shell Sessions#

rt sh launches a shell (or wraps any command) with full Ragtime integration:

rt sh                         # interactive shell with ragtime context
rt sh new -- claude           # launch Claude Code with correlation
rt sh new --name orchestrator -- claude  # named session

Shells launched via rt sh automatically set two environment variables:

  • RAGTIME_SOCKET — the daemon socket path, so any rt command inside the shell talks to the same daemon instance
  • RAGTIME_SHELL_ID — a unique ID for this PTY session, included in every hook event the agent fires

That shell ID is the thread that ties everything together. Hook events, statusline telemetry, and PTY output all carry it. In the dashboard and in the FUSE filesystem, you can trace every tool call back to the shell it came from.

% ls ~/.ragtime/fs/agents/
claude-abc123/

% cat ~/.ragtime/fs/agents/claude-abc123/output.log
# live PTY output from that session

% echo "wrap up, context window is getting full" \
    > ~/.ragtime/fs/agents/claude-abc123/input
# writes directly to the agent's PTY stdin

The Message Bus: Agent-to-Agent Communication#

The Ensemble’s coordination primitive is a filesystem-backed message bus declared by the builtin:message-bus rule. Each active agent session gets an inbox directory under ~/.ragtime/fs/messages/<session-id>/. Writing a message to an inbox delivers it as a nudge — injected into the agent’s context the next time it’s between turns.

Here’s the rule in full:

name: builtin:message-bus
priority: -100
vfs:
  - root: messages
    table: messages
    dyn_key:
      source: active_sessions   # one subdirectory per active session
      field: session_id
    dirs:
      - name: inbox
        filter: {state: pending}
        writable: true
      - name: read
        filter: {state: delivered}
match:
  event: "vfs-write"
  vfs_path: "messages/*/inbox/**"
actions:
  - type: starlark
    script: |
      parts = event.vfs_path.split("/")
      if len(parts) >= 4:
          session_id = parts[1]
          raw_name = parts[3]
          msg_id = raw_name[:-5] if raw_name.endswith(".json") else raw_name

          data = {}
          if event.vfs_data:
              decoded = json.decode(event.vfs_data)
              if type(decoded) == "dict":
                  data = decoded

          data["session_id"] = session_id
          data["state"] = "pending"

          msgs = db.table("messages")
          msgs.upsert(msg_id, data)

          text = data.get("text", "")
          if text:
              agents.nudge(session_id, text)

The dyn_key block is what makes this namespace different from the task system — instead of fixed subdirectories, there’s one inbox per session, sourced dynamically from the list of active sessions. The path structure becomes messages/<session-id>/inbox/<message-id>.json.