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,285 @@
|
|
|
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, type ChildProcess } from "node:child_process"
|
|
7
|
+
import type { ToolResult } from "./types.js"
|
|
8
|
+
|
|
9
|
+
const err = (output: string, metadata?: Record<string, unknown>): ToolResult => ({ output, isError: true, metadata })
|
|
10
|
+
const ok = (output: string, metadata?: Record<string, unknown>): ToolResult => ({ output, isError: false, metadata })
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// background shells
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/** One background command, for a host UI. */
|
|
17
|
+
export interface BackgroundShellInfo {
|
|
18
|
+
id: string
|
|
19
|
+
command: string
|
|
20
|
+
started: Date
|
|
21
|
+
running: boolean
|
|
22
|
+
/** Valid when not running; -1 when killed by a signal. */
|
|
23
|
+
exitCode: number
|
|
24
|
+
killed: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface BgJob {
|
|
28
|
+
id: string
|
|
29
|
+
command: string
|
|
30
|
+
started: Date
|
|
31
|
+
child: ChildProcess
|
|
32
|
+
pgid?: number
|
|
33
|
+
grace: number
|
|
34
|
+
out: string
|
|
35
|
+
read: number
|
|
36
|
+
running: boolean
|
|
37
|
+
exitCode: number
|
|
38
|
+
killed: boolean
|
|
39
|
+
done: Promise<void>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Owns the commands `bash` started with `run_in_background`. Pass one as
|
|
44
|
+
* `builtins.background`, and call `closeAll()` when the host shuts down — a background command
|
|
45
|
+
* is NOT tied to the turn that started it, so nothing else stops it.
|
|
46
|
+
*/
|
|
47
|
+
export class BackgroundShells {
|
|
48
|
+
private next = 0
|
|
49
|
+
private readonly jobs = new Map<string, BgJob>()
|
|
50
|
+
|
|
51
|
+
/** @internal Start `command` under `argv` in its own process group. */
|
|
52
|
+
start(argv: string[], command: string, workdir: string, grace: number): BgJob {
|
|
53
|
+
const child = spawn(argv[0], [...argv.slice(1), command], { cwd: workdir, detached: process.platform !== "win32" })
|
|
54
|
+
let resolveDone!: () => void
|
|
55
|
+
const done = new Promise<void>((r) => (resolveDone = r))
|
|
56
|
+
const j: BgJob = {
|
|
57
|
+
id: `bg${++this.next}`, command, started: new Date(), child, pgid: child.pid, grace,
|
|
58
|
+
out: "", read: 0, running: true, exitCode: 0, killed: false, done,
|
|
59
|
+
}
|
|
60
|
+
child.stdout?.on("data", (d) => (j.out += d.toString()))
|
|
61
|
+
child.stderr?.on("data", (d) => (j.out += d.toString()))
|
|
62
|
+
const finish = (code: number | null) => {
|
|
63
|
+
if (!j.running) return
|
|
64
|
+
j.running = false
|
|
65
|
+
j.exitCode = code ?? -1
|
|
66
|
+
resolveDone()
|
|
67
|
+
}
|
|
68
|
+
child.on("error", (e) => {
|
|
69
|
+
j.out += `bash: ${e.message}\n`
|
|
70
|
+
finish(-1)
|
|
71
|
+
})
|
|
72
|
+
child.on("close", (code) => finish(code))
|
|
73
|
+
// A background job must never keep the host's event loop alive on its own.
|
|
74
|
+
child.unref()
|
|
75
|
+
;(child.stdout as any)?.unref?.()
|
|
76
|
+
;(child.stderr as any)?.unref?.()
|
|
77
|
+
this.jobs.set(j.id, j)
|
|
78
|
+
return j
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @internal */
|
|
82
|
+
get(id: string): BgJob | undefined {
|
|
83
|
+
return this.jobs.get(id)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private signal(j: BgJob, graceful: boolean): void {
|
|
87
|
+
const pgid = j.pgid
|
|
88
|
+
if (typeof pgid !== "number" || pgid <= 1 || !j.running) return
|
|
89
|
+
try {
|
|
90
|
+
if (process.platform === "win32") spawnSync("taskkill", ["/T", "/F", "/PID", String(pgid)])
|
|
91
|
+
else process.kill(-pgid, graceful ? "SIGTERM" : "SIGKILL")
|
|
92
|
+
} catch {
|
|
93
|
+
/* already gone */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @internal Stop the job's whole process group: SIGTERM, the grace window, SIGKILL.
|
|
98
|
+
* Resolves whether it was still running. */
|
|
99
|
+
async kill(j: BgJob): Promise<boolean> {
|
|
100
|
+
if (!j.running) return false
|
|
101
|
+
j.killed = true
|
|
102
|
+
// The child and its pipes are unref'd so an idle job never pins the host. While we are
|
|
103
|
+
// WAITING on it, though, they must be ref'd: otherwise, once the grace timer is cleared,
|
|
104
|
+
// nothing holds the event loop open until 'close' fires and Node exits with this promise
|
|
105
|
+
// still pending (seen on Linux, where 'close' lands an iteration later than on macOS).
|
|
106
|
+
j.child.ref()
|
|
107
|
+
;(j.child.stdout as any)?.ref?.()
|
|
108
|
+
;(j.child.stderr as any)?.ref?.()
|
|
109
|
+
this.signal(j, true)
|
|
110
|
+
let t: NodeJS.Timeout | undefined
|
|
111
|
+
const timedOut = await Promise.race([j.done.then(() => false), new Promise<boolean>((r) => (t = setTimeout(() => r(true), j.grace)))])
|
|
112
|
+
if (t) clearTimeout(t)
|
|
113
|
+
if (timedOut) {
|
|
114
|
+
this.signal(j, false)
|
|
115
|
+
await j.done
|
|
116
|
+
}
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static info(j: BgJob): BackgroundShellInfo {
|
|
121
|
+
return { id: j.id, command: j.command, started: j.started, running: j.running, exitCode: j.exitCode, killed: j.killed }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Every background command, oldest first. */
|
|
125
|
+
list(): BackgroundShellInfo[] {
|
|
126
|
+
return [...this.jobs.values()].sort((a, b) => a.started.getTime() - b.started.getTime()).map(BackgroundShells.info)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Stop one background command's process group. Unknown id ⇒ throws. */
|
|
130
|
+
async killShell(id: string): Promise<void> {
|
|
131
|
+
const j = this.jobs.get(id)
|
|
132
|
+
if (!j) throw new Error(`no background shell "${id}"`)
|
|
133
|
+
await this.kill(j)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Stop every background command still running, concurrently. */
|
|
137
|
+
async closeAll(): Promise<void> {
|
|
138
|
+
await Promise.all([...this.jobs.values()].map((j) => this.kill(j)))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function bgStatus(i: BackgroundShellInfo): string {
|
|
143
|
+
if (i.running) return "running"
|
|
144
|
+
if (i.killed) return "killed"
|
|
145
|
+
return `exited with code ${i.exitCode}`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** `bash_output(id)`. */
|
|
149
|
+
export function bashOutput(bg: BackgroundShells, id: string): ToolResult {
|
|
150
|
+
const j = bg.get(id)
|
|
151
|
+
if (!j) return err(`bash_output: no background shell "${id}"`)
|
|
152
|
+
const fresh = j.out.slice(j.read)
|
|
153
|
+
j.read = j.out.length
|
|
154
|
+
const info = BackgroundShells.info(j)
|
|
155
|
+
const status = bgStatus(info)
|
|
156
|
+
const meta: Record<string, unknown> = { id, status: status.split(" ")[0] }
|
|
157
|
+
if (!info.running && !info.killed) meta.exitCode = info.exitCode
|
|
158
|
+
return ok(`${fresh}\n[status: ${status}]`, meta)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** `kill_shell(id)`. */
|
|
162
|
+
export async function killShell(bg: BackgroundShells, id: string): Promise<ToolResult> {
|
|
163
|
+
const j = bg.get(id)
|
|
164
|
+
if (!j) return err(`kill_shell: no background shell "${id}"`)
|
|
165
|
+
if (!(await bg.kill(j))) {
|
|
166
|
+
return ok(`Background shell ${id} had already ${bgStatus(BackgroundShells.info(j))}`, { id, killed: false })
|
|
167
|
+
}
|
|
168
|
+
return ok(`Killed background shell ${id}`, { id, killed: true })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// websearch
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
/** One hit from a host search provider. */
|
|
176
|
+
export interface WebSearchResult {
|
|
177
|
+
title: string
|
|
178
|
+
url: string
|
|
179
|
+
snippet?: string
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The host's search provider. The library ships none. */
|
|
183
|
+
export type WebSearchFunc = (query: string, limit: number, ctx: { signal?: AbortSignal }) => Promise<WebSearchResult[]>
|
|
184
|
+
|
|
185
|
+
export async function runWebSearch(search: WebSearchFunc, args: Record<string, unknown>, signal?: AbortSignal): Promise<ToolResult> {
|
|
186
|
+
const q = typeof args.query === "string" ? args.query : ""
|
|
187
|
+
if (!q.trim()) return err("websearch: query is required")
|
|
188
|
+
const limit = typeof args.limit === "number" && args.limit >= 1 ? Math.trunc(args.limit) : 5
|
|
189
|
+
let results: WebSearchResult[]
|
|
190
|
+
try {
|
|
191
|
+
results = await search(q, limit, { signal })
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return err(`websearch: ${e instanceof Error ? e.message : String(e)}`)
|
|
194
|
+
}
|
|
195
|
+
results = (results ?? []).slice(0, limit)
|
|
196
|
+
if (results.length === 0) return ok("No results.", { count: 0 })
|
|
197
|
+
const text = results
|
|
198
|
+
.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}` + (r.snippet ? `\n ${r.snippet}` : ""))
|
|
199
|
+
.join("\n\n")
|
|
200
|
+
return ok(text, { count: results.length, results: results.map((r) => ({ title: r.title, url: r.url, snippet: r.snippet ?? "" })) })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// notebook_edit
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
/** Split text into nbformat's list of lines, each keeping its "\n". */
|
|
208
|
+
export function notebookSource(s: string): string[] {
|
|
209
|
+
if (!s) return []
|
|
210
|
+
return s.split(/(?<=\n)/).filter((p) => p !== "")
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Go's json.MarshalIndent(v, "", " ") with sorted keys, plus Go's HTML escaping. */
|
|
214
|
+
export function goJsonIndent(v: unknown): string {
|
|
215
|
+
const sort = (x: unknown): unknown => {
|
|
216
|
+
if (Array.isArray(x)) return x.map(sort)
|
|
217
|
+
if (x && typeof x === "object") {
|
|
218
|
+
const out: Record<string, unknown> = {}
|
|
219
|
+
for (const k of Object.keys(x as object).sort()) out[k] = sort((x as Record<string, unknown>)[k])
|
|
220
|
+
return out
|
|
221
|
+
}
|
|
222
|
+
return x
|
|
223
|
+
}
|
|
224
|
+
return JSON.stringify(sort(v), null, " ")
|
|
225
|
+
.replace(/</g, "\\u003c")
|
|
226
|
+
.replace(/>/g, "\\u003e")
|
|
227
|
+
.replace(/&/g, "\\u0026")
|
|
228
|
+
.replace(new RegExp(String.fromCharCode(0x2028), "g"), "\\u2028")
|
|
229
|
+
.replace(new RegExp(String.fromCharCode(0x2029), "g"), "\\u2029")
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Apply one notebook_edit to the parsed notebook, in place. Returns the summary, or an error result. */
|
|
233
|
+
export function editNotebook(nb: Record<string, any>, args: Record<string, unknown>, p: string): { summary: string } | ToolResult {
|
|
234
|
+
let cells: any[] = Array.isArray(nb.cells) ? nb.cells : []
|
|
235
|
+
const mode = typeof args.edit_mode === "string" && args.edit_mode ? args.edit_mode : "replace"
|
|
236
|
+
let idx = -1
|
|
237
|
+
if (typeof args.cell_id === "string" && args.cell_id) {
|
|
238
|
+
idx = cells.findIndex((c) => c && typeof c === "object" && c.id === args.cell_id)
|
|
239
|
+
if (idx < 0) return err(`notebook_edit: no cell with id "${args.cell_id}"`)
|
|
240
|
+
} else if (typeof args.cell_index === "number") {
|
|
241
|
+
idx = Math.trunc(args.cell_index)
|
|
242
|
+
if (idx < 0 || idx >= cells.length) return err(`notebook_edit: cell_index ${idx} out of range (0-${cells.length - 1})`)
|
|
243
|
+
} else if (mode !== "insert") {
|
|
244
|
+
return err("notebook_edit: cell_id or cell_index is required")
|
|
245
|
+
}
|
|
246
|
+
const src = typeof args.new_source === "string" ? args.new_source : ""
|
|
247
|
+
let cellType = typeof args.cell_type === "string" ? args.cell_type : ""
|
|
248
|
+
let summary: string
|
|
249
|
+
switch (mode) {
|
|
250
|
+
case "replace": {
|
|
251
|
+
const cm = cells[idx]
|
|
252
|
+
cm.source = notebookSource(src)
|
|
253
|
+
if (cellType) cm.cell_type = cellType
|
|
254
|
+
if (cm.cell_type === "code") {
|
|
255
|
+
cm.outputs = []
|
|
256
|
+
cm.execution_count = null
|
|
257
|
+
} else {
|
|
258
|
+
delete cm.outputs
|
|
259
|
+
delete cm.execution_count
|
|
260
|
+
}
|
|
261
|
+
summary = `Replaced cell ${idx} in ${p}`
|
|
262
|
+
break
|
|
263
|
+
}
|
|
264
|
+
case "insert": {
|
|
265
|
+
if (!cellType) cellType = "code"
|
|
266
|
+
const cell: Record<string, unknown> = { cell_type: cellType, metadata: {}, source: notebookSource(src) }
|
|
267
|
+
if (cellType === "code") {
|
|
268
|
+
cell.outputs = []
|
|
269
|
+
cell.execution_count = null
|
|
270
|
+
}
|
|
271
|
+
const at = idx + 1
|
|
272
|
+
cells = [...cells.slice(0, at), cell, ...cells.slice(at)]
|
|
273
|
+
summary = `Inserted ${cellType} cell at ${at} in ${p}`
|
|
274
|
+
break
|
|
275
|
+
}
|
|
276
|
+
case "delete":
|
|
277
|
+
cells = [...cells.slice(0, idx), ...cells.slice(idx + 1)]
|
|
278
|
+
summary = `Deleted cell ${idx} from ${p}`
|
|
279
|
+
break
|
|
280
|
+
default:
|
|
281
|
+
return err(`notebook_edit: unknown edit_mode "${mode}"`)
|
|
282
|
+
}
|
|
283
|
+
nb.cells = cells
|
|
284
|
+
return { summary }
|
|
285
|
+
}
|
package/src/classifier.ts
CHANGED
|
@@ -23,6 +23,8 @@ export const DEFAULT_CLASSIFIER_MODEL = "jev-latest"
|
|
|
23
23
|
export const DEFAULT_CLASSIFIER_API_KEY_ENV = "TYPESAFE_API_KEY"
|
|
24
24
|
/** Bounds ONE request; a classifier has no loop to bound. */
|
|
25
25
|
export const DEFAULT_CLASSIFIER_TIMEOUT_MS = 10_000
|
|
26
|
+
/** `evaluateBatch`: at most this many states in flight by default. */
|
|
27
|
+
export const DEFAULT_BATCH_CONCURRENCY = 16
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* A named BACKEND: `baseUrl`, `model` and `apiKeyEnv` are only jointly valid, so they travel as
|
|
@@ -123,7 +125,7 @@ export function choiceOver(instructions: string, items: Record<string, string>):
|
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
/** The wire form of one question — the object itself, minus an absent `criteria`. */
|
|
126
|
-
function questionWire(q: Question): Record<string, unknown> {
|
|
128
|
+
export function questionWire(q: Question): Record<string, unknown> {
|
|
127
129
|
switch (q.type) {
|
|
128
130
|
case "noul":
|
|
129
131
|
return q.criteria === undefined
|
|
@@ -564,6 +566,39 @@ export class Classifier {
|
|
|
564
566
|
return decision
|
|
565
567
|
}
|
|
566
568
|
|
|
569
|
+
/**
|
|
570
|
+
* The same questions over many states (SPEC §8B "Batch"). Each state goes through
|
|
571
|
+
* `evaluate`, at most `concurrency` (default 16) in flight; decisions come back in STATE
|
|
572
|
+
* order. Fails closed: the first failing state's index is named and no decisions return.
|
|
573
|
+
* No states is an error and sends nothing.
|
|
574
|
+
*/
|
|
575
|
+
async evaluateBatch(
|
|
576
|
+
states: readonly unknown[],
|
|
577
|
+
questions: Record<string, Question>,
|
|
578
|
+
opts: { signal?: AbortSignal; concurrency?: number } = {},
|
|
579
|
+
): Promise<Decision[]> {
|
|
580
|
+
if (!states || states.length === 0) throw new Error("classifier: no states to evaluate")
|
|
581
|
+
const limit = Math.max(1, opts.concurrency ?? DEFAULT_BATCH_CONCURRENCY)
|
|
582
|
+
const out: Decision[] = new Array(states.length)
|
|
583
|
+
const errs: unknown[] = new Array(states.length)
|
|
584
|
+
let next = 0
|
|
585
|
+
const worker = async () => {
|
|
586
|
+
while (next < states.length) {
|
|
587
|
+
const i = next++
|
|
588
|
+
try {
|
|
589
|
+
out[i] = await this.evaluate(states[i], questions, opts.signal)
|
|
590
|
+
} catch (e) {
|
|
591
|
+
errs[i] = e
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
await Promise.all(Array.from({ length: Math.min(limit, states.length) }, worker))
|
|
596
|
+
for (let i = 0; i < states.length; i++) {
|
|
597
|
+
if (i in errs) throw new Error(`classifier: state ${i}: ${errText(errs[i])}`, { cause: errs[i] })
|
|
598
|
+
}
|
|
599
|
+
return out
|
|
600
|
+
}
|
|
601
|
+
|
|
567
602
|
// ---------------------------------------------------------------- degenerate
|
|
568
603
|
|
|
569
604
|
/**
|