toolnexus 0.23.0 → 0.25.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 +139 -1
- package/dist/acp.d.ts +37 -6
- package/dist/acp.js +239 -52
- package/dist/agents/loop.js +4 -1
- package/dist/agents/runtime.js +4 -1
- package/dist/client.d.ts +55 -9
- package/dist/client.js +154 -41
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/mcp.d.ts +32 -7
- package/dist/mcp.js +125 -36
- package/dist/mcp2026/auth.d.ts +137 -0
- package/dist/mcp2026/auth.js +446 -0
- package/dist/mcp2026/client.d.ts +48 -0
- package/dist/mcp2026/client.js +160 -0
- package/dist/mcp2026/protocol.d.ts +54 -0
- package/dist/mcp2026/protocol.js +113 -0
- package/dist/mcp2026/skills.d.ts +5 -0
- package/dist/mcp2026/skills.js +25 -0
- package/dist/mcp2026/source.d.ts +86 -0
- package/dist/mcp2026/source.js +372 -0
- package/dist/mcp2026/stdio.d.ts +24 -0
- package/dist/mcp2026/stdio.js +99 -0
- package/dist/mcp2026/toolsearch.d.ts +5 -0
- package/dist/mcp2026/toolsearch.js +58 -0
- package/dist/mcphandler.d.ts +169 -0
- package/dist/mcphandler.js +1145 -0
- package/dist/mcpheader.d.ts +5 -0
- package/dist/mcpheader.js +18 -0
- package/dist/mcpserve.d.ts +24 -15
- package/dist/mcpserve.js +105 -2
- package/dist/mcpserverauth.d.ts +48 -0
- package/dist/mcpserverauth.js +203 -0
- package/dist/media.d.ts +126 -0
- package/dist/media.js +540 -0
- package/dist/skill.d.ts +21 -0
- package/dist/skill.js +44 -0
- package/dist/toolconcurrency.d.ts +16 -0
- package/dist/toolconcurrency.js +59 -0
- package/dist/toolkit.d.ts +23 -1
- package/dist/toolkit.js +26 -4
- package/dist/types.d.ts +26 -1
- package/package.json +5 -3
- package/src/acp.ts +258 -55
- package/src/agents/loop.ts +4 -1
- package/src/agents/runtime.ts +4 -1
- package/src/client.ts +196 -48
- package/src/index.ts +4 -0
- package/src/mcp.ts +162 -43
- package/src/mcp2026/auth.ts +473 -0
- package/src/mcp2026/client.ts +161 -0
- package/src/mcp2026/protocol.ts +135 -0
- package/src/mcp2026/skills.ts +22 -0
- package/src/mcp2026/source.ts +407 -0
- package/src/mcp2026/stdio.ts +102 -0
- package/src/mcp2026/toolsearch.ts +56 -0
- package/src/mcphandler.ts +1252 -0
- package/src/mcpheader.ts +19 -0
- package/src/mcpserve.ts +115 -2
- package/src/mcpserverauth.ts +216 -0
- package/src/media.ts +604 -0
- package/src/skill.ts +50 -0
- package/src/toolconcurrency.ts +67 -0
- package/src/toolkit.ts +35 -5
- package/src/types.ts +27 -1
package/dist/client.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { Toolkit } from "./toolkit.js";
|
|
|
7
7
|
import type { ToolHints, ToolResult, Request, Answer } from "./types.js";
|
|
8
8
|
import { type PromptCacheOptions } from "./promptcache.js";
|
|
9
9
|
import { type ToolTier, type ToolTierOptions } from "./tooltiers.js";
|
|
10
|
-
import type { PromptInput, UnsupportedPartMode } from "./content.js";
|
|
10
|
+
import type { ContentPart, PromptInput, UnsupportedPartMode } from "./content.js";
|
|
11
11
|
import type { TranslateRequest, TranslateResult } from "./translate.js";
|
|
12
12
|
export type ClientStyle = "openai" | "anthropic";
|
|
13
13
|
export interface ClientOptions {
|
|
@@ -26,6 +26,10 @@ export interface ClientOptions {
|
|
|
26
26
|
headers?: Record<string, string>;
|
|
27
27
|
systemPrompt?: string;
|
|
28
28
|
maxTurns?: number;
|
|
29
|
+
/** Cap on concurrency-safe tool calls of one response running at once (SPEC §8 "Tool
|
|
30
|
+
* scheduling"). Absent ⇒ env `TOOLNEXUS_MAX_TOOL_CONCURRENCY` when a positive integer, else 10.
|
|
31
|
+
* Unsafe calls (anything not read-only) always run alone. */
|
|
32
|
+
maxToolConcurrency?: number;
|
|
29
33
|
hooks?: Hooks;
|
|
30
34
|
/** Retries on transient LLM errors (`429`/`500`/`502`/`503`/`504`/`529` + network). Default 2.
|
|
31
35
|
* Widen the status set with `retryableStatuses`. */
|
|
@@ -299,7 +303,19 @@ export interface ToolCallRecord {
|
|
|
299
303
|
output: string;
|
|
300
304
|
isError: boolean;
|
|
301
305
|
metadata?: Record<string, unknown>;
|
|
306
|
+
/** §8: the ToolResult's parts — present iff the result had parts. */
|
|
307
|
+
parts?: ContentPart[];
|
|
302
308
|
}
|
|
309
|
+
/** §8 RunResult.files entry: a non-text part a tool call produced this run. */
|
|
310
|
+
export type RunFile = {
|
|
311
|
+
type: "image" | "file" | "audio";
|
|
312
|
+
mimeType: string;
|
|
313
|
+
url?: string;
|
|
314
|
+
data?: string;
|
|
315
|
+
name?: string;
|
|
316
|
+
from: string;
|
|
317
|
+
callId: string;
|
|
318
|
+
};
|
|
303
319
|
/** Token usage, summed across every LLM round trip in the run. */
|
|
304
320
|
export interface Usage {
|
|
305
321
|
promptTokens: number;
|
|
@@ -319,6 +335,8 @@ export interface RunResult {
|
|
|
319
335
|
messages: any[];
|
|
320
336
|
/** Every tool call made, with its output, error flag, and metadata. */
|
|
321
337
|
toolCalls: ToolCallRecord[];
|
|
338
|
+
/** Every non-text part produced by tool calls this run, in call order (§8). Always present. */
|
|
339
|
+
files: RunFile[];
|
|
322
340
|
/** Total number of tool calls (= toolCalls.length). */
|
|
323
341
|
toolCallCount: number;
|
|
324
342
|
/** Number of LLM round trips. */
|
|
@@ -340,12 +358,21 @@ export interface RunResult {
|
|
|
340
358
|
export type StreamEvent = {
|
|
341
359
|
type: "text";
|
|
342
360
|
delta: string;
|
|
343
|
-
}
|
|
361
|
+
}
|
|
362
|
+
/** Model reasoning narration (e.g. an ACP agent's `agent_thought_chunk`). Never part of
|
|
363
|
+
* the answer text or the RunResult. */
|
|
364
|
+
| {
|
|
365
|
+
type: "thought";
|
|
366
|
+
delta: string;
|
|
367
|
+
via?: "acp";
|
|
368
|
+
}
|
|
369
|
+
/** `via: "acp"` marks a tool the ACP agent ran itself: reported, never executed by the loop. */
|
|
370
|
+
| {
|
|
344
371
|
type: "tool_call";
|
|
345
372
|
id: string;
|
|
346
373
|
name: string;
|
|
347
374
|
args: Record<string, unknown>;
|
|
348
|
-
via?: "call_tool";
|
|
375
|
+
via?: "call_tool" | "acp";
|
|
349
376
|
} | {
|
|
350
377
|
type: "tool_result";
|
|
351
378
|
id: string;
|
|
@@ -353,7 +380,7 @@ export type StreamEvent = {
|
|
|
353
380
|
output: string;
|
|
354
381
|
isError: boolean;
|
|
355
382
|
metadata?: Record<string, unknown>;
|
|
356
|
-
via?: "call_tool";
|
|
383
|
+
via?: "call_tool" | "acp";
|
|
357
384
|
} | {
|
|
358
385
|
type: "usage";
|
|
359
386
|
usage: Usage;
|
|
@@ -491,6 +518,10 @@ export declare class Client {
|
|
|
491
518
|
/** Run one tool through beforeTool/afterTool hooks (mutate args, short-circuit, transform result). */
|
|
492
519
|
private runTool;
|
|
493
520
|
private runToolDirect;
|
|
521
|
+
/** Runs one response's calls under the tool schedule (SPEC §8 "Tool scheduling"). */
|
|
522
|
+
private schedule;
|
|
523
|
+
/** One call's safety; looks through `call_tool` to the tool it names. Unknown ⇒ unsafe. */
|
|
524
|
+
private callSafe;
|
|
494
525
|
/** ToolTiers `call_tool {name, arguments}` -> the real call; undefined when tiers are off. */
|
|
495
526
|
private unwrapCall;
|
|
496
527
|
/** The RunResult.toolCalls entry: a `call_tool` call is recorded as the real tool with its
|
|
@@ -560,11 +591,22 @@ export interface InProcessResponse {
|
|
|
560
591
|
totalTokens?: number;
|
|
561
592
|
};
|
|
562
593
|
}
|
|
594
|
+
/** A streaming in-process model: `emit` each event as it happens — `text` deltas become
|
|
595
|
+
* the streamed answer as they arrive; any other event (thought, an agent's own tool_call /
|
|
596
|
+
* tool_result) goes straight to the stream consumer and is never executed by the loop.
|
|
597
|
+
* Return the whole message when done (its `toolCalls`, if any, are what the loop runs). */
|
|
598
|
+
export type InProcessGenerateStream = (request: InProcessRequest, emit: (event: StreamEvent) => void) => InProcessResponse | Promise<InProcessResponse>;
|
|
599
|
+
type InProcessGenerate = (request: InProcessRequest) => InProcessResponse | Promise<InProcessResponse>;
|
|
563
600
|
/** Options for {@link createInProcessClient}: every client option EXCEPT the three
|
|
564
|
-
* that describe a wire — `baseUrl`, `apiKey` and `style` — plus `generate
|
|
601
|
+
* that describe a wire — `baseUrl`, `apiKey` and `style` — plus `generate` and/or
|
|
602
|
+
* `generateStream` (at least one). */
|
|
565
603
|
export type InProcessOptions = Omit<ClientOptions, "baseUrl" | "apiKey" | "style" | "fetch"> & {
|
|
566
604
|
/** Your model. Receives the assembled request, returns one assistant message. */
|
|
567
|
-
generate
|
|
605
|
+
generate?: InProcessGenerate;
|
|
606
|
+
/** Your model, streaming for real. With it, `stream()` is incremental; without it a
|
|
607
|
+
* `stream()` on a plain `generate` is refused. When both are set, `generate` serves
|
|
608
|
+
* non-streaming calls; alone, it serves them too (with a no-op `emit`). */
|
|
609
|
+
generateStream?: InProcessGenerateStream;
|
|
568
610
|
};
|
|
569
611
|
/**
|
|
570
612
|
* Build a `fetch`-shaped adapter over a semantic `generate` function (ADR 0030):
|
|
@@ -574,9 +616,11 @@ export type InProcessOptions = Omit<ClientOptions, "baseUrl" | "apiKey" | "style
|
|
|
574
616
|
* or the agent runtime's `inProcess` option) goes through this ONE function, so there
|
|
575
617
|
* is no second copy of the request/response assembly to drift.
|
|
576
618
|
*
|
|
577
|
-
* Streaming is refused rather than faked — see the thrown
|
|
619
|
+
* Streaming with only a plain `generate` is refused rather than faked — see the thrown
|
|
620
|
+
* error. With `generateStream` it streams for real: each text event is written to the
|
|
621
|
+
* SSE body the moment it is emitted.
|
|
578
622
|
*/
|
|
579
|
-
export declare function createInProcessFetch(generate
|
|
623
|
+
export declare function createInProcessFetch(generate?: InProcessGenerate, generateStream?: InProcessGenerateStream): typeof fetch;
|
|
580
624
|
/**
|
|
581
625
|
* A client backed by a model running IN THIS PROCESS — no server, no socket, and no
|
|
582
626
|
* HTTP types to construct.
|
|
@@ -587,7 +631,8 @@ export declare function createInProcessFetch(generate: (request: InProcessReques
|
|
|
587
631
|
* The `fetch`-shaped adapter itself is {@link createInProcessFetch} — this function
|
|
588
632
|
* is just that adapter wired into a `Client`.
|
|
589
633
|
*
|
|
590
|
-
* Streaming is refused rather than faked
|
|
634
|
+
* Streaming is refused rather than faked for a plain `generate`; with `generateStream`
|
|
635
|
+
* it streams for real — see {@link createInProcessFetch}.
|
|
591
636
|
*/
|
|
592
637
|
export declare function createInProcessClient(opts: InProcessOptions): Client;
|
|
593
638
|
/** Stateful multi-turn conversation: each send() continues the same transcript (memory). */
|
|
@@ -603,3 +648,4 @@ export declare class Conversation {
|
|
|
603
648
|
/** Reset the conversation memory. */
|
|
604
649
|
reset(): void;
|
|
605
650
|
}
|
|
651
|
+
export {};
|
package/dist/client.js
CHANGED
|
@@ -3,6 +3,7 @@ import { toOpenAI, toAnthropic } from "./adapters.js";
|
|
|
3
3
|
import { applyPromptCache } from "./promptcache.js";
|
|
4
4
|
import { buildToolPlan, toolSearch, unwrapCallTool } from "./tooltiers.js";
|
|
5
5
|
import { isRetryableStatus, retryAfterMs } from "./retry.js";
|
|
6
|
+
import { isConcurrencySafe, resolveMaxToolConcurrency, scheduleCalls } from "./toolconcurrency.js";
|
|
6
7
|
import { toOpenAIWire, toAnthropicWire, checkPromptParts } from "./wire.js";
|
|
7
8
|
import { finishReasonFor, argsString, openAIMessagesToAnthropic, openAIToolsToAnthropic, openAIToolChoiceToAnthropic, hasSystemMessage, } from "./translate.js";
|
|
8
9
|
/**
|
|
@@ -106,6 +107,20 @@ function delay(ms, signal) {
|
|
|
106
107
|
signal.addEventListener("abort", () => { clearTimeout(t); reject(signal.reason ?? new Error("aborted")); }, { once: true });
|
|
107
108
|
});
|
|
108
109
|
}
|
|
110
|
+
/** Model tool-call id of each record, kept off the record itself (§8 shape has no id). */
|
|
111
|
+
const callIds = new WeakMap();
|
|
112
|
+
/** RunResult.files: every non-text part of every tool call, in call order (§8). */
|
|
113
|
+
function runFiles(toolCalls) {
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const r of toolCalls) {
|
|
116
|
+
for (const p of r.parts ?? []) {
|
|
117
|
+
if (p.type === "text")
|
|
118
|
+
continue;
|
|
119
|
+
out.push({ ...p, from: r.name, callId: callIds.get(r) ?? "" });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
109
124
|
function addUsage(acc, raw, style) {
|
|
110
125
|
if (!raw)
|
|
111
126
|
return;
|
|
@@ -718,15 +733,34 @@ export class Client {
|
|
|
718
733
|
this.emitTool(name, source, result, t0);
|
|
719
734
|
return { args: a, result };
|
|
720
735
|
}
|
|
736
|
+
/** Runs one response's calls under the tool schedule (SPEC §8 "Tool scheduling"). */
|
|
737
|
+
schedule(toolkit, items, key, fn) {
|
|
738
|
+
return scheduleCalls(items, (item) => {
|
|
739
|
+
const [name, args] = key(item);
|
|
740
|
+
return this.callSafe(toolkit, name, args);
|
|
741
|
+
}, resolveMaxToolConcurrency(this.opts.maxToolConcurrency), fn);
|
|
742
|
+
}
|
|
743
|
+
/** One call's safety; looks through `call_tool` to the tool it names. Unknown ⇒ unsafe. */
|
|
744
|
+
callSafe(toolkit, name, args) {
|
|
745
|
+
if (this.opts.toolTiers && name === "tool_search")
|
|
746
|
+
return true;
|
|
747
|
+
const inner = this.unwrapCall(name, args ?? {});
|
|
748
|
+
const tool = toolkit?.get(inner ? inner.name : name);
|
|
749
|
+
return tool ? isConcurrencySafe(tool, inner ? inner.args : (args ?? {})) : false;
|
|
750
|
+
}
|
|
721
751
|
/** ToolTiers `call_tool {name, arguments}` -> the real call; undefined when tiers are off. */
|
|
722
752
|
unwrapCall(name, args) {
|
|
723
753
|
return this.opts.toolTiers ? unwrapCallTool(name, args) : undefined;
|
|
724
754
|
}
|
|
725
755
|
/** The RunResult.toolCalls entry: a `call_tool` call is recorded as the real tool with its
|
|
726
756
|
* inner arguments, the name hooks and stream events see (the transcript keeps the wrapper). */
|
|
727
|
-
toolCallRecord(name, args, result) {
|
|
757
|
+
toolCallRecord(name, args, result, callId) {
|
|
728
758
|
const inner = this.unwrapCall(name, args);
|
|
729
|
-
|
|
759
|
+
const rec = { name: inner ? inner.name : name, args: inner ? inner.args : args, output: result.output, isError: result.isError, metadata: result.metadata };
|
|
760
|
+
if (result.parts?.length)
|
|
761
|
+
rec.parts = result.parts;
|
|
762
|
+
callIds.set(rec, callId ?? "");
|
|
763
|
+
return rec;
|
|
730
764
|
}
|
|
731
765
|
/** The stream's tool_call event, naming the real tool for a `call_tool` call. */
|
|
732
766
|
callEvent(id, name, args) {
|
|
@@ -886,7 +920,7 @@ export class Client {
|
|
|
886
920
|
return this.endRun(runStart, lastModel, msg.content ?? "", messages, toolCalls, turns, usage);
|
|
887
921
|
// execute all tool calls in this turn concurrently (true parallel tool calling)
|
|
888
922
|
done.clear();
|
|
889
|
-
const settled = await untilAborted(signal,
|
|
923
|
+
const settled = await untilAborted(signal, this.schedule(toolkit, calls, (call) => [call.function.name, safeJson(call.function.arguments)], async (call) => {
|
|
890
924
|
let { args, result } = await this.runTool(toolkit, call.function.name, safeJson(call.function.arguments), call.id, turn, signal);
|
|
891
925
|
let halted;
|
|
892
926
|
const req = pendingOf(result);
|
|
@@ -898,12 +932,12 @@ export class Client {
|
|
|
898
932
|
if (!halted && call.id)
|
|
899
933
|
done.set(call.id, result);
|
|
900
934
|
return { call, args, result, halted };
|
|
901
|
-
}))
|
|
935
|
+
}));
|
|
902
936
|
// Record in tool-call order; on the FIRST durable halt, surface it and stop — deterministic,
|
|
903
937
|
// and the later suspensions' placeholder results never enter the transcript (they re-suspend
|
|
904
938
|
// on resume). Mirrors the streaming path.
|
|
905
939
|
for (const s of settled) {
|
|
906
|
-
toolCalls.push(this.toolCallRecord(s.call.function.name, s.args, s.result));
|
|
940
|
+
toolCalls.push(this.toolCallRecord(s.call.function.name, s.args, s.result, s.call.id));
|
|
907
941
|
messages.push(toolMessage(s.call.id, s.call.function.name, s.result));
|
|
908
942
|
if (s.halted)
|
|
909
943
|
return this.pendingRun(runStart, lastModel, s.halted, messages, toolCalls, turns, usage);
|
|
@@ -919,7 +953,7 @@ export class Client {
|
|
|
919
953
|
}
|
|
920
954
|
result(model, text, messages, toolCalls, turns, usage, exhausted = false) {
|
|
921
955
|
const incomplete = exhausted && text === "";
|
|
922
|
-
const r = { text, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model, status: incomplete ? "incomplete" : "done" };
|
|
956
|
+
const r = { text, messages, toolCalls, files: runFiles(toolCalls), toolCallCount: toolCalls.length, turns, usage, model, status: incomplete ? "incomplete" : "done" };
|
|
923
957
|
if (incomplete)
|
|
924
958
|
r.limit = "maxTurns";
|
|
925
959
|
return r;
|
|
@@ -927,7 +961,7 @@ export class Client {
|
|
|
927
961
|
/** §10: a run halted because a tool suspended and no `waitFor` was configured. */
|
|
928
962
|
pendingRun(runStart, model, request, messages, toolCalls, turns, usage) {
|
|
929
963
|
this.emit({ event: "run", model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart });
|
|
930
|
-
return { text: request.prompt, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model, status: "pending", pending: request };
|
|
964
|
+
return { text: request.prompt, messages, toolCalls, files: runFiles(toolCalls), toolCallCount: toolCalls.length, turns, usage, model, status: "pending", pending: request };
|
|
931
965
|
}
|
|
932
966
|
/**
|
|
933
967
|
* §10: resolve a suspended tool result. Calls `waitFor(request)`, and on `ok` re-executes the
|
|
@@ -1008,7 +1042,7 @@ export class Client {
|
|
|
1008
1042
|
}
|
|
1009
1043
|
// execute all tool_use blocks in this turn concurrently (true parallel tool calling)
|
|
1010
1044
|
done.clear();
|
|
1011
|
-
const settled = await untilAborted(signal,
|
|
1045
|
+
const settled = await untilAborted(signal, this.schedule(toolkit, uses, (use) => [use.name, use.input ?? {}], async (use) => {
|
|
1012
1046
|
let { args, result } = await this.runTool(toolkit, use.name, use.input ?? {}, use.id, turn, signal);
|
|
1013
1047
|
let halted;
|
|
1014
1048
|
const req = pendingOf(result);
|
|
@@ -1020,14 +1054,14 @@ export class Client {
|
|
|
1020
1054
|
if (!halted && use.id)
|
|
1021
1055
|
done.set(use.id, result);
|
|
1022
1056
|
return { use, args, result, halted };
|
|
1023
|
-
}))
|
|
1057
|
+
}));
|
|
1024
1058
|
// Record in tool-call order; on the FIRST durable halt, include that tool_result block, stop
|
|
1025
1059
|
// building the content, and surface it — deterministic (G3), later suspensions re-suspend on
|
|
1026
1060
|
// resume. Mirrors the OpenAI path and the streaming loops.
|
|
1027
1061
|
const content = [];
|
|
1028
1062
|
let halted;
|
|
1029
1063
|
for (const s of settled) {
|
|
1030
|
-
toolCalls.push(this.toolCallRecord(s.use.name, s.args, s.result));
|
|
1064
|
+
toolCalls.push(this.toolCallRecord(s.use.name, s.args, s.result, s.use.id));
|
|
1031
1065
|
content.push(toolResultBlock(s.use.id, s.result));
|
|
1032
1066
|
if (s.halted) {
|
|
1033
1067
|
halted = s.halted;
|
|
@@ -1098,6 +1132,13 @@ export class Client {
|
|
|
1098
1132
|
if (payload === "[DONE]")
|
|
1099
1133
|
break;
|
|
1100
1134
|
const j = safeParse(payload);
|
|
1135
|
+
// A non-text event from an in-process streaming generate rides in-band so it keeps
|
|
1136
|
+
// its order and arrives as it happens; it goes straight to the consumer and is never
|
|
1137
|
+
// executed. Honoured only on the in-process sentinel URL, never from a real server.
|
|
1138
|
+
if (j && j[IN_PROCESS_SIDE_KEY] && this.opts.baseUrl === IN_PROCESS_BASE_URL) {
|
|
1139
|
+
yield j[IN_PROCESS_SIDE_KEY];
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1101
1142
|
if (j?.usage)
|
|
1102
1143
|
addUsage(usage, j.usage, "openai");
|
|
1103
1144
|
const choice = j?.choices?.[0];
|
|
@@ -1136,12 +1177,12 @@ export class Client {
|
|
|
1136
1177
|
for (const c of calls)
|
|
1137
1178
|
yield this.callEvent(c.id, c.name, safeJson(c.args));
|
|
1138
1179
|
done.clear();
|
|
1139
|
-
const settled = await untilAborted(signal,
|
|
1180
|
+
const settled = await untilAborted(signal, this.schedule(toolkit, calls, (c) => [c.name, safeJson(c.args)], async (c) => {
|
|
1140
1181
|
const r = await this.runTool(toolkit, c.name, safeJson(c.args), c.id, turn, signal);
|
|
1141
1182
|
if (!pendingOf(r.result) && c.id)
|
|
1142
1183
|
done.set(c.id, r.result);
|
|
1143
1184
|
return r;
|
|
1144
|
-
}))
|
|
1185
|
+
}));
|
|
1145
1186
|
for (let i = 0; i < calls.length; i++) {
|
|
1146
1187
|
const c = calls[i];
|
|
1147
1188
|
let { args, result } = settled[i];
|
|
@@ -1151,13 +1192,13 @@ export class Client {
|
|
|
1151
1192
|
const r = await this.resolvePending(toolkit, c.name, args, req, c.id, turn, signal);
|
|
1152
1193
|
result = r.result;
|
|
1153
1194
|
if (r.halted) {
|
|
1154
|
-
toolCalls.push(this.toolCallRecord(c.name, args, result));
|
|
1195
|
+
toolCalls.push(this.toolCallRecord(c.name, args, result, c.id));
|
|
1155
1196
|
messages.push(toolMessage(c.id, c.name, result));
|
|
1156
1197
|
yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) };
|
|
1157
1198
|
return;
|
|
1158
1199
|
}
|
|
1159
1200
|
}
|
|
1160
|
-
toolCalls.push(this.toolCallRecord(c.name, args, result));
|
|
1201
|
+
toolCalls.push(this.toolCallRecord(c.name, args, result, c.id));
|
|
1161
1202
|
messages.push(toolMessage(c.id, c.name, result));
|
|
1162
1203
|
yield this.resultEvent(c.id, c.name, safeJson(c.args), result);
|
|
1163
1204
|
}
|
|
@@ -1256,12 +1297,12 @@ export class Client {
|
|
|
1256
1297
|
for (const u of uses)
|
|
1257
1298
|
yield this.callEvent(u.id, u.name, u.input);
|
|
1258
1299
|
done.clear();
|
|
1259
|
-
const settled = await untilAborted(signal,
|
|
1300
|
+
const settled = await untilAborted(signal, this.schedule(toolkit, uses, (u) => [u.name, u.input ?? {}], async (u) => {
|
|
1260
1301
|
const r = await this.runTool(toolkit, u.name, u.input ?? {}, u.id, turn, signal);
|
|
1261
1302
|
if (!pendingOf(r.result) && u.id)
|
|
1262
1303
|
done.set(u.id, r.result);
|
|
1263
1304
|
return r;
|
|
1264
|
-
}))
|
|
1305
|
+
}));
|
|
1265
1306
|
const results = [];
|
|
1266
1307
|
for (let i = 0; i < uses.length; i++) {
|
|
1267
1308
|
const u = uses[i];
|
|
@@ -1272,14 +1313,14 @@ export class Client {
|
|
|
1272
1313
|
const r = await this.resolvePending(toolkit, u.name, args, req, u.id, turn, signal);
|
|
1273
1314
|
result = r.result;
|
|
1274
1315
|
if (r.halted) {
|
|
1275
|
-
toolCalls.push(this.toolCallRecord(u.name, args, result));
|
|
1316
|
+
toolCalls.push(this.toolCallRecord(u.name, args, result, u.id));
|
|
1276
1317
|
results.push(toolResultBlock(u.id, result));
|
|
1277
1318
|
messages.push({ role: "user", content: results });
|
|
1278
1319
|
yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) };
|
|
1279
1320
|
return;
|
|
1280
1321
|
}
|
|
1281
1322
|
}
|
|
1282
|
-
toolCalls.push(this.toolCallRecord(u.name, args, result));
|
|
1323
|
+
toolCalls.push(this.toolCallRecord(u.name, args, result, u.id));
|
|
1283
1324
|
results.push(toolResultBlock(u.id, result));
|
|
1284
1325
|
yield this.resultEvent(u.id, u.name, u.input, result);
|
|
1285
1326
|
}
|
|
@@ -1302,6 +1343,9 @@ export function createClient(opts) {
|
|
|
1302
1343
|
* it must be syntactically valid. `.invalid` is reserved by RFC 2606 precisely so a
|
|
1303
1344
|
* name can never resolve. */
|
|
1304
1345
|
const IN_PROCESS_BASE_URL = "http://in-process.invalid/v1";
|
|
1346
|
+
/** The in-band SSE key that carries a non-text stream event from an in-process
|
|
1347
|
+
* streaming generate to the consumer (honoured only on {@link IN_PROCESS_BASE_URL}). */
|
|
1348
|
+
const IN_PROCESS_SIDE_KEY = "toolnexus_event";
|
|
1305
1349
|
/**
|
|
1306
1350
|
* Build a `fetch`-shaped adapter over a semantic `generate` function (ADR 0030):
|
|
1307
1351
|
* the reusable piece of {@link createInProcessClient} — assembles the wire request,
|
|
@@ -1310,14 +1354,19 @@ const IN_PROCESS_BASE_URL = "http://in-process.invalid/v1";
|
|
|
1310
1354
|
* or the agent runtime's `inProcess` option) goes through this ONE function, so there
|
|
1311
1355
|
* is no second copy of the request/response assembly to drift.
|
|
1312
1356
|
*
|
|
1313
|
-
* Streaming is refused rather than faked — see the thrown
|
|
1357
|
+
* Streaming with only a plain `generate` is refused rather than faked — see the thrown
|
|
1358
|
+
* error. With `generateStream` it streams for real: each text event is written to the
|
|
1359
|
+
* SSE body the moment it is emitted.
|
|
1314
1360
|
*/
|
|
1315
|
-
export function createInProcessFetch(generate) {
|
|
1316
|
-
if (typeof generate !== "function") {
|
|
1317
|
-
throw new Error("toolnexus: createInProcessFetch requires a `generate` function");
|
|
1361
|
+
export function createInProcessFetch(generate, generateStream) {
|
|
1362
|
+
if (typeof generate !== "function" && typeof generateStream !== "function") {
|
|
1363
|
+
throw new Error("toolnexus: createInProcessFetch requires a `generate` (or `generateStream`) function");
|
|
1318
1364
|
}
|
|
1319
1365
|
const inProcessFetch = async (_url, init) => {
|
|
1320
1366
|
const body = JSON.parse(String(init?.body ?? "{}"));
|
|
1367
|
+
const request = { messages: body.messages ?? [], tools: body.tools, model: body.model, body };
|
|
1368
|
+
if (body.stream && generateStream)
|
|
1369
|
+
return inProcessStream(generateStream, request);
|
|
1321
1370
|
// A generate() returns a whole answer, so a stream would be one chunk pretending
|
|
1322
1371
|
// to be many. Content and delta-count assertions cannot tell that from a real
|
|
1323
1372
|
// stream, so refuse instead of degrading silently.
|
|
@@ -1325,24 +1374,10 @@ export function createInProcessFetch(generate) {
|
|
|
1325
1374
|
throw new Error("toolnexus: createInProcessClient does not support streaming — `generate` returns a " +
|
|
1326
1375
|
"complete answer. Use the non-streaming run, or supply a `fetch`/transport that streams.");
|
|
1327
1376
|
}
|
|
1328
|
-
const answer = await generate({
|
|
1329
|
-
messages: body.messages ?? [],
|
|
1330
|
-
tools: body.tools,
|
|
1331
|
-
model: body.model,
|
|
1332
|
-
body,
|
|
1333
|
-
});
|
|
1377
|
+
const answer = generate ? await generate(request) : await generateStream(request, () => { });
|
|
1334
1378
|
const message = { role: "assistant" };
|
|
1335
1379
|
if (answer.toolCalls && answer.toolCalls.length > 0) {
|
|
1336
|
-
message.tool_calls = answer.toolCalls
|
|
1337
|
-
id: c.id ?? `call_${i}`,
|
|
1338
|
-
type: "function",
|
|
1339
|
-
function: {
|
|
1340
|
-
name: c.name,
|
|
1341
|
-
// A string is already encoded; anything else is encoded here so a model
|
|
1342
|
-
// author never has to think about the wire.
|
|
1343
|
-
arguments: typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {}),
|
|
1344
|
-
},
|
|
1345
|
-
}));
|
|
1380
|
+
message.tool_calls = wireToolCalls(answer.toolCalls, false);
|
|
1346
1381
|
}
|
|
1347
1382
|
else {
|
|
1348
1383
|
message.content = answer.content ?? "";
|
|
@@ -1361,6 +1396,80 @@ export function createInProcessFetch(generate) {
|
|
|
1361
1396
|
};
|
|
1362
1397
|
return inProcessFetch;
|
|
1363
1398
|
}
|
|
1399
|
+
function wireToolCalls(calls, indexed) {
|
|
1400
|
+
return calls.map((c, i) => ({
|
|
1401
|
+
...(indexed ? { index: i } : {}),
|
|
1402
|
+
id: c.id ?? `call_${i}`,
|
|
1403
|
+
type: "function",
|
|
1404
|
+
function: {
|
|
1405
|
+
name: c.name,
|
|
1406
|
+
// A string is already encoded; anything else is encoded here so a model
|
|
1407
|
+
// author never has to think about the wire.
|
|
1408
|
+
arguments: typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {}),
|
|
1409
|
+
},
|
|
1410
|
+
}));
|
|
1411
|
+
}
|
|
1412
|
+
/** Runs a streaming generate and answers with an OpenAI SSE body written AS EVENTS ARRIVE:
|
|
1413
|
+
* text → a content delta; any other event → an in-band side chunk for the consumer; then
|
|
1414
|
+
* (tool calls as one delta | the content as one delta when no text was emitted), the
|
|
1415
|
+
* finish_reason chunk, usage, and [DONE]. */
|
|
1416
|
+
function inProcessStream(generateStream, request) {
|
|
1417
|
+
const enc = new TextEncoder();
|
|
1418
|
+
let open = true;
|
|
1419
|
+
const sse = new ReadableStream({
|
|
1420
|
+
start(controller) {
|
|
1421
|
+
const chunk = (v) => {
|
|
1422
|
+
if (open)
|
|
1423
|
+
controller.enqueue(enc.encode(`data: ${JSON.stringify(v)}\n\n`));
|
|
1424
|
+
};
|
|
1425
|
+
let sawText = false;
|
|
1426
|
+
void (async () => {
|
|
1427
|
+
try {
|
|
1428
|
+
const answer = await generateStream(request, (ev) => {
|
|
1429
|
+
if (!ev || typeof ev !== "object")
|
|
1430
|
+
return;
|
|
1431
|
+
if (ev.type === "text") {
|
|
1432
|
+
if (ev.delta) {
|
|
1433
|
+
sawText = true;
|
|
1434
|
+
chunk({ choices: [{ index: 0, delta: { content: ev.delta } }] });
|
|
1435
|
+
}
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
chunk({ [IN_PROCESS_SIDE_KEY]: ev });
|
|
1439
|
+
});
|
|
1440
|
+
let finish = "stop";
|
|
1441
|
+
if (answer.toolCalls && answer.toolCalls.length > 0) {
|
|
1442
|
+
chunk({ choices: [{ index: 0, delta: { tool_calls: wireToolCalls(answer.toolCalls, true) } }] });
|
|
1443
|
+
finish = "tool_calls";
|
|
1444
|
+
}
|
|
1445
|
+
else if (!sawText && answer.content) {
|
|
1446
|
+
chunk({ choices: [{ index: 0, delta: { content: answer.content } }] });
|
|
1447
|
+
}
|
|
1448
|
+
chunk({ choices: [{ index: 0, delta: {}, finish_reason: finish }] });
|
|
1449
|
+
const u = answer.usage;
|
|
1450
|
+
if (u) {
|
|
1451
|
+
const p = u.promptTokens ?? 0, c = u.completionTokens ?? 0;
|
|
1452
|
+
chunk({ choices: [], usage: { prompt_tokens: p, completion_tokens: c, total_tokens: u.totalTokens || p + c } });
|
|
1453
|
+
}
|
|
1454
|
+
if (open)
|
|
1455
|
+
controller.enqueue(enc.encode("data: [DONE]\n\n"));
|
|
1456
|
+
open = false;
|
|
1457
|
+
controller.close();
|
|
1458
|
+
}
|
|
1459
|
+
catch (e) {
|
|
1460
|
+
if (open) {
|
|
1461
|
+
open = false;
|
|
1462
|
+
controller.error(e);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
})();
|
|
1466
|
+
},
|
|
1467
|
+
cancel() {
|
|
1468
|
+
open = false;
|
|
1469
|
+
},
|
|
1470
|
+
});
|
|
1471
|
+
return new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } });
|
|
1472
|
+
}
|
|
1364
1473
|
/**
|
|
1365
1474
|
* A client backed by a model running IN THIS PROCESS — no server, no socket, and no
|
|
1366
1475
|
* HTTP types to construct.
|
|
@@ -1371,11 +1480,15 @@ export function createInProcessFetch(generate) {
|
|
|
1371
1480
|
* The `fetch`-shaped adapter itself is {@link createInProcessFetch} — this function
|
|
1372
1481
|
* is just that adapter wired into a `Client`.
|
|
1373
1482
|
*
|
|
1374
|
-
* Streaming is refused rather than faked
|
|
1483
|
+
* Streaming is refused rather than faked for a plain `generate`; with `generateStream`
|
|
1484
|
+
* it streams for real — see {@link createInProcessFetch}.
|
|
1375
1485
|
*/
|
|
1376
1486
|
export function createInProcessClient(opts) {
|
|
1377
|
-
const { generate, ...rest } = opts;
|
|
1378
|
-
|
|
1487
|
+
const { generate, generateStream, ...rest } = opts;
|
|
1488
|
+
if (typeof generate !== "function" && typeof generateStream !== "function") {
|
|
1489
|
+
throw new Error("toolnexus: createInProcessClient requires a `generate` (or `generateStream`) function");
|
|
1490
|
+
}
|
|
1491
|
+
const inProcessFetch = createInProcessFetch(generate, generateStream);
|
|
1379
1492
|
return new Client({
|
|
1380
1493
|
// Retries default to ZERO here, unlike a network client. There is no wire, so
|
|
1381
1494
|
// there is no transient failure to ride out: whatever `generate` throws will
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export * from "./a2a.js";
|
|
|
10
10
|
export * from "./acp.js";
|
|
11
11
|
export * from "./serve.js";
|
|
12
12
|
export * from "./mcpserve.js";
|
|
13
|
+
export * from "./mcphandler.js";
|
|
14
|
+
export * from "./mcpserverauth.js";
|
|
13
15
|
export * from "./adapters.js";
|
|
14
16
|
export * from "./translate.js";
|
|
15
17
|
export * from "./toolkit.js";
|
|
@@ -22,5 +24,7 @@ export * from "./batteries.js";
|
|
|
22
24
|
export { State, context, questionMap, ask, gate, decide, applyGate, applyPolicy, answersOf, staticClassifier, Tape, DEFAULT_BANDS, type NamedQuestion, type Band, type Bands, type Answers, type Rule, type GateOutcome, type Policy, } from "./judge.js";
|
|
23
25
|
export * from "./promptcache.js";
|
|
24
26
|
export * from "./tooltiers.js";
|
|
27
|
+
export * from "./toolconcurrency.js";
|
|
25
28
|
export * from "./models.js";
|
|
26
29
|
export * from "./builtin_extras.js";
|
|
30
|
+
export * from "./media.js";
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,8 @@ export * from "./a2a.js";
|
|
|
10
10
|
export * from "./acp.js";
|
|
11
11
|
export * from "./serve.js";
|
|
12
12
|
export * from "./mcpserve.js";
|
|
13
|
+
export * from "./mcphandler.js";
|
|
14
|
+
export * from "./mcpserverauth.js";
|
|
13
15
|
export * from "./adapters.js";
|
|
14
16
|
export * from "./translate.js";
|
|
15
17
|
export * from "./toolkit.js";
|
|
@@ -25,5 +27,7 @@ export * from "./batteries.js";
|
|
|
25
27
|
export { State, context, questionMap, ask, gate, decide, applyGate, applyPolicy, answersOf, staticClassifier, Tape, DEFAULT_BANDS, } from "./judge.js";
|
|
26
28
|
export * from "./promptcache.js";
|
|
27
29
|
export * from "./tooltiers.js";
|
|
30
|
+
export * from "./toolconcurrency.js";
|
|
28
31
|
export * from "./models.js";
|
|
29
32
|
export * from "./builtin_extras.js";
|
|
33
|
+
export * from "./media.js";
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ElicitRequest, ElicitResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
import { type JSONSchema, type Tool, type ToolHints, type McpStatus, type Request, type Answer } from "./types.js";
|
|
2
|
+
import { type JSONSchema, type Tool, type ToolHints, type ToolResult, type McpStatus, type Request, type Answer } from "./types.js";
|
|
3
3
|
import { type ContentPart } from "./content.js";
|
|
4
|
+
import type { SkillDef } from "./skill.js";
|
|
5
|
+
import { type AuthConfig } from "./mcp2026/source.js";
|
|
6
|
+
import { type TokenStore } from "./mcp2026/auth.js";
|
|
7
|
+
export { Token, MemoryTokenStore, type TokenStore } from "./mcp2026/auth.js";
|
|
8
|
+
export type { AuthConfig } from "./mcp2026/source.js";
|
|
4
9
|
/** Map an MCP `elicitation/create` request onto a §10 `Request`. */
|
|
5
10
|
export declare function elicitationToRequest(params: ElicitRequest["params"]): Request;
|
|
6
11
|
/** Map a resolved §10 `Answer` back onto an MCP `ElicitResult`. ok→accept; declined→decline; else→cancel. */
|
|
@@ -18,6 +23,10 @@ export interface LocalServer {
|
|
|
18
23
|
* builtins/skills filter (nil/empty ⇒ all; ≥1 true ⇒ allowlist; only-false ⇒ drop-list; unknown
|
|
19
24
|
* ⇒ ignore+warn). Applied to the listed defs before sanitize/prefix. */
|
|
20
25
|
tools?: Record<string, boolean>;
|
|
26
|
+
/** §2.8 lazy tool search: expose `<server>_search_tools` + `<server>_call_tool` instead of every tool. */
|
|
27
|
+
toolSearch?: boolean;
|
|
28
|
+
/** §2.8 what this server is for (shown by search_tools). */
|
|
29
|
+
description?: string;
|
|
21
30
|
}
|
|
22
31
|
export interface RemoteServer {
|
|
23
32
|
type?: "remote";
|
|
@@ -28,6 +37,11 @@ export interface RemoteServer {
|
|
|
28
37
|
timeout?: number;
|
|
29
38
|
/** §2 Gap 7. Per-server tool allowlist (see LocalServer.tools). */
|
|
30
39
|
tools?: Record<string, boolean>;
|
|
40
|
+
/** §2.8 lazy tool search (see LocalServer.toolSearch). */
|
|
41
|
+
toolSearch?: boolean;
|
|
42
|
+
description?: string;
|
|
43
|
+
/** §2A OAuth 2.1 client settings. Absent ⇒ a 401 still triggers the flow (CIMD if the AS supports it). */
|
|
44
|
+
auth?: AuthConfig;
|
|
31
45
|
}
|
|
32
46
|
export type ServerConfig = LocalServer | RemoteServer;
|
|
33
47
|
export type McpConfig = Record<string, ServerConfig>;
|
|
@@ -46,6 +60,8 @@ export declare function expandEnvHeaders(headers?: Record<string, string>): Reco
|
|
|
46
60
|
* takes it there.
|
|
47
61
|
*/
|
|
48
62
|
export declare function mcpContentToParts(content: unknown): ContentPart[] | undefined;
|
|
63
|
+
/** §2 `tools/call` result → ToolResult (shared by the 2026 path and the frozen fallback). */
|
|
64
|
+
export declare function mapCallToolResult(result: any, server: string): ToolResult;
|
|
49
65
|
export interface McpSource {
|
|
50
66
|
tools: Tool[];
|
|
51
67
|
status: Record<string, McpStatus>;
|
|
@@ -56,6 +72,8 @@ export interface McpSource {
|
|
|
56
72
|
* source is closed.
|
|
57
73
|
*/
|
|
58
74
|
resourceTools(): Tool[];
|
|
75
|
+
/** §2.7 skills served by 2026 servers (skills extension); the toolkit adds them to the `skill` tool. */
|
|
76
|
+
skills: SkillDef[];
|
|
59
77
|
}
|
|
60
78
|
/** One server's outcome, reported by `loadMcp`'s `onServer` the moment that server finishes. */
|
|
61
79
|
export interface McpServerResult {
|
|
@@ -75,12 +93,21 @@ export declare function mcpHints(a: {
|
|
|
75
93
|
destructiveHint?: boolean;
|
|
76
94
|
openWorldHint?: boolean;
|
|
77
95
|
} | undefined): ToolHints | undefined;
|
|
96
|
+
/** Options shared by loadMcp / listMcpTools for the 2026 path and OAuth (§2A.1a). */
|
|
97
|
+
export interface McpLoadOptions {
|
|
98
|
+
waitFor?: (request: Request) => Promise<Answer>;
|
|
99
|
+
signal?: AbortSignal;
|
|
100
|
+
/** Where OAuth tokens live (default: in memory, per load). */
|
|
101
|
+
tokenStore?: TokenStore;
|
|
102
|
+
/** How `auto` OAuth mode opens the authorization URL (default: the OS opener). */
|
|
103
|
+
openUrl?: (url: string) => unknown | Promise<unknown>;
|
|
104
|
+
/** Final exposed names to drop; on a tool-search server they are also hidden from search/call. */
|
|
105
|
+
disableTools?: string[];
|
|
106
|
+
}
|
|
78
107
|
/** Connect to every enabled server, list + convert tools. Failures are isolated.
|
|
79
108
|
* §2 Gap 3: an optional `signal` bounds each connect/list by the server timeout and aborts the
|
|
80
109
|
* whole load promptly on parent cancellation. */
|
|
81
|
-
export declare function loadMcp(input: string | object, opts?: {
|
|
82
|
-
waitFor?: (request: Request) => Promise<Answer>;
|
|
83
|
-
signal?: AbortSignal;
|
|
110
|
+
export declare function loadMcp(input: string | object, opts?: McpLoadOptions & {
|
|
84
111
|
/**
|
|
85
112
|
* Called once per server the moment it is connected, failed or found disabled (SPEC §4C),
|
|
86
113
|
* so a host can register a fast server's tools while a slow one is still connecting.
|
|
@@ -105,6 +132,4 @@ export interface McpInventory {
|
|
|
105
132
|
* §2 Gap 6. Connect to every enabled server, list tool definitions, and DISCONNECT before
|
|
106
133
|
* returning — no toolkit, no execute wiring, nothing left running. Failure isolation as in loadMcp.
|
|
107
134
|
*/
|
|
108
|
-
export declare function listMcpTools(input: string | object, opts?:
|
|
109
|
-
signal?: AbortSignal;
|
|
110
|
-
}): Promise<McpInventory>;
|
|
135
|
+
export declare function listMcpTools(input: string | object, opts?: McpLoadOptions): Promise<McpInventory>;
|