@theokit/sdk 2.15.0 → 2.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/a2a/index.js CHANGED
@@ -10329,11 +10329,16 @@ var init_sanitize_tool_input = __esm({
10329
10329
  });
10330
10330
 
10331
10331
  // src/internal/llm/hermes-tool-extract.ts
10332
- function extractHermesToolCalls(content, makeId) {
10332
+ function extractHermesToolCalls(content, makeId, allowedToolNames) {
10333
+ const isPromoted = (name) => name.length > 0 && (allowedToolNames === void 0 || allowedToolNames.has(name));
10333
10334
  const toolCalls = [];
10335
+ const droppedNames = [];
10334
10336
  for (const block of content.matchAll(HERMES_BLOCK)) {
10335
10337
  const name = (block[1] ?? "").trim();
10336
- if (name.length === 0) continue;
10338
+ if (!isPromoted(name)) {
10339
+ if (name.length > 0 && allowedToolNames !== void 0) droppedNames.push(name);
10340
+ continue;
10341
+ }
10337
10342
  toolCalls.push({
10338
10343
  type: "tool_use",
10339
10344
  id: makeId(),
@@ -10341,8 +10346,11 @@ function extractHermesToolCalls(content, makeId) {
10341
10346
  input: parseHermesParams(block[2] ?? "")
10342
10347
  });
10343
10348
  }
10344
- const residualText = toolCalls.length === 0 ? content : content.replace(HERMES_BLOCK, "").trim();
10345
- return { toolCalls, residualText };
10349
+ const residualText = toolCalls.length === 0 ? content : content.replace(
10350
+ HERMES_BLOCK,
10351
+ (full, rawName) => isPromoted((rawName ?? "").trim()) ? "" : full
10352
+ ).trim();
10353
+ return { toolCalls, residualText, droppedNames };
10346
10354
  }
10347
10355
  function parseHermesParams(inner) {
10348
10356
  const input = {};
@@ -10354,12 +10362,83 @@ function parseHermesParams(inner) {
10354
10362
  }
10355
10363
  return sanitizeToolInput(input, { trim: true }).value;
10356
10364
  }
10357
- var HERMES_BLOCK, HERMES_PARAM;
10365
+ function streamToolCallBufferState(held, allowedToolNames, cap = DEFAULT_STREAM_BUFFER_CAP) {
10366
+ if (allowedToolNames.size === 0) return "impossible";
10367
+ const t = held.trimStart();
10368
+ if (t.length < STREAM_MARKER.length) {
10369
+ return STREAM_MARKER.startsWith(t) ? "possible" : "impossible";
10370
+ }
10371
+ if (!t.startsWith(STREAM_MARKER)) return "impossible";
10372
+ const parsed = parseStreamMarkerName(t);
10373
+ if (parsed === "building") return "possible";
10374
+ if (parsed === "invalid") return "impossible";
10375
+ const nameOk = parsed.complete ? allowedToolNames.has(parsed.name) : someToolNameStartsWith(allowedToolNames, parsed.name);
10376
+ if (!nameOk) return "impossible";
10377
+ return held.length > cap ? "impossible" : "possible";
10378
+ }
10379
+ function parseStreamMarkerName(t) {
10380
+ let cursor = STREAM_MARKER.length;
10381
+ while (cursor < t.length && isStreamWs(t[cursor])) cursor += 1;
10382
+ const nameStart = cursor;
10383
+ while (cursor < t.length && t[cursor] !== ">" && !isStreamWs(t[cursor])) cursor += 1;
10384
+ const name = t.slice(nameStart, cursor);
10385
+ if (name.length === 0) return cursor >= t.length ? "building" : "invalid";
10386
+ return { name, complete: cursor < t.length && t[cursor] === ">" };
10387
+ }
10388
+ function someToolNameStartsWith(allowedToolNames, prefix) {
10389
+ for (const name of allowedToolNames) {
10390
+ if (name.startsWith(prefix)) return true;
10391
+ }
10392
+ return false;
10393
+ }
10394
+ function firstPossibleMarkerStart(held, allowedToolNames) {
10395
+ for (let i = held.indexOf("<"); i !== -1; i = held.indexOf("<", i + 1)) {
10396
+ if (streamToolCallBufferState(held.slice(i), allowedToolNames) === "possible") return i;
10397
+ }
10398
+ return -1;
10399
+ }
10400
+ var HERMES_BLOCK, HERMES_PARAM, STREAM_MARKER, DEFAULT_STREAM_BUFFER_CAP, isStreamWs, StreamSuppressionBuffer;
10358
10401
  var init_hermes_tool_extract = __esm({
10359
10402
  "src/internal/llm/hermes-tool-extract.ts"() {
10360
10403
  init_sanitize_tool_input();
10361
10404
  HERMES_BLOCK = /<function=\s*([^>\s]+)\s*>([\s\S]*?)<\/tool_call>/g;
10362
10405
  HERMES_PARAM = /<parameter=\s*([^>\s]+)\s*>([\s\S]*?)<\/parameter>/g;
10406
+ STREAM_MARKER = "<function=";
10407
+ DEFAULT_STREAM_BUFFER_CAP = 8192;
10408
+ isStreamWs = (c) => c === " " || c === " " || c === "\n" || c === "\r";
10409
+ StreamSuppressionBuffer = class {
10410
+ constructor(allowedToolNames) {
10411
+ this.allowedToolNames = allowedToolNames;
10412
+ }
10413
+ allowedToolNames;
10414
+ #held = "";
10415
+ /** Feed a content delta; returns the text to emit as a `text_delta` now, or `undefined` to hold. */
10416
+ push(content) {
10417
+ this.#held += content;
10418
+ if (streamToolCallBufferState(this.#held, this.allowedToolNames) === "possible")
10419
+ return void 0;
10420
+ const holdStart = firstPossibleMarkerStart(this.#held, this.allowedToolNames);
10421
+ if (holdStart > 0) {
10422
+ const flush2 = this.#held.slice(0, holdStart);
10423
+ this.#held = this.#held.slice(holdStart);
10424
+ return flush2;
10425
+ }
10426
+ const flush = this.#held;
10427
+ this.#held = "";
10428
+ return flush;
10429
+ }
10430
+ /** Drain the held buffer at stream end. `hasNativeCalls` mirrors `finish()`'s size-guard: when
10431
+ * native `tool_calls` exist, `finish()` won't strip the leaked block, so stream the held text WHOLE
10432
+ * (keeping `accumulatedText == finish.text`); otherwise strip the recoverable blocks. Idempotent. */
10433
+ drain(hasNativeCalls) {
10434
+ if (this.#held.length === 0) return void 0;
10435
+ const held = this.#held;
10436
+ this.#held = "";
10437
+ if (hasNativeCalls) return held;
10438
+ const residual = extractHermesToolCalls(held, () => "held", this.allowedToolNames).residualText;
10439
+ return residual.length > 0 ? residual : void 0;
10440
+ }
10441
+ };
10363
10442
  }
10364
10443
  });
10365
10444
 
@@ -10547,7 +10626,10 @@ var init_openai2 = __esm({
10547
10626
  }
10548
10627
  const accumulator = new OpenAIStreamAccumulator(
10549
10628
  this.options.extractToolCallsFromContent ?? false,
10550
- providerId
10629
+ providerId,
10630
+ // R5: request-scoped allowlist — leaked recovery only promotes a block whose name is a tool the
10631
+ // model was actually given. Empty set (no tools) recovers nothing.
10632
+ new Set(request.tools?.map((tool) => tool.name) ?? [])
10551
10633
  );
10552
10634
  for await (const record of parseSseStream(response.body, signal)) {
10553
10635
  if (record.data === "[DONE]") break;
@@ -10570,6 +10652,8 @@ var init_openai2 = __esm({
10570
10652
  const events = accumulator.consume(chunk);
10571
10653
  for (const event of events) yield event;
10572
10654
  }
10655
+ const drainEvent = accumulator.finalizeHeldText();
10656
+ if (drainEvent !== void 0) yield drainEvent;
10573
10657
  return accumulator.finish();
10574
10658
  }
10575
10659
  };
@@ -10577,13 +10661,19 @@ var init_openai2 = __esm({
10577
10661
  /**
10578
10662
  * @param extractFromContent opt-in leaked-dialect safe-parse (theokit#58). Default false.
10579
10663
  * @param providerName provider id, used only to label the recovery log line.
10664
+ * @param allowedToolNames R5 request-scoped allowlist — built from `request.tools` at `stream()`;
10665
+ * leaked recovery in `finish()` only promotes a block whose name is in this set. `undefined`
10666
+ * (direct construction) recovers all (back-compat); an empty set recovers nothing.
10580
10667
  */
10581
- constructor(extractFromContent = false, providerName = "openai") {
10668
+ constructor(extractFromContent = false, providerName = "openai", allowedToolNames) {
10582
10669
  this.extractFromContent = extractFromContent;
10583
10670
  this.providerName = providerName;
10671
+ this.allowedToolNames = allowedToolNames;
10672
+ this.suppress = extractFromContent && allowedToolNames !== void 0 && allowedToolNames.size > 0 ? new StreamSuppressionBuffer(allowedToolNames) : void 0;
10584
10673
  }
10585
10674
  extractFromContent;
10586
10675
  providerName;
10676
+ allowedToolNames;
10587
10677
  text = "";
10588
10678
  stopReason = "end_turn";
10589
10679
  inputTokens;
@@ -10592,18 +10682,30 @@ var init_openai2 = __esm({
10592
10682
  cacheWriteTokens;
10593
10683
  reasoningTokens;
10594
10684
  toolCalls = /* @__PURE__ */ new Map();
10685
+ /** R7: present only when recovery is enabled AND the request declares tools — holds suspected
10686
+ * leaked-dialect content back from the `text_delta` stream. `undefined` ⇒ stream immediately. */
10687
+ suppress;
10595
10688
  consume(chunk) {
10596
10689
  const events = [];
10597
10690
  this.applyUsage(chunk.usage);
10598
10691
  for (const choice of chunk.choices ?? []) {
10599
- const reasoningEvent = this.applyReasoningDelta(
10600
- choice.delta?.reasoning ?? choice.delta?.reasoning_content
10601
- );
10602
- if (reasoningEvent !== void 0) events.push(reasoningEvent);
10603
- const textEvent = this.applyContentDelta(choice.delta?.content);
10604
- if (textEvent !== void 0) events.push(textEvent);
10605
- this.mergeToolCallDeltas(choice.delta?.tool_calls);
10606
- this.applyFinishReason(choice.finish_reason);
10692
+ events.push(...this.applyChoice(choice));
10693
+ }
10694
+ return events;
10695
+ }
10696
+ applyChoice(choice) {
10697
+ const events = [];
10698
+ const reasoningEvent = this.applyReasoningDelta(
10699
+ choice.delta?.reasoning ?? choice.delta?.reasoning_content
10700
+ );
10701
+ if (reasoningEvent !== void 0) events.push(reasoningEvent);
10702
+ const textEvent = this.applyContentDelta(choice.delta?.content);
10703
+ if (textEvent !== void 0) events.push(textEvent);
10704
+ this.mergeToolCallDeltas(choice.delta?.tool_calls);
10705
+ this.applyFinishReason(choice.finish_reason);
10706
+ if (choice.finish_reason !== void 0 && choice.finish_reason !== null) {
10707
+ const flushEvent = this.finalizeHeldText();
10708
+ if (flushEvent !== void 0) events.push(flushEvent);
10607
10709
  }
10608
10710
  return events;
10609
10711
  }
@@ -10628,7 +10730,17 @@ var init_openai2 = __esm({
10628
10730
  applyContentDelta(content) {
10629
10731
  if (typeof content !== "string" || content.length === 0) return void 0;
10630
10732
  this.text += content;
10631
- return { type: "text_delta", text: content };
10733
+ if (this.suppress === void 0) return { type: "text_delta", text: content };
10734
+ const emit = this.suppress.push(content);
10735
+ return emit !== void 0 ? { type: "text_delta", text: emit } : void 0;
10736
+ }
10737
+ /** R7 held-buffer finalizer, called at the `finish_reason` chunk (in `applyChoice`) AND after the
10738
+ * SSE loop in `stream()` — so a stream that omits a `finish_reason` terminal never silently drops
10739
+ * held text. `toolCalls.size > 0` (native calls present) makes `finish()` skip recovery, so the
10740
+ * buffer streams the held text whole. Idempotent once drained. */
10741
+ finalizeHeldText() {
10742
+ const emit = this.suppress?.drain(this.toolCalls.size > 0);
10743
+ return emit !== void 0 ? { type: "text_delta", text: emit } : void 0;
10632
10744
  }
10633
10745
  mergeToolCallDeltas(deltas) {
10634
10746
  for (const call of deltas ?? []) {
@@ -10654,7 +10766,8 @@ var init_openai2 = __esm({
10654
10766
  if (this.extractFromContent && toolCalls.length === 0) {
10655
10767
  const recovered = extractHermesToolCalls(
10656
10768
  this.text,
10657
- () => `hermes-${globalThis.crypto.randomUUID()}`
10769
+ () => `hermes-${globalThis.crypto.randomUUID()}`,
10770
+ this.allowedToolNames
10658
10771
  );
10659
10772
  if (recovered.toolCalls.length > 0) {
10660
10773
  toolCalls.push(...recovered.toolCalls);
@@ -10662,6 +10775,12 @@ var init_openai2 = __esm({
10662
10775
  stopReason = "tool_use";
10663
10776
  process.stderr.write(
10664
10777
  `[theokit-sdk] recovered ${recovered.toolCalls.length} leaked tool call(s) from assistant content (provider="${this.providerName}", names=${recovered.toolCalls.map((c) => c.name).join(",")})
10778
+ `
10779
+ );
10780
+ }
10781
+ if (recovered.droppedNames.length > 0) {
10782
+ process.stderr.write(
10783
+ `[theokit-sdk] dropped ${recovered.droppedNames.length} leaked block(s) whose name is not a tool in the request (provider="${this.providerName}", names=${recovered.droppedNames.join(",")})
10665
10784
  `
10666
10785
  );
10667
10786
  }