feat(agents): add Agent + ChatThread + ChatMessage schema (Stage 1)
Introduces the persistence layer for the upcoming Agent feature: an LLM persona pinned to a specific Llm, optionally attached to a Project, with persisted chat threads/messages so conversations survive REPL exits. Constraint shape: - Agent.llm uses ON DELETE RESTRICT — deleting an Llm in active use fails. - Agent.project uses ON DELETE SET NULL — agents survive project deletion. - ChatThread → ChatMessage cascade so deleting an agent purges its history. - ChatMessage @@unique([threadId, turnIndex]) gives append ordering even under racing writers (services retry on collision). LiteLLM-style per-call overrides will live in Agent.defaultParams (Json); the loose extras Json field is reserved for future LoRA/tool-allowlist work. Pinned vitest fileParallelism=false in @mcpctl/db: all suites share the same Postgres, and adding a second suite exposed FK contention between a clearAllTables in one file and a create in another. Per-test isolation still comes from beforeEach. Tests: 8/8 green in src/db/tests/agent-schema.test.ts (defaults, name uniqueness, llm-in-use Restrict, project-delete SetNull, agent-delete cascade, duplicate (threadId, turnIndex) blocked, tool-call payload round-trip, lastTurnAt DESC ordering). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,8 @@ model User {
|
||||
ownedProjects Project[]
|
||||
groupMemberships GroupMember[]
|
||||
mcpTokens McpToken[]
|
||||
ownedAgents Agent[]
|
||||
chatThreads ChatThread[]
|
||||
|
||||
@@index([email])
|
||||
}
|
||||
@@ -197,6 +199,7 @@ model Llm {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
apiKeySecret Secret? @relation(fields: [apiKeySecretId], references: [id], onDelete: SetNull)
|
||||
agents Agent[]
|
||||
|
||||
@@index([name])
|
||||
@@index([tier])
|
||||
@@ -268,6 +271,7 @@ model Project {
|
||||
prompts Prompt[]
|
||||
promptRequests PromptRequest[]
|
||||
mcpTokens McpToken[]
|
||||
agents Agent[]
|
||||
|
||||
@@index([name])
|
||||
@@index([ownerId])
|
||||
@@ -427,6 +431,81 @@ model BackupPending {
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
// ── Agents (LLM personas pinned to a specific Llm) ──
|
||||
//
|
||||
// Agents combine a system prompt, a pinned LLM, and (optionally) a project to
|
||||
// inherit Prompts from. Each Agent is also exposed by mcplocal as a virtual
|
||||
// MCP server (`agent-<name>/chat`), so other clients can consult it as a tool.
|
||||
// Per-call LiteLLM-style overrides stack on top of `defaultParams`.
|
||||
|
||||
model Agent {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String @default("") // shown in MCP tools/list
|
||||
systemPrompt String @default("") @db.Text // agent persona
|
||||
llmId String
|
||||
projectId String?
|
||||
proxyModelName String? // optional informational override
|
||||
defaultParams Json @default("{}") // LiteLLM-style: temperature, top_p, top_k, max_tokens, stop, ...
|
||||
extras Json @default("{}") // future LoRA / tool-allowlist
|
||||
ownerId String
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
llm Llm @relation(fields: [llmId], references: [id], onDelete: Restrict)
|
||||
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
threads ChatThread[]
|
||||
|
||||
@@index([name])
|
||||
@@index([llmId])
|
||||
@@index([projectId])
|
||||
@@index([ownerId])
|
||||
}
|
||||
|
||||
// ── Chat Threads (persisted conversation per Agent) ──
|
||||
|
||||
model ChatThread {
|
||||
id String @id @default(cuid())
|
||||
agentId String
|
||||
ownerId String
|
||||
title String @default("")
|
||||
lastTurnAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
|
||||
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
messages ChatMessage[]
|
||||
|
||||
@@index([agentId, lastTurnAt(sort: Desc)])
|
||||
@@index([ownerId])
|
||||
}
|
||||
|
||||
// ── Chat Messages ──
|
||||
//
|
||||
// `turnIndex` is monotonic per thread; the @@unique enforces ordering even
|
||||
// under racing appends (callers retry on collision). `status` stays `pending`
|
||||
// until the orchestrator confirms the turn completed successfully.
|
||||
|
||||
model ChatMessage {
|
||||
id String @id @default(cuid())
|
||||
threadId String
|
||||
turnIndex Int
|
||||
role String // 'system' | 'user' | 'assistant' | 'tool'
|
||||
content String @db.Text
|
||||
toolCalls Json? // assistant turn: [{id,name,arguments}]
|
||||
toolCallId String? // tool turn: which call this answers
|
||||
status String @default("complete") // 'pending' | 'complete' | 'error'
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
thread ChatThread @relation(fields: [threadId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([threadId, turnIndex])
|
||||
@@index([threadId, createdAt])
|
||||
}
|
||||
|
||||
// ── Audit Logs ──
|
||||
|
||||
model AuditLog {
|
||||
|
||||
Reference in New Issue
Block a user