@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.
Files changed (51) hide show
  1. package/README.md +169 -104
  2. package/dist/agents.d.ts +25 -0
  3. package/dist/agents.js +52 -0
  4. package/dist/bindings.d.ts +25 -0
  5. package/dist/bindings.js +81 -0
  6. package/dist/box.d.ts +39 -36
  7. package/dist/box.js +183 -217
  8. package/dist/cache.d.ts +24 -14
  9. package/dist/cache.js +102 -66
  10. package/dist/catalog.d.ts +83 -0
  11. package/dist/catalog.js +293 -0
  12. package/dist/command.d.ts +1 -0
  13. package/dist/command.js +235 -68
  14. package/dist/fake.d.ts +4 -0
  15. package/dist/fake.js +142 -0
  16. package/dist/generate.d.ts +28 -24
  17. package/dist/generate.js +417 -120
  18. package/dist/inputs.d.ts +12 -0
  19. package/dist/inputs.js +132 -0
  20. package/dist/knowledge.d.ts +50 -0
  21. package/dist/knowledge.js +199 -0
  22. package/dist/logger.d.ts +10 -8
  23. package/dist/logger.js +4 -3
  24. package/dist/paging.d.ts +2 -2
  25. package/dist/paging.js +2 -2
  26. package/dist/probe.d.ts +5 -5
  27. package/dist/probe.js +15 -15
  28. package/dist/prompt.d.ts +31 -18
  29. package/dist/prompt.js +160 -146
  30. package/dist/quiet.d.ts +2 -0
  31. package/dist/quiet.js +7 -0
  32. package/dist/router.js +5 -1
  33. package/dist/runtime.d.ts +160 -0
  34. package/dist/runtime.js +229 -0
  35. package/dist/schema.js +1 -2
  36. package/dist/server.js +98 -57
  37. package/dist/setup.d.ts +47 -6
  38. package/dist/setup.js +157 -21
  39. package/dist/spec.d.ts +4 -3
  40. package/dist/spec.js +73 -22
  41. package/dist/store.d.ts +78 -0
  42. package/dist/store.js +253 -0
  43. package/dist/synth.d.ts +42 -0
  44. package/dist/synth.js +589 -0
  45. package/dist/worker.d.ts +41 -0
  46. package/dist/worker.js +99 -0
  47. package/package.json +5 -3
  48. package/dist/envelope.d.ts +0 -13
  49. package/dist/envelope.js +0 -9
  50. package/dist/image.d.ts +0 -25
  51. package/dist/image.js +0 -81
package/dist/command.js CHANGED
@@ -1,14 +1,17 @@
1
- import { bold, cacheItems, clearCache, CliError, cyan, dim, EXIT, green, json, KeyStore, note, ownedContainers, parse, paths, red, removeContainers, table, usageError, write, writeAll, yellow, } from '@zenera/cli/lib';
2
- import { createModel } from '@zenera/neo';
3
- import { rmSync } from 'node:fs';
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 { CONTAINER_PREFIX, GENERATORS } from "./box.js";
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
- image: { type: 'string' },
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 <spec...>', dim('Serve the documents. Generators are written on demand.')],
51
- [' build <spec...>', dim('Write every generator now and exit.')],
52
- [' cache ls|clear', dim('What has been generated, or throw it away.')],
53
- [' model [ref]', dim('Show or set the model it uses. Remembered between runs.')],
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
- [' --model <ref>', dim('Which model writes the generators, for this run only.')],
61
- [' --image <ref>', dim('Skip the baked image and use this one.')],
62
- [' --cache <dir>', dim("The container's workspace. Default ~/.zenera/neo/gym.")],
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('Tries per generator before giving up. Default 5.')],
65
- [' --concurrency <n>', dim('Generators written at once. Default 4.')],
66
- [' --timeout <s>', dim('Seconds one generator may take. Default 30.')],
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
- [' --clear', dim('With `model`: forget the stored one.')],
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, 'zen gym serve <spec...>');
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(`generators: ${setup.root}/${GENERATORS}`));
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 generator, up front
187
+ // build — every handler, up front
156
188
  // ---------------------------------------------------------------------------
157
189
  async function warm(args, ctx) {
158
- const { values, positionals } = parse(args, OPTIONS, 'zen gym build <spec...>');
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 generator = await setup.cache.ensure(operation);
174
- results.push({ operation: id, status: generator.cached ? 'cached' : 'built' });
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} generators failed`, EXIT.failed, 'run again to retry, or raise --attempts');
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 = listGenerators();
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
- rmSync(join(root, GENERATORS), { recursive: true, force: true });
238
- // The container is named after its configuration, so a stale one would
239
- // otherwise sit there stopped forever with nothing pointing at it.
240
- // `zn-<key>-<digest>` is the shape, and this one's key is `gym`.
241
- const mine = (await ownedContainers()).filter((c) => c.name.startsWith(CONTAINER_PREFIX));
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 listGenerators() {
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
- if (ref || values.clear) {
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
- writeSettings({ ...file, model: ref });
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 (ctx.json) {
293
- json({ model: ref });
294
- return;
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
- json({ model: chosen?.ref, from: chosen?.from, stored: (await readSettings()).model });
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, specs, ctx, what) {
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
- specs,
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
- image: values.image,
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
- onImageBuild: loud
375
- ? (tag) => note(`${dim('building')} ${tag} ${dim('— once, then cached')}`)
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 generator for')} ${operation.method.toUpperCase()} ${called(operation)}`)
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
- note(dim(`model ${setup.choice.ref} from ${SOURCE_LABELS[setup.choice.from]}`));
566
+ printSpecs(setup.router.operations, ctx.cwd);
567
+ printInputs(setup);
401
568
  }
402
569
  return setup;
403
570
  }
package/dist/fake.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ type Schema = Record<string, unknown>;
2
+ export declare function sample(schema: unknown, types: Readonly<Record<string, Schema>>, seed?: number): unknown;
3
+ export {};
4
+ //# sourceMappingURL=fake.d.ts.map
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