Skip to main content

ADR-0001 ยท Tavus integration approach

2026-06-05/06 status: Phase 1 production-shape shipped โ€” #2052 (tavus_personas + video_calls tables), #2054 (security guardrails preventing cross-client data leakage), #2055 (on-demand video call creation API).

Status: Accepted โ€” implemented 2026-05-13 via PM-* commit series. POC repo hwork-persona-manager/ archived. ยท Date: 2026-05-13 ยท Deciders: Builder agent (autonomous) ยท Supersedes: none

Historical note: This ADR was authored under the original "Persona Manager" working name. The platform-facing surface is the Tavus integration โ€” API module api/src/tavus/ (TavusManagerService, TavusController), frontend routes /ops/tavus/* (rename completed via #1031, closes #630; legacy /ops/personas/* paths 308-redirect for ~30 days). References below to the archived POC repo hwork-persona-manager/ and migration filenames (PersonaManagerInit.ts etc.) are immutable historical artifacts.

Contextโ€‹

A standalone Tavus integration POC at the archived hwork-persona-manager/ repo is build-complete. It is its own FastAPI + Next.js stack with its own Postgres, its own Celery queue, its own audit log, and its own hwork_client outbound to the main h.work application.

The integration task is to land its functionality inside the existing h.work monorepo (api/ NestJS, agent/ FastAPI, frontend/ Next.js) without disrupting a production-grade codebase that is one operational task away from MVP launch (45/46 P0 items complete per MVP_LAUNCH_CHECKLIST.md).

The Builder operating prompt (prompts/persona-builder.md) sets a hard constraint: do not modify existing code unless a task explicitly tells you to.

Three high-level approaches were considered.

Optionsโ€‹

A โ€” "Lift and shift": import the POC repo as-isโ€‹

Copy hwork-persona-manager/apps/api into the monorepo as a third backend app alongside api/ and agent/. Run it on its own port and DB connection. Bridge to hwork-app over HTTP using the POC's existing hwork_client.

  • Pros: preserves the POC verbatim. Zero re-implementation effort.
  • Cons: duplicates auth, multi-tenancy, audit, the Postgres connection, CI orchestration, deployment surface, secret management. Two systems drift over time. Cross-tenant isolation now spans two services and two DBs โ€” testing it nightly becomes a meaningfully harder problem. Violates the spirit of "single source of truth for org data."

B โ€” "Additive re-implementation": rebuild inside the NestJS appโ€‹

Build new NestJS modules (api/src/environment-profiles/, api/src/meetings/, api/src/tavus/) and new Next.js routes that read from the existing entities, write to five new tables, and reuse all existing cross-cutting infrastructure (AuditService, OrgRlsInterceptor, NotificationsGateway, JwtAuthGuard, PlatformRolesGuard, OrgRolesGuard, ExpertQueueService).

  • Pros: single Postgres connection, single auth model, single audit trail, single multi-tenancy mechanism. The integration touch points are countable and reviewable. Tests run in the existing harness. Nightly cross-tenant isolation already covers the new tables when added to its list (PM-0005). No drift between two parallel systems.
  • Cons: re-implements ~70% of what the POC already wrote in Python + SQLAlchemy. The POC's Celery workflow becomes BullMQ. The POC's separate Postgres becomes a schema namespace inside the main one. Some POC abstractions don't translate (e.g. work_log_reference collapses to live service-layer reads).

C โ€” "Hybrid extraction": vendor select POC pieces, ignore the restโ€‹

Pull only the Tavus client, prompt templates, and function-call definitions from the POC; ignore everything else and rebuild the rest. Use the POC's hwork_client reversed โ€” call from POC outward into hwork-app โ€” converted to a NestJS module.

  • Pros: salvages the genuinely reusable POC outputs (Tavus contract understanding, function-call schemas, prompt structure).
  • Cons: ambiguous boundary. Halfway between A and B without the benefits of either. Hard to draw the line during implementation.

Decisionโ€‹

Option B. Additive re-implementation inside the NestJS app.

The five new tables (environment_profiles, meetings, meeting_decisions, meeting_priorities, tavus_function_definitions) stand alone and reference existing entities by FK. New code lives in new directories. Cross-cutting infrastructure is reused, not duplicated. Agent service gets new prompt files but no code changes (the existing load_prompt(role) already routes on filename).

Update (2026-06-04): PR #1602 renamed the DB table environment_profiles โ†’ client_briefs at the SQL level. The API TypeScript module is still api/src/environment-profiles/ with the EnvironmentProfile entity (mapped to client_briefs) until follow-up #1199 lands the module/entity rename. Treat references to environment_profiles in this ADR as the historical name; the design and decision are unchanged.

The POC remains in hwork-persona-manager/ as reference material for entity mapping, function-call schemas, and Tavus contract details, but it is not imported, deployed, or run.

Consequencesโ€‹

Positiveโ€‹

  • One auth model, one audit log, one DB connection, one multi-tenancy mechanism, one notification gateway.
  • The integration touch points are explicit and small: additive functions in frontend/src/lib/api.ts, single-line module imports in api/src/app.module.ts, additive entity entries in .github/workflows/agent-cross-org-isolation-nightly.yml. Anything else in an existing file is a violation requiring a requires_existing_code_change event.
  • Reporting decisions ride the existing Action entity as a designed extension point (actorType=reporting_meeting). The existing audit log viewer surfaces them for free.
  • Reporting escalations go through ExpertQueueService.create(...). The expert workspace queue surface is reused unchanged.
  • The brief assembler reads live from Conversation/Message/Action/ ExpertQueueItem/Correction via service-layer queries โ€” no parallel work-log store, no sync job.
  • Single Postgres + single OrgRlsInterceptor covers tenancy. Each new table gets an RLS policy in its migration matching migrations 9, 12, 17, 20, 27, 33.

Negative / acceptedโ€‹

  • Re-implementation effort. The POC's Python services become TypeScript services. The mitigation is that the POC's domain model carries over cleanly to TypeORM entities โ€” most of the translation is mechanical.
  • The POC's Celery + Redis async pattern is replaced by BullMQ (already in package.json and minimally used). Only the persona archival job (PM-0066) actually needs background scheduling; the rest of the workflow is request-response.
  • Versioning of role templates (POC's role_template_version entity) is deferred. See ADR-0003 if/when an actual need arises.

Neutralโ€‹

  • The agent/prompts/ directory becomes a shared prompt registry read by both the agent service (load_prompt(role)) and the NestJS persona-generator.service.ts (file read + parameter substitution before passing to tavusClient.createPersona(prompt)). This is fine because the agent's loader keys on role and the NestJS reader simply renders the same template into a different consumer. No code change to the loader.

Permitted touches to existing filesโ€‹

These are sanctioned by the Builder prompt and listed exhaustively here so future cycles can reference them without re-deriving:

FileAllowed changeWhy
frontend/src/lib/api.tsAppend new exported functions only. No signature changes, no reordering, no deletion.Single API client convention in the frontend.
api/src/app.module.tsAdd imports: [..., NewModule] single-line entries.NestJS module registration.
.github/workflows/agent-cross-org-isolation-nightly.ymlAppend new entity names to the test list.Cross-tenant isolation is existential; new tables must be covered.

Every other existing file is read-only. Any task that appears to require otherwise must emit a requires_existing_code_change event.

Verificationโ€‹

  • PM-0001 confirmed all referenced entities and patterns exist where the Builder prompt indicated.
  • The HMAC mirror pattern (PM-0012) reproduces channels.controller.ts:47-62 without import.
  • The Action extension-point write (PM-0044) is exercised by the Phase 6 Playwright E2E test (PM-0061) which asserts the row appears in the existing audit log viewer.

Referencesโ€‹

  • prompts/persona-builder.md โ€” Builder operating prompt (canonical)
  • docs/specs/tavus-integration.md โ€” system-level integration spec (renamed from persona-manager-integration.md on 2026-06-02)
  • hwork-persona-manager/docs/architecture.md โ€” POC reference
  • api/migrations/ 9, 12 (MemoryRLS), 13, 17, 27, 33 โ€” RLS patterns (PM-0067 fix)
  • api/src/channels/channels.controller.ts:23-62 โ€” HMAC pattern
  • api/src/common/entities.ts โ€” Action extension-point entity