@data-club/ai-hub-server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/argon2.d.ts +72 -0
- package/dist/auth/argon2.d.ts.map +1 -0
- package/dist/auth/argon2.js +145 -0
- package/dist/auth/argon2.js.map +1 -0
- package/dist/auth/encryption.d.ts +52 -0
- package/dist/auth/encryption.d.ts.map +1 -0
- package/dist/auth/encryption.js +98 -0
- package/dist/auth/encryption.js.map +1 -0
- package/dist/auth/jwt.d.ts +133 -0
- package/dist/auth/jwt.d.ts.map +1 -0
- package/dist/auth/jwt.js +147 -0
- package/dist/auth/jwt.js.map +1 -0
- package/dist/auth/middleware.d.ts +101 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +161 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/passwordPolicy.d.ts +72 -0
- package/dist/auth/passwordPolicy.d.ts.map +1 -0
- package/dist/auth/passwordPolicy.js +88 -0
- package/dist/auth/passwordPolicy.js.map +1 -0
- package/dist/auth/roleRules.d.ts +200 -0
- package/dist/auth/roleRules.d.ts.map +1 -0
- package/dist/auth/roleRules.js +365 -0
- package/dist/auth/roleRules.js.map +1 -0
- package/dist/auth/userView.d.ts +55 -0
- package/dist/auth/userView.d.ts.map +1 -0
- package/dist/auth/userView.js +77 -0
- package/dist/auth/userView.js.map +1 -0
- package/dist/bootstrap.d.ts +104 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +160 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/db/connection.d.ts +39 -0
- package/dist/db/connection.d.ts.map +1 -0
- package/dist/db/connection.js +62 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrate.d.ts +31 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +197 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/schema.d.ts +1902 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +662 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/seed.d.ts +92 -0
- package/dist/db/seed.d.ts.map +1 -0
- package/dist/db/seed.js +160 -0
- package/dist/db/seed.js.map +1 -0
- package/dist/handlers/fetch.d.ts +89 -0
- package/dist/handlers/fetch.d.ts.map +1 -0
- package/dist/handlers/fetch.js +173 -0
- package/dist/handlers/fetch.js.map +1 -0
- package/dist/handlers/nextjs.d.ts +40 -0
- package/dist/handlers/nextjs.d.ts.map +1 -0
- package/dist/handlers/nextjs.js +42 -0
- package/dist/handlers/nextjs.js.map +1 -0
- package/dist/handlers/responses.d.ts +46 -0
- package/dist/handlers/responses.d.ts.map +1 -0
- package/dist/handlers/responses.js +74 -0
- package/dist/handlers/responses.js.map +1 -0
- package/dist/handlers/routeTable.d.ts +21 -0
- package/dist/handlers/routeTable.d.ts.map +1 -0
- package/dist/handlers/routeTable.js +133 -0
- package/dist/handlers/routeTable.js.map +1 -0
- package/dist/handlers/router.d.ts +37 -0
- package/dist/handlers/router.d.ts.map +1 -0
- package/dist/handlers/router.js +82 -0
- package/dist/handlers/router.js.map +1 -0
- package/dist/handlers/sse.d.ts +38 -0
- package/dist/handlers/sse.d.ts.map +1 -0
- package/dist/handlers/sse.js +52 -0
- package/dist/handlers/sse.js.map +1 -0
- package/dist/handlers/types.d.ts +96 -0
- package/dist/handlers/types.d.ts.map +1 -0
- package/dist/handlers/types.js +14 -0
- package/dist/handlers/types.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/letta/client.d.ts +355 -0
- package/dist/letta/client.d.ts.map +1 -0
- package/dist/letta/client.js +529 -0
- package/dist/letta/client.js.map +1 -0
- package/dist/letta/eventTranslator.d.ts +166 -0
- package/dist/letta/eventTranslator.d.ts.map +1 -0
- package/dist/letta/eventTranslator.js +532 -0
- package/dist/letta/eventTranslator.js.map +1 -0
- package/dist/letta/index.d.ts +13 -0
- package/dist/letta/index.d.ts.map +1 -0
- package/dist/letta/index.js +12 -0
- package/dist/letta/index.js.map +1 -0
- package/dist/letta/modelSettings.d.ts +176 -0
- package/dist/letta/modelSettings.d.ts.map +1 -0
- package/dist/letta/modelSettings.js +314 -0
- package/dist/letta/modelSettings.js.map +1 -0
- package/dist/letta/promptWrapping.d.ts +141 -0
- package/dist/letta/promptWrapping.d.ts.map +1 -0
- package/dist/letta/promptWrapping.js +213 -0
- package/dist/letta/promptWrapping.js.map +1 -0
- package/dist/letta/sandbox.d.ts +100 -0
- package/dist/letta/sandbox.d.ts.map +1 -0
- package/dist/letta/sandbox.js +151 -0
- package/dist/letta/sandbox.js.map +1 -0
- package/dist/letta/settingsFileSeed.d.ts +146 -0
- package/dist/letta/settingsFileSeed.d.ts.map +1 -0
- package/dist/letta/settingsFileSeed.js +177 -0
- package/dist/letta/settingsFileSeed.js.map +1 -0
- package/dist/letta/subprocessThrottle.d.ts +151 -0
- package/dist/letta/subprocessThrottle.d.ts.map +1 -0
- package/dist/letta/subprocessThrottle.js +195 -0
- package/dist/letta/subprocessThrottle.js.map +1 -0
- package/dist/letta/tpmRecovery.d.ts +202 -0
- package/dist/letta/tpmRecovery.d.ts.map +1 -0
- package/dist/letta/tpmRecovery.js +314 -0
- package/dist/letta/tpmRecovery.js.map +1 -0
- package/dist/letta/types.d.ts +54 -0
- package/dist/letta/types.d.ts.map +1 -0
- package/dist/letta/types.js +16 -0
- package/dist/letta/types.js.map +1 -0
- package/dist/observability/logger.d.ts +48 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/observability/logger.js +81 -0
- package/dist/observability/logger.js.map +1 -0
- package/dist/observability/metrics.d.ts +53 -0
- package/dist/observability/metrics.d.ts.map +1 -0
- package/dist/observability/metrics.js +92 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/routes/admin/agents.d.ts +63 -0
- package/dist/routes/admin/agents.d.ts.map +1 -0
- package/dist/routes/admin/agents.js +461 -0
- package/dist/routes/admin/agents.js.map +1 -0
- package/dist/routes/admin/rotatingMessages.d.ts +51 -0
- package/dist/routes/admin/rotatingMessages.d.ts.map +1 -0
- package/dist/routes/admin/rotatingMessages.js +292 -0
- package/dist/routes/admin/rotatingMessages.js.map +1 -0
- package/dist/routes/admin/sessions.d.ts +61 -0
- package/dist/routes/admin/sessions.d.ts.map +1 -0
- package/dist/routes/admin/sessions.js +232 -0
- package/dist/routes/admin/sessions.js.map +1 -0
- package/dist/routes/admin/settings.d.ts +110 -0
- package/dist/routes/admin/settings.d.ts.map +1 -0
- package/dist/routes/admin/settings.js +401 -0
- package/dist/routes/admin/settings.js.map +1 -0
- package/dist/routes/admin/users.d.ts +115 -0
- package/dist/routes/admin/users.d.ts.map +1 -0
- package/dist/routes/admin/users.js +374 -0
- package/dist/routes/admin/users.js.map +1 -0
- package/dist/routes/agents.d.ts +30 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +55 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/auth.d.ts +121 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +422 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/chat.d.ts +97 -0
- package/dist/routes/chat.d.ts.map +1 -0
- package/dist/routes/chat.js +484 -0
- package/dist/routes/chat.js.map +1 -0
- package/dist/routes/featureToggles.d.ts +34 -0
- package/dist/routes/featureToggles.d.ts.map +1 -0
- package/dist/routes/featureToggles.js +47 -0
- package/dist/routes/featureToggles.js.map +1 -0
- package/dist/routes/health.d.ts +24 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +37 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/metrics.d.ts +35 -0
- package/dist/routes/metrics.d.ts.map +1 -0
- package/dist/routes/metrics.js +106 -0
- package/dist/routes/metrics.js.map +1 -0
- package/dist/routes/sessions.d.ts +161 -0
- package/dist/routes/sessions.d.ts.map +1 -0
- package/dist/routes/sessions.js +1501 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/stubs.d.ts +25 -0
- package/dist/routes/stubs.d.ts.map +1 -0
- package/dist/routes/stubs.js +25 -0
- package/dist/routes/stubs.js.map +1 -0
- package/dist/state/cron.d.ts +58 -0
- package/dist/state/cron.d.ts.map +1 -0
- package/dist/state/cron.js +160 -0
- package/dist/state/cron.js.map +1 -0
- package/dist/state/rotationRunner.d.ts +96 -0
- package/dist/state/rotationRunner.d.ts.map +1 -0
- package/dist/state/rotationRunner.js +246 -0
- package/dist/state/rotationRunner.js.map +1 -0
- package/dist/state/rotationScheduler.d.ts +76 -0
- package/dist/state/rotationScheduler.d.ts.map +1 -0
- package/dist/state/rotationScheduler.js +124 -0
- package/dist/state/rotationScheduler.js.map +1 -0
- package/dist/state/turnRegistry.d.ts +95 -0
- package/dist/state/turnRegistry.d.ts.map +1 -0
- package/dist/state/turnRegistry.js +149 -0
- package/dist/state/turnRegistry.js.map +1 -0
- package/dist/state/turnRunner.d.ts +134 -0
- package/dist/state/turnRunner.d.ts.map +1 -0
- package/dist/state/turnRunner.js +476 -0
- package/dist/state/turnRunner.js.map +1 -0
- package/package.json +47 -0
- package/src/auth/argon2.ts +152 -0
- package/src/auth/encryption.ts +110 -0
- package/src/auth/jwt.ts +217 -0
- package/src/auth/middleware.ts +250 -0
- package/src/auth/passwordPolicy.ts +99 -0
- package/src/auth/roleRules.ts +497 -0
- package/src/auth/userView.ts +88 -0
- package/src/bootstrap.ts +254 -0
- package/src/db/connection.ts +75 -0
- package/src/db/migrate.ts +212 -0
- package/src/db/schema.ts +750 -0
- package/src/db/seed.ts +236 -0
- package/src/handlers/fetch.ts +251 -0
- package/src/handlers/nextjs.ts +49 -0
- package/src/handlers/responses.ts +95 -0
- package/src/handlers/routeTable.ts +188 -0
- package/src/handlers/router.ts +96 -0
- package/src/handlers/sse.ts +55 -0
- package/src/handlers/types.ts +104 -0
- package/src/index.ts +60 -0
- package/src/letta/client.ts +860 -0
- package/src/letta/eventTranslator.ts +667 -0
- package/src/letta/index.ts +46 -0
- package/src/letta/lettaCliShim.mjs +40 -0
- package/src/letta/sandbox.ts +167 -0
- package/src/letta/settingsFileSeed.ts +254 -0
- package/src/letta/subprocessThrottle.ts +251 -0
- package/src/letta/types.ts +65 -0
- package/src/observability/logger.ts +103 -0
- package/src/observability/metrics.ts +114 -0
- package/src/routes/admin/agents.ts +574 -0
- package/src/routes/admin/rotatingMessages.ts +351 -0
- package/src/routes/admin/sessions.ts +283 -0
- package/src/routes/admin/settings.ts +463 -0
- package/src/routes/admin/users.ts +455 -0
- package/src/routes/agents.ts +66 -0
- package/src/routes/auth.ts +513 -0
- package/src/routes/chat.ts +534 -0
- package/src/routes/featureToggles.ts +52 -0
- package/src/routes/health.ts +44 -0
- package/src/routes/metrics.ts +111 -0
- package/src/routes/sessions.ts +1878 -0
- package/src/state/cron.ts +179 -0
- package/src/state/rotationRunner.ts +326 -0
- package/src/state/rotationScheduler.ts +164 -0
- package/src/state/turnRegistry.ts +210 -0
- package/src/state/turnRunner.ts +588 -0
package/src/db/schema.ts
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle schema for the ai-hub server's SQLite database.
|
|
3
|
+
*
|
|
4
|
+
* One row per real-world entity. v1 has no tenant scoping — a deployment is
|
|
5
|
+
* one customer organization, every row belongs to that customer.
|
|
6
|
+
*
|
|
7
|
+
* The shapes here are the canonical source of truth: the migration runner
|
|
8
|
+
* (`migrate.ts`) builds tables off these definitions, and every handler reads
|
|
9
|
+
* and writes through Drizzle's typed query builder.
|
|
10
|
+
*
|
|
11
|
+
* Forward-compat seams (intentionally non-obvious — see §6.7 "Future
|
|
12
|
+
* direction"):
|
|
13
|
+
*
|
|
14
|
+
* - `user.password_hash` is **nullable**. v1 always populates it (local login is
|
|
15
|
+
* the only flow that creates users). Future external IdPs leave it NULL.
|
|
16
|
+
* - `user.identity_source` defaults to `"local"`. v1 only ever writes
|
|
17
|
+
* `"local"`; future external IdPs add values like `"oidc:bankxyz"`.
|
|
18
|
+
* - `agent.is_archived` and `session.is_archived` are soft-hide flags.
|
|
19
|
+
* **Agents and sessions cannot be hard-deleted in v1** (§6.3.4 / §11) — the
|
|
20
|
+
* FK on `session.agent_id` is therefore always satisfiable and orphan
|
|
21
|
+
* sessions never happen.
|
|
22
|
+
*
|
|
23
|
+
* @see SPECIFICATIONS.md §11 — data model
|
|
24
|
+
* @see SPECIFICATIONS.md §6.7 — auth model + future IdPs
|
|
25
|
+
* @see SPECIFICATIONS.md §6.8 — persistence
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { sql } from "drizzle-orm";
|
|
29
|
+
import {
|
|
30
|
+
blob,
|
|
31
|
+
index,
|
|
32
|
+
integer,
|
|
33
|
+
primaryKey,
|
|
34
|
+
sqliteTable,
|
|
35
|
+
text,
|
|
36
|
+
} from "drizzle-orm/sqlite-core";
|
|
37
|
+
|
|
38
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
39
|
+
/* user */
|
|
40
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Every human in this deployment, including super admins. ai-hub owns auth.
|
|
44
|
+
*
|
|
45
|
+
* The PRD uses singular table names; we follow that exactly so SQL tooling
|
|
46
|
+
* matches what implementing agents see in the spec.
|
|
47
|
+
*
|
|
48
|
+
* @see SPECIFICATIONS.md §11
|
|
49
|
+
*/
|
|
50
|
+
export const user = sqliteTable(
|
|
51
|
+
"user",
|
|
52
|
+
{
|
|
53
|
+
/** Stable UUID PK assigned at insert time (server-generated). */
|
|
54
|
+
id: text("id").notNull().primaryKey(),
|
|
55
|
+
/** Login identifier — unique per deployment. */
|
|
56
|
+
username: text("username").notNull().unique(),
|
|
57
|
+
/** Optional human-readable name shown in the admin user list. */
|
|
58
|
+
displayName: text("display_name"),
|
|
59
|
+
/**
|
|
60
|
+
* Argon2id encoded hash (PHC string format: embeds m / t / p / salt /
|
|
61
|
+
* digest). Nullable for forward-compat with external IdPs (§6.7); v1
|
|
62
|
+
* always populates it.
|
|
63
|
+
*/
|
|
64
|
+
passwordHash: text("password_hash"),
|
|
65
|
+
/**
|
|
66
|
+
* `"local"` in v1. Future external IdPs add values like `"oidc:bankxyz"`.
|
|
67
|
+
* Designed-for-extension seam per §6.7.
|
|
68
|
+
*/
|
|
69
|
+
identitySource: text("identity_source").notNull().default("local"),
|
|
70
|
+
/**
|
|
71
|
+
* `true` = the user must change their password on the next successful
|
|
72
|
+
* login (§8.5). Set on the seeded super-admin and after every admin-driven
|
|
73
|
+
* password reset.
|
|
74
|
+
*/
|
|
75
|
+
isInitialPassword: integer("is_initial_password", { mode: "boolean" })
|
|
76
|
+
.notNull()
|
|
77
|
+
.default(false),
|
|
78
|
+
/** One of the four roles in §8.1. */
|
|
79
|
+
role: text("role", {
|
|
80
|
+
enum: ["user", "power_user", "admin", "super_admin"],
|
|
81
|
+
})
|
|
82
|
+
.notNull()
|
|
83
|
+
.default("user"),
|
|
84
|
+
/**
|
|
85
|
+
* `"light"` / `"dark"` / NULL. NULL = follow `admin_setting.theme_default`.
|
|
86
|
+
* Set when the user picks via `AIHub.ThemeSwitcher`. Ignored as long as
|
|
87
|
+
* `theme_switcher_enabled = false` (§7.6).
|
|
88
|
+
*/
|
|
89
|
+
themePreference: text("theme_preference", { enum: ["light", "dark"] }),
|
|
90
|
+
/**
|
|
91
|
+
* ISO-8601 timestamp of the last password mutation. Bumped on:
|
|
92
|
+
*
|
|
93
|
+
* - Admin password reset (`POST /admin/users/:id/reset-password`).
|
|
94
|
+
* - Voluntary password change (`POST /auth/change-password` with a
|
|
95
|
+
* session token + `current_password`).
|
|
96
|
+
* - Forced change after admin reset (`POST /auth/change-password`
|
|
97
|
+
* with a password-change-only token).
|
|
98
|
+
*
|
|
99
|
+
* The middleware compares `passwordChangedAt` against the JWT's
|
|
100
|
+
* `iat` claim — session tokens issued **before** the last password
|
|
101
|
+
* mutation are rejected as `401 invalid_credentials`. This is how
|
|
102
|
+
* an admin-driven reset signs the affected user out immediately
|
|
103
|
+
* without waiting for the 24h JWT expiry (Step 11 bugfix).
|
|
104
|
+
*/
|
|
105
|
+
passwordChangedAt: text("password_changed_at")
|
|
106
|
+
.notNull()
|
|
107
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
108
|
+
/** ISO-8601-string seconds-precision timestamp (better-sqlite3 stores as TEXT). */
|
|
109
|
+
createdAt: text("created_at")
|
|
110
|
+
.notNull()
|
|
111
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
112
|
+
/** Bumped on every UPDATE — handlers SET this explicitly so it's deterministic. */
|
|
113
|
+
updatedAt: text("updated_at")
|
|
114
|
+
.notNull()
|
|
115
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
116
|
+
},
|
|
117
|
+
(t) => ({
|
|
118
|
+
/** Speeds up the role-matrix's "find any user with role super_admin" query. */
|
|
119
|
+
roleIdx: index("user_role_idx").on(t.role),
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
124
|
+
/* agent */
|
|
125
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* `power_user`/`admin`/`super_admin`-curated catalog of ai-hub agents.
|
|
129
|
+
*
|
|
130
|
+
* Each row is a use-case (e.g. *Customer Support*) backed by exactly one Letta
|
|
131
|
+
* agent. Shared across every user in the deployment (§13.2).
|
|
132
|
+
*
|
|
133
|
+
* Cannot be hard-deleted in v1 — only archived. `is_archived = true` filters
|
|
134
|
+
* the row out of the user-facing `GET /agents` response so no new sessions can
|
|
135
|
+
* start against it; existing sessions remain readable + writable.
|
|
136
|
+
*
|
|
137
|
+
* @see SPECIFICATIONS.md §11 — `agent` table
|
|
138
|
+
* @see SPECIFICATIONS.md §6.5 — TPM recovery defaults
|
|
139
|
+
*/
|
|
140
|
+
export const agent = sqliteTable("agent", {
|
|
141
|
+
/** Stable UUID PK. */
|
|
142
|
+
id: text("id").notNull().primaryKey(),
|
|
143
|
+
/** Operator-facing display name (no length cap in v1). */
|
|
144
|
+
displayName: text("display_name").notNull(),
|
|
145
|
+
/**
|
|
146
|
+
* The Letta-side agent's id. Validated against `client.agents.list` at
|
|
147
|
+
* write time (Step 5 wires the real check; Step 4 stubs it through).
|
|
148
|
+
*/
|
|
149
|
+
lettaAgentId: text("letta_agent_id").notNull(),
|
|
150
|
+
/** Master toggle for the §6.5 recovery loop. Default `false`. */
|
|
151
|
+
tpmRecoveryEnabled: integer("tpm_recovery_enabled", { mode: "boolean" })
|
|
152
|
+
.notNull()
|
|
153
|
+
.default(false),
|
|
154
|
+
/** Effective ceiling on recovery attempts (§6.5 double-failure abort). */
|
|
155
|
+
maxRecoveryAttempts: integer("max_recovery_attempts").notNull().default(2),
|
|
156
|
+
/** Used when 429 doesn't include `Retry-After`. Default 65 (§6.5). */
|
|
157
|
+
defaultRetryAfterSeconds: integer("default_retry_after_seconds")
|
|
158
|
+
.notNull()
|
|
159
|
+
.default(65),
|
|
160
|
+
/**
|
|
161
|
+
* Inter-event idle watchdog for the chat stream. If no Letta event
|
|
162
|
+
* arrives within this many seconds while a turn is `running`, the
|
|
163
|
+
* runner aborts the turn cleanly: emits
|
|
164
|
+
* `error{code:"stream_idle"}` + `stream_end{reason:"error"}`, cancels
|
|
165
|
+
* the upstream Letta run via `client.agents.messages.cancel`, and
|
|
166
|
+
* transitions `turn.status -> failed`. The timer **resets on every
|
|
167
|
+
* received event** — well-behaved long-running turns (lots of small
|
|
168
|
+
* deltas) never trip it; truly wedged ones (provider hang, Letta
|
|
169
|
+
* deadlock) terminate predictably. Default 60s. Set higher for
|
|
170
|
+
* tool-heavy agents whose tools may legitimately take longer to run
|
|
171
|
+
* with no streaming output in between; set lower if you'd rather
|
|
172
|
+
* fail fast on a stalled provider.
|
|
173
|
+
*/
|
|
174
|
+
streamIdleTimeoutSeconds: integer("stream_idle_timeout_seconds")
|
|
175
|
+
.notNull()
|
|
176
|
+
.default(60),
|
|
177
|
+
/**
|
|
178
|
+
* Step-12.5 specialized-agent lever #1 — per-session sandbox directories.
|
|
179
|
+
*
|
|
180
|
+
* Relative folder name under `{AI_HUB_FILES_DIR}/seeds/` whose contents
|
|
181
|
+
* the server recursively copies into `{AI_HUB_FILES_DIR}/sandboxes/{session_id}/`
|
|
182
|
+
* at session-create time. The Letta Code subprocess for the session then
|
|
183
|
+
* spawns with `cwd` pointed at the sandbox, and its bundled filesystem
|
|
184
|
+
* tools (`Read` / `Write` / `Edit` / `Bash` / `Grep` / `Glob`) are
|
|
185
|
+
* re-enabled so the agent can use the seeded files. NULL = no sandbox;
|
|
186
|
+
* sessions run with no `cwd` and the existing disallowed-tools list
|
|
187
|
+
* (today's stateless agent behavior). Validated `^[a-zA-Z0-9_-]+$` at
|
|
188
|
+
* admin-write time + checked to exist on disk.
|
|
189
|
+
*
|
|
190
|
+
* @see docs/server/session-sandboxes.md
|
|
191
|
+
*/
|
|
192
|
+
sandboxSeedName: text("sandbox_seed_name"),
|
|
193
|
+
/**
|
|
194
|
+
* Step-12.5 specialized-agent lever #2 — base-conversation forking.
|
|
195
|
+
*
|
|
196
|
+
* Letta conversation id (e.g. `conv-…`) to fork from at `POST /sessions`
|
|
197
|
+
* time via `client.conversations.fork(...)`. The fork inherits the
|
|
198
|
+
* source's in-context messages + gets a freshly compiled system message.
|
|
199
|
+
* NULL = brand-new conversation via `client.conversations.create({ agent_id })`
|
|
200
|
+
* (today's behavior). Validated at admin-write time: must resolve via
|
|
201
|
+
* `client.conversations.retrieve` AND its `agent_id` must equal
|
|
202
|
+
* `letta_agent_id`.
|
|
203
|
+
*
|
|
204
|
+
* @see docs/server/session-sandboxes.md
|
|
205
|
+
*/
|
|
206
|
+
forkSourceConversationId: text("fork_source_conversation_id"),
|
|
207
|
+
/** Soft-hide flag — see file-level comment. */
|
|
208
|
+
isArchived: integer("is_archived", { mode: "boolean" }).notNull().default(false),
|
|
209
|
+
/** ISO-8601 timestamp. */
|
|
210
|
+
createdAt: text("created_at")
|
|
211
|
+
.notNull()
|
|
212
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
213
|
+
/** ISO-8601 timestamp. */
|
|
214
|
+
updatedAt: text("updated_at")
|
|
215
|
+
.notNull()
|
|
216
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
220
|
+
/* session */
|
|
221
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* One row per ai-hub session — a user's chat against a given ai-hub agent.
|
|
225
|
+
*
|
|
226
|
+
* `letta_conversation_id` is server-internal: the React client sees only
|
|
227
|
+
* `id`. `letta_agent_id` is a snapshot of `agent.letta_agent_id` at session
|
|
228
|
+
* creation time (re-pointing the catalog row does not migrate existing
|
|
229
|
+
* conversations).
|
|
230
|
+
*
|
|
231
|
+
* @see SPECIFICATIONS.md §11 — `session` table
|
|
232
|
+
* @see SPECIFICATIONS.md §6.3.1 — sessions API
|
|
233
|
+
*/
|
|
234
|
+
export const session = sqliteTable(
|
|
235
|
+
"session",
|
|
236
|
+
{
|
|
237
|
+
/** Stable UUID PK. */
|
|
238
|
+
id: text("id").notNull().primaryKey(),
|
|
239
|
+
/** Owner — enforces (caller_id === user_id) on read/write (§13.2). */
|
|
240
|
+
userId: text("user_id")
|
|
241
|
+
.notNull()
|
|
242
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
243
|
+
/**
|
|
244
|
+
* The ai-hub agent (use-case) this session is scoped to. RESTRICT-on-delete
|
|
245
|
+
* is moot in v1 because agents are never hard-deleted — only archived.
|
|
246
|
+
*/
|
|
247
|
+
agentId: text("agent_id")
|
|
248
|
+
.notNull()
|
|
249
|
+
.references(() => agent.id, { onDelete: "restrict" }),
|
|
250
|
+
/** Identifier returned by Letta on `client.conversations.create`. */
|
|
251
|
+
lettaConversationId: text("letta_conversation_id").notNull(),
|
|
252
|
+
/**
|
|
253
|
+
* Snapshot of `agent.letta_agent_id` at create-time. Re-pointing the
|
|
254
|
+
* catalog entry does not move existing conversations.
|
|
255
|
+
*/
|
|
256
|
+
lettaAgentId: text("letta_agent_id").notNull(),
|
|
257
|
+
/**
|
|
258
|
+
* Required at create-time (1..80 chars enforced by Zod at the handler
|
|
259
|
+
* layer, §6.3.1); editable via PATCH `/sessions/{id}`.
|
|
260
|
+
*/
|
|
261
|
+
title: text("title").notNull(),
|
|
262
|
+
/**
|
|
263
|
+
* Soft-hide. Excluded from `GET /sessions` by default; `?include_archived=true`
|
|
264
|
+
* surfaces archived rows. There is no hard-delete (§6.3.1).
|
|
265
|
+
*/
|
|
266
|
+
isArchived: integer("is_archived", { mode: "boolean" }).notNull().default(false),
|
|
267
|
+
/**
|
|
268
|
+
* Per-session reasoning effort, applied to the agent's `model_settings`
|
|
269
|
+
* before each turn. NULL = use agent's persisted default. The accepted
|
|
270
|
+
* values depend on the agent's model — Anthropic uses `low|medium|high|xhigh|max`,
|
|
271
|
+
* OpenAI uses `none|minimal|low|medium|high|xhigh`. The capability map
|
|
272
|
+
* (computed from the agent's model handle) drives UI rendering.
|
|
273
|
+
*/
|
|
274
|
+
reasoningEffort: text("reasoning_effort"),
|
|
275
|
+
/**
|
|
276
|
+
* Per-session verbosity, OpenAI-family models only. NULL = default.
|
|
277
|
+
*/
|
|
278
|
+
verbosity: text("verbosity"),
|
|
279
|
+
/**
|
|
280
|
+
* Per-session thinking budget for Google AI/Vertex models (numeric token
|
|
281
|
+
* budget). NULL = default.
|
|
282
|
+
*/
|
|
283
|
+
thinkingBudget: integer("thinking_budget"),
|
|
284
|
+
/**
|
|
285
|
+
* Step-12.5 — absolute filesystem path of the per-session sandbox
|
|
286
|
+
* directory. Populated at create time **only when** the parent agent's
|
|
287
|
+
* `sandbox_seed_name` was non-null (the server recursively copied the
|
|
288
|
+
* seed folder into this path before responding with the new session).
|
|
289
|
+
* NULL = no sandbox; the Letta Code subprocess for this session spawns
|
|
290
|
+
* with no `cwd` and the existing disallowed-tools list.
|
|
291
|
+
*
|
|
292
|
+
* Server-internal — never exposed via wire (mirrors
|
|
293
|
+
* `letta_conversation_id`). Cleared to NULL by
|
|
294
|
+
* `POST /admin/sessions/:id/purge-sandbox`.
|
|
295
|
+
*
|
|
296
|
+
* @see docs/server/session-sandboxes.md
|
|
297
|
+
*/
|
|
298
|
+
sandboxDir: text("sandbox_dir"),
|
|
299
|
+
/** ISO-8601 timestamp. */
|
|
300
|
+
createdAt: text("created_at")
|
|
301
|
+
.notNull()
|
|
302
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
303
|
+
/** ISO-8601 timestamp. */
|
|
304
|
+
updatedAt: text("updated_at")
|
|
305
|
+
.notNull()
|
|
306
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
307
|
+
},
|
|
308
|
+
(t) => ({
|
|
309
|
+
/** Drives the user-scoped session list + the per-caller session_count on /agents. */
|
|
310
|
+
userAgentIdx: index("session_user_agent_idx").on(t.userId, t.agentId),
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
315
|
+
/* turn */
|
|
316
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* One row per AI turn (user message + assistant response cycle).
|
|
320
|
+
*
|
|
321
|
+
* Lives in the ai-hub DB for the *streaming session* (resumption + state
|
|
322
|
+
* transitions); message history itself is Letta-owned.
|
|
323
|
+
*
|
|
324
|
+
* @see SPECIFICATIONS.md §11
|
|
325
|
+
* @see SPECIFICATIONS.md §10 — state machine
|
|
326
|
+
*/
|
|
327
|
+
export const turn = sqliteTable("turn", {
|
|
328
|
+
/** Stable UUID PK. */
|
|
329
|
+
id: text("id").notNull().primaryKey(),
|
|
330
|
+
/** Parent session. Cascades on delete — but sessions never delete in v1. */
|
|
331
|
+
sessionId: text("session_id")
|
|
332
|
+
.notNull()
|
|
333
|
+
.references(() => session.id, { onDelete: "cascade" }),
|
|
334
|
+
/** Top-level §10.1 turn status. `idle` is excluded — no row exists yet then. */
|
|
335
|
+
status: text("status", {
|
|
336
|
+
enum: ["running", "done", "cancelled", "failed"],
|
|
337
|
+
}).notNull(),
|
|
338
|
+
/** Sub-state per §10.1; only meaningful while `status = 'running'`. */
|
|
339
|
+
phase: text("phase", {
|
|
340
|
+
enum: [
|
|
341
|
+
"model_text",
|
|
342
|
+
"model_reasoning",
|
|
343
|
+
"model_tool_input",
|
|
344
|
+
"tool_executing",
|
|
345
|
+
"waiting_provider",
|
|
346
|
+
"awaiting_user",
|
|
347
|
+
],
|
|
348
|
+
}),
|
|
349
|
+
/** Highest emitted `seq` (§9). 0 if no events yet. */
|
|
350
|
+
lastSeq: integer("last_seq").notNull().default(0),
|
|
351
|
+
/** Optional client-supplied key for resume-by-rerequest (§6.3.2). */
|
|
352
|
+
idempotencyKey: text("idempotency_key"),
|
|
353
|
+
/** ISO-8601. */
|
|
354
|
+
startedAt: text("started_at")
|
|
355
|
+
.notNull()
|
|
356
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
357
|
+
/** ISO-8601 — set when the turn enters a terminal state. */
|
|
358
|
+
finishedAt: text("finished_at"),
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
362
|
+
/* turn_event */
|
|
363
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Append-only log of SSE events per turn for `/chat/{turn_id}/events`
|
|
367
|
+
* resumption. Composite PK = `(turn_id, seq)`.
|
|
368
|
+
*
|
|
369
|
+
* @see SPECIFICATIONS.md §11
|
|
370
|
+
* @see SPECIFICATIONS.md §6.6 — state machine and SSE
|
|
371
|
+
*/
|
|
372
|
+
export const turnEvent = sqliteTable(
|
|
373
|
+
"turn_event",
|
|
374
|
+
{
|
|
375
|
+
/** Parent turn. Cascade on parent delete (rare in practice — turns persist). */
|
|
376
|
+
turnId: text("turn_id")
|
|
377
|
+
.notNull()
|
|
378
|
+
.references(() => turn.id, { onDelete: "cascade" }),
|
|
379
|
+
/** Monotonic per-turn (§9). 0-based. */
|
|
380
|
+
seq: integer("seq").notNull(),
|
|
381
|
+
/** Discriminator from the §9 wire protocol. */
|
|
382
|
+
eventName: text("event_name").notNull(),
|
|
383
|
+
/** JSON-serialized payload. Written via `JSON.stringify` at the handler layer. */
|
|
384
|
+
data: text("data", { mode: "json" }).notNull(),
|
|
385
|
+
/** `(status, phase)` snapshot at emission. */
|
|
386
|
+
state: text("state", { mode: "json" }).notNull(),
|
|
387
|
+
/** ISO-8601 timestamp. */
|
|
388
|
+
createdAt: text("created_at")
|
|
389
|
+
.notNull()
|
|
390
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
391
|
+
},
|
|
392
|
+
(t) => ({
|
|
393
|
+
pk: primaryKey({ columns: [t.turnId, t.seq] }),
|
|
394
|
+
}),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
398
|
+
/* admin_setting */
|
|
399
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Singleton row holding deployment-wide settings.
|
|
403
|
+
*
|
|
404
|
+
* `id` is a fixed integer (= 1) so the singleton constraint is enforceable
|
|
405
|
+
* via PK. The encryption key (`AI_HUB_ENCRYPTION_KEY`) is **not** a column —
|
|
406
|
+
* it lives in env only (§6.8 / §12.1).
|
|
407
|
+
*
|
|
408
|
+
* Bootstrap-only env semantics for `service_token_signing_key` and
|
|
409
|
+
* `metrics_scrape_token` are documented in §6.7 / §6.10 / §12.1: on first
|
|
410
|
+
* boot, env values seed these columns; after that, env is no longer
|
|
411
|
+
* consulted. Rotation lives at `/admin/settings/rotate-{signing-key,
|
|
412
|
+
* metrics-token}`.
|
|
413
|
+
*
|
|
414
|
+
* @see SPECIFICATIONS.md §11
|
|
415
|
+
* @see SPECIFICATIONS.md §6.10 — `/metrics` bearer auth
|
|
416
|
+
* @see SPECIFICATIONS.md §6.7 — single signing key
|
|
417
|
+
* @see SPECIFICATIONS.md §12.1 — env vars
|
|
418
|
+
*/
|
|
419
|
+
export const adminSetting = sqliteTable("admin_setting", {
|
|
420
|
+
/** Always 1 — singleton row. */
|
|
421
|
+
id: integer("id").notNull().primaryKey(),
|
|
422
|
+
/** AES-256 encrypted Letta API key. Encrypted under env `AI_HUB_ENCRYPTION_KEY`. */
|
|
423
|
+
lettaApiKeyEncrypted: blob("letta_api_key_encrypted", { mode: "buffer" }),
|
|
424
|
+
/** Letta server base URL (e.g. `http://localhost:8283/v1`). */
|
|
425
|
+
lettaBaseUrl: text("letta_base_url"),
|
|
426
|
+
/** Optional super-admin override for the "Open Letta ADE" link target (§6.3.4). */
|
|
427
|
+
lettaAdeUrl: text("letta_ade_url"),
|
|
428
|
+
/** Flat JSON blob of UI feature toggles per §8.4. Defaults to `{}`. */
|
|
429
|
+
featureToggles: text("feature_toggles", { mode: "json" }).notNull().default("{}"),
|
|
430
|
+
/** When false, the deployment is single-theme (§7.6). */
|
|
431
|
+
themeSwitcherEnabled: integer("theme_switcher_enabled", { mode: "boolean" })
|
|
432
|
+
.notNull()
|
|
433
|
+
.default(true),
|
|
434
|
+
/** `"light"` | `"dark"`. Initial theme for new users (§7.6). */
|
|
435
|
+
themeDefault: text("theme_default", { enum: ["light", "dark"] })
|
|
436
|
+
.notNull()
|
|
437
|
+
.default("light"),
|
|
438
|
+
/**
|
|
439
|
+
* JWT signing key. Bootstrap-seeded from env `SERVICE_TOKEN_SIGNING_KEY` on
|
|
440
|
+
* first boot if set, else random. Authoritative after first boot — rotate
|
|
441
|
+
* via `/admin/settings/rotate-signing-key` (§6.7).
|
|
442
|
+
*/
|
|
443
|
+
serviceTokenSigningKey: blob("service_token_signing_key", { mode: "buffer" }),
|
|
444
|
+
/**
|
|
445
|
+
* 32-byte hex token used by `/metrics` (§6.10). Bootstrap-seeded from env
|
|
446
|
+
* `METRICS_SCRAPE_TOKEN` if set, else random + logged once on stdout.
|
|
447
|
+
*/
|
|
448
|
+
metricsScrapeToken: text("metrics_scrape_token"),
|
|
449
|
+
/** Last rotation timestamp; surfaced in the metrics-token settings section. */
|
|
450
|
+
metricsScrapeTokenRotatedAt: text("metrics_scrape_token_rotated_at"),
|
|
451
|
+
/** Last rotation timestamp; surfaced in the signing-key settings section. */
|
|
452
|
+
signingKeyRotatedAt: text("signing_key_rotated_at"),
|
|
453
|
+
/** ISO-8601 timestamp. */
|
|
454
|
+
updatedAt: text("updated_at")
|
|
455
|
+
.notNull()
|
|
456
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
460
|
+
/* message_override */
|
|
461
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Per-conversation, per-position **two-layer** content overrides for the
|
|
465
|
+
* messages rendered by `GET /sessions/:id/messages` and (forward-looking)
|
|
466
|
+
* the LLM-visible content at that position.
|
|
467
|
+
*
|
|
468
|
+
* **Why two layers.** Every message has two viewpoints that can legitimately
|
|
469
|
+
* diverge in tier-3 customizations:
|
|
470
|
+
*
|
|
471
|
+
* - **system view** (`system_override`) — what the LLM saw / should see at
|
|
472
|
+
* this position. Today this column is *stored but inert at read time* —
|
|
473
|
+
* we keep it because Letta owns its own conversation history and we
|
|
474
|
+
* can't actually rewrite past rows it persisted. It exists for two
|
|
475
|
+
* forward-looking purposes: audit/debug tooling that wants to know
|
|
476
|
+
* what the operator intends the LLM context to look like, and a future
|
|
477
|
+
* context-replay feature.
|
|
478
|
+
*
|
|
479
|
+
* - **user view** (`user_override`) — what the human sees in the chat
|
|
480
|
+
* transcript at this position. This is what the messages-list route
|
|
481
|
+
* substitutes for display.
|
|
482
|
+
*
|
|
483
|
+
* Both columns are nullable; at least one must be non-null per a CHECK
|
|
484
|
+
* constraint (enforced in the migration SQL). The "set just one column"
|
|
485
|
+
* shape lets one writer set the user-facing view without clobbering an
|
|
486
|
+
* earlier-set system-facing one and vice-versa.
|
|
487
|
+
*
|
|
488
|
+
* **Why position-keyed instead of id-keyed.** Letta rotates the
|
|
489
|
+
* `system_message.id` on every prompt recompile, so id-based keys would
|
|
490
|
+
* go stale. The chronologically-sorted index into
|
|
491
|
+
* `listMessages(conversation_id)` is stable across recompiles.
|
|
492
|
+
*
|
|
493
|
+
* **Lifecycle / writers.**
|
|
494
|
+
*
|
|
495
|
+
* - The `POST /chat` post-turn writer creates a row with `user_override`
|
|
496
|
+
* populated (and `system_override` left null) when the request body
|
|
497
|
+
* carried a `user_override` parameter AND the turn terminated `done`.
|
|
498
|
+
* It runs immediately after the turn-end event, at which point we know
|
|
499
|
+
* the user message persisted and no turn is active.
|
|
500
|
+
*
|
|
501
|
+
* - `PUT /sessions/:id/message-overrides/:position` is the manual writer
|
|
502
|
+
* for past messages — operator / SDK callers. Gated by two checks: the
|
|
503
|
+
* position must already exist in Letta's message list, and no turn for
|
|
504
|
+
* the session may be live. Merge semantics: only the columns the body
|
|
505
|
+
* provided are written; an existing column on the other layer is
|
|
506
|
+
* preserved.
|
|
507
|
+
*
|
|
508
|
+
* @see SPECIFICATIONS.md §6.3.1 — sessions API
|
|
509
|
+
* @see SPECIFICATIONS.md §6.4 — Letta integration boundary
|
|
510
|
+
*/
|
|
511
|
+
export const messageOverride = sqliteTable(
|
|
512
|
+
"message_override",
|
|
513
|
+
{
|
|
514
|
+
/** Letta conversation id this override applies within. */
|
|
515
|
+
conversationId: text("conversation_id").notNull(),
|
|
516
|
+
/**
|
|
517
|
+
* 0-based index into the chronologically-sorted message list returned
|
|
518
|
+
* by `LettaClient.listMessages(conversationId)`. Stable across
|
|
519
|
+
* system-prompt recompiles — only the `system_message` id rotates,
|
|
520
|
+
* not its position.
|
|
521
|
+
*/
|
|
522
|
+
ordinalPosition: integer("ordinal_position").notNull(),
|
|
523
|
+
/**
|
|
524
|
+
* What the LLM saw / should see at this position. Stored but inert
|
|
525
|
+
* at display time today (Letta owns its own history). Tomorrow:
|
|
526
|
+
* audit/debug rendering, context-replay features. NULL when only the
|
|
527
|
+
* `user_override` layer was set.
|
|
528
|
+
*/
|
|
529
|
+
systemOverride: text("system_override"),
|
|
530
|
+
/**
|
|
531
|
+
* What the human sees in the chat transcript at this position.
|
|
532
|
+
* Substituted verbatim into the wire `content` field by the
|
|
533
|
+
* messages-list route. Empty string is valid — it hides the message
|
|
534
|
+
* visually but keeps the bubble slot (use DELETE to remove the slot
|
|
535
|
+
* entirely). NULL when only the `system_override` layer was set.
|
|
536
|
+
*/
|
|
537
|
+
userOverride: text("user_override"),
|
|
538
|
+
/** ISO-8601 timestamp. */
|
|
539
|
+
createdAt: text("created_at")
|
|
540
|
+
.notNull()
|
|
541
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
542
|
+
/** ISO-8601 timestamp. */
|
|
543
|
+
updatedAt: text("updated_at")
|
|
544
|
+
.notNull()
|
|
545
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
546
|
+
},
|
|
547
|
+
(t) => ({
|
|
548
|
+
pk: primaryKey({ columns: [t.conversationId, t.ordinalPosition] }),
|
|
549
|
+
}),
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
553
|
+
/* Inferred row types */
|
|
554
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
555
|
+
|
|
556
|
+
/** Row shape for the `user` table (Drizzle-inferred). */
|
|
557
|
+
export type UserRow = typeof user.$inferSelect;
|
|
558
|
+
/** Insert shape for the `user` table. */
|
|
559
|
+
export type UserInsert = typeof user.$inferInsert;
|
|
560
|
+
/** Row shape for the `agent` table. */
|
|
561
|
+
export type AgentRow = typeof agent.$inferSelect;
|
|
562
|
+
/** Insert shape for the `agent` table. */
|
|
563
|
+
export type AgentInsert = typeof agent.$inferInsert;
|
|
564
|
+
/** Row shape for the `session` table. */
|
|
565
|
+
export type SessionRow = typeof session.$inferSelect;
|
|
566
|
+
/** Insert shape for the `session` table. */
|
|
567
|
+
export type SessionInsert = typeof session.$inferInsert;
|
|
568
|
+
/** Row shape for the `turn` table. */
|
|
569
|
+
export type TurnRow = typeof turn.$inferSelect;
|
|
570
|
+
/** Insert shape for the `turn` table. */
|
|
571
|
+
export type TurnInsert = typeof turn.$inferInsert;
|
|
572
|
+
/** Row shape for the `turn_event` table. */
|
|
573
|
+
export type TurnEventRow = typeof turnEvent.$inferSelect;
|
|
574
|
+
/** Insert shape for the `turn_event` table. */
|
|
575
|
+
export type TurnEventInsert = typeof turnEvent.$inferInsert;
|
|
576
|
+
/** Row shape for the `admin_setting` table. */
|
|
577
|
+
export type AdminSettingRow = typeof adminSetting.$inferSelect;
|
|
578
|
+
/** Insert shape for the `admin_setting` table. */
|
|
579
|
+
export type AdminSettingInsert = typeof adminSetting.$inferInsert;
|
|
580
|
+
/** Row shape for the `message_override` table. */
|
|
581
|
+
export type MessageOverrideRow = typeof messageOverride.$inferSelect;
|
|
582
|
+
/** Insert shape for the `message_override` table. */
|
|
583
|
+
export type MessageOverrideInsert = typeof messageOverride.$inferInsert;
|
|
584
|
+
|
|
585
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
586
|
+
/* message_usage */
|
|
587
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Provider-reported per-turn token usage, stamped on the LAST assistant
|
|
591
|
+
* message of each turn. Surfaced to the chat UI via a hover badge so
|
|
592
|
+
* users can see (a) total tokens consumed and (b) prompt-cache
|
|
593
|
+
* hit/miss split (Anthropic + OpenAI + Gemini all report
|
|
594
|
+
* `cached_input_tokens`; Anthropic also reports `cache_write_tokens`).
|
|
595
|
+
*
|
|
596
|
+
* The runner fetches Letta's `GET /v1/runs/{run_id}/messages` after
|
|
597
|
+
* the turn ends, finds the `usage_statistics`-typed message, and
|
|
598
|
+
* inserts one row here keyed by the last assistant message's Letta id.
|
|
599
|
+
*
|
|
600
|
+
* @see SPECIFICATIONS.md §9 — `usage` SSE event
|
|
601
|
+
* @see docs/server/chat-sse.md — wire protocol
|
|
602
|
+
*/
|
|
603
|
+
export const messageUsage = sqliteTable(
|
|
604
|
+
"message_usage",
|
|
605
|
+
{
|
|
606
|
+
/**
|
|
607
|
+
* Letta message id this usage belongs to — typically the LAST
|
|
608
|
+
* assistant message of the turn that produced it.
|
|
609
|
+
*/
|
|
610
|
+
lettaMessageId: text("letta_message_id").notNull().primaryKey(),
|
|
611
|
+
/** Owning session; cascade-deletes when the session is hard-deleted. */
|
|
612
|
+
sessionId: text("session_id")
|
|
613
|
+
.notNull()
|
|
614
|
+
.references(() => session.id, { onDelete: "cascade" }),
|
|
615
|
+
/** Total non-cached prompt tokens (input). */
|
|
616
|
+
promptTokens: integer("prompt_tokens"),
|
|
617
|
+
/** Completion tokens (output). */
|
|
618
|
+
completionTokens: integer("completion_tokens"),
|
|
619
|
+
/** Sum reported by the provider — may differ from prompt+completion when reasoning is counted separately. */
|
|
620
|
+
totalTokens: integer("total_tokens"),
|
|
621
|
+
/** Prompt-cache HIT tokens. Anthropic / OpenAI / Gemini all report. */
|
|
622
|
+
cachedInputTokens: integer("cached_input_tokens"),
|
|
623
|
+
/** Prompt-cache WRITE tokens. Anthropic only — null elsewhere. */
|
|
624
|
+
cacheWriteTokens: integer("cache_write_tokens"),
|
|
625
|
+
/** Reasoning/thinking tokens. Anthropic + OpenAI o-series. */
|
|
626
|
+
reasoningTokens: integer("reasoning_tokens"),
|
|
627
|
+
/** Rolling context-window size estimate post-turn (Letta-side). */
|
|
628
|
+
contextTokens: integer("context_tokens"),
|
|
629
|
+
/** Optional Letta run id for traceback. */
|
|
630
|
+
runId: text("run_id"),
|
|
631
|
+
/** Stamped at insert time. */
|
|
632
|
+
createdAt: text("created_at")
|
|
633
|
+
.notNull()
|
|
634
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
635
|
+
},
|
|
636
|
+
(t) => ({
|
|
637
|
+
/** Per-session lookups when rendering /sessions/{id}/messages. */
|
|
638
|
+
sessionIdx: index("message_usage_session_idx").on(t.sessionId),
|
|
639
|
+
}),
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
/** Row shape for the `message_usage` table. */
|
|
643
|
+
export type MessageUsageRow = typeof messageUsage.$inferSelect;
|
|
644
|
+
/** Insert shape for the `message_usage` table. */
|
|
645
|
+
export type MessageUsageInsert = typeof messageUsage.$inferInsert;
|
|
646
|
+
|
|
647
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
648
|
+
/* rotating_message */
|
|
649
|
+
/* ────────────────────────────────────────────────────────────────────────── */
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Admin-curated **rotating starting message** — a prompt that ai-hub runs
|
|
653
|
+
* against a Letta agent on a schedule, capturing the agent's reply as a
|
|
654
|
+
* *pre-prepared conversation* that refreshes automatically.
|
|
655
|
+
*
|
|
656
|
+
* This is an automated, self-refreshing evolution of `agent.fork_source_conversation_id`
|
|
657
|
+
* (static base-conversation forking). Instead of an operator hand-authoring a
|
|
658
|
+
* base conversation in the ADE and pasting its id, a rotation runner:
|
|
659
|
+
*
|
|
660
|
+
* 1. `createConversation(letta_agent_id)`,
|
|
661
|
+
* 2. sends `prompt` (drained to completion via the normal streaming path),
|
|
662
|
+
* 3. deletes the prompt's `user_message` so the conversation opens with the
|
|
663
|
+
* agent's reply,
|
|
664
|
+
* 4. stores the resulting conversation id in `current_conversation_id`.
|
|
665
|
+
*
|
|
666
|
+
* The row's `id` (a `rmsg-…` token) is the identifier callers reference
|
|
667
|
+
* anywhere a conversation is referenced. Today that's the agent's **Base
|
|
668
|
+
* conversation ID** field: an `agent.fork_source_conversation_id` of
|
|
669
|
+
* `rmsg-<id>` resolves (at `POST /sessions`) to this row's
|
|
670
|
+
* `current_conversation_id`, which is then forked like any static base.
|
|
671
|
+
*
|
|
672
|
+
* **Same-agent fork constraint.** Letta's fork API requires the new
|
|
673
|
+
* conversation to share the source's agent, so a rotating message can only
|
|
674
|
+
* back ai-hub agents whose `letta_agent_id` equals this row's
|
|
675
|
+
* `letta_agent_id`. The admin agents validator enforces this at write time
|
|
676
|
+
* (mirrors the static `fork_source_agent_mismatch` check).
|
|
677
|
+
*
|
|
678
|
+
* **Resilience.** A failed rotation never blanks a working
|
|
679
|
+
* `current_conversation_id` — the runner keeps the last-good value, records
|
|
680
|
+
* `last_status = 'error'` + `last_error`, and retries on the next interval.
|
|
681
|
+
*
|
|
682
|
+
* @see docs/server/rotating-messages.md
|
|
683
|
+
*/
|
|
684
|
+
export const rotatingMessage = sqliteTable("rotating_message", {
|
|
685
|
+
/** Stable PK in `rmsg-<uuid>` form — also the public reference token. */
|
|
686
|
+
id: text("id").notNull().primaryKey(),
|
|
687
|
+
/** Operator-facing display name. */
|
|
688
|
+
displayName: text("display_name").notNull(),
|
|
689
|
+
/** The prompt sent to the agent each rotation. */
|
|
690
|
+
prompt: text("prompt").notNull(),
|
|
691
|
+
/**
|
|
692
|
+
* Raw Letta agent id the prompt runs against. Validated via
|
|
693
|
+
* `client.agents.retrieve` at admin-write time. Must equal the
|
|
694
|
+
* `letta_agent_id` of any ai-hub agent that references this row.
|
|
695
|
+
*/
|
|
696
|
+
lettaAgentId: text("letta_agent_id").notNull(),
|
|
697
|
+
/**
|
|
698
|
+
* Schedule discriminator:
|
|
699
|
+
* - `"every"` — fixed interval; `interval_seconds` is authoritative.
|
|
700
|
+
* - `"daily"` — once per day at `daily_time` (`"HH:MM"`, server-local).
|
|
701
|
+
* - `"cron"` — `cron_expression` (5-field) parsed via `cron-parser`.
|
|
702
|
+
*/
|
|
703
|
+
scheduleKind: text("schedule_kind", {
|
|
704
|
+
enum: ["every", "daily", "cron"],
|
|
705
|
+
}).notNull(),
|
|
706
|
+
/** Period in seconds for `schedule_kind = 'every'`. NULL otherwise. */
|
|
707
|
+
intervalSeconds: integer("interval_seconds"),
|
|
708
|
+
/** `"HH:MM"` (24h, server-local) for `schedule_kind = 'daily'`. NULL otherwise. */
|
|
709
|
+
dailyTime: text("daily_time"),
|
|
710
|
+
/** 5-field cron expression for `schedule_kind = 'cron'`. NULL otherwise. */
|
|
711
|
+
cronExpression: text("cron_expression"),
|
|
712
|
+
/**
|
|
713
|
+
* The current pre-prepared, forkable Letta conversation id. NULL until the
|
|
714
|
+
* first successful rotation; kept at its last-good value across failures.
|
|
715
|
+
*/
|
|
716
|
+
currentConversationId: text("current_conversation_id"),
|
|
717
|
+
/** Outcome of the most recent rotation attempt. */
|
|
718
|
+
lastStatus: text("last_status", {
|
|
719
|
+
enum: ["pending", "ok", "error"],
|
|
720
|
+
})
|
|
721
|
+
.notNull()
|
|
722
|
+
.default("pending"),
|
|
723
|
+
/** Human-readable error from the last failed rotation. NULL when ok/pending. */
|
|
724
|
+
lastError: text("last_error"),
|
|
725
|
+
/** ISO-8601 timestamp of the last successful rotation. NULL until first success. */
|
|
726
|
+
lastRotatedAt: text("last_rotated_at"),
|
|
727
|
+
/**
|
|
728
|
+
* ISO-8601 timestamp the scheduler compares against `now` to decide when to
|
|
729
|
+
* rotate next. Set to "now" on create (so the first rotation fires on the
|
|
730
|
+
* next tick) and recomputed after every attempt.
|
|
731
|
+
*/
|
|
732
|
+
nextRotationAt: text("next_rotation_at")
|
|
733
|
+
.notNull()
|
|
734
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
735
|
+
/** Soft-hide flag — archived rows are skipped by the scheduler + rejected as fork sources. */
|
|
736
|
+
isArchived: integer("is_archived", { mode: "boolean" }).notNull().default(false),
|
|
737
|
+
/** ISO-8601 timestamp. */
|
|
738
|
+
createdAt: text("created_at")
|
|
739
|
+
.notNull()
|
|
740
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
741
|
+
/** ISO-8601 timestamp. */
|
|
742
|
+
updatedAt: text("updated_at")
|
|
743
|
+
.notNull()
|
|
744
|
+
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
/** Row shape for the `rotating_message` table. */
|
|
748
|
+
export type RotatingMessageRow = typeof rotatingMessage.$inferSelect;
|
|
749
|
+
/** Insert shape for the `rotating_message` table. */
|
|
750
|
+
export type RotatingMessageInsert = typeof rotatingMessage.$inferInsert;
|