@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/refine.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//@ts-check
|
|
2
1
|
/**
|
|
3
2
|
* Refinement: how an agent's durable state improves without anyone
|
|
4
3
|
* rewriting it.
|
|
@@ -10,7 +9,7 @@
|
|
|
10
9
|
* lands:
|
|
11
10
|
*
|
|
12
11
|
* 1. **shape** — the patch validates against the constrained schema
|
|
13
|
-
* (`schemas/patch.
|
|
12
|
+
* (`schemas/patch.ts`): three verbs, a path pattern that matches
|
|
14
13
|
* only the supplemental subtree, a cap on the number of operations.
|
|
15
14
|
* 2. **semantics** — the patch is applied to a COPY of the state
|
|
16
15
|
* through the injected patch engine. An operation that cannot apply
|
|
@@ -44,15 +43,12 @@
|
|
|
44
43
|
* declines with a stated reason and the rest of the ledger is unaffected
|
|
45
44
|
* — the same degrade-to-nothing posture as the retrieval seam.
|
|
46
45
|
*/
|
|
47
|
-
|
|
48
46
|
import { JarenValidator } from '@jarenjs/validate';
|
|
49
|
-
|
|
50
47
|
import { checkOutcome } from '@jarenjs/core/check';
|
|
51
48
|
import { createGuardedRefiner } from '@jarenjs/core/guarded';
|
|
52
49
|
import { createStructuredOutput } from '@tangleai/models/structured';
|
|
53
50
|
import { REFINEMENT_PATCH_SCHEMA, refinementPatchSchema } from '@tangleai/context/schemas/patch';
|
|
54
51
|
import { excerpt, truncate } from '@jarenjs/core/chunk';
|
|
55
|
-
|
|
56
52
|
/**
|
|
57
53
|
* The id stood in while a proposal is validated. A proposal never
|
|
58
54
|
* carries an id — identity is the ledger's to mint, or a model could
|
|
@@ -62,7 +58,6 @@ import { excerpt, truncate } from '@jarenjs/core/chunk';
|
|
|
62
58
|
* (non-empty string), so it exercises the same schema the real one will.
|
|
63
59
|
*/
|
|
64
60
|
const PENDING_ID = '(minted on commit)';
|
|
65
|
-
|
|
66
61
|
/**
|
|
67
62
|
* The members a proposal may leave out and the ledger fills in. They
|
|
68
63
|
* mirror `addMemory`/`addSkill`, and they are here because stage 3 has
|
|
@@ -75,19 +70,14 @@ const PENDING_ID = '(minted on commit)';
|
|
|
75
70
|
* the member, not as a record that should never have been written.
|
|
76
71
|
*/
|
|
77
72
|
const RECORD_DEFAULTS = { memory: { tags: [] }, skill: { tools: [] } };
|
|
78
|
-
|
|
79
73
|
/** How much of a run's trajectory travels in the proposal prompt. */
|
|
80
74
|
const TRAJECTORY_CHARS = 4000;
|
|
81
|
-
|
|
82
75
|
/** The per-line cap inside that block. */
|
|
83
76
|
const LINE_CHARS = 200;
|
|
84
|
-
|
|
85
|
-
/** @param {any} value */
|
|
77
|
+
/** @param value */
|
|
86
78
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
87
|
-
|
|
88
79
|
/** Structural equality over JSON values, by canonical-enough comparison. */
|
|
89
80
|
const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
90
|
-
|
|
91
81
|
/**
|
|
92
82
|
* One error record, in the shape the repair prompt carries: a code, a
|
|
93
83
|
* pointer and a reason. `docPath` points into the PATCH document
|
|
@@ -96,7 +86,7 @@ const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
|
96
86
|
* round that converges and one that flails.
|
|
97
87
|
*
|
|
98
88
|
* The codes this module raises, all of them records rather than throws
|
|
99
|
-
* (`errors.
|
|
89
|
+
* (`errors.ts` reserves `AiError` for transport and misuse; a rejected
|
|
100
90
|
* proposal is neither — it is something a model reads and fixes):
|
|
101
91
|
*
|
|
102
92
|
* AI0100 — a supplemental record is not an object
|
|
@@ -112,72 +102,59 @@ const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
|
112
102
|
* (`JP0004`, `JP2001`, …) rather than being renumbered here: the pointer
|
|
113
103
|
* and the code are what the model repairs from, and translating them
|
|
114
104
|
* would lose the only precise thing about them.
|
|
115
|
-
* @param {string} code
|
|
116
|
-
* @param {string} message
|
|
117
|
-
* @param {string} docPath
|
|
118
105
|
*/
|
|
119
106
|
const problem = (code, message, docPath) => ({ code, docPath, message });
|
|
120
|
-
|
|
121
107
|
/**
|
|
122
108
|
* The coded, pointered form of whatever the injected patch engine threw.
|
|
123
109
|
* `@jarenjs/json` raises `JsonPatchCompileError` / `JsonPatchRuntimeError`
|
|
124
110
|
* with a `code`, a `docPath` into the patch and (at runtime) a
|
|
125
111
|
* `dataPath` into the document; a different engine may raise anything.
|
|
126
112
|
* Both end up as one record the model can act on.
|
|
127
|
-
* @param {any} error
|
|
128
113
|
*/
|
|
129
114
|
function patchFailure(error) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
115
|
+
const code = typeof error?.code === 'string' ? error.code : 'PATCH';
|
|
116
|
+
const docPath = typeof error?.docPath === 'string' ? error.docPath : '';
|
|
117
|
+
const reason = error?.reason ?? error?.message ?? String(error);
|
|
118
|
+
const at = typeof error?.dataPath === 'string' && error.dataPath !== ''
|
|
119
|
+
? ` (target '${error.dataPath}' in the supplemental state)`
|
|
120
|
+
: '';
|
|
121
|
+
return problem(code, `the patch does not apply: ${reason}${at}`, docPath);
|
|
137
122
|
}
|
|
138
|
-
|
|
139
123
|
/**
|
|
140
124
|
* The run, as the few hundred characters worth putting in front of the
|
|
141
125
|
* model. Tool steps are preferred over wire messages: the steps ARE the
|
|
142
126
|
* evidence a memory would cite, and a transcript's assistant turns are
|
|
143
127
|
* mostly the model reading its own prose back.
|
|
144
|
-
* @param
|
|
145
|
-
* @param {number} max
|
|
146
|
-
* @returns {string}
|
|
128
|
+
* @param trajectory - a `send` result, its `steps`, or wire messages
|
|
147
129
|
*/
|
|
148
130
|
export function describeTrajectory(trajectory, max = TRAJECTORY_CHARS) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
+ `${excerpt(JSON.stringify(step.result ?? null), LINE_CHARS)}`);
|
|
131
|
+
const steps = Array.isArray(trajectory?.steps) ? trajectory.steps : null;
|
|
132
|
+
const messages = Array.isArray(trajectory)
|
|
133
|
+
? trajectory
|
|
134
|
+
: (Array.isArray(trajectory?.messages) ? trajectory.messages : null);
|
|
135
|
+
const lines = [];
|
|
136
|
+
if (steps !== null && steps.length > 0) {
|
|
137
|
+
for (const step of steps) {
|
|
138
|
+
lines.push(`- ${step.name}(${excerpt(step.arguments, 80)}) → `
|
|
139
|
+
+ `${excerpt(JSON.stringify(step.result ?? null), LINE_CHARS)}`);
|
|
140
|
+
}
|
|
160
141
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
142
|
+
if (messages !== null) {
|
|
143
|
+
for (const message of messages) {
|
|
144
|
+
if (message?.role === 'system')
|
|
145
|
+
continue;
|
|
146
|
+
if (steps !== null && steps.length > 0 && message?.role === 'tool')
|
|
147
|
+
continue;
|
|
148
|
+
const text = excerpt(message?.content, LINE_CHARS);
|
|
149
|
+
if (text !== '')
|
|
150
|
+
lines.push(`- ${message.role}: ${text}`);
|
|
151
|
+
}
|
|
168
152
|
}
|
|
169
|
-
|
|
170
|
-
return truncate(lines.join('\n'), max);
|
|
153
|
+
return truncate(lines.join('\n'), max);
|
|
171
154
|
}
|
|
172
|
-
|
|
173
155
|
/**
|
|
174
156
|
* Create a refiner over a ledger.
|
|
175
157
|
*
|
|
176
|
-
* @param {{ client: any, ledger: any,
|
|
177
|
-
* applyPatch?: ((document: any, patch: any[]) => any) | null,
|
|
178
|
-
* maxOps?: number, maxRepairs?: number, validator?: any,
|
|
179
|
-
* now?: () => string, trajectoryChars?: number,
|
|
180
|
-
* instructions?: string, deduplicate?: 'exact-evidence' }} options
|
|
181
158
|
* - `applyPatch` is the RFC 6902 seam: `(document, patch) => document`,
|
|
182
159
|
* normally `(doc, patch) => applyJSONPatch(doc, patch)` from
|
|
183
160
|
* `@jarenjs/json`. Absent, `refine`/`commit` decline with a stated
|
|
@@ -194,406 +171,380 @@ export function describeTrajectory(trajectory, max = TRAJECTORY_CHARS) {
|
|
|
194
171
|
* text and evidence and the same tags. Existing records are never merged
|
|
195
172
|
* or removed by this option; case, whitespace and independent citations
|
|
196
173
|
* remain distinct. The result reports every skipped proposal in `deduplicated`.
|
|
197
|
-
* @returns {{ state: () => Promise<any>,
|
|
198
|
-
* commit: (patch: any[]) => Promise<any>,
|
|
199
|
-
* refine: (trajectory: any, hooks?: { signal?: AbortSignal }) => Promise<any>,
|
|
200
|
-
* patchSchema: any }}
|
|
201
174
|
*/
|
|
202
175
|
export function createRefiner(options) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
const applyPatch = typeof options.applyPatch === 'function' ? options.applyPatch : null;
|
|
210
|
-
const now = options.now ?? (() => new Date().toISOString());
|
|
211
|
-
const maxRepairs = options.maxRepairs ?? 1;
|
|
212
|
-
const trajectoryChars = options.trajectoryChars ?? TRAJECTORY_CHARS;
|
|
213
|
-
const patchSchema = options.maxOps === undefined
|
|
214
|
-
? REFINEMENT_PATCH_SCHEMA
|
|
215
|
-
: refinementPatchSchema({ maxOps: options.maxOps });
|
|
216
|
-
// the shape check, compiled once — the same schema the generator
|
|
217
|
-
// constrains decoding with, so a hand-written patch handed to
|
|
218
|
-
// `commit` is held to exactly what a generated one is held to
|
|
219
|
-
const compiled = new JarenValidator({ skipErrors: false, collectErrors: true, unknownFormats: 'ignore' })
|
|
220
|
-
.compile(patchSchema);
|
|
221
|
-
const checkShape = (patch) => {
|
|
222
|
-
const full = checkOutcome(compiled(patch));
|
|
223
|
-
return full.valid && options.validator ? checkOutcome(options.validator(patch)) : full;
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const noEngine = {
|
|
227
|
-
error: 'refinement needs the applyPatch seam — inject '
|
|
228
|
-
+ '(doc, patch) => applyJSONPatch(doc, patch) from @jarenjs/json',
|
|
229
|
-
};
|
|
230
|
-
// One refiner serializes state-read, generation and commit together. Hosts
|
|
231
|
-
// sharing a ledger across refiners or other writers still own that coordination.
|
|
232
|
-
let pending = Promise.resolve();
|
|
233
|
-
const serial = (operation) => {
|
|
234
|
-
const result = pending.then(operation);
|
|
235
|
-
pending = result.then(() => undefined, () => undefined);
|
|
236
|
-
return result;
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* The supplemental state, as the document a patch is applied to. It is
|
|
241
|
-
* the ledger's own records verbatim — nothing here is a projection
|
|
242
|
-
* that would have to be mapped back — and it contains the goal, the
|
|
243
|
-
* memories and the skills and NOTHING else. The base system prompt is
|
|
244
|
-
* absent by construction, which is the first half of why it cannot be
|
|
245
|
-
* patched (the path pattern is the second).
|
|
246
|
-
*/
|
|
247
|
-
async function state() {
|
|
248
|
-
return {
|
|
249
|
-
goal: await ledger.getGoal(),
|
|
250
|
-
memories: await ledger.listMemories(),
|
|
251
|
-
skills: await ledger.listSkills(),
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Stage 3, for one kind: the writes a patched document implies, with
|
|
257
|
-
* every record completed and validated. Identity is the `id`, which a
|
|
258
|
-
* proposal never carries — so an entry without one is a new record and
|
|
259
|
-
* a stored record missing from the next document was removed. A
|
|
260
|
-
* `replace` therefore reads as "remove that one, store this one",
|
|
261
|
-
* which is what revising a memory actually is: a different claim, with
|
|
262
|
-
* different evidence, made at a different time.
|
|
263
|
-
* @param {'memory'|'skill'} kind
|
|
264
|
-
* @param {any[]} previous
|
|
265
|
-
* @param {any[]} next
|
|
266
|
-
* @param {string} field - the document member, for the error pointer
|
|
267
|
-
*/
|
|
268
|
-
function planKind(kind, previous, next, field) {
|
|
269
|
-
/** @type {any[]} */
|
|
270
|
-
const errors = [];
|
|
271
|
-
/** @type {any[]} */
|
|
272
|
-
const add = [];
|
|
273
|
-
const known = new Map(previous.map((record) => [record.id, record]));
|
|
274
|
-
const kept = new Set();
|
|
275
|
-
const deduplicated = [];
|
|
276
|
-
// Only records surviving unchanged are witnesses; matching one scheduled
|
|
277
|
-
// for removal would discard both copies of the evidence.
|
|
278
|
-
const witnesses = next.flatMap((record, index) => known.has(record?.id)
|
|
279
|
-
&& sameJson(record, known.get(record.id)) ? [{ record, path: `/${field}/${index}` }] : []);
|
|
280
|
-
|
|
281
|
-
next.forEach((entry, index) => {
|
|
282
|
-
const at = `/${field}/${index}`;
|
|
283
|
-
if (!isRecord(entry)) {
|
|
284
|
-
errors.push(problem('AI0100', `a ${kind} must be an object`, at));
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
if (typeof entry.id === 'string' && known.has(entry.id)) {
|
|
288
|
-
kept.add(entry.id);
|
|
289
|
-
// unchanged records are not rewritten: a refinement that touched
|
|
290
|
-
// nothing must leave the timestamps it did not touch alone
|
|
291
|
-
if (sameJson(entry, known.get(entry.id))) return;
|
|
292
|
-
}
|
|
293
|
-
else if (entry.id !== undefined) {
|
|
294
|
-
errors.push(problem('AI0101',
|
|
295
|
-
`a ${kind} may not choose its own id ('${entry.id}') — omit it and the ledger mints one`,
|
|
296
|
-
at));
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
const record = { ...RECORD_DEFAULTS[kind], ...entry, at: entry.at ?? now() };
|
|
300
|
-
const rejected = ledger.validate(kind, { ...record, id: record.id ?? PENDING_ID });
|
|
301
|
-
if (rejected !== null) {
|
|
302
|
-
for (const error of rejected.errors ?? []) {
|
|
303
|
-
errors.push(problem(error.keyword === '' ? 'AI0102' : error.keyword,
|
|
304
|
-
`the ${kind} is not storable: ${error.message}`,
|
|
305
|
-
`${at}${error.instancePath ?? ''}`));
|
|
306
|
-
}
|
|
307
|
-
if ((rejected.errors ?? []).length === 0)
|
|
308
|
-
errors.push(problem('AI0102', `the ${kind} is not storable`, at));
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
if (kind === 'memory' && options.deduplicate === 'exact-evidence' && entry.id === undefined
|
|
312
|
-
&& Object.keys(entry).every((key) => key === 'text' || key === 'evidence' || key === 'tags')) {
|
|
313
|
-
const match = witnesses.find(({ record: held }) => held.text === record.text && sameJson(held.evidence, record.evidence)
|
|
314
|
-
&& sameJson([...held.tags].sort(), [...record.tags].sort()));
|
|
315
|
-
if (match) {
|
|
316
|
-
deduplicated.push({ path: at, retainedPath: match.path,
|
|
317
|
-
...(match.record.id === undefined ? {} : { retainedId: match.record.id }) });
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
add.push(record);
|
|
322
|
-
witnesses.push({ record, path: at });
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
const remove = previous.filter((record) => !kept.has(record.id)).map((record) => record.id);
|
|
326
|
-
return { add, remove, errors, deduplicated };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* The progress the patched document appends. Append-only is checked
|
|
331
|
-
* rather than assumed: the path pattern cannot express a rewrite, and
|
|
332
|
-
* this is the assertion that says so even if a caller hands `commit` a
|
|
333
|
-
* patch that never met the pattern.
|
|
334
|
-
* @param {any} previous - the stored goal, or null
|
|
335
|
-
* @param {any} next - the patched goal
|
|
336
|
-
*/
|
|
337
|
-
function planProgress(previous, next) {
|
|
338
|
-
/** @type {any[]} */
|
|
339
|
-
const errors = [];
|
|
340
|
-
const before = previous?.progress ?? [];
|
|
341
|
-
const after = next?.progress ?? [];
|
|
342
|
-
if (previous === null && (next !== null || after.length > 0)) {
|
|
343
|
-
errors.push(problem('AI0103',
|
|
344
|
-
'there is no active goal to record progress against — call setGoal first', '/goal'));
|
|
345
|
-
return { append: [], errors };
|
|
176
|
+
const { client, ledger } = options;
|
|
177
|
+
if (options.deduplicate !== undefined && options.deduplicate !== 'exact-evidence')
|
|
178
|
+
throw new TypeError('deduplicate must be exact-evidence or omitted');
|
|
179
|
+
if (ledger === null || typeof ledger?.snapshot !== 'function') {
|
|
180
|
+
throw new TypeError('createRefiner needs a ledger (createLedger())');
|
|
346
181
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
182
|
+
const applyPatch = typeof options.applyPatch === 'function' ? options.applyPatch : null;
|
|
183
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
184
|
+
const maxRepairs = options.maxRepairs ?? 1;
|
|
185
|
+
const trajectoryChars = options.trajectoryChars ?? TRAJECTORY_CHARS;
|
|
186
|
+
const patchSchema = options.maxOps === undefined
|
|
187
|
+
? REFINEMENT_PATCH_SCHEMA
|
|
188
|
+
: refinementPatchSchema({ maxOps: options.maxOps });
|
|
189
|
+
// the shape check, compiled once — the same schema the generator
|
|
190
|
+
// constrains decoding with, so a hand-written patch handed to
|
|
191
|
+
// `commit` is held to exactly what a generated one is held to
|
|
192
|
+
const compiled = new JarenValidator({ skipErrors: false, collectErrors: true, unknownFormats: 'ignore' })
|
|
193
|
+
.compile(patchSchema);
|
|
194
|
+
const checkShape = (patch) => {
|
|
195
|
+
const full = checkOutcome(compiled(patch));
|
|
196
|
+
return full.valid && options.validator ? checkOutcome(options.validator(patch)) : full;
|
|
197
|
+
};
|
|
198
|
+
const noEngine = {
|
|
199
|
+
error: 'refinement needs the applyPatch seam — inject '
|
|
200
|
+
+ '(doc, patch) => applyJSONPatch(doc, patch) from @jarenjs/json',
|
|
201
|
+
};
|
|
202
|
+
// One refiner serializes state-read, generation and commit together. Hosts
|
|
203
|
+
// sharing a ledger across refiners or other writers still own that coordination.
|
|
204
|
+
let pending = Promise.resolve();
|
|
205
|
+
const serial = (operation) => {
|
|
206
|
+
const result = pending.then(operation);
|
|
207
|
+
pending = result.then(() => undefined, () => undefined);
|
|
208
|
+
return result;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* The supplemental state, as the document a patch is applied to. It is
|
|
212
|
+
* the ledger's own records verbatim — nothing here is a projection
|
|
213
|
+
* that would have to be mapped back — and it contains the goal, the
|
|
214
|
+
* memories and the skills and NOTHING else. The base system prompt is
|
|
215
|
+
* absent by construction, which is the first half of why it cannot be
|
|
216
|
+
* patched (the path pattern is the second).
|
|
217
|
+
*/
|
|
218
|
+
async function state() {
|
|
219
|
+
return {
|
|
220
|
+
goal: await ledger.getGoal(),
|
|
221
|
+
memories: await ledger.listMemories(),
|
|
222
|
+
skills: await ledger.listSkills(),
|
|
223
|
+
};
|
|
352
224
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Stage 3, for one kind: the writes a patched document implies, with
|
|
227
|
+
* every record completed and validated. Identity is the `id`, which a
|
|
228
|
+
* proposal never carries — so an entry without one is a new record and
|
|
229
|
+
* a stored record missing from the next document was removed. A
|
|
230
|
+
* `replace` therefore reads as "remove that one, store this one",
|
|
231
|
+
* which is what revising a memory actually is: a different claim, with
|
|
232
|
+
* different evidence, made at a different time.
|
|
233
|
+
* @param field - the document member, for the error pointer
|
|
234
|
+
*/
|
|
235
|
+
function planKind(kind, previous, next, field) {
|
|
236
|
+
const errors = [];
|
|
237
|
+
const add = [];
|
|
238
|
+
const known = new Map(previous.map((record) => [record.id, record]));
|
|
239
|
+
const kept = new Set();
|
|
240
|
+
const deduplicated = [];
|
|
241
|
+
// Only records surviving unchanged are witnesses; matching one scheduled
|
|
242
|
+
// for removal would discard both copies of the evidence.
|
|
243
|
+
const witnesses = next.flatMap((record, index) => known.has(record?.id)
|
|
244
|
+
&& sameJson(record, known.get(record.id)) ? [{ record, path: `/${field}/${index}` }] : []);
|
|
245
|
+
next.forEach((entry, index) => {
|
|
246
|
+
const at = `/${field}/${index}`;
|
|
247
|
+
if (!isRecord(entry)) {
|
|
248
|
+
errors.push(problem('AI0100', `a ${kind} must be an object`, at));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (typeof entry.id === 'string' && known.has(entry.id)) {
|
|
252
|
+
kept.add(entry.id);
|
|
253
|
+
// unchanged records are not rewritten: a refinement that touched
|
|
254
|
+
// nothing must leave the timestamps it did not touch alone
|
|
255
|
+
if (sameJson(entry, known.get(entry.id)))
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
else if (entry.id !== undefined) {
|
|
259
|
+
errors.push(problem('AI0101', `a ${kind} may not choose its own id ('${entry.id}') — omit it and the ledger mints one`, at));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const record = { ...RECORD_DEFAULTS[kind], ...entry, at: entry.at ?? now() };
|
|
263
|
+
const rejected = ledger.validate(kind, { ...record, id: record.id ?? PENDING_ID });
|
|
264
|
+
if (rejected !== null) {
|
|
265
|
+
for (const error of rejected.errors ?? []) {
|
|
266
|
+
errors.push(problem(error.keyword === '' ? 'AI0102' : error.keyword, `the ${kind} is not storable: ${error.message}`, `${at}${error.instancePath ?? ''}`));
|
|
267
|
+
}
|
|
268
|
+
if ((rejected.errors ?? []).length === 0)
|
|
269
|
+
errors.push(problem('AI0102', `the ${kind} is not storable`, at));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (kind === 'memory' && options.deduplicate === 'exact-evidence' && entry.id === undefined
|
|
273
|
+
&& Object.keys(entry).every((key) => key === 'text' || key === 'evidence' || key === 'tags')) {
|
|
274
|
+
const match = witnesses.find(({ record: held }) => held.text === record.text && sameJson(held.evidence, record.evidence)
|
|
275
|
+
&& sameJson([...held.tags].sort(), [...record.tags].sort()));
|
|
276
|
+
if (match) {
|
|
277
|
+
deduplicated.push({
|
|
278
|
+
path: at, retainedPath: match.path,
|
|
279
|
+
...(match.record.id === undefined ? {} : { retainedId: match.record.id })
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
add.push(record);
|
|
285
|
+
witnesses.push({ record, path: at });
|
|
286
|
+
});
|
|
287
|
+
const remove = previous.filter((record) => !kept.has(record.id)).map((record) => record.id);
|
|
288
|
+
return { add, remove, errors, deduplicated };
|
|
359
289
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
290
|
+
/**
|
|
291
|
+
* The progress the patched document appends. Append-only is checked
|
|
292
|
+
* rather than assumed: the path pattern cannot express a rewrite, and
|
|
293
|
+
* this is the assertion that says so even if a caller hands `commit` a
|
|
294
|
+
* patch that never met the pattern.
|
|
295
|
+
* @param previous - the stored goal, or null
|
|
296
|
+
* @param next - the patched goal
|
|
297
|
+
*/
|
|
298
|
+
function planProgress(previous, next) {
|
|
299
|
+
const errors = [];
|
|
300
|
+
const before = previous?.progress ?? [];
|
|
301
|
+
const after = next?.progress ?? [];
|
|
302
|
+
if (previous === null && (next !== null || after.length > 0)) {
|
|
303
|
+
errors.push(problem('AI0103', 'there is no active goal to record progress against — call setGoal first', '/goal'));
|
|
304
|
+
return { append: [], errors };
|
|
305
|
+
}
|
|
306
|
+
if (next !== null && next !== undefined && !sameJson({ ...next, progress: before }, { ...previous, progress: before })) {
|
|
307
|
+
errors.push(problem('AI0104', 'a refinement may not change the goal itself — only append to its progress', '/goal'));
|
|
308
|
+
return { append: [], errors };
|
|
373
309
|
}
|
|
374
|
-
|
|
310
|
+
if (after.length < before.length
|
|
311
|
+
|| !sameJson(after.slice(0, before.length), before)) {
|
|
312
|
+
errors.push(problem('AI0105', 'progress is append-only — earlier entries may not be edited or removed', '/goal/progress'));
|
|
313
|
+
return { append: [], errors };
|
|
314
|
+
}
|
|
315
|
+
const append = after.slice(before.length).map((entry) => (isRecord(entry)
|
|
316
|
+
? { ...entry, at: entry.at ?? now() }
|
|
317
|
+
: entry));
|
|
318
|
+
if (append.length > 0) {
|
|
319
|
+
// validated as a WHOLE goal, because that is the record the ledger
|
|
320
|
+
// stores: an entry legal on its own but illegal in place would
|
|
321
|
+
// otherwise pass here and be rejected at commit
|
|
322
|
+
const rejected = ledger.validate('goal', { ...previous, progress: [...before, ...append] });
|
|
323
|
+
if (rejected !== null) {
|
|
324
|
+
for (const error of rejected.errors ?? []) {
|
|
325
|
+
errors.push(problem(error.keyword === '' ? 'AI0102' : error.keyword, `the progress entry is not storable: ${error.message}`, `/goal${error.instancePath ?? ''}`));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { append, errors };
|
|
375
330
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
* @param {any[]} patch
|
|
386
|
-
* @returns {{ valid: boolean, errors: any[], plan?: any, next?: any }}
|
|
387
|
-
*/
|
|
388
|
-
function dryRun(document, patch) {
|
|
389
|
-
return guarded.prepare(document, patch);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const guarded = createGuardedRefiner({
|
|
393
|
-
read: state,
|
|
394
|
-
validateProposal: checkShape,
|
|
395
|
-
apply: (document, patch) => applyPatch(document, patch),
|
|
396
|
-
applyFailure: patchFailure,
|
|
397
|
-
validateCandidate: (next) => isRecord(next)
|
|
398
|
-
? { valid: true, errors: [] }
|
|
399
|
-
: { valid: false, errors: [problem('AI0106', 'the patched state is not an object', '')] },
|
|
400
|
-
planCommit: (next, document) => {
|
|
401
|
-
const memories = planKind('memory', document.memories, next.memories ?? [], 'memories');
|
|
402
|
-
const skills = planKind('skill', document.skills, next.skills ?? [], 'skills');
|
|
403
|
-
const progress = planProgress(document.goal, next.goal ?? null);
|
|
404
|
-
const errors = [...memories.errors, ...skills.errors, ...progress.errors];
|
|
405
|
-
return { valid: errors.length === 0, errors, plan: { memories, skills, progress } };
|
|
406
|
-
},
|
|
407
|
-
snapshot: () => ledger.snapshot(),
|
|
408
|
-
restore: (token) => ledger.rollback(token),
|
|
409
|
-
commit: async (plan) => {
|
|
410
|
-
const written = { memories: [], skills: [], progress: [] };
|
|
411
|
-
const accept = (outcome, what) => {
|
|
412
|
-
if (outcome?.error !== undefined) throw Object.assign(new Error(`${what}: ${outcome.error}`), { outcome, what });
|
|
413
|
-
};
|
|
414
|
-
for (const record of plan.memories.add) {
|
|
415
|
-
accept(await ledger.addMemory(record), 'a memory could not be stored');
|
|
416
|
-
written.memories.push(record);
|
|
417
|
-
}
|
|
418
|
-
for (const record of plan.skills.add) {
|
|
419
|
-
accept(await ledger.addSkill(record), 'a skill could not be stored');
|
|
420
|
-
written.skills.push(record);
|
|
421
|
-
}
|
|
422
|
-
for (const id of plan.memories.remove) await ledger.deleteMemory(id);
|
|
423
|
-
for (const id of plan.skills.remove) await ledger.deleteSkill(id);
|
|
424
|
-
for (const entry of plan.progress.append) {
|
|
425
|
-
accept(await ledger.recordProgress(entry), 'a progress entry could not be recorded');
|
|
426
|
-
written.progress.push(entry);
|
|
427
|
-
}
|
|
428
|
-
return written;
|
|
429
|
-
},
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
/** Whether a plan would write anything at all. */
|
|
433
|
-
const empty = (plan) => plan.memories.add.length === 0 && plan.memories.remove.length === 0
|
|
434
|
-
&& plan.skills.add.length === 0 && plan.skills.remove.length === 0
|
|
435
|
-
&& plan.progress.append.length === 0;
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Stage 4: snapshot, then write through the ledger's own API — never
|
|
439
|
-
* into storage, so every record passes the same validation a
|
|
440
|
-
* hand-written one does. A rejection anywhere rolls the whole
|
|
441
|
-
* refinement back, so the state after a failed commit is the state
|
|
442
|
-
* before it, byte for byte.
|
|
443
|
-
* @param {any} document
|
|
444
|
-
* @param {any[]} patch
|
|
445
|
-
*/
|
|
446
|
-
async function commitAgainst(document, patch) {
|
|
447
|
-
const dry = dryRun(document, patch);
|
|
448
|
-
if (!dry.valid) {
|
|
449
|
-
return { error: 'the refinement was rejected', errors: dry.errors, patchSchema };
|
|
331
|
+
/**
|
|
332
|
+
* Stages 1 to 3 over a copy — everything that can be known without
|
|
333
|
+
* writing anything. This is what the generator's `gate` runs, so a
|
|
334
|
+
* proposal that cannot land comes back to the model with pointers
|
|
335
|
+
* instead of landing half-way and being rolled back.
|
|
336
|
+
* @param document - the state as it stands
|
|
337
|
+
*/
|
|
338
|
+
function dryRun(document, patch) {
|
|
339
|
+
return guarded.prepare(document, patch);
|
|
450
340
|
}
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
341
|
+
const guarded = createGuardedRefiner({
|
|
342
|
+
read: state,
|
|
343
|
+
validateProposal: checkShape,
|
|
344
|
+
apply: (document, patch) => applyPatch(document, patch),
|
|
345
|
+
applyFailure: patchFailure,
|
|
346
|
+
validateCandidate: (next) => isRecord(next)
|
|
347
|
+
? { valid: true, errors: [] }
|
|
348
|
+
: { valid: false, errors: [problem('AI0106', 'the patched state is not an object', '')] },
|
|
349
|
+
planCommit: (next, document) => {
|
|
350
|
+
const memories = planKind('memory', document.memories, next.memories ?? [], 'memories');
|
|
351
|
+
const skills = planKind('skill', document.skills, next.skills ?? [], 'skills');
|
|
352
|
+
const progress = planProgress(document.goal, next.goal ?? null);
|
|
353
|
+
const errors = [...memories.errors, ...skills.errors, ...progress.errors];
|
|
354
|
+
return { valid: errors.length === 0, errors, plan: { memories, skills, progress } };
|
|
355
|
+
},
|
|
356
|
+
snapshot: () => ledger.snapshot(),
|
|
357
|
+
restore: (token) => ledger.rollback(token),
|
|
358
|
+
commit: async (plan) => {
|
|
359
|
+
const written = { memories: [], skills: [], progress: [] };
|
|
360
|
+
const accept = (outcome, what) => {
|
|
361
|
+
if (outcome?.error !== undefined)
|
|
362
|
+
throw Object.assign(new Error(`${what}: ${outcome.error}`), { outcome, what });
|
|
363
|
+
};
|
|
364
|
+
for (const record of plan.memories.add) {
|
|
365
|
+
accept(await ledger.addMemory(record), 'a memory could not be stored');
|
|
366
|
+
written.memories.push(record);
|
|
367
|
+
}
|
|
368
|
+
for (const record of plan.skills.add) {
|
|
369
|
+
accept(await ledger.addSkill(record), 'a skill could not be stored');
|
|
370
|
+
written.skills.push(record);
|
|
371
|
+
}
|
|
372
|
+
for (const id of plan.memories.remove)
|
|
373
|
+
await ledger.deleteMemory(id);
|
|
374
|
+
for (const id of plan.skills.remove)
|
|
375
|
+
await ledger.deleteSkill(id);
|
|
376
|
+
for (const entry of plan.progress.append) {
|
|
377
|
+
accept(await ledger.recordProgress(entry), 'a progress entry could not be recorded');
|
|
378
|
+
written.progress.push(entry);
|
|
379
|
+
}
|
|
380
|
+
return written;
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
/** Whether a plan would write anything at all. */
|
|
384
|
+
const empty = (plan) => plan.memories.add.length === 0 && plan.memories.remove.length === 0
|
|
385
|
+
&& plan.skills.add.length === 0 && plan.skills.remove.length === 0
|
|
386
|
+
&& plan.progress.append.length === 0;
|
|
387
|
+
/**
|
|
388
|
+
* Stage 4: snapshot, then write through the ledger's own API — never
|
|
389
|
+
* into storage, so every record passes the same validation a
|
|
390
|
+
* hand-written one does. A rejection anywhere rolls the whole
|
|
391
|
+
* refinement back, so the state after a failed commit is the state
|
|
392
|
+
* before it, byte for byte.
|
|
393
|
+
*/
|
|
394
|
+
async function commitAgainst(document, patch) {
|
|
395
|
+
const dry = dryRun(document, patch);
|
|
396
|
+
if (!dry.valid) {
|
|
397
|
+
return { error: 'the refinement was rejected', errors: dry.errors, patchSchema };
|
|
398
|
+
}
|
|
399
|
+
const plan = dry.plan;
|
|
400
|
+
if (empty(plan)) {
|
|
401
|
+
return {
|
|
402
|
+
ok: true, patch, snapshot: null, memories: [], skills: [], progress: [],
|
|
403
|
+
...(options.deduplicate ? { deduplicated: plan.memories.deduplicated } : {})
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (ledger.concurrency === 'atomic' && typeof ledger.transaction === 'function') {
|
|
407
|
+
try {
|
|
408
|
+
return await ledger.transaction(document, (scoped) => createRefiner({ ...options, ledger: scoped }).commit(patch));
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
if (error?.outcome)
|
|
412
|
+
return { ...error.outcome, snapshot: null };
|
|
413
|
+
return {
|
|
414
|
+
error: `the refinement was rolled back after a storage failure: ${error instanceof Error ? error.message : String(error)}`,
|
|
415
|
+
snapshot: null, patchSchema
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const outcome = await guarded.commitPrepared(document, dry);
|
|
420
|
+
const token = outcome.snapshot;
|
|
421
|
+
if (!outcome.ok) {
|
|
422
|
+
if (outcome.stage === 'snapshot')
|
|
423
|
+
return {
|
|
424
|
+
error: 'the refinement could not take a snapshot; nothing was written',
|
|
425
|
+
cause: outcome.cause, errors: outcome.errors, patchSchema
|
|
426
|
+
};
|
|
427
|
+
if ('restoreError' in outcome && outcome.restoreError !== undefined)
|
|
428
|
+
return {
|
|
429
|
+
error: 'the refinement failed and rollback failed; storage requires recovery',
|
|
430
|
+
snapshot: token, patchSchema, cause: outcome.cause, restoreError: outcome.restoreError,
|
|
431
|
+
};
|
|
432
|
+
const cause = outcome.cause;
|
|
433
|
+
if (cause?.outcome)
|
|
434
|
+
return {
|
|
435
|
+
error: `the refinement was rolled back: ${cause.what} — ${cause.outcome.error}`,
|
|
436
|
+
errors: cause.outcome.errors ?? [], patchSchema, snapshot: token,
|
|
437
|
+
};
|
|
438
|
+
return {
|
|
439
|
+
error: `the refinement was rolled back after a storage failure: ${outcome.cause instanceof Error ? outcome.cause.message : String(outcome.cause)}`,
|
|
440
|
+
snapshot: token, patchSchema
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const written = outcome.value;
|
|
444
|
+
return {
|
|
445
|
+
ok: true,
|
|
446
|
+
patch,
|
|
447
|
+
// the token is returned, not kept: rolling a refinement back is the
|
|
448
|
+
// host's call to make, and a snapshot nobody was told about is a
|
|
449
|
+
// reversal nobody can perform
|
|
450
|
+
snapshot: token,
|
|
451
|
+
removed: { memories: plan.memories.remove, skills: plan.skills.remove },
|
|
452
|
+
...written,
|
|
453
|
+
...(options.deduplicate ? { deduplicated: plan.memories.deduplicated } : {}),
|
|
454
|
+
};
|
|
455
455
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
return
|
|
465
|
-
snapshot: null, patchSchema };
|
|
466
|
-
}
|
|
456
|
+
/**
|
|
457
|
+
* Apply a patch that already exists — the same four stages a generated
|
|
458
|
+
* one goes through, exposed because a host (or a test) writing the
|
|
459
|
+
* patch itself must not get a weaker gate than a model does.
|
|
460
|
+
*/
|
|
461
|
+
async function commit(patch) {
|
|
462
|
+
if (applyPatch === null)
|
|
463
|
+
return { ...noEngine, patchSchema };
|
|
464
|
+
return serial(async () => commitAgainst(await state(), patch));
|
|
467
465
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
466
|
+
/**
|
|
467
|
+
* The proposal prompt. The state is shown in full and the rules are
|
|
468
|
+
* stated in the imperative, because the field notes are clear that
|
|
469
|
+
* breadth and posture belong in the prompt while correctness belongs
|
|
470
|
+
* in the gate.
|
|
471
|
+
*/
|
|
472
|
+
function prompt(document, trajectory) {
|
|
473
|
+
const task = options.instructions ?? [
|
|
474
|
+
'You have just finished a run. Propose what should be remembered from it, as an RFC 6902',
|
|
475
|
+
'JSON Patch over your supplemental state.',
|
|
476
|
+
'',
|
|
477
|
+
'Rules:',
|
|
478
|
+
'- Every memory and every progress entry MUST carry `evidence`: what in the run below',
|
|
479
|
+
' establishes it. Quote the tool result or name the fact. Never write a memory you cannot',
|
|
480
|
+
' cite — an empty patch is a better answer than an invented one.',
|
|
481
|
+
'- Store what will still be true and still be useful next time. Not what happened; what it',
|
|
482
|
+
' means.',
|
|
483
|
+
'- Do not restate a memory that is already stored, and do not add a near-duplicate of one.',
|
|
484
|
+
'- Append with /memories/-, /skills/- and /goal/progress/-. Replace or remove an existing',
|
|
485
|
+
' record by its index. Nothing else is addressable.',
|
|
486
|
+
'',
|
|
487
|
+
// Examples help authoring; the schema enforces the path/value relationship.
|
|
488
|
+
'Each path takes its own shape:',
|
|
489
|
+
' /memories/- {"text": …, "evidence": …, "tags": [ … ]}',
|
|
490
|
+
' /skills/- {"name": …, "when": …, "instructions": …, "tools": [ … ]}',
|
|
491
|
+
' /goal/progress/- {"note": …, "evidence": …} <- note, NOT text',
|
|
492
|
+
'',
|
|
493
|
+
'Example of a well-formed refinement:',
|
|
494
|
+
'[{"op":"add","path":"/memories/-","value":{"text":"…","evidence":"…","tags":["…"]}},'
|
|
495
|
+
+ '{"op":"add","path":"/goal/progress/-","value":{"note":"…","evidence":"…"}}]',
|
|
496
|
+
].join('\n');
|
|
497
|
+
return [
|
|
498
|
+
task,
|
|
499
|
+
'',
|
|
500
|
+
'Your supplemental state right now:',
|
|
501
|
+
JSON.stringify(document, null, 1),
|
|
502
|
+
'',
|
|
503
|
+
'The run:',
|
|
504
|
+
describeTrajectory(trajectory, trajectoryChars),
|
|
505
|
+
].join('\n');
|
|
484
506
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
'- Every memory and every progress entry MUST carry `evidence`: what in the run below',
|
|
526
|
-
' establishes it. Quote the tool result or name the fact. Never write a memory you cannot',
|
|
527
|
-
' cite — an empty patch is a better answer than an invented one.',
|
|
528
|
-
'- Store what will still be true and still be useful next time. Not what happened; what it',
|
|
529
|
-
' means.',
|
|
530
|
-
'- Do not restate a memory that is already stored, and do not add a near-duplicate of one.',
|
|
531
|
-
'- Append with /memories/-, /skills/- and /goal/progress/-. Replace or remove an existing',
|
|
532
|
-
' record by its index. Nothing else is addressable.',
|
|
533
|
-
'',
|
|
534
|
-
// Examples help authoring; the schema enforces the path/value relationship.
|
|
535
|
-
'Each path takes its own shape:',
|
|
536
|
-
' /memories/- {"text": …, "evidence": …, "tags": [ … ]}',
|
|
537
|
-
' /skills/- {"name": …, "when": …, "instructions": …, "tools": [ … ]}',
|
|
538
|
-
' /goal/progress/- {"note": …, "evidence": …} <- note, NOT text',
|
|
539
|
-
'',
|
|
540
|
-
'Example of a well-formed refinement:',
|
|
541
|
-
'[{"op":"add","path":"/memories/-","value":{"text":"…","evidence":"…","tags":["…"]}},'
|
|
542
|
-
+ '{"op":"add","path":"/goal/progress/-","value":{"note":"…","evidence":"…"}}]',
|
|
543
|
-
].join('\n');
|
|
544
|
-
return [
|
|
545
|
-
task,
|
|
546
|
-
'',
|
|
547
|
-
'Your supplemental state right now:',
|
|
548
|
-
JSON.stringify(document, null, 1),
|
|
549
|
-
'',
|
|
550
|
-
'The run:',
|
|
551
|
-
describeTrajectory(trajectory, trajectoryChars),
|
|
552
|
-
].join('\n');
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Propose and apply a refinement for one run.
|
|
557
|
-
*
|
|
558
|
-
* The gate runs INSIDE generation (as `createStructuredOutput`'s
|
|
559
|
-
* `gate`), so a patch that does not apply or would store an
|
|
560
|
-
* unevidenced memory comes back to the model with its pointers for a
|
|
561
|
-
* bounded repair. What survives generation is committed against the
|
|
562
|
-
* same state the gate ran over.
|
|
563
|
-
* @param {any} trajectory - a `send` result, its `steps`, or messages
|
|
564
|
-
* @param {{ signal?: AbortSignal }} [hooks]
|
|
565
|
-
*/
|
|
566
|
-
async function refineAgainst(trajectory, hooks = {}) {
|
|
567
|
-
if (applyPatch === null) return { ...noEngine, patchSchema };
|
|
568
|
-
// read ONCE: the gate has to be synchronous (a compiled check is),
|
|
569
|
-
// and a gate that re-read the ledger between attempts would be
|
|
570
|
-
// judging each attempt against a different state
|
|
571
|
-
const document = await state();
|
|
572
|
-
const generator = createStructuredOutput({
|
|
573
|
-
client,
|
|
574
|
-
schema: patchSchema,
|
|
575
|
-
name: 'refinement',
|
|
576
|
-
// a `remove` carries no `value`, so `value` is optional, and
|
|
577
|
-
// OpenAI's strict mode requires every declared property to be
|
|
578
|
-
// required. The local check is the authority either way.
|
|
579
|
-
strict: false,
|
|
580
|
-
gate: (patch) => dryRun(document, patch),
|
|
581
|
-
maxRepairs,
|
|
582
|
-
});
|
|
583
|
-
const result = await generator.generate([{ role: 'user', content: prompt(document, trajectory) }],
|
|
584
|
-
hooks);
|
|
585
|
-
if (result.errors !== undefined) {
|
|
586
|
-
return {
|
|
587
|
-
error: 'the model did not produce an applicable refinement',
|
|
588
|
-
errors: result.errors,
|
|
589
|
-
attempts: result.attempts,
|
|
590
|
-
raw: result.raw,
|
|
591
|
-
patchSchema,
|
|
592
|
-
};
|
|
507
|
+
/**
|
|
508
|
+
* Propose and apply a refinement for one run.
|
|
509
|
+
*
|
|
510
|
+
* The gate runs INSIDE generation (as `createStructuredOutput`'s
|
|
511
|
+
* `gate`), so a patch that does not apply or would store an
|
|
512
|
+
* unevidenced memory comes back to the model with its pointers for a
|
|
513
|
+
* bounded repair. What survives generation is committed against the
|
|
514
|
+
* same state the gate ran over.
|
|
515
|
+
* @param trajectory - a `send` result, its `steps`, or messages
|
|
516
|
+
* @param [hooks]
|
|
517
|
+
*/
|
|
518
|
+
async function refineAgainst(trajectory, hooks = {}) {
|
|
519
|
+
if (applyPatch === null)
|
|
520
|
+
return { ...noEngine, patchSchema };
|
|
521
|
+
// read ONCE: the gate has to be synchronous (a compiled check is),
|
|
522
|
+
// and a gate that re-read the ledger between attempts would be
|
|
523
|
+
// judging each attempt against a different state
|
|
524
|
+
const document = await state();
|
|
525
|
+
const generator = createStructuredOutput({
|
|
526
|
+
client,
|
|
527
|
+
schema: patchSchema,
|
|
528
|
+
name: 'refinement',
|
|
529
|
+
// a `remove` carries no `value`, so `value` is optional, and
|
|
530
|
+
// OpenAI's strict mode requires every declared property to be
|
|
531
|
+
// required. The local check is the authority either way.
|
|
532
|
+
strict: false,
|
|
533
|
+
gate: (patch) => dryRun(document, patch),
|
|
534
|
+
maxRepairs,
|
|
535
|
+
});
|
|
536
|
+
const result = await generator.generate([{ role: 'user', content: prompt(document, trajectory) }], hooks);
|
|
537
|
+
if (result.errors !== undefined) {
|
|
538
|
+
return {
|
|
539
|
+
error: 'the model did not produce an applicable refinement',
|
|
540
|
+
errors: result.errors,
|
|
541
|
+
attempts: result.attempts,
|
|
542
|
+
raw: result.raw,
|
|
543
|
+
patchSchema,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
return { ...await commitAgainst(document, result.value), attempts: result.attempts };
|
|
593
547
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
const refine = (trajectory, hooks = {}) => serial(() => refineAgainst(trajectory, hooks));
|
|
598
|
-
return { state, commit, refine, patchSchema };
|
|
548
|
+
const refine = (trajectory, hooks = {}) => serial(() => refineAgainst(trajectory, hooks));
|
|
549
|
+
return { state, commit, refine, patchSchema };
|
|
599
550
|
}
|