Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Agent Observability Template

Observability for chat-based agents (iMessage, WhatsApp, Telegram, SMS, …): message volume, channel mix, live inbox with human-in-loop replies, contacts, and opt-out compliance — on a normalized schema so every platform looks the same.

your agent ──adapter──▶ @agent-obs/core ──▶ Postgres (obs_messages / obs_contacts)
                                                   ▲
dashboard (Next.js) ── /api/observ/* ──────────────┘        inbox "Send" ──▶ your agent's control endpoint

Packages

Path What it is
packages/core Ingestion SDK + schema.sql. Bounded write queue, idempotent inserts, sticky-channel resolution, opt-out helpers.
packages/adapters/spectrum-imessage Reference adapter: Spectrum (spectrum-ts) iMessage → normalized events. Copy it to write your own.
apps/dashboard Next.js 16 + Tailwind v4 dashboard styled with Pho (@photon-ai/pho-ui): Analytics, Inbox (observe + reply), Contacts.

The dashboard self-hosts its fonts from apps/dashboard/public/fonts/. Azeret Mono and Instrument Serif (both OFL) are included; PolySans is commercial and not distributed here — without it the UI falls back to your system sans. If you have a license, drop PolySansVF.woff2 into that folder (gitignored).

Quick start

Using an AI coding agent (Claude Code, Cursor, …)? Clone this repo, open it in your agent, and say “set this up for my agent” — AGENTS.md is a step-by-step runbook it will follow, including database setup, env configuration (your own Supabase keys — none ship with this repo), wiring the SDK into your agent, and end-to-end verification.

Manual path:

  1. Database — run packages/core/schema.sql in your Supabase SQL editor (or psql). Idempotent.

  2. Instrument your agent

    import { createObservability } from "@agent-obs/core";
    import { spectrumIMessageAdapter, outboundEvent } from "@agent-obs/adapter-spectrum-imessage";
    
    const obs = createObservability({
      supabaseUrl: process.env.SUPABASE_URL!,
      serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY!,
    });
    await obs.seedChannelCache(); // survive restarts without "unknown" channels
    
    // inbound loop
    const event = spectrumIMessageAdapter.toEvent(message);
    if (event) {
      obs.logMessage(event); // non-blocking; never throws into your reply path
      obs.upsertContact({ handle: event.conversationId, platform: event.platform, channel: event.channel });
    }
    
    // after each outbound send
    const channel = await obs.resolveOutboundChannel(handle);
    obs.logMessage(outboundEvent(handle, text, channel, sent?.id));
  3. Human-in-loop send — expose a tiny HTTP endpoint on your agent (POST /send { conversationId, text }, checked against an x-agent-secret header) and set AGENT_CONTROL_URL / AGENT_CONTROL_SECRET. Guard it with await obs.isOptedOut(handle) for opt-out compliance.

  4. Dashboard

    npm install
    cp apps/dashboard/.env.example apps/dashboard/.env.local   # fill in Supabase creds
    npm run dev    # → http://localhost:3000

Adding a platform (e.g. WhatsApp)

  1. Write an adapter (packages/adapters/whatsapp) — one toEvent() mapping.
  2. Add a registry entry in apps/dashboard/src/channels.config.ts:
    { key: "WhatsApp", label: "WhatsApp", color: "#25d366", badgeBg: "#dcf7e8", badgeFg: "#0b7a53", tile: true }

That's it — badges, thread bubbles, channel mix, and analytics tiles all pick it up. No schema migration (channels are not CHECK-constrained by design).

Choosing your database

The schema is vanilla Postgres (packages/core/schema.sql); Supabase is the default because its free tier + SQL editor + REST API make "clone → running" a 10-minute path. Pick by use-case:

Use-case Recommendation What it takes
Getting started, side project, one agent Supabase (default) Works out of the box — core writes via PostgREST, dashboard reads via supabase-js RPC.
You already run Postgres, want serverless scale-to-zero, or branch-per-PR dev Neon (or RDS / self-hosted Postgres) Same schema.sql, applied via psql "$DATABASE_URL" -f packages/core/schema.sql. Swap the two transport points: the core SDK's HTTP writes → a small pg insert sink, and apps/dashboard/src/server/observ.ts's db().rpc(...) calls → pg/postgres.js calling the same SQL functions. The SQL itself is unchanged.
High-volume analytics — millions of messages/day, long retention, heavy dashboards ClickHouse (or Tinybird) for the message log Columnar storage makes the windowed aggregates near-free at scale. Keep contacts + inbox on Postgres (row-level reads); dual-write messages, move obs_message_stats/obs_message_series to ClickHouse queries. This is a v2 move — don't start here.
You want live-updating UI over SQL aggregates Convex Reactive queries replace this template's polling + read-cache entirely, but you give up SQL GROUP BY — stats become function-side aggregation or maintained counters. A different architecture, not a drop-in swap.

Rule of thumb: stay on the default until the aggregates are slow (that's tens of millions of rows with these indexes), then reach for ClickHouse for the log while Postgres keeps the inbox.

Hard-won production notes (baked into core)

  • Sticky channel, never downgrade. Providers omit the transport on some messages (cloud iMessage omits sender.service sporadically). Learn the channel from any confident message, persist it on the contact, and never let an untagged message overwrite it with unknown.
  • Outbound transport is inferred. Send results usually don't carry the channel (Apple decides iMessage→SMS fallback asynchronously). Use resolveOutboundChannel() — memory cache → persisted contact.
  • Observability must never hurt the agent. All writes are queued, bounded, timed out, and shed under flood.
  • Reads collapse under concurrency. The dashboard's read layer uses a single-flight micro-cache: N clients polling the same window = 1 DB query per TTL. Aggregates run in SQL, one round-trip.
  • Don't call space.getMembers() on Spectrum DMs — it throws; the transport lives on message.sender.service.

About

Observability boilerplate for chat-based AI agents — analytics, live inbox with human-in-loop replies, contacts, opt-out compliance. iMessage/SMS/RCS out of the box; any platform via adapters.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages