@letta-ai/letta-code 0.27.18 → 0.27.19

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.
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env bun
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ type JsonRecord = Record<string, unknown>;
5
+
6
+ function isRecord(value: unknown): value is JsonRecord {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+
10
+ function isStringArray(value: unknown): value is string[] {
11
+ return (
12
+ Array.isArray(value) && value.every((item) => typeof item === "string")
13
+ );
14
+ }
15
+
16
+ function assertValid(condition: unknown, message: string): asserts condition {
17
+ if (!condition) throw new Error(message);
18
+ }
19
+
20
+ function optionalString(record: JsonRecord, key: string, path = key): void {
21
+ const value = record[key];
22
+ assertValid(
23
+ value === undefined ||
24
+ (typeof value === "string" && value.trim().length > 0),
25
+ `${path} must be a non-empty string when present`,
26
+ );
27
+ }
28
+
29
+ function optionalStringArray(
30
+ record: JsonRecord,
31
+ key: string,
32
+ path = key,
33
+ ): void {
34
+ const value = record[key];
35
+ assertValid(
36
+ value === undefined || isStringArray(value),
37
+ `${path} must be an array of strings`,
38
+ );
39
+ }
40
+
41
+ function optionalPositiveNumber(
42
+ record: JsonRecord,
43
+ key: string,
44
+ path = key,
45
+ ): void {
46
+ const value = record[key];
47
+ assertValid(
48
+ value === undefined ||
49
+ (typeof value === "number" && Number.isFinite(value) && value > 0),
50
+ `${path} must be a positive number`,
51
+ );
52
+ }
53
+
54
+ function validateMemoryFiles(value: unknown, path: string): void {
55
+ if (value === undefined) return;
56
+ assertValid(isRecord(value), `${path} must be an object`);
57
+ for (const [filePath, content] of Object.entries(value)) {
58
+ assertValid(
59
+ filePath.length > 0 && !filePath.startsWith("/"),
60
+ `${path} paths must be relative`,
61
+ );
62
+ assertValid(
63
+ typeof content === "string",
64
+ `${path}[${filePath}] must be a string`,
65
+ );
66
+ }
67
+ }
68
+
69
+ function validateMarkerFields(record: JsonRecord, path: string): void {
70
+ optionalStringArray(
71
+ record,
72
+ "requiredResultMarkers",
73
+ `${path}.requiredResultMarkers`,
74
+ );
75
+ optionalStringArray(
76
+ record,
77
+ "requiredTraceMarkers",
78
+ `${path}.requiredTraceMarkers`,
79
+ );
80
+ optionalStringArray(
81
+ record,
82
+ "forbiddenResultMarkers",
83
+ `${path}.forbiddenResultMarkers`,
84
+ );
85
+ optionalStringArray(
86
+ record,
87
+ "forbiddenTraceMarkers",
88
+ `${path}.forbiddenTraceMarkers`,
89
+ );
90
+ }
91
+
92
+ function requiredMarkerCount(record: JsonRecord): number {
93
+ return ["requiredResultMarkers", "requiredTraceMarkers"].reduce(
94
+ (count, key) => {
95
+ const value = record[key];
96
+ return count + (Array.isArray(value) ? value.length : 0);
97
+ },
98
+ 0,
99
+ );
100
+ }
101
+
102
+ function assertionCount(record: JsonRecord): number {
103
+ const value = record.assertions;
104
+ return Array.isArray(value) ? value.length : 0;
105
+ }
106
+
107
+ function validateScenario(params: {
108
+ evaluation: JsonRecord;
109
+ index: number;
110
+ scenario: unknown;
111
+ warnings: string[];
112
+ }): void {
113
+ const path = `evaluation.scenarios[${params.index}]`;
114
+ assertValid(isRecord(params.scenario), `${path} must be an object`);
115
+ const scenario = params.scenario;
116
+
117
+ optionalString(scenario, "name", `${path}.name`);
118
+ optionalString(scenario, "prompt", `${path}.prompt`);
119
+ assertValid(
120
+ typeof scenario.prompt === "string" ||
121
+ typeof params.evaluation.prompt === "string" ||
122
+ assertionCount(scenario) > 0 ||
123
+ assertionCount(params.evaluation) > 0,
124
+ `${path}.prompt or ${path}.assertions is required when evaluation.prompt is absent`,
125
+ );
126
+ assertValid(
127
+ scenario.outputFormat === undefined ||
128
+ scenario.outputFormat === "json" ||
129
+ scenario.outputFormat === "stream-json",
130
+ `${path}.outputFormat must be json or stream-json`,
131
+ );
132
+ optionalPositiveNumber(scenario, "timeoutMs", `${path}.timeoutMs`);
133
+ optionalPositiveNumber(scenario, "maxTurns", `${path}.maxTurns`);
134
+ validateMemoryFiles(scenario.memoryFiles, `${path}.memoryFiles`);
135
+ validateMarkerFields(scenario, path);
136
+
137
+ if (
138
+ requiredMarkerCount(scenario) === 0 &&
139
+ requiredMarkerCount(params.evaluation) === 0
140
+ ) {
141
+ params.warnings.push(
142
+ `${path} has no required result/trace markers and no parent required markers`,
143
+ );
144
+ }
145
+ }
146
+
147
+ function validateEnv(env: unknown): string[] {
148
+ assertValid(isRecord(env), "env must be a JSON object");
149
+ assertValid(
150
+ typeof env.name === "string" && env.name.trim(),
151
+ "name is required",
152
+ );
153
+ assertValid(
154
+ typeof env.objective === "string" && env.objective.trim(),
155
+ "objective is required",
156
+ );
157
+ assertValid(
158
+ isStringArray(env.requirements) && env.requirements.length > 0,
159
+ "requirements must be a non-empty array of strings",
160
+ );
161
+
162
+ optionalString(env, "slug");
163
+ optionalString(env, "targetModName");
164
+ optionalStringArray(env, "candidateDiversityHints");
165
+ optionalStringArray(env, "modApiHints");
166
+
167
+ if (env.examples !== undefined) {
168
+ assertValid(Array.isArray(env.examples), "examples must be an array");
169
+ for (const [index, example] of env.examples.entries()) {
170
+ assertValid(isRecord(example), `examples[${index}] must be an object`);
171
+ assertValid(
172
+ typeof example.input === "string" && example.input.trim(),
173
+ `examples[${index}].input is required`,
174
+ );
175
+ assertValid(
176
+ example.expected === undefined || typeof example.expected === "string",
177
+ `examples[${index}].expected must be a string`,
178
+ );
179
+ assertValid(
180
+ example.notes === undefined || typeof example.notes === "string",
181
+ `examples[${index}].notes must be a string`,
182
+ );
183
+ }
184
+ }
185
+
186
+ assertValid(isRecord(env.evaluation), "evaluation is required");
187
+ const evaluation = env.evaluation;
188
+ optionalString(evaluation, "prompt", "evaluation.prompt");
189
+ assertValid(
190
+ evaluation.outputFormat === undefined ||
191
+ evaluation.outputFormat === "json" ||
192
+ evaluation.outputFormat === "stream-json",
193
+ "evaluation.outputFormat must be json or stream-json",
194
+ );
195
+ optionalPositiveNumber(evaluation, "timeoutMs", "evaluation.timeoutMs");
196
+ optionalPositiveNumber(evaluation, "maxTurns", "evaluation.maxTurns");
197
+ validateMemoryFiles(evaluation.memoryFiles, "evaluation.memoryFiles");
198
+ validateMarkerFields(evaluation, "evaluation");
199
+
200
+ const warnings: string[] = [];
201
+ if (evaluation.scenarios !== undefined) {
202
+ assertValid(
203
+ Array.isArray(evaluation.scenarios),
204
+ "evaluation.scenarios must be an array",
205
+ );
206
+ assertValid(
207
+ evaluation.scenarios.length > 0,
208
+ "evaluation.scenarios must not be empty",
209
+ );
210
+ for (const [index, scenario] of evaluation.scenarios.entries()) {
211
+ validateScenario({ evaluation, index, scenario, warnings });
212
+ }
213
+ } else {
214
+ assertValid(
215
+ typeof evaluation.prompt === "string" && evaluation.prompt.trim(),
216
+ "evaluation.prompt is required when no scenarios are configured",
217
+ );
218
+ if (requiredMarkerCount(evaluation) === 0) {
219
+ warnings.push(
220
+ "add at least one requiredResultMarkers or requiredTraceMarkers entry so the eval can fail a placebo mod",
221
+ );
222
+ }
223
+ }
224
+
225
+ if (
226
+ typeof env.slug === "string" &&
227
+ !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(env.slug)
228
+ ) {
229
+ warnings.push(
230
+ "slug should be kebab-case for stable run and candidate paths",
231
+ );
232
+ }
233
+ if (
234
+ evaluation.scenarios !== undefined &&
235
+ Array.isArray(evaluation.scenarios) &&
236
+ evaluation.scenarios.length < 2
237
+ ) {
238
+ warnings.push(
239
+ "prefer at least a happy path and a negative-control scenario",
240
+ );
241
+ }
242
+ return warnings;
243
+ }
244
+
245
+ async function main(): Promise<void> {
246
+ const envPath = process.argv[2];
247
+ if (!envPath || envPath === "--help" || envPath === "-h") {
248
+ console.error("Usage: bun validate-mod-env.ts path/to/env.json");
249
+ process.exit(envPath ? 0 : 1);
250
+ }
251
+
252
+ const env = JSON.parse(await readFile(envPath, "utf8"));
253
+ const warnings = validateEnv(env);
254
+ console.log(`OK ${envPath}`);
255
+ for (const warning of warnings) console.warn(`warning: ${warning}`);
256
+ }
257
+
258
+ main().catch((error) => {
259
+ console.error(error instanceof Error ? error.message : String(error));
260
+ process.exit(1);
261
+ });