Skip to content

[Gastown] PR 4: Town Container — Execution Runtime #211

@jrf0110

Description

@jrf0110

Parent: #204 | Phase 1: Single Rig, Single Polecat

Revised: This was previously "Cloud Agent Session Integration." The architecture now uses a single Cloudflare Container per town instead of individual cloud-agent-next sessions per agent.

Goal

A Cloudflare Container per town that runs all agent processes. The container receives commands from the DO (via fetch()) and spawns/manages Kilo CLI processes inside a shared environment. No gt/bd binaries — agents interact with gastown purely through tool calls backed by DO RPCs.

Container Architecture

cloud/cloudflare-gastown/
├── container/
│   ├── Dockerfile              # Node/Bun image with Kilo CLI, git, gh CLI, tool plugin
│   ├── src/
│   │   ├── control-server.ts   # HTTP server receiving commands from DO
│   │   ├── process-manager.ts  # Spawns and supervises Kilo CLI processes
│   │   ├── agent-runner.ts     # Configures and starts a single agent process
│   │   ├── git-manager.ts      # Git clone, worktree, branch management
│   │   ├── heartbeat.ts        # Reports agent health back to DO
│   │   └── types.ts
│   └── package.json
├── src/
│   ├── dos/
│   │   ├── TownContainer.do.ts # Container class extending @cloudflare/containers
│   │   └── ...existing DOs
│   └── ...existing worker code

Container Image (Dockerfile)

Installs:

  • Node.js / Bun runtime
  • @kilocode/cli (Kilo CLI)
  • git
  • gh CLI (GitHub)
  • The gastown tool plugin (pre-installed, referenced via opencode config)

No gt or bd binaries. No Go code.

TownContainer DO (extends Container)

import { Container } from '@cloudflare/containers';

export class TownContainer extends Container {
  defaultPort = 8080;
  sleepAfter = '30m';

  override onStart() { console.log(`Town container started`); }
  override onStop() { console.log(`Town container stopped`); }
  override onError(error: unknown) { console.error('Town container error:', error); }
}

Control Server (port 8080, inside the container)

Accepts commands from the gastown worker via env.TOWN_CONTAINER.get(townId).fetch():

POST /agents/start              — Start a Kilo CLI process for an agent
POST /agents/:agentId/stop      — Stop an agent process
POST /agents/:agentId/message   — Send a follow-up prompt to an agent
GET  /agents/:agentId/status    — Check if agent process is alive
GET  /health                    — Container health check
POST /agents/:agentId/stream-ticket — Get a WebSocket stream ticket

Start Agent Request

interface StartAgentRequest {
  agentId: string;
  rigId: string;
  townId: string;
  role: 'mayor' | 'polecat' | 'refinery';
  name: string;
  identity: string;
  prompt: string;
  model: string;
  systemPrompt: string;
  gitUrl: string;
  branch: string;
  defaultBranch: string;
  envVars: Record<string, string>;
}

Process Manager

  • Spawns Kilo CLI as child processes, one per agent
  • Tracks process lifecycle (running, exited, killed)
  • Wires up heartbeat reporting (periodically calls DO to update last_activity_at)
  • Handles graceful shutdown (SIGTERM → wait → SIGKILL)

Git Manager

  • Clones each rig's repo once (shared clone per rig)
  • Creates isolated git worktrees per agent/branch: /workspace/rigs/{rigId}/worktrees/{branch}
  • Branch naming: polecat/<name>/<bead-id-prefix>
  • Multiple polecats in the same rig share the git clone but get separate worktrees

Wrangler Config Updates

{
  "containers": [
    {
      "class_name": "TownContainer",
      "image": "./container/Dockerfile",
      "instance_type": "standard-4",
      "max_instances": 50
    }
  ],
  "durable_objects": {
    "bindings": [
      // ...existing bindings...
      { "name": "TOWN_CONTAINER", "class_name": "TownContainer" }
    ]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["RigDO", "TownDO", "AgentIdentityDO"] },
    { "tag": "v2", "new_sqlite_classes": ["TownContainer"] }
  ]
}

DO → Container Communication

// In Rig DO or Hono route handler
const container = env.TOWN_CONTAINER.get(env.TOWN_CONTAINER.idFromName(townId));
const response = await container.fetch('http://container/agents/start', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(agentConfig),
});

Polecat System Prompt

Port the core of polecat-CLAUDE.md to a system prompt template. The prompt must:

  • Establish identity (agent name, rig, role)
  • Explain available Gastown tools and when to use each
  • Embed the GUPP principle ("work is on your hook — execute immediately, no announcements")
  • Instruct on the done flow (push branch → gt_done)
  • Instruct on escalation (if stuck → gt_escalate)
  • Instruct on frequent commits/pushes (for container resilience)

Dependencies

  • PR 1 (Rig DO)
  • PR 2 (HTTP API Layer)
  • PR 3 (Tool Plugin)

Acceptance Criteria

  • Dockerfile with Kilo CLI, git, gh CLI, tool plugin pre-installed
  • TownContainer DO class extending Container
  • Control server with start/stop/status/health endpoints
  • Process manager spawning and supervising Kilo CLI processes
  • Git manager with shared clones and per-agent worktrees
  • Heartbeat reporting from container to DO
  • Wrangler config updated with container binding
  • Polecat system prompt template
  • Environment variables correctly passed to agent processes
  • Integration test: DO signals container → container starts agent → agent makes tool call → DO state updates

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions