@theokit/sdk 2.14.0 → 2.15.1

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.
@@ -1,4 +1,4 @@
1
- import { C as CustomTool, M as ModelSelection, F as SDKUserMessage, H as SendOptions, b as Run, r as RunToCompletionOptions, s as RunToCompletionResult, S as SDKMessage, T as StreamToCompletionResult, a as McpServerConfig } from './run-TMdc7gmo.cjs';
1
+ import { C as CustomTool, M as ModelSelection, G as SDKUserMessage, J as SendOptions, b as Run, r as RunToCompletionOptions, s as RunToCompletionResult, S as SDKMessage, U as StreamToCompletionResult, a as McpServerConfig } from './run-DXy_MVwz.cjs';
2
2
 
3
3
  /**
4
4
  * Fork primitive public type contracts (T1.2, ADRs D110-D114).
@@ -1,4 +1,4 @@
1
- import { C as CustomTool, M as ModelSelection, F as SDKUserMessage, H as SendOptions, b as Run, r as RunToCompletionOptions, s as RunToCompletionResult, S as SDKMessage, T as StreamToCompletionResult, a as McpServerConfig } from './run-TMdc7gmo.js';
1
+ import { C as CustomTool, M as ModelSelection, G as SDKUserMessage, J as SendOptions, b as Run, r as RunToCompletionOptions, s as RunToCompletionResult, S as SDKMessage, U as StreamToCompletionResult, a as McpServerConfig } from './run-DXy_MVwz.js';
2
2
 
3
3
  /**
4
4
  * Fork primitive public type contracts (T1.2, ADRs D110-D114).
package/dist/cron.cjs CHANGED
@@ -1458,6 +1458,7 @@ function isEmptyRound(result) {
1458
1458
  return (result.result ?? "").trim() === "";
1459
1459
  }
1460
1460
  function classifyRound(result, round, maxRounds, emptyStreak) {
1461
+ if (result.stoppedByDoomLoop === true) return "no_progress";
1461
1462
  if (result.stoppedAtIterationLimit !== true) return "done";
1462
1463
  if (isEmptyRound(result) && emptyStreak >= 1) return "no_progress";
1463
1464
  if (round >= maxRounds) return "step_limit";
@@ -4495,6 +4496,7 @@ function applyScriptMetrics(base, script) {
4495
4496
  if (script.usage !== void 0) base.usage = script.usage;
4496
4497
  if (script.cost !== void 0) base.cost = script.cost;
4497
4498
  if (script.stoppedAtIterationLimit === true) base.stoppedAtIterationLimit = true;
4499
+ if (script.stoppedByDoomLoop === true) base.stoppedByDoomLoop = true;
4498
4500
  }
4499
4501
 
4500
4502
  // src/internal/runtime/cloud/cloud-run.ts
@@ -8276,6 +8278,93 @@ function evaluateBudgetGate(tracker) {
8276
8278
  }
8277
8279
  }
8278
8280
 
8281
+ // src/internal/agent-loop/doom-loop-tracker.ts
8282
+ init_errors();
8283
+ function createDoomLoopTracker(option) {
8284
+ if (option === false) return void 0;
8285
+ return new DoomLoopTracker(option);
8286
+ }
8287
+ var DEFAULT_CONFIG = { softThreshold: 3, hardThreshold: 5 };
8288
+ function assertValidThresholds(soft, hard) {
8289
+ for (const [label, value] of [
8290
+ ["softThreshold", soft],
8291
+ ["hardThreshold", hard]
8292
+ ]) {
8293
+ if (!Number.isInteger(value) || value < 1) {
8294
+ throw new ConfigurationError(
8295
+ `doomLoop.${label} must be a positive integer (received ${value}).`,
8296
+ { code: "invalid_doom_loop_threshold" }
8297
+ );
8298
+ }
8299
+ }
8300
+ }
8301
+ function sortKeys(value) {
8302
+ if (value === null || typeof value !== "object") return value;
8303
+ if (Array.isArray(value)) return value.map(sortKeys);
8304
+ const out = {};
8305
+ for (const key of Object.keys(value).sort()) {
8306
+ out[key] = sortKeys(value[key]);
8307
+ }
8308
+ return out;
8309
+ }
8310
+ function signatureOf(call) {
8311
+ const { input } = call;
8312
+ let inputSig;
8313
+ if (input === null || input === void 0) inputSig = "null";
8314
+ else if (typeof input !== "object") inputSig = String(input);
8315
+ else {
8316
+ try {
8317
+ inputSig = JSON.stringify(sortKeys(input)) ?? "null";
8318
+ } catch {
8319
+ inputSig = String(input);
8320
+ }
8321
+ }
8322
+ return `${call.name}\0${inputSig}`;
8323
+ }
8324
+ var DoomLoopTracker = class {
8325
+ #config;
8326
+ #lastSignature = "";
8327
+ #count = 0;
8328
+ constructor(config) {
8329
+ const softThreshold = config?.softThreshold ?? DEFAULT_CONFIG.softThreshold;
8330
+ const hardThreshold = config?.hardThreshold ?? DEFAULT_CONFIG.hardThreshold;
8331
+ assertValidThresholds(softThreshold, hardThreshold);
8332
+ this.#config = { softThreshold, hardThreshold };
8333
+ }
8334
+ inspect(call) {
8335
+ const signature = signatureOf(call);
8336
+ this.#count = signature === this.#lastSignature ? this.#count + 1 : 1;
8337
+ this.#lastSignature = signature;
8338
+ const count = this.#count;
8339
+ if (count >= this.#config.hardThreshold) {
8340
+ return {
8341
+ kind: "hard",
8342
+ message: `Detected ${count} consecutive identical calls to \`${call.name}\`; stopping to avoid a loop.`
8343
+ };
8344
+ }
8345
+ if (count === this.#config.softThreshold) {
8346
+ return {
8347
+ kind: "soft",
8348
+ message: `Detected ${count} consecutive identical calls to \`${call.name}\`; try a different approach.`
8349
+ };
8350
+ }
8351
+ return { kind: "ok" };
8352
+ }
8353
+ reset() {
8354
+ this.#lastSignature = "";
8355
+ this.#count = 0;
8356
+ }
8357
+ };
8358
+ function firstDoomLoopVerdict(tracker, calls) {
8359
+ let escalation = { kind: "ok" };
8360
+ for (const call of calls) {
8361
+ const v = tracker.inspect(call);
8362
+ if (v.kind === "hard") return v;
8363
+ if (v.kind === "soft" && escalation.kind === "ok") escalation = v;
8364
+ }
8365
+ return escalation;
8366
+ }
8367
+
8279
8368
  // src/internal/budget/usage-accumulator.ts
8280
8369
  var UsageAccumulator = class {
8281
8370
  input = 0;
@@ -8456,6 +8545,7 @@ async function initLoopContext(inputs) {
8456
8545
  tools,
8457
8546
  finalText: "",
8458
8547
  finalStatus: "finished",
8548
+ doomLoop: createDoomLoopTracker(inputs.doomLoop),
8459
8549
  usage: new UsageAccumulator(),
8460
8550
  nudgeAttempts: 0,
8461
8551
  stopFeedbackAttempts: 0,
@@ -9480,6 +9570,7 @@ async function runAgentLoop(inputs) {
9480
9570
  ctx.finalStatus = "error";
9481
9571
  }
9482
9572
  sendSpan?.setAttribute("status", ctx.finalStatus);
9573
+ if (ctx.stoppedByDoomLoop === true) sendSpan?.setAttribute("stoppedByDoomLoop", true);
9483
9574
  if (inputs.telemetry?.includeContent === true && ctx.finalText.length > 0) {
9484
9575
  sendSpan?.addEvent("response", { content: ctx.finalText });
9485
9576
  }
@@ -9506,7 +9597,8 @@ async function runAgentLoop(inputs) {
9506
9597
  ...usage !== void 0 ? { usage } : {},
9507
9598
  ...cost !== void 0 ? { cost } : {},
9508
9599
  ...ctx.error !== void 0 ? { error: ctx.error } : {},
9509
- ...ctx.stoppedAtIterationLimit === true ? { stoppedAtIterationLimit: true } : {}
9600
+ ...ctx.stoppedAtIterationLimit === true ? { stoppedAtIterationLimit: true } : {},
9601
+ ...ctx.stoppedByDoomLoop === true ? { stoppedByDoomLoop: true } : {}
9510
9602
  };
9511
9603
  } finally {
9512
9604
  if (ctxRef !== void 0 && ctxRef.memoryProviderHandle !== void 0 && inputs.memoryProvider !== void 0) {
@@ -9665,8 +9757,26 @@ async function continueOrTerminate(inputs, ctx, llmOutput) {
9665
9757
  }
9666
9758
  }
9667
9759
  pushToolConversationSteps(ctx, llmOutput.toolCalls, toolResults);
9760
+ if (await inspectDoomLoop(inputs, ctx, llmOutput.toolCalls) === "stop") return "done";
9668
9761
  return handleToolErrorContinuation(inputs, ctx, toolResults);
9669
9762
  }
9763
+ async function inspectDoomLoop(inputs, ctx, toolCalls) {
9764
+ if (ctx.doomLoop === void 0) return "continue";
9765
+ const verdict = firstDoomLoopVerdict(ctx.doomLoop, toolCalls);
9766
+ if (verdict.kind === "hard") {
9767
+ ctx.stoppedByDoomLoop = true;
9768
+ await emitAssistantTextStep(
9769
+ inputs,
9770
+ ctx,
9771
+ verdict.message ?? "Stopped: repeated identical tool calls made no progress."
9772
+ );
9773
+ return "stop";
9774
+ }
9775
+ if (verdict.kind === "soft") {
9776
+ ctx.messages.push({ role: "user", content: [{ type: "text", text: verdict.message ?? "" }] });
9777
+ }
9778
+ return "continue";
9779
+ }
9670
9780
 
9671
9781
  // src/internal/llm/fallback-client.ts
9672
9782
  init_errors();
@@ -11068,11 +11178,16 @@ function sanitizeToolInput(input, options) {
11068
11178
  // src/internal/llm/hermes-tool-extract.ts
11069
11179
  var HERMES_BLOCK = /<function=\s*([^>\s]+)\s*>([\s\S]*?)<\/tool_call>/g;
11070
11180
  var HERMES_PARAM = /<parameter=\s*([^>\s]+)\s*>([\s\S]*?)<\/parameter>/g;
11071
- function extractHermesToolCalls(content, makeId) {
11181
+ function extractHermesToolCalls(content, makeId, allowedToolNames) {
11182
+ const isPromoted = (name) => name.length > 0 && (allowedToolNames === void 0 || allowedToolNames.has(name));
11072
11183
  const toolCalls = [];
11184
+ const droppedNames = [];
11073
11185
  for (const block of content.matchAll(HERMES_BLOCK)) {
11074
11186
  const name = (block[1] ?? "").trim();
11075
- if (name.length === 0) continue;
11187
+ if (!isPromoted(name)) {
11188
+ if (name.length > 0 && allowedToolNames !== void 0) droppedNames.push(name);
11189
+ continue;
11190
+ }
11076
11191
  toolCalls.push({
11077
11192
  type: "tool_use",
11078
11193
  id: makeId(),
@@ -11080,8 +11195,11 @@ function extractHermesToolCalls(content, makeId) {
11080
11195
  input: parseHermesParams(block[2] ?? "")
11081
11196
  });
11082
11197
  }
11083
- const residualText = toolCalls.length === 0 ? content : content.replace(HERMES_BLOCK, "").trim();
11084
- return { toolCalls, residualText };
11198
+ const residualText = toolCalls.length === 0 ? content : content.replace(
11199
+ HERMES_BLOCK,
11200
+ (full, rawName) => isPromoted((rawName ?? "").trim()) ? "" : full
11201
+ ).trim();
11202
+ return { toolCalls, residualText, droppedNames };
11085
11203
  }
11086
11204
  function parseHermesParams(inner) {
11087
11205
  const input = {};
@@ -11165,7 +11283,10 @@ var OpenAIClient = class {
11165
11283
  }
11166
11284
  const accumulator = new OpenAIStreamAccumulator(
11167
11285
  this.options.extractToolCallsFromContent ?? false,
11168
- providerId
11286
+ providerId,
11287
+ // R5: request-scoped allowlist — leaked recovery only promotes a block whose name is a tool the
11288
+ // model was actually given. Empty set (no tools) recovers nothing.
11289
+ new Set(request.tools?.map((tool) => tool.name) ?? [])
11169
11290
  );
11170
11291
  for await (const record of parseSseStream(response.body, signal)) {
11171
11292
  if (record.data === "[DONE]") break;
@@ -11195,13 +11316,18 @@ var OpenAIStreamAccumulator = class {
11195
11316
  /**
11196
11317
  * @param extractFromContent opt-in leaked-dialect safe-parse (theokit#58). Default false.
11197
11318
  * @param providerName provider id, used only to label the recovery log line.
11319
+ * @param allowedToolNames R5 request-scoped allowlist — built from `request.tools` at `stream()`;
11320
+ * leaked recovery in `finish()` only promotes a block whose name is in this set. `undefined`
11321
+ * (direct construction) recovers all (back-compat); an empty set recovers nothing.
11198
11322
  */
