Project One

Conductor Chat

The member's chat bar. Free-form messages go to the live Conductor agent (Claude) when configured, otherwise to a deterministic on-brand demo responder — same response shape either way, and every reply (live or demo) passes the deterministic no-jargon guard.

Endpoint Auth Rate limit Body cap
POST /api/conductor session cookie when platform configured; public in keyless demo 20/min per IP 1 MB

POST /api/conductor

Request (JSON)

Field Type Notes
message string max 6,000 chars (raw length, before trim). Non-string values and malformed JSON are silently treated as an empty message.

Empty/whitespace-only messages short-circuit to a canned 200 reply — not an error.

Responses

Status Body
200 (live) { "reply": "<conductor message>", "mode": "live" }
200 (demo) { "reply": "<demo responder>", "mode": "demo" }
200 (empty message) { "reply": "Tell me what you'd like to know and I'll help.", "mode": "demo" }
400 (too long) { "error": "That message is too long — please shorten it." }
401 (configured, no session) { "error": "Please sign in to chat." }
411 / 413 / 429 shared guard envelopes (conventions §4-5)

⚠️ Envelope quirk: the guard errors (411/413/429) carry "ok": false; this route's own 400/401 use bare { "error": ... }; success carries neither. Parse defensively.

Order of checks

rate limit → body limit → parse → 6,000-char cap → empty-message short-circuit → auth (live only) → LLM.

Demo vs live

  • Gate: ANTHROPIC_API_KEY (isLLMConfigured). Live calls run at effort: 'low' on the chat model role (ANTHROPIC_MODEL_CHAT override, default claude-opus-4-8).
  • Tenant-scoped facts (#112): with a configured platform, a signed-in member's chat answers against their instance's assembled facts — business, pipeline stage, live angle, angle-bank count, funnel counts to date (funnel_to_date — cumulative snapshot, not a trailing week), latest diagnosis, pending approvals — via their RLS client (oldest-instance pick, matching v1). The demo business's facts feed the live model only on keyless deployments (the ANTHROPIC-key-without-Supabase combo, where no tenants exist). Fact assembly is all-or-nothing: any failed read throws — the member gets an honest try-again reply, never a plausible wrong fact ("brand new account", "0 angles banked") and never the sample business's numbers.
  • Auth exists only when the platform is configured — "anonymous LLM spend is never allowed". Keyless deployments are fully public (demo replies are free and deterministic).
  • Live-failure semantics split by who's asking (review fix): for an authenticated member (configured platform), any live failure — API error, fact-assembly failure, or the reply failing the jargon guard twice — returns an honest retry line ("I hit a snag pulling up your numbers just now — please ask me again in a moment.") with "mode": "live" and status 200; the member never receives the sample business's numbers. Only keyless paths fall back to the demo responder with "mode": "demo". Check mode, not the status code, to know which path answered.
  • The demo responder is an ordered regex intent list (greeting, leak, cost, angles, …) with a fixed fallback; its output also runs through the jargon guard.

Notes for callers

  • Stateless — nothing is persisted per message; retries are safe (they burn rate-limit quota and, live, LLM spend).
  • Live calls record llm_usage telemetry (task/model/token counts only — never message content).
  • A client-side bug sending a non-string message looks like a working chat (canned demo replies) — watch for an all-demo response pattern while live.

Source: app/api/conductor/route.ts, src/agents/conductor/index.ts (conductorChat), src/agents/conductor/jargon.ts, lib/conductor-demo.ts