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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions