@zenera/gym 1.1.40 → 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/README.md
CHANGED
|
@@ -23,8 +23,10 @@
|
|
|
23
23
|
# @zenera/gym
|
|
24
24
|
|
|
25
25
|
**Simulated API environments for agent training and evaluation. Point Gym at
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
OpenAPI/Swagger documents — and, if you like, a story about the world behind
|
|
27
|
+
them and the documentation that goes with it — and it serves the API:
|
|
28
|
+
stateful, consistent across endpoints and processes, and populated with
|
|
29
|
+
content that follows the story.**
|
|
28
30
|
|
|
29
31
|
Gym currently provides the API simulation layer, not a training loop, task
|
|
30
32
|
scoring, or rewards.
|
|
@@ -36,143 +38,206 @@ scoring, or rewards.
|
|
|
36
38
|
|
|
37
39
|
## What you get
|
|
38
40
|
|
|
39
|
-
- **A mock server from
|
|
40
|
-
|
|
41
|
-
- **
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **
|
|
45
|
-
|
|
41
|
+
- **A mock server from documents** - name OpenAPI/Swagger files, directories or
|
|
42
|
+
patterns, and every operation answers.
|
|
43
|
+
- **A world, not random bodies** - every answer comes from a shared world of
|
|
44
|
+
records. `GET /messages` lists what `GET /messages/{id}` returns, a `POST` is
|
|
45
|
+
visible to the next `GET`, and a `DELETE` deletes.
|
|
46
|
+
- **A story it follows** - describe the people, the projects and what is going
|
|
47
|
+
on, and the mailbox, the calendar and everything else read like that place.
|
|
48
|
+
- **Documentation it uses** - hand it a knowledge base, runbooks, an admin
|
|
49
|
+
guide; agents search it for realistic values and for what the API does.
|
|
50
|
+
- **Several APIs, one world** - mail on one port and calendar on another, given
|
|
51
|
+
the same story, share one world: the meeting in an email is the meeting on
|
|
52
|
+
the calendar.
|
|
53
|
+
- **Code written once** - an agent writes a TypeScript handler per operation,
|
|
54
|
+
judged before it is kept; after that a request is a function call, and the
|
|
55
|
+
only tokens spent are on content the world does not have yet.
|
|
56
|
+
- **Large APIs** - thousands of operations load in seconds; nothing is written
|
|
57
|
+
until it is called.
|
|
58
|
+
- **Lists that end** - paging is built in, judged, and cut at request time.
|
|
46
59
|
|
|
47
60
|
## Install
|
|
48
61
|
|
|
49
|
-
Node.js 24
|
|
62
|
+
Node.js 24+. Install it alongside the CLI:
|
|
50
63
|
|
|
51
64
|
```sh
|
|
52
65
|
npm i -g @zenera/cli @zenera/gym
|
|
53
66
|
zen key add openai # the keyring `zen` already uses
|
|
54
67
|
```
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
lists `gym` whether or not it is installed, and says what to run if not.
|
|
69
|
+
No container engine is needed: handlers are TypeScript that Node runs as it is.
|
|
58
70
|
|
|
59
71
|
## Use
|
|
60
72
|
|
|
61
73
|
```sh
|
|
62
|
-
zen gym serve api/openapi.yaml
|
|
74
|
+
zen gym serve api/openapi.yaml
|
|
63
75
|
curl -s localhost:8787/users/12324
|
|
64
|
-
# { "user_id": 12324, "email": "brooke.hoffman@example.org", ... }
|
|
65
76
|
```
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
Three kinds of input, each a file, a directory or a quoted pattern, each
|
|
79
|
+
repeatable:
|
|
80
|
+
|
|
81
|
+
| Input | What it is | How the agents get it |
|
|
82
|
+
| ------------------- | ------------------------------------------------------------------------ | ------------------------------- |
|
|
83
|
+
| `spec…`, `--schema` | API documents (`.yaml`, `.yml`, `.json` declaring `swagger:`/`openapi:`) | a graph of operations and types |
|
|
84
|
+
| `--story` | the state of this world: who is who, what is happening | quoted whole, in every prompt |
|
|
85
|
+
| `--docs` | reference material: a knowledge base, guides, runbooks | searched when needed |
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
zen gym serve --schema 'specs/*.yaml' --story world.md --docs kb/ --docs 'runbooks/*.md'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A directory is walked, and only files that declare `swagger:` or `openapi:` are
|
|
92
|
+
taken as API documents — so pointing `--schema` at a folder of mixed JSON, or at
|
|
93
|
+
an index somebody built, finds the APIs in it. Quote patterns so the shell leaves
|
|
94
|
+
them to Gym.
|
|
95
|
+
|
|
96
|
+
Two APIs, two processes, one world:
|
|
69
97
|
|
|
70
98
|
```sh
|
|
71
|
-
zen gym serve specs
|
|
99
|
+
zen gym serve specs/mail.yaml --story world.md --docs kb/ --port 8787
|
|
100
|
+
zen gym serve specs/calendar.yaml --story world.md --docs kb/ --port 8788
|
|
72
101
|
```
|
|
73
102
|
|
|
74
|
-
|
|
103
|
+
[`examples/mock-apis`](https://github.com/zenera-ai/ZeneraNeo/tree/main/examples/mock-apis)
|
|
104
|
+
has a story and a knowledge base for the mail and calendar specs beside it, and
|
|
105
|
+
another for an NSX network.
|
|
106
|
+
|
|
107
|
+
Everything is indexed before the server listens, so no request waits on an
|
|
108
|
+
index. Write every handler up front, before a demo or a test run:
|
|
75
109
|
|
|
76
110
|
```sh
|
|
77
|
-
zen gym build
|
|
78
|
-
zen gym cache ls # what has been generated
|
|
79
|
-
zen gym cache clear # throw it away
|
|
111
|
+
zen gym build --schema 'specs/*.yaml' --story world.md --docs kb/
|
|
80
112
|
```
|
|
81
113
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
## How an answer is produced
|
|
115
|
+
|
|
116
|
+
Two agents, and a split between them that is the whole design.
|
|
117
|
+
|
|
118
|
+
**The handler agent writes mechanics.** The first time an operation is called,
|
|
119
|
+
an agent writes its **handler**: one TypeScript module, `export default async
|
|
120
|
+
function handle(req, ctx)`. It is shown the operation as TypeScript types, the
|
|
121
|
+
operations it must agree with (found by tracing its record type through the API
|
|
122
|
+
graph, so they are found however far apart their paths are), where each type
|
|
123
|
+
already lives, and the documentation about the operation. It can look further:
|
|
124
|
+
`read_handler`, `trace_api`, `grep_api`, `list_api`, `describe_types`, the world's
|
|
125
|
+
collections and the docs.
|
|
126
|
+
|
|
127
|
+
A handler owns which type an operation is about, how the query filters it, what
|
|
128
|
+
a write changes and how the list pages — and never the content. It names records
|
|
129
|
+
by their schema type:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
export default async function handle(req, ctx) {
|
|
133
|
+
const all = await ctx.ensure('Message', { hint: req.query.q });
|
|
134
|
+
const q = (req.query.q ?? '').toLowerCase();
|
|
135
|
+
const hits = all.filter((m) => `${m.subject} ${m.snippet}`.toLowerCase().includes(q));
|
|
136
|
+
const page = ctx.page(hits, { token: req.query.pageToken, size: req.query.maxResults });
|
|
137
|
+
return {
|
|
138
|
+
messages: page.items.map(({ id, threadId }) => ({ id, threadId })),
|
|
139
|
+
nextPageToken: page.nextToken ?? undefined,
|
|
140
|
+
resultSizeEstimate: hits.length,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Where types live is decided once.** The first handler to use a type binds it to
|
|
146
|
+
a collection and an id field; every later handler is told the binding and held
|
|
147
|
+
to it. So `listMessages` and `getMessage` read the same records, whichever was
|
|
148
|
+
written first and whichever was rebuilt since.
|
|
149
|
+
|
|
150
|
+
**The world agent writes content.** When a handler finds the world short —
|
|
151
|
+
an inbox nobody has read yet, an id nobody has seen — `ctx.ensure` and
|
|
152
|
+
`ctx.lookup` ask, and an agent writes the records: from the story, from the
|
|
153
|
+
docs, and consistent with everything already in the world, checked against the
|
|
154
|
+
schema type before anything can read them. On a first read it makes what the
|
|
155
|
+
story says exists, however many that is. Records it refers to that do not exist
|
|
156
|
+
yet — a label, a transport zone — it makes too, in their own collections.
|
|
157
|
+
`me`, `primary` and the like resolve through `ctx.owner()`, the account the story
|
|
158
|
+
says the API belongs to.
|
|
159
|
+
|
|
160
|
+
Because content lives in the world and not in the code, one cached handler
|
|
161
|
+
serves every story.
|
|
162
|
+
|
|
163
|
+
**Nothing is kept until it is judged.** `test_handler` runs the module against
|
|
164
|
+
probe requests in a throwaway copy of the world (content there is sampled from
|
|
165
|
+
the schema, so judging costs no model calls) and checks every answer against the
|
|
166
|
+
response schema, the **echo rule** (`GET /users/12324` must be about user
|
|
167
|
+
12324), the paging walk, and the bindings. The first test that passes ends the
|
|
168
|
+
build. A build that never passes leaves a report in `<cache>/builds/`, and every
|
|
169
|
+
agent run leaves an HTML report in `<cache>/reports/`.
|
|
170
|
+
|
|
171
|
+
Handlers run in a worker thread with an empty environment. A handler that
|
|
172
|
+
throws or times out is repaired, up to `--regen-limit` times per URL; a failure
|
|
173
|
+
of the world agent is never treated as a fault in the handler.
|
|
174
|
+
|
|
175
|
+
## Worlds
|
|
176
|
+
|
|
177
|
+
A world is one SQLite file under `<cache>/worlds/`. With `--story` and no
|
|
178
|
+
`--world`, it is named after what the story says, so every process given the
|
|
179
|
+
same story shares it; editing the story starts a new one, and Gym says so.
|
|
180
|
+
`--world <name>` names one that survives story edits; `--world ./path.sqlite`
|
|
181
|
+
puts it anywhere. Without either, there is one default world.
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
zen gym world ls # every world, what it holds, when it was used
|
|
185
|
+
zen gym world rm northwind # delete one
|
|
186
|
+
zen gym serve … --reset # start the one in use from empty
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Worlds are data, not cache: `zen gym cache clear` leaves them alone.
|
|
190
|
+
|
|
191
|
+
## Docs and embeddings
|
|
192
|
+
|
|
193
|
+
`--docs` are indexed into a directory Gym owns, keyed by their content and by the
|
|
194
|
+
model that embeds them: a restart reuses the index, and an edit pays only for the
|
|
195
|
+
chunks that changed. By default search is by wording (a full-text index), and no
|
|
196
|
+
embedding model is called. `--embedding` adds search by meaning: `--embedding <ref>`
|
|
197
|
+
names the model, and `--embedding on` (or `default`) takes the configured one —
|
|
198
|
+
`ZENERA_GYM_EMBEDDING`, then `zen gym model --embedding <ref>`, then the first
|
|
199
|
+
provider your keys offer. Those settings only answer which model; without the
|
|
200
|
+
flag they turn nothing on. `--embedding off` (or `none`) is the default.
|
|
114
201
|
|
|
115
202
|
## Pages that end
|
|
116
203
|
|
|
117
|
-
A list endpoint is the one place a mock can hang a real client.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
out of the paging parameter rather than the seed, and to end the list - null,
|
|
129
|
-
absent, or `has_more: false` (and empty items) where the schema leaves no other room;
|
|
130
|
-
- the generator is then **walked**: Gym calls it with no cursor, follows
|
|
131
|
-
the token it gets back, and rejects the file if the token repeats, cycles, or
|
|
132
|
-
never runs out. The diagnostics say which, and the model gets another go;
|
|
133
|
-
- at request time, pagination is tracked and bounded: a token identical to the one
|
|
134
|
-
just sent is **cut** (nulled or dropped), cycles are stopped, and streams are capped
|
|
135
|
-
to random 1-10 pages, returning empty results if a client loops past the end. The
|
|
136
|
-
request line narrates each cut. Nothing is invented in its place; a generator written
|
|
137
|
-
before this rule existed is still cached, and a cache is not rebuilt because a rule changed.
|
|
138
|
-
|
|
139
|
-
Only paginated operations are affected. Their cache keys changed once, so they
|
|
140
|
-
are written again on first use; everything else keeps the key it had.
|
|
141
|
-
`GET /__gym/routes` reports the shape that was recognised, per operation.
|
|
142
|
-
|
|
143
|
-
Plenty of documents describe the envelope and never write down the parameter
|
|
144
|
-
that reads it back. The first two steps cannot help there - nothing static can
|
|
145
|
-
see a parameter that is not declared - but the cut still applies: it takes the
|
|
146
|
-
paging parameter from query parameters or request body, since a client only sends
|
|
147
|
-
`cursor=X` because a body handed it X.
|
|
204
|
+
A list endpoint is the one place a mock can hang a real client. Where an
|
|
205
|
+
operation has a paging parameter (`cursor`, `page`, `offset`, `page_token`, …) and
|
|
206
|
+
a response property that carries the next one (`next`, `next_cursor`,
|
|
207
|
+
`has_more`, …), the handler is told to page with `ctx.page`, whose token always
|
|
208
|
+
advances and is absent on the last page; it is walked before it is kept; and at
|
|
209
|
+
request time a repeated or cycling token is cut, and streams are capped to a
|
|
210
|
+
random 1-10 pages. `GET /__gym/routes` reports the shape recognised.
|
|
211
|
+
|
|
212
|
+
A path key with a query string on it — `/x/{id}?action=retry` — is read for what
|
|
213
|
+
it means: the path is the part before the `?`, and each pair after it becomes a
|
|
214
|
+
required query value that tells those operations apart.
|
|
148
215
|
|
|
149
216
|
## Commands
|
|
150
217
|
|
|
151
218
|
```
|
|
152
|
-
zen gym serve
|
|
153
|
-
zen gym build
|
|
154
|
-
zen gym cache ls | clear
|
|
155
|
-
zen gym
|
|
219
|
+
zen gym serve [spec...] Serve the APIs. Handlers are written on first use.
|
|
220
|
+
zen gym build [spec...] Index everything and write every handler, then exit.
|
|
221
|
+
zen gym cache ls | clear Handlers, bindings and indexes.
|
|
222
|
+
zen gym world ls | rm Worlds.
|
|
223
|
+
zen gym model [ref] Show or set the chat and embedding models.
|
|
156
224
|
```
|
|
157
225
|
|
|
226
|
+
Useful options: `--schema`, `--story`, `--docs`, `--world`, `--reset`,
|
|
227
|
+
`--embedding`, `--port`, `--host` (reachable only from this machine by default),
|
|
228
|
+
`--model`, `--seed`, `--rebuild`, `--attempts`, `--concurrency`, `--timeout`,
|
|
229
|
+
`--cache <dir>`, `--quiet`. `zen help gym` prints the full table.
|
|
230
|
+
|
|
158
231
|
The model is looked for in this order: `--model`, `ZENERA_GYM_MODEL`, what
|
|
159
232
|
`zen gym model <provider>:<id>` stored, then the best one your keys can buy.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
`GET /` is a contents page: every operation the mock is serving, grouped by the
|
|
170
|
-
document it came from, with its parameters and the shape of its answer. Each
|
|
171
|
-
one has a **Run** button, which opens a form — a box per parameter, required
|
|
172
|
-
ones marked, a body prefilled with something the mock will accept — and shows
|
|
173
|
-
the status, the time and the answer. `GET /__gym/routes` is the same thing as
|
|
174
|
-
JSON; `GET /__gym/health` is a health check. A document that declares `/`
|
|
175
|
-
itself keeps it.
|
|
233
|
+
Both agents call tools; OpenAI's reasoning models take tools over the Responses
|
|
234
|
+
API (`openai/responses:<id>`), which is what the default names.
|
|
235
|
+
|
|
236
|
+
`GET /` is a contents page: every operation, grouped by document, with a **Run**
|
|
237
|
+
button and a form per operation. `GET /__gym/routes` is the same as JSON;
|
|
238
|
+
`GET /__gym/health` is a health check.
|
|
239
|
+
|
|
240
|
+
See [DESIGN.md](DESIGN.md) for how the pieces fit together.
|
|
176
241
|
|
|
177
242
|
## Credentials
|
|
178
243
|
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type AgentState, type AnyTool, type Model } from '@zenera/neo';
|
|
2
|
+
export interface AgentRunOptions {
|
|
3
|
+
model: Model;
|
|
4
|
+
name: string;
|
|
5
|
+
instructions: string;
|
|
6
|
+
tools: AnyTool[];
|
|
7
|
+
input: string;
|
|
8
|
+
/** the run is stopped after this many tool calls, finished or not */
|
|
9
|
+
maxToolCalls: number;
|
|
10
|
+
/** checked after every tool call; true ends the run there */
|
|
11
|
+
done?: () => boolean;
|
|
12
|
+
/** where the HTML report goes; no report when unset */
|
|
13
|
+
reportDir?: string;
|
|
14
|
+
/** a word for the report's file name */
|
|
15
|
+
label: string;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}
|
|
18
|
+
export interface AgentRun {
|
|
19
|
+
state: AgentState;
|
|
20
|
+
stopReason: 'final' | 'aborted' | 'failed';
|
|
21
|
+
toolCalls: number;
|
|
22
|
+
report?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function runAgent(opts: AgentRunOptions): Promise<AgentRun>;
|
|
25
|
+
//# sourceMappingURL=agents.d.ts.map
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AgentRunner, buildRunReport, renderReportHtml, } from '@zenera/neo';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
export async function runAgent(opts) {
|
|
6
|
+
const runner = new AgentRunner({ model: opts.model, recordRequests: Boolean(opts.reportDir) });
|
|
7
|
+
const agent = runner.agent({
|
|
8
|
+
name: opts.name,
|
|
9
|
+
instructions: opts.instructions,
|
|
10
|
+
tools: opts.tools,
|
|
11
|
+
});
|
|
12
|
+
const stop = new AbortController();
|
|
13
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, stop.signal]) : stop.signal;
|
|
14
|
+
const stream = runner.run(agent, opts.input, { signal });
|
|
15
|
+
let toolCalls = 0;
|
|
16
|
+
for await (const event of stream) {
|
|
17
|
+
if (event.type === 'after_tool_call') {
|
|
18
|
+
toolCalls++;
|
|
19
|
+
if (opts.done?.() || toolCalls >= opts.maxToolCalls) {
|
|
20
|
+
stop.abort();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const result = await stream.final();
|
|
25
|
+
const report = opts.reportDir
|
|
26
|
+
? await saveReport(runner, result.state, opts.reportDir, `${opts.name}-${opts.label}`)
|
|
27
|
+
: undefined;
|
|
28
|
+
if (result.stopReason === 'failed' && !opts.done?.()) {
|
|
29
|
+
throw Object.assign(new Error(result.state.error ?? `the ${opts.name} agent failed`), {
|
|
30
|
+
hint: report ? `report: ${report}` : undefined,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return { state: result.state, stopReason: result.stopReason, toolCalls, report };
|
|
34
|
+
}
|
|
35
|
+
/** A report is a convenience; failing to write one must not fail the run. */
|
|
36
|
+
async function saveReport(runner, state, dir, label) {
|
|
37
|
+
try {
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
const built = await buildRunReport(state, runner.services.payloads, {
|
|
40
|
+
title: label,
|
|
41
|
+
architecture: await runner.describe(),
|
|
42
|
+
});
|
|
43
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
44
|
+
const path = join(dir, `${stamp}-${label.replace(/[^A-Za-z0-9_.-]+/g, '_')}.html`);
|
|
45
|
+
await writeFile(path, renderReportHtml(built), 'utf8');
|
|
46
|
+
return resolve(path);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=agents.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Binding, type Bindings as Map } from './runtime.ts';
|
|
2
|
+
export declare const BINDINGS_KIND = "gym-bindings";
|
|
3
|
+
export interface BindingsOptions {
|
|
4
|
+
/** keep them somewhere other than the shared store */
|
|
5
|
+
cacheDir?: string;
|
|
6
|
+
/** keep nothing */
|
|
7
|
+
ephemeral?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class Bindings {
|
|
10
|
+
#private;
|
|
11
|
+
constructor(types: readonly string[], opts?: BindingsOptions);
|
|
12
|
+
/** Every binding, the owner account included. */
|
|
13
|
+
all(): Map;
|
|
14
|
+
of(type: string): Binding | undefined;
|
|
15
|
+
/** The type a collection holds, if anything is bound to it. */
|
|
16
|
+
typeOf(collection: string): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Records a binding. Nothing happens if it is already there; an error comes
|
|
19
|
+
* back if the type lives elsewhere or the collection holds another type.
|
|
20
|
+
*/
|
|
21
|
+
bind(type: string, binding: Binding): string | undefined;
|
|
22
|
+
/** Why this binding cannot be made, or undefined if it can (or exists). */
|
|
23
|
+
conflict(type: string, binding: Binding): string | undefined;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=bindings.d.ts.map
|
package/dist/bindings.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Cache as Store, NO_CACHE } from '@zenera/cli/lib';
|
|
2
|
+
import { ACCOUNTS, OWNER_TYPE } from "./runtime.js";
|
|
3
|
+
import { digest } from "./store.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Bindings: which collection each type lives in
|
|
6
|
+
//
|
|
7
|
+
// The one decision every handler of an API must agree on, so it is made once
|
|
8
|
+
// and written down rather than left to each handler's author to rediscover. A
|
|
9
|
+
// handler names types — `ctx.lookup('Message', id)` — and the first handler
|
|
10
|
+
// to use a type binds it to the collection it used; every later one is told
|
|
11
|
+
// the binding and held to it by the judge.
|
|
12
|
+
//
|
|
13
|
+
// Add-only. Changing where a type lives would strand every record already
|
|
14
|
+
// stored under the old name and every handler written against it, so the only
|
|
15
|
+
// way to rebind is to clear the cache (and, sensibly, the world).
|
|
16
|
+
//
|
|
17
|
+
// Kept beside the handlers, in the machine's cache, and keyed by the set of type
|
|
18
|
+
// names the documents declare — the same documents served from anywhere share
|
|
19
|
+
// one set of bindings, as they share their handlers.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
export const BINDINGS_KIND = 'gym-bindings';
|
|
22
|
+
export class Bindings {
|
|
23
|
+
#store;
|
|
24
|
+
#key;
|
|
25
|
+
#map;
|
|
26
|
+
constructor(types, opts = {}) {
|
|
27
|
+
this.#store = opts.ephemeral
|
|
28
|
+
? NO_CACHE
|
|
29
|
+
: new Store(BINDINGS_KIND, { dir: opts.cacheDir, mode: 0o600 });
|
|
30
|
+
this.#key = digest(JSON.stringify([...types].sort()));
|
|
31
|
+
this.#map = this.#load();
|
|
32
|
+
}
|
|
33
|
+
/** Every binding, the owner account included. */
|
|
34
|
+
all() {
|
|
35
|
+
return { ...this.#map, [OWNER_TYPE]: { collection: ACCOUNTS, idField: 'id' } };
|
|
36
|
+
}
|
|
37
|
+
of(type) {
|
|
38
|
+
return this.all()[type];
|
|
39
|
+
}
|
|
40
|
+
/** The type a collection holds, if anything is bound to it. */
|
|
41
|
+
typeOf(collection) {
|
|
42
|
+
return Object.entries(this.all()).find(([, b]) => b.collection === collection)?.[0];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Records a binding. Nothing happens if it is already there; an error comes
|
|
46
|
+
* back if the type lives elsewhere or the collection holds another type.
|
|
47
|
+
*/
|
|
48
|
+
bind(type, binding) {
|
|
49
|
+
// Another process may have bound something since this one started.
|
|
50
|
+
this.#map = { ...this.#load(), ...this.#map };
|
|
51
|
+
const problem = this.conflict(type, binding);
|
|
52
|
+
if (problem || this.#map[type]) {
|
|
53
|
+
return problem;
|
|
54
|
+
}
|
|
55
|
+
this.#map[type] = binding;
|
|
56
|
+
this.#store.put(this.#key, this.#map);
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
/** Why this binding cannot be made, or undefined if it can (or exists). */
|
|
60
|
+
conflict(type, binding) {
|
|
61
|
+
const current = this.of(type);
|
|
62
|
+
if (current) {
|
|
63
|
+
if (current.collection !== binding.collection) {
|
|
64
|
+
return `\`${type}\` lives in \`${current.collection}\`, not \`${binding.collection}\``;
|
|
65
|
+
}
|
|
66
|
+
if (current.idField !== binding.idField) {
|
|
67
|
+
return `\`${type}\` records are keyed by \`${current.idField}\`, not \`${binding.idField}\``;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const holder = this.typeOf(binding.collection);
|
|
72
|
+
if (holder && holder !== type) {
|
|
73
|
+
return `\`${binding.collection}\` holds \`${holder}\` records, not \`${type}\``;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
#load() {
|
|
78
|
+
return { ...(this.#store.get(this.#key) ?? {}) };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=bindings.js.map
|
package/dist/box.d.ts
CHANGED
|
@@ -1,66 +1,69 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare const
|
|
3
|
-
/**
|
|
4
|
-
export
|
|
1
|
+
import type { Bindings, HandlerRequest, SynthesisRequest, Use } from './runtime.ts';
|
|
2
|
+
export declare const HANDLERS = "handlers";
|
|
3
|
+
/** Asked when a handler wants content the world does not have yet. */
|
|
4
|
+
export type SynthesizeHook = (request: SynthesisRequest, context: {
|
|
5
|
+
db: string;
|
|
6
|
+
probe: boolean;
|
|
7
|
+
input: HandlerRequest;
|
|
8
|
+
}) => Promise<Record<string, unknown>[]>;
|
|
5
9
|
export interface BoxOptions {
|
|
6
|
-
/**
|
|
10
|
+
/** holds handlers/ */
|
|
7
11
|
root: string;
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
/** the live world every real request reads and writes */
|
|
13
|
+
db: string;
|
|
14
|
+
/** seconds one handler may run, not counting time spent waiting on an agent */
|
|
10
15
|
timeout?: number;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
synthesize?: SynthesizeHook;
|
|
17
|
+
/** where each type lives, as of now; asked on every run */
|
|
18
|
+
bindings?: () => Bindings;
|
|
19
|
+
}
|
|
20
|
+
export interface RunOptions {
|
|
21
|
+
/** another world to run against, e.g. a snapshot for judging */
|
|
22
|
+
db?: string;
|
|
23
|
+
/** the call is a judgement, not traffic: content is sampled, not written by an agent */
|
|
24
|
+
probe?: boolean;
|
|
25
|
+
/** bindings to run with instead of the current ones */
|
|
26
|
+
bindings?: Bindings;
|
|
15
27
|
}
|
|
16
28
|
/**
|
|
17
|
-
* The
|
|
18
|
-
*
|
|
19
|
-
* fault in the code: rewriting a working file cannot bring a container back.
|
|
29
|
+
* The worker could not be started at all. This says nothing about the handler
|
|
30
|
+
* and must never be fed back to a model as a fault in its code.
|
|
20
31
|
*/
|
|
21
|
-
export declare class
|
|
32
|
+
export declare class WorkerUnavailable extends Error {
|
|
22
33
|
readonly hint: string;
|
|
23
|
-
readonly container: string;
|
|
24
34
|
readonly stderr?: string;
|
|
25
35
|
constructor(message: string, opts: {
|
|
26
36
|
hint: string;
|
|
27
|
-
container: string;
|
|
28
37
|
stderr?: string;
|
|
29
38
|
});
|
|
30
39
|
}
|
|
31
40
|
export interface Outcome {
|
|
32
41
|
ok: boolean;
|
|
33
|
-
/**
|
|
42
|
+
/** the body the handler answered with */
|
|
34
43
|
value?: unknown;
|
|
44
|
+
/** set when the handler answered with `ctx.fail` rather than a body */
|
|
45
|
+
status?: number;
|
|
35
46
|
/** what went wrong, in the words the build loop feeds back to the model */
|
|
36
47
|
fault?: string;
|
|
37
48
|
stderr?: string;
|
|
49
|
+
/** the handler failed because the world agent did, not because of its code */
|
|
50
|
+
agentFault?: boolean;
|
|
51
|
+
/** the collections the run touched, and as which types */
|
|
52
|
+
used?: Use[];
|
|
38
53
|
durationMs: number;
|
|
39
54
|
}
|
|
40
55
|
export declare class Box {
|
|
41
56
|
#private;
|
|
42
57
|
readonly root: string;
|
|
58
|
+
readonly db: string;
|
|
59
|
+
synthesize?: SynthesizeHook;
|
|
43
60
|
constructor(opts: BoxOptions);
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Removes this process's container if a previous owner of the pid left one,
|
|
47
|
-
* and every gym container whose owning process is gone. Belt to
|
|
48
|
-
* `persist: false`'s braces: a hard kill never runs `dispose`, and the
|
|
49
|
-
* leftover is exactly the stale-mount trap above. A container whose owner
|
|
50
|
-
* is alive is another server's, and is never touched.
|
|
51
|
-
*/
|
|
52
|
-
fresh(): Promise<void>;
|
|
53
|
-
/** Host path of a generator's source file. */
|
|
61
|
+
/** Host path of a handler's source file. */
|
|
54
62
|
sourceOf(key: string): string;
|
|
55
63
|
write(key: string, source: string): Promise<void>;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
* not fill a disk with request envelopes.
|
|
60
|
-
*
|
|
61
|
-
* Throws `SandboxUnavailable` when the engine, not the generator, failed.
|
|
62
|
-
*/
|
|
63
|
-
run(key: string, input: unknown): Promise<Outcome>;
|
|
64
|
+
run(key: string, input: HandlerRequest, opts?: RunOptions): Promise<Outcome>;
|
|
65
|
+
/** Closes the worker's handle on a borrowed world, so its file can be deleted. */
|
|
66
|
+
release(db: string): void;
|
|
64
67
|
dispose(): Promise<void>;
|
|
65
68
|
}
|
|
66
69
|
//# sourceMappingURL=box.d.ts.map
|