@zenera/gym 1.1.39 → 1.1.41
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/README.md +169 -104
- package/dist/agents.d.ts +25 -0
- package/dist/agents.js +52 -0
- package/dist/bindings.d.ts +25 -0
- package/dist/bindings.js +81 -0
- package/dist/box.d.ts +39 -36
- package/dist/box.js +183 -217
- package/dist/cache.d.ts +24 -14
- package/dist/cache.js +102 -66
- package/dist/catalog.d.ts +83 -0
- package/dist/catalog.js +293 -0
- package/dist/command.d.ts +1 -0
- package/dist/command.js +235 -68
- package/dist/fake.d.ts +4 -0
- package/dist/fake.js +142 -0
- package/dist/generate.d.ts +28 -24
- package/dist/generate.js +417 -120
- package/dist/inputs.d.ts +12 -0
- package/dist/inputs.js +132 -0
- package/dist/knowledge.d.ts +50 -0
- package/dist/knowledge.js +199 -0
- package/dist/logger.d.ts +10 -8
- package/dist/logger.js +4 -3
- package/dist/paging.d.ts +2 -2
- package/dist/paging.js +2 -2
- package/dist/probe.d.ts +5 -5
- package/dist/probe.js +15 -15
- package/dist/prompt.d.ts +31 -18
- package/dist/prompt.js +160 -146
- package/dist/quiet.d.ts +2 -0
- package/dist/quiet.js +7 -0
- package/dist/router.js +5 -1
- package/dist/runtime.d.ts +160 -0
- package/dist/runtime.js +229 -0
- package/dist/schema.js +1 -2
- package/dist/server.js +98 -57
- package/dist/setup.d.ts +47 -6
- package/dist/setup.js +157 -21
- package/dist/spec.d.ts +4 -3
- package/dist/spec.js +73 -22
- package/dist/store.d.ts +78 -0
- package/dist/store.js +253 -0
- package/dist/synth.d.ts +42 -0
- package/dist/synth.js +589 -0
- package/dist/worker.d.ts +41 -0
- package/dist/worker.js +99 -0
- package/package.json +5 -3
- package/dist/envelope.d.ts +0 -13
- package/dist/envelope.js +0 -9
- package/dist/image.d.ts +0 -25
- package/dist/image.js +0 -81
package/dist/command.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import "./quiet.js";
|
|
2
|
+
import { bold, cacheItems, clearCache, CliError, cyan, dim, EXIT, green, json, KeyStore, note, parse, paths, red, table, usageError, write, writeAll, yellow, } from '@zenera/cli/lib';
|
|
3
|
+
import { createEmbedder, createModel } from '@zenera/neo';
|
|
4
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
4
5
|
import { join, relative, resolve } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
+
import { BINDINGS_KIND } from "./bindings.js";
|
|
7
|
+
import { HANDLERS } from "./box.js";
|
|
6
8
|
import { GYM_KIND } from "./cache.js";
|
|
7
9
|
import { reason } from "./generate.js";
|
|
8
10
|
import { formatDumpPath } from "./logger.js";
|
|
9
11
|
import { listen } from "./server.js";
|
|
10
|
-
import { chooseModel, MODEL_ENV, open, readSettings, SOURCE_LABELS, writeSettings, } from "./setup.js";
|
|
12
|
+
import { configuredEmbedding, chooseModel, EMBEDDING_ENV, MODEL_ENV, open, readSettings, SOURCE_LABELS, writeSettings, } from "./setup.js";
|
|
11
13
|
import { called } from "./spec.js";
|
|
14
|
+
import { listWorlds, removeWorld, worldPath, worldsDir } from "./store.js";
|
|
12
15
|
// ---------------------------------------------------------------------------
|
|
13
16
|
// zen gym — a mock API from a specification
|
|
14
17
|
//
|
|
@@ -20,14 +23,19 @@ import { called } from "./spec.js";
|
|
|
20
23
|
// resolve until a signal arrives, and the process lives as long as the answer
|
|
21
24
|
// takes, which is what every other command already means.
|
|
22
25
|
// ---------------------------------------------------------------------------
|
|
23
|
-
const USAGE = 'zen gym <serve|build|cache|model> [spec...]';
|
|
26
|
+
const USAGE = 'zen gym <serve|build|cache|world|model> [spec...]';
|
|
27
|
+
const SERVE_USAGE = 'zen gym serve [spec...] [--schema <path>]... [--story <path>]... [--docs <path>]... [--world <name|file>]';
|
|
24
28
|
// `--json` is the frame's, lifted out of the arguments before they arrive; it
|
|
25
29
|
// reaches us as `ctx.json` and must not be declared again or `strict` rejects it.
|
|
26
30
|
const OPTIONS = {
|
|
27
31
|
port: { type: 'string' },
|
|
28
32
|
host: { type: 'string' },
|
|
29
33
|
model: { type: 'string' },
|
|
30
|
-
|
|
34
|
+
schema: { type: 'string', multiple: true },
|
|
35
|
+
story: { type: 'string', multiple: true },
|
|
36
|
+
docs: { type: 'string', multiple: true },
|
|
37
|
+
world: { type: 'string' },
|
|
38
|
+
embedding: { type: 'string' },
|
|
31
39
|
cache: { type: 'string' },
|
|
32
40
|
attempts: { type: 'string' },
|
|
33
41
|
concurrency: { type: 'string' },
|
|
@@ -37,6 +45,7 @@ const OPTIONS = {
|
|
|
37
45
|
'regen-limit': { type: 'string' },
|
|
38
46
|
'requests-dir': { type: 'string' },
|
|
39
47
|
rebuild: { type: 'boolean' },
|
|
48
|
+
reset: { type: 'boolean' },
|
|
40
49
|
'no-cache': { type: 'boolean' },
|
|
41
50
|
clear: { type: 'boolean' },
|
|
42
51
|
quiet: { type: 'boolean' },
|
|
@@ -47,34 +56,59 @@ export const command = {
|
|
|
47
56
|
details: [
|
|
48
57
|
'Commands',
|
|
49
58
|
...table([
|
|
50
|
-
[' serve
|
|
51
|
-
[' build
|
|
52
|
-
[' cache ls|clear', dim('
|
|
53
|
-
['
|
|
59
|
+
[' serve [spec...]', dim('Serve the APIs. Handlers are written on first use.')],
|
|
60
|
+
[' build [spec...]', dim('Index everything and write every handler, then exit.')],
|
|
61
|
+
[' cache ls|clear', dim('Handlers, bindings and indexes: list them, or clear them.')],
|
|
62
|
+
[' world ls|rm <name>', dim('Worlds are data: list them, or delete one.')],
|
|
63
|
+
[' model [ref]', dim('Show or set the models it uses. Remembered between runs.')],
|
|
54
64
|
]),
|
|
55
65
|
'',
|
|
56
66
|
'Options',
|
|
57
67
|
...table([
|
|
58
68
|
[' --port <n>', dim('Default 8787.')],
|
|
59
69
|
[' --host <h>', dim('Default 127.0.0.1. Anything else is reachable off-machine.')],
|
|
60
|
-
[
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
[
|
|
71
|
+
' --schema <path>',
|
|
72
|
+
dim('API documents: files, directories, quoted globs. Repeatable.'),
|
|
73
|
+
],
|
|
74
|
+
[' --story <path>', dim('The state of the world, quoted to every agent. Repeatable.')],
|
|
75
|
+
[' --docs <path>', dim('Reference material, searched by agents. Repeatable.')],
|
|
76
|
+
[
|
|
77
|
+
' --world <name|file>',
|
|
78
|
+
dim('Which world. Default: one per story, shared by processes.'),
|
|
79
|
+
],
|
|
80
|
+
[' --reset', dim('Start from an empty world.')],
|
|
81
|
+
[' --model <ref>', dim('Which model writes handlers and content, for this run only.')],
|
|
82
|
+
[
|
|
83
|
+
' --embedding <on|ref>',
|
|
84
|
+
dim('Search --docs by meaning too: `on` for the configured model, or a ref.'),
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
' --cache <dir>',
|
|
88
|
+
dim('Handlers, worlds, indexes and reports. Default ~/.zenera/neo/gym.'),
|
|
89
|
+
],
|
|
63
90
|
[' --seed <n>', dim('Answer the same request the same way every time.')],
|
|
64
|
-
[' --attempts <n>', dim('
|
|
65
|
-
[' --concurrency <n>', dim('
|
|
66
|
-
[' --timeout <s>', dim('Seconds one
|
|
91
|
+
[' --attempts <n>', dim('Failed tests per handler before giving up. Default 5.')],
|
|
92
|
+
[' --concurrency <n>', dim('Handlers written at once. Default 4.')],
|
|
93
|
+
[' --timeout <s>', dim('Seconds one handler may run, agent time aside. Default 30.')],
|
|
67
94
|
[' --max-body <n>', dim('Largest request body accepted, in bytes.')],
|
|
68
95
|
[' --regen-limit <n>', dim('Maximum regenerations per URL on error. Default 3.')],
|
|
69
96
|
[' --requests-dir <dir>', dim('Where to save request/response .md files.')],
|
|
70
97
|
[' --rebuild', dim('Ignore what is cached and write it again.')],
|
|
71
98
|
[' --no-cache', dim('Do not record what is written.')],
|
|
72
|
-
[
|
|
99
|
+
[
|
|
100
|
+
' --clear',
|
|
101
|
+
dim('With `model`: forget the stored model. `--embedding clear` forgets that one.'),
|
|
102
|
+
],
|
|
73
103
|
[' --quiet', dim('No narration.')],
|
|
74
104
|
]),
|
|
75
105
|
'',
|
|
76
106
|
dim(`The model is looked for in this order: ${cyan('--model')}, ${cyan(MODEL_ENV)}, ` +
|
|
77
107
|
`${cyan('zen gym model')}, then the best one your keys can buy.`),
|
|
108
|
+
dim(`Docs are searched by wording unless ${cyan('--embedding')} is given. ` +
|
|
109
|
+
`${cyan('--embedding on')} uses ${cyan(EMBEDDING_ENV)}, then ` +
|
|
110
|
+
`${cyan('zen gym model --embedding')}, then the first model your keys offer.`),
|
|
111
|
+
dim(`Quote globs (${cyan("--docs 'kb/*.md'")}) so the shell leaves them to gym.`),
|
|
78
112
|
dim(`Credentials come from the ${cyan('zen')} keyring — try ${cyan('zen key ls')}.`),
|
|
79
113
|
],
|
|
80
114
|
async run(ctx) {
|
|
@@ -86,6 +120,8 @@ export const command = {
|
|
|
86
120
|
return await warm(rest, ctx);
|
|
87
121
|
case 'cache':
|
|
88
122
|
return await cache(rest, ctx);
|
|
123
|
+
case 'world':
|
|
124
|
+
return await worlds(rest, ctx);
|
|
89
125
|
case 'model':
|
|
90
126
|
return await model(rest, ctx);
|
|
91
127
|
default:
|
|
@@ -97,12 +133,8 @@ export const command = {
|
|
|
97
133
|
// serve
|
|
98
134
|
// ---------------------------------------------------------------------------
|
|
99
135
|
async function serve(args, ctx) {
|
|
100
|
-
const { values, positionals } = parse(args, OPTIONS,
|
|
101
|
-
const loud = !values.quiet && !ctx.json;
|
|
136
|
+
const { values, positionals } = parse(args, OPTIONS, SERVE_USAGE);
|
|
102
137
|
const setup = await start(values, positionals, ctx, 'serve');
|
|
103
|
-
if (loud) {
|
|
104
|
-
printSpecs(setup.router.operations, ctx.cwd);
|
|
105
|
-
}
|
|
106
138
|
const host = values.host ?? '127.0.0.1';
|
|
107
139
|
const requestsDir = values['requests-dir']
|
|
108
140
|
? resolve(ctx.cwd, values['requests-dir'])
|
|
@@ -124,9 +156,9 @@ async function serve(args, ctx) {
|
|
|
124
156
|
if (host !== '127.0.0.1' && host !== 'localhost') {
|
|
125
157
|
note(yellow(`bound to ${host} — this mock is reachable from the network`));
|
|
126
158
|
}
|
|
127
|
-
note(dim(`
|
|
159
|
+
note(dim(`handlers: ${setup.root}/${HANDLERS}`));
|
|
128
160
|
note(dim(`request logs: ${requestsDir ?? `${setup.root}/requests`}`));
|
|
129
|
-
note(dim(`failed builds: ${setup.root}/builds`));
|
|
161
|
+
note(dim(`agent runs: ${setup.root}/reports failed builds: ${setup.root}/builds`));
|
|
130
162
|
}
|
|
131
163
|
// The address is the answer; the rest was narration.
|
|
132
164
|
write(`http://${host}:${listener.port}`);
|
|
@@ -152,26 +184,19 @@ function until(signals) {
|
|
|
152
184
|
});
|
|
153
185
|
}
|
|
154
186
|
// ---------------------------------------------------------------------------
|
|
155
|
-
// build — every
|
|
187
|
+
// build — every handler, up front
|
|
156
188
|
// ---------------------------------------------------------------------------
|
|
157
189
|
async function warm(args, ctx) {
|
|
158
|
-
const { values, positionals } = parse(args, OPTIONS, '
|
|
190
|
+
const { values, positionals } = parse(args, OPTIONS, SERVE_USAGE.replace('serve', 'build'));
|
|
159
191
|
const loud = !values.quiet && !ctx.json;
|
|
160
192
|
const setup = await start(values, positionals, ctx, 'build');
|
|
161
|
-
if (loud) {
|
|
162
|
-
printSpecs(setup.router.operations, ctx.cwd);
|
|
163
|
-
}
|
|
164
193
|
const results = [];
|
|
165
194
|
try {
|
|
166
195
|
for (const operation of setup.router.operations) {
|
|
167
196
|
const id = `${operation.method.toUpperCase()} ${called(operation)}`;
|
|
168
|
-
if (!operation.success.schema) {
|
|
169
|
-
results.push({ operation: id, status: 'skipped', detail: 'no response body' });
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
197
|
try {
|
|
173
|
-
const
|
|
174
|
-
results.push({ operation: id, status:
|
|
198
|
+
const handler = await setup.cache.ensure(operation);
|
|
199
|
+
results.push({ operation: id, status: handler.cached ? 'cached' : 'built' });
|
|
175
200
|
}
|
|
176
201
|
catch (err) {
|
|
177
202
|
results.push({
|
|
@@ -198,7 +223,7 @@ async function warm(args, ctx) {
|
|
|
198
223
|
// Reported first, then failed: the table is the answer either way.
|
|
199
224
|
const failed = results.filter((r) => r.status === 'failed').length;
|
|
200
225
|
if (failed > 0) {
|
|
201
|
-
throw new CliError(`${failed} of ${results.length}
|
|
226
|
+
throw new CliError(`${failed} of ${results.length} handlers failed`, EXIT.failed, 'run again to retry, or raise --attempts');
|
|
202
227
|
}
|
|
203
228
|
}
|
|
204
229
|
const mark = (status) => status === 'failed' ? red('failed') : status === 'skipped' ? dim('skipped') : green(status);
|
|
@@ -210,7 +235,7 @@ async function cache(args, ctx) {
|
|
|
210
235
|
const root = values.cache ? resolve(ctx.cwd, values.cache) : paths.gym();
|
|
211
236
|
const sub = positionals[0] ?? 'ls';
|
|
212
237
|
if (sub === 'ls') {
|
|
213
|
-
const entries =
|
|
238
|
+
const entries = listHandlers();
|
|
214
239
|
if (ctx.json) {
|
|
215
240
|
json(entries);
|
|
216
241
|
return;
|
|
@@ -232,17 +257,14 @@ async function cache(args, ctx) {
|
|
|
232
257
|
}
|
|
233
258
|
if (sub === 'clear') {
|
|
234
259
|
clearCache({ kind: GYM_KIND });
|
|
260
|
+
clearCache({ kind: BINDINGS_KIND });
|
|
235
261
|
// The workspace is scratch — whatever a hit was copied into it is
|
|
236
|
-
// written again from the cache, or by the model
|
|
237
|
-
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (mine.length > 0) {
|
|
243
|
-
await removeContainers(mine.map((c) => c.name));
|
|
244
|
-
}
|
|
245
|
-
note(`${green('cleared')} ${dim(paths.cache())}`);
|
|
262
|
+
// written again from the cache, or by the model — and an index is
|
|
263
|
+
// rebuilt from the documents. Worlds are not touched: they are data,
|
|
264
|
+
// and `zen gym world rm` or `serve --reset` is how one goes.
|
|
265
|
+
rmSync(join(root, HANDLERS), { recursive: true, force: true });
|
|
266
|
+
rmSync(join(root, 'indexes'), { recursive: true, force: true });
|
|
267
|
+
note(`${green('cleared')} ${dim(`handlers, bindings and indexes; worlds kept in ${worldsDir(root)}`)}`);
|
|
246
268
|
return;
|
|
247
269
|
}
|
|
248
270
|
throw usageError(`unknown cache command "${sub}"`, 'zen gym cache <ls|clear>');
|
|
@@ -252,7 +274,7 @@ async function cache(args, ctx) {
|
|
|
252
274
|
* listed at all. Anything the store cannot parse is left out rather than shown
|
|
253
275
|
* as a row with nothing in it.
|
|
254
276
|
*/
|
|
255
|
-
function
|
|
277
|
+
function listHandlers() {
|
|
256
278
|
const { rows } = cacheItems(GYM_KIND);
|
|
257
279
|
return rows
|
|
258
280
|
.map((row) => ({ key: row.key, ...row.value?.meta }))
|
|
@@ -266,7 +288,7 @@ function listGenerators() {
|
|
|
266
288
|
// this machine. Shown rather than merely set, since the question when a run
|
|
267
289
|
// surprises you is not "which model" but "why that one".
|
|
268
290
|
// ---------------------------------------------------------------------------
|
|
269
|
-
const MODEL_USAGE = 'zen gym model [ref|--clear]';
|
|
291
|
+
const MODEL_USAGE = 'zen gym model [ref|--clear] [--embedding <ref|none> [--clear]]';
|
|
270
292
|
async function model(args, ctx) {
|
|
271
293
|
const { values, positionals } = parse(args, OPTIONS, MODEL_USAGE);
|
|
272
294
|
const ref = positionals[0];
|
|
@@ -277,27 +299,60 @@ async function model(args, ctx) {
|
|
|
277
299
|
// environment, and the keyring is what puts it there.
|
|
278
300
|
const keys = await KeyStore.open();
|
|
279
301
|
keys.materialize();
|
|
280
|
-
|
|
302
|
+
// `--embedding <ref>` sets that one; `--embedding --clear` is not
|
|
303
|
+
// expressible with a string flag, so `--clear` with no ref and no
|
|
304
|
+
// `--embedding` clears the chat model and `--embedding none` stores none.
|
|
305
|
+
const embedding = values.embedding?.trim();
|
|
306
|
+
if (ref || values.clear || embedding) {
|
|
281
307
|
const file = await readSettings();
|
|
282
308
|
if (ref) {
|
|
283
309
|
// Only that the ref parses: asking the provider costs a call, and
|
|
284
310
|
// the next run makes one anyway.
|
|
285
311
|
createModel(ref);
|
|
286
|
-
|
|
312
|
+
file.model = ref;
|
|
287
313
|
}
|
|
288
|
-
else {
|
|
314
|
+
else if (values.clear && !embedding) {
|
|
289
315
|
delete file.model;
|
|
290
|
-
writeSettings(file);
|
|
291
316
|
}
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
317
|
+
if (embedding === 'clear') {
|
|
318
|
+
delete file.embedding;
|
|
319
|
+
}
|
|
320
|
+
else if (embedding) {
|
|
321
|
+
if (embedding === 'on' || embedding === 'default') {
|
|
322
|
+
throw usageError(`${embedding} is what \`serve --embedding\` asks for; store a model ref, or none`, MODEL_USAGE);
|
|
323
|
+
}
|
|
324
|
+
if (embedding !== 'none') {
|
|
325
|
+
createEmbedder(embedding);
|
|
326
|
+
}
|
|
327
|
+
file.embedding = embedding;
|
|
328
|
+
}
|
|
329
|
+
writeSettings(file);
|
|
330
|
+
if (!ctx.json) {
|
|
331
|
+
if (ref) {
|
|
332
|
+
note(`${green('set')} ${ref}`);
|
|
333
|
+
}
|
|
334
|
+
else if (values.clear && !embedding) {
|
|
335
|
+
note(`${green('cleared')} ${dim('the stored model')}`);
|
|
336
|
+
}
|
|
337
|
+
if (embedding) {
|
|
338
|
+
note(embedding === 'clear'
|
|
339
|
+
? `${green('cleared')} ${dim('the stored embedding model')}`
|
|
340
|
+
: `${green('set')} ${dim('embedding')} ${embedding}`);
|
|
341
|
+
}
|
|
295
342
|
}
|
|
296
|
-
note(ref ? `${green('set')} ${ref}` : `${green('cleared')} ${dim('the stored model')}`);
|
|
297
343
|
}
|
|
298
|
-
const chosen = await chooseModel(values.model, keys);
|
|
344
|
+
const chosen = await chooseModel(ref ? undefined : values.model, keys);
|
|
345
|
+
const embedder = await configuredEmbedding(keys);
|
|
299
346
|
if (ctx.json) {
|
|
300
|
-
|
|
347
|
+
const stored = await readSettings();
|
|
348
|
+
json({
|
|
349
|
+
model: chosen?.ref,
|
|
350
|
+
from: chosen?.from,
|
|
351
|
+
stored: stored.model,
|
|
352
|
+
embedding: embedder.ref ?? 'none',
|
|
353
|
+
embeddingFrom: embedder.from,
|
|
354
|
+
storedEmbedding: stored.embedding,
|
|
355
|
+
});
|
|
301
356
|
return;
|
|
302
357
|
}
|
|
303
358
|
if (!chosen) {
|
|
@@ -305,11 +360,68 @@ async function model(args, ctx) {
|
|
|
305
360
|
note(dim('add a key: zen key add openai'));
|
|
306
361
|
return;
|
|
307
362
|
}
|
|
308
|
-
note(`${cyan(chosen.ref)} ${dim(`from ${SOURCE_LABELS[chosen.from]}`)}`);
|
|
363
|
+
note(`${dim('model ')} ${cyan(chosen.ref)} ${dim(`from ${SOURCE_LABELS[chosen.from]}`)}`);
|
|
364
|
+
note(`${dim('embedding')} ${embedder.ref ? cyan(embedder.ref) : yellow('none')} ` +
|
|
365
|
+
dim((embedder.from === 'none'
|
|
366
|
+
? ''
|
|
367
|
+
: `from ${embedder.from === 'env' ? EMBEDDING_ENV : SOURCE_LABELS[embedder.from]} · `) +
|
|
368
|
+
'used only with --embedding on; docs are searched by wording otherwise'));
|
|
309
369
|
if (chosen.from === 'default') {
|
|
310
370
|
note(dim(`set one: zen gym model <provider>:<id>`));
|
|
311
371
|
}
|
|
312
372
|
}
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// world
|
|
375
|
+
//
|
|
376
|
+
// Worlds are data rather than cache — they hold what clients wrote, and
|
|
377
|
+
// content an agent made that would come out differently next time — so they
|
|
378
|
+
// have a command of their own and `cache clear` leaves them alone.
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
const WORLD_USAGE = 'zen gym world <ls|rm <name|file>>';
|
|
381
|
+
async function worlds(args, ctx) {
|
|
382
|
+
const { values, positionals } = parse(args, OPTIONS, WORLD_USAGE);
|
|
383
|
+
const root = values.cache ? resolve(ctx.cwd, values.cache) : paths.gym();
|
|
384
|
+
const [sub = 'ls', target] = positionals;
|
|
385
|
+
if (sub === 'ls') {
|
|
386
|
+
const found = listWorlds(root);
|
|
387
|
+
if (ctx.json) {
|
|
388
|
+
json(found);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (found.length === 0) {
|
|
392
|
+
note(`no worlds yet in ${worldsDir(root)}`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
writeAll(table([
|
|
396
|
+
[bold('WORLD'), bold('RECORDS'), bold('COLLECTIONS'), bold('USED'), bold('SIZE')],
|
|
397
|
+
...found.map((w) => [
|
|
398
|
+
w.name,
|
|
399
|
+
String(w.collections.reduce((n, c) => n + c.count, 0)),
|
|
400
|
+
dim(w.collections
|
|
401
|
+
.map((c) => `${c.name} ${c.count}`)
|
|
402
|
+
.join(', ')
|
|
403
|
+
.slice(0, 60) || '—'),
|
|
404
|
+
dim(w.usedAt.toISOString().slice(0, 16).replace('T', ' ')),
|
|
405
|
+
dim(`${Math.ceil(w.bytes / 1024)} KB`),
|
|
406
|
+
]),
|
|
407
|
+
]));
|
|
408
|
+
note(dim(worldsDir(root)));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (sub === 'rm') {
|
|
412
|
+
if (!target) {
|
|
413
|
+
throw usageError('name the world to remove', WORLD_USAGE);
|
|
414
|
+
}
|
|
415
|
+
const path = worldPath({ root, cwd: ctx.cwd, world: target });
|
|
416
|
+
if (!existsSync(path)) {
|
|
417
|
+
throw usageError(`no world at ${path}`, 'see them with: zen gym world ls');
|
|
418
|
+
}
|
|
419
|
+
removeWorld(path);
|
|
420
|
+
note(`${green('removed')} ${dim(path)}`);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
throw usageError(`unknown world command "${sub}"`, WORLD_USAGE);
|
|
424
|
+
}
|
|
313
425
|
function summarize(operations) {
|
|
314
426
|
const by = new Map();
|
|
315
427
|
for (const op of operations) {
|
|
@@ -355,28 +467,82 @@ function printSpecs(operations, cwd) {
|
|
|
355
467
|
}
|
|
356
468
|
note('');
|
|
357
469
|
}
|
|
470
|
+
/** The rest of what was loaded, and what it will be served with. */
|
|
471
|
+
function printInputs(setup) {
|
|
472
|
+
const k = setup.knowledge;
|
|
473
|
+
const rows = [
|
|
474
|
+
['model', `${setup.choice.ref} ${dim(`from ${SOURCE_LABELS[setup.choice.from]}`)}`],
|
|
475
|
+
[
|
|
476
|
+
'story',
|
|
477
|
+
k.storyFiles > 0
|
|
478
|
+
? `${k.storyFiles} file${k.storyFiles === 1 ? '' : 's'}`
|
|
479
|
+
: dim('none — the agents invent a world'),
|
|
480
|
+
],
|
|
481
|
+
];
|
|
482
|
+
if (k.docs) {
|
|
483
|
+
rows.push([
|
|
484
|
+
'docs',
|
|
485
|
+
`${k.docs.files} files, ${k.docs.chunks} chunks · ` +
|
|
486
|
+
(k.docs.search === 'hybrid'
|
|
487
|
+
? `search by meaning and wording (${k.docs.embedding})`
|
|
488
|
+
: `search by wording ${dim('(--embedding <ref> adds meaning)')}`) +
|
|
489
|
+
dim(k.docs.reused ? ' · index reused' : ' · indexed'),
|
|
490
|
+
]);
|
|
491
|
+
}
|
|
492
|
+
rows.push(['world', setup.worldFile]);
|
|
493
|
+
for (const line of table(rows.map(([a, b]) => [` ${dim(a)}`, b]))) {
|
|
494
|
+
note(line);
|
|
495
|
+
}
|
|
496
|
+
note('');
|
|
497
|
+
}
|
|
498
|
+
/** Narrates indexing at the quarters, so a long first run is not silent. */
|
|
499
|
+
function indexing() {
|
|
500
|
+
let last = '';
|
|
501
|
+
return (phase, done, total) => {
|
|
502
|
+
const quarter = total === 0 ? 4 : Math.floor((done / total) * 4);
|
|
503
|
+
const mark = `${phase}:${quarter}`;
|
|
504
|
+
if (mark === last || total === 0) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
last = mark;
|
|
508
|
+
note(dim(` ${phase === 'reading' ? 'reading docs' : 'embedding docs'} ${done}/${total}`));
|
|
509
|
+
};
|
|
510
|
+
}
|
|
358
511
|
// ---------------------------------------------------------------------------
|
|
359
512
|
// Shared
|
|
360
513
|
// ---------------------------------------------------------------------------
|
|
361
|
-
async function start(values,
|
|
514
|
+
async function start(values, positionals, ctx, what) {
|
|
362
515
|
const loud = !values.quiet && !ctx.json;
|
|
516
|
+
const progress = loud ? indexing() : undefined;
|
|
363
517
|
const setup = await open({
|
|
364
|
-
|
|
518
|
+
schemas: [...positionals, ...(values.schema ?? [])],
|
|
519
|
+
story: values.story,
|
|
520
|
+
docs: values.docs,
|
|
365
521
|
cwd: ctx.cwd,
|
|
366
522
|
cache: values.cache,
|
|
367
523
|
model: values.model,
|
|
368
|
-
|
|
524
|
+
embedding: values.embedding,
|
|
525
|
+
world: values.world,
|
|
526
|
+
reset: values.reset,
|
|
369
527
|
attempts: number(values.attempts, 'attempts'),
|
|
370
528
|
concurrency: number(values.concurrency, 'concurrency'),
|
|
371
529
|
timeout: number(values.timeout, 'timeout'),
|
|
372
530
|
rebuild: values.rebuild,
|
|
373
531
|
ephemeral: values['no-cache'],
|
|
374
|
-
|
|
375
|
-
|
|
532
|
+
onWarn: loud ? (message) => note(yellow(message)) : undefined,
|
|
533
|
+
onIndexing: progress,
|
|
534
|
+
onWorld: loud
|
|
535
|
+
? (e) => note(` ${e.fallback ? yellow('sampled') : green('world')} ${dim(`${e.made} × ${e.collection} in ${e.durationMs}ms`)}` +
|
|
536
|
+
(e.dependents ? dim(` + ${e.dependents} they refer to`) : '') +
|
|
537
|
+
(e.fallback ? ` ${dim(e.fallback)}` : '') +
|
|
538
|
+
(e.dangling?.length
|
|
539
|
+
? `\n ${yellow('dangling')} ${dim(e.dangling.slice(0, 5).join(', '))}`
|
|
540
|
+
: '') +
|
|
541
|
+
(e.report ? `\n${formatDumpPath('agent run', e.report)}` : ''))
|
|
376
542
|
: undefined,
|
|
377
543
|
events: {
|
|
378
544
|
onStart: loud
|
|
379
|
-
? ({ operation }) => note(`${dim('writing a
|
|
545
|
+
? ({ operation }) => note(`${dim('writing a handler for')} ${operation.method.toUpperCase()} ${called(operation)}`)
|
|
380
546
|
: undefined,
|
|
381
547
|
onAttempt: loud
|
|
382
548
|
? ({ operation, attempt, diagnostics }) => note(` ${yellow(`attempt ${attempt} failed`)} ${dim(`${operation.operationId}: ${(diagnostics ?? []).join(' ').slice(0, 160)}`)}`)
|
|
@@ -397,7 +563,8 @@ async function start(values, specs, ctx, what) {
|
|
|
397
563
|
},
|
|
398
564
|
});
|
|
399
565
|
if (loud) {
|
|
400
|
-
|
|
566
|
+
printSpecs(setup.router.operations, ctx.cwd);
|
|
567
|
+
printInputs(setup);
|
|
401
568
|
}
|
|
402
569
|
return setup;
|
|
403
570
|
}
|
package/dist/fake.d.ts
ADDED
package/dist/fake.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import RandExp from 'randexp';
|
|
2
|
+
const WORDS = ['alpha', 'bravo', 'charlie', 'delta', 'echo', 'foxtrot', 'golf', 'hotel'];
|
|
3
|
+
export function sample(schema, types, seed = 1) {
|
|
4
|
+
let state = seed >>> 0 || 1;
|
|
5
|
+
const random = () => {
|
|
6
|
+
state = (Math.imul(state, 1103515245) + 12345) >>> 0;
|
|
7
|
+
return state / 4294967296;
|
|
8
|
+
};
|
|
9
|
+
const int = (min, max) => min + Math.floor(random() * (max - min + 1));
|
|
10
|
+
return value(schema, 0);
|
|
11
|
+
function value(node, depth) {
|
|
12
|
+
if (!isObject(node) || depth > 8) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const ref = typeof node.$ref === 'string' ? refName(node.$ref) : undefined;
|
|
16
|
+
if (ref !== undefined) {
|
|
17
|
+
const target = types[ref] ?? defs(node)?.[ref];
|
|
18
|
+
return target ? value(target, depth + 1) : null;
|
|
19
|
+
}
|
|
20
|
+
if ('const' in node) {
|
|
21
|
+
return node.const;
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(node.enum) && node.enum.length > 0) {
|
|
24
|
+
return node.enum[int(0, node.enum.length - 1)];
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(node.allOf)) {
|
|
27
|
+
const merged = {};
|
|
28
|
+
for (const part of node.allOf) {
|
|
29
|
+
const v = value(part, depth + 1);
|
|
30
|
+
if (isObject(v)) {
|
|
31
|
+
Object.assign(merged, v);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const own = node.properties ? value({ ...node, allOf: undefined }, depth + 1) : {};
|
|
35
|
+
return { ...merged, ...(isObject(own) ? own : {}) };
|
|
36
|
+
}
|
|
37
|
+
for (const key of ['oneOf', 'anyOf']) {
|
|
38
|
+
const branches = node[key];
|
|
39
|
+
if (Array.isArray(branches) && branches.length > 0) {
|
|
40
|
+
return value(branches[0], depth + 1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
switch (typeOf(node)) {
|
|
44
|
+
case 'null':
|
|
45
|
+
return null;
|
|
46
|
+
case 'boolean':
|
|
47
|
+
return random() < 0.5;
|
|
48
|
+
case 'integer':
|
|
49
|
+
return Math.round(bounded(node, int(1, 1000)));
|
|
50
|
+
case 'number':
|
|
51
|
+
return bounded(node, int(1, 100000) / 100);
|
|
52
|
+
case 'array': {
|
|
53
|
+
// Recursive types end where the schema lets them: empty lists, deep down.
|
|
54
|
+
const min = num(node.minItems) ?? (depth >= 3 ? 0 : 1);
|
|
55
|
+
const max = Math.max(min, Math.min(num(node.maxItems) ?? 3, min + 2));
|
|
56
|
+
return Array.from({ length: int(min, max) }, () => value(node.items, depth + 1));
|
|
57
|
+
}
|
|
58
|
+
case 'object': {
|
|
59
|
+
const out = {};
|
|
60
|
+
const props = isObject(node.properties) ? node.properties : {};
|
|
61
|
+
const required = new Set(Array.isArray(node.required) ? node.required : []);
|
|
62
|
+
for (const [name, sub] of Object.entries(props)) {
|
|
63
|
+
if (required.has(name) || (depth < 3 && random() < 0.6)) {
|
|
64
|
+
out[name] = value(sub, depth + 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (isObject(node.additionalProperties) && Object.keys(props).length === 0) {
|
|
68
|
+
out[WORDS[int(0, WORDS.length - 1)]] = value(node.additionalProperties, depth + 1);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
default:
|
|
73
|
+
return text(node);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function text(node) {
|
|
77
|
+
if (typeof node.pattern === 'string') {
|
|
78
|
+
try {
|
|
79
|
+
const gen = new RandExp(node.pattern);
|
|
80
|
+
gen.max = 6;
|
|
81
|
+
gen.randInt = int;
|
|
82
|
+
return gen.gen();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// an unreadable pattern falls through to plain text
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const at = new Date(Date.UTC(2026, 0, 1) + int(0, 300) * 86_400_000 + int(0, 86_399) * 1000);
|
|
89
|
+
switch (node.format) {
|
|
90
|
+
case 'date-time':
|
|
91
|
+
return at.toISOString().replace(/\.\d+Z$/, 'Z');
|
|
92
|
+
case 'date':
|
|
93
|
+
return at.toISOString().slice(0, 10);
|
|
94
|
+
case 'email':
|
|
95
|
+
return `${WORDS[int(0, WORDS.length - 1)]}@example.com`;
|
|
96
|
+
case 'uuid':
|
|
97
|
+
return '00000000-0000-4000-8000-'.concat(Array.from({ length: 12 }, () => int(0, 15).toString(16)).join(''));
|
|
98
|
+
case 'uri':
|
|
99
|
+
case 'url':
|
|
100
|
+
return `https://example.com/${WORDS[int(0, WORDS.length - 1)]}`;
|
|
101
|
+
case 'ipv4':
|
|
102
|
+
return `10.0.${int(0, 255)}.${int(1, 254)}`;
|
|
103
|
+
default:
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
const min = num(node.minLength) ?? 0;
|
|
107
|
+
const max = num(node.maxLength) ?? 64;
|
|
108
|
+
const word = `${WORDS[int(0, WORDS.length - 1)]} ${WORDS[int(0, WORDS.length - 1)]}`;
|
|
109
|
+
return word.padEnd(min, 'x').slice(0, Math.max(min, Math.min(max, word.length)));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
113
|
+
const num = (v) => (typeof v === 'number' ? v : undefined);
|
|
114
|
+
function refName(ref) {
|
|
115
|
+
const m = /^#\/(?:\$defs|definitions|components\/schemas)\/(.+)$/.exec(ref);
|
|
116
|
+
return m ? decodeURIComponent(m[1]).replace(/~1/g, '/').replace(/~0/g, '~') : undefined;
|
|
117
|
+
}
|
|
118
|
+
/** A dereferenced schema keeps its shared parts under its own `$defs`. */
|
|
119
|
+
const defs = (node) => isObject(node.$defs) ? node.$defs : undefined;
|
|
120
|
+
function typeOf(node) {
|
|
121
|
+
const t = node.type;
|
|
122
|
+
if (typeof t === 'string') {
|
|
123
|
+
return t;
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(t)) {
|
|
126
|
+
return t.find((x) => x !== 'null');
|
|
127
|
+
}
|
|
128
|
+
return node.properties ? 'object' : node.items ? 'array' : undefined;
|
|
129
|
+
}
|
|
130
|
+
function bounded(node, wanted) {
|
|
131
|
+
const min = num(node.minimum) ?? num(node.exclusiveMinimum);
|
|
132
|
+
const max = num(node.maximum) ?? num(node.exclusiveMaximum);
|
|
133
|
+
let v = wanted;
|
|
134
|
+
if (min !== undefined && v <= min) {
|
|
135
|
+
v = min + 1;
|
|
136
|
+
}
|
|
137
|
+
if (max !== undefined && v >= max) {
|
|
138
|
+
v = max - 1;
|
|
139
|
+
}
|
|
140
|
+
return v;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=fake.js.map
|