Skip to content

Схема БД

Постоянная форма данных Agentfy.ai 2.0. Prisma — это репозиторий (отдельного repository-слоя нет); одна база Postgres, модели сгруппированы по тем же слоям-слайсам.

api/prisma/schema.prisma закоммичен, и это сгенерированный файл — так написано в его первой же строке. Каждый слайс держит свои модели в <slice>.prisma у себя в корне, а схема собирается из них, и отсюда следуют две вещи, которые стоит знать до любой правки: менять надо файл слайса, а не сборку; и генерировать надо командой bun run prisma:generate, потому что голый npx prisma generate ничего не собирает и выходит нулём на том, что уже лежит на диске, отдавая клиент, который не знает вашей новой модели. Сниппеты ниже иллюстративные; источник правды — файлы слайсов.

Конвенции

  • ID в формате {slice}-{uuid} (agent-3f9c…, team-a17f…), генерируются в mapper.toCreate слайса — читаемо и самоописательно.
  • Мультитенантность: почти каждая строка несёт teamId (team = тенант / магазин). Строки agent-домена несут agentId, который резолвится в team.
  • Таймстемпы: createdAt / updatedAt у всех моделей; soft-delete (deletedAt) только там, где важна история.
  • JSON для открытых форм: config агента, payload задачи, settings канала — типизированы в коде, Json в БД.
  • Секреты никогда не в плейнтексте — см. envelope-зашифрованную модель Secret ниже и Секреты агента.

Сущности по слоям

user      Team ─┬─ UserTeam ─ User                 apiKey · invite · role
                └─ (planId ← billing)

agent     Agent ─┬─ Memory                          ← agent = строка, которая И ЕСТЬ агент
                 ├─ KnowledgeBase ─ Document
                 ├─ Chat ─ Message
                 ├─ Channel · Integration · Skill · Cron · Access
                 └─ Secret (envelope-зашифрован)

runtime   Task ─ AgentRuntimeSession ─ Event        ← эфемерное исполнение

system    Usage · Setting · Notification · File     ← общие сервисы
admin     AuditLog · FeatureFlag
billing   Subscription ─ Price ─ Product · Invoice ─ Payment · PaymentMethod · WebhookEvent

Всё висит на Team (тенант) сверху и Agent (идентичность агента) в середине.

Ключевые модели (иллюстративно)

Тенантность — user

prisma
model Team {
  id          String   @id            // team-{uuid}
  name        String
  planId      String?                 // written by billing; read by system/setting (inverted dep)
  members     UserTeam[]
  agents      Agent[]
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

model User {
  id        String     @id            // user-{uuid}
  email     String     @unique
  name      String?
  teams     UserTeam[]
  createdAt DateTime   @default(now())
}

model UserTeam {                       // membership + role
  id      String @id
  userId  String
  teamId  String
  role    String                       // owner | admin | member
  user    User   @relation(fields: [userId], references: [id])
  team    Team   @relation(fields: [teamId], references: [id])
  @@unique([userId, teamId])
}

Агент — agent

prisma
model Agent {
  id               String   @id       // agent-{uuid} — this row IS the agent
  teamId           String
  name             String
  status           String   @default("active")     // active | disabled | archived
  type             String   @default("standard")   // standard | concierge — назначает только сервер
  soul             String?            // SOUL.md · USER.md · HEARTBEAT.md — собственный текст агента,
  user             String?            //   дословно; уезжает в экспорт и возвращается байт в байт
  heartbeat        String?
  config           Json     @default("{}")   // agent.config.json рантайма
  runtimeProfile   String   @default("none")    // можно ли ему руки и сколько машины
  promptLogEnabled Boolean  @default(false)   // записывать собранные промпты этого агента
  team             Team     @relation(fields: [teamId], references: [id], onDelete: Cascade)
  createdAt        DateTime @default(now())
  updatedAt        DateTime @updatedAt
  @@index([teamId])
}

Всё, что называет agentId, уходит каскадом от этой строки — переписка, память, навыки, секреты, конфигурация автозапуска, история работы. Каскад — это один оператор в одной транзакции, поэтому удаление, оборванное посередине, не удаляет ничего; явная уборка семи таблиц может остановиться между любыми двумя из них и оставить ровно тех сирот, ради которых каскад и заведён. Единственное, что вне его, — VectorEmbedding: у него по замыслу нет внешнего ключа, и его убирают явно.

Что означает каждая колонка — см. Состав.

Envelope-зашифрованные секреты — agent/secret

prisma
model Secret {
  id          String   @id            // secret-{uuid}
  agentId     String
  name        String                  // e.g. OPENCART_API_KEY
  ciphertext  Bytes                   // AES-256-GCM payload (the wrapped value)
  iv          Bytes                   // per-record nonce
  authTag     Bytes                   // GCM auth tag (AEAD)
  keyVersion  Int                     // which KEK wrapped the DEK
  agent       Agent    @relation(fields: [agentId], references: [id])
  createdAt   DateTime @default(now())
  @@unique([agentId, name])
}

KEK никогда не покидает api; под воркера не видит плейнтекст. См. Секреты агента.

Эфемерное исполнение — runtime

prisma
model AgentRuntimeSession {
  id            String    @id          // session-{uuid}
  agentId       String
  taskId        String?
  status        String                 // pending|starting|running|idle|stopping|stopped|failed
  failureReason String?                // причина отказа: dial_timeout | handshake_timeout |
                                       // ready_timeout | session_mismatch | channel_version |
                                       // heartbeat_lost
  runtimeType   String                 // none | light | browser | heavy | warm
  cpuLimit      String?
  memLimit      String?
  storageLimit  String?
  ttlSeconds    Int
  k8sJobName    String?
  namespace     String?
  workerUrl    String?
  logsUrl       String?
  startedAt     DateTime?
  lastActivityAt DateTime?
  stoppedAt     DateTime?
  @@index([agentId, status])
}

(Машину состояний см. в Жизненном цикле воркера.)

Остальное, по слоям

СлойМодели (эскиз)
userApiKey { teamId, hash, scopes } · Invite { teamId, email, role, token, expiresAt } · Role
systemUsage { teamId, agentId?, kind, amount, unit, ts } · Setting { scope, key, value } · Notification { userId, type, readAt } · File { teamId, agentId?, storageKey, name, size, mime, tags }
adminAuditLog { actorId, action, target, meta, ts } · FeatureFlag { key, enabled, scope }
runtimeTask { teamId, agentId, kind, status, payload, attempts } · Event { sessionId?, type, data } (в основном стримится через Redis; персистится только для аудита)
agentMemory { agentId, kind, content, embeddingRef? } · KnowledgeBase { agentId, workspace }Document · Chat { agentId, channel }Message { chatId, role, content } · Channel · Integration · Skill · Cron · Access
billingSubscription { teamId, priceId, status, currentPeriodEnd } · ProductPrice · Invoice ─ Payment · PaymentMethod · WebhookEvent

Где живёт нереляционное состояние (не в этой схеме)

  • Векторы + граф знаний → Postgres pgvector + Apache AGE, владелец — LightRAG по workspace (один на KnowledgeBase). См. LightRAG.
  • Очередь + pub/sub + кэшRedis (BullMQ-задачи, стрим events). Не durable-правда.
  • Объектное хранилище → файлы/артефакты по ссылке; строка File держит storageKey, байты лежат в S3/R2.

См. также