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/src/agents/loop.ts
CHANGED
|
@@ -141,6 +141,7 @@ export async function runGated(
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
let accumulated: RunResult["toolCalls"] = []
|
|
144
|
+
let accFiles: RunResult["files"] = []
|
|
144
145
|
let last: RunResult | undefined
|
|
145
146
|
let reason = ""
|
|
146
147
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
@@ -149,7 +150,9 @@ export async function runGated(
|
|
|
149
150
|
// The gate judges the ACCUMULATED work, so an agent cannot escape it by
|
|
150
151
|
// declining to re-declare its plan on a retry.
|
|
151
152
|
accumulated = [...accumulated, ...r.toolCalls]
|
|
152
|
-
|
|
153
|
+
// RunResult.files is derived from toolCalls, so it accumulates with them (§8).
|
|
154
|
+
accFiles = [...accFiles, ...(r.files ?? [])]
|
|
155
|
+
const judged: RunResult = { ...r, toolCalls: accumulated, files: accFiles }
|
|
153
156
|
last = judged
|
|
154
157
|
|
|
155
158
|
if (r.status && r.status !== "done") {
|
package/src/agents/runtime.ts
CHANGED
|
@@ -1012,7 +1012,7 @@ export class AgentRuntime {
|
|
|
1012
1012
|
const team = [...(parent.def.team ?? [])].sort()
|
|
1013
1013
|
const advertised = team.map((n) => (this.registry[n] ? `${n}: ${this.registry[n].does}` : n)).join("; ")
|
|
1014
1014
|
const rt = this
|
|
1015
|
-
|
|
1015
|
+
const tool = defineTool({
|
|
1016
1016
|
name: "task",
|
|
1017
1017
|
description: `Delegate a subtask to an isolated subagent (it runs on a fresh transcript and returns only its final answer). Available agents — ${advertised}`,
|
|
1018
1018
|
inputSchema: {
|
|
@@ -1068,5 +1068,8 @@ export class AgentRuntime {
|
|
|
1068
1068
|
}
|
|
1069
1069
|
},
|
|
1070
1070
|
})
|
|
1071
|
+
// Each subagent is isolated and the runtime's own gate schedules the children,
|
|
1072
|
+
// so a fan-out of task calls runs concurrently (SPEC §8 "Tool scheduling").
|
|
1073
|
+
return { ...tool, concurrencySafe: () => true }
|
|
1071
1074
|
}
|
|
1072
1075
|
}
|
package/src/client.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { toOpenAI, toAnthropic } from "./adapters.js"
|
|
|
10
10
|
import { applyPromptCache, type PromptCacheOptions } from "./promptcache.js"
|
|
11
11
|
import { buildToolPlan, toolSearch, unwrapCallTool, type ToolPlan, type ToolTier, type ToolTierOptions } from "./tooltiers.js"
|
|
12
12
|
import { isRetryableStatus, retryAfterMs } from "./retry.js"
|
|
13
|
+
import { isConcurrencySafe, resolveMaxToolConcurrency, scheduleCalls } from "./toolconcurrency.js"
|
|
13
14
|
import type { ContentPart, PromptInput, UnsupportedPartMode } from "./content.js"
|
|
14
15
|
import { toOpenAIWire, toAnthropicWire, checkPromptParts, type WireOptions } from "./wire.js"
|
|
15
16
|
import type { TranslateRequest, TranslateResult, TranslatedToolCall } from "./translate.js"
|
|
@@ -41,6 +42,10 @@ export interface ClientOptions {
|
|
|
41
42
|
headers?: Record<string, string>
|
|
42
43
|
systemPrompt?: string
|
|
43
44
|
maxTurns?: number
|
|
45
|
+
/** Cap on concurrency-safe tool calls of one response running at once (SPEC §8 "Tool
|
|
46
|
+
* scheduling"). Absent ⇒ env `TOOLNEXUS_MAX_TOOL_CONCURRENCY` when a positive integer, else 10.
|
|
47
|
+
* Unsafe calls (anything not read-only) always run alone. */
|
|
48
|
+
maxToolConcurrency?: number
|
|
44
49
|
hooks?: Hooks
|
|
45
50
|
/** Retries on transient LLM errors (`429`/`500`/`502`/`503`/`504`/`529` + network). Default 2.
|
|
46
51
|
* Widen the status set with `retryableStatuses`. */
|
|
@@ -318,6 +323,26 @@ export interface ToolCallRecord {
|
|
|
318
323
|
output: string
|
|
319
324
|
isError: boolean
|
|
320
325
|
metadata?: Record<string, unknown>
|
|
326
|
+
/** §8: the ToolResult's parts — present iff the result had parts. */
|
|
327
|
+
parts?: ContentPart[]
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** §8 RunResult.files entry: a non-text part a tool call produced this run. */
|
|
331
|
+
export type RunFile = { type: "image" | "file" | "audio"; mimeType: string; url?: string; data?: string; name?: string; from: string; callId: string }
|
|
332
|
+
|
|
333
|
+
/** Model tool-call id of each record, kept off the record itself (§8 shape has no id). */
|
|
334
|
+
const callIds = new WeakMap<ToolCallRecord, string>()
|
|
335
|
+
|
|
336
|
+
/** RunResult.files: every non-text part of every tool call, in call order (§8). */
|
|
337
|
+
function runFiles(toolCalls: ToolCallRecord[]): RunFile[] {
|
|
338
|
+
const out: RunFile[] = []
|
|
339
|
+
for (const r of toolCalls) {
|
|
340
|
+
for (const p of r.parts ?? []) {
|
|
341
|
+
if (p.type === "text") continue
|
|
342
|
+
out.push({ ...p, from: r.name, callId: callIds.get(r) ?? "" })
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return out
|
|
321
346
|
}
|
|
322
347
|
|
|
323
348
|
/** Token usage, summed across every LLM round trip in the run. */
|
|
@@ -340,6 +365,8 @@ export interface RunResult {
|
|
|
340
365
|
messages: any[]
|
|
341
366
|
/** Every tool call made, with its output, error flag, and metadata. */
|
|
342
367
|
toolCalls: ToolCallRecord[]
|
|
368
|
+
/** Every non-text part produced by tool calls this run, in call order (§8). Always present. */
|
|
369
|
+
files: RunFile[]
|
|
343
370
|
/** Total number of tool calls (= toolCalls.length). */
|
|
344
371
|
toolCallCount: number
|
|
345
372
|
/** Number of LLM round trips. */
|
|
@@ -361,8 +388,12 @@ export interface RunResult {
|
|
|
361
388
|
/** Events yielded by client.stream(). */
|
|
362
389
|
export type StreamEvent =
|
|
363
390
|
| { type: "text"; delta: string }
|
|
364
|
-
|
|
365
|
-
|
|
391
|
+
/** Model reasoning narration (e.g. an ACP agent's `agent_thought_chunk`). Never part of
|
|
392
|
+
* the answer text or the RunResult. */
|
|
393
|
+
| { type: "thought"; delta: string; via?: "acp" }
|
|
394
|
+
/** `via: "acp"` marks a tool the ACP agent ran itself: reported, never executed by the loop. */
|
|
395
|
+
| { type: "tool_call"; id: string; name: string; args: Record<string, unknown>; via?: "call_tool" | "acp" }
|
|
396
|
+
| { type: "tool_result"; id: string; name: string; output: string; isError: boolean; metadata?: Record<string, unknown>; via?: "call_tool" | "acp" }
|
|
366
397
|
| { type: "usage"; usage: Usage }
|
|
367
398
|
| { type: "pending"; request: Request }
|
|
368
399
|
| { type: "done"; result: RunResult }
|
|
@@ -990,6 +1021,27 @@ export class Client {
|
|
|
990
1021
|
return { args: a, result }
|
|
991
1022
|
}
|
|
992
1023
|
|
|
1024
|
+
/** Runs one response's calls under the tool schedule (SPEC §8 "Tool scheduling"). */
|
|
1025
|
+
private schedule<T, R>(
|
|
1026
|
+
toolkit: Toolkit | undefined,
|
|
1027
|
+
items: T[],
|
|
1028
|
+
key: (item: T) => [string, Record<string, unknown>],
|
|
1029
|
+
fn: (item: T) => Promise<R>,
|
|
1030
|
+
): Promise<R[]> {
|
|
1031
|
+
return scheduleCalls(items, (item) => {
|
|
1032
|
+
const [name, args] = key(item)
|
|
1033
|
+
return this.callSafe(toolkit, name, args)
|
|
1034
|
+
}, resolveMaxToolConcurrency(this.opts.maxToolConcurrency), fn)
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** One call's safety; looks through `call_tool` to the tool it names. Unknown ⇒ unsafe. */
|
|
1038
|
+
private callSafe(toolkit: Toolkit | undefined, name: string, args: Record<string, unknown>): boolean {
|
|
1039
|
+
if (this.opts.toolTiers && name === "tool_search") return true
|
|
1040
|
+
const inner = this.unwrapCall(name, args ?? {})
|
|
1041
|
+
const tool = toolkit?.get(inner ? inner.name : name)
|
|
1042
|
+
return tool ? isConcurrencySafe(tool, inner ? inner.args : (args ?? {})) : false
|
|
1043
|
+
}
|
|
1044
|
+
|
|
993
1045
|
/** ToolTiers `call_tool {name, arguments}` -> the real call; undefined when tiers are off. */
|
|
994
1046
|
private unwrapCall(name: string, args: Record<string, unknown>): { name: string; args: Record<string, unknown> } | undefined {
|
|
995
1047
|
return this.opts.toolTiers ? unwrapCallTool(name, args) : undefined
|
|
@@ -997,9 +1049,12 @@ export class Client {
|
|
|
997
1049
|
|
|
998
1050
|
/** The RunResult.toolCalls entry: a `call_tool` call is recorded as the real tool with its
|
|
999
1051
|
* inner arguments, the name hooks and stream events see (the transcript keeps the wrapper). */
|
|
1000
|
-
private toolCallRecord(name: string, args: Record<string, unknown>, result: ToolResult): ToolCallRecord {
|
|
1052
|
+
private toolCallRecord(name: string, args: Record<string, unknown>, result: ToolResult, callId?: string): ToolCallRecord {
|
|
1001
1053
|
const inner = this.unwrapCall(name, args)
|
|
1002
|
-
|
|
1054
|
+
const rec: ToolCallRecord = { name: inner ? inner.name : name, args: inner ? inner.args : args, output: result.output, isError: result.isError, metadata: result.metadata }
|
|
1055
|
+
if (result.parts?.length) rec.parts = result.parts
|
|
1056
|
+
callIds.set(rec, callId ?? "")
|
|
1057
|
+
return rec
|
|
1003
1058
|
}
|
|
1004
1059
|
|
|
1005
1060
|
/** The stream's tool_call event, naming the real tool for a `call_tool` call. */
|
|
@@ -1157,8 +1212,8 @@ export class Client {
|
|
|
1157
1212
|
if (calls.length === 0) return this.endRun(runStart, lastModel, msg.content ?? "", messages, toolCalls, turns, usage)
|
|
1158
1213
|
// execute all tool calls in this turn concurrently (true parallel tool calling)
|
|
1159
1214
|
done.clear()
|
|
1160
|
-
const settled = await untilAborted(signal,
|
|
1161
|
-
calls.
|
|
1215
|
+
const settled = await untilAborted(signal, this.schedule(toolkit,
|
|
1216
|
+
calls, (call: any) => [call.function.name, safeJson(call.function.arguments)], async (call: any) => {
|
|
1162
1217
|
let { args, result } = await this.runTool(toolkit, call.function.name, safeJson(call.function.arguments), call.id, turn, signal)
|
|
1163
1218
|
let halted: Request | undefined
|
|
1164
1219
|
const req = pendingOf(result)
|
|
@@ -1169,13 +1224,13 @@ export class Client {
|
|
|
1169
1224
|
}
|
|
1170
1225
|
if (!halted && call.id) done.set(call.id, result)
|
|
1171
1226
|
return { call, args, result, halted }
|
|
1172
|
-
}
|
|
1227
|
+
},
|
|
1173
1228
|
))
|
|
1174
1229
|
// Record in tool-call order; on the FIRST durable halt, surface it and stop — deterministic,
|
|
1175
1230
|
// and the later suspensions' placeholder results never enter the transcript (they re-suspend
|
|
1176
1231
|
// on resume). Mirrors the streaming path.
|
|
1177
1232
|
for (const s of settled) {
|
|
1178
|
-
toolCalls.push(this.toolCallRecord(s.call.function.name, s.args, s.result))
|
|
1233
|
+
toolCalls.push(this.toolCallRecord(s.call.function.name, s.args, s.result, s.call.id))
|
|
1179
1234
|
messages.push(toolMessage(s.call.id, s.call.function.name, s.result))
|
|
1180
1235
|
if (s.halted) return this.pendingRun(runStart, lastModel, s.halted, messages, toolCalls, turns, usage)
|
|
1181
1236
|
}
|
|
@@ -1190,7 +1245,7 @@ export class Client {
|
|
|
1190
1245
|
|
|
1191
1246
|
private result(model: string, text: string, messages: any[], toolCalls: ToolCallRecord[], turns: number, usage: Usage, exhausted = false): RunResult {
|
|
1192
1247
|
const incomplete = exhausted && text === ""
|
|
1193
|
-
const r: RunResult = { text, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model, status: incomplete ? "incomplete" : "done" }
|
|
1248
|
+
const r: RunResult = { text, messages, toolCalls, files: runFiles(toolCalls), toolCallCount: toolCalls.length, turns, usage, model, status: incomplete ? "incomplete" : "done" }
|
|
1194
1249
|
if (incomplete) r.limit = "maxTurns"
|
|
1195
1250
|
return r
|
|
1196
1251
|
}
|
|
@@ -1198,7 +1253,7 @@ export class Client {
|
|
|
1198
1253
|
/** §10: a run halted because a tool suspended and no `waitFor` was configured. */
|
|
1199
1254
|
private pendingRun(runStart: number, model: string, request: Request, messages: any[], toolCalls: ToolCallRecord[], turns: number, usage: Usage): RunResult {
|
|
1200
1255
|
this.emit({ event: "run", model, turns, toolCalls: toolCalls.length, totalTokens: usage.totalTokens, ms: Date.now() - runStart })
|
|
1201
|
-
return { text: request.prompt, messages, toolCalls, toolCallCount: toolCalls.length, turns, usage, model, status: "pending", pending: request }
|
|
1256
|
+
return { text: request.prompt, messages, toolCalls, files: runFiles(toolCalls), toolCallCount: toolCalls.length, turns, usage, model, status: "pending", pending: request }
|
|
1202
1257
|
}
|
|
1203
1258
|
|
|
1204
1259
|
/**
|
|
@@ -1275,8 +1330,8 @@ export class Client {
|
|
|
1275
1330
|
}
|
|
1276
1331
|
// execute all tool_use blocks in this turn concurrently (true parallel tool calling)
|
|
1277
1332
|
done.clear()
|
|
1278
|
-
const settled = await untilAborted(signal,
|
|
1279
|
-
uses.
|
|
1333
|
+
const settled = await untilAborted(signal, this.schedule(toolkit,
|
|
1334
|
+
uses, (use: any) => [use.name, use.input ?? {}], async (use: any) => {
|
|
1280
1335
|
let { args, result } = await this.runTool(toolkit, use.name, use.input ?? {}, use.id, turn, signal)
|
|
1281
1336
|
let halted: Request | undefined
|
|
1282
1337
|
const req = pendingOf(result)
|
|
@@ -1287,7 +1342,7 @@ export class Client {
|
|
|
1287
1342
|
}
|
|
1288
1343
|
if (!halted && use.id) done.set(use.id, result)
|
|
1289
1344
|
return { use, args, result, halted }
|
|
1290
|
-
}
|
|
1345
|
+
},
|
|
1291
1346
|
))
|
|
1292
1347
|
// Record in tool-call order; on the FIRST durable halt, include that tool_result block, stop
|
|
1293
1348
|
// building the content, and surface it — deterministic (G3), later suspensions re-suspend on
|
|
@@ -1295,7 +1350,7 @@ export class Client {
|
|
|
1295
1350
|
const content: any[] = []
|
|
1296
1351
|
let halted: Request | undefined
|
|
1297
1352
|
for (const s of settled) {
|
|
1298
|
-
toolCalls.push(this.toolCallRecord(s.use.name, s.args, s.result))
|
|
1353
|
+
toolCalls.push(this.toolCallRecord(s.use.name, s.args, s.result, s.use.id))
|
|
1299
1354
|
content.push(toolResultBlock(s.use.id, s.result))
|
|
1300
1355
|
if (s.halted) { halted = s.halted; break }
|
|
1301
1356
|
}
|
|
@@ -1356,6 +1411,13 @@ export class Client {
|
|
|
1356
1411
|
const payload = line.slice(5).trim()
|
|
1357
1412
|
if (payload === "[DONE]") break
|
|
1358
1413
|
const j = safeParse(payload)
|
|
1414
|
+
// A non-text event from an in-process streaming generate rides in-band so it keeps
|
|
1415
|
+
// its order and arrives as it happens; it goes straight to the consumer and is never
|
|
1416
|
+
// executed. Honoured only on the in-process sentinel URL, never from a real server.
|
|
1417
|
+
if (j && j[IN_PROCESS_SIDE_KEY] && this.opts.baseUrl === IN_PROCESS_BASE_URL) {
|
|
1418
|
+
yield j[IN_PROCESS_SIDE_KEY] as StreamEvent
|
|
1419
|
+
continue
|
|
1420
|
+
}
|
|
1359
1421
|
if (j?.usage) addUsage(usage, j.usage, "openai")
|
|
1360
1422
|
const choice = j?.choices?.[0]
|
|
1361
1423
|
const d = choice?.delta
|
|
@@ -1385,11 +1447,11 @@ export class Client {
|
|
|
1385
1447
|
messages.push({ role: "assistant", content: content || null, tool_calls: calls.map((c) => ({ id: c.id, type: "function", function: { name: c.name, arguments: c.args } })) })
|
|
1386
1448
|
for (const c of calls) yield this.callEvent(c.id, c.name, safeJson(c.args))
|
|
1387
1449
|
done.clear()
|
|
1388
|
-
const settled = await untilAborted(signal,
|
|
1450
|
+
const settled = await untilAborted(signal, this.schedule(toolkit, calls, (c) => [c.name, safeJson(c.args)], async (c) => {
|
|
1389
1451
|
const r = await this.runTool(toolkit, c.name, safeJson(c.args), c.id, turn, signal)
|
|
1390
|
-
|
|
1452
|
+
if (!pendingOf(r.result) && c.id) done.set(c.id, r.result)
|
|
1391
1453
|
return r
|
|
1392
|
-
}))
|
|
1454
|
+
}))
|
|
1393
1455
|
for (let i = 0; i < calls.length; i++) {
|
|
1394
1456
|
const c = calls[i]
|
|
1395
1457
|
let { args, result } = settled[i]
|
|
@@ -1399,13 +1461,13 @@ export class Client {
|
|
|
1399
1461
|
const r = await this.resolvePending(toolkit, c.name, args, req, c.id, turn, signal)
|
|
1400
1462
|
result = r.result
|
|
1401
1463
|
if (r.halted) {
|
|
1402
|
-
toolCalls.push(this.toolCallRecord(c.name, args, result))
|
|
1464
|
+
toolCalls.push(this.toolCallRecord(c.name, args, result, c.id))
|
|
1403
1465
|
messages.push(toolMessage(c.id, c.name, result))
|
|
1404
1466
|
yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) }
|
|
1405
1467
|
return
|
|
1406
1468
|
}
|
|
1407
1469
|
}
|
|
1408
|
-
toolCalls.push(this.toolCallRecord(c.name, args, result))
|
|
1470
|
+
toolCalls.push(this.toolCallRecord(c.name, args, result, c.id))
|
|
1409
1471
|
messages.push(toolMessage(c.id, c.name, result))
|
|
1410
1472
|
yield this.resultEvent(c.id, c.name, safeJson(c.args), result)
|
|
1411
1473
|
}
|
|
@@ -1487,11 +1549,11 @@ export class Client {
|
|
|
1487
1549
|
}
|
|
1488
1550
|
for (const u of uses as any[]) yield this.callEvent(u.id, u.name, u.input)
|
|
1489
1551
|
done.clear()
|
|
1490
|
-
const settled = await untilAborted(signal,
|
|
1552
|
+
const settled = await untilAborted(signal, this.schedule(toolkit, uses as any[], (u: any) => [u.name, u.input ?? {}], async (u: any) => {
|
|
1491
1553
|
const r = await this.runTool(toolkit, u.name, u.input ?? {}, u.id, turn, signal)
|
|
1492
1554
|
if (!pendingOf(r.result) && u.id) done.set(u.id, r.result)
|
|
1493
1555
|
return r
|
|
1494
|
-
}))
|
|
1556
|
+
}))
|
|
1495
1557
|
const results: any[] = []
|
|
1496
1558
|
for (let i = 0; i < uses.length; i++) {
|
|
1497
1559
|
const u = uses[i] as any
|
|
@@ -1502,14 +1564,14 @@ export class Client {
|
|
|
1502
1564
|
const r = await this.resolvePending(toolkit, u.name, args, req, u.id, turn, signal)
|
|
1503
1565
|
result = r.result
|
|
1504
1566
|
if (r.halted) {
|
|
1505
|
-
toolCalls.push(this.toolCallRecord(u.name, args, result))
|
|
1567
|
+
toolCalls.push(this.toolCallRecord(u.name, args, result, u.id))
|
|
1506
1568
|
results.push(toolResultBlock(u.id, result))
|
|
1507
1569
|
messages.push({ role: "user", content: results })
|
|
1508
1570
|
yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) }
|
|
1509
1571
|
return
|
|
1510
1572
|
}
|
|
1511
1573
|
}
|
|
1512
|
-
toolCalls.push(this.toolCallRecord(u.name, args, result))
|
|
1574
|
+
toolCalls.push(this.toolCallRecord(u.name, args, result, u.id))
|
|
1513
1575
|
results.push(toolResultBlock(u.id, result))
|
|
1514
1576
|
yield this.resultEvent(u.id, u.name, u.input, result)
|
|
1515
1577
|
}
|
|
@@ -1560,11 +1622,27 @@ export interface InProcessResponse {
|
|
|
1560
1622
|
usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number }
|
|
1561
1623
|
}
|
|
1562
1624
|
|
|
1625
|
+
/** A streaming in-process model: `emit` each event as it happens — `text` deltas become
|
|
1626
|
+
* the streamed answer as they arrive; any other event (thought, an agent's own tool_call /
|
|
1627
|
+
* tool_result) goes straight to the stream consumer and is never executed by the loop.
|
|
1628
|
+
* Return the whole message when done (its `toolCalls`, if any, are what the loop runs). */
|
|
1629
|
+
export type InProcessGenerateStream = (
|
|
1630
|
+
request: InProcessRequest,
|
|
1631
|
+
emit: (event: StreamEvent) => void,
|
|
1632
|
+
) => InProcessResponse | Promise<InProcessResponse>
|
|
1633
|
+
|
|
1634
|
+
type InProcessGenerate = (request: InProcessRequest) => InProcessResponse | Promise<InProcessResponse>
|
|
1635
|
+
|
|
1563
1636
|
/** Options for {@link createInProcessClient}: every client option EXCEPT the three
|
|
1564
|
-
* that describe a wire — `baseUrl`, `apiKey` and `style` — plus `generate
|
|
1637
|
+
* that describe a wire — `baseUrl`, `apiKey` and `style` — plus `generate` and/or
|
|
1638
|
+
* `generateStream` (at least one). */
|
|
1565
1639
|
export type InProcessOptions = Omit<ClientOptions, "baseUrl" | "apiKey" | "style" | "fetch"> & {
|
|
1566
1640
|
/** Your model. Receives the assembled request, returns one assistant message. */
|
|
1567
|
-
generate
|
|
1641
|
+
generate?: InProcessGenerate
|
|
1642
|
+
/** Your model, streaming for real. With it, `stream()` is incremental; without it a
|
|
1643
|
+
* `stream()` on a plain `generate` is refused. When both are set, `generate` serves
|
|
1644
|
+
* non-streaming calls; alone, it serves them too (with a no-op `emit`). */
|
|
1645
|
+
generateStream?: InProcessGenerateStream
|
|
1568
1646
|
}
|
|
1569
1647
|
|
|
1570
1648
|
/** The sentinel base URL. Never dialled — the transport below answers every request
|
|
@@ -1573,6 +1651,10 @@ export type InProcessOptions = Omit<ClientOptions, "baseUrl" | "apiKey" | "style
|
|
|
1573
1651
|
* name can never resolve. */
|
|
1574
1652
|
const IN_PROCESS_BASE_URL = "http://in-process.invalid/v1"
|
|
1575
1653
|
|
|
1654
|
+
/** The in-band SSE key that carries a non-text stream event from an in-process
|
|
1655
|
+
* streaming generate to the consumer (honoured only on {@link IN_PROCESS_BASE_URL}). */
|
|
1656
|
+
const IN_PROCESS_SIDE_KEY = "toolnexus_event"
|
|
1657
|
+
|
|
1576
1658
|
/**
|
|
1577
1659
|
* Build a `fetch`-shaped adapter over a semantic `generate` function (ADR 0030):
|
|
1578
1660
|
* the reusable piece of {@link createInProcessClient} — assembles the wire request,
|
|
@@ -1581,15 +1663,20 @@ const IN_PROCESS_BASE_URL = "http://in-process.invalid/v1"
|
|
|
1581
1663
|
* or the agent runtime's `inProcess` option) goes through this ONE function, so there
|
|
1582
1664
|
* is no second copy of the request/response assembly to drift.
|
|
1583
1665
|
*
|
|
1584
|
-
* Streaming is refused rather than faked — see the thrown
|
|
1666
|
+
* Streaming with only a plain `generate` is refused rather than faked — see the thrown
|
|
1667
|
+
* error. With `generateStream` it streams for real: each text event is written to the
|
|
1668
|
+
* SSE body the moment it is emitted.
|
|
1585
1669
|
*/
|
|
1586
|
-
export function createInProcessFetch(generate
|
|
1587
|
-
if (typeof generate !== "function") {
|
|
1588
|
-
throw new Error("toolnexus: createInProcessFetch requires a `generate` function")
|
|
1670
|
+
export function createInProcessFetch(generate?: InProcessGenerate, generateStream?: InProcessGenerateStream): typeof fetch {
|
|
1671
|
+
if (typeof generate !== "function" && typeof generateStream !== "function") {
|
|
1672
|
+
throw new Error("toolnexus: createInProcessFetch requires a `generate` (or `generateStream`) function")
|
|
1589
1673
|
}
|
|
1590
1674
|
|
|
1591
1675
|
const inProcessFetch: typeof fetch = async (_url, init) => {
|
|
1592
1676
|
const body = JSON.parse(String((init as any)?.body ?? "{}"))
|
|
1677
|
+
const request: InProcessRequest = { messages: body.messages ?? [], tools: body.tools, model: body.model, body }
|
|
1678
|
+
|
|
1679
|
+
if (body.stream && generateStream) return inProcessStream(generateStream, request)
|
|
1593
1680
|
|
|
1594
1681
|
// A generate() returns a whole answer, so a stream would be one chunk pretending
|
|
1595
1682
|
// to be many. Content and delta-count assertions cannot tell that from a real
|
|
@@ -1601,25 +1688,11 @@ export function createInProcessFetch(generate: (request: InProcessRequest) => In
|
|
|
1601
1688
|
)
|
|
1602
1689
|
}
|
|
1603
1690
|
|
|
1604
|
-
const answer = await generate({
|
|
1605
|
-
messages: body.messages ?? [],
|
|
1606
|
-
tools: body.tools,
|
|
1607
|
-
model: body.model,
|
|
1608
|
-
body,
|
|
1609
|
-
})
|
|
1691
|
+
const answer = generate ? await generate(request) : await generateStream!(request, () => {})
|
|
1610
1692
|
|
|
1611
1693
|
const message: Record<string, any> = { role: "assistant" }
|
|
1612
1694
|
if (answer.toolCalls && answer.toolCalls.length > 0) {
|
|
1613
|
-
message.tool_calls = answer.toolCalls
|
|
1614
|
-
id: c.id ?? `call_${i}`,
|
|
1615
|
-
type: "function",
|
|
1616
|
-
function: {
|
|
1617
|
-
name: c.name,
|
|
1618
|
-
// A string is already encoded; anything else is encoded here so a model
|
|
1619
|
-
// author never has to think about the wire.
|
|
1620
|
-
arguments: typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {}),
|
|
1621
|
-
},
|
|
1622
|
-
}))
|
|
1695
|
+
message.tool_calls = wireToolCalls(answer.toolCalls, false)
|
|
1623
1696
|
} else {
|
|
1624
1697
|
message.content = answer.content ?? ""
|
|
1625
1698
|
}
|
|
@@ -1643,6 +1716,77 @@ export function createInProcessFetch(generate: (request: InProcessRequest) => In
|
|
|
1643
1716
|
return inProcessFetch
|
|
1644
1717
|
}
|
|
1645
1718
|
|
|
1719
|
+
function wireToolCalls(calls: InProcessToolCall[], indexed: boolean): any[] {
|
|
1720
|
+
return calls.map((c, i) => ({
|
|
1721
|
+
...(indexed ? { index: i } : {}),
|
|
1722
|
+
id: c.id ?? `call_${i}`,
|
|
1723
|
+
type: "function",
|
|
1724
|
+
function: {
|
|
1725
|
+
name: c.name,
|
|
1726
|
+
// A string is already encoded; anything else is encoded here so a model
|
|
1727
|
+
// author never has to think about the wire.
|
|
1728
|
+
arguments: typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {}),
|
|
1729
|
+
},
|
|
1730
|
+
}))
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/** Runs a streaming generate and answers with an OpenAI SSE body written AS EVENTS ARRIVE:
|
|
1734
|
+
* text → a content delta; any other event → an in-band side chunk for the consumer; then
|
|
1735
|
+
* (tool calls as one delta | the content as one delta when no text was emitted), the
|
|
1736
|
+
* finish_reason chunk, usage, and [DONE]. */
|
|
1737
|
+
function inProcessStream(generateStream: InProcessGenerateStream, request: InProcessRequest): Response {
|
|
1738
|
+
const enc = new TextEncoder()
|
|
1739
|
+
let open = true
|
|
1740
|
+
const sse = new ReadableStream<Uint8Array>({
|
|
1741
|
+
start(controller) {
|
|
1742
|
+
const chunk = (v: unknown) => {
|
|
1743
|
+
if (open) controller.enqueue(enc.encode(`data: ${JSON.stringify(v)}\n\n`))
|
|
1744
|
+
}
|
|
1745
|
+
let sawText = false
|
|
1746
|
+
void (async () => {
|
|
1747
|
+
try {
|
|
1748
|
+
const answer = await generateStream(request, (ev) => {
|
|
1749
|
+
if (!ev || typeof ev !== "object") return
|
|
1750
|
+
if (ev.type === "text") {
|
|
1751
|
+
if (ev.delta) {
|
|
1752
|
+
sawText = true
|
|
1753
|
+
chunk({ choices: [{ index: 0, delta: { content: ev.delta } }] })
|
|
1754
|
+
}
|
|
1755
|
+
return
|
|
1756
|
+
}
|
|
1757
|
+
chunk({ [IN_PROCESS_SIDE_KEY]: ev })
|
|
1758
|
+
})
|
|
1759
|
+
let finish = "stop"
|
|
1760
|
+
if (answer.toolCalls && answer.toolCalls.length > 0) {
|
|
1761
|
+
chunk({ choices: [{ index: 0, delta: { tool_calls: wireToolCalls(answer.toolCalls, true) } }] })
|
|
1762
|
+
finish = "tool_calls"
|
|
1763
|
+
} else if (!sawText && answer.content) {
|
|
1764
|
+
chunk({ choices: [{ index: 0, delta: { content: answer.content } }] })
|
|
1765
|
+
}
|
|
1766
|
+
chunk({ choices: [{ index: 0, delta: {}, finish_reason: finish }] })
|
|
1767
|
+
const u = answer.usage
|
|
1768
|
+
if (u) {
|
|
1769
|
+
const p = u.promptTokens ?? 0, c = u.completionTokens ?? 0
|
|
1770
|
+
chunk({ choices: [], usage: { prompt_tokens: p, completion_tokens: c, total_tokens: u.totalTokens || p + c } })
|
|
1771
|
+
}
|
|
1772
|
+
if (open) controller.enqueue(enc.encode("data: [DONE]\n\n"))
|
|
1773
|
+
open = false
|
|
1774
|
+
controller.close()
|
|
1775
|
+
} catch (e) {
|
|
1776
|
+
if (open) {
|
|
1777
|
+
open = false
|
|
1778
|
+
controller.error(e)
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
})()
|
|
1782
|
+
},
|
|
1783
|
+
cancel() {
|
|
1784
|
+
open = false
|
|
1785
|
+
},
|
|
1786
|
+
})
|
|
1787
|
+
return new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } })
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1646
1790
|
/**
|
|
1647
1791
|
* A client backed by a model running IN THIS PROCESS — no server, no socket, and no
|
|
1648
1792
|
* HTTP types to construct.
|
|
@@ -1653,11 +1797,15 @@ export function createInProcessFetch(generate: (request: InProcessRequest) => In
|
|
|
1653
1797
|
* The `fetch`-shaped adapter itself is {@link createInProcessFetch} — this function
|
|
1654
1798
|
* is just that adapter wired into a `Client`.
|
|
1655
1799
|
*
|
|
1656
|
-
* Streaming is refused rather than faked
|
|
1800
|
+
* Streaming is refused rather than faked for a plain `generate`; with `generateStream`
|
|
1801
|
+
* it streams for real — see {@link createInProcessFetch}.
|
|
1657
1802
|
*/
|
|
1658
1803
|
export function createInProcessClient(opts: InProcessOptions): Client {
|
|
1659
|
-
const { generate, ...rest } = opts
|
|
1660
|
-
|
|
1804
|
+
const { generate, generateStream, ...rest } = opts
|
|
1805
|
+
if (typeof generate !== "function" && typeof generateStream !== "function") {
|
|
1806
|
+
throw new Error("toolnexus: createInProcessClient requires a `generate` (or `generateStream`) function")
|
|
1807
|
+
}
|
|
1808
|
+
const inProcessFetch = createInProcessFetch(generate, generateStream)
|
|
1661
1809
|
|
|
1662
1810
|
return new Client({
|
|
1663
1811
|
// Retries default to ZERO here, unlike a network client. There is no wire, so
|
package/src/index.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"
|
|
@@ -45,5 +47,7 @@ export {
|
|
|
45
47
|
} from "./judge.js"
|
|
46
48
|
export * from "./promptcache.js"
|
|
47
49
|
export * from "./tooltiers.js"
|
|
50
|
+
export * from "./toolconcurrency.js"
|
|
48
51
|
export * from "./models.js"
|
|
49
52
|
export * from "./builtin_extras.js"
|
|
53
|
+
export * from "./media.js"
|