11199
- constructor(extractFromContent = false, providerName = "openai") {
11323
+ constructor(extractFromContent = false, providerName = "openai", allowedToolNames) {
11200
11324
  this.extractFromContent = extractFromContent;
11201
11325
  this.providerName = providerName;
11326
+ this.allowedToolNames = allowedToolNames;
11202
11327
  }
11203
11328
  extractFromContent;
11204
11329
  providerName;
11330
+ allowedToolNames;
11205
11331
  text = "";
11206
11332
  stopReason = "end_turn";
11207
11333
  inputTokens;
@@ -11272,7 +11398,8 @@ var OpenAIStreamAccumulator = class {
11272
11398
  if (this.extractFromContent && toolCalls.length === 0) {
11273
11399
  const recovered = extractHermesToolCalls(
11274
11400
  this.text,
11275
- () => `hermes-${globalThis.crypto.randomUUID()}`
11401
+ () => `hermes-${globalThis.crypto.randomUUID()}`,
11402
+ this.allowedToolNames
11276
11403
  );
11277
11404
  if (recovered.toolCalls.length > 0) {
11278
11405
  toolCalls.push(...recovered.toolCalls);
@@ -11280,6 +11407,12 @@ var OpenAIStreamAccumulator = class {
11280
11407
  stopReason = "tool_use";
11281
11408
  process.stderr.write(
11282
11409
  `[theokit-sdk] recovered ${recovered.toolCalls.length} leaked tool call(s) from assistant content (provider="${this.providerName}", names=${recovered.toolCalls.map((c) => c.name).join(",")})
11410
+ `
11411
+ );
11412
+ }
11413
+ if (recovered.droppedNames.length > 0) {
11414
+ process.stderr.write(
11415
+ `[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(",")})
11283
11416
  `
11284
11417
  );
11285
11418
  }
@@ -12211,6 +12344,8 @@ function buildLoopInputs(options, runId, userText) {
12211
12344
  // M1-2: per-send iteration ceiling (validated above). The loop reads
12212
12345
  // inputs.maxIterations (default 8 when unset).
12213
12346
  ...maxIterations !== void 0 ? { maxIterations } : {},
12347
+ // Doom-loop guard config (default on; `false` disables, object tunes thresholds).
12348
+ ...options.sendOptions.doomLoop !== void 0 ? { doomLoop: options.sendOptions.doomLoop } : {},
12214
12349
  // D315-D317 — tool lifecycle hooks (cost tracking + audit + retry/alert)
12215
12350
  ...options.agentOptions.onToolStart !== void 0 ? { onToolStart: options.agentOptions.onToolStart } : {},
12216
12351
  ...options.agentOptions.onToolEnd !== void 0 ? { onToolEnd: options.agentOptions.onToolEnd } : {},
@@ -12343,6 +12478,7 @@ var RealLocalRun = class extends FixtureRunBase {
12343
12478
  if (output.usage !== void 0) this.script.usage = output.usage;
12344
12479
  if (output.cost !== void 0) this.script.cost = output.cost;
12345
12480
  if (output.stoppedAtIterationLimit === true) this.script.stoppedAtIterationLimit = true;
12481
+ if (output.stoppedByDoomLoop === true) this.script.stoppedByDoomLoop = true;
12346
12482
  if (output.error !== void 0 && this.script.errorDetail === void 0) {
12347
12483
  this.script.errorDetail = {
12348
12484
  message: output.error.message,