@tangleai/agents 0.21.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 +30 -0
- package/LICENSE +21 -0
- package/README.md +854 -0
- package/package.json +85 -0
- package/src/agent.d.ts +160 -0
- package/src/agent.js +1021 -0
- package/src/index.d.ts +11 -0
- package/src/index.js +13 -0
- package/src/program-result.d.ts +111 -0
- package/src/program-result.js +48 -0
- package/src/program-session.d.ts +48 -0
- package/src/program-session.js +121 -0
- package/src/program-shape.d.ts +21 -0
- package/src/program-shape.js +53 -0
- package/src/program.d.ts +244 -0
- package/src/program.js +940 -0
- package/src/recursive.d.ts +148 -0
- package/src/recursive.js +384 -0
- package/src/refine.d.ts +58 -0
- package/src/refine.js +599 -0
- package/src/schemas/program.d.ts +82 -0
- package/src/schemas/program.js +205 -0
- package/src/toolbox.d.ts +55 -0
- package/src/toolbox.js +178 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One account, charged by every model call at every depth.
|
|
3
|
+
*
|
|
4
|
+
* This is the whole of "budgets are global, not per-call". It is a
|
|
5
|
+
* mutable object on purpose: a snapshot passed down would let each
|
|
6
|
+
* branch spend the full budget independently, which is precisely the
|
|
7
|
+
* runaway the cap exists to prevent.
|
|
8
|
+
*
|
|
9
|
+
* @param {{ turns?: number, tokens?: number, ms?: number,
|
|
10
|
+
* spent?: { turns?: number, tokens?: number, ms?: number } }} [budget]
|
|
11
|
+
* @param {() => number} [clock]
|
|
12
|
+
* **Turns are reserved, tokens are settled**, and the split is what makes
|
|
13
|
+
* a concurrent fan-out bounded. A budget checked before a call and
|
|
14
|
+
* charged after it can be beaten by concurrency: four sub-calls launched
|
|
15
|
+
* together all see the same unspent budget and all spend it. Taking the
|
|
16
|
+
* turn at the moment the call is launched closes that for the dimension
|
|
17
|
+
* that can be known in advance. Tokens cannot be — nobody knows what a
|
|
18
|
+
* reply will cost until it arrives — so a token budget may overshoot by
|
|
19
|
+
* at most `maxConcurrentSubcalls - 1` calls' worth. That is stated here
|
|
20
|
+
* rather than hidden, because a caller sizing a budget needs it.
|
|
21
|
+
*
|
|
22
|
+
* @returns {{ reserve: () => void, settle: (usage: any, text?: string) => void,
|
|
23
|
+
* stop: () => string | null, spent: () => { turns: number, tokens: number, ms: number },
|
|
24
|
+
* remaining: () => Record<string, number | null> }}
|
|
25
|
+
*/
|
|
26
|
+
export function createBudgetAccount(budget?: {
|
|
27
|
+
turns?: number;
|
|
28
|
+
tokens?: number;
|
|
29
|
+
ms?: number;
|
|
30
|
+
spent?: {
|
|
31
|
+
turns?: number;
|
|
32
|
+
tokens?: number;
|
|
33
|
+
ms?: number;
|
|
34
|
+
};
|
|
35
|
+
}, clock?: () => number): {
|
|
36
|
+
reserve: () => void;
|
|
37
|
+
settle: (usage: any, text?: string) => void;
|
|
38
|
+
stop: () => string | null;
|
|
39
|
+
spent: () => {
|
|
40
|
+
turns: number;
|
|
41
|
+
tokens: number;
|
|
42
|
+
ms: number;
|
|
43
|
+
};
|
|
44
|
+
remaining: () => Record<string, number | null>;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The record of what a run actually did.
|
|
48
|
+
*
|
|
49
|
+
* A long autonomous run is only reviewable if it says what it did, and
|
|
50
|
+
* "what it did" is not a log line — it is the program it authored, the
|
|
51
|
+
* calls it made and the slots it wrote, at every depth. Kept as plain
|
|
52
|
+
* data so a host can persist it, render it, or diff two runs.
|
|
53
|
+
*
|
|
54
|
+
* @returns {{ add: (entry: any) => any, entries: () => any[],
|
|
55
|
+
* summary: () => { steps: number, calls: number, slots: number, depths: number[] } }}
|
|
56
|
+
*/
|
|
57
|
+
export function createTrajectory(): {
|
|
58
|
+
add: (entry: any) => any;
|
|
59
|
+
entries: () => any[];
|
|
60
|
+
summary: () => {
|
|
61
|
+
steps: number;
|
|
62
|
+
calls: number;
|
|
63
|
+
slots: number;
|
|
64
|
+
depths: number[];
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Clamp a requested depth to what this package will actually run.
|
|
69
|
+
*
|
|
70
|
+
* Returns the depth AND whether it was clamped, because a run that
|
|
71
|
+
* quietly did less than it was asked to is indistinguishable from one
|
|
72
|
+
* that failed to find anything — the caller has to be able to tell.
|
|
73
|
+
* @param {number | undefined} requested
|
|
74
|
+
* @returns {{ depth: number, clamped: boolean }}
|
|
75
|
+
*/
|
|
76
|
+
export function resolveDepth(requested: number | undefined): {
|
|
77
|
+
depth: number;
|
|
78
|
+
clamped: boolean;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* The long-horizon entry point: hand it a question and an environment,
|
|
82
|
+
* get back an answer and the trajectory that produced it.
|
|
83
|
+
*
|
|
84
|
+
* This is the Phase B counterpart to `createAgent`, and the docs say
|
|
85
|
+
* plainly which to reach for: `createAgent` is a bounded tool loop for a
|
|
86
|
+
* CONVERSATION, this is for a JOB — something with a corpus, a question
|
|
87
|
+
* over all of it, and no user waiting to answer a follow-up.
|
|
88
|
+
*
|
|
89
|
+
* @param {{ client: any, environment: any, compileQuery?: any,
|
|
90
|
+
* analyzeQuery?: any, annotateTypes?: any, reuse?: any,
|
|
91
|
+
* selectModel?: any, limits?: any, onRoute?: any,
|
|
92
|
+
* createStructuredOutput: (options: any) => { generate: Function },
|
|
93
|
+
* createProgramAuthor: (options: any) => { author: Function },
|
|
94
|
+
* createProgramRunner: (options: any) => { run: Function },
|
|
95
|
+
* createEnvironment: (options: any) => any,
|
|
96
|
+
* querySchema?: any, budget?: any, depth?: number,
|
|
97
|
+
* maxConcurrentSubcalls?: number, maxSubcalls?: number,
|
|
98
|
+
* subcallChars?: number, maxAnswerChars?: number, clock?: () => number }} options
|
|
99
|
+
* The three factories and `createEnvironment` are injected for the
|
|
100
|
+
* same reason everything heavy in this package is: it keeps this
|
|
101
|
+
* module free of a cycle with `program.js` and lets a probe wrap any
|
|
102
|
+
* of them to count what a run actually did.
|
|
103
|
+
* @returns {{ run: (question: string, hooks?: { signal?: AbortSignal }) => Promise<any> }}
|
|
104
|
+
*/
|
|
105
|
+
export function createLongHorizonAgent(options: {
|
|
106
|
+
client: any;
|
|
107
|
+
environment: any;
|
|
108
|
+
compileQuery?: any;
|
|
109
|
+
analyzeQuery?: any;
|
|
110
|
+
annotateTypes?: any;
|
|
111
|
+
reuse?: any;
|
|
112
|
+
selectModel?: any;
|
|
113
|
+
limits?: any;
|
|
114
|
+
onRoute?: any;
|
|
115
|
+
createStructuredOutput: (options: any) => {
|
|
116
|
+
generate: Function;
|
|
117
|
+
};
|
|
118
|
+
createProgramAuthor: (options: any) => {
|
|
119
|
+
author: Function;
|
|
120
|
+
};
|
|
121
|
+
createProgramRunner: (options: any) => {
|
|
122
|
+
run: Function;
|
|
123
|
+
};
|
|
124
|
+
createEnvironment: (options: any) => any;
|
|
125
|
+
querySchema?: any;
|
|
126
|
+
budget?: any;
|
|
127
|
+
depth?: number;
|
|
128
|
+
maxConcurrentSubcalls?: number;
|
|
129
|
+
maxSubcalls?: number;
|
|
130
|
+
subcallChars?: number;
|
|
131
|
+
maxAnswerChars?: number;
|
|
132
|
+
clock?: () => number;
|
|
133
|
+
}): {
|
|
134
|
+
run: (question: string, hooks?: {
|
|
135
|
+
signal?: AbortSignal;
|
|
136
|
+
}) => Promise<any>;
|
|
137
|
+
};
|
|
138
|
+
/** The deepest a tree may go, whatever it asks for. */
|
|
139
|
+
export const MAX_DEPTH: 3;
|
|
140
|
+
/** The conservative default; local depth measurements have not justified raising it. */
|
|
141
|
+
export const DEFAULT_DEPTH: 1;
|
|
142
|
+
/**
|
|
143
|
+
* The dimensions a tree is bounded by. The same three the agent loop
|
|
144
|
+
* uses, in the same order, because a caller who has budgeted one should
|
|
145
|
+
* not have to learn a second vocabulary for the other.
|
|
146
|
+
*/
|
|
147
|
+
export const BUDGET_DIMENSIONS: readonly ["turns", "tokens", "ms"];
|
|
148
|
+
export function childScope(depth: any, index: any): string;
|
package/src/recursive.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* The long-horizon entry point: a job, not a conversation.
|
|
4
|
+
*
|
|
5
|
+
* `createAgent` is a bounded tool loop — you talk to it. This is the
|
|
6
|
+
* other shape: you hand it a corpus and a question, and it authors a
|
|
7
|
+
* program over an environment, fans model calls out across the pieces,
|
|
8
|
+
* and may let any of those calls be **another agent over its own slice**
|
|
9
|
+
* — depth-capped, budget-shared, and traced at every level.
|
|
10
|
+
*
|
|
11
|
+
* Four things about recursion are load-bearing, and each one exists
|
|
12
|
+
* because the RLM paper reports the failure it prevents:
|
|
13
|
+
*
|
|
14
|
+
* - **Depth defaults to 1 and caps at 3.** The paper runs depths 0–3
|
|
15
|
+
* and finds most of its gain at depth 1, with depth 3 helping only on
|
|
16
|
+
* information-dense tasks. Depth multiplies cost on every task where
|
|
17
|
+
* it does not help, so the default is the depth that usually pays and
|
|
18
|
+
* the cap is asserted rather than documented — a program asking for
|
|
19
|
+
* more is clamped, and the clamp is recorded in the trajectory rather
|
|
20
|
+
* than applied silently.
|
|
21
|
+
* - **Budgets are shared by the whole tree, not per call.** Depth times
|
|
22
|
+
* fan-out is multiplicative: depth 3 fanning 20 ways at each level is
|
|
23
|
+
* 8000 leaf calls. A per-call budget cannot bound that. One account is
|
|
24
|
+
* threaded through every depth, charged by every model call, and when
|
|
25
|
+
* it runs out the tree stops with a named reason and **leaves its
|
|
26
|
+
* partial work in slots** — which is why Phase A came first: a
|
|
27
|
+
* budget-stopped run is resumable because its work is durable.
|
|
28
|
+
* - **A child cannot reach a sibling.** Each child gets an environment
|
|
29
|
+
* scoped to a prefix of its own. Isolation is what makes one failing
|
|
30
|
+
* branch containable instead of a corruption of the shared corpus.
|
|
31
|
+
* - **A child's failure is a value.** A child whose program will not
|
|
32
|
+
* compile returns `{ error, depth, slot }` to its parent, recorded and
|
|
33
|
+
* addressable. The paper reports syntax errors propagating silently
|
|
34
|
+
* through recursion; here the parent's map completes with a recorded
|
|
35
|
+
* failure in the child's own result slot, exactly as a failed leaf
|
|
36
|
+
* sub-call does.
|
|
37
|
+
*
|
|
38
|
+
* **What is not here, stated rather than implied:** guardrails for
|
|
39
|
+
* recursive LM systems are under-explored, and this package does not
|
|
40
|
+
* pretend otherwise. Three bounds exist and they are the only three —
|
|
41
|
+
* the depth cap, the shared budget and the abort signal. There is no
|
|
42
|
+
* general proof of a child's factual correctness or loop detection beyond
|
|
43
|
+
* depth. Recursive envelopes are compile/runtime checked; hosts may also
|
|
44
|
+
* opt into checked root reuse and per-call route limits.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { excerpt } from '@jarenjs/core/chunk';
|
|
48
|
+
import { recursiveItems } from './program-shape.js';
|
|
49
|
+
import { createProgramSession } from './program-session.js';
|
|
50
|
+
import { readProgramAnswer } from './program-result.js';
|
|
51
|
+
|
|
52
|
+
/** The deepest a tree may go, whatever it asks for. */
|
|
53
|
+
export const MAX_DEPTH = 3;
|
|
54
|
+
|
|
55
|
+
/** The conservative default; local depth measurements have not justified raising it. */
|
|
56
|
+
export const DEFAULT_DEPTH = 1;
|
|
57
|
+
|
|
58
|
+
/** How much of a child's answer its parent's trajectory keeps. */
|
|
59
|
+
const TRACE_EXCERPT = 200;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The dimensions a tree is bounded by. The same three the agent loop
|
|
63
|
+
* uses, in the same order, because a caller who has budgeted one should
|
|
64
|
+
* not have to learn a second vocabulary for the other.
|
|
65
|
+
*/
|
|
66
|
+
export const BUDGET_DIMENSIONS = /** @type {const} */ (['turns', 'tokens', 'ms']);
|
|
67
|
+
|
|
68
|
+
/** Characters per token when a provider reports no usage — the agent
|
|
69
|
+
* loop's ratio, stated there and unchanged here. */
|
|
70
|
+
const TOKEN_CHARS = 4;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* One account, charged by every model call at every depth.
|
|
74
|
+
*
|
|
75
|
+
* This is the whole of "budgets are global, not per-call". It is a
|
|
76
|
+
* mutable object on purpose: a snapshot passed down would let each
|
|
77
|
+
* branch spend the full budget independently, which is precisely the
|
|
78
|
+
* runaway the cap exists to prevent.
|
|
79
|
+
*
|
|
80
|
+
* @param {{ turns?: number, tokens?: number, ms?: number,
|
|
81
|
+
* spent?: { turns?: number, tokens?: number, ms?: number } }} [budget]
|
|
82
|
+
* @param {() => number} [clock]
|
|
83
|
+
* **Turns are reserved, tokens are settled**, and the split is what makes
|
|
84
|
+
* a concurrent fan-out bounded. A budget checked before a call and
|
|
85
|
+
* charged after it can be beaten by concurrency: four sub-calls launched
|
|
86
|
+
* together all see the same unspent budget and all spend it. Taking the
|
|
87
|
+
* turn at the moment the call is launched closes that for the dimension
|
|
88
|
+
* that can be known in advance. Tokens cannot be — nobody knows what a
|
|
89
|
+
* reply will cost until it arrives — so a token budget may overshoot by
|
|
90
|
+
* at most `maxConcurrentSubcalls - 1` calls' worth. That is stated here
|
|
91
|
+
* rather than hidden, because a caller sizing a budget needs it.
|
|
92
|
+
*
|
|
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
|
+
*/
|
|
97
|
+
export function createBudgetAccount(budget = {}, clock = Date.now) {
|
|
98
|
+
const started = clock();
|
|
99
|
+
const spent = {
|
|
100
|
+
turns: budget.spent?.turns ?? 0,
|
|
101
|
+
tokens: budget.spent?.tokens ?? 0,
|
|
102
|
+
ms: budget.spent?.ms ?? 0,
|
|
103
|
+
};
|
|
104
|
+
const elapsed = () => spent.ms + (clock() - started);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
/** A call is about to be made. Taken now, not after, so concurrent
|
|
108
|
+
* launches cannot each see the same unspent turn. */
|
|
109
|
+
reserve() {
|
|
110
|
+
spent.turns += 1;
|
|
111
|
+
},
|
|
112
|
+
/**
|
|
113
|
+
* A call came back. The provider's own usage wins; the character
|
|
114
|
+
* estimate is the fallback and it is only computed when a token
|
|
115
|
+
* budget exists to spend it against.
|
|
116
|
+
* @param {any} usage - the provider's usage block, if any
|
|
117
|
+
* @param {string} [text] - what was sent and returned, for the estimate
|
|
118
|
+
*/
|
|
119
|
+
settle(usage, text) {
|
|
120
|
+
const reported = typeof usage?.total_tokens === 'number' && usage.total_tokens > 0
|
|
121
|
+
? usage.total_tokens
|
|
122
|
+
: (usage?.prompt_tokens ?? 0) + (usage?.completion_tokens ?? 0);
|
|
123
|
+
if (reported > 0) spent.tokens += reported;
|
|
124
|
+
else if (typeof budget.tokens === 'number' && text !== undefined) {
|
|
125
|
+
spent.tokens += Math.ceil(text.length / TOKEN_CHARS);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
/** The dimension that is spent, or null. Checked BEFORE a call. */
|
|
129
|
+
stop() {
|
|
130
|
+
const now = { turns: spent.turns, tokens: spent.tokens, ms: elapsed() };
|
|
131
|
+
for (const dimension of BUDGET_DIMENSIONS) {
|
|
132
|
+
const limit = budget[dimension];
|
|
133
|
+
if (typeof limit === 'number' && now[dimension] >= limit) return `budget-${dimension}`;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
},
|
|
137
|
+
spent: () => ({ turns: spent.turns, tokens: spent.tokens, ms: elapsed() }),
|
|
138
|
+
remaining: () => Object.fromEntries(BUDGET_DIMENSIONS.map((name) => [name,
|
|
139
|
+
typeof budget[name] === 'number'
|
|
140
|
+
? Math.max(0, budget[name] - (name === 'ms' ? elapsed() : spent[name]))
|
|
141
|
+
: null])),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The record of what a run actually did.
|
|
147
|
+
*
|
|
148
|
+
* A long autonomous run is only reviewable if it says what it did, and
|
|
149
|
+
* "what it did" is not a log line — it is the program it authored, the
|
|
150
|
+
* calls it made and the slots it wrote, at every depth. Kept as plain
|
|
151
|
+
* data so a host can persist it, render it, or diff two runs.
|
|
152
|
+
*
|
|
153
|
+
* @returns {{ add: (entry: any) => any, entries: () => any[],
|
|
154
|
+
* summary: () => { steps: number, calls: number, slots: number, depths: number[] } }}
|
|
155
|
+
*/
|
|
156
|
+
export function createTrajectory() {
|
|
157
|
+
/** @type {any[]} */
|
|
158
|
+
const entries = [];
|
|
159
|
+
return {
|
|
160
|
+
/**
|
|
161
|
+
* Record one thing that happened. Returns the entry so a caller can
|
|
162
|
+
* hold it — never the array, which would let a caller mutate history.
|
|
163
|
+
* @param {any} entry
|
|
164
|
+
*/
|
|
165
|
+
add(entry) {
|
|
166
|
+
const record = { seq: entries.length, ...entry };
|
|
167
|
+
if (typeof record.answer === 'string') record.answer = excerpt(record.answer, TRACE_EXCERPT);
|
|
168
|
+
entries.push(record);
|
|
169
|
+
return record;
|
|
170
|
+
},
|
|
171
|
+
entries: () => entries.slice(),
|
|
172
|
+
summary: () => ({
|
|
173
|
+
steps: entries.filter((e) => e.kind === 'program').length,
|
|
174
|
+
calls: entries.filter((e) => e.kind === 'subcall' || e.kind === 'author').length,
|
|
175
|
+
slots: entries.filter((e) => e.slot !== undefined).length,
|
|
176
|
+
depths: [...new Set(entries.map((e) => e.depth ?? 0))].sort((a, b) => a - b),
|
|
177
|
+
}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Clamp a requested depth to what this package will actually run.
|
|
183
|
+
*
|
|
184
|
+
* Returns the depth AND whether it was clamped, because a run that
|
|
185
|
+
* quietly did less than it was asked to is indistinguishable from one
|
|
186
|
+
* 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
|
+
*/
|
|
190
|
+
export function resolveDepth(requested) {
|
|
191
|
+
if (requested === undefined || requested === null) return { depth: DEFAULT_DEPTH, clamped: false };
|
|
192
|
+
const asked = Math.floor(Number(requested));
|
|
193
|
+
if (!Number.isFinite(asked) || asked < 0) return { depth: DEFAULT_DEPTH, clamped: true };
|
|
194
|
+
return { depth: Math.min(asked, MAX_DEPTH), clamped: asked > MAX_DEPTH };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The slot prefix a child at `depth` working on `index` owns. Derived,
|
|
198
|
+
* like every other address in this package, so nothing has to store a
|
|
199
|
+
* mapping from a child to its workspace. */
|
|
200
|
+
export const childScope = (depth, index) => `child/${depth}/${index}/`;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The long-horizon entry point: hand it a question and an environment,
|
|
204
|
+
* get back an answer and the trajectory that produced it.
|
|
205
|
+
*
|
|
206
|
+
* This is the Phase B counterpart to `createAgent`, and the docs say
|
|
207
|
+
* plainly which to reach for: `createAgent` is a bounded tool loop for a
|
|
208
|
+
* CONVERSATION, this is for a JOB — something with a corpus, a question
|
|
209
|
+
* over all of it, and no user waiting to answer a follow-up.
|
|
210
|
+
*
|
|
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
|
+
* The three factories and `createEnvironment` are injected for the
|
|
222
|
+
* same reason everything heavy in this package is: it keeps this
|
|
223
|
+
* module free of a cycle with `program.js` and lets a probe wrap any
|
|
224
|
+
* of them to count what a run actually did.
|
|
225
|
+
* @returns {{ run: (question: string, hooks?: { signal?: AbortSignal }) => Promise<any> }}
|
|
226
|
+
*/
|
|
227
|
+
export function createLongHorizonAgent(options) {
|
|
228
|
+
const {
|
|
229
|
+
client, environment, createProgramAuthor: authorFactory,
|
|
230
|
+
createProgramRunner: runnerFactory, createEnvironment: environmentFactory,
|
|
231
|
+
} = options;
|
|
232
|
+
const { depth: maxDepth, clamped } = resolveDepth(options.depth);
|
|
233
|
+
let account = createBudgetAccount(options.budget ?? {}, options.clock);
|
|
234
|
+
let trajectory = createTrajectory();
|
|
235
|
+
const subcallChars = options.subcallChars ?? 8000;
|
|
236
|
+
const maxAnswerChars = options.maxAnswerChars ?? 200000;
|
|
237
|
+
if (!Number.isSafeInteger(maxAnswerChars) || maxAnswerChars < 1)
|
|
238
|
+
throw new RangeError('maxAnswerChars must be a positive safe integer');
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* One level: author a program over `env`, run it, answer.
|
|
243
|
+
* @param {number} depth
|
|
244
|
+
* @param {any} env
|
|
245
|
+
* @param {string} question
|
|
246
|
+
* @param {AbortSignal} [signal]
|
|
247
|
+
*/
|
|
248
|
+
async function level(depth, env, question, signal) {
|
|
249
|
+
const stop = account.stop();
|
|
250
|
+
if (stop !== null) return { ok: false, depth, stopped: stop, answer: null };
|
|
251
|
+
|
|
252
|
+
// every authoring call is charged where it HAPPENS, not once per
|
|
253
|
+
// level: structured generation may spend up to `1 + maxRepairs`
|
|
254
|
+
// calls on one document, and an account that counted the level
|
|
255
|
+
// instead of the calls would let a repair loop run free — the exact
|
|
256
|
+
// shape of overspend a shared budget exists to stop. The provider's
|
|
257
|
+
// usage is captured here too, which the generator's return value
|
|
258
|
+
// does not carry.
|
|
259
|
+
|
|
260
|
+
const author = authorFactory({
|
|
261
|
+
client, account, selectModel: options.selectModel, limits: options.limits,
|
|
262
|
+
depth, onRoute: (event) => { trajectory.add({ kind: 'route', depth, ...event }); options.onRoute?.(event); },
|
|
263
|
+
environment: env,
|
|
264
|
+
compileQuery: options.compileQuery,
|
|
265
|
+
createStructuredOutput: options.createStructuredOutput,
|
|
266
|
+
querySchema: options.querySchema,
|
|
267
|
+
recursive: true, analyzeQuery: options.analyzeQuery, annotateTypes: options.annotateTypes,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const runner = runnerFactory({
|
|
271
|
+
environment: env,
|
|
272
|
+
client,
|
|
273
|
+
compileQuery: options.compileQuery,
|
|
274
|
+
recursive: true, analyzeQuery: options.analyzeQuery, annotateTypes: options.annotateTypes,
|
|
275
|
+
account, selectModel: options.selectModel, limits: options.limits,
|
|
276
|
+
depth, onRoute: (event) => { trajectory.add({ kind: 'route', depth, ...event }); options.onRoute?.(event); },
|
|
277
|
+
maxSubcalls: options.maxSubcalls,
|
|
278
|
+
maxConcurrentSubcalls: options.maxConcurrentSubcalls,
|
|
279
|
+
// the recursion point, and the only one: below the cap a piece is
|
|
280
|
+
// worth a whole child agent; at the cap it is worth one model call
|
|
281
|
+
subcall: depth >= maxDepth ? undefined : (name, prompt, sig, index) =>
|
|
282
|
+
child(depth + 1, env, name, prompt, sig, index),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
let result;
|
|
286
|
+
try {
|
|
287
|
+
result = await createProgramSession({ ...options, environment: env, author, runner,
|
|
288
|
+
recursive: true, reuse: depth === 0 ? options.reuse : undefined }).run(question, { signal });
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
trajectory.add({ kind: 'author', depth, ok: false });
|
|
292
|
+
return { ok: false, depth, stopped: account.stop(), error: /** @type {Error} */ (error).message, answer: null };
|
|
293
|
+
}
|
|
294
|
+
trajectory.add({ kind: result.reuse?.reused ? 'reuse' : 'author', depth,
|
|
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' };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let pending = Promise.resolve();
|
|
353
|
+
return {
|
|
354
|
+
run(question, hooks = {}) {
|
|
355
|
+
const next = pending.then(async () => {
|
|
356
|
+
account = createBudgetAccount(options.budget ?? {}, options.clock);
|
|
357
|
+
trajectory = createTrajectory();
|
|
358
|
+
if (clamped) {
|
|
359
|
+
trajectory.add({
|
|
360
|
+
kind: 'note', depth: 0,
|
|
361
|
+
note: `depth ${options.depth} was asked for; ${maxDepth} is the cap this package runs`,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
const result = await level(0, environment, question, hooks.signal);
|
|
365
|
+
const stopped = account.stop();
|
|
366
|
+
return {
|
|
367
|
+
ok: result.ok === true,
|
|
368
|
+
answer: result.answer ?? null,
|
|
369
|
+
depth: maxDepth,
|
|
370
|
+
depthClamped: clamped,
|
|
371
|
+
stopReason: result.stopped ?? stopped ?? null,
|
|
372
|
+
spent: account.spent(),
|
|
373
|
+
remaining: account.remaining(),
|
|
374
|
+
trajectory: trajectory.entries(),
|
|
375
|
+
summary: trajectory.summary(),
|
|
376
|
+
...(result.error === undefined ? {} : { error: result.error }),
|
|
377
|
+
...(result.errors === undefined ? {} : { errors: result.errors }),
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
pending = next.then(() => {}, () => {});
|
|
381
|
+
return next;
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
package/src/refine.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run, as the few hundred characters worth putting in front of the
|
|
3
|
+
* model. Tool steps are preferred over wire messages: the steps ARE the
|
|
4
|
+
* evidence a memory would cite, and a transcript's assistant turns are
|
|
5
|
+
* mostly the model reading its own prose back.
|
|
6
|
+
* @param {any} trajectory - a `send` result, its `steps`, or wire messages
|
|
7
|
+
* @param {number} max
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function describeTrajectory(trajectory: any, max?: number): string;
|
|
11
|
+
/**
|
|
12
|
+
* Create a refiner over a ledger.
|
|
13
|
+
*
|
|
14
|
+
* @param {{ client: any, ledger: any,
|
|
15
|
+
* applyPatch?: ((document: any, patch: any[]) => any) | null,
|
|
16
|
+
* maxOps?: number, maxRepairs?: number, validator?: any,
|
|
17
|
+
* now?: () => string, trajectoryChars?: number,
|
|
18
|
+
* instructions?: string, deduplicate?: 'exact-evidence' }} options
|
|
19
|
+
* - `applyPatch` is the RFC 6902 seam: `(document, patch) => document`,
|
|
20
|
+
* normally `(doc, patch) => applyJSONPatch(doc, patch)` from
|
|
21
|
+
* `@jarenjs/json`. Absent, `refine`/`commit` decline with a stated
|
|
22
|
+
* reason rather than half-working.
|
|
23
|
+
* - `maxOps` caps the operations one refinement may propose (default
|
|
24
|
+
* 6, from the schema).
|
|
25
|
+
* - `maxRepairs` is how many failed rounds go back to the model with
|
|
26
|
+
* the errors before it declines (default 1).
|
|
27
|
+
* - `instructions` replaces the proposal prompt's task description
|
|
28
|
+
* (the state, the rules and the trajectory are always appended).
|
|
29
|
+
* - `now` returns an RFC 3339 timestamp, injected for deterministic
|
|
30
|
+
* tests exactly as the ledger injects its clock.
|
|
31
|
+
* - `deduplicate: 'exact-evidence'` skips new memories with byte-identical
|
|
32
|
+
* text and evidence and the same tags. Existing records are never merged
|
|
33
|
+
* or removed by this option; case, whitespace and independent citations
|
|
34
|
+
* remain distinct. The result reports every skipped proposal in `deduplicated`.
|
|
35
|
+
* @returns {{ state: () => Promise<any>,
|
|
36
|
+
* commit: (patch: any[]) => Promise<any>,
|
|
37
|
+
* refine: (trajectory: any, hooks?: { signal?: AbortSignal }) => Promise<any>,
|
|
38
|
+
* patchSchema: any }}
|
|
39
|
+
*/
|
|
40
|
+
export function createRefiner(options: {
|
|
41
|
+
client: any;
|
|
42
|
+
ledger: any;
|
|
43
|
+
applyPatch?: ((document: any, patch: any[]) => any) | null;
|
|
44
|
+
maxOps?: number;
|
|
45
|
+
maxRepairs?: number;
|
|
46
|
+
validator?: any;
|
|
47
|
+
now?: () => string;
|
|
48
|
+
trajectoryChars?: number;
|
|
49
|
+
instructions?: string;
|
|
50
|
+
deduplicate?: "exact-evidence";
|
|
51
|
+
}): {
|
|
52
|
+
state: () => Promise<any>;
|
|
53
|
+
commit: (patch: any[]) => Promise<any>;
|
|
54
|
+
refine: (trajectory: any, hooks?: {
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
}) => Promise<any>;
|
|
57
|
+
patchSchema: any;
|
|
58
|
+
};
|