echo is an agent observability workspace for watching AI coding agents in real time.
It has three main pieces:
- Backend: a Bun server that ingests agent lifecycle events, stores them in SQLite, and streams them over WebSocket.
- Envelope contract: shared TypeScript types and JSON Schema for every event adapter.
- Web frontends: visual workroom dashboards for observing sessions, navigation, thoughts, and human-in-the-loop requests.
The project is intentionally small: one Bun workspace, no Redis/Postgres/Docker requirement for local development, and clean interfaces where cloud infrastructure can be swapped in later.
- Capture lifecycle events from tools such as Claude Code.
- Watch active sessions in a browser-based workroom UI.
- Stream recent events to any WebSocket subscriber.
- Handle human-in-the-loop permission requests through the API or dashboard.
- Build a PlayCanvas editor-compatible bundle for embedded 3D runtime experiments.
- Bun >= 1.0
Install dependencies:
bun installStart the backend:
bun startThe backend listens on:
- HTTP:
http://localhost:4000 - WebSocket:
ws://localhost:4000/stream
Check that it is alive:
curl http://localhost:4000/health
# {"status":"ok"}Start a web dashboard in another terminal:
# Three.js / React Three Fiber app
bun run web:dev:r3f
# PlayCanvas app
bun run web:dev:playcanvasapps/
server/ Bun API + WebSocket backend
adapter-claude-code/ Claude Code hook adapter
hitl-helper-python/ Python helper for HITL polling flows
web/
package.json Nested web workspace root
app-r3f/ Three.js / React Three Fiber dashboard
app-playcanvas/ PlayCanvas dashboard and ESM bundle target
packages/ Web-only shared packages
packages/
envelope/ EventEnvelope types, schema, and event vocabulary
docs/ API, adapter, migration, and web planning docs
openspec/ Product/spec governance and archived changes
The legacy sonar/ and sonar-playcanvas/ roots are retained outside this package as temporary validation references. New web work should happen under apps/web/*.
Backend:
bun start # start the API server
bun dev # start the API server with --watch
bun test # run all workspace testsWeb apps:
bun run web:dev:r3f
bun run web:dev:playcanvas
bun run web:build:r3f
bun run web:build:playcanvas
bun run web:test:r3f
bun run web:test:playcanvas
bun run web:typecheck:r3f
bun run web:typecheck:playcanvasPlayCanvas editor bundle:
bun run web:build:esm
# apps/web/app-playcanvas/dist/esm/echo-web-playcanvas.mjsEvery adapter sends an EventEnvelope to the backend. Here is the smallest useful example:
curl -s -X POST http://localhost:4000/events \
-H 'Content-Type: application/json' \
-d '{
"envelope_version": 1,
"agent_kind": "claude-code",
"agent_version": "1.0.0",
"source_app": "my-app",
"session_id": "sess-001",
"event_type": "session.start",
"raw_event_type": "SessionStart",
"payload": {}
}'Open a stream with wscat:
wscat -c ws://localhost:4000/streamSee docs/api.md for the full endpoint reference.
| Method | Path | Description |
|---|---|---|
| GET | /health |
Liveness probe |
| POST | /events |
Ingest an envelope |
| GET | /events/recent |
Most recent events, oldest first |
| GET | /events/filter-options |
Distinct filter values |
| POST | /events/:id/respond |
Submit a human-in-the-loop response |
| GET | /events/:id/response?wait=30 |
Long-poll for a HITL response |
| WS | /stream |
Real-time event push |
Use a polling callback when the agent cannot receive inbound requests, for example on a laptop, in CI, or behind NAT.
- Post a HITL request:
curl -s -X POST http://localhost:4000/events \
-H 'Content-Type: application/json' \
-d '{
"envelope_version": 1,
"agent_kind": "my-agent",
"agent_version": "0.1.0",
"source_app": "my-app",
"session_id": "s1",
"event_type": "hitl.request",
"raw_event_type": "PermissionRequest",
"payload": {},
"human_in_the_loop": {
"question": "Run rm -rf?",
"type": "permission",
"callback": { "kind": "polling" }
}
}' | jq .id- Have the agent long-poll for the answer:
curl -m 35 'http://localhost:4000/events/<id>/response?wait=30'- Submit the human response:
curl -X POST http://localhost:4000/events/<id>/respond \
-H 'Content-Type: application/json' \
-d '{"permission":true,"responded_by":"alice"}'wait defaults to 30 seconds and is capped at 60 seconds. The response endpoint returns 200 once answered or 408 on timeout.
Every adapter must POST a JSON object that matches the v1 EventEnvelope shape.
- TypeScript types: packages/envelope/types.ts
- JSON Schema: packages/envelope/envelope.schema.json
- Event vocabulary: packages/envelope/event-types.ts
See docs/adapter-guide.md for adapter examples in Python and TypeScript.
The Claude Code adapter translates Claude Code hook events into echo envelopes.
PermissionRequest becomes a real hitl.request with a polling callback, so Allow/Deny actions in the dashboard can unblock Claude Code permission prompts.
See apps/adapter-claude-code/README.md for installation instructions and copy-pasteable settings.json examples.
Copy .env.example to .env to override local defaults.
| Variable | Default | Description |
|---|---|---|
SERVER_PORT |
4000 |
HTTP + WebSocket listen port |
DB_PATH |
events.db |
SQLite file path. Use :memory: for ephemeral runs. |
CORS_ORIGINS |
* |
Comma-separated allowed origins, or * for open. |
WS_SNAPSHOT_LIMIT |
300 |
Max events in the initial /stream snapshot. |
Local development runs in one Bun process. Multi-replica cloud deployment mainly requires swapping these two interfaces:
- EventRepository ->
PostgresEventRepository - Broadcaster ->
RedisPubSubBroadcaster
See docs/migration-cloud.md for the full checklist.