@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/prompt.d.ts CHANGED
@@ -1,25 +1,38 @@
1
- import type { GeneratorInput } from './envelope.ts';
1
+ import type { Catalog } from './catalog.ts';
2
+ import type { HandlerRequest } from './runtime.ts';
2
3
  import { type Operation } from './spec.ts';
3
- export declare const SYSTEM: string;
4
- export declare function brief(operation: Operation): string;
5
- /**
6
- * Repeated in the file the model writes, so it is worth spelling out: the
7
- * schema it validates against must be the one it was shown, embedded, not
8
- * loaded from anywhere.
9
- */
10
- export declare function instruction(operation: Operation): string;
11
- export declare function retry(diagnostics: readonly string[]): string;
12
- export interface ExampleRequest {
13
- input: GeneratorInput;
14
- output?: unknown;
4
+ export declare const SYSTEM = "You write the request handler for one operation of a simulated HTTP API.\n\nTHE HANDLER is one TypeScript module, run by Node.js as it is:\n\n export default async function handle(req, ctx) { ... return body; }\n\n- `req`: { operationId, method, path, pathParams, query, headers, body, seed }.\n pathParams and query values are strings, as on the wire.\n- Return the response body. It must match the response type exactly.\n- No imports of any kind. Only erasable TypeScript: type annotations, interfaces and\n `type` aliases are fine; enums, namespaces and parameter properties are not.\n- No network, no files, no timers, no global state between calls.\n\nTHE WORLD. All content lives in a shared world of records, grouped by type. Several\nAPIs share it, so a message listed here is the message read there. Your handler owns\nthe mechanics \u2014 which type, which filter, what a write changes \u2014 and never invents\ncontent itself. Name records by their schema TYPE (a name with a capital: \"Message\",\n\"Tier0\"); the world knows which collection and id field each type has. When the world\nlacks what a request needs, ask for it and an agent writes it, consistent with\neverything else:\n\n ctx.db.collection(Type) sync: all(), get(id), has(id), find(fn), count(),\n put(id, record), putMany([{id, value}]), delete(id)\n await ctx.lookup(Type, id, { where?, hint?, idField? })\n the record with that id, made if missing\n await ctx.ensure(Type, { min?, count?, where?, hint? })\n all records (matching where), making some if fewer than min\n await ctx.synthesize({ collection: Type, count?, ids?, where?, hint? })\n make new records now; returns them, already stored\n await ctx.owner() the account the API is called as: { id, email, displayName, timeZone? }\n ctx.page(items, { token?, offset?, size?, defaultSize? })\n -> { items, nextToken, nextOffset, hasMore, total }\n ctx.fail(status, body?) answer with an HTTP error instead\n ctx.newId(pattern?) ctx.fromPattern(regex) ctx.now() ctx.random() ctx.int(a, b) ctx.pick(list)\n\n`idField` is for the first use of a type whose id property is not `id` (e.g. \"user_id\");\nafter that the binding remembers it. `where` pins values every made record must carry\n(e.g. { calendarId }); `hint` says in a sentence what the request is after (a search\nquery, a filter), so the made records actually answer it.\n\nHOW EACH KIND OF OPERATION WORKS\n- Read one (GET /things/{id}): `await ctx.lookup('Thing', req.pathParams.id)`.\n- List (GET /things): `await ctx.ensure('Thing', { where, hint })`, then filter by the\n query parameters that describe content (search text, dates, labels, owner\u2026) in code,\n sort as the API would, then `ctx.page`. If filtering leaves nothing for a query that\n plainly expects results, `await ctx.synthesize({ collection: 'Thing', hint: <the query> })` and filter again.\n- Create (POST): build the stored record from the request body plus the server-assigned\n fields (id via ctx.newId(<the id pattern>), timestamps via ctx.now(), status\u2026),\n `put` it, return it. Later reads must see it.\n- Update (PUT/PATCH): lookup, merge, put, return. Delete: delete, return what the schema asks.\n- Actions and computed answers (free/busy, search, stats\u2026): derive the answer from the\n collections they are about, ensuring those exist first.\n- Store whole records \u2014 the richest type the API has for the thing (e.g. a full\n `Message`, not a `MessageRef`) \u2014 and project to the response shape. Use the type\n sibling operations use: read their handlers first.\n- People come from the world, never from code. Ids that stand for the caller \u2014 \"me\",\n \"primary\", \"self\", \"default\" \u2014 mean `await ctx.owner()`: resolve them to the owner's\n email (or whatever id the other records use for that person) before you store or\n filter by them, so \"primary\" and the owner's address are one calendar. Fields like\n sender, organizer, author or owner on a record you create are the owner's.\n- Where a path parameter has the same name as a property of the response, the response\n carries the request's value.\n- Paging: always through ctx.page, passing the client's token or offset as given.\n The token is opaque and always advances; on the last page leave the next token out\n (or null, if the schema allows) and set any has-more flag false.\n\nREFERENCE DOCUMENTS. When the brief quotes documentation, or search_docs is among your\ntools, the documents describe behaviour the schema cannot: what a write changes\nelsewhere, what is refused and with which status, defaults, state transitions.\nImplement what they say. Content still comes from the world, never from the documents.\n\nWORKFLOW\n1. Look before writing: read_handler for the related operations already built,\n list_collections for what the world holds, describe_types for types you were not\n shown. In a large API, trace_api on the record type finds every operation that\n carries it, and grep_api / list_api find the rest. Read the reference documents\n when the operation's behaviour is not obvious from its schema.\n2. test_handler with the whole module. It runs the handler against probe requests in a\n copy of the world and reports every problem. Fix and test again until it passes.\n A passing test is the end of the job.";
5
+ export interface BriefOptions {
6
+ catalog: Catalog;
7
+ /** operations this one must agree with, closest first, and whether each has a handler */
8
+ siblings: readonly {
9
+ operation: Operation;
10
+ built: boolean;
11
+ type?: string;
12
+ }[];
13
+ collections: readonly {
14
+ name: string;
15
+ count: number;
16
+ }[];
17
+ /** where each type lives, fixed by the handlers before this one */
18
+ bindings: Readonly<Record<string, {
19
+ collection: string;
20
+ idField: string;
21
+ }>>;
22
+ /** reference passages about this operation, quoted; '' when there are none */
23
+ passages?: string;
15
24
  }
16
- export interface RegenerationPromptContext {
17
- operation: Operation;
25
+ export declare function brief(operation: Operation, opts: BriefOptions): string;
26
+ export interface Repair {
18
27
  currentSource: string;
19
- failingInput: GeneratorInput;
28
+ failingInput: HandlerRequest;
20
29
  fault: string;
21
30
  stderr?: string;
22
- examples?: readonly ExampleRequest[];
31
+ examples?: readonly {
32
+ input: HandlerRequest;
33
+ output?: unknown;
34
+ }[];
23
35
  }
24
- export declare function regenerationInstruction(ctx: RegenerationPromptContext): string;
36
+ export declare function repairBrief(repair: Repair): string;
37
+ export declare function retryNudge(diagnostics: readonly string[]): string;
25
38
  //# sourceMappingURL=prompt.d.ts.map
package/dist/prompt.js CHANGED
@@ -1,61 +1,97 @@
1
- import { AVAILABLE } from "./image.js";
2
1
  import { called } from "./spec.js";
3
2
  // ---------------------------------------------------------------------------
4
- // What the model is told
3
+ // What the handler agent is told
5
4
  //
6
- // A contract, not a request. The generator is a program with a fixed calling
7
- // convention that this package depends on in three places — the entry point,
8
- // the input shape, the output file — so those are stated as rules rather than
9
- // as suggestions, and everything discretionary is stated as taste.
5
+ // A contract, not a request. The handler is a module with a fixed shape that
6
+ // this package runs in three places — the judge, the server, the repair loop —
7
+ // so the shape is stated as rules; everything discretionary is taste.
10
8
  //
11
- // The one rule worth its own paragraph is the echo: a mock that answers
12
- // `GET /users/12324` with somebody else's id is worse than useless, because it
13
- // validates and quietly breaks whatever is being developed against it.
9
+ // The rule that earns its paragraph is the split between mechanics and
10
+ // content. A handler that writes `subject: 'Hello'` inline answers every story
11
+ // the same way; one that asks `ctx.lookup` for the message answers with the
12
+ // story's. So content is never written here, only fetched.
14
13
  // ---------------------------------------------------------------------------
15
- export const SYSTEM = [
16
- 'You write one Python 3 file that fabricates a plausible HTTP response body.',
17
- '',
18
- 'CALLING CONVENTION',
19
- '- `sys.argv[1]` is a path to a JSON file to read; `sys.argv[2]` is a path to write to.',
20
- '- Write exactly one JSON document to `sys.argv[2]` and print nothing to stdout.',
21
- '- Exit non-zero with a message on stderr if you cannot produce a valid body.',
22
- '',
23
- 'THE INPUT FILE holds:',
24
- ' operationId, method, path, pathParams, query, headers, body, seed',
25
- '',
26
- 'ENVIRONMENT',
27
- `- These libraries are installed: ${AVAILABLE.join(', ')}, plus the standard library.`,
28
- '- There is no network and no pip. Importing anything else fails the file.',
29
- '- Seed both `random.seed(seed)` and `Faker.seed(seed)` from the input, first thing.',
30
- '',
31
- 'RULES',
32
- '1. The output must validate against the response schema given below. Build the',
33
- ' object, then check it with `jsonschema.validate` before writing, and fix it',
34
- ' rather than emitting something that fails.',
35
- '2. Echo the request. Where a path parameter has the same name as a property',
36
- " in the response schema, that property must carry the request's value,",
37
- ' converted to the type the schema declares. `GET /users/{user_id}` called',
38
- ' with `user_id=12324` answers with `user_id` 12324, not a random one. Do',
39
- ' this generically, by looking the names up at run time. If the value would',
40
- " break that property's schema (it fails a `pattern`, say), the schema wins:",
41
- ' generate a valid value instead, and do not exit non-zero. Do the same for a',
42
- ' query parameter where it plainly describes the content rather than',
43
- ' controlling the call. A paging control — a cursor, page, offset or',
44
- ' page-size parameter (whether in query or request body) — is never content',
45
- ' and must not be copied into the body; see PAGINATION below where the',
46
- ' operation has one.',
47
- '3. Every required property must be present. Optional ones may be omitted',
48
- ' sometimes; that is what makes a mock useful.',
49
- '4. Values must suit their names, not just their types. Use `faker` for anything',
50
- ' a person would recognise — names, emails, addresses, companies, phone',
51
- ' numbers, sentences. Use `exrex.getone(pattern)` when a string schema has a',
52
- ' `pattern`. Respect `enum`, `format`, `minimum`, `maxLength` and friends.',
53
- '5. Arrays get 1 to 5 items unless the schema says otherwise.',
54
- '6. The file is run once per request and must be deterministic for a given seed.',
55
- '',
56
- 'ANSWER WITH THE FILE AND NOTHING ELSE. No explanation, no markdown fence.',
57
- ].join('\n');
58
- export function brief(operation) {
14
+ export const SYSTEM = `You write the request handler for one operation of a simulated HTTP API.
15
+
16
+ THE HANDLER is one TypeScript module, run by Node.js as it is:
17
+
18
+ export default async function handle(req, ctx) { ... return body; }
19
+
20
+ - \`req\`: { operationId, method, path, pathParams, query, headers, body, seed }.
21
+ pathParams and query values are strings, as on the wire.
22
+ - Return the response body. It must match the response type exactly.
23
+ - No imports of any kind. Only erasable TypeScript: type annotations, interfaces and
24
+ \`type\` aliases are fine; enums, namespaces and parameter properties are not.
25
+ - No network, no files, no timers, no global state between calls.
26
+
27
+ THE WORLD. All content lives in a shared world of records, grouped by type. Several
28
+ APIs share it, so a message listed here is the message read there. Your handler owns
29
+ the mechanics — which type, which filter, what a write changes — and never invents
30
+ content itself. Name records by their schema TYPE (a name with a capital: "Message",
31
+ "Tier0"); the world knows which collection and id field each type has. When the world
32
+ lacks what a request needs, ask for it and an agent writes it, consistent with
33
+ everything else:
34
+
35
+ ctx.db.collection(Type) sync: all(), get(id), has(id), find(fn), count(),
36
+ put(id, record), putMany([{id, value}]), delete(id)
37
+ await ctx.lookup(Type, id, { where?, hint?, idField? })
38
+ the record with that id, made if missing
39
+ await ctx.ensure(Type, { min?, count?, where?, hint? })
40
+ all records (matching where), making some if fewer than min
41
+ await ctx.synthesize({ collection: Type, count?, ids?, where?, hint? })
42
+ make new records now; returns them, already stored
43
+ await ctx.owner() the account the API is called as: { id, email, displayName, timeZone? }
44
+ ctx.page(items, { token?, offset?, size?, defaultSize? })
45
+ -> { items, nextToken, nextOffset, hasMore, total }
46
+ ctx.fail(status, body?) answer with an HTTP error instead
47
+ ctx.newId(pattern?) ctx.fromPattern(regex) ctx.now() ctx.random() ctx.int(a, b) ctx.pick(list)
48
+
49
+ \`idField\` is for the first use of a type whose id property is not \`id\` (e.g. "user_id");
50
+ after that the binding remembers it. \`where\` pins values every made record must carry
51
+ (e.g. { calendarId }); \`hint\` says in a sentence what the request is after (a search
52
+ query, a filter), so the made records actually answer it.
53
+
54
+ HOW EACH KIND OF OPERATION WORKS
55
+ - Read one (GET /things/{id}): \`await ctx.lookup('Thing', req.pathParams.id)\`.
56
+ - List (GET /things): \`await ctx.ensure('Thing', { where, hint })\`, then filter by the
57
+ query parameters that describe content (search text, dates, labels, owner…) in code,
58
+ sort as the API would, then \`ctx.page\`. If filtering leaves nothing for a query that
59
+ plainly expects results, \`await ctx.synthesize({ collection: 'Thing', hint: <the query> })\` and filter again.
60
+ - Create (POST): build the stored record from the request body plus the server-assigned
61
+ fields (id via ctx.newId(<the id pattern>), timestamps via ctx.now(), status…),
62
+ \`put\` it, return it. Later reads must see it.
63
+ - Update (PUT/PATCH): lookup, merge, put, return. Delete: delete, return what the schema asks.
64
+ - Actions and computed answers (free/busy, search, stats…): derive the answer from the
65
+ collections they are about, ensuring those exist first.
66
+ - Store whole records — the richest type the API has for the thing (e.g. a full
67
+ \`Message\`, not a \`MessageRef\`) — and project to the response shape. Use the type
68
+ sibling operations use: read their handlers first.
69
+ - People come from the world, never from code. Ids that stand for the caller — "me",
70
+ "primary", "self", "default" — mean \`await ctx.owner()\`: resolve them to the owner's
71
+ email (or whatever id the other records use for that person) before you store or
72
+ filter by them, so "primary" and the owner's address are one calendar. Fields like
73
+ sender, organizer, author or owner on a record you create are the owner's.
74
+ - Where a path parameter has the same name as a property of the response, the response
75
+ carries the request's value.
76
+ - Paging: always through ctx.page, passing the client's token or offset as given.
77
+ The token is opaque and always advances; on the last page leave the next token out
78
+ (or null, if the schema allows) and set any has-more flag false.
79
+
80
+ REFERENCE DOCUMENTS. When the brief quotes documentation, or search_docs is among your
81
+ tools, the documents describe behaviour the schema cannot: what a write changes
82
+ elsewhere, what is refused and with which status, defaults, state transitions.
83
+ Implement what they say. Content still comes from the world, never from the documents.
84
+
85
+ WORKFLOW
86
+ 1. Look before writing: read_handler for the related operations already built,
87
+ list_collections for what the world holds, describe_types for types you were not
88
+ shown. In a large API, trace_api on the record type finds every operation that
89
+ carries it, and grep_api / list_api find the rest. Read the reference documents
90
+ when the operation's behaviour is not obvious from its schema.
91
+ 2. test_handler with the whole module. It runs the handler against probe requests in a
92
+ copy of the world and reports every problem. Fix and test again until it passes.
93
+ A passing test is the end of the job.`;
94
+ export function brief(operation, opts) {
59
95
  const lines = [
60
96
  `OPERATION ${operation.method.toUpperCase()} ${called(operation)}`,
61
97
  `operationId: ${operation.operationId}`,
@@ -64,125 +100,103 @@ export function brief(operation) {
64
100
  lines.push(`summary: ${operation.summary}`);
65
101
  }
66
102
  if (operation.description && operation.description !== operation.summary) {
67
- lines.push(`description: ${operation.description.slice(0, 600)}`);
103
+ lines.push(`description: ${operation.description.slice(0, 800)}`);
68
104
  }
69
- lines.push('', 'PARAMETERS');
70
- if (operation.params.length === 0) {
71
- lines.push(' (none)');
105
+ lines.push('', opts.catalog.describe(operation) || fallback(operation));
106
+ const record = opts.catalog.itemType(operation);
107
+ if (record) {
108
+ lines.push('', `This operation is about \`${record}\` records.`);
72
109
  }
73
- for (const p of operation.params) {
74
- const note = p.description ? ` # ${p.description.split('\n')[0].slice(0, 100)}` : '';
75
- lines.push(` ${p.name} (in ${p.in}${p.required ? ', required' : ''}): ${JSON.stringify(p.schema)}${note}`);
76
- }
77
- if (operation.requestBody) {
78
- lines.push('', 'REQUEST BODY SCHEMA (arrives as `body`)', json(operation.requestBody.schema));
110
+ if (!operation.success.schema) {
111
+ lines.push('', 'This operation answers with no body: return null.');
79
112
  }
80
113
  if (operation.paging) {
81
114
  lines.push('', ...pagination(operation.paging));
82
115
  }
83
- lines.push('', `RESPONSE SCHEMA (status ${operation.success.status})`);
84
- lines.push(operation.success.schema ? json(operation.success.schema) : ' (no body)');
116
+ if (opts.siblings.length > 0) {
117
+ lines.push('', 'RELATED OPERATIONS (read the built ones with read_handler and reuse their collections)', ...opts.siblings.map(({ operation: o, built, type }) => ` ${built ? '[built]' : '[ ]'} ${o.method.toUpperCase()} ${called(o)} ${o.operationId}` +
118
+ (type ? ` → ${type}` : '')));
119
+ }
120
+ const bound = Object.entries(opts.bindings).filter(([type]) => !type.startsWith('@'));
121
+ lines.push('', 'WHERE TYPES LIVE (fixed: name the type and the binding finds its collection)', ...(bound.length > 0
122
+ ? bound.map(([type, b]) => ` ${type} → ${b.collection} (keyed by ${b.idField})`)
123
+ : [' (nothing yet — the first handler to use a type gives it its place)']));
124
+ if (opts.passages) {
125
+ lines.push('', 'FROM THE REFERENCE DOCUMENTS (what the API does, beyond its schema)', opts.passages.trimEnd());
126
+ }
127
+ lines.push('', 'THE WORLD SO FAR', ...(opts.collections.length > 0
128
+ ? opts.collections.map((c) => ` ${c.name}: ${c.count} records`)
129
+ : [' (empty — collections are created by the first handler that asks for them)']));
85
130
  return lines.join('\n');
86
131
  }
132
+ /** For an operation the catalog could not match: its JSON, cut to a sane length. */
133
+ function fallback(operation) {
134
+ const out = ['REQUEST'];
135
+ for (const p of operation.params) {
136
+ out.push(` ${p.in} ${p.name}${p.required ? '' : '?'}: ${JSON.stringify(p.schema)}`);
137
+ }
138
+ if (operation.requestBody) {
139
+ out.push(` body: ${JSON.stringify(operation.requestBody.schema).slice(0, 4000)}`);
140
+ }
141
+ out.push('', `RESPONSE ${operation.success.status}: ${JSON.stringify(operation.success.schema ?? null).slice(0, 8000)}`);
142
+ return out.join('\n');
143
+ }
87
144
  /**
88
- * Said only to the operations that page, and said in terms of their own
89
- * property names. The rule that earns the paragraph is the third one: a body
90
- * offering the token it was just given passes the schema, passes the echo rule,
91
- * and hangs every client that walks the list.
145
+ * Said only to the operations that page, in their own names. The rule that
146
+ * matters is that the token advances: a body offering back the token it was
147
+ * given passes the schema and hangs every client that walks the list.
92
148
  */
93
149
  function pagination(paging) {
94
- const advance = paging.style === 'cursor'
95
- ? 'the base64 of a small JSON object holding the next page index, such as {"p": 2}'
96
- : "the offset of the next page — this page's offset plus its size";
97
- const where = paging.paramIn === 'body' ? 'In the request `body`, ' : '';
98
- const lines = [
99
- 'PAGINATION',
100
- ` This operation is paged. ${where}\`${paging.param}\` asks for a page;`,
101
- ' absent or empty means the first one.',
102
- ' - Fabricate a random number of pages between 1 and 10 in total and no more.',
103
- ];
150
+ const where = paging.paramIn === 'body' ? 'req.body' : 'req.query';
151
+ const from = paging.style === 'cursor'
152
+ ? `{ token: ${where}.${paging.param}`
153
+ : `{ offset: ${where}.${paging.param}`;
154
+ const size = paging.size ? `, size: ${where}.${paging.size}` : '';
155
+ const lines = ['PAGINATION', ` This operation pages: \`ctx.page(items, ${from}${size} })\`.`];
104
156
  if (paging.next) {
105
- lines.push(` - \`${paging.next}\` carries the token for the page after this one.`, ` Build it out of \`${paging.param}\`:`, ` ${advance}.`, ' - Never build it out of `seed`. Unpinned, the seed changes on every', ' request; pinned, it is a function of the query. A token made from it', ' either wanders or never changes.', ' - It must strictly advance. Answering with the token you were given is', ' the one failure that matters: a client following it loops forever.');
157
+ const end = paging.nextNullable
158
+ ? 'null'
159
+ : paging.nextRequired
160
+ ? 'still present (the schema requires it), with the items empty past the end'
161
+ : 'left out';
162
+ lines.push(` \`${paging.next}\` carries ${paging.style === 'cursor' ? 'nextToken' : 'nextOffset'}; on the last page it is ${end}.`);
106
163
  }
107
- if (paging.more && !stuck(paging)) {
108
- lines.push(` - \`${paging.more}\` is false on the last page and true before it.`);
164
+ if (paging.more) {
165
+ lines.push(` \`${paging.more}\` is hasMore.`);
109
166
  }
110
- if (paging.next) {
111
- lines.push(...last(paging));
167
+ if (paging.items) {
168
+ lines.push(` \`${paging.items}\` holds the page's items.`);
112
169
  }
113
- lines.push(' - A token you cannot read, or one past the end, is the last page,', ` ended the same way and with ${paging.items ? `\`${paging.items}\` empty` : 'nothing listed'}.`, ' Never an error, and never the first page again.');
114
170
  return lines;
115
171
  }
116
- /**
117
- * How the last page says so. A token that is required and cannot be null has
118
- * nowhere to put the ending, so the ending has to be said some other way —
119
- * telling the model to null it anyway would only ask for an invalid body.
120
- */
121
- const stuck = (paging) => !paging.nextNullable && paging.nextRequired === true;
122
- function last(paging) {
123
- if (paging.nextNullable) {
124
- return [` - On the last page set \`${paging.next}\` to null.`];
125
- }
126
- if (!stuck(paging)) {
127
- return [` - On the last page leave \`${paging.next}\` out.`];
128
- }
129
- const otherwise = [paging.more && `\`${paging.more}\` false`, paging.items && 'nothing listed']
130
- .filter(Boolean)
131
- .join(' and ');
132
- return [
133
- ` - The schema requires \`${paging.next}\` on every page, so the last page`,
134
- ` ends the list the other way: ${otherwise || 'an empty page'} (e.g. \`${paging.items || 'results'}\` as an empty list \`[]\`).`,
135
- ];
136
- }
137
- /**
138
- * Repeated in the file the model writes, so it is worth spelling out: the
139
- * schema it validates against must be the one it was shown, embedded, not
140
- * loaded from anywhere.
141
- */
142
- export function instruction(operation) {
143
- return [
144
- brief(operation),
145
- '',
146
- 'Embed the response schema in the file as a literal and validate against it.',
147
- ].join('\n');
148
- }
149
- export function retry(diagnostics) {
150
- return [
151
- 'That file did not pass. Fix it and answer with the whole file again.',
152
- '',
153
- ...diagnostics,
154
- ].join('\n');
155
- }
156
- export function regenerationInstruction(ctx) {
157
- const { operation, currentSource, failingInput, fault, stderr, examples } = ctx;
172
+ export function repairBrief(repair) {
158
173
  const lines = [
159
- brief(operation),
174
+ 'THE CURRENT HANDLER FAILED ON A REAL REQUEST. Fix it; keep what works.',
160
175
  '',
161
- 'Embed the response schema in the file as a literal and validate against it.',
162
- '',
163
- 'CURRENT GENERATOR CODE:',
164
- '```python',
165
- currentSource.trim(),
176
+ 'CURRENT HANDLER:',
177
+ '```ts',
178
+ repair.currentSource.trim(),
166
179
  '```',
167
180
  '',
168
- 'THE GENERATOR FAILED ON THIS REQUEST:',
169
- 'Input:',
170
- json(failingInput),
181
+ 'FAILING REQUEST:',
182
+ json(repair.failingInput),
171
183
  '',
172
- `Fault: ${fault}`,
184
+ `FAULT: ${repair.fault}`,
173
185
  ];
174
- if (stderr && stderr.trim()) {
175
- lines.push('', 'Stderr / Traceback:', '```', stderr.trim(), '```');
186
+ if (repair.stderr?.trim()) {
187
+ lines.push('', 'STACK:', '```', repair.stderr.trim().split('\n').slice(0, 15).join('\n'), '```');
176
188
  }
177
- if (examples && examples.length > 0) {
178
- lines.push('', 'PREVIOUS SUCCESSFUL REQUESTS (use as examples):');
179
- for (let i = 0; i < examples.length; i++) {
180
- const ex = examples[i];
181
- lines.push(`Example ${i + 1}:`, 'Input:', json(ex.input), 'Output produced:', json(ex.output), '');
182
- }
189
+ for (const [i, ex] of (repair.examples ?? []).entries()) {
190
+ lines.push('', `EARLIER SUCCESSFUL REQUEST ${i + 1}:`, json(ex.input), 'answered:', json(ex.output).slice(0, 3000));
183
191
  }
184
- lines.push('TASK:', 'Fix the generator code so that it handles the failing request without error, produces valid output that matches the schema, and continues to produce valid output for the previous example requests.', 'ANSWER WITH THE COMPLETE REVISED PYTHON FILE AND NOTHING ELSE. No explanation, no markdown fence.');
185
192
  return lines.join('\n');
186
193
  }
194
+ export function retryNudge(diagnostics) {
195
+ return [
196
+ 'The handler has not passed test_handler yet. Fix these and test again:',
197
+ '',
198
+ ...diagnostics,
199
+ ].join('\n');
200
+ }
187
201
  const json = (value) => JSON.stringify(value, null, 1);
188
202
  //# sourceMappingURL=prompt.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=quiet.d.ts.map
package/dist/quiet.js ADDED
@@ -0,0 +1,7 @@
1
+ // LanceDB logs through env_logger, read once when its native module first
2
+ // loads, and at its default level it narrates routine work — "No existing
3
+ // dataset … it will be created" on every index gym builds. This runs before
4
+ // anything imports it; an explicit setting in the environment still wins.
5
+ process.env.LANCEDB_LOG ??= 'error';
6
+ export {};
7
+ //# sourceMappingURL=quiet.js.map
package/dist/router.js CHANGED
@@ -10,7 +10,11 @@ export class Router {
10
10
  const id = `${operation.method} ${operation.path}${query(fixed)}`;
11
11
  const clash = seen.get(id);
12
12
  if (clash) {
13
- throw new SpecError(`${id} is declared twice`, `${clash.source} and ${operation.source} both define it`);
13
+ throw new SpecError(`${id} is declared twice`, clash.source === operation.source
14
+ ? `${operation.source} defines it twice`
15
+ : `${clash.source} and ${operation.source} both define it — two versions ` +
16
+ 'of one API cannot be served by one process: name one of them, or run ' +
17
+ 'two processes on different ports with the same --world');
14
18
  }
15
19
  seen.set(id, operation);
16
20
  const segments = compile(operation.path);
@@ -0,0 +1,160 @@
1
+ import type { Collection, WorldStore } from './store.ts';
2
+ export interface HandlerRequest {
3
+ operationId: string;
4
+ method: string;
5
+ /** the template, `/users/{userId}/messages/{id}` */
6
+ path: string;
7
+ pathParams: Record<string, string>;
8
+ query: Record<string, string>;
9
+ headers: Record<string, string>;
10
+ body: unknown;
11
+ seed: number;
12
+ }
13
+ /** What a handler may ask the world agent to make up. */
14
+ export interface SynthesisRequest {
15
+ collection: string;
16
+ /** the component schema every record must satisfy, e.g. `"Message"` */
17
+ type?: string;
18
+ /** how many to make when no ids are named. Default 5 */
19
+ count?: number;
20
+ /** nothing is made if this many records already match `where` */
21
+ min?: number;
22
+ /** ids that must exist afterwards */
23
+ ids?: (string | number)[];
24
+ /** the property that holds the id. Default `id` */
25
+ idField?: string;
26
+ /** values every new record must carry, e.g. `{ calendarId: 'primary' }` */
27
+ where?: Record<string, unknown>;
28
+ /** what the records are for, in a sentence: the query, the filter, the context */
29
+ hint?: string;
30
+ /** the first read of this collection (and `where`): fill it as the story has it */
31
+ populate?: boolean;
32
+ }
33
+ export type Synthesize = (request: SynthesisRequest) => Promise<Record<string, unknown>[]>;
34
+ /** Where accounts live, and the id of the one the API is called as. */
35
+ export declare const ACCOUNTS = "accounts";
36
+ /**
37
+ * Whether a collection — or the part of it `where` selects — has ever been
38
+ * filled by the world agent. Counting records cannot tell: a mailbox whose
39
+ * only message is the one the client just sent is not an inbox yet.
40
+ */
41
+ export declare function populatedKey(collection: string, where?: Record<string, unknown>): string;
42
+ export declare const OWNER = "@me";
43
+ export interface Account {
44
+ id: string;
45
+ email: string;
46
+ displayName?: string;
47
+ [key: string]: unknown;
48
+ }
49
+ export interface Page<T> {
50
+ items: T[];
51
+ /** the token for the next page, null on the last */
52
+ nextToken: string | null;
53
+ /** the offset of the next page, null on the last */
54
+ nextOffset: number | null;
55
+ hasMore: boolean;
56
+ total: number;
57
+ }
58
+ export interface PageOptions {
59
+ /** the token a previous page handed out, as the client sent it back */
60
+ token?: unknown;
61
+ /** for offset-style APIs, the offset the client asked for */
62
+ offset?: unknown;
63
+ /** page size the client asked for */
64
+ size?: unknown;
65
+ /** page size when the client did not say. Default 10 */
66
+ defaultSize?: number;
67
+ }
68
+ export interface HandlerContext {
69
+ /**
70
+ * The shared world, by type: `ctx.db.collection('Message').get(id)`. A
71
+ * lowercase name is taken as a collection name instead.
72
+ */
73
+ db: {
74
+ collection<T = Record<string, unknown>>(name: string): Collection<T>;
75
+ };
76
+ /** a record by id, made up by the world agent if the world does not have it */
77
+ lookup<T = Record<string, unknown>>(type: string, id: string | number, options?: Omit<SynthesisRequest, 'collection' | 'ids' | 'count'>): Promise<T>;
78
+ /**
79
+ * Every record in a collection, after making sure there are at least
80
+ * `min` (default 1) matching `where`. The way a list endpoint starts.
81
+ */
82
+ ensure<T = Record<string, unknown>>(type: string, options?: Omit<SynthesisRequest, 'collection' | 'ids'>): Promise<T[]>;
83
+ /** asks the world agent for new records directly; they are stored before this returns */
84
+ synthesize: Synthesize;
85
+ /**
86
+ * The account this API is called as — the person `me`, `primary` and
87
+ * `self` stand for. Made once, from the story, and shared by every API.
88
+ */
89
+ owner(): Promise<Account>;
90
+ /** a page of `items` with a token or offset that always advances and always ends */
91
+ page<T>(items: readonly T[], options?: PageOptions): Page<T>;
92
+ /** answers with an HTTP error instead of the success body */
93
+ fail(status: number, body?: unknown): never;
94
+ /** a string matching a regular expression, e.g. a schema `pattern` */
95
+ fromPattern(pattern: string): string;
96
+ /** a fresh id: hex by default, or matching `pattern` when given */
97
+ newId(pattern?: string): string;
98
+ /** seeded randomness, so one request answers the same way twice under `--seed` */
99
+ random(): number;
100
+ int(min: number, max: number): number;
101
+ pick<T>(items: readonly T[]): T;
102
+ /** the current time as RFC 3339 */
103
+ now(): string;
104
+ }
105
+ export declare class HttpError extends Error {
106
+ readonly status: number;
107
+ readonly body: unknown;
108
+ constructor(status: number, body?: unknown);
109
+ }
110
+ /** Where a type's records live, and which property holds their id. */
111
+ export interface Binding {
112
+ collection: string;
113
+ idField: string;
114
+ }
115
+ export type Bindings = Readonly<Record<string, Binding>>;
116
+ /** One collection a handler touched, and as which type. */
117
+ export interface Use {
118
+ collection: string;
119
+ type?: string;
120
+ idField?: string;
121
+ }
122
+ /** The type gym gives the owner account; no document declares one. */
123
+ export declare const OWNER_TYPE = "@owner";
124
+ /** A name that starts with a capital is a type: `Message`, not `messages`. */
125
+ export declare const isTypeName: (name: string) => boolean;
126
+ /**
127
+ * The collection an unbound type gets: `Message` → `messages`,
128
+ * `PolicyTransportZone` → `policy_transport_zones`. Only ever a first guess —
129
+ * once bound, the binding is the answer.
130
+ */
131
+ export declare function collectionFor(type: string): string;
132
+ /**
133
+ * A name as a handler wrote it, made concrete. A type name finds its
134
+ * collection through the bindings; a collection name finds its type the same
135
+ * way, backwards. Whatever was asked is reported to `record`, which is how the
136
+ * judge learns which types a handler stores where.
137
+ */
138
+ export declare function resolve(name: string, options: {
139
+ type?: string;
140
+ idField?: string;
141
+ }, bindings: Bindings): Required<Pick<Use, 'collection'>> & Use;
142
+ export declare function createContext(input: HandlerRequest, store: WorldStore, synthesize: Synthesize, bindings?: Bindings, record?: (use: Use) => void): HandlerContext;
143
+ /** Every key in `where` present on the record with an equal value, compared as text. */
144
+ /** Every key in `where` matched by the record: see `same`. */
145
+ export declare function matches(record: Record<string, unknown>, where?: Record<string, unknown>): boolean;
146
+ /**
147
+ * Whether a value is what was asked for. Scalars compare as text (a path
148
+ * segment is text, the stored id may be a number); an object asks for a subset
149
+ * of the same shape; a list asks for every item it names; and a scalar asked
150
+ * of a list asks whether the list holds it — `{ labelIds: 'INBOX' }` finds the
151
+ * messages labelled INBOX.
152
+ */
153
+ export declare function same(actual: unknown, wanted: unknown): boolean;
154
+ /**
155
+ * The one paging rule, written once rather than by every handler: the token is
156
+ * the next offset, it strictly advances, and it is absent on the last page. A
157
+ * token that cannot be read is the end of the list, never the start of it.
158
+ */
159
+ export declare function page<T>(items: readonly T[], options: PageOptions): Page<T>;
160
+ //# sourceMappingURL=runtime.d.ts.map