@zenera/gym 1.1.40 → 1.1.42
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/worker.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import { parentPort } from 'node:worker_threads';
|
|
11
|
+
import { createContext, HttpError, } from "./runtime.js";
|
|
12
|
+
import { WorldStore } from "./store.js";
|
|
13
|
+
const port = parentPort;
|
|
14
|
+
if (!port) {
|
|
15
|
+
throw new Error('worker.ts must be started as a worker thread');
|
|
16
|
+
}
|
|
17
|
+
const stores = new Map();
|
|
18
|
+
const pending = new Map();
|
|
19
|
+
let calls = 0;
|
|
20
|
+
const storeFor = (path) => {
|
|
21
|
+
let store = stores.get(path);
|
|
22
|
+
if (!store) {
|
|
23
|
+
store = new WorldStore(path);
|
|
24
|
+
stores.set(path, store);
|
|
25
|
+
}
|
|
26
|
+
return store;
|
|
27
|
+
};
|
|
28
|
+
port.on('message', (message) => {
|
|
29
|
+
if (message.type === 'close') {
|
|
30
|
+
stores.get(message.db)?.close();
|
|
31
|
+
stores.delete(message.db);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (message.type === 'synthesized') {
|
|
35
|
+
const key = `${message.id}:${message.call}`;
|
|
36
|
+
const waiter = pending.get(key);
|
|
37
|
+
pending.delete(key);
|
|
38
|
+
if (message.error !== undefined) {
|
|
39
|
+
waiter?.reject(new Error(message.error));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
waiter?.resolve(message.records ?? []);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
void run(message);
|
|
47
|
+
});
|
|
48
|
+
async function run(message) {
|
|
49
|
+
const { id } = message;
|
|
50
|
+
const used = new Map();
|
|
51
|
+
const record = (use) => {
|
|
52
|
+
used.set(`${use.collection}\u0000${use.type ?? ''}\u0000${use.idField ?? ''}`, use);
|
|
53
|
+
};
|
|
54
|
+
const reply = (m) => port.postMessage({ ...m, used: [...used.values()] });
|
|
55
|
+
try {
|
|
56
|
+
const mod = (await import(__rewriteRelativeImportExtension(`${pathToFileURL(message.file).href}?v=${message.version}`)));
|
|
57
|
+
const handle = (mod.default ?? mod.handle);
|
|
58
|
+
if (typeof handle !== 'function') {
|
|
59
|
+
reply({
|
|
60
|
+
type: 'done',
|
|
61
|
+
id,
|
|
62
|
+
ok: false,
|
|
63
|
+
fault: 'exports no handler: write `export default async function handle(req, ctx)`',
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const store = storeFor(message.db);
|
|
68
|
+
const ctx = createContext(message.input, store, (request) => {
|
|
69
|
+
const call = ++calls;
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
pending.set(`${id}:${call}`, { resolve, reject });
|
|
72
|
+
port.postMessage({
|
|
73
|
+
type: 'synthesize',
|
|
74
|
+
id,
|
|
75
|
+
call,
|
|
76
|
+
db: message.db,
|
|
77
|
+
request,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}, message.bindings, record);
|
|
81
|
+
const value = await handle(structuredClone(message.input), ctx);
|
|
82
|
+
reply({ type: 'done', id, ok: true, value: value === undefined ? null : value });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (err instanceof HttpError) {
|
|
86
|
+
reply({ type: 'done', id, ok: true, value: err.body, status: err.status });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
90
|
+
reply({
|
|
91
|
+
type: 'done',
|
|
92
|
+
id,
|
|
93
|
+
ok: false,
|
|
94
|
+
fault: `threw ${e.name}: ${e.message.split('\n')[0]}`,
|
|
95
|
+
stderr: e.stack,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=worker.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/gym",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.42",
|
|
4
4
|
"description": "Simulated API environments for agent training and evaluation, powered by OpenAPI and model-written generators.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openapi",
|
|
@@ -46,9 +46,11 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@apidevtools/swagger-parser": "^12.0.0",
|
|
49
|
+
"@zenera/cli": "^1.1.42",
|
|
50
|
+
"@zenera/neo": "^1.1.42",
|
|
51
|
+
"@zenera/rag": "^1.1.42",
|
|
49
52
|
"ajv": "^8.17.1",
|
|
50
53
|
"ajv-formats": "^3.0.1",
|
|
51
|
-
"
|
|
52
|
-
"@zenera/neo": "^1.1.40"
|
|
54
|
+
"randexp": "^0.5.3"
|
|
53
55
|
}
|
|
54
56
|
}
|
package/dist/envelope.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export interface GeneratorInput {
|
|
2
|
-
operationId: string;
|
|
3
|
-
method: string;
|
|
4
|
-
/** the template, not the resolved path — `/users/{user_id}` */
|
|
5
|
-
path: string;
|
|
6
|
-
pathParams: Record<string, unknown>;
|
|
7
|
-
query: Record<string, unknown>;
|
|
8
|
-
headers: Record<string, string>;
|
|
9
|
-
body: unknown;
|
|
10
|
-
/** seeds `random` and `Faker` inside the generator */
|
|
11
|
-
seed: number;
|
|
12
|
-
}
|
|
13
|
-
//# sourceMappingURL=envelope.d.ts.map
|
package/dist/envelope.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// What a generator is handed
|
|
3
|
-
//
|
|
4
|
-
// One shape, written by the server for a real request and by the build loop for
|
|
5
|
-
// a synthetic probe. That they are the same shape is the whole reason a
|
|
6
|
-
// generator that satisfies its probes also satisfies traffic.
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
export {};
|
|
9
|
-
//# sourceMappingURL=envelope.js.map
|
package/dist/image.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { type Runner } from '@zenera/neo';
|
|
2
|
-
/**
|
|
3
|
-
* Lower bounds rather than exact pins: the tag is a function of this list, so
|
|
4
|
-
* an edit here yields a new image and a new container either way, and pinning a
|
|
5
|
-
* patch release would only mean a rebuild every time one is published.
|
|
6
|
-
*/
|
|
7
|
-
export declare const REQUIREMENTS: readonly ["Faker>=37", "exrex>=0.11", "jsonschema>=4.23", "python-dateutil>=2.9"];
|
|
8
|
-
/** What the generator may import. Quoted verbatim in the prompt. */
|
|
9
|
-
export declare const AVAILABLE: readonly ["faker", "exrex", "jsonschema", "dateutil"];
|
|
10
|
-
export declare const BASE_IMAGE = "docker.io/library/python:3.14-slim-bookworm";
|
|
11
|
-
export declare function imageTag(base?: string): string;
|
|
12
|
-
export interface ImageOptions {
|
|
13
|
-
/** where the empty build context is created */
|
|
14
|
-
root: string;
|
|
15
|
-
base?: string;
|
|
16
|
-
engine?: string;
|
|
17
|
-
exec?: Runner;
|
|
18
|
-
onBuild?: (tag: string) => void;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* The tag to run, built if it is not there yet. Returns without touching podman
|
|
22
|
-
* when the image already exists, which is every start after the first.
|
|
23
|
-
*/
|
|
24
|
-
export declare function ensureImage(opts: ImageOptions): Promise<string>;
|
|
25
|
-
//# sourceMappingURL=image.d.ts.map
|
package/dist/image.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { DEFAULT_SANDBOX_IMAGE, runProcess } from '@zenera/neo';
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// The image
|
|
7
|
-
//
|
|
8
|
-
// The generators are Python, and Python that has to invent a plausible name out
|
|
9
|
-
// of `random.choice` writes output that looks like test data because it is. So
|
|
10
|
-
// the container gets real libraries.
|
|
11
|
-
//
|
|
12
|
-
// They cannot be installed into the *running* box. `containerName()` in
|
|
13
|
-
// @zenera/neo hashes the image and the network, so "start online, install, then
|
|
14
|
-
// switch to `network: none`" resolves to two different containers and the
|
|
15
|
-
// install goes with the first one. Baking them into an image instead is one
|
|
16
|
-
// build, cached by podman's own layers, and leaves the serving container
|
|
17
|
-
// offline — which is the property that matters, because the code it runs was
|
|
18
|
-
// written by a model.
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
/**
|
|
21
|
-
* Lower bounds rather than exact pins: the tag is a function of this list, so
|
|
22
|
-
* an edit here yields a new image and a new container either way, and pinning a
|
|
23
|
-
* patch release would only mean a rebuild every time one is published.
|
|
24
|
-
*/
|
|
25
|
-
export const REQUIREMENTS = [
|
|
26
|
-
'Faker>=37',
|
|
27
|
-
'exrex>=0.11',
|
|
28
|
-
'jsonschema>=4.23',
|
|
29
|
-
'python-dateutil>=2.9',
|
|
30
|
-
];
|
|
31
|
-
/** What the generator may import. Quoted verbatim in the prompt. */
|
|
32
|
-
export const AVAILABLE = ['faker', 'exrex', 'jsonschema', 'dateutil'];
|
|
33
|
-
export const BASE_IMAGE = DEFAULT_SANDBOX_IMAGE;
|
|
34
|
-
export function imageTag(base = BASE_IMAGE) {
|
|
35
|
-
const digest = createHash('sha256')
|
|
36
|
-
.update(JSON.stringify([base, [...REQUIREMENTS].sort()]))
|
|
37
|
-
.digest('hex')
|
|
38
|
-
.slice(0, 12);
|
|
39
|
-
return `localhost/zenera-gym:${digest}`;
|
|
40
|
-
}
|
|
41
|
-
function containerfile(base) {
|
|
42
|
-
return [
|
|
43
|
-
`FROM ${base}`,
|
|
44
|
-
`RUN pip install --no-cache-dir --disable-pip-version-check ${REQUIREMENTS.join(' ')}`,
|
|
45
|
-
'',
|
|
46
|
-
].join('\n');
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* The tag to run, built if it is not there yet. Returns without touching podman
|
|
50
|
-
* when the image already exists, which is every start after the first.
|
|
51
|
-
*/
|
|
52
|
-
export async function ensureImage(opts) {
|
|
53
|
-
const base = opts.base ?? BASE_IMAGE;
|
|
54
|
-
const tag = imageTag(base);
|
|
55
|
-
const engine = opts.engine ?? 'podman';
|
|
56
|
-
const run = opts.exec ?? runProcess;
|
|
57
|
-
const exists = await run(engine, ['image', 'exists', tag], { timeoutMs: 60_000 });
|
|
58
|
-
if (exists.code === 0) {
|
|
59
|
-
return tag;
|
|
60
|
-
}
|
|
61
|
-
opts.onBuild?.(tag);
|
|
62
|
-
// An empty directory, because there is nothing to COPY: handing podman the
|
|
63
|
-
// working directory instead would tar up whatever happened to be in it.
|
|
64
|
-
const context = join(opts.root, 'build');
|
|
65
|
-
mkdirSync(context, { recursive: true });
|
|
66
|
-
// A real file rather than `--file -`: a spawned child's stdin is a
|
|
67
|
-
// socketpair, and a socket cannot be reopened through /dev/stdin, so the
|
|
68
|
-
// engine fails with ENXIO. It sits beside the context, not inside it, so
|
|
69
|
-
// the context stays empty.
|
|
70
|
-
const file = join(opts.root, 'Containerfile');
|
|
71
|
-
writeFileSync(file, containerfile(base));
|
|
72
|
-
const built = await run(engine, ['build', '--tag', tag, '--file', file, context], {
|
|
73
|
-
timeoutMs: 900_000,
|
|
74
|
-
maxBytes: 256 * 1024,
|
|
75
|
-
});
|
|
76
|
-
if (built.code !== 0) {
|
|
77
|
-
throw new Error(`could not build ${tag}: ${(built.stderr.trim() || built.stdout.trim()).split('\n').slice(-3).join(' ')}`);
|
|
78
|
-
}
|
|
79
|
-
return tag;
|
|
80
|
-
}
|
|
81
|
-
//# sourceMappingURL=image.js.map
|