@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/recursive.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//@ts-check
|
|
2
1
|
/**
|
|
3
2
|
* The long-horizon entry point: a job, not a conversation.
|
|
4
3
|
*
|
|
@@ -43,32 +42,25 @@
|
|
|
43
42
|
* depth. Recursive envelopes are compile/runtime checked; hosts may also
|
|
44
43
|
* opt into checked root reuse and per-call route limits.
|
|
45
44
|
*/
|
|
46
|
-
|
|
47
45
|
import { excerpt } from '@jarenjs/core/chunk';
|
|
48
|
-
import { recursiveItems } from
|
|
49
|
-
import { createProgramSession } from
|
|
50
|
-
import { readProgramAnswer } from
|
|
51
|
-
|
|
46
|
+
import { recursiveItems } from "./program-shape.js";
|
|
47
|
+
import { createProgramSession } from "./program-session.js";
|
|
48
|
+
import { readProgramAnswer } from "./program-result.js";
|
|
52
49
|
/** The deepest a tree may go, whatever it asks for. */
|
|
53
50
|
export const MAX_DEPTH = 3;
|
|
54
|
-
|
|
55
51
|
/** The conservative default; local depth measurements have not justified raising it. */
|
|
56
52
|
export const DEFAULT_DEPTH = 1;
|
|
57
|
-
|
|
58
53
|
/** How much of a child's answer its parent's trajectory keeps. */
|
|
59
54
|
const TRACE_EXCERPT = 200;
|
|
60
|
-
|
|
61
55
|
/**
|
|
62
56
|
* The dimensions a tree is bounded by. The same three the agent loop
|
|
63
57
|
* uses, in the same order, because a caller who has budgeted one should
|
|
64
58
|
* not have to learn a second vocabulary for the other.
|
|
65
59
|
*/
|
|
66
|
-
export const BUDGET_DIMENSIONS =
|
|
67
|
-
|
|
60
|
+
export const BUDGET_DIMENSIONS = ['turns', 'tokens', 'ms'];
|
|
68
61
|
/** Characters per token when a provider reports no usage — the agent
|
|
69
62
|
* loop's ratio, stated there and unchanged here. */
|
|
70
63
|
const TOKEN_CHARS = 4;
|
|
71
|
-
|
|
72
64
|
/**
|
|
73
65
|
* One account, charged by every model call at every depth.
|
|
74
66
|
*
|
|
@@ -77,9 +69,8 @@ const TOKEN_CHARS = 4;
|
|
|
77
69
|
* branch spend the full budget independently, which is precisely the
|
|
78
70
|
* runaway the cap exists to prevent.
|
|
79
71
|
*
|
|
80
|
-
* @param
|
|
81
|
-
*
|
|
82
|
-
* @param {() => number} [clock]
|
|
72
|
+
* @param [budget]
|
|
73
|
+
* @param [clock]
|
|
83
74
|
* **Turns are reserved, tokens are settled**, and the split is what makes
|
|
84
75
|
* a concurrent fan-out bounded. A budget checked before a call and
|
|
85
76
|
* charged after it can be beaten by concurrency: four sub-calls launched
|
|
@@ -90,58 +81,55 @@ const TOKEN_CHARS = 4;
|
|
|
90
81
|
* at most `maxConcurrentSubcalls - 1` calls' worth. That is stated here
|
|
91
82
|
* rather than hidden, because a caller sizing a budget needs it.
|
|
92
83
|
*
|
|
93
|
-
* @returns {{ reserve: () => void, settle: (usage: any, text?: string) => void,
|
|
94
|
-
* stop: () => string | null, spent: () => { turns: number, tokens: number, ms: number },
|
|
95
|
-
* remaining: () => Record<string, number | null> }}
|
|
96
84
|
*/
|
|
97
85
|
export function createBudgetAccount(budget = {}, clock = Date.now) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
86
|
+
const started = clock();
|
|
87
|
+
const spent = {
|
|
88
|
+
turns: budget.spent?.turns ?? 0,
|
|
89
|
+
tokens: budget.spent?.tokens ?? 0,
|
|
90
|
+
ms: budget.spent?.ms ?? 0,
|
|
91
|
+
};
|
|
92
|
+
const elapsed = () => spent.ms + (clock() - started);
|
|
93
|
+
return {
|
|
94
|
+
/** A call is about to be made. Taken now, not after, so concurrent
|
|
95
|
+
* launches cannot each see the same unspent turn. */
|
|
96
|
+
reserve() {
|
|
97
|
+
spent.turns += 1;
|
|
98
|
+
},
|
|
99
|
+
/**
|
|
100
|
+
* A call came back. The provider's own usage wins; the character
|
|
101
|
+
* estimate is the fallback and it is only computed when a token
|
|
102
|
+
* budget exists to spend it against.
|
|
103
|
+
* @param usage - the provider's usage block, if any
|
|
104
|
+
* @param [text] - what was sent and returned, for the estimate
|
|
105
|
+
*/
|
|
106
|
+
settle(usage, text) {
|
|
107
|
+
const reported = typeof usage?.total_tokens === 'number' && usage.total_tokens > 0
|
|
108
|
+
? usage.total_tokens
|
|
109
|
+
: (usage?.prompt_tokens ?? 0) + (usage?.completion_tokens ?? 0);
|
|
110
|
+
if (reported > 0)
|
|
111
|
+
spent.tokens += reported;
|
|
112
|
+
else if (typeof budget.tokens === 'number' && text !== undefined) {
|
|
113
|
+
spent.tokens += Math.ceil(text.length / TOKEN_CHARS);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
/** The dimension that is spent, or null. Checked BEFORE a call. */
|
|
117
|
+
stop() {
|
|
118
|
+
const now = { turns: spent.turns, tokens: spent.tokens, ms: elapsed() };
|
|
119
|
+
for (const dimension of BUDGET_DIMENSIONS) {
|
|
120
|
+
const limit = budget[dimension];
|
|
121
|
+
if (typeof limit === 'number' && now[dimension] >= limit)
|
|
122
|
+
return `budget-${dimension}`;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
},
|
|
126
|
+
spent: () => ({ turns: spent.turns, tokens: spent.tokens, ms: elapsed() }),
|
|
127
|
+
remaining: () => Object.fromEntries(BUDGET_DIMENSIONS.map((name) => [name,
|
|
128
|
+
typeof budget[name] === 'number'
|
|
129
|
+
? Math.max(0, budget[name] - (name === 'ms' ? elapsed() : spent[name]))
|
|
130
|
+
: null])),
|
|
131
|
+
};
|
|
143
132
|
}
|
|
144
|
-
|
|
145
133
|
/**
|
|
146
134
|
* The record of what a run actually did.
|
|
147
135
|
*
|
|
@@ -150,55 +138,49 @@ export function createBudgetAccount(budget = {}, clock = Date.now) {
|
|
|
150
138
|
* calls it made and the slots it wrote, at every depth. Kept as plain
|
|
151
139
|
* data so a host can persist it, render it, or diff two runs.
|
|
152
140
|
*
|
|
153
|
-
* @returns {{ add: (entry: any) => any, entries: () => any[],
|
|
154
|
-
* summary: () => { steps: number, calls: number, slots: number, depths: number[] } }}
|
|
155
141
|
*/
|
|
156
142
|
export function createTrajectory() {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
};
|
|
143
|
+
const entries = [];
|
|
144
|
+
return {
|
|
145
|
+
/**
|
|
146
|
+
* Record one thing that happened. Returns the entry so a caller can
|
|
147
|
+
* hold it — never the array, which would let a caller mutate history.
|
|
148
|
+
*/
|
|
149
|
+
add(entry) {
|
|
150
|
+
const record = { seq: entries.length, ...entry };
|
|
151
|
+
if (typeof record.answer === 'string')
|
|
152
|
+
record.answer = excerpt(record.answer, TRACE_EXCERPT);
|
|
153
|
+
entries.push(record);
|
|
154
|
+
return record;
|
|
155
|
+
},
|
|
156
|
+
entries: () => entries.slice(),
|
|
157
|
+
summary: () => ({
|
|
158
|
+
steps: entries.filter((e) => e.kind === 'program').length,
|
|
159
|
+
calls: entries.filter((e) => e.kind === 'subcall' || e.kind === 'author').length,
|
|
160
|
+
slots: entries.filter((e) => e.slot !== undefined).length,
|
|
161
|
+
depths: [...new Set(entries.map((e) => e.depth ?? 0))].sort((a, b) => a - b),
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
179
164
|
}
|
|
180
|
-
|
|
181
165
|
/**
|
|
182
166
|
* Clamp a requested depth to what this package will actually run.
|
|
183
167
|
*
|
|
184
168
|
* Returns the depth AND whether it was clamped, because a run that
|
|
185
169
|
* quietly did less than it was asked to is indistinguishable from one
|
|
186
170
|
* that failed to find anything — the caller has to be able to tell.
|
|
187
|
-
* @param {number | undefined} requested
|
|
188
|
-
* @returns {{ depth: number, clamped: boolean }}
|
|
189
171
|
*/
|
|
190
172
|
export function resolveDepth(requested) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
173
|
+
if (requested === undefined || requested === null)
|
|
174
|
+
return { depth: DEFAULT_DEPTH, clamped: false };
|
|
175
|
+
const asked = Math.floor(Number(requested));
|
|
176
|
+
if (!Number.isFinite(asked) || asked < 0)
|
|
177
|
+
return { depth: DEFAULT_DEPTH, clamped: true };
|
|
178
|
+
return { depth: Math.min(asked, MAX_DEPTH), clamped: asked > MAX_DEPTH };
|
|
195
179
|
}
|
|
196
|
-
|
|
197
180
|
/** The slot prefix a child at `depth` working on `index` owns. Derived,
|
|
198
181
|
* like every other address in this package, so nothing has to store a
|
|
199
182
|
* mapping from a child to its workspace. */
|
|
200
183
|
export const childScope = (depth, index) => `child/${depth}/${index}/`;
|
|
201
|
-
|
|
202
184
|
/**
|
|
203
185
|
* The long-horizon entry point: hand it a question and an environment,
|
|
204
186
|
* get back an answer and the trajectory that produced it.
|
|
@@ -208,177 +190,155 @@ export const childScope = (depth, index) => `child/${depth}/${index}/`;
|
|
|
208
190
|
* CONVERSATION, this is for a JOB — something with a corpus, a question
|
|
209
191
|
* over all of it, and no user waiting to answer a follow-up.
|
|
210
192
|
*
|
|
211
|
-
* @param {{ client: any, environment: any, compileQuery?: any,
|
|
212
|
-
* analyzeQuery?: any, annotateTypes?: any, reuse?: any,
|
|
213
|
-
* selectModel?: any, limits?: any, onRoute?: any,
|
|
214
|
-
* createStructuredOutput: (options: any) => { generate: Function },
|
|
215
|
-
* createProgramAuthor: (options: any) => { author: Function },
|
|
216
|
-
* createProgramRunner: (options: any) => { run: Function },
|
|
217
|
-
* createEnvironment: (options: any) => any,
|
|
218
|
-
* querySchema?: any, budget?: any, depth?: number,
|
|
219
|
-
* maxConcurrentSubcalls?: number, maxSubcalls?: number,
|
|
220
|
-
* subcallChars?: number, maxAnswerChars?: number, clock?: () => number }} options
|
|
221
193
|
* The three factories and `createEnvironment` are injected for the
|
|
222
194
|
* same reason everything heavy in this package is: it keeps this
|
|
223
|
-
* module free of a cycle with `program.
|
|
195
|
+
* module free of a cycle with `program.ts` and lets a probe wrap any
|
|
224
196
|
* of them to count what a run actually did.
|
|
225
|
-
* @returns {{ run: (question: string, hooks?: { signal?: AbortSignal }) => Promise<any> }}
|
|
226
197
|
*/
|
|
227
198
|
export function createLongHorizonAgent(options) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
ok: result.program !== undefined, attempts: result.reuse?.authorCalls ?? 0, errors: result.errors ?? [] });
|
|
296
|
-
trajectory.add({
|
|
297
|
-
kind: 'program', depth, ok: result.ok, steps: result.steps ?? [],
|
|
298
|
-
subcalls: result.subcalls ?? 0, failed: result.failed ?? 0,
|
|
299
|
-
slot: result.answer?.slot, answer: result.answer?.text,
|
|
300
|
-
...(result.stopped === undefined ? {} : { stopped: result.stopped }),
|
|
301
|
-
});
|
|
302
|
-
return { ...result, depth };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* One child: its own scoped workspace, seeded with the one piece it
|
|
307
|
-
* was given, and its own program over it.
|
|
308
|
-
* @param {number} depth
|
|
309
|
-
* @param {any} parentEnv
|
|
310
|
-
* @param {string} name - the parent's slot this child works on
|
|
311
|
-
* @param {string} prompt
|
|
312
|
-
* @param {AbortSignal} [signal]
|
|
313
|
-
* @param {number} [index]
|
|
314
|
-
*/
|
|
315
|
-
async function child(depth, parentEnv, name, prompt, signal, index = 0) {
|
|
316
|
-
const piece = await parentEnv.read(name, { chars: subcallChars });
|
|
317
|
-
if (piece.error !== undefined) return { slot: name, error: piece.error, depth };
|
|
318
|
-
|
|
319
|
-
const scope = childScope(depth, index);
|
|
320
|
-
const env = environmentFactory({
|
|
321
|
-
ledger: parentEnv.ledger,
|
|
322
|
-
scope,
|
|
323
|
-
compileQuery: options.compileQuery,
|
|
324
|
-
});
|
|
325
|
-
await env.put('corpus', piece.text, { kind: 'text' });
|
|
326
|
-
|
|
327
|
-
const result = await level(depth, env, prompt, signal);
|
|
328
|
-
trajectory.add({ kind: 'subcall', depth, slot: `${scope}corpus`, ok: result.ok === true });
|
|
329
|
-
|
|
330
|
-
if (result.ok !== true) {
|
|
331
|
-
// the failure travels UP with its depth and its address, so a
|
|
332
|
-
// parent's map records which branch failed and where to look
|
|
333
|
-
return { slot: name, depth, address: scope,
|
|
334
|
-
error: result.error ?? `child stopped: ${result.stopped ?? 'unknown'}` };
|
|
335
|
-
}
|
|
336
|
-
// only the child's ANSWER crosses the boundary — never its corpus,
|
|
337
|
-
// never its slots (D2, at every level and not just the root)
|
|
338
|
-
const complete = await readProgramAnswer(env, result.answer, { maxChars: maxAnswerChars });
|
|
339
|
-
if (!complete.ok) return { slot: name, depth, address: scope, error: complete.error };
|
|
340
|
-
try {
|
|
341
|
-
const items = recursiveItems(JSON.parse(complete.answer.text));
|
|
342
|
-
if (items === null) return { slot: name, depth, error: 'AI0209: child answer violates recursive shape' };
|
|
343
|
-
return items.length === 1
|
|
344
|
-
? { ...items[0], slot: name, depth, address: scope }
|
|
345
|
-
: { slot: name, depth, address: scope, items };
|
|
346
|
-
}
|
|
347
|
-
catch {
|
|
348
|
-
return { slot: name, depth, address: scope, error: 'AI0209: child answer is not complete JSON' };
|
|
199
|
+
const { client, environment, createProgramAuthor: authorFactory, createProgramRunner: runnerFactory, createEnvironment: environmentFactory, } = options;
|
|
200
|
+
const { depth: maxDepth, clamped } = resolveDepth(options.depth);
|
|
201
|
+
let account = createBudgetAccount(options.budget ?? {}, options.clock);
|
|
202
|
+
let trajectory = createTrajectory();
|
|
203
|
+
const subcallChars = options.subcallChars ?? 8000;
|
|
204
|
+
const maxAnswerChars = options.maxAnswerChars ?? 200000;
|
|
205
|
+
if (!Number.isSafeInteger(maxAnswerChars) || maxAnswerChars < 1)
|
|
206
|
+
throw new RangeError('maxAnswerChars must be a positive safe integer');
|
|
207
|
+
/**
|
|
208
|
+
* One level: author a program over `env`, run it, answer.
|
|
209
|
+
* @param [signal]
|
|
210
|
+
*/
|
|
211
|
+
async function level(depth, env, question, signal) {
|
|
212
|
+
const stop = account.stop();
|
|
213
|
+
if (stop !== null)
|
|
214
|
+
return { ok: false, depth, stopped: stop, answer: null };
|
|
215
|
+
// every authoring call is charged where it HAPPENS, not once per
|
|
216
|
+
// level: structured generation may spend up to `1 + maxRepairs`
|
|
217
|
+
// calls on one document, and an account that counted the level
|
|
218
|
+
// instead of the calls would let a repair loop run free — the exact
|
|
219
|
+
// shape of overspend a shared budget exists to stop. The provider's
|
|
220
|
+
// usage is captured here too, which the generator's return value
|
|
221
|
+
// does not carry.
|
|
222
|
+
const author = authorFactory({
|
|
223
|
+
client, account, selectModel: options.selectModel, limits: options.limits,
|
|
224
|
+
depth, onRoute: (event) => { trajectory.add({ kind: 'route', depth, ...event }); options.onRoute?.(event); },
|
|
225
|
+
environment: env,
|
|
226
|
+
compileQuery: options.compileQuery,
|
|
227
|
+
createStructuredOutput: options.createStructuredOutput,
|
|
228
|
+
querySchema: options.querySchema,
|
|
229
|
+
recursive: true, analyzeQuery: options.analyzeQuery, annotateTypes: options.annotateTypes,
|
|
230
|
+
});
|
|
231
|
+
const runner = runnerFactory({
|
|
232
|
+
environment: env,
|
|
233
|
+
client,
|
|
234
|
+
compileQuery: options.compileQuery,
|
|
235
|
+
recursive: true, analyzeQuery: options.analyzeQuery, annotateTypes: options.annotateTypes,
|
|
236
|
+
account, selectModel: options.selectModel, limits: options.limits,
|
|
237
|
+
depth, onRoute: (event) => { trajectory.add({ kind: 'route', depth, ...event }); options.onRoute?.(event); },
|
|
238
|
+
maxSubcalls: options.maxSubcalls,
|
|
239
|
+
maxConcurrentSubcalls: options.maxConcurrentSubcalls,
|
|
240
|
+
// the recursion point, and the only one: below the cap a piece is
|
|
241
|
+
// worth a whole child agent; at the cap it is worth one model call
|
|
242
|
+
subcall: depth >= maxDepth ? undefined : (name, prompt, sig, index) => child(depth + 1, env, name, prompt, sig, index),
|
|
243
|
+
});
|
|
244
|
+
let result;
|
|
245
|
+
try {
|
|
246
|
+
result = await createProgramSession({
|
|
247
|
+
...options, environment: env, author, runner,
|
|
248
|
+
recursive: true, reuse: depth === 0 ? options.reuse : undefined
|
|
249
|
+
}).run(question, { signal });
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
trajectory.add({ kind: 'author', depth, ok: false });
|
|
253
|
+
return { ok: false, depth, stopped: account.stop(), error: error.message, answer: null };
|
|
254
|
+
}
|
|
255
|
+
trajectory.add({
|
|
256
|
+
kind: result.reuse?.reused ? 'reuse' : 'author', depth,
|
|
257
|
+
ok: result.program !== undefined, attempts: result.reuse?.authorCalls ?? 0, errors: result.errors ?? []
|
|
258
|
+
});
|
|
259
|
+
trajectory.add({
|
|
260
|
+
kind: 'program', depth, ok: result.ok, steps: result.steps ?? [],
|
|
261
|
+
subcalls: result.subcalls ?? 0, failed: result.failed ?? 0,
|
|
262
|
+
slot: result.answer?.slot, answer: result.answer?.text,
|
|
263
|
+
...(result.stopped === undefined ? {} : { stopped: result.stopped }),
|
|
264
|
+
});
|
|
265
|
+
return { ...result, depth };
|
|
349
266
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
267
|
+
/**
|
|
268
|
+
* One child: its own scoped workspace, seeded with the one piece it
|
|
269
|
+
* was given, and its own program over it.
|
|
270
|
+
* @param name - the parent's slot this child works on
|
|
271
|
+
* @param [signal]
|
|
272
|
+
* @param [index]
|
|
273
|
+
*/
|
|
274
|
+
async function child(depth, parentEnv, name, prompt, signal, index = 0) {
|
|
275
|
+
const piece = await parentEnv.read(name, { chars: subcallChars });
|
|
276
|
+
if (piece.error !== undefined)
|
|
277
|
+
return { slot: name, error: piece.error, depth };
|
|
278
|
+
const scope = childScope(depth, index);
|
|
279
|
+
const env = environmentFactory({
|
|
280
|
+
ledger: parentEnv.ledger,
|
|
281
|
+
scope,
|
|
282
|
+
compileQuery: options.compileQuery,
|
|
283
|
+
});
|
|
284
|
+
await env.put('corpus', piece.text, { kind: 'text' });
|
|
285
|
+
const result = await level(depth, env, prompt, signal);
|
|
286
|
+
trajectory.add({ kind: 'subcall', depth, slot: `${scope}corpus`, ok: result.ok === true });
|
|
287
|
+
if (result.ok !== true) {
|
|
288
|
+
// the failure travels UP with its depth and its address, so a
|
|
289
|
+
// parent's map records which branch failed and where to look
|
|
290
|
+
return {
|
|
291
|
+
slot: name, depth, address: scope,
|
|
292
|
+
error: result.error ?? `child stopped: ${result.stopped ?? 'unknown'}`
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
// only the child's ANSWER crosses the boundary — never its corpus,
|
|
296
|
+
// never its slots (D2, at every level and not just the root)
|
|
297
|
+
const complete = await readProgramAnswer(env, result.answer, { maxChars: maxAnswerChars });
|
|
298
|
+
if (!complete.ok)
|
|
299
|
+
return { slot: name, depth, address: scope, error: complete.error };
|
|
300
|
+
try {
|
|
301
|
+
const items = recursiveItems(JSON.parse(complete.answer.text));
|
|
302
|
+
if (items === null)
|
|
303
|
+
return { slot: name, depth, error: 'AI0209: child answer violates recursive shape' };
|
|
304
|
+
return items.length === 1
|
|
305
|
+
? { ...items[0], slot: name, depth, address: scope }
|
|
306
|
+
: { slot: name, depth, address: scope, items };
|
|
363
307
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
308
|
+
catch {
|
|
309
|
+
return { slot: name, depth, address: scope, error: 'AI0209: child answer is not complete JSON' };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
let pending = Promise.resolve();
|
|
313
|
+
return {
|
|
314
|
+
run(question, hooks = {}) {
|
|
315
|
+
const next = pending.then(async () => {
|
|
316
|
+
account = createBudgetAccount(options.budget ?? {}, options.clock);
|
|
317
|
+
trajectory = createTrajectory();
|
|
318
|
+
if (clamped) {
|
|
319
|
+
trajectory.add({
|
|
320
|
+
kind: 'note', depth: 0,
|
|
321
|
+
note: `depth ${options.depth} was asked for; ${maxDepth} is the cap this package runs`,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const result = await level(0, environment, question, hooks.signal);
|
|
325
|
+
const stopped = account.stop();
|
|
326
|
+
return {
|
|
327
|
+
ok: result.ok === true,
|
|
328
|
+
answer: result.answer ?? null,
|
|
329
|
+
depth: maxDepth,
|
|
330
|
+
depthClamped: clamped,
|
|
331
|
+
stopReason: result.stopped ?? stopped ?? null,
|
|
332
|
+
spent: account.spent(),
|
|
333
|
+
remaining: account.remaining(),
|
|
334
|
+
trajectory: trajectory.entries(),
|
|
335
|
+
summary: trajectory.summary(),
|
|
336
|
+
...(result.error === undefined ? {} : { error: result.error }),
|
|
337
|
+
...(result.errors === undefined ? {} : { errors: result.errors }),
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
pending = next.then(() => { }, () => { });
|
|
341
|
+
return next;
|
|
342
|
+
},
|
|
343
|
+
};
|
|
384
344
|
}
|