@tangleai/agents 0.21.1 → 0.24.1
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/CHANGELOG.md +38 -0
- package/README.md +7 -6
- package/package.json +6 -6
- package/src/agent.d.ts +31 -33
- package/src/agent.js +522 -669
- package/src/index.d.ts +12 -11
- package/src/index.js +7 -12
- package/src/program-result.d.ts +7 -29
- package/src/program-result.js +16 -40
- package/src/program-session.d.ts +7 -13
- package/src/program-session.js +130 -108
- package/src/program-shape.d.ts +17 -17
- package/src/program-shape.js +47 -40
- package/src/program.d.ts +140 -108
- package/src/program.js +637 -712
- package/src/recursive.d.ts +65 -37
- package/src/recursive.js +223 -263
- package/src/refine.d.ts +49 -15
- package/src/refine.js +396 -445
- package/src/schemas/program.d.ts +25 -25
- package/src/schemas/program.js +88 -104
- package/src/toolbox.d.ts +27 -26
- package/src/toolbox.js +90 -124
package/src/program.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//@ts-check
|
|
2
1
|
/**
|
|
3
2
|
* The action language: compile it, then run it.
|
|
4
3
|
*
|
|
@@ -50,59 +49,46 @@
|
|
|
50
49
|
* cancellable).** `map` is the seam between the two halves, and it is
|
|
51
50
|
* the only one.
|
|
52
51
|
*/
|
|
53
|
-
|
|
54
52
|
import { CodedError } from '@jarenjs/core/errors';
|
|
55
53
|
import { excerpt } from '@jarenjs/core/chunk';
|
|
56
54
|
import { mapConcurrent } from '@jarenjs/core/async';
|
|
57
55
|
import { JarenValidator } from '@jarenjs/validate';
|
|
58
|
-
|
|
59
|
-
import { MAX_PROGRAM_CHARS, NAME_PATTERN, PROGRAM_SCHEMA, programSchema } from './schemas/program.js';
|
|
56
|
+
import { MAX_PROGRAM_CHARS, NAME_PATTERN, PROGRAM_SCHEMA, programSchema } from "./schemas/program.js";
|
|
60
57
|
import { checkOutcome } from '@jarenjs/core/check';
|
|
61
58
|
import { unfence } from '@tangleai/models/structured';
|
|
62
|
-
import { recursiveShape, recursiveSchema, recursiveItems } from
|
|
59
|
+
import { recursiveShape, recursiveSchema, recursiveItems } from "./program-shape.js";
|
|
63
60
|
import { createRoutedClient } from '@tangleai/models/routing';
|
|
64
|
-
|
|
65
|
-
export { readProgramAnswer } from './program-result.js';
|
|
66
|
-
/** @typedef {import('./program-result.js').ProgramRunResult} ProgramRunResult */
|
|
67
|
-
|
|
61
|
+
export { readProgramAnswer } from "./program-result.js";
|
|
68
62
|
/** How much of one piece a sub-call is shown. The sub-call is the only
|
|
69
63
|
* place content reaches a model at all, and it sees ONE piece — a cap
|
|
70
64
|
* here is the difference between a bounded fan-out and the corpus
|
|
71
65
|
* arriving in a different envelope. */
|
|
72
66
|
const SUBCALL_CHARS = 8000;
|
|
73
|
-
|
|
74
67
|
/** Sub-calls one run may make, whatever the corpus. A hard ceiling, not
|
|
75
68
|
* a hint: depth × fan-out is multiplicative and a runaway map is a bill. */
|
|
76
69
|
const MAX_SUBCALLS = 64;
|
|
77
|
-
|
|
78
70
|
/** Sub-calls in flight at once. Four is the paper's limitation fixed —
|
|
79
71
|
* it reports its own sub-calls are sequential and slow — and low enough
|
|
80
72
|
* that a free-tier provider does not answer with 429s. */
|
|
81
73
|
const MAX_CONCURRENT = 4;
|
|
82
|
-
|
|
83
74
|
/** The characters a reduce may assemble from its map's results. The
|
|
84
75
|
* reduction is small BY CONSTRUCTION (that is what a map is for), so
|
|
85
76
|
* this cap is a tripwire on a program that mapped identity over a
|
|
86
77
|
* corpus, not a working limit. */
|
|
87
78
|
const MAX_REDUCE_CHARS = 200000;
|
|
88
|
-
|
|
89
79
|
/** How much of the answer slot the final step returns unasked. */
|
|
90
80
|
const ANSWER_CHARS = 2000;
|
|
91
|
-
|
|
92
81
|
/** How much of a failed reply is kept beside its error. */
|
|
93
82
|
const RAW_EXCERPT = 200;
|
|
94
|
-
|
|
95
83
|
/** The namespace a run's own results live under, kept apart from the
|
|
96
84
|
* corpus so a digest can tell working notes from the thing being worked
|
|
97
85
|
* on — and so a re-run overwrites rather than accumulating. */
|
|
98
86
|
const RESULT_PREFIX = 'program/';
|
|
99
|
-
|
|
100
87
|
const NAME_RE = new RegExp(NAME_PATTERN);
|
|
101
|
-
|
|
102
88
|
/**
|
|
103
89
|
* A program that will not compile.
|
|
104
90
|
*
|
|
105
|
-
* Thrown, unlike `refine.
|
|
91
|
+
* Thrown, unlike `refine.ts`'s AI01xx records, because this is a
|
|
106
92
|
* COMPILER and the suite's compilers throw coded errors with a
|
|
107
93
|
* `docPath` — which is what makes the documented two-line gate adapter
|
|
108
94
|
* (`try { compile(doc) } catch (e) { … }`) work here exactly as it does
|
|
@@ -125,35 +111,30 @@ const NAME_RE = new RegExp(NAME_PATTERN);
|
|
|
125
111
|
* A query that does not compile keeps the QUERY engine's own code
|
|
126
112
|
* (`JQ0002`, …) and its pointer is rebased onto the program document,
|
|
127
113
|
* so the model is told which step and which member — the same posture
|
|
128
|
-
* `refine.
|
|
114
|
+
* `refine.ts` takes with the patch engine's codes.
|
|
129
115
|
*/
|
|
130
116
|
export class ProgramError extends CodedError {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
117
|
+
/**
|
|
118
|
+
* @param code - 'AI0200' … 'AI0209'
|
|
119
|
+
* @param reason - the bare reason
|
|
120
|
+
* @param [docPath] - JSON Pointer into the program document
|
|
121
|
+
*/
|
|
122
|
+
constructor(code, reason, docPath) {
|
|
123
|
+
super('ProgramError', code, reason, docPath);
|
|
124
|
+
}
|
|
139
125
|
}
|
|
140
|
-
|
|
141
126
|
/**
|
|
142
127
|
* One error in the shape a repair prompt carries. A rebased query error
|
|
143
128
|
* keeps its engine code and gains the step it came from.
|
|
144
|
-
* @param {any} err
|
|
145
|
-
* @returns {{ code: string, docPath: string, message: string }}
|
|
146
129
|
*/
|
|
147
130
|
function errorRecord(err) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
131
|
+
return {
|
|
132
|
+
code: err?.code ?? 'AI0200',
|
|
133
|
+
docPath: err?.docPath ?? '',
|
|
134
|
+
message: err?.reason ?? err?.message ?? String(err),
|
|
135
|
+
};
|
|
153
136
|
}
|
|
154
|
-
|
|
155
137
|
//#region the compiler
|
|
156
|
-
|
|
157
138
|
/**
|
|
158
139
|
* Compile a program document.
|
|
159
140
|
*
|
|
@@ -163,9 +144,8 @@ function errorRecord(err) {
|
|
|
163
144
|
* model, so a compile is cheap enough to run on every authored candidate
|
|
164
145
|
* — which is exactly what makes it usable as a decoding gate.
|
|
165
146
|
*
|
|
166
|
-
* @param
|
|
167
|
-
* @param
|
|
168
|
-
* known?: Iterable<string>, recursive?: boolean, analyzeQuery?: any, annotateTypes?: any }} [options]
|
|
147
|
+
* @param doc - the program document
|
|
148
|
+
* @param [options]
|
|
169
149
|
* - `compileQuery` is the D3 seam. Absent, a program using `select` or
|
|
170
150
|
* `reduce` is refused with AI0206 rather than half-compiled.
|
|
171
151
|
* - `known` is what the environment already holds. Given, a `from`
|
|
@@ -174,220 +154,180 @@ function errorRecord(err) {
|
|
|
174
154
|
* reason a compile gate catches what a schema cannot. Absent, only
|
|
175
155
|
* bindings are resolved: nothing else can be checked without an
|
|
176
156
|
* environment, and inventing an error would be worse than saying so.
|
|
177
|
-
* @returns
|
|
178
|
-
* bindings: string[], chars: number }}
|
|
157
|
+
* @returns
|
|
179
158
|
* @throws {ProgramError}
|
|
180
159
|
*/
|
|
181
160
|
export function compileProgram(doc, options = {}) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
throw new ProgramError('AI0205',
|
|
193
|
-
`the program is ${text.length} characters, over the ${MAX_PROGRAM_CHARS} cap —`
|
|
194
|
-
+ ' a plan naming slots is small; one carrying content is not', '');
|
|
195
|
-
}
|
|
196
|
-
const shape = checkOutcome(new JarenValidator({ skipErrors: false, collectErrors: true }).compile(PROGRAM_SCHEMA)(doc));
|
|
197
|
-
if (!shape.valid) throw new ProgramError('AI0200', 'the document violates the program schema', shape.errors[0]?.instancePath ?? '');
|
|
198
|
-
|
|
199
|
-
/** binding name → the step index that produced it and what kind it is */
|
|
200
|
-
const bound = new Map();
|
|
201
|
-
const steps = [];
|
|
202
|
-
|
|
203
|
-
for (let i = 0; i < doc.steps.length; i++) {
|
|
204
|
-
const raw = doc.steps[i];
|
|
205
|
-
const at = `/steps/${i}`;
|
|
206
|
-
const op = raw?.op;
|
|
207
|
-
const from = raw?.from;
|
|
208
|
-
|
|
209
|
-
if (typeof from !== 'string' || from === '')
|
|
210
|
-
throw new ProgramError('AI0200', `step ${i} has no "from"`, `${at}/from`);
|
|
211
|
-
|
|
212
|
-
// resolve the input: a binding first, then the environment
|
|
213
|
-
const source = bound.get(from);
|
|
214
|
-
if (source === undefined && known !== null && !known.has(from)) {
|
|
215
|
-
// the names a model could plausibly have meant: this program's own
|
|
216
|
-
// bindings, then the slots a digest would have shown it — never the
|
|
217
|
-
// runner's `program/` scratch and never a derived chunk address,
|
|
218
|
-
// both of which are noise in a repair prompt
|
|
219
|
-
const names = [...bound.keys(),
|
|
220
|
-
...[...known].filter((n) => !n.startsWith(RESULT_PREFIX) && !n.includes('#'))]
|
|
221
|
-
.slice(0, 8);
|
|
222
|
-
throw new ProgramError('AI0201',
|
|
223
|
-
`"${from}" is not a slot and no earlier step produced it`
|
|
224
|
-
+ (names.length === 0 ? '' : ` — available: ${names.join(', ')}`),
|
|
225
|
-
`${at}/from`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// `chunk` and `map` bind a FAMILY of pieces, and two steps read one
|
|
229
|
-
// slot rather than a family: a select parses a document, an answer
|
|
230
|
-
// reads a text. Catching it here is worth a code of its own —
|
|
231
|
-
// "reduce it first" is a repair a model lands, where the runtime
|
|
232
|
-
// error it would otherwise get ("no slot program/found/") points at
|
|
233
|
-
// an address the model never wrote
|
|
234
|
-
if ((op === 'answer' || op === 'select') && (source?.op === 'chunk' || source?.op === 'map')) {
|
|
235
|
-
throw new ProgramError('AI0207',
|
|
236
|
-
`${op} reads one slot and "${from}" is a ${source.op} — every piece of it. `
|
|
237
|
-
+ (source.op === 'map'
|
|
238
|
-
? 'Combine them with a reduce step and read that.'
|
|
239
|
-
: 'Map over it, then reduce, and read that.'),
|
|
240
|
-
`${at}/from`);
|
|
161
|
+
const compileQuery = typeof options.compileQuery === 'function' ? options.compileQuery : null;
|
|
162
|
+
const known = options.known === undefined ? null : new Set(options.known);
|
|
163
|
+
if (doc === null || typeof doc !== 'object' || !Array.isArray(doc.steps))
|
|
164
|
+
throw new ProgramError('AI0200', 'a program is an object with a steps array', '');
|
|
165
|
+
if (doc.steps.length === 0)
|
|
166
|
+
throw new ProgramError('AI0200', 'a program needs at least one step', '/steps');
|
|
167
|
+
const text = JSON.stringify(doc);
|
|
168
|
+
if (text.length > MAX_PROGRAM_CHARS) {
|
|
169
|
+
throw new ProgramError('AI0205', `the program is ${text.length} characters, over the ${MAX_PROGRAM_CHARS} cap —`
|
|
170
|
+
+ ' a plan naming slots is small; one carrying content is not', '');
|
|
241
171
|
}
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
throw new ProgramError('AI0206',
|
|
268
|
-
`${op} needs the compileQuery seam — inject compileJsonQuery from @jarenjs/json/query`,
|
|
269
|
-
`${at}/query`);
|
|
270
|
-
}
|
|
271
|
-
try {
|
|
272
|
-
// compiled once, here, and kept: the house two-stage rule, and
|
|
273
|
-
// it is also what lets the gate reject a bad query before a
|
|
274
|
-
// single slot has been touched
|
|
275
|
-
step.run = compileQuery(raw.query);
|
|
276
|
-
}
|
|
277
|
-
catch (err) {
|
|
278
|
-
const e = /** @type {any} */ (err);
|
|
279
|
-
// the engine's own code and its pointer, rebased onto the step:
|
|
280
|
-
// "which member of which step" is the difference between a
|
|
281
|
-
// repair round that converges and one that flails
|
|
282
|
-
throw new ProgramError(e?.code ?? 'AI0200',
|
|
283
|
-
e?.reason ?? e?.message ?? 'the query does not compile',
|
|
284
|
-
`${at}/query${e?.docPath ?? ''}`);
|
|
285
|
-
}
|
|
286
|
-
step.query = raw.query;
|
|
287
|
-
if (options.analyzeQuery && options.annotateTypes) {
|
|
288
|
-
try { step.outputType = options.annotateTypes(options.analyzeQuery(raw.query)).root; }
|
|
289
|
-
catch { step.outputType = undefined; }
|
|
290
|
-
}
|
|
291
|
-
if (raw.outputSchema !== undefined) {
|
|
292
|
-
try { step.validateOutput = new JarenValidator({ skipErrors: false, collectErrors: true }).compile(raw.outputSchema); }
|
|
293
|
-
catch { throw new ProgramError('AI0208', 'invalid outputSchema', `${at}/outputSchema`); }
|
|
294
|
-
}
|
|
295
|
-
if (options.recursive === true && op === 'reduce') {
|
|
296
|
-
const inferred = recursiveShape(step.outputType);
|
|
297
|
-
if (inferred === 'incompatible' || (inferred === 'unknown' && !recursiveSchema(raw.outputSchema))) {
|
|
298
|
-
throw new ProgramError('AI0208', `reduce ${as} infers ${inferred}; required {slot:string,value:any} item or sequence. Unknown inference needs outputSchema.`, `${at}/query`);
|
|
172
|
+
const shape = checkOutcome(new JarenValidator({ skipErrors: false, collectErrors: true }).compile(PROGRAM_SCHEMA)(doc));
|
|
173
|
+
if (!shape.valid)
|
|
174
|
+
throw new ProgramError('AI0200', 'the document violates the program schema', shape.errors[0]?.instancePath ?? '');
|
|
175
|
+
/** binding name → the step index that produced it and what kind it is */
|
|
176
|
+
const bound = new Map();
|
|
177
|
+
const steps = [];
|
|
178
|
+
for (let i = 0; i < doc.steps.length; i++) {
|
|
179
|
+
const raw = doc.steps[i];
|
|
180
|
+
const at = `/steps/${i}`;
|
|
181
|
+
const op = raw?.op;
|
|
182
|
+
const from = raw?.from;
|
|
183
|
+
if (typeof from !== 'string' || from === '')
|
|
184
|
+
throw new ProgramError('AI0200', `step ${i} has no "from"`, `${at}/from`);
|
|
185
|
+
// resolve the input: a binding first, then the environment
|
|
186
|
+
const source = bound.get(from);
|
|
187
|
+
if (source === undefined && known !== null && !known.has(from)) {
|
|
188
|
+
// the names a model could plausibly have meant: this program's own
|
|
189
|
+
// bindings, then the slots a digest would have shown it — never the
|
|
190
|
+
// runner's `program/` scratch and never a derived chunk address,
|
|
191
|
+
// both of which are noise in a repair prompt
|
|
192
|
+
const names = [...bound.keys(),
|
|
193
|
+
...[...known].filter((n) => !n.startsWith(RESULT_PREFIX) && !n.includes('#'))]
|
|
194
|
+
.slice(0, 8);
|
|
195
|
+
throw new ProgramError('AI0201', `"${from}" is not a slot and no earlier step produced it`
|
|
196
|
+
+ (names.length === 0 ? '' : ` — available: ${names.join(', ')}`), `${at}/from`);
|
|
299
197
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
198
|
+
// `chunk` and `map` bind a FAMILY of pieces, and two steps read one
|
|
199
|
+
// slot rather than a family: a select parses a document, an answer
|
|
200
|
+
// reads a text. Catching it here is worth a code of its own —
|
|
201
|
+
// "reduce it first" is a repair a model lands, where the runtime
|
|
202
|
+
// error it would otherwise get ("no slot program/found/") points at
|
|
203
|
+
// an address the model never wrote
|
|
204
|
+
if ((op === 'answer' || op === 'select') && (source?.op === 'chunk' || source?.op === 'map')) {
|
|
205
|
+
throw new ProgramError('AI0207', `${op} reads one slot and "${from}" is a ${source.op} — every piece of it. `
|
|
206
|
+
+ (source.op === 'map'
|
|
207
|
+
? 'Combine them with a reduce step and read that.'
|
|
208
|
+
: 'Map over it, then reduce, and read that.'), `${at}/from`);
|
|
209
|
+
}
|
|
210
|
+
if (op === 'answer') {
|
|
211
|
+
if (i !== doc.steps.length - 1)
|
|
212
|
+
throw new ProgramError('AI0203', 'answer is the last step of a program', `${at}/op`);
|
|
213
|
+
steps.push({ op, from, index: i });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const as = raw?.as;
|
|
217
|
+
if (typeof as !== 'string' || !NAME_RE.test(as))
|
|
218
|
+
throw new ProgramError('AI0200', `step ${i} has no usable "as"`, `${at}/as`);
|
|
219
|
+
if (bound.has(as))
|
|
220
|
+
throw new ProgramError('AI0202', `"${as}" is already the name of step ${bound.get(as).index}`, `${at}/as`);
|
|
221
|
+
const step = { op, from, as, index: i };
|
|
222
|
+
if (op === 'reduce' && source?.op !== 'map') {
|
|
223
|
+
throw new ProgramError('AI0204', `reduce combines a map's results; "${from}" is ${source === undefined ? 'a slot' : `a ${source.op}`}`, `${at}/from`);
|
|
224
|
+
}
|
|
225
|
+
if (op === 'select' || op === 'reduce') {
|
|
226
|
+
if (compileQuery === null) {
|
|
227
|
+
throw new ProgramError('AI0206', `${op} needs the compileQuery seam — inject compileJsonQuery from @jarenjs/json/query`, `${at}/query`);
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
// compiled once, here, and kept: the house two-stage rule, and
|
|
231
|
+
// it is also what lets the gate reject a bad query before a
|
|
232
|
+
// single slot has been touched
|
|
233
|
+
step.run = compileQuery(raw.query);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
const e = err;
|
|
237
|
+
// the engine's own code and its pointer, rebased onto the step:
|
|
238
|
+
// "which member of which step" is the difference between a
|
|
239
|
+
// repair round that converges and one that flails
|
|
240
|
+
throw new ProgramError(e?.code ?? 'AI0200', e?.reason ?? e?.message ?? 'the query does not compile', `${at}/query${e?.docPath ?? ''}`);
|
|
241
|
+
}
|
|
242
|
+
step.query = raw.query;
|
|
243
|
+
if (options.analyzeQuery && options.annotateTypes) {
|
|
244
|
+
try {
|
|
245
|
+
step.outputType = options.annotateTypes(options.analyzeQuery(raw.query)).root;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
step.outputType = undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (raw.outputSchema !== undefined) {
|
|
252
|
+
try {
|
|
253
|
+
step.validateOutput = new JarenValidator({ skipErrors: false, collectErrors: true }).compile(raw.outputSchema);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
throw new ProgramError('AI0208', 'invalid outputSchema', `${at}/outputSchema`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (options.recursive === true && op === 'reduce') {
|
|
260
|
+
const inferred = recursiveShape(step.outputType);
|
|
261
|
+
if (inferred === 'incompatible' || (inferred === 'unknown' && !recursiveSchema(raw.outputSchema))) {
|
|
262
|
+
throw new ProgramError('AI0208', `reduce ${as} infers ${inferred}; required {slot:string,value:any} item or sequence. Unknown inference needs outputSchema.`, `${at}/query`);
|
|
263
|
+
}
|
|
264
|
+
step.recursive = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (op === 'map') {
|
|
268
|
+
if (typeof raw.prompt !== 'string' || raw.prompt.trim() === '')
|
|
269
|
+
throw new ProgramError('AI0200', 'a map needs a prompt', `${at}/prompt`);
|
|
270
|
+
step.prompt = raw.prompt;
|
|
271
|
+
}
|
|
272
|
+
if (op === 'grep') {
|
|
273
|
+
if (typeof raw.pattern !== 'string' || raw.pattern === '')
|
|
274
|
+
throw new ProgramError('AI0200', 'a grep needs a pattern', `${at}/pattern`);
|
|
275
|
+
step.pattern = raw.pattern;
|
|
276
|
+
step.flags = raw.flags;
|
|
277
|
+
step.limit = raw.limit;
|
|
278
|
+
}
|
|
279
|
+
if (op === 'chunk') {
|
|
280
|
+
step.strategy = raw.strategy;
|
|
281
|
+
step.size = raw.size;
|
|
282
|
+
}
|
|
283
|
+
bound.set(as, step);
|
|
284
|
+
steps.push(step);
|
|
321
285
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
from: last.from,
|
|
339
|
-
chars: Math.min(doc.steps[last.index].chars ?? ANSWER_CHARS, ANSWER_CHARS * 4),
|
|
340
|
-
},
|
|
341
|
-
bindings: [...bound.keys()],
|
|
342
|
-
chars: text.length,
|
|
343
|
-
};
|
|
286
|
+
const last = steps[steps.length - 1];
|
|
287
|
+
if (last.op !== 'answer')
|
|
288
|
+
throw new ProgramError('AI0203', 'a program ends with an answer step', `/steps/${last.index}/op`);
|
|
289
|
+
if (steps.filter((s) => s.op === 'answer').length > 1)
|
|
290
|
+
throw new ProgramError('AI0203', 'a program has exactly one answer step', '/steps');
|
|
291
|
+
if (options.recursive === true && !bound.get(last.from)?.recursive)
|
|
292
|
+
throw new ProgramError('AI0208', 'a recursive answer must read a shape-checked reduce', `/steps/${last.index}/from`);
|
|
293
|
+
return {
|
|
294
|
+
steps,
|
|
295
|
+
answer: {
|
|
296
|
+
from: last.from,
|
|
297
|
+
chars: Math.min(doc.steps[last.index].chars ?? ANSWER_CHARS, ANSWER_CHARS * 4),
|
|
298
|
+
},
|
|
299
|
+
bindings: [...bound.keys()],
|
|
300
|
+
chars: text.length,
|
|
301
|
+
};
|
|
344
302
|
}
|
|
345
|
-
|
|
346
303
|
/**
|
|
347
304
|
* The compile gate, ready for `createStructuredOutput({ gate })`.
|
|
348
305
|
*
|
|
349
306
|
* One implementation of "does this program compile", used by the
|
|
350
307
|
* authoring path and by the runner, so a program that authored cleanly
|
|
351
308
|
* cannot fail differently when it runs.
|
|
352
|
-
* @param
|
|
353
|
-
* @returns {(doc: any) => true | { valid: false, errors: any[] }}
|
|
309
|
+
* @param [options]
|
|
354
310
|
*/
|
|
355
311
|
export function programGate(options = {}) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
312
|
+
return (doc) => {
|
|
313
|
+
try {
|
|
314
|
+
compileProgram(doc, options);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
return { valid: false, errors: [errorRecord(err)] };
|
|
319
|
+
}
|
|
320
|
+
};
|
|
365
321
|
}
|
|
366
|
-
|
|
367
322
|
//#endregion
|
|
368
|
-
|
|
369
323
|
//#region the runner
|
|
370
|
-
|
|
371
324
|
/** The address a step's result is stored under, derived from its binding. */
|
|
372
325
|
const resultSlot = (as) => `${RESULT_PREFIX}${as}`;
|
|
373
|
-
|
|
374
326
|
/** The family a map's per-piece results live under. */
|
|
375
327
|
const mapFamily = (as) => `${RESULT_PREFIX}${as}/`;
|
|
376
|
-
|
|
377
328
|
/**
|
|
378
329
|
* Create a runner over an environment.
|
|
379
330
|
*
|
|
380
|
-
* @param {{ environment: any,
|
|
381
|
-
* client?: { complete: (request: any) => Promise<any> },
|
|
382
|
-
* compileQuery?: any,
|
|
383
|
-
* recursive?: boolean, analyzeQuery?: any, annotateTypes?: any,
|
|
384
|
-
* selectModel?: any, limits?: any, onRoute?: any, depth?: number,
|
|
385
|
-
* model?: string,
|
|
386
|
-
* maxSubcalls?: number, maxConcurrentSubcalls?: number,
|
|
387
|
-
* subcallChars?: number, maxReduceChars?: number,
|
|
388
|
-
* sequential?: boolean,
|
|
389
|
-
* subcall?: (name: string, prompt: string, signal?: AbortSignal, index?: number) => Promise<any>,
|
|
390
|
-
* account?: { reserve: Function, settle: Function, stop: () => string | null } }} options
|
|
391
331
|
* - `client` is only needed by `map`; a program without one is a
|
|
392
332
|
* perfectly good program (chunk / grep / select / stat / answer are
|
|
393
333
|
* model-free), and running a `map` without a client is a stated
|
|
@@ -404,462 +344,449 @@ const mapFamily = (as) => `${RESULT_PREFIX}${as}/`;
|
|
|
404
344
|
* - `account` is a budget shared with everything else in the run,
|
|
405
345
|
* including other depths. Checked before each sub-call and charged
|
|
406
346
|
* by it, so a tree cannot outspend the sum of its branches.
|
|
407
|
-
* @returns {{ run: (doc: any, hooks?: { signal?: AbortSignal }) => Promise<ProgramRunResult> }}
|
|
408
347
|
*/
|
|
409
348
|
export function createProgramRunner(options) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
return compileProgram(doc, { compileQuery, known: names, recursive: options.recursive,
|
|
453
|
-
analyzeQuery: options.analyzeQuery, annotateTypes: options.annotateTypes });
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* The slots one binding stands for, in address order.
|
|
458
|
-
*
|
|
459
|
-
* Three cases, and the third is why `grep` is worth having: a chunk or
|
|
460
|
-
* a map binding is a family; a grep binding is the set of slots it
|
|
461
|
-
* MATCHED, so `map` over a grep visits the matching pieces and not the
|
|
462
|
-
* match list. That is the narrowing the whole design is for — grep to
|
|
463
|
-
* find which forty of four hundred pieces are relevant, then spend
|
|
464
|
-
* sub-calls on those forty only. Anything else is one slot.
|
|
465
|
-
* @param {any} target - `{ family }`, `{ matches }` or `{ slot }`
|
|
466
|
-
* @returns {Promise<string[]>}
|
|
467
|
-
*/
|
|
468
|
-
async function membersOf(target) {
|
|
469
|
-
if (target.matches !== undefined) {
|
|
470
|
-
const stored = await environment.ledger.readSlot(target.slot);
|
|
471
|
-
/** @type {any} */
|
|
472
|
-
let listing;
|
|
473
|
-
try { listing = JSON.parse(String(stored ?? '{}')); }
|
|
474
|
-
catch { return [target.slot]; }
|
|
475
|
-
const names = [...new Set((listing.matches ?? []).map((m) => m.slot))];
|
|
476
|
-
return /** @type {string[]} */ (names);
|
|
477
|
-
}
|
|
478
|
-
if (target.slot !== undefined) return [target.slot];
|
|
479
|
-
const slots = await environment.ledger.listSlots();
|
|
480
|
-
return slots
|
|
481
|
-
.filter((s) => s.name.startsWith(target.family))
|
|
482
|
-
.map((s) => s.name)
|
|
483
|
-
.sort((a, b) => {
|
|
484
|
-
// numeric on the trailing index, so piece 10 follows piece 9 and
|
|
485
|
-
// a reduce sees its map's results in the corpus's own order
|
|
486
|
-
const ai = Number(a.slice(a.lastIndexOf('/') + 1));
|
|
487
|
-
const bi = Number(b.slice(b.lastIndexOf('/') + 1));
|
|
488
|
-
return Number.isNaN(ai) || Number.isNaN(bi) ? a.localeCompare(b) : ai - bi;
|
|
489
|
-
}).slice(0, target.count ?? Infinity);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* One sub-call: the model sees ONE piece and answers with one JSON
|
|
494
|
-
* value. Failures are recorded, never thrown — §3.
|
|
495
|
-
* @param {string} name - the piece's slot
|
|
496
|
-
* @param {string} prompt
|
|
497
|
-
* @param {AbortSignal} [signal]
|
|
498
|
-
*/
|
|
499
|
-
async function subcall(name, prompt, signal) {
|
|
500
|
-
const piece = await environment.read(name, { chars: subcallChars });
|
|
501
|
-
if (piece.error !== undefined) return { slot: name, error: piece.error };
|
|
502
|
-
/** @type {any} */
|
|
503
|
-
let reply;
|
|
504
|
-
// the turn is taken before the call, not after it: four sub-calls
|
|
505
|
-
// launched together would otherwise each see the same unspent budget
|
|
506
|
-
try {
|
|
507
|
-
reply = await createRoutedClient({ client, selectModel: options.selectModel,
|
|
508
|
-
limits: options.limits, onRoute: options.onRoute, account },
|
|
509
|
-
{ purpose: 'subcall', grammar: 'program', depth: options.depth ?? 0 }).complete({
|
|
510
|
-
// `stream` is deliberately NOT set. A sub-call has no UI to
|
|
511
|
-
// stream to, so `stream: false` looks right — and it is the
|
|
512
|
-
// exact request shape this package's own benchmark measured
|
|
513
|
-
// hanging past a 300 s deadline on the cheap tier and answering
|
|
514
|
-
// in seconds when streamed. Leaving it to the client's default
|
|
515
|
-
// (streaming) costs nothing here, because `complete` returns the
|
|
516
|
-
// accumulated message either way.
|
|
517
|
-
signal,
|
|
518
|
-
messages: [
|
|
519
|
-
{
|
|
520
|
-
role: 'system',
|
|
521
|
-
content: 'You are given ONE piece of a larger corpus and a question about it.'
|
|
522
|
-
+ ' Answer with a single JSON value and nothing else — no prose, no code fences.'
|
|
523
|
-
+ ' If the piece does not contain what was asked for, answer with null.',
|
|
524
|
-
},
|
|
525
|
-
{ role: 'user', content: `${prompt}\n\n--- piece ${name} ---\n${piece.text}` },
|
|
526
|
-
],
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
catch (err) {
|
|
530
|
-
if (signal?.aborted === true) throw err;
|
|
531
|
-
return { slot: name, error: `the sub-call failed: ${/** @type {Error} */ (err).message}` };
|
|
532
|
-
}
|
|
533
|
-
const raw = String(reply?.message?.content ?? '');
|
|
534
|
-
// settled here rather than by the caller: this is where the call
|
|
535
|
-
// actually happened, and an account that only saw the calls someone
|
|
536
|
-
// remembered to report is not a bound
|
|
537
|
-
try {
|
|
538
|
-
return { slot: name, value: JSON.parse(unfence(raw)) };
|
|
539
|
-
}
|
|
540
|
-
catch (err) {
|
|
541
|
-
return {
|
|
542
|
-
slot: name,
|
|
543
|
-
error: `the reply is not JSON: ${/** @type {Error} */ (err).message}`,
|
|
544
|
-
raw: excerpt(raw, RAW_EXCERPT),
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Execute a compiled program.
|
|
551
|
-
* @param {any} doc
|
|
552
|
-
* @param {{ signal?: AbortSignal }} [hooks]
|
|
553
|
-
* @returns {Promise<ProgramRunResult>}
|
|
554
|
-
*/
|
|
555
|
-
async function run(doc, hooks = {}) {
|
|
556
|
-
const signal = hooks.signal;
|
|
557
|
-
const started = Date.now();
|
|
558
|
-
|
|
559
|
-
/** @type {any} */
|
|
560
|
-
let plan;
|
|
561
|
-
try {
|
|
562
|
-
plan = await compileHere(doc);
|
|
563
|
-
}
|
|
564
|
-
catch (err) {
|
|
565
|
-
// NOTHING has run: the compile reads no slot and writes none, so a
|
|
566
|
-
// rejected program leaves the environment exactly as it was
|
|
567
|
-
return { ok: false, ran: 0, steps: [], subcalls: 0, failed: 0, concurrency,
|
|
568
|
-
answer: null, ms: Date.now() - started,
|
|
569
|
-
error: 'the program does not compile', errors: [errorRecord(err)] };
|
|
349
|
+
const { environment } = options;
|
|
350
|
+
if (environment === null || typeof environment !== 'object')
|
|
351
|
+
throw new TypeError('createProgramRunner needs an environment');
|
|
352
|
+
const client = options.client ?? null;
|
|
353
|
+
const compileQuery = options.compileQuery ?? null;
|
|
354
|
+
const maxSubcalls = options.maxSubcalls ?? MAX_SUBCALLS;
|
|
355
|
+
const concurrency = options.sequential === true
|
|
356
|
+
? 1
|
|
357
|
+
: Math.max(1, options.maxConcurrentSubcalls ?? MAX_CONCURRENT);
|
|
358
|
+
const subcallChars = options.subcallChars ?? SUBCALL_CHARS;
|
|
359
|
+
const maxReduceChars = options.maxReduceChars ?? MAX_REDUCE_CHARS;
|
|
360
|
+
const account = options.account ?? null;
|
|
361
|
+
/**
|
|
362
|
+
* The shape gate, compiled once.
|
|
363
|
+
*
|
|
364
|
+
* The runner validates the document as well as compiling it, so the
|
|
365
|
+
* two ways a program arrives — authored under constrained decoding, or
|
|
366
|
+
* hand-written by a caller — meet the same two gates in the same
|
|
367
|
+
* order. Without this a hand-written program could carry a member the
|
|
368
|
+
* grammar forbids (the D2 rule that no step inlines content is
|
|
369
|
+
* `additionalProperties: false` on every step), and the restriction
|
|
370
|
+
* would hold only for the path that happened to go through
|
|
371
|
+
* `createStructuredOutput`.
|
|
372
|
+
*
|
|
373
|
+
* The seam-free schema on purpose: `query` is unconstrained here and
|
|
374
|
+
* the compile gate judges it, so a runner needs no grammar registered
|
|
375
|
+
* to check a shape (D3).
|
|
376
|
+
*/
|
|
377
|
+
const shape = new JarenValidator({ skipErrors: false, collectErrors: true })
|
|
378
|
+
.compile(PROGRAM_SCHEMA);
|
|
379
|
+
/** Compile against what the environment actually holds right now —
|
|
380
|
+
* which is what turns AI0201 from a shape check into a real one. */
|
|
381
|
+
async function compileHere(doc) {
|
|
382
|
+
const outcome = checkOutcome(shape(doc));
|
|
383
|
+
if (!outcome.valid) {
|
|
384
|
+
throw new ProgramError('AI0200', outcome.errors[0]?.message ?? 'the document is not a program', outcome.errors[0]?.instancePath ?? '');
|
|
385
|
+
}
|
|
386
|
+
const names = (await environment.ledger.listSlots()).map((s) => s.name);
|
|
387
|
+
return compileProgram(doc, {
|
|
388
|
+
compileQuery, known: names, recursive: options.recursive,
|
|
389
|
+
analyzeQuery: options.analyzeQuery, annotateTypes: options.annotateTypes
|
|
390
|
+
});
|
|
570
391
|
}
|
|
571
|
-
|
|
572
|
-
/** binding → `{ slot }` or `{ family, count }` */
|
|
573
|
-
const bindings = new Map();
|
|
574
|
-
/** @type {any[]} */
|
|
575
|
-
const report = [];
|
|
576
|
-
let subcalls = 0;
|
|
577
|
-
let failed = 0;
|
|
578
|
-
/** @type {string|null} */
|
|
579
|
-
let stopped = null;
|
|
580
|
-
|
|
581
|
-
/** Where a step's `from` points, as an address the environment knows. */
|
|
582
|
-
const address = (from) => {
|
|
583
|
-
const binding = bindings.get(from);
|
|
584
|
-
if (binding === undefined) return { slot: from };
|
|
585
|
-
return binding;
|
|
586
|
-
};
|
|
587
|
-
const addressOf = (from) => {
|
|
588
|
-
const target = address(from);
|
|
589
|
-
return target.slot ?? target.family;
|
|
590
|
-
};
|
|
591
|
-
|
|
592
392
|
/**
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
393
|
+
* The slots one binding stands for, in address order.
|
|
394
|
+
*
|
|
395
|
+
* Three cases, and the third is why `grep` is worth having: a chunk or
|
|
396
|
+
* a map binding is a family; a grep binding is the set of slots it
|
|
397
|
+
* MATCHED, so `map` over a grep visits the matching pieces and not the
|
|
398
|
+
* match list. That is the narrowing the whole design is for — grep to
|
|
399
|
+
* find which forty of four hundred pieces are relevant, then spend
|
|
400
|
+
* sub-calls on those forty only. Anything else is one slot.
|
|
401
|
+
* @param target - `{ family }`, `{ matches }` or `{ slot }`
|
|
599
402
|
*/
|
|
600
|
-
async function
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
403
|
+
async function membersOf(target) {
|
|
404
|
+
if (target.matches !== undefined) {
|
|
405
|
+
const stored = await environment.ledger.readSlot(target.slot);
|
|
406
|
+
let listing;
|
|
407
|
+
try {
|
|
408
|
+
listing = JSON.parse(String(stored ?? '{}'));
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return [target.slot];
|
|
412
|
+
}
|
|
413
|
+
const names = [...new Set((listing.matches ?? []).map((m) => m.slot))];
|
|
414
|
+
return names;
|
|
415
|
+
}
|
|
416
|
+
if (target.slot !== undefined)
|
|
417
|
+
return [target.slot];
|
|
418
|
+
const slots = await environment.ledger.listSlots();
|
|
419
|
+
return slots
|
|
420
|
+
.filter((s) => s.name.startsWith(target.family))
|
|
421
|
+
.map((s) => s.name)
|
|
422
|
+
.sort((a, b) => {
|
|
423
|
+
// numeric on the trailing index, so piece 10 follows piece 9 and
|
|
424
|
+
// a reduce sees its map's results in the corpus's own order
|
|
425
|
+
const ai = Number(a.slice(a.lastIndexOf('/') + 1));
|
|
426
|
+
const bi = Number(b.slice(b.lastIndexOf('/') + 1));
|
|
427
|
+
return Number.isNaN(ai) || Number.isNaN(bi) ? a.localeCompare(b) : ai - bi;
|
|
428
|
+
}).slice(0, target.count ?? Infinity);
|
|
617
429
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
// function is the better artifact anyway: compiled once at
|
|
656
|
-
// compile time, not once per call site
|
|
657
|
-
const name = addressOf(step.from);
|
|
658
|
-
const raw = await environment.ledger.readSlot(name);
|
|
659
|
-
if (raw === null || raw === undefined) return failure(step, { error: `no slot '${name}'` });
|
|
660
|
-
/** @type {any} */
|
|
661
|
-
let data;
|
|
662
|
-
try { data = JSON.parse(String(raw)); }
|
|
430
|
+
/**
|
|
431
|
+
* One sub-call: the model sees ONE piece and answers with one JSON
|
|
432
|
+
* value. Failures are recorded, never thrown — §3.
|
|
433
|
+
* @param name - the piece's slot
|
|
434
|
+
* @param [signal]
|
|
435
|
+
*/
|
|
436
|
+
async function subcall(name, prompt, signal) {
|
|
437
|
+
const piece = await environment.read(name, { chars: subcallChars });
|
|
438
|
+
if (piece.error !== undefined)
|
|
439
|
+
return { slot: name, error: piece.error };
|
|
440
|
+
let reply;
|
|
441
|
+
// the turn is taken before the call, not after it: four sub-calls
|
|
442
|
+
// launched together would otherwise each see the same unspent budget
|
|
443
|
+
try {
|
|
444
|
+
reply = await createRoutedClient({
|
|
445
|
+
client, selectModel: options.selectModel,
|
|
446
|
+
limits: options.limits, onRoute: options.onRoute, account
|
|
447
|
+
}, { purpose: 'subcall', grammar: 'program', depth: options.depth ?? 0 }).complete({
|
|
448
|
+
// `stream` is deliberately NOT set. A sub-call has no UI to
|
|
449
|
+
// stream to, so `stream: false` looks right — and it is the
|
|
450
|
+
// exact request shape this package's own benchmark measured
|
|
451
|
+
// hanging past a 300 s deadline on the cheap tier and answering
|
|
452
|
+
// in seconds when streamed. Leaving it to the client's default
|
|
453
|
+
// (streaming) costs nothing here, because `complete` returns the
|
|
454
|
+
// accumulated message either way.
|
|
455
|
+
signal,
|
|
456
|
+
messages: [
|
|
457
|
+
{
|
|
458
|
+
role: 'system',
|
|
459
|
+
content: 'You are given ONE piece of a larger corpus and a question about it.'
|
|
460
|
+
+ ' Answer with a single JSON value and nothing else — no prose, no code fences.'
|
|
461
|
+
+ ' If the piece does not contain what was asked for, answer with null.',
|
|
462
|
+
},
|
|
463
|
+
{ role: 'user', content: `${prompt}\n\n--- piece ${name} ---\n${piece.text}` },
|
|
464
|
+
],
|
|
465
|
+
});
|
|
466
|
+
}
|
|
663
467
|
catch (err) {
|
|
664
|
-
|
|
665
|
-
|
|
468
|
+
if (signal?.aborted === true)
|
|
469
|
+
throw err;
|
|
470
|
+
return { slot: name, error: `the sub-call failed: ${err.message}` };
|
|
471
|
+
}
|
|
472
|
+
const raw = String(reply?.message?.content ?? '');
|
|
473
|
+
// settled here rather than by the caller: this is where the call
|
|
474
|
+
// actually happened, and an account that only saw the calls someone
|
|
475
|
+
// remembered to report is not a bound
|
|
476
|
+
try {
|
|
477
|
+
return { slot: name, value: JSON.parse(unfence(raw)) };
|
|
666
478
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (step.op === 'stat' || step.op === 'peek') {
|
|
674
|
-
const result = step.op === 'stat'
|
|
675
|
-
? await environment.stat(addressOf(step.from))
|
|
676
|
-
: await environment.peek(addressOf(step.from));
|
|
677
|
-
if (result.error !== undefined) return failure(step, result);
|
|
678
|
-
const stored = await environment.put(resultSlot(step.as), JSON.stringify(result),
|
|
679
|
-
{ kind: 'selection' });
|
|
680
|
-
bindings.set(step.as, { slot: resultSlot(step.as) });
|
|
681
|
-
report.push({ op: step.op, as: step.as, size: stored.size });
|
|
682
|
-
continue;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
if (step.op === 'map') {
|
|
686
|
-
if (client === null && options.subcall === undefined) {
|
|
687
|
-
return failure(step, { error: 'map needs a client — this runner was built without one' });
|
|
479
|
+
catch (err) {
|
|
480
|
+
return {
|
|
481
|
+
slot: name,
|
|
482
|
+
error: `the reply is not JSON: ${err.message}`,
|
|
483
|
+
raw: excerpt(raw, RAW_EXCERPT),
|
|
484
|
+
};
|
|
688
485
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Execute a compiled program.
|
|
489
|
+
* @param [hooks]
|
|
490
|
+
*/
|
|
491
|
+
async function run(doc, hooks = {}) {
|
|
492
|
+
const signal = hooks.signal;
|
|
493
|
+
const started = Date.now();
|
|
494
|
+
let plan;
|
|
696
495
|
try {
|
|
697
|
-
|
|
698
|
-
// paper states its own sub-calls are sequential and that "RLMs
|
|
699
|
-
// without asynchronous LM calls are slow", and doing the fan-out
|
|
700
|
-
// in the harness instead of inside an evaluator is what makes
|
|
701
|
-
// concurrency available at all. `concurrency` of 1 is the
|
|
702
|
-
// sequential mode the benchmark compares against — one
|
|
703
|
-
// implementation, so the comparison is of scheduling and
|
|
704
|
-
// nothing else
|
|
705
|
-
results = await mapConcurrent(budgeted, concurrency, (name, index) => {
|
|
706
|
-
// BEFORE the call, at every depth: an exhausted budget
|
|
707
|
-
// refuses rather than overruns, and the pieces it did not
|
|
708
|
-
// reach are recorded rather than silently missing
|
|
709
|
-
const reason = account?.stop() ?? null;
|
|
710
|
-
if (reason !== null) {
|
|
711
|
-
spent = reason;
|
|
712
|
-
return Promise.resolve({ slot: name, error: `stopped: ${reason}` });
|
|
713
|
-
}
|
|
714
|
-
return (options.subcall ?? subcall)(name, step.prompt, signal, index);
|
|
715
|
-
}, { signal });
|
|
496
|
+
plan = await compileHere(doc);
|
|
716
497
|
}
|
|
717
498
|
catch (err) {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
499
|
+
// NOTHING has run: the compile reads no slot and writes none, so a
|
|
500
|
+
// rejected program leaves the environment exactly as it was
|
|
501
|
+
return {
|
|
502
|
+
ok: false, ran: 0, steps: [], subcalls: 0, failed: 0, concurrency,
|
|
503
|
+
answer: null, ms: Date.now() - started,
|
|
504
|
+
error: 'the program does not compile', errors: [errorRecord(err)]
|
|
505
|
+
};
|
|
725
506
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
507
|
+
/** binding → `{ slot }` or `{ family, count }` */
|
|
508
|
+
const bindings = new Map();
|
|
509
|
+
const report = [];
|
|
510
|
+
let subcalls = 0;
|
|
511
|
+
let failed = 0;
|
|
512
|
+
let stopped = null;
|
|
513
|
+
/** Where a step's `from` points, as an address the environment knows. */
|
|
514
|
+
const address = (from) => {
|
|
515
|
+
const binding = bindings.get(from);
|
|
516
|
+
if (binding === undefined)
|
|
517
|
+
return { slot: from };
|
|
518
|
+
return binding;
|
|
519
|
+
};
|
|
520
|
+
const addressOf = (from) => {
|
|
521
|
+
const target = address(from);
|
|
522
|
+
return target.slot ?? target.family;
|
|
523
|
+
};
|
|
524
|
+
/**
|
|
525
|
+
* Run a step's compiled query over `data`, store the result, bind it.
|
|
526
|
+
* `select` and `reduce` differ only in where their input comes from
|
|
527
|
+
* — one slot's document, or a map's collected results — so the
|
|
528
|
+
* running and the storing live here once.
|
|
529
|
+
*/
|
|
530
|
+
async function store(step, data) {
|
|
531
|
+
let value;
|
|
532
|
+
try {
|
|
533
|
+
value = step.run(data);
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
const e = err;
|
|
537
|
+
return { error: `the query failed: ${e?.reason ?? e?.message ?? err}`, code: e?.code };
|
|
538
|
+
}
|
|
539
|
+
if ((step.validateOutput && !checkOutcome(step.validateOutput(value)).valid)
|
|
540
|
+
|| (step.recursive && recursiveItems(value) === null))
|
|
541
|
+
return { error: `reduce ${step.as} violates its output shape before storage`, code: 'AI0209' };
|
|
542
|
+
const written = await environment.put(resultSlot(step.as), JSON.stringify(value ?? null), { kind: 'selection', count: Array.isArray(value) ? value.length : undefined });
|
|
543
|
+
if (written.error === undefined)
|
|
544
|
+
bindings.set(step.as, { slot: resultSlot(step.as) });
|
|
545
|
+
return written;
|
|
735
546
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
547
|
+
for (const step of plan.steps) {
|
|
548
|
+
if (signal?.aborted === true) {
|
|
549
|
+
stopped = 'aborted';
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
const target = address(step.from);
|
|
553
|
+
if (step.op === 'chunk') {
|
|
554
|
+
const result = await environment.chunk(addressOf(step.from), {
|
|
555
|
+
strategy: step.strategy, size: step.size,
|
|
556
|
+
});
|
|
557
|
+
if (result.error !== undefined)
|
|
558
|
+
return failure(step, result);
|
|
559
|
+
bindings.set(step.as, { family: result.family, count: result.count });
|
|
560
|
+
report.push({ op: step.op, as: step.as, count: result.count, family: result.family });
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (step.op === 'grep') {
|
|
564
|
+
const result = await environment.grep(step.pattern, {
|
|
565
|
+
in: addressOf(step.from), limit: step.limit, flags: step.flags,
|
|
566
|
+
});
|
|
567
|
+
if (result.error !== undefined)
|
|
568
|
+
return failure(step, result);
|
|
569
|
+
const stored = await environment.put(resultSlot(step.as), JSON.stringify(result), { kind: 'selection', count: result.total });
|
|
570
|
+
// `matches` marks this binding as a SET OF ADDRESSES: reading it
|
|
571
|
+
// gives the listing, mapping over it visits what it found
|
|
572
|
+
bindings.set(step.as, { slot: resultSlot(step.as), matches: result.matches.length });
|
|
573
|
+
report.push({
|
|
574
|
+
op: step.op, as: step.as, total: result.total,
|
|
575
|
+
slots: new Set(result.matches.map((m) => m.slot)).size, size: stored.size,
|
|
576
|
+
});
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (step.op === 'select') {
|
|
580
|
+
// the query is the one this program COMPILED, run here rather
|
|
581
|
+
// than handed back to `environment.select` as a document. Two
|
|
582
|
+
// seams for one job is how a runner with a compiler wired ends
|
|
583
|
+
// up refused by an environment without one — and the compiled
|
|
584
|
+
// function is the better artifact anyway: compiled once at
|
|
585
|
+
// compile time, not once per call site
|
|
586
|
+
const name = addressOf(step.from);
|
|
587
|
+
const raw = await environment.ledger.readSlot(name);
|
|
588
|
+
if (raw === null || raw === undefined)
|
|
589
|
+
return failure(step, { error: `no slot '${name}'` });
|
|
590
|
+
let data;
|
|
591
|
+
try {
|
|
592
|
+
data = JSON.parse(String(raw));
|
|
593
|
+
}
|
|
594
|
+
catch (err) {
|
|
595
|
+
return failure(step, { error: `slot '${name}' is not JSON: ${err.message}` });
|
|
596
|
+
}
|
|
597
|
+
const stored = await store(step, data);
|
|
598
|
+
if (stored.error !== undefined)
|
|
599
|
+
return failure(step, stored);
|
|
600
|
+
report.push({ op: step.op, as: step.as, size: stored.size, count: stored.count });
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (step.op === 'stat' || step.op === 'peek') {
|
|
604
|
+
const result = step.op === 'stat'
|
|
605
|
+
? await environment.stat(addressOf(step.from))
|
|
606
|
+
: await environment.peek(addressOf(step.from));
|
|
607
|
+
if (result.error !== undefined)
|
|
608
|
+
return failure(step, result);
|
|
609
|
+
const stored = await environment.put(resultSlot(step.as), JSON.stringify(result), { kind: 'selection' });
|
|
610
|
+
bindings.set(step.as, { slot: resultSlot(step.as) });
|
|
611
|
+
report.push({ op: step.op, as: step.as, size: stored.size });
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (step.op === 'map') {
|
|
615
|
+
if (client === null && options.subcall === undefined) {
|
|
616
|
+
return failure(step, { error: 'map needs a client — this runner was built without one' });
|
|
617
|
+
}
|
|
618
|
+
const members = await membersOf(target);
|
|
619
|
+
const budgeted = members.slice(0, Math.max(0, maxSubcalls - subcalls));
|
|
620
|
+
const skipped = members.length - budgeted.length;
|
|
621
|
+
let spent = null;
|
|
622
|
+
let results;
|
|
623
|
+
try {
|
|
624
|
+
// the whole of §3's "fan-out is parallel" is this call: the
|
|
625
|
+
// paper states its own sub-calls are sequential and that "RLMs
|
|
626
|
+
// without asynchronous LM calls are slow", and doing the fan-out
|
|
627
|
+
// in the harness instead of inside an evaluator is what makes
|
|
628
|
+
// concurrency available at all. `concurrency` of 1 is the
|
|
629
|
+
// sequential mode the benchmark compares against — one
|
|
630
|
+
// implementation, so the comparison is of scheduling and
|
|
631
|
+
// nothing else
|
|
632
|
+
results = await mapConcurrent(budgeted, concurrency, (name, index) => {
|
|
633
|
+
// BEFORE the call, at every depth: an exhausted budget
|
|
634
|
+
// refuses rather than overruns, and the pieces it did not
|
|
635
|
+
// reach are recorded rather than silently missing
|
|
636
|
+
const reason = account?.stop() ?? null;
|
|
637
|
+
if (reason !== null) {
|
|
638
|
+
spent = reason;
|
|
639
|
+
return Promise.resolve({ slot: name, error: `stopped: ${reason}` });
|
|
640
|
+
}
|
|
641
|
+
return (options.subcall ?? subcall)(name, step.prompt, signal, index);
|
|
642
|
+
}, { signal });
|
|
643
|
+
}
|
|
644
|
+
catch (err) {
|
|
645
|
+
// the only way out of the map is an abort: a sub-call's own
|
|
646
|
+
// failure is a value (§3), so a throw here is the run being
|
|
647
|
+
// cancelled — the sub-calls that were in flight have settled
|
|
648
|
+
// by now, and the partial work is kept
|
|
649
|
+
if (!Boolean(signal?.aborted))
|
|
650
|
+
throw err;
|
|
651
|
+
stopped = 'aborted';
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
subcalls += budgeted.length;
|
|
655
|
+
for (let i = 0; i < results.length; i++) {
|
|
656
|
+
if (results[i].error !== undefined)
|
|
657
|
+
failed++;
|
|
658
|
+
// Recursive failures remain distinguishable from an ordinary null
|
|
659
|
+
// reply while satisfying the same envelope as successful leaves.
|
|
660
|
+
if (options.recursive && results[i].error !== undefined && !Object.hasOwn(results[i], 'value'))
|
|
661
|
+
results[i] = { ...results[i], value: null };
|
|
662
|
+
await environment.put(`${mapFamily(step.as)}${i}`, JSON.stringify(results[i]), { kind: 'selection' });
|
|
663
|
+
}
|
|
664
|
+
bindings.set(step.as, { family: mapFamily(step.as), count: results.length });
|
|
665
|
+
if (spent !== null)
|
|
666
|
+
stopped = spent;
|
|
667
|
+
report.push({
|
|
668
|
+
op: step.op,
|
|
669
|
+
as: step.as,
|
|
670
|
+
subcalls: budgeted.length,
|
|
671
|
+
failed: results.filter((r) => r.error !== undefined).length,
|
|
672
|
+
...(spent === null ? {} : { stopped: spent }),
|
|
673
|
+
// never silently: a capped map that said nothing would read as
|
|
674
|
+
// a map over everything
|
|
675
|
+
...(skipped > 0
|
|
676
|
+
? { skipped, note: `maxSubcalls ${maxSubcalls} reached — ${skipped} piece(s) not visited` }
|
|
677
|
+
: {}),
|
|
678
|
+
concurrency,
|
|
679
|
+
});
|
|
680
|
+
// a spent budget ends the run here rather than reducing over a
|
|
681
|
+
// map it knows is incomplete: the pieces that DID answer are in
|
|
682
|
+
// their slots, which is what makes a stopped run resumable
|
|
683
|
+
// instead of merely failed (§2)
|
|
684
|
+
if (spent !== null)
|
|
685
|
+
break;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (step.op === 'reduce') {
|
|
689
|
+
const members = await membersOf(target);
|
|
690
|
+
const collected = [];
|
|
691
|
+
let chars = 0;
|
|
692
|
+
for (const name of members) {
|
|
693
|
+
const text = String(await environment.ledger.readSlot(name) ?? 'null');
|
|
694
|
+
chars += text.length;
|
|
695
|
+
if (chars > maxReduceChars) {
|
|
696
|
+
return failure(step, {
|
|
697
|
+
error: `the reduce input passed ${maxReduceChars} characters at ${name} —`
|
|
698
|
+
+ ' a map that returns its input is not a reduction; narrow the map prompt',
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
const item = JSON.parse(text);
|
|
703
|
+
if (options.recursive && Array.isArray(item.items))
|
|
704
|
+
collected.push(...item.items);
|
|
705
|
+
else
|
|
706
|
+
collected.push(item);
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
collected.push({ slot: name, error: 'the stored result is not JSON' });
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const stored = await store(step, collected);
|
|
713
|
+
if (stored.error !== undefined)
|
|
714
|
+
return failure(step, stored);
|
|
715
|
+
report.push({ op: step.op, as: step.as, over: members.length, size: stored.size });
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
779
718
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
719
|
+
/** @param step @param result @returns */
|
|
720
|
+
function failure(step, result) {
|
|
721
|
+
return {
|
|
722
|
+
ok: false,
|
|
723
|
+
answer: null,
|
|
724
|
+
ran: report.length,
|
|
725
|
+
steps: report,
|
|
726
|
+
subcalls,
|
|
727
|
+
failed,
|
|
728
|
+
concurrency,
|
|
729
|
+
...(stopped === null ? {} : { stopped }),
|
|
730
|
+
error: result.error,
|
|
731
|
+
errors: [{ code: result.code ?? 'AI0200', docPath: `/steps/${step.index}`, message: result.error }],
|
|
732
|
+
ms: Date.now() - started,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
const answered = stopped === null
|
|
736
|
+
? await environment.read(addressOf(plan.answer.from), { chars: plan.answer.chars })
|
|
737
|
+
: { error: `the run was ${stopped}` };
|
|
738
|
+
const metrics = {
|
|
739
|
+
steps: report,
|
|
740
|
+
ran: report.length,
|
|
741
|
+
subcalls,
|
|
742
|
+
failed,
|
|
743
|
+
concurrency,
|
|
744
|
+
ms: Date.now() - started,
|
|
745
|
+
};
|
|
746
|
+
if (answered.error !== undefined)
|
|
747
|
+
return {
|
|
748
|
+
...metrics, ok: false, answer: null, error: answered.error,
|
|
749
|
+
...(stopped === null ? {} : { stopped }),
|
|
750
|
+
};
|
|
751
|
+
return {
|
|
752
|
+
...metrics, ok: true,
|
|
753
|
+
// the one place content comes back, and it is the step the program
|
|
754
|
+
// asked for by name
|
|
755
|
+
answer: {
|
|
756
|
+
slot: answered.name, size: answered.size, text: answered.text,
|
|
757
|
+
truncated: answered.text.length < answered.size
|
|
758
|
+
},
|
|
759
|
+
};
|
|
802
760
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
const metrics = {
|
|
809
|
-
steps: report,
|
|
810
|
-
ran: report.length,
|
|
811
|
-
subcalls,
|
|
812
|
-
failed,
|
|
813
|
-
concurrency,
|
|
814
|
-
ms: Date.now() - started,
|
|
815
|
-
};
|
|
816
|
-
if (answered.error !== undefined) return {
|
|
817
|
-
...metrics, ok: false, answer: null, error: answered.error,
|
|
818
|
-
...(stopped === null ? {} : { stopped }),
|
|
819
|
-
};
|
|
820
|
-
return {
|
|
821
|
-
...metrics, ok: true,
|
|
822
|
-
// the one place content comes back, and it is the step the program
|
|
823
|
-
// asked for by name
|
|
824
|
-
answer: { slot: answered.name, size: answered.size, text: answered.text,
|
|
825
|
-
truncated: answered.text.length < answered.size },
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// `run` only: a `compile` here would be a second, WEAKER compile than
|
|
830
|
-
// the one `run` does (it could not resolve names against the
|
|
831
|
-
// environment), and callers who want to check a document without
|
|
832
|
-
// running it have `compileProgram` and `programGate` directly
|
|
833
|
-
return { run };
|
|
761
|
+
// `run` only: a `compile` here would be a second, WEAKER compile than
|
|
762
|
+
// the one `run` does (it could not resolve names against the
|
|
763
|
+
// environment), and callers who want to check a document without
|
|
764
|
+
// running it have `compileProgram` and `programGate` directly
|
|
765
|
+
return { run };
|
|
834
766
|
}
|
|
835
|
-
|
|
836
767
|
//#endregion
|
|
837
|
-
|
|
838
768
|
//#region authoring
|
|
839
|
-
|
|
840
769
|
/**
|
|
841
770
|
* The worked example. Field notes: prose describes the shape, an example
|
|
842
771
|
* FIXES it — a small model told in words to plan over slots still tends
|
|
843
772
|
* to emit one step that does everything.
|
|
844
773
|
*
|
|
845
|
-
* Its query is a real one and it COMPILES; `test/
|
|
774
|
+
* Its query is a real one and it COMPILES; `test/agents/program.test.ts`
|
|
846
775
|
* asserts that. An example carrying a query the engine rejects would be
|
|
847
776
|
* teaching the failure it is meant to prevent, and the model would have
|
|
848
777
|
* copied it before the gate ever saw it.
|
|
849
778
|
*/
|
|
850
779
|
const EXAMPLE = {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
780
|
+
steps: [
|
|
781
|
+
{ op: 'chunk', from: 'corpus', as: 'pieces', strategy: 'line', size: 2000 },
|
|
782
|
+
{ op: 'map', from: 'pieces', as: 'found', prompt: 'Return the record in this piece as {"id":…,"value":…}.' },
|
|
783
|
+
{ op: 'reduce', from: 'found', as: 'summary', query: { $for: { r: '$[*].value' }, $return: '$r.value' } },
|
|
784
|
+
{ op: 'answer', from: 'summary' },
|
|
785
|
+
],
|
|
857
786
|
};
|
|
858
|
-
|
|
859
787
|
/** The example, so a test can compile it. Exported for exactly that:
|
|
860
788
|
* the prompt's correctness is a property worth a gate. */
|
|
861
789
|
export const PROGRAM_EXAMPLE = EXAMPLE;
|
|
862
|
-
|
|
863
790
|
/**
|
|
864
791
|
* Author a program with a model, gated on the compiler.
|
|
865
792
|
*
|
|
@@ -869,72 +796,70 @@ export const PROGRAM_EXAMPLE = EXAMPLE;
|
|
|
869
796
|
* decoding, the compile gate constrains meaning, and a rejected
|
|
870
797
|
* candidate goes back with its code and its pointer.
|
|
871
798
|
*
|
|
872
|
-
* @param {{ client: any, environment: any, compileQuery?: any,
|
|
873
|
-
* recursive?: boolean, analyzeQuery?: any, annotateTypes?: any,
|
|
874
|
-
* selectModel?: any, limits?: any, onRoute?: any, depth?: number, account?: any,
|
|
875
|
-
* createStructuredOutput: (options: any) => { generate: Function },
|
|
876
|
-
* querySchema?: any, maxRepairs?: number, system?: string }} options
|
|
877
799
|
* - `createStructuredOutput` is injected rather than imported so a
|
|
878
800
|
* caller can wrap it (a probe counting attempts, a cache); the
|
|
879
801
|
* package's own is the obvious argument.
|
|
880
802
|
* - `querySchema` is the published query grammar. Given, `select` and
|
|
881
803
|
* `reduce` are shape-constrained too and it is registered as a
|
|
882
804
|
* `$ref`; absent, the compile gate carries it alone (D3).
|
|
883
|
-
* @returns {{ author: (question: string, hooks?: { signal?: AbortSignal }) => Promise<any> }}
|
|
884
805
|
*/
|
|
885
806
|
export function createProgramAuthor(options) {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
807
|
+
const { environment, createStructuredOutput: structured } = options;
|
|
808
|
+
const compileQuery = options.compileQuery ?? null;
|
|
809
|
+
const querySchema = options.querySchema ?? null;
|
|
810
|
+
const schema = programSchema(querySchema === null ? {} : { queryRef: querySchema.$id });
|
|
811
|
+
async function author(question, hooks = {}) {
|
|
812
|
+
const digest = await environment.digest();
|
|
813
|
+
const known = (await environment.ledger.listSlots()).map((s) => s.name);
|
|
814
|
+
const generate = structured({
|
|
815
|
+
client: createRoutedClient(options, { purpose: 'author', grammar: 'program', depth: options.depth ?? 0 }),
|
|
816
|
+
schema,
|
|
817
|
+
name: 'jaren_program',
|
|
818
|
+
strict: false,
|
|
819
|
+
...(querySchema === null ? {} : { refs: [querySchema] }),
|
|
820
|
+
gate: programGate({
|
|
821
|
+
compileQuery, known, recursive: options.recursive,
|
|
822
|
+
analyzeQuery: options.analyzeQuery, annotateTypes: options.annotateTypes
|
|
823
|
+
}),
|
|
824
|
+
maxRepairs: options.maxRepairs ?? 2,
|
|
825
|
+
});
|
|
826
|
+
const result = await generate.generate([
|
|
827
|
+
{
|
|
828
|
+
role: 'system',
|
|
829
|
+
content: options.system ?? 'You plan work over a corpus you cannot see.'
|
|
830
|
+
+ ' You are given only the NAMES and sizes of the slots that hold it.'
|
|
831
|
+
+ ' Write a program whose steps name those slots. Never paste content into a step.'
|
|
832
|
+
+ ' Use map to ask a question of every piece — it is the only step that reads text —'
|
|
833
|
+
+ ' and reduce to combine what the map found. Here is a program in the right shape:\n'
|
|
834
|
+
+ JSON.stringify(options.recursive ? {
|
|
835
|
+
steps: EXAMPLE.steps.map((step) => step.op === 'reduce'
|
|
836
|
+
? {
|
|
837
|
+
...step, query: ['$[*]'],
|
|
838
|
+
outputSchema: {
|
|
839
|
+
type: 'array', items: {
|
|
840
|
+
type: 'object',
|
|
841
|
+
properties: { slot: { type: 'string' }, value: {} }, required: ['slot', 'value']
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
: step)
|
|
846
|
+
} : EXAMPLE)
|
|
847
|
+
+ (options.recursive ? '\nEach map result wraps the parsed leaf reply in {slot:string,value:any}. '
|
|
848
|
+
+ 'Failed map entries have value:null and an error diagnostic; preserve both, and distinguish them from a successful null reply. '
|
|
849
|
+
+ 'The worked reducer preserves every envelope, including null leaf values. An array constructor [expr] collects a sequence into one array; '
|
|
850
|
+
+ 'a bare wildcard in an object member fails when it produces multiple items. '
|
|
851
|
+
+ 'Recursive reduce MUST return {slot:string,value:any}, with value matching the leaf reply, or a sequence of these envelopes. '
|
|
852
|
+
+ 'Use min/max only for a sequence of numbers or strings, never to combine general facts. '
|
|
853
|
+
+ 'Declare outputSchema on reduce when inference is unavailable. A final answer preview may be truncated; inspect its truncated flag.' : ''),
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
role: 'user',
|
|
857
|
+
content: `The environment holds:\n${JSON.stringify(digest.slots)}\n`
|
|
858
|
+
+ `(${digest.total} slot(s), ${digest.size} characters in total)\n\n`
|
|
859
|
+
+ `Question: ${question}`,
|
|
860
|
+
},
|
|
861
|
+
], { signal: hooks.signal });
|
|
862
|
+
return result;
|
|
863
|
+
}
|
|
864
|
+
return { author };
|
|
938
865
|
}
|
|
939
|
-
|
|
940
|
-
//#endregion
|