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.
Files changed (65) hide show
  1. package/README.md +139 -1
  2. package/dist/acp.d.ts +37 -6
  3. package/dist/acp.js +239 -52
  4. package/dist/agents/loop.js +4 -1
  5. package/dist/agents/runtime.js +4 -1
  6. package/dist/client.d.ts +55 -9
  7. package/dist/client.js +154 -41
  8. package/dist/index.d.ts +4 -0
  9. package/dist/index.js +4 -0
  10. package/dist/mcp.d.ts +32 -7
  11. package/dist/mcp.js +125 -36
  12. package/dist/mcp2026/auth.d.ts +137 -0
  13. package/dist/mcp2026/auth.js +446 -0
  14. package/dist/mcp2026/client.d.ts +48 -0
  15. package/dist/mcp2026/client.js +160 -0
  16. package/dist/mcp2026/protocol.d.ts +54 -0
  17. package/dist/mcp2026/protocol.js +113 -0
  18. package/dist/mcp2026/skills.d.ts +5 -0
  19. package/dist/mcp2026/skills.js +25 -0
  20. package/dist/mcp2026/source.d.ts +86 -0
  21. package/dist/mcp2026/source.js +372 -0
  22. package/dist/mcp2026/stdio.d.ts +24 -0
  23. package/dist/mcp2026/stdio.js +99 -0
  24. package/dist/mcp2026/toolsearch.d.ts +5 -0
  25. package/dist/mcp2026/toolsearch.js +58 -0
  26. package/dist/mcphandler.d.ts +169 -0
  27. package/dist/mcphandler.js +1145 -0
  28. package/dist/mcpheader.d.ts +5 -0
  29. package/dist/mcpheader.js +18 -0
  30. package/dist/mcpserve.d.ts +24 -15
  31. package/dist/mcpserve.js +105 -2
  32. package/dist/mcpserverauth.d.ts +48 -0
  33. package/dist/mcpserverauth.js +203 -0
  34. package/dist/media.d.ts +126 -0
  35. package/dist/media.js +540 -0
  36. package/dist/skill.d.ts +21 -0
  37. package/dist/skill.js +44 -0
  38. package/dist/toolconcurrency.d.ts +16 -0
  39. package/dist/toolconcurrency.js +59 -0
  40. package/dist/toolkit.d.ts +23 -1
  41. package/dist/toolkit.js +26 -4
  42. package/dist/types.d.ts +26 -1
  43. package/package.json +5 -3
  44. package/src/acp.ts +258 -55
  45. package/src/agents/loop.ts +4 -1
  46. package/src/agents/runtime.ts +4 -1
  47. package/src/client.ts +196 -48
  48. package/src/index.ts +4 -0
  49. package/src/mcp.ts +162 -43
  50. package/src/mcp2026/auth.ts +473 -0
  51. package/src/mcp2026/client.ts +161 -0
  52. package/src/mcp2026/protocol.ts +135 -0
  53. package/src/mcp2026/skills.ts +22 -0
  54. package/src/mcp2026/source.ts +407 -0
  55. package/src/mcp2026/stdio.ts +102 -0
  56. package/src/mcp2026/toolsearch.ts +56 -0
  57. package/src/mcphandler.ts +1252 -0
  58. package/src/mcpheader.ts +19 -0
  59. package/src/mcpserve.ts +115 -2
  60. package/src/mcpserverauth.ts +216 -0
  61. package/src/media.ts +604 -0
  62. package/src/skill.ts +50 -0
  63. package/src/toolconcurrency.ts +67 -0
  64. package/src/toolkit.ts +35 -5
  65. package/src/types.ts +27 -1
@@ -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
- const judged: RunResult = { ...r, toolCalls: accumulated }
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") {
@@ -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
- return defineTool({
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
- | { type: "tool_call"; id: string; name: string; args: Record<string, unknown>; via?: "call_tool" }
365
- | { type: "tool_result"; id: string; name: string; output: string; isError: boolean; metadata?: Record<string, unknown>; via?: "call_tool" }
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
- return { name: inner ? inner.name : name, args: inner ? inner.args : args, output: result.output, isError: result.isError, metadata: result.metadata }
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, Promise.all(
1161
- calls.map(async (call: any) => {
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, Promise.all(
1279
- uses.map(async (use: any) => {
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, Promise.all(calls.map(async (c) => {
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
- if (!pendingOf(r.result) && c.id) done.set(c.id, r.result)
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, Promise.all((uses as any[]).map(async (u) => {
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: (request: InProcessRequest) => InProcessResponse | Promise<InProcessResponse>
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 error.
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: (request: InProcessRequest) => InProcessResponse | Promise<InProcessResponse>): typeof fetch {
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.map((c, i) => ({
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 — see {@link createInProcessFetch}.
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
- const inProcessFetch = createInProcessFetch(generate)
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"