Storage
Де живе чат-першоджерело — довговічна розмова, яку рендерить UI і яку агент перечитує.
Postgres, за agent/chat
Чат — це транзакційні, реляційні дані: Chat → Message, прив'язані до Agent / Team. Йому потрібні сильна консистентність і чиста пагінація для UI → Postgres — правильний дім.
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. Тримати телеметрію осторонь — це й тримає цю таблицю швидкою.
Старт спільний → розділяй, коли заслужить
- Зараз: чати живуть у головному Postgres (одна БД, найпростіше).
- На масштабі: перенеси дані
agent/chatна виділений інстанс Postgres, коли write-IO чатів почне конкурувати з транзакційним навантаженням. Оскільки це за gateway, це зміна конфігу, а не переписування — рівно той шлях «спершу в api, окрема база потім».
Дивіться також
- Debug & tracing — чому промпти/trace'и тримаються поза цією таблицею.
- Memory — пошуковий індекс, похідний від цих повідомлень.
- DB schema —
Chat/Messageу ширшій моделі.