@data-club/ai-hub-server 0.0.9 → 0.0.11
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/encryption.d.ts +5 -5
- package/dist/auth/encryption.js +5 -5
- package/dist/auth/userView.d.ts +8 -15
- package/dist/auth/userView.d.ts.map +1 -1
- package/dist/auth/userView.js +10 -23
- package/dist/auth/userView.js.map +1 -1
- package/dist/bootstrap.d.ts +2 -8
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +2 -35
- package/dist/bootstrap.js.map +1 -1
- package/dist/db/migrate.js +6 -17
- package/dist/db/migrate.js.map +1 -1
- package/dist/db/schema.d.ts +17 -263
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +8 -57
- package/dist/db/schema.js.map +1 -1
- package/dist/db/seed.d.ts +4 -19
- package/dist/db/seed.d.ts.map +1 -1
- package/dist/db/seed.js +6 -33
- package/dist/db/seed.js.map +1 -1
- package/dist/handlers/fetch.d.ts +6 -7
- package/dist/handlers/fetch.d.ts.map +1 -1
- package/dist/handlers/fetch.js +1 -3
- package/dist/handlers/fetch.js.map +1 -1
- package/dist/handlers/types.d.ts +5 -6
- package/dist/handlers/types.d.ts.map +1 -1
- package/dist/letta/client.d.ts +86 -263
- package/dist/letta/client.d.ts.map +1 -1
- package/dist/letta/client.js +99 -404
- package/dist/letta/client.js.map +1 -1
- package/dist/letta/index.d.ts +6 -8
- package/dist/letta/index.d.ts.map +1 -1
- package/dist/letta/index.js +5 -7
- package/dist/letta/index.js.map +1 -1
- package/dist/letta/sandbox.d.ts +45 -9
- package/dist/letta/sandbox.d.ts.map +1 -1
- package/dist/letta/sandbox.js +123 -11
- package/dist/letta/sandbox.js.map +1 -1
- package/dist/letta/types.d.ts +6 -49
- package/dist/letta/types.d.ts.map +1 -1
- package/dist/letta/types.js +5 -10
- package/dist/letta/types.js.map +1 -1
- package/dist/observability/logger.d.ts.map +1 -1
- package/dist/observability/logger.js +0 -2
- package/dist/observability/logger.js.map +1 -1
- package/dist/routes/admin/agents.d.ts.map +1 -1
- package/dist/routes/admin/agents.js +0 -40
- package/dist/routes/admin/agents.js.map +1 -1
- package/dist/routes/admin/rotatingMessages.d.ts.map +1 -1
- package/dist/routes/admin/rotatingMessages.js +37 -1
- package/dist/routes/admin/rotatingMessages.js.map +1 -1
- package/dist/routes/admin/settings.d.ts +6 -15
- package/dist/routes/admin/settings.d.ts.map +1 -1
- package/dist/routes/admin/settings.js +9 -107
- package/dist/routes/admin/settings.js.map +1 -1
- package/dist/routes/auth.d.ts +0 -4
- package/dist/routes/auth.d.ts.map +1 -1
- package/dist/routes/auth.js +1 -5
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +9 -30
- package/dist/routes/sessions.js.map +1 -1
- package/dist/state/rotationRunner.d.ts +6 -0
- package/dist/state/rotationRunner.d.ts.map +1 -1
- package/dist/state/rotationRunner.js +19 -2
- package/dist/state/rotationRunner.js.map +1 -1
- package/dist/state/rotationScheduler.d.ts +2 -0
- package/dist/state/rotationScheduler.d.ts.map +1 -1
- package/dist/state/rotationScheduler.js.map +1 -1
- package/dist/state/turnRunner.d.ts.map +1 -1
- package/dist/state/turnRunner.js +1 -75
- package/dist/state/turnRunner.js.map +1 -1
- package/package.json +4 -4
- package/src/auth/encryption.ts +5 -5
- package/src/auth/userView.ts +9 -22
- package/src/bootstrap.ts +4 -43
- package/src/db/migrate.ts +6 -17
- package/src/db/schema.ts +8 -68
- package/src/db/seed.ts +6 -47
- package/src/handlers/fetch.ts +7 -10
- package/src/handlers/types.ts +5 -6
- package/src/letta/client.ts +170 -634
- package/src/letta/index.ts +8 -21
- package/src/letta/sandbox.ts +151 -11
- package/src/letta/types.ts +9 -60
- package/src/observability/logger.ts +0 -2
- package/src/routes/admin/agents.ts +0 -49
- package/src/routes/admin/rotatingMessages.ts +47 -1
- package/src/routes/admin/settings.ts +9 -113
- package/src/routes/auth.ts +1 -5
- package/src/routes/sessions.ts +11 -32
- package/src/state/rotationRunner.ts +37 -1
- package/src/state/rotationScheduler.ts +2 -0
- package/src/state/turnRunner.ts +1 -77
- package/src/letta/lettaCliShim.mjs +0 -40
- package/src/letta/settingsFileSeed.ts +0 -254
package/src/letta/index.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Public exports for the `letta/` module — the single entry point the rest
|
|
3
|
-
* of the server uses to reach Letta.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* @see SPECIFICATIONS.md §6.4
|
|
3
|
+
* of the server uses to reach Letta. Every Letta interaction goes through
|
|
4
|
+
* `LettaClient` (implemented over `@data-club/letta-runtime`'s server-less
|
|
5
|
+
* local backend); no raw HTTP, no other entry points.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
export {
|
|
10
9
|
createDefaultLettaClient,
|
|
10
|
+
ForkSourceNotFoundError,
|
|
11
|
+
ServerOverloadedError,
|
|
12
|
+
type ChatTurnHandle,
|
|
11
13
|
type CreateLettaClientDeps,
|
|
12
14
|
type LettaClient,
|
|
15
|
+
type LettaMessage,
|
|
13
16
|
type StreamUserMessageOpts,
|
|
14
17
|
} from "./client.js";
|
|
15
18
|
export {
|
|
@@ -22,25 +25,9 @@ export {
|
|
|
22
25
|
type UnsequencedEvent,
|
|
23
26
|
} from "./eventTranslator.js";
|
|
24
27
|
export {
|
|
25
|
-
ServerOverloadedError,
|
|
26
28
|
SubprocessThrottle,
|
|
27
29
|
readThrottleConfigFromEnv,
|
|
28
30
|
type ThrottleConfig,
|
|
29
31
|
type ThrottleLease,
|
|
30
32
|
} from "./subprocessThrottle.js";
|
|
31
|
-
export {
|
|
32
|
-
seedAgentToolsetNone,
|
|
33
|
-
type SeedOutcome,
|
|
34
|
-
type ToolsetPreference,
|
|
35
|
-
} from "./settingsFileSeed.js";
|
|
36
|
-
export type {
|
|
37
|
-
LettaAssistantMessage,
|
|
38
|
-
LettaHiddenReasoningMessage,
|
|
39
|
-
LettaMessage,
|
|
40
|
-
LettaReasoningMessage,
|
|
41
|
-
LettaSdk,
|
|
42
|
-
LettaStreamEvent,
|
|
43
|
-
LettaToolCallMessage,
|
|
44
|
-
LettaToolReturnMessage,
|
|
45
|
-
LettaUserMessage,
|
|
46
|
-
} from "./types.js";
|
|
33
|
+
export type { SDKMessage } from "./types.js";
|
package/src/letta/sandbox.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* @see docs/server/session-sandboxes.md
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
20
|
-
import { cp, rm } from "node:fs/promises";
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
20
|
+
import { cp, link, mkdir, readdir, rm } from "node:fs/promises";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -108,16 +108,150 @@ export function resolveSeedDir(filesDir: string, seedName: string): string {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
|
|
111
|
+
* Filename of the optional per-seed materialization manifest, looked up in
|
|
112
|
+
* the seed folder's root. When absent, `materializeSandbox` behaves exactly
|
|
113
|
+
* as before (full recursive copy). The manifest itself is never
|
|
114
|
+
* materialized into the sandbox.
|
|
115
|
+
*/
|
|
116
|
+
export const SEED_MANIFEST_FILENAME = "seed.manifest.json";
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parsed shape of `seed.manifest.json`.
|
|
120
|
+
*
|
|
121
|
+
* `link` — glob patterns (relative to the seed root, `/`-separated) of
|
|
122
|
+
* files to **hardlink** into the sandbox instead of copying. Intended for
|
|
123
|
+
* large read-only assets (multi-hundred-MB SQLite databases) where a
|
|
124
|
+
* per-session copy would cost seconds of latency and gigabytes of disk.
|
|
125
|
+
*
|
|
126
|
+
* Operator contract for linked files (documented in
|
|
127
|
+
* docs/server/session-sandboxes.md):
|
|
128
|
+
* - make them read-only (`chmod 444`) — a hardlink shares the inode, so a
|
|
129
|
+
* write through any sandbox would mutate every session's view;
|
|
130
|
+
* - update them by replace-and-rename (new inode), never in place —
|
|
131
|
+
* running sessions then keep the old data, new sessions get the new;
|
|
132
|
+
* - SQLite seeds should be in `journal_mode=DELETE` (not WAL) so read-only
|
|
133
|
+
* opens need no `-shm`/`-wal` sidecar writes.
|
|
134
|
+
*/
|
|
135
|
+
export interface SeedManifest {
|
|
136
|
+
link?: string[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Converts a manifest glob into an anchored RegExp over the seed-relative
|
|
141
|
+
* posix path. Supported syntax (deliberately minimal, dependency-free):
|
|
142
|
+
* `**` matches any depth, `*` matches within one path segment, `?` matches
|
|
143
|
+
* a single non-separator char. Everything else is literal.
|
|
144
|
+
*/
|
|
145
|
+
function globToRegExp(glob: string): RegExp {
|
|
146
|
+
let out = "";
|
|
147
|
+
for (let i = 0; i < glob.length; i++) {
|
|
148
|
+
const ch = glob[i] as string;
|
|
149
|
+
if (ch === "*") {
|
|
150
|
+
if (glob[i + 1] === "*") {
|
|
151
|
+
out += ".*";
|
|
152
|
+
i++;
|
|
153
|
+
} else {
|
|
154
|
+
out += "[^/]*";
|
|
155
|
+
}
|
|
156
|
+
} else if (ch === "?") {
|
|
157
|
+
out += "[^/]";
|
|
158
|
+
} else {
|
|
159
|
+
out += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return new RegExp(`^${out}$`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Reads and validates the seed manifest, if present. Returns `null` when
|
|
167
|
+
* the seed has no manifest (the common case — plain full-copy seeds).
|
|
168
|
+
* Throws with an operator-actionable message on malformed JSON or shape —
|
|
169
|
+
* a broken manifest should fail session-create loudly, not silently fall
|
|
170
|
+
* back to copying a 500 MB database per session.
|
|
171
|
+
*
|
|
172
|
+
* @param seedDir - absolute seed folder path.
|
|
173
|
+
*/
|
|
174
|
+
export function readSeedManifest(seedDir: string): SeedManifest | null {
|
|
175
|
+
const manifestPath = join(seedDir, SEED_MANIFEST_FILENAME);
|
|
176
|
+
if (!existsSync(manifestPath)) return null;
|
|
177
|
+
let parsed: unknown;
|
|
178
|
+
try {
|
|
179
|
+
parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
180
|
+
} catch (err) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Seed manifest is not valid JSON: ${manifestPath} (${String(err)})`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
186
|
+
throw new Error(`Seed manifest must be a JSON object: ${manifestPath}`);
|
|
187
|
+
}
|
|
188
|
+
const linkRaw = (parsed as Record<string, unknown>).link;
|
|
189
|
+
if (linkRaw !== undefined) {
|
|
190
|
+
if (
|
|
191
|
+
!Array.isArray(linkRaw) ||
|
|
192
|
+
linkRaw.some((p) => typeof p !== "string" || p.length === 0)
|
|
193
|
+
) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Seed manifest "link" must be an array of non-empty glob strings: ${manifestPath}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return linkRaw === undefined ? {} : { link: linkRaw as string[] };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Recursively materializes one directory level of the seed into the
|
|
204
|
+
* sandbox: entries whose seed-relative path matches a `link` glob are
|
|
205
|
+
* hardlinked (zero-copy, instant — seeds and sandboxes live under the same
|
|
206
|
+
* `AI_HUB_FILES_DIR`, hence the same filesystem); everything else is
|
|
207
|
+
* copied. A hardlink attempt that fails with `EXDEV` (cross-device — some
|
|
208
|
+
* exotic bind-mount layout) degrades to a copy rather than failing the
|
|
209
|
+
* session.
|
|
210
|
+
*/
|
|
211
|
+
async function materializeWithManifest(
|
|
212
|
+
seedDir: string,
|
|
213
|
+
targetDir: string,
|
|
214
|
+
relDir: string,
|
|
215
|
+
linkMatchers: RegExp[],
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
const srcDir = relDir === "" ? seedDir : join(seedDir, relDir);
|
|
218
|
+
const dstDir = relDir === "" ? targetDir : join(targetDir, relDir);
|
|
219
|
+
await mkdir(dstDir, { recursive: true });
|
|
220
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
const rel = relDir === "" ? entry.name : `${relDir}/${entry.name}`;
|
|
223
|
+
if (rel === SEED_MANIFEST_FILENAME) continue;
|
|
224
|
+
if (entry.isDirectory()) {
|
|
225
|
+
await materializeWithManifest(seedDir, targetDir, rel, linkMatchers);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const src = join(seedDir, rel);
|
|
229
|
+
const dst = join(targetDir, rel);
|
|
230
|
+
if (entry.isFile() && linkMatchers.some((m) => m.test(rel))) {
|
|
231
|
+
try {
|
|
232
|
+
await link(src, dst);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if ((err as NodeJS.ErrnoException).code !== "EXDEV") throw err;
|
|
235
|
+
await cp(src, dst);
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
// Non-linked files and symlinks: same per-entry semantics as the
|
|
240
|
+
// full-copy path below.
|
|
241
|
+
await cp(src, dst, { errorOnExist: false });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Materializes the seed folder into the (not-yet-existing) sandbox target.
|
|
116
247
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* `
|
|
120
|
-
*
|
|
248
|
+
* Without a `seed.manifest.json` in the seed root this is a full recursive
|
|
249
|
+
* copy — byte-for-byte the pre-manifest behavior. With a manifest, files
|
|
250
|
+
* matching its `link` globs are hardlinked instead of copied (see
|
|
251
|
+
* {@link SeedManifest}); the manifest file itself is skipped.
|
|
252
|
+
*
|
|
253
|
+
* Throws on any I/O failure — the caller is responsible for rolling back
|
|
254
|
+
* the just-created Letta conversation if this throws.
|
|
121
255
|
*
|
|
122
256
|
* @param seedDir - source folder (must exist).
|
|
123
257
|
* @param targetDir - destination folder (must NOT exist).
|
|
@@ -129,6 +263,12 @@ export async function materializeSandbox(
|
|
|
129
263
|
if (!existsSync(seedDir)) {
|
|
130
264
|
throw new Error(`Seed folder does not exist on disk: ${seedDir}`);
|
|
131
265
|
}
|
|
266
|
+
const manifest = readSeedManifest(seedDir);
|
|
267
|
+
if (manifest !== null) {
|
|
268
|
+
const linkMatchers = (manifest.link ?? []).map(globToRegExp);
|
|
269
|
+
await materializeWithManifest(seedDir, targetDir, "", linkMatchers);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
132
272
|
// `recursive: true` + `errorOnExist: false` matches `cp -R` semantics —
|
|
133
273
|
// if `targetDir` somehow already exists (impossible in the happy path
|
|
134
274
|
// since session ids are fresh UUIDs, but cheap to be safe), the
|
package/src/letta/types.ts
CHANGED
|
@@ -1,65 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project-shaped Letta types.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* types
|
|
8
|
-
*
|
|
9
|
-
* If the SDK rev removes or renames one of these, the typechecker flags it
|
|
10
|
-
* at upgrade time and `letta/client.ts` is the single place to adapt.
|
|
11
|
-
*
|
|
12
|
-
* @see SPECIFICATIONS.md §6.4 — SDK-only access pattern
|
|
13
|
-
* @see SPECIFICATIONS.md §6.4.1 — first-class surfaces we depend on
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type Letta from "@letta-ai/letta-client";
|
|
17
|
-
import type {
|
|
18
|
-
AssistantMessage,
|
|
19
|
-
HiddenReasoningMessage,
|
|
20
|
-
Message as LettaSdkMessage,
|
|
21
|
-
ReasoningMessage,
|
|
22
|
-
ToolCallMessage,
|
|
23
|
-
UserMessage,
|
|
24
|
-
} from "@letta-ai/letta-client/resources/agents/messages.js";
|
|
25
|
-
import type { LettaStreamingResponse } from "@letta-ai/letta-client/resources/agents/messages.js";
|
|
26
|
-
import type { ToolReturnMessage } from "@letta-ai/letta-client/resources/tools.js";
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The underlying SDK client. We `import type` everywhere except in
|
|
30
|
-
* `letta/client.ts` (where the constructor actually runs) so the rest of the
|
|
31
|
-
* codebase stays SDK-version-agnostic in shape.
|
|
32
|
-
*/
|
|
33
|
-
export type LettaSdk = Letta;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* One streaming event from `client.conversations.messages.create({ streaming: true })`.
|
|
37
|
-
*
|
|
38
|
-
* The union spans every variant Letta emits in an SSE stream — assistant
|
|
39
|
-
* text, reasoning, tool calls, tool returns, errors, pings, stop reasons,
|
|
40
|
-
* usage stats. `letta/eventTranslator.ts` is the pure function from this
|
|
41
|
-
* union to ai-hub's wire-protocol events (§9).
|
|
42
|
-
*
|
|
43
|
-
* @see SPECIFICATIONS.md §9 — wire protocol
|
|
44
|
-
*/
|
|
45
|
-
export type LettaStreamEvent = LettaStreamingResponse;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Alias for `AssistantMessage` — used by the translator + tests. See
|
|
49
|
-
* `letta/eventTranslator.ts` for the streamed-delta vs. cumulative-content
|
|
50
|
-
* note.
|
|
4
|
+
* Post-migration (server-less local backend), the message shape comes from
|
|
5
|
+
* `@data-club/letta-runtime` — the fork's `messages list` subcommand prints
|
|
6
|
+
* the backend's Letta-format records verbatim, and `LettaMessageRecord`
|
|
7
|
+
* types the fields AI Hub's renderers actually read. The old
|
|
8
|
+
* `@letta-ai/letta-client` aliases are gone with the server.
|
|
51
9
|
*/
|
|
52
|
-
export type LettaAssistantMessage = AssistantMessage;
|
|
53
|
-
/** Alias for `ReasoningMessage` — see `eventTranslator.ts`. */
|
|
54
|
-
export type LettaReasoningMessage = ReasoningMessage;
|
|
55
|
-
/** Alias for `HiddenReasoningMessage` — see `eventTranslator.ts`. */
|
|
56
|
-
export type LettaHiddenReasoningMessage = HiddenReasoningMessage;
|
|
57
|
-
/** Alias for `ToolCallMessage` — see `eventTranslator.ts`. */
|
|
58
|
-
export type LettaToolCallMessage = ToolCallMessage;
|
|
59
|
-
/** Alias for `ToolReturnMessage` — see `eventTranslator.ts`. */
|
|
60
|
-
export type LettaToolReturnMessage = ToolReturnMessage;
|
|
61
|
-
/** Alias for `UserMessage` — only appears in history reads, not in streams. */
|
|
62
|
-
export type LettaUserMessage = UserMessage;
|
|
63
10
|
|
|
64
|
-
|
|
65
|
-
|
|
11
|
+
export type {
|
|
12
|
+
LettaMessageRecord as LettaMessage,
|
|
13
|
+
SDKMessage,
|
|
14
|
+
} from "@data-club/letta-runtime";
|
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
} from "../../handlers/responses.js";
|
|
40
40
|
import type { AIHubServerContext, RouteHandler } from "../../handlers/types.js";
|
|
41
41
|
import { seedDirExists, validateSeedName } from "../../letta/sandbox.js";
|
|
42
|
-
import { seedAgentToolsetNone } from "../../letta/settingsFileSeed.js";
|
|
43
42
|
|
|
44
43
|
/* ────────────────────────────────────────────────────────────────────────── */
|
|
45
44
|
/* Helpers */
|
|
@@ -247,42 +246,6 @@ function respondSpecializedFailure(
|
|
|
247
246
|
}
|
|
248
247
|
}
|
|
249
248
|
|
|
250
|
-
/**
|
|
251
|
-
* Best-effort seed of `~/.letta/settings.json` with `{toolset: "none"}` for
|
|
252
|
-
* the given Letta agent id. Idempotent. Logs the outcome but **does not
|
|
253
|
-
* throw**: if seeding fails (read-only home dir, permission denied, …) the
|
|
254
|
-
* agent creation should still succeed — the toolset suppression is then
|
|
255
|
-
* a runtime concern the operator notices when chats start producing weird
|
|
256
|
-
* file-edit tool calls. The intent here is hands-off automation, not a
|
|
257
|
-
* required precondition.
|
|
258
|
-
*
|
|
259
|
-
* @param ctx - server context (for logging).
|
|
260
|
-
* @param lettaAgentId - id to seed the toolset preference for.
|
|
261
|
-
*
|
|
262
|
-
* @see ../../letta/settingsFileSeed.ts
|
|
263
|
-
*/
|
|
264
|
-
function seedToolsetNoneBestEffort(
|
|
265
|
-
ctx: AIHubServerContext,
|
|
266
|
-
lettaAgentId: string,
|
|
267
|
-
): void {
|
|
268
|
-
try {
|
|
269
|
-
seedAgentToolsetNone({
|
|
270
|
-
agentId: lettaAgentId,
|
|
271
|
-
lettaBaseUrl: process.env.LETTA_BASE_URL,
|
|
272
|
-
logger: ctx.logger,
|
|
273
|
-
});
|
|
274
|
-
} catch (err) {
|
|
275
|
-
ctx.logger.warn(
|
|
276
|
-
{
|
|
277
|
-
agent_id: lettaAgentId,
|
|
278
|
-
err: err instanceof Error ? err.message : String(err),
|
|
279
|
-
event: "letta_code_settings_seed_failed",
|
|
280
|
-
},
|
|
281
|
-
"Failed to seed Letta Code toolset preference; continuing anyway.",
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
249
|
/* ────────────────────────────────────────────────────────────────────────── */
|
|
287
250
|
/* GET /admin/agents */
|
|
288
251
|
/* ────────────────────────────────────────────────────────────────────────── */
|
|
@@ -415,12 +378,6 @@ export const handleCreateAgent: RouteHandler = protectedRoute(
|
|
|
415
378
|
const row = ctx.db.select().from(agent).where(eq(agent.id, id)).get();
|
|
416
379
|
if (!row) throw new Error("Just-inserted agent vanished.");
|
|
417
380
|
|
|
418
|
-
// Seed `~/.letta/settings.json` so Letta Code's bundled CLI suppresses
|
|
419
|
-
// its auto-toolset (Read/Write/Bash/etc.) when chat sessions spawn
|
|
420
|
-
// against this agent. ai-hub wants only the tools the ADE admin
|
|
421
|
-
// explicitly attached. Best-effort: a write failure logs + continues.
|
|
422
|
-
seedToolsetNoneBestEffort(ctx, row.lettaAgentId);
|
|
423
|
-
|
|
424
381
|
ctx.logger.info(
|
|
425
382
|
{
|
|
426
383
|
actor_id: auth.user.id,
|
|
@@ -553,12 +510,6 @@ export const handlePatchAgent: RouteHandler = protectedRoute(
|
|
|
553
510
|
// remove the old agent's seeded record — it may still be backing live
|
|
554
511
|
// sessions per the §11 schema note: existing sessions keep their
|
|
555
512
|
// original `letta_conversation_id` against the original Letta agent.)
|
|
556
|
-
if (
|
|
557
|
-
body.letta_agent_id !== undefined &&
|
|
558
|
-
body.letta_agent_id !== existing.lettaAgentId
|
|
559
|
-
) {
|
|
560
|
-
seedToolsetNoneBestEffort(ctx, body.letta_agent_id);
|
|
561
|
-
}
|
|
562
513
|
|
|
563
514
|
ctx.logger.info(
|
|
564
515
|
{
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
validationError,
|
|
36
36
|
} from "../../handlers/responses.js";
|
|
37
37
|
import type { AIHubServerContext, RouteHandler } from "../../handlers/types.js";
|
|
38
|
+
import { seedDirExists, validateSeedName } from "../../letta/sandbox.js";
|
|
38
39
|
import {
|
|
39
40
|
computeNextRotation,
|
|
40
41
|
runRotation,
|
|
@@ -42,6 +43,39 @@ import {
|
|
|
42
43
|
type RotationSchedule,
|
|
43
44
|
} from "../../state/rotationRunner.js";
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Validates a `sandbox_seed_name` patch/create value the same way
|
|
48
|
+
* `/admin/agents` does. `null`/`""` mean "clear" and normalize to `null`.
|
|
49
|
+
* Returns an error Response, or the normalized value.
|
|
50
|
+
*/
|
|
51
|
+
function checkSandboxSeedName(
|
|
52
|
+
ctx: AIHubServerContext,
|
|
53
|
+
value: string | null,
|
|
54
|
+
): { ok: true; normalized: string | null } | { ok: false; response: Response } {
|
|
55
|
+
if (value === null || value === "") return { ok: true, normalized: null };
|
|
56
|
+
if (validateSeedName(value) !== "ok") {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
response: errorResponse(
|
|
60
|
+
400,
|
|
61
|
+
"invalid_seed_name",
|
|
62
|
+
"sandbox_seed_name may only contain letters, digits, underscores and dashes.",
|
|
63
|
+
),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (!seedDirExists(ctx.filesDir, value)) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
response: errorResponse(
|
|
70
|
+
400,
|
|
71
|
+
"seed_dir_not_found",
|
|
72
|
+
"No seed folder with that name exists on disk.",
|
|
73
|
+
),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, normalized: value };
|
|
77
|
+
}
|
|
78
|
+
|
|
45
79
|
/* ────────────────────────────────────────────────────────────────────────── */
|
|
46
80
|
/* Helpers */
|
|
47
81
|
/* ────────────────────────────────────────────────────────────────────────── */
|
|
@@ -63,6 +97,7 @@ function rotatingMessageView(row: RotatingMessageRow): Record<string, unknown> {
|
|
|
63
97
|
last_rotated_at: row.lastRotatedAt,
|
|
64
98
|
next_rotation_at: row.nextRotationAt,
|
|
65
99
|
is_archived: row.isArchived,
|
|
100
|
+
sandbox_seed_name: row.sandboxSeedName,
|
|
66
101
|
reasoning_visible: row.reasoningVisible,
|
|
67
102
|
tool_calls_visible: row.toolCallsVisible,
|
|
68
103
|
created_at: row.createdAt,
|
|
@@ -132,6 +167,7 @@ const createSchema = z
|
|
|
132
167
|
interval_seconds: z.number().int().nullable().optional(),
|
|
133
168
|
daily_time: z.string().max(5).nullable().optional(),
|
|
134
169
|
cron_expression: z.string().max(255).nullable().optional(),
|
|
170
|
+
sandbox_seed_name: z.string().max(128).nullable().optional(),
|
|
135
171
|
reasoning_visible: z.boolean().optional(),
|
|
136
172
|
tool_calls_visible: z.boolean().optional(),
|
|
137
173
|
})
|
|
@@ -173,6 +209,9 @@ export const handleCreateRotatingMessage: RouteHandler = protectedRoute(
|
|
|
173
209
|
return errorResponse(400, "invalid_schedule", scheduleCheck.message);
|
|
174
210
|
}
|
|
175
211
|
|
|
212
|
+
const seedCheck = checkSandboxSeedName(ctx, body.sandbox_seed_name ?? null);
|
|
213
|
+
if (!seedCheck.ok) return seedCheck.response;
|
|
214
|
+
|
|
176
215
|
const id = `rmsg-${randomUUID()}`;
|
|
177
216
|
const ts = nowIso();
|
|
178
217
|
ctx.db
|
|
@@ -193,6 +232,7 @@ export const handleCreateRotatingMessage: RouteHandler = protectedRoute(
|
|
|
193
232
|
// Due immediately — the scheduler picks it up on the next tick.
|
|
194
233
|
nextRotationAt: ts,
|
|
195
234
|
isArchived: false,
|
|
235
|
+
sandboxSeedName: seedCheck.normalized,
|
|
196
236
|
reasoningVisible: body.reasoning_visible ?? true,
|
|
197
237
|
toolCallsVisible: body.tool_calls_visible ?? true,
|
|
198
238
|
createdAt: ts,
|
|
@@ -230,6 +270,7 @@ const patchSchema = z
|
|
|
230
270
|
daily_time: z.string().max(5).nullable().optional(),
|
|
231
271
|
cron_expression: z.string().max(255).nullable().optional(),
|
|
232
272
|
is_archived: z.boolean().optional(),
|
|
273
|
+
sandbox_seed_name: z.string().max(128).nullable().optional(),
|
|
233
274
|
reasoning_visible: z.boolean().optional(),
|
|
234
275
|
tool_calls_visible: z.boolean().optional(),
|
|
235
276
|
})
|
|
@@ -274,6 +315,11 @@ export const handlePatchRotatingMessage: RouteHandler = protectedRoute(
|
|
|
274
315
|
if (body.prompt !== undefined) update.prompt = body.prompt;
|
|
275
316
|
if (body.letta_agent_id !== undefined) update.lettaAgentId = body.letta_agent_id;
|
|
276
317
|
if (body.is_archived !== undefined) update.isArchived = body.is_archived;
|
|
318
|
+
if (body.sandbox_seed_name !== undefined) {
|
|
319
|
+
const seedCheck = checkSandboxSeedName(ctx, body.sandbox_seed_name);
|
|
320
|
+
if (!seedCheck.ok) return seedCheck.response;
|
|
321
|
+
update.sandboxSeedName = seedCheck.normalized;
|
|
322
|
+
}
|
|
277
323
|
if (body.reasoning_visible !== undefined)
|
|
278
324
|
update.reasoningVisible = body.reasoning_visible;
|
|
279
325
|
if (body.tool_calls_visible !== undefined)
|
|
@@ -340,7 +386,7 @@ export const handleRotateRotatingMessageNow: RouteHandler = protectedRoute(
|
|
|
340
386
|
if (!row) return notFound("Rotating message not found.");
|
|
341
387
|
|
|
342
388
|
const outcome = await runRotation(
|
|
343
|
-
{ db: ctx.db, lettaClient: ctx.lettaClient, logger: ctx.logger },
|
|
389
|
+
{ db: ctx.db, lettaClient: ctx.lettaClient, logger: ctx.logger, filesDir: ctx.filesDir },
|
|
344
390
|
row,
|
|
345
391
|
);
|
|
346
392
|
const fresh = ctx.db.select().from(rotatingMessage).where(eq(rotatingMessage.id, id)).get();
|