toolnexus 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dist/agents/agent.d.ts +12 -1
- package/dist/agents/agent.js +43 -7
- package/dist/agents/compaction.d.ts +19 -0
- package/dist/agents/compaction.js +26 -4
- package/dist/agents/runtime.d.ts +5 -0
- package/dist/agents/runtime.js +13 -2
- package/dist/batteries.d.ts +223 -0
- package/dist/batteries.js +488 -0
- package/dist/builtin.d.ts +16 -0
- package/dist/builtin.js +315 -17
- package/dist/builtin_extras.d.ts +80 -0
- package/dist/builtin_extras.js +248 -0
- package/dist/classifier.d.ts +14 -0
- package/dist/classifier.js +34 -1
- package/dist/client.d.ts +91 -15
- package/dist/client.js +377 -110
- package/dist/index.d.ts +8 -0
- package/dist/index.js +11 -0
- package/dist/judge.d.ts +98 -0
- package/dist/judge.js +199 -0
- package/dist/mcp.d.ts +32 -1
- package/dist/mcp.js +184 -6
- package/dist/models.d.ts +20 -0
- package/dist/models.js +46 -0
- package/dist/promptcache.d.ts +21 -0
- package/dist/promptcache.js +59 -0
- package/dist/store.d.ts +32 -0
- package/dist/store.js +131 -0
- package/dist/tooltiers.d.ts +49 -0
- package/dist/tooltiers.js +197 -0
- package/dist/types.d.ts +19 -0
- package/package.json +2 -2
- package/src/agents/agent.ts +39 -8
- package/src/agents/compaction.ts +45 -4
- package/src/agents/runtime.ts +16 -2
- package/src/batteries.ts +614 -0
- package/src/builtin.ts +333 -18
- package/src/builtin_extras.ts +285 -0
- package/src/classifier.ts +36 -1
- package/src/client.ts +415 -115
- package/src/index.ts +31 -0
- package/src/judge.ts +305 -0
- package/src/mcp.ts +204 -7
- package/src/models.ts +56 -0
- package/src/promptcache.ts +63 -0
- package/src/store.ts +142 -0
- package/src/tooltiers.ts +221 -0
- package/src/types.ts +20 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in builtins for coding-agent hosts (SPEC §4C): background shells (`bash`
|
|
3
|
+
* run_in_background + `bash_output` + `kill_shell`), `websearch` over a host-supplied provider,
|
|
4
|
+
* and `notebook_edit`. None is on by default: a host that sets nothing sees the same ten builtins.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
+
const err = (output, metadata) => ({ output, isError: true, metadata });
|
|
8
|
+
const ok = (output, metadata) => ({ output, isError: false, metadata });
|
|
9
|
+
/**
|
|
10
|
+
* Owns the commands `bash` started with `run_in_background`. Pass one as
|
|
11
|
+
* `builtins.background`, and call `closeAll()` when the host shuts down — a background command
|
|
12
|
+
* is NOT tied to the turn that started it, so nothing else stops it.
|
|
13
|
+
*/
|
|
14
|
+
export class BackgroundShells {
|
|
15
|
+
next = 0;
|
|
16
|
+
jobs = new Map();
|
|
17
|
+
/** @internal Start `command` under `argv` in its own process group. */
|
|
18
|
+
start(argv, command, workdir, grace) {
|
|
19
|
+
const child = spawn(argv[0], [...argv.slice(1), command], { cwd: workdir, detached: process.platform !== "win32" });
|
|
20
|
+
let resolveDone;
|
|
21
|
+
const done = new Promise((r) => (resolveDone = r));
|
|
22
|
+
const j = {
|
|
23
|
+
id: `bg${++this.next}`, command, started: new Date(), child, pgid: child.pid, grace,
|
|
24
|
+
out: "", read: 0, running: true, exitCode: 0, killed: false, done,
|
|
25
|
+
};
|
|
26
|
+
child.stdout?.on("data", (d) => (j.out += d.toString()));
|
|
27
|
+
child.stderr?.on("data", (d) => (j.out += d.toString()));
|
|
28
|
+
const finish = (code) => {
|
|
29
|
+
if (!j.running)
|
|
30
|
+
return;
|
|
31
|
+
j.running = false;
|
|
32
|
+
j.exitCode = code ?? -1;
|
|
33
|
+
resolveDone();
|
|
34
|
+
};
|
|
35
|
+
child.on("error", (e) => {
|
|
36
|
+
j.out += `bash: ${e.message}\n`;
|
|
37
|
+
finish(-1);
|
|
38
|
+
});
|
|
39
|
+
child.on("close", (code) => finish(code));
|
|
40
|
+
// A background job must never keep the host's event loop alive on its own.
|
|
41
|
+
child.unref();
|
|
42
|
+
child.stdout?.unref?.();
|
|
43
|
+
child.stderr?.unref?.();
|
|
44
|
+
this.jobs.set(j.id, j);
|
|
45
|
+
return j;
|
|
46
|
+
}
|
|
47
|
+
/** @internal */
|
|
48
|
+
get(id) {
|
|
49
|
+
return this.jobs.get(id);
|
|
50
|
+
}
|
|
51
|
+
signal(j, graceful) {
|
|
52
|
+
const pgid = j.pgid;
|
|
53
|
+
if (typeof pgid !== "number" || pgid <= 1 || !j.running)
|
|
54
|
+
return;
|
|
55
|
+
try {
|
|
56
|
+
if (process.platform === "win32")
|
|
57
|
+
spawnSync("taskkill", ["/T", "/F", "/PID", String(pgid)]);
|
|
58
|
+
else
|
|
59
|
+
process.kill(-pgid, graceful ? "SIGTERM" : "SIGKILL");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* already gone */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** @internal Stop the job's whole process group: SIGTERM, the grace window, SIGKILL.
|
|
66
|
+
* Resolves whether it was still running. */
|
|
67
|
+
async kill(j) {
|
|
68
|
+
if (!j.running)
|
|
69
|
+
return false;
|
|
70
|
+
j.killed = true;
|
|
71
|
+
// The child and its pipes are unref'd so an idle job never pins the host. While we are
|
|
72
|
+
// WAITING on it, though, they must be ref'd: otherwise, once the grace timer is cleared,
|
|
73
|
+
// nothing holds the event loop open until 'close' fires and Node exits with this promise
|
|
74
|
+
// still pending (seen on Linux, where 'close' lands an iteration later than on macOS).
|
|
75
|
+
j.child.ref();
|
|
76
|
+
j.child.stdout?.ref?.();
|
|
77
|
+
j.child.stderr?.ref?.();
|
|
78
|
+
this.signal(j, true);
|
|
79
|
+
let t;
|
|
80
|
+
const timedOut = await Promise.race([j.done.then(() => false), new Promise((r) => (t = setTimeout(() => r(true), j.grace)))]);
|
|
81
|
+
if (t)
|
|
82
|
+
clearTimeout(t);
|
|
83
|
+
if (timedOut) {
|
|
84
|
+
this.signal(j, false);
|
|
85
|
+
await j.done;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
static info(j) {
|
|
90
|
+
return { id: j.id, command: j.command, started: j.started, running: j.running, exitCode: j.exitCode, killed: j.killed };
|
|
91
|
+
}
|
|
92
|
+
/** Every background command, oldest first. */
|
|
93
|
+
list() {
|
|
94
|
+
return [...this.jobs.values()].sort((a, b) => a.started.getTime() - b.started.getTime()).map(BackgroundShells.info);
|
|
95
|
+
}
|
|
96
|
+
/** Stop one background command's process group. Unknown id ⇒ throws. */
|
|
97
|
+
async killShell(id) {
|
|
98
|
+
const j = this.jobs.get(id);
|
|
99
|
+
if (!j)
|
|
100
|
+
throw new Error(`no background shell "${id}"`);
|
|
101
|
+
await this.kill(j);
|
|
102
|
+
}
|
|
103
|
+
/** Stop every background command still running, concurrently. */
|
|
104
|
+
async closeAll() {
|
|
105
|
+
await Promise.all([...this.jobs.values()].map((j) => this.kill(j)));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function bgStatus(i) {
|
|
109
|
+
if (i.running)
|
|
110
|
+
return "running";
|
|
111
|
+
if (i.killed)
|
|
112
|
+
return "killed";
|
|
113
|
+
return `exited with code ${i.exitCode}`;
|
|
114
|
+
}
|
|
115
|
+
/** `bash_output(id)`. */
|
|
116
|
+
export function bashOutput(bg, id) {
|
|
117
|
+
const j = bg.get(id);
|
|
118
|
+
if (!j)
|
|
119
|
+
return err(`bash_output: no background shell "${id}"`);
|
|
120
|
+
const fresh = j.out.slice(j.read);
|
|
121
|
+
j.read = j.out.length;
|
|
122
|
+
const info = BackgroundShells.info(j);
|
|
123
|
+
const status = bgStatus(info);
|
|
124
|
+
const meta = { id, status: status.split(" ")[0] };
|
|
125
|
+
if (!info.running && !info.killed)
|
|
126
|
+
meta.exitCode = info.exitCode;
|
|
127
|
+
return ok(`${fresh}\n[status: ${status}]`, meta);
|
|
128
|
+
}
|
|
129
|
+
/** `kill_shell(id)`. */
|
|
130
|
+
export async function killShell(bg, id) {
|
|
131
|
+
const j = bg.get(id);
|
|
132
|
+
if (!j)
|
|
133
|
+
return err(`kill_shell: no background shell "${id}"`);
|
|
134
|
+
if (!(await bg.kill(j))) {
|
|
135
|
+
return ok(`Background shell ${id} had already ${bgStatus(BackgroundShells.info(j))}`, { id, killed: false });
|
|
136
|
+
}
|
|
137
|
+
return ok(`Killed background shell ${id}`, { id, killed: true });
|
|
138
|
+
}
|
|
139
|
+
export async function runWebSearch(search, args, signal) {
|
|
140
|
+
const q = typeof args.query === "string" ? args.query : "";
|
|
141
|
+
if (!q.trim())
|
|
142
|
+
return err("websearch: query is required");
|
|
143
|
+
const limit = typeof args.limit === "number" && args.limit >= 1 ? Math.trunc(args.limit) : 5;
|
|
144
|
+
let results;
|
|
145
|
+
try {
|
|
146
|
+
results = await search(q, limit, { signal });
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
return err(`websearch: ${e instanceof Error ? e.message : String(e)}`);
|
|
150
|
+
}
|
|
151
|
+
results = (results ?? []).slice(0, limit);
|
|
152
|
+
if (results.length === 0)
|
|
153
|
+
return ok("No results.", { count: 0 });
|
|
154
|
+
const text = results
|
|
155
|
+
.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}` + (r.snippet ? `\n ${r.snippet}` : ""))
|
|
156
|
+
.join("\n\n");
|
|
157
|
+
return ok(text, { count: results.length, results: results.map((r) => ({ title: r.title, url: r.url, snippet: r.snippet ?? "" })) });
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// notebook_edit
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
/** Split text into nbformat's list of lines, each keeping its "\n". */
|
|
163
|
+
export function notebookSource(s) {
|
|
164
|
+
if (!s)
|
|
165
|
+
return [];
|
|
166
|
+
return s.split(/(?<=\n)/).filter((p) => p !== "");
|
|
167
|
+
}
|
|
168
|
+
/** Go's json.MarshalIndent(v, "", " ") with sorted keys, plus Go's HTML escaping. */
|
|
169
|
+
export function goJsonIndent(v) {
|
|
170
|
+
const sort = (x) => {
|
|
171
|
+
if (Array.isArray(x))
|
|
172
|
+
return x.map(sort);
|
|
173
|
+
if (x && typeof x === "object") {
|
|
174
|
+
const out = {};
|
|
175
|
+
for (const k of Object.keys(x).sort())
|
|
176
|
+
out[k] = sort(x[k]);
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
return x;
|
|
180
|
+
};
|
|
181
|
+
return JSON.stringify(sort(v), null, " ")
|
|
182
|
+
.replace(/</g, "\\u003c")
|
|
183
|
+
.replace(/>/g, "\\u003e")
|
|
184
|
+
.replace(/&/g, "\\u0026")
|
|
185
|
+
.replace(new RegExp(String.fromCharCode(0x2028), "g"), "\\u2028")
|
|
186
|
+
.replace(new RegExp(String.fromCharCode(0x2029), "g"), "\\u2029");
|
|
187
|
+
}
|
|
188
|
+
/** Apply one notebook_edit to the parsed notebook, in place. Returns the summary, or an error result. */
|
|
189
|
+
export function editNotebook(nb, args, p) {
|
|
190
|
+
let cells = Array.isArray(nb.cells) ? nb.cells : [];
|
|
191
|
+
const mode = typeof args.edit_mode === "string" && args.edit_mode ? args.edit_mode : "replace";
|
|
192
|
+
let idx = -1;
|
|
193
|
+
if (typeof args.cell_id === "string" && args.cell_id) {
|
|
194
|
+
idx = cells.findIndex((c) => c && typeof c === "object" && c.id === args.cell_id);
|
|
195
|
+
if (idx < 0)
|
|
196
|
+
return err(`notebook_edit: no cell with id "${args.cell_id}"`);
|
|
197
|
+
}
|
|
198
|
+
else if (typeof args.cell_index === "number") {
|
|
199
|
+
idx = Math.trunc(args.cell_index);
|
|
200
|
+
if (idx < 0 || idx >= cells.length)
|
|
201
|
+
return err(`notebook_edit: cell_index ${idx} out of range (0-${cells.length - 1})`);
|
|
202
|
+
}
|
|
203
|
+
else if (mode !== "insert") {
|
|
204
|
+
return err("notebook_edit: cell_id or cell_index is required");
|
|
205
|
+
}
|
|
206
|
+
const src = typeof args.new_source === "string" ? args.new_source : "";
|
|
207
|
+
let cellType = typeof args.cell_type === "string" ? args.cell_type : "";
|
|
208
|
+
let summary;
|
|
209
|
+
switch (mode) {
|
|
210
|
+
case "replace": {
|
|
211
|
+
const cm = cells[idx];
|
|
212
|
+
cm.source = notebookSource(src);
|
|
213
|
+
if (cellType)
|
|
214
|
+
cm.cell_type = cellType;
|
|
215
|
+
if (cm.cell_type === "code") {
|
|
216
|
+
cm.outputs = [];
|
|
217
|
+
cm.execution_count = null;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
delete cm.outputs;
|
|
221
|
+
delete cm.execution_count;
|
|
222
|
+
}
|
|
223
|
+
summary = `Replaced cell ${idx} in ${p}`;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case "insert": {
|
|
227
|
+
if (!cellType)
|
|
228
|
+
cellType = "code";
|
|
229
|
+
const cell = { cell_type: cellType, metadata: {}, source: notebookSource(src) };
|
|
230
|
+
if (cellType === "code") {
|
|
231
|
+
cell.outputs = [];
|
|
232
|
+
cell.execution_count = null;
|
|
233
|
+
}
|
|
234
|
+
const at = idx + 1;
|
|
235
|
+
cells = [...cells.slice(0, at), cell, ...cells.slice(at)];
|
|
236
|
+
summary = `Inserted ${cellType} cell at ${at} in ${p}`;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "delete":
|
|
240
|
+
cells = [...cells.slice(0, idx), ...cells.slice(idx + 1)];
|
|
241
|
+
summary = `Deleted cell ${idx} from ${p}`;
|
|
242
|
+
break;
|
|
243
|
+
default:
|
|
244
|
+
return err(`notebook_edit: unknown edit_mode "${mode}"`);
|
|
245
|
+
}
|
|
246
|
+
nb.cells = cells;
|
|
247
|
+
return { summary };
|
|
248
|
+
}
|
package/dist/classifier.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export declare const DEFAULT_CLASSIFIER_MODEL = "jev-latest";
|
|
|
19
19
|
export declare const DEFAULT_CLASSIFIER_API_KEY_ENV = "TYPESAFE_API_KEY";
|
|
20
20
|
/** Bounds ONE request; a classifier has no loop to bound. */
|
|
21
21
|
export declare const DEFAULT_CLASSIFIER_TIMEOUT_MS = 10000;
|
|
22
|
+
/** `evaluateBatch`: at most this many states in flight by default. */
|
|
23
|
+
export declare const DEFAULT_BATCH_CONCURRENCY = 16;
|
|
22
24
|
/**
|
|
23
25
|
* A named BACKEND: `baseUrl`, `model` and `apiKeyEnv` are only jointly valid, so they travel as
|
|
24
26
|
* a UNIT (ADR 0027 D1). `jev-latest` is TypeSafe's spelling; the gateway spells the same model
|
|
@@ -95,6 +97,8 @@ export declare function score(instructions: string, criteria: string[]): ScoreQu
|
|
|
95
97
|
* obligation on `ChoiceQuestion`.
|
|
96
98
|
*/
|
|
97
99
|
export declare function choiceOver(instructions: string, items: Record<string, string>): ChoiceQuestion;
|
|
100
|
+
/** The wire form of one question — the object itself, minus an absent `criteria`. */
|
|
101
|
+
export declare function questionWire(q: Question): Record<string, unknown>;
|
|
98
102
|
/**
|
|
99
103
|
* Canonical JSON: object keys sorted recursively in CODE-UNIT (ASCII) order, arrays NEVER
|
|
100
104
|
* reordered, compact separators, and `<>&'"` plus non-ASCII transmitted raw.
|
|
@@ -298,6 +302,16 @@ export declare class Classifier {
|
|
|
298
302
|
* INDEPENDENT — one answer is never context for another.
|
|
299
303
|
*/
|
|
300
304
|
evaluate(state: unknown, questions: Record<string, Question>, signal?: AbortSignal): Promise<Decision>;
|
|
305
|
+
/**
|
|
306
|
+
* The same questions over many states (SPEC §8B "Batch"). Each state goes through
|
|
307
|
+
* `evaluate`, at most `concurrency` (default 16) in flight; decisions come back in STATE
|
|
308
|
+
* order. Fails closed: the first failing state's index is named and no decisions return.
|
|
309
|
+
* No states is an error and sends nothing.
|
|
310
|
+
*/
|
|
311
|
+
evaluateBatch(states: readonly unknown[], questions: Record<string, Question>, opts?: {
|
|
312
|
+
signal?: AbortSignal;
|
|
313
|
+
concurrency?: number;
|
|
314
|
+
}): Promise<Decision[]>;
|
|
301
315
|
/**
|
|
302
316
|
* Emits ONE warning per degenerate question key per classifier, naming the key, and changes
|
|
303
317
|
* nothing about the request. Repairing would invent option descriptions the caller did not
|
package/dist/classifier.js
CHANGED
|
@@ -9,6 +9,8 @@ export const DEFAULT_CLASSIFIER_MODEL = "jev-latest";
|
|
|
9
9
|
export const DEFAULT_CLASSIFIER_API_KEY_ENV = "TYPESAFE_API_KEY";
|
|
10
10
|
/** Bounds ONE request; a classifier has no loop to bound. */
|
|
11
11
|
export const DEFAULT_CLASSIFIER_TIMEOUT_MS = 10_000;
|
|
12
|
+
/** `evaluateBatch`: at most this many states in flight by default. */
|
|
13
|
+
export const DEFAULT_BATCH_CONCURRENCY = 16;
|
|
12
14
|
/** The two servable pairings, as configurations rather than six independently-choosable cells. */
|
|
13
15
|
export const CLASSIFIER_BACKENDS = {
|
|
14
16
|
typesafe: { baseUrl: DEFAULT_CLASSIFIER_BASE_URL, model: DEFAULT_CLASSIFIER_MODEL, apiKeyEnv: DEFAULT_CLASSIFIER_API_KEY_ENV },
|
|
@@ -45,7 +47,7 @@ export function choiceOver(instructions, items) {
|
|
|
45
47
|
return choice(instructions, items);
|
|
46
48
|
}
|
|
47
49
|
/** The wire form of one question — the object itself, minus an absent `criteria`. */
|
|
48
|
-
function questionWire(q) {
|
|
50
|
+
export function questionWire(q) {
|
|
49
51
|
switch (q.type) {
|
|
50
52
|
case "noul":
|
|
51
53
|
return q.criteria === undefined
|
|
@@ -362,6 +364,37 @@ export class Classifier {
|
|
|
362
364
|
});
|
|
363
365
|
return decision;
|
|
364
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* The same questions over many states (SPEC §8B "Batch"). Each state goes through
|
|
369
|
+
* `evaluate`, at most `concurrency` (default 16) in flight; decisions come back in STATE
|
|
370
|
+
* order. Fails closed: the first failing state's index is named and no decisions return.
|
|
371
|
+
* No states is an error and sends nothing.
|
|
372
|
+
*/
|
|
373
|
+
async evaluateBatch(states, questions, opts = {}) {
|
|
374
|
+
if (!states || states.length === 0)
|
|
375
|
+
throw new Error("classifier: no states to evaluate");
|
|
376
|
+
const limit = Math.max(1, opts.concurrency ?? DEFAULT_BATCH_CONCURRENCY);
|
|
377
|
+
const out = new Array(states.length);
|
|
378
|
+
const errs = new Array(states.length);
|
|
379
|
+
let next = 0;
|
|
380
|
+
const worker = async () => {
|
|
381
|
+
while (next < states.length) {
|
|
382
|
+
const i = next++;
|
|
383
|
+
try {
|
|
384
|
+
out[i] = await this.evaluate(states[i], questions, opts.signal);
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
errs[i] = e;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
await Promise.all(Array.from({ length: Math.min(limit, states.length) }, worker));
|
|
392
|
+
for (let i = 0; i < states.length; i++) {
|
|
393
|
+
if (i in errs)
|
|
394
|
+
throw new Error(`classifier: state ${i}: ${errText(errs[i])}`, { cause: errs[i] });
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
365
398
|
// ---------------------------------------------------------------- degenerate
|
|
366
399
|
/**
|
|
367
400
|
* Emits ONE warning per degenerate question key per classifier, naming the key, and changes
|
package/dist/client.d.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Toolkit. See ../../SPEC.md §8.
|
|
5
5
|
*/
|
|
6
6
|
import type { Toolkit } from "./toolkit.js";
|
|
7
|
-
import type { ToolResult, Request, Answer } from "./types.js";
|
|
7
|
+
import type { ToolHints, ToolResult, Request, Answer } from "./types.js";
|
|
8
|
+
import { type PromptCacheOptions } from "./promptcache.js";
|
|
9
|
+
import { type ToolTier, type ToolTierOptions } from "./tooltiers.js";
|
|
8
10
|
import type { PromptInput, UnsupportedPartMode } from "./content.js";
|
|
9
11
|
import type { TranslateRequest, TranslateResult } from "./translate.js";
|
|
10
12
|
export type ClientStyle = "openai" | "anthropic";
|
|
@@ -12,7 +14,15 @@ export interface ClientOptions {
|
|
|
12
14
|
baseUrl: string;
|
|
13
15
|
style: ClientStyle;
|
|
14
16
|
model: string;
|
|
17
|
+
/** The key, used as-is. Absent => `apiKeyEnv`, else ONLY the env var that belongs to the
|
|
18
|
+
* `baseUrl` host over https: api.openai.com -> OPENAI_API_KEY, api.anthropic.com ->
|
|
19
|
+
* ANTHROPIC_API_KEY, openrouter.ai (and subdomains) -> OPENROUTER_API_KEY. Any other host (or
|
|
20
|
+
* plain http) needs `apiKey` or `apiKeyEnv` - a key is never sent to a host it does not belong
|
|
21
|
+
* to (SPEC 4C). Never logged. */
|
|
15
22
|
apiKey?: string;
|
|
23
|
+
/** The NAME of the env var to read the key from when `apiKey` is absent - for a host not in
|
|
24
|
+
* the table above. Read at call time; the value is never logged. */
|
|
25
|
+
apiKeyEnv?: string;
|
|
16
26
|
headers?: Record<string, string>;
|
|
17
27
|
systemPrompt?: string;
|
|
18
28
|
maxTurns?: number;
|
|
@@ -53,7 +63,19 @@ export interface ClientOptions {
|
|
|
53
63
|
* Replaying the leaf's stored transcript instead is DEFERRED to its own change; it would not
|
|
54
64
|
* remove this requirement anyway.
|
|
55
65
|
*/
|
|
56
|
-
|
|
66
|
+
/** The second argument carries the turn's abort signal (SPEC 4C), so a pending ask can end
|
|
67
|
+
* when the turn is cancelled: settle once `ctx.signal` aborts. */
|
|
68
|
+
waitFor?: (request: Request, ctx: {
|
|
69
|
+
signal: AbortSignal;
|
|
70
|
+
}) => Promise<Answer>;
|
|
71
|
+
/** Prompt caching (SPEC 8): anthropic `cache_control` breakpoints on the last tool, the system
|
|
72
|
+
* prompt and the latest message; openai `prompt_cache_key`. Applied before `requestParams`
|
|
73
|
+
* and `bodyTransform`, so both can still override it. Absent => body byte-identical. */
|
|
74
|
+
promptCache?: PromptCacheOptions;
|
|
75
|
+
/** Tool tiers (SPEC 8): send only core tool schemas plus a `tool_search` tool that loads the
|
|
76
|
+
* rest on demand (and `call_tool`). The plan is frozen per toolkit until `resetToolTiers`.
|
|
77
|
+
* Absent => every tool is sent, as before. */
|
|
78
|
+
toolTiers?: ToolTierOptions;
|
|
57
79
|
/** §8 Gap 1. Extra top-level keys shallow-merged into EVERY LLM body after the client builds
|
|
58
80
|
* its own — a requestParams key WINS on collision (e.g. max_tokens overrides the anthropic
|
|
59
81
|
* default). `messages`/`tools`/`stream` are forbidden here (stripped + warned) — rewrite
|
|
@@ -200,27 +222,43 @@ export interface ConversationStore {
|
|
|
200
222
|
/** Default conversation provider — keeps transcripts in memory for the client's lifetime. */
|
|
201
223
|
export declare class InMemoryConversationStore implements ConversationStore {
|
|
202
224
|
private readonly map;
|
|
225
|
+
private readonly updated;
|
|
203
226
|
get(id: string): Promise<any[] | undefined>;
|
|
204
227
|
save(id: string, messages: any[]): Promise<void>;
|
|
228
|
+
/** Every stored conversation, newest first (ties by id ascending). */
|
|
229
|
+
list(): Promise<ConversationEntry[]>;
|
|
230
|
+
/** Remove a conversation; absent ids are not an error. */
|
|
231
|
+
delete(id: string): Promise<void>;
|
|
205
232
|
}
|
|
233
|
+
/** One `list()` row of a shipped conversation store. */
|
|
234
|
+
export interface ConversationEntry {
|
|
235
|
+
id: string;
|
|
236
|
+
updatedAt: Date;
|
|
237
|
+
}
|
|
238
|
+
/** @internal Newest first, ties by id ascending (SPEC.md "Conversation memory"). */
|
|
239
|
+
export declare function sortEntries(entries: ConversationEntry[]): ConversationEntry[];
|
|
206
240
|
/**
|
|
207
241
|
* Lifecycle hooks around the agent loop. Each may observe, and (where noted)
|
|
208
242
|
* mutate or short-circuit. All may be async.
|
|
209
243
|
*/
|
|
244
|
+
/** The `beforeLLM` hook event. */
|
|
245
|
+
export interface BeforeLLMEvent {
|
|
246
|
+
messages: any[];
|
|
247
|
+
tools: any[];
|
|
248
|
+
model: string;
|
|
249
|
+
turn: number;
|
|
250
|
+
}
|
|
251
|
+
/** What a `beforeLLM` hook may return. `model` (non-empty) applies to that turn only. */
|
|
252
|
+
export interface LLMOverride {
|
|
253
|
+
messages?: any[];
|
|
254
|
+
tools?: any[];
|
|
255
|
+
model?: string | null;
|
|
256
|
+
}
|
|
210
257
|
export interface Hooks {
|
|
211
|
-
/** Before each model call. Return { messages?, tools? } to replace them
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
model: string;
|
|
216
|
-
turn: number;
|
|
217
|
-
}): void | {
|
|
218
|
-
messages?: any[];
|
|
219
|
-
tools?: any[];
|
|
220
|
-
} | Promise<void | {
|
|
221
|
-
messages?: any[];
|
|
222
|
-
tools?: any[];
|
|
223
|
-
}>;
|
|
258
|
+
/** Before each model call. Return { messages?, tools? } to replace them, and { model } to send a
|
|
259
|
+
* different model for THIS turn only (request body + afterLLM event); absent/null/empty keeps the
|
|
260
|
+
* configured model verbatim (SPEC §8, e.g. a ModelRouterClassifier). */
|
|
261
|
+
beforeLLM?(ev: BeforeLLMEvent): void | LLMOverride | Promise<void | LLMOverride>;
|
|
224
262
|
/** After each model call (observe: logging, cost, tracing). */
|
|
225
263
|
afterLLM?(ev: {
|
|
226
264
|
response: any;
|
|
@@ -233,6 +271,7 @@ export interface Hooks {
|
|
|
233
271
|
args: Record<string, unknown>;
|
|
234
272
|
id?: string;
|
|
235
273
|
turn: number;
|
|
274
|
+
hints?: ToolHints;
|
|
236
275
|
}): void | {
|
|
237
276
|
args?: Record<string, unknown>;
|
|
238
277
|
result?: ToolResult;
|
|
@@ -266,6 +305,14 @@ export interface Usage {
|
|
|
266
305
|
promptTokens: number;
|
|
267
306
|
completionTokens: number;
|
|
268
307
|
totalTokens: number;
|
|
308
|
+
/** Prompt tokens served from the provider's prompt cache (anthropic cache_read_input_tokens;
|
|
309
|
+
* openai prompt_tokens_details.cached_tokens). Present once a provider reports any.
|
|
310
|
+
* Under anthropic `promptTokens` EXCLUDES cached tokens: the context sent is
|
|
311
|
+
* promptTokens + cacheReadTokens + cacheWriteTokens. Under openai `promptTokens` already
|
|
312
|
+
* includes cacheReadTokens. */
|
|
313
|
+
cacheReadTokens?: number;
|
|
314
|
+
/** Tokens written to the prompt cache (anthropic cache_creation_input_tokens). */
|
|
315
|
+
cacheWriteTokens?: number;
|
|
269
316
|
}
|
|
270
317
|
export interface RunResult {
|
|
271
318
|
text: string;
|
|
@@ -298,12 +345,15 @@ export type StreamEvent = {
|
|
|
298
345
|
id: string;
|
|
299
346
|
name: string;
|
|
300
347
|
args: Record<string, unknown>;
|
|
348
|
+
via?: "call_tool";
|
|
301
349
|
} | {
|
|
302
350
|
type: "tool_result";
|
|
303
351
|
id: string;
|
|
304
352
|
name: string;
|
|
305
353
|
output: string;
|
|
306
354
|
isError: boolean;
|
|
355
|
+
metadata?: Record<string, unknown>;
|
|
356
|
+
via?: "call_tool";
|
|
307
357
|
} | {
|
|
308
358
|
type: "usage";
|
|
309
359
|
usage: Usage;
|
|
@@ -314,12 +364,22 @@ export type StreamEvent = {
|
|
|
314
364
|
type: "done";
|
|
315
365
|
result: RunResult;
|
|
316
366
|
};
|
|
367
|
+
/**
|
|
368
|
+
* Pick the API key (SPEC 4C, security). An explicit `apiKey` is used as-is; else `apiKeyEnv`;
|
|
369
|
+
* else ONLY the env var that belongs to the `baseUrl` host. There is no cross-provider fallback:
|
|
370
|
+
* any other host needs `apiKey` or `apiKeyEnv`. The value is never logged.
|
|
371
|
+
*/
|
|
372
|
+
export declare function resolveKey(opts: Pick<ClientOptions, "apiKey" | "apiKeyEnv" | "baseUrl">): string;
|
|
373
|
+
/** The env var whose key may be sent to `baseUrl`'s host, or "" when none may. */
|
|
374
|
+
export declare function keyEnvForBaseUrl(baseUrl: string): string;
|
|
317
375
|
export declare class Client {
|
|
318
376
|
private readonly opts;
|
|
319
377
|
/** Conversation provider for `ask()` — from opts.store, else in-memory. */
|
|
320
378
|
private readonly store;
|
|
321
379
|
/** Cumulative Prometheus registry fed by the same events `onMetric` sees. */
|
|
322
380
|
private readonly registry;
|
|
381
|
+
/** The frozen ToolTiers plan per toolkit (SPEC 8 Tool tiers). */
|
|
382
|
+
private tierPlans;
|
|
323
383
|
constructor(opts: ClientOptions);
|
|
324
384
|
/**
|
|
325
385
|
* §8 Gap 4. The client's conversation store — the exact instance passed in `opts.store`, else
|
|
@@ -430,6 +490,22 @@ export declare class Client {
|
|
|
430
490
|
private llmFetch;
|
|
431
491
|
/** Run one tool through beforeTool/afterTool hooks (mutate args, short-circuit, transform result). */
|
|
432
492
|
private runTool;
|
|
493
|
+
private runToolDirect;
|
|
494
|
+
/** ToolTiers `call_tool {name, arguments}` -> the real call; undefined when tiers are off. */
|
|
495
|
+
private unwrapCall;
|
|
496
|
+
/** The stream's tool_call event, naming the real tool for a `call_tool` call. */
|
|
497
|
+
private callEvent;
|
|
498
|
+
private resultEvent;
|
|
499
|
+
private plan;
|
|
500
|
+
/** The tools array for a request: core tools (sorted) then tool_search and call_tool. Without
|
|
501
|
+
* tiers it is every tool, as before. */
|
|
502
|
+
private requestTools;
|
|
503
|
+
/** The frozen tier plan for `toolkit` (built on first use): tool name -> tier. Undefined when
|
|
504
|
+
* `toolTiers` is not set. For a host UI or log. */
|
|
505
|
+
toolTierPlan(toolkit: Toolkit): Record<string, ToolTier> | undefined;
|
|
506
|
+
/** Forget `toolkit`'s plan so the next request rebuilds it - call at a new session start.
|
|
507
|
+
* Mid-session it breaks the cached prefix. */
|
|
508
|
+
resetToolTiers(toolkit: Toolkit): void;
|
|
433
509
|
/** Emit the `tool` metric, classifying a §10 suspension as `pending` (isError:false), not a failure. */
|
|
434
510
|
private emitTool;
|
|
435
511
|
/** One non-streaming LLM call, with an `llm` metric event (ok/error + per-call tokens + ms). */
|