Boatman Ecosystem documentation is live!
Architecture
Event Protocol

Event Protocol

The CLI and Desktop communicate through a structured JSON event protocol emitted via stdout.

Boatman now also has a provider-neutral runtime event contract in shared/agentruntime. The legacy protocol below remains the default for backward compatibility. Set BOATMAN_RUNTIME_EVENTS=1 to emit a normalized runtime event after each legacy event during migration.

Protocol Design

Format

  • Encoding: Newline-delimited JSON (NDJSON)
  • Transport: stdout (CLI process)
  • Direction: CLI → Desktop (one-way)
  • Parsing: Line-by-line, ignore non-JSON lines

Why NDJSON?

  • Streaming-friendly (no buffering required)
  • Standard format used by logging systems
  • Easy to parse with bufio.Scanner
  • Easy to filter with grep '^{'

Event Schema

{
  "type": "string",           // Required: event type
  "id": "string",             // Optional: unique identifier
  "name": "string",           // Optional: human-readable name
  "description": "string",    // Optional: detailed description
  "status": "string",         // Optional: "success" or "failed"
  "message": "string",        // Optional: progress message
  "data": {}                  // Optional: additional metadata
}

Schema Rules

  • Events must have a type field (string)
  • Use snake_case for event types
  • Optional fields: id, name, description, status, message, data
  • The data field is free-form for phase-specific metadata

Event Types

TypePurposeKey Fields
agent_startedAgent begins executionid, name, description
agent_completedAgent finishesid, name, status
progressGeneral progress updatemessage
task_createdInternal task created (reserved)id, name, description
task_updatedTask status change (reserved)id, status

Runtime Event Bridge

The runtime event bridge maps legacy events to provider-neutral event names:

Legacy TypeRuntime Type
agent_startedphase.started
agent_completedphase.completed
task_createdtask.created
task_updatedtask.updated
progresslog.message
claude_streamprovider.raw

The bridge preserves raw provider payloads in provider.raw events while giving new UI and runtime consumers stable fields such as phaseId, provider, status, usage, tool, approval, artifact, and schema.

Broker-backed providers emit tool.call and tool.result when a model requests a local tool through shared/agentruntime/toolbroker. The raw provider event is still preserved, but UI and audit consumers can rely on the normalized tool payload:

{"version":1,"type":"tool.call","provider":"openai-responses","tool":{"id":"call_1","name":"Read","input":{"file_path":"README.md"}}}

Integration brokers emit integration.state when a service descriptor changes health or configuration state. The normalized status field maps to runtime status, while data.state preserves the integration-specific state:

{"version":1,"type":"integration.state","name":"datadog","status":"waiting","message":"integration is missing required configuration","data":{"state":"needs_configuration","missing_env":["DD_API_KEY","DD_APP_KEY"]}}

boatman integrations check --emit-events prints run.started, integration.state, and run.completed events for local integration health checks. If runtime-store env vars are configured, the same events are also persisted for boatman runs list and boatman runs show.

Memory loaders emit memory.loaded when inspectable runtime memory documents are rendered for a run. The data.documents array carries document IDs, scope, title, path, provenance, and source run IDs when available:

{"version":1,"type":"memory.loaded","runId":"run-1","role":"memory","status":"completed","data":{"documents":[{"id":"project","scope":"project","title":"Project Memory"}]}}

Use boatman memory list, boatman memory show <id>, and boatman memory context --emit-event --run-id <run-id> to inspect the same Markdown memory documents and event shape locally.

Runtime events can also be persisted by shared/agentruntime/runstore. Project-scoped provider runs now record under .boatman/runs by default. Set BOATMAN_RUNTIME_STORE_DIR to an explicit directory, or set BOATMAN_RUNTIME_STORE=0 to disable default recording. Each run directory contains:

metadata.json
request.json
events.ndjson
artifacts.json

Recorded runs can be inspected with:

boatman runs list
boatman runs show <run-id>
boatman runs show <run-id> --json
boatman runs request <run-id>
boatman runs artifacts <run-id>

request.json stores the original provider-neutral RunRequest so future resume and replay tooling can reconstruct the model call without scraping logs. artifacts.json is a compact index derived from artifact.changed events, so files, diffs, PRs, and URLs can be inspected without replaying the full event stream.

The desktop subprocess bridge recognizes these runtime event types and ignores them for legacy task/message updates during the transition. This lets the CLI emit both streams without duplicating UI state.

See Runtime Platform for the target architecture.


Agent ID Convention

IDs follow {step}-{taskID} for uniqueness and traceability:

StepPatternExample
Prepareprepare-{taskID}prepare-ENG-123
Worktreeworktree-{taskID}worktree-ENG-123
Planningplanning-{taskID}planning-ENG-123
Preflightpreflight-{taskID}preflight-ENG-123
Executeexecute-{taskID}execute-ENG-123
Testtest-{taskID}test-ENG-123
Reviewreview-{N}-{taskID}review-1-ENG-123
Refactorrefactor-{N}-{taskID}refactor-2-ENG-123
Commitcommit-{taskID}commit-ENG-123
PRpr-{taskID}pr-ENG-123

Integration Pipeline

┌─────────────────┐
│  BoatmanMode CLI │
│                 │
│  Emits JSON to  │
│  stdout         │
└────────┬────────┘
         │ {"type": "agent_started", ...}

┌─────────────────────────────┐
│  boatmanmode/integration.go │
│                             │
│  bufio.Scanner              │
│  json.Unmarshal             │
│  Emits Wails event          │
└────────┬────────────────────┘
         │ runtime.EventsEmit("boatmanmode:event", ...)

┌─────────────────────────────┐
│  useAgent.ts (React hook)   │
│                             │
│  EventsOn("boatmanmode:event")
│  HandleBoatmanModeEvent()   │
└────────┬────────────────────┘
         │ Updates session tasks

┌─────────────────────────────┐
│  Tasks Tab (React component)│
│                             │
│  Displays agent progress    │
│  Icons: in_progress/done/fail│
└─────────────────────────────┘

Adding a New Event Type

1. Define in CLI

// cli/internal/events/emitter.go
func MyNewEvent(id, name string) {
    Emit(Event{
        Type: "my_new_event",
        ID:   id,
        Name: name,
    })
}

2. Emit in CLI

// cli/internal/agent/agent.go
events.MyNewEvent("agent-123", "My Agent")

3. Handle in Desktop Backend

// desktop/app.go
case "my_new_event":
    id, _ := eventData["id"].(string)
    name, _ := eventData["name"].(string)
    // Handle the event

4. Update Desktop Frontend

// desktop/frontend/src/hooks/useAgent.ts
const eventHandler = (data: BoatmanModeEventPayload) => {
    if (data.event.type === 'my_new_event') {
        // Handle in React
    }
}

5. Document

Update event protocol docs in both components.


Best Practices

  • Always emit agent_completed after agent_started (use defer)
  • Include task ID in agent IDs for uniqueness
  • Use descriptive names and descriptions
  • Emit progress events for long-running operations
  • Keep the data field minimal (only phase-specific metadata)