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
typefield (string) - Use
snake_casefor event types - Optional fields:
id,name,description,status,message,data - The
datafield is free-form for phase-specific metadata
Event Types
| Type | Purpose | Key Fields |
|---|---|---|
agent_started | Agent begins execution | id, name, description |
agent_completed | Agent finishes | id, name, status |
progress | General progress update | message |
task_created | Internal task created (reserved) | id, name, description |
task_updated | Task status change (reserved) | id, status |
Runtime Event Bridge
The runtime event bridge maps legacy events to provider-neutral event names:
| Legacy Type | Runtime Type |
|---|---|
agent_started | phase.started |
agent_completed | phase.completed |
task_created | task.created |
task_updated | task.updated |
progress | log.message |
claude_stream | provider.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.jsonRecorded 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:
| Step | Pattern | Example |
|---|---|---|
| Prepare | prepare-{taskID} | prepare-ENG-123 |
| Worktree | worktree-{taskID} | worktree-ENG-123 |
| Planning | planning-{taskID} | planning-ENG-123 |
| Preflight | preflight-{taskID} | preflight-ENG-123 |
| Execute | execute-{taskID} | execute-ENG-123 |
| Test | test-{taskID} | test-ENG-123 |
| Review | review-{N}-{taskID} | review-1-ENG-123 |
| Refactor | refactor-{N}-{taskID} | refactor-2-ENG-123 |
| Commit | commit-{taskID} | commit-ENG-123 |
| PR | pr-{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 event4. 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_completedafteragent_started(usedefer) - Include task ID in agent IDs for uniqueness
- Use descriptive names and descriptions
- Emit progress events for long-running operations
- Keep the
datafield minimal (only phase-specific metadata)