toolnexus 0.19.0 → 0.20.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/dist/agents/home.js +4 -2
- package/dist/agents/runtime.d.ts +12 -3
- package/dist/agents/runtime.js +4 -3
- package/dist/builtin.d.ts +32 -2
- package/dist/builtin.js +324 -46
- package/dist/classifier.js +3 -1
- package/dist/client.d.ts +18 -4
- package/dist/client.js +68 -12
- package/package.json +2 -2
- package/src/agents/home.ts +4 -2
- package/src/agents/runtime.ts +12 -5
- package/src/builtin.ts +361 -46
- package/src/classifier.ts +3 -1
- package/src/client.ts +68 -12
package/dist/client.js
CHANGED
|
@@ -349,7 +349,7 @@ export class Client {
|
|
|
349
349
|
if (req.maxTokens)
|
|
350
350
|
body.max_tokens = req.maxTokens;
|
|
351
351
|
const signal = this.makeSignal(req.signal);
|
|
352
|
-
{
|
|
352
|
+
try {
|
|
353
353
|
const data = await this.llmCallJson(`${this.opts.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
354
354
|
method: "POST",
|
|
355
355
|
headers: { "content-type": "application/json", authorization: `Bearer ${key}`, ...this.opts.headers },
|
|
@@ -374,6 +374,9 @@ export class Client {
|
|
|
374
374
|
raw: data,
|
|
375
375
|
};
|
|
376
376
|
}
|
|
377
|
+
finally {
|
|
378
|
+
this.releaseSignal(signal);
|
|
379
|
+
}
|
|
377
380
|
}
|
|
378
381
|
/** Anthropic-style upstream: the real translation. */
|
|
379
382
|
async translateAnthropic(req) {
|
|
@@ -407,7 +410,7 @@ export class Client {
|
|
|
407
410
|
if (tc)
|
|
408
411
|
body.tool_choice = tc;
|
|
409
412
|
const signal = this.makeSignal(req.signal);
|
|
410
|
-
{
|
|
413
|
+
try {
|
|
411
414
|
const data = await this.llmCallJson(endpoint, {
|
|
412
415
|
method: "POST",
|
|
413
416
|
headers: {
|
|
@@ -444,6 +447,9 @@ export class Client {
|
|
|
444
447
|
raw: data,
|
|
445
448
|
};
|
|
446
449
|
}
|
|
450
|
+
finally {
|
|
451
|
+
this.releaseSignal(signal);
|
|
452
|
+
}
|
|
447
453
|
}
|
|
448
454
|
/**
|
|
449
455
|
* Renders a translate result's tool calls as an OpenAI `tool_calls` array, ready to put
|
|
@@ -508,12 +514,28 @@ export class Client {
|
|
|
508
514
|
system(toolkit) {
|
|
509
515
|
return [this.opts.systemPrompt ?? "", toolkit?.skillsPrompt() ?? ""].filter(Boolean).join("\n\n");
|
|
510
516
|
}
|
|
511
|
-
/**
|
|
517
|
+
/** Run-scoped timeout timers, cleared by `releaseSignal` when the run settles. */
|
|
518
|
+
runTimers = new WeakMap();
|
|
519
|
+
/** Drop a run's timeout timer. Idempotent, and safe on a signal that never had one. */
|
|
520
|
+
releaseSignal(signal) {
|
|
521
|
+
const t = this.runTimers.get(signal);
|
|
522
|
+
if (t !== undefined) {
|
|
523
|
+
clearTimeout(t);
|
|
524
|
+
this.runTimers.delete(signal);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/** Build a run-scoped abort signal from the optional run-level timeout + an external signal.
|
|
528
|
+
*
|
|
529
|
+
* The timer is deliberately NOT `unref()`d. An unref'd timer cannot keep the event loop
|
|
530
|
+
* alive, so a run whose only pending work is a request that never settles — an injected
|
|
531
|
+
* `fetch`, an in-process transport, a stalled stream — drains the loop and the timeout never
|
|
532
|
+
* fires at all, which is the one case `timeoutMs` exists for. It is RELEASED instead: the
|
|
533
|
+
* caller clears it however the run leaves, so a finished run holds nothing open. */
|
|
512
534
|
makeSignal(external) {
|
|
513
535
|
const ctrl = new AbortController();
|
|
514
536
|
if (this.opts.timeoutMs) {
|
|
515
537
|
const t = setTimeout(() => ctrl.abort(new Error(`run timeout after ${this.opts.timeoutMs}ms`)), this.opts.timeoutMs);
|
|
516
|
-
|
|
538
|
+
this.runTimers.set(ctrl.signal, t);
|
|
517
539
|
}
|
|
518
540
|
if (external) {
|
|
519
541
|
if (external.aborted)
|
|
@@ -612,10 +634,47 @@ export class Client {
|
|
|
612
634
|
throw e;
|
|
613
635
|
}
|
|
614
636
|
}
|
|
615
|
-
//
|
|
637
|
+
// The run-scoped timeout timer is owned by these four wrappers, so it is cleared however the
|
|
638
|
+
// run leaves: return, throw, or a consumer abandoning the stream half-way.
|
|
616
639
|
async runOpenAI(prompt, toolkit, external, history) {
|
|
617
|
-
const key = resolveKey(this.opts);
|
|
618
640
|
const signal = this.makeSignal(external);
|
|
641
|
+
try {
|
|
642
|
+
return await this.runOpenAIWithSignal(signal, prompt, toolkit, history);
|
|
643
|
+
}
|
|
644
|
+
finally {
|
|
645
|
+
this.releaseSignal(signal);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async runAnthropic(prompt, toolkit, external, history) {
|
|
649
|
+
const signal = this.makeSignal(external);
|
|
650
|
+
try {
|
|
651
|
+
return await this.runAnthropicWithSignal(signal, prompt, toolkit, history);
|
|
652
|
+
}
|
|
653
|
+
finally {
|
|
654
|
+
this.releaseSignal(signal);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async *streamOpenAI(prompt, toolkit, external, history) {
|
|
658
|
+
const signal = this.makeSignal(external);
|
|
659
|
+
try {
|
|
660
|
+
yield* this.streamOpenAIWithSignal(signal, prompt, toolkit, history);
|
|
661
|
+
}
|
|
662
|
+
finally {
|
|
663
|
+
this.releaseSignal(signal);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async *streamAnthropic(prompt, toolkit, external, history) {
|
|
667
|
+
const signal = this.makeSignal(external);
|
|
668
|
+
try {
|
|
669
|
+
yield* this.streamAnthropicWithSignal(signal, prompt, toolkit, history);
|
|
670
|
+
}
|
|
671
|
+
finally {
|
|
672
|
+
this.releaseSignal(signal);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// ---- OpenAI-style: POST {baseUrl}/chat/completions ----
|
|
676
|
+
async runOpenAIWithSignal(signal, prompt, toolkit, history) {
|
|
677
|
+
const key = resolveKey(this.opts);
|
|
619
678
|
const runStart = Date.now();
|
|
620
679
|
let messages = history && history.length ? [...history] : [];
|
|
621
680
|
if (!messages.length) {
|
|
@@ -718,9 +777,8 @@ export class Client {
|
|
|
718
777
|
return { result };
|
|
719
778
|
}
|
|
720
779
|
// ---- Anthropic-style: POST {baseUrl}/messages ----
|
|
721
|
-
async
|
|
780
|
+
async runAnthropicWithSignal(signal, prompt, toolkit, history) {
|
|
722
781
|
const key = resolveKey(this.opts);
|
|
723
|
-
const signal = this.makeSignal(external);
|
|
724
782
|
const base = this.opts.baseUrl.replace(/\/$/, "");
|
|
725
783
|
const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
|
|
726
784
|
const system = this.system(toolkit);
|
|
@@ -796,9 +854,8 @@ export class Client {
|
|
|
796
854
|
}
|
|
797
855
|
}
|
|
798
856
|
// ---- Streaming: OpenAI-style ----
|
|
799
|
-
async *
|
|
857
|
+
async *streamOpenAIWithSignal(signal, prompt, toolkit, history) {
|
|
800
858
|
const key = resolveKey(this.opts);
|
|
801
|
-
const signal = this.makeSignal(external);
|
|
802
859
|
const runStart = Date.now();
|
|
803
860
|
let messages = history && history.length ? [...history] : [];
|
|
804
861
|
if (!messages.length) {
|
|
@@ -906,9 +963,8 @@ export class Client {
|
|
|
906
963
|
}
|
|
907
964
|
}
|
|
908
965
|
// ---- Streaming: Anthropic-style ----
|
|
909
|
-
async *
|
|
966
|
+
async *streamAnthropicWithSignal(signal, prompt, toolkit, history) {
|
|
910
967
|
const key = resolveKey(this.opts);
|
|
911
|
-
const signal = this.makeSignal(external);
|
|
912
968
|
const runStart = Date.now();
|
|
913
969
|
const base = this.opts.baseUrl.replace(/\/$/, "");
|
|
914
970
|
const endpoint = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolnexus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Dynamic MCP servers + agent skills as uniform tools for any LLM (opencode-style).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc -p tsconfig.json",
|
|
20
|
-
"test": "npm run build && node --experimental-strip-types --test test/unit.test.ts test/agents.test.ts test/agent-hooks.test.ts test/agent-inprocess.test.ts test/home.test.ts test/compaction.test.ts test/loop.test.ts test/in-process.test.ts test/translate.test.ts test/multimodal.test.ts test/classifier.test.ts test/acp.test.ts test/issues.test.ts",
|
|
20
|
+
"test": "npm run build && node --experimental-strip-types --test test/unit.test.ts test/agents.test.ts test/agent-hooks.test.ts test/agent-inprocess.test.ts test/home.test.ts test/compaction.test.ts test/loop.test.ts test/in-process.test.ts test/translate.test.ts test/multimodal.test.ts test/classifier.test.ts test/acp.test.ts test/issues.test.ts test/host-boundary.test.ts",
|
|
21
21
|
"example": "node --experimental-strip-types examples/basic.ts",
|
|
22
22
|
"example:judge": "node --experimental-strip-types examples/judge.ts",
|
|
23
23
|
"example:multimodal": "node --experimental-strip-types examples/multimodal.ts"
|
package/src/agents/home.ts
CHANGED
|
@@ -234,9 +234,11 @@ export function startAgent(a: Agent, run: AgentRunOptions, hb: HeartbeatOptions)
|
|
|
234
234
|
})
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
|
-
|
|
237
|
+
// keepAlive:false — a heartbeat is housekeeping and must never be the reason
|
|
238
|
+
// a host process stays up.
|
|
239
|
+
cancel = runtime.clock.setTimeout(beat, hb.everyMs, { keepAlive: false }) // self-reschedule → one live timer
|
|
238
240
|
}
|
|
239
|
-
cancel = runtime.clock.setTimeout(beat, hb.everyMs)
|
|
241
|
+
cancel = runtime.clock.setTimeout(beat, hb.everyMs, { keepAlive: false })
|
|
240
242
|
|
|
241
243
|
return {
|
|
242
244
|
runtime,
|
package/src/agents/runtime.ts
CHANGED
|
@@ -39,16 +39,23 @@ import type { Answer, Request, Tool, ToolResult } from "../types.js"
|
|
|
39
39
|
export interface Clock {
|
|
40
40
|
/** Current time in ms (epoch or virtual). */
|
|
41
41
|
now(): number
|
|
42
|
-
/** Schedule `fn` after `ms`; returns a cancel function.
|
|
43
|
-
|
|
42
|
+
/** Schedule `fn` after `ms`; returns a cancel function.
|
|
43
|
+
*
|
|
44
|
+
* `keepAlive` defaults to TRUE, and the default is the important half: a timer
|
|
45
|
+
* that cannot hold the event loop open never fires when it is the only thing
|
|
46
|
+
* left to do — which is precisely a deadline whose promise nothing else will
|
|
47
|
+
* settle. Background housekeeping that should never hold a process open (a
|
|
48
|
+
* heartbeat) passes `false` explicitly. A host's own Clock may ignore the
|
|
49
|
+
* option; it then behaves as that host's timers always have. */
|
|
50
|
+
setTimeout(fn: () => void, ms: number, opts?: { keepAlive?: boolean }): () => void
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
/** The real clock (default).
|
|
53
|
+
/** The real clock (default). See `Clock.setTimeout` for why `keepAlive` defaults to true. */
|
|
47
54
|
export const systemClock: Clock = {
|
|
48
55
|
now: () => Date.now(),
|
|
49
|
-
setTimeout(fn, ms) {
|
|
56
|
+
setTimeout(fn, ms, opts) {
|
|
50
57
|
const t = setTimeout(fn, ms)
|
|
51
|
-
|
|
58
|
+
if (opts?.keepAlive === false) (t as { unref?: () => void }).unref?.()
|
|
52
59
|
return () => clearTimeout(t)
|
|
53
60
|
},
|
|
54
61
|
}
|