Skip to content

Storage

Де живе чат-першоджерело — довговічна розмова, яку рендерить UI і яку агент перечитує.

Postgres, за agent/chat

Чат — це транзакційні, реляційні дані: Chat → Message, прив'язані до Agent / Team. Йому потрібні сильна консистентність і чиста пагінація для UI → Postgres — правильний дім.

prisma
model Chat {
  id        String    @id            // chat-{uuid}
  agentId   String
  channel   String                   // widget | telegram | api …
  messages  Message[]
  createdAt DateTime  @default(now())
  @@index([agentId, createdAt])
}

model Message {
  id        String   @id             // msg-{uuid}
  chatId    String
  role      String                   // user | assistant | tool | system
  content   Json                     // text + tool refs + attachments
  tokens    Int?
  createdAt DateTime @default(now())
  chat      Chat     @relation(fields: [chatId], references: [id])
  @@index([chatId, createdAt])
}

Слайс agent/chat володіє цим за gateway, тож фізичне сховище взаємозамінне без втручання у викликачів.

Читання: сторінка, а не весь тред

Усі три читання історії пагіновані курсором, і попросити все не можна ніяк:

GET /agents/:id/chat/messages?limit=&cursor=
GET /agents/:id/chat/threads?limit=&cursor=
GET /agents/:id/chat/threads/:threadId/messages?limit=&cursor=
  • limit за замовчуванням 50 і обмежений згори 200.
  • Відповідь — { items, nextCursor, hasMore }, items ідуть від старих до нових. hasMore винесено окремим полем навмисно: висновок із items.length === limit помиляється рівно один раз на тред — на останній сторінці, яка виявилася повною.
  • cursor непрозорий і йде назад, у старіші рядки. Курсор, якого цей api не видавав, відхиляється, а не ігнорується мовчки.

Це не оздоба. Агент з автозапуском пише у свій тред цілодобово, тому тред росте без жодної участі людини.

Почати заново — риска у треді, а не другий тред

Розмові не було де закінчитися: щось обговорили, за тиждень прийшли з іншим — а модель тягла за собою весь перший предмет. «Почати заново» проводить у тому самому треді рядок-межу. Від неї модель не отримує нічого з того, що було раніше, а людина зберігає всі попередні повідомлення і може до них прокрутити.

Тред і далі рівно один на (агента, канал, людину) — так каже ключ, — і нової сутності не з'явилося: межа це той самий маркер, який ставить компакція, замінюючи старі ходи переказом, лише поставлений свідомо, а не в міру зростання треду. Натискання кнопки двічі не малює другої риски.

Чого скидання не торкається — це пам'ять агента. Нотатки, які він записав про людину, лежать не в треді, тому після скидання він їх і далі знає. Виглядає як поломка, але нею не є — тому екран каже про це прямо, а не лишає людину з'ясовувати це самій.

Що не зберігається

  • Хід, що завершився помилкою, не зберігає нічого. Хід, який зупинив користувач, — інша річ: уже доставлений текст зберігається з позначкою «зупинено», тому тред після перезавантаження збігається з тим, що було на екрані. Див. Що завершує хід.

Обсяг — Postgres справляється

«Значний обсяг» — це нормальний кейс для таблиці Message; важелі:

  • Партиціонуй Message (за часом, або за agentId/teamId), щоб гарячі діапазони лишалися малими.
  • Індексуй під реальні запити (chatId, createdAt); уникай широких сканів.
  • Архівуй холодні розмови в об'єктне сховище (у рядку лишається вказівник); жива таблиця тримається лінивою.
  • Сирі trace/prompt-блоби тут НЕ живуть — вони йдуть у Debug & tracing. Тримати телеметрію осторонь — це й тримає цю таблицю швидкою.

Старт спільний → розділяй, коли заслужить

  1. Зараз: чати живуть у головному Postgres (одна БД, найпростіше).
  2. На масштабі: перенеси дані agent/chat на виділений інстанс Postgres, коли write-IO чатів почне конкурувати з транзакційним навантаженням. Оскільки це за gateway, це зміна конфігу, а не переписування — рівно той шлях «спершу в api, окрема база потім».

Дивіться також

  • Debug & tracing — чому промпти/trace'и тримаються поза цією таблицею.
  • Memory — пошуковий індекс, похідний від цих повідомлень.
  • DB schemaChat / Message у ширшій моделі.