@theokit/sdk 2.14.0 → 2.15.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/dist/eval.js CHANGED
@@ -1454,6 +1454,7 @@ function isEmptyRound(result) {
1454
1454
  return (result.result ?? "").trim() === "";
1455
1455
  }
1456
1456
  function classifyRound(result, round, maxRounds, emptyStreak) {
1457
+ if (result.stoppedByDoomLoop === true) return "no_progress";
1457
1458
  if (result.stoppedAtIterationLimit !== true) return "done";
1458
1459
  if (isEmptyRound(result) && emptyStreak >= 1) return "no_progress";
1459
1460
  if (round >= maxRounds) return "step_limit";
@@ -4487,6 +4488,7 @@ function applyScriptMetrics(base, script) {
4487
4488
  if (script.usage !== void 0) base.usage = script.usage;
4488
4489
  if (script.cost !== void 0) base.cost = script.cost;
4489
4490
  if (script.stoppedAtIterationLimit === true) base.stoppedAtIterationLimit = true;
4491
+ if (script.stoppedByDoomLoop === true) base.stoppedByDoomLoop = true;
4490
4492
  }
4491
4493
 
4492
4494
  // src/internal/runtime/cloud/cloud-run.ts
@@ -8268,6 +8270,93 @@ function evaluateBudgetGate(tracker) {
8268
8270
  }
8269
8271
  }
8270
8272
 
8273
+ // src/internal/agent-loop/doom-loop-tracker.ts
8274
+ init_errors();
8275
+ function createDoomLoopTracker(option) {
8276
+ if (option === false) return void 0;
8277
+ return new DoomLoopTracker(option);
8278
+ }
8279
+ var DEFAULT_CONFIG = { softThreshold: 3, hardThreshold: 5 };
8280
+ function assertValidThresholds(soft, hard) {
8281
+ for (const [label, value] of [
8282
+ ["softThreshold", soft],
8283
+ ["hardThreshold", hard]
8284
+ ]) {
8285
+ if (!Number.isInteger(value) || value < 1) {
8286
+ throw new ConfigurationError(
8287
+ `doomLoop.${label} must be a positive integer (received ${value}).`,
8288
+ { code: "invalid_doom_loop_threshold" }
8289
+ );
8290
+ }
8291
+ }
8292
+ }
8293
+ function sortKeys(value) {
8294
+ if (value === null || typeof value !== "object") return value;
8295
+ if (Array.isArray(value)) return value.map(sortKeys);
8296
+ const out = {};
8297
+ for (const key of Object.keys(value).sort()) {
8298
+ out[key] = sortKeys(value[key]);
8299
+ }
8300
+ return out;
8301
+ }
8302
+ function signatureOf(call) {
8303
+ const { input } = call;
8304
+ let inputSig;
8305
+ if (input === null || input === void 0) inputSig = "null";
8306
+ else if (typeof input !== "object") inputSig = String(input);
8307
+ else {
8308
+ try {
8309
+ inputSig = JSON.stringify(sortKeys(input)) ?? "null";
8310
+ } catch {
8311
+ inputSig = String(input);
8312
+ }
8313
+ }
8314
+ return `${call.name}\0${inputSig}`;
8315
+ }
8316
+ var DoomLoopTracker = class {
8317
+ #config;
8318
+ #lastSignature = "";
8319
+ #count = 0;
8320
+ constructor(config) {
8321
+ const softThreshold = config?.softThreshold ?? DEFAULT_CONFIG.softThreshold;
8322
+ const hardThreshold = config?.hardThreshold ?? DEFAULT_CONFIG.hardThreshold;
8323
+ assertValidThresholds(softThreshold, hardThreshold);
8324
+ this.#config = { softThreshold, hardThreshold };
8325
+ }
8326
+ inspect(call) {
8327
+ const signature = signatureOf(call);
8328
+ this.#count = signature === this.#lastSignature ? this.#count + 1 : 1;
8329
+ this.#lastSignature = signature;
8330
+ const count = this.#count;
8331
+ if (count >= this.#config.hardThreshold) {
8332
+ return {
8333
+ kind: "hard",
8334
+ message: `Detected ${count} consecutive identical calls to \`${call.name}\`; stopping to avoid a loop.`
8335
+ };
8336
+ }
8337
+ if (count === this.#config.softThreshold) {
8338
+ return {
8339
+ kind: "soft",
8340
+ message: `Detected ${count} consecutive identical calls to \`${call.name}\`; try a different approach.`
8341
+ };
8342
+ }
8343
+ return { kind: "ok" };
8344
+ }
8345
+ reset() {
8346
+ this.#lastSignature = "";
8347
+ this.#count = 0;
8348
+ }
8349
+ };
8350
+ function firstDoomLoopVerdict(tracker, calls) {
8351
+ let escalation = { kind: "ok" };
8352
+ for (const call of calls) {
8353
+ const v = tracker.inspect(call);
8354
+ if (v.kind === "hard") return v;
8355
+ if (v.kind === "soft" && escalation.kind === "ok") escalation = v;
8356
+ }
8357
+ return escalation;
8358
+ }
8359
+
8271
8360
  // src/internal/budget/usage-accumulator.ts
8272
8361
  var UsageAccumulator = class {
8273
8362
  input = 0;
@@ -8448,6 +8537,7 @@ async function initLoopContext(inputs) {
8448
8537
  tools,
8449
8538
  finalText: "",
8450
8539
  finalStatus: "finished",
8540
+ doomLoop: createDoomLoopTracker(inputs.doomLoop),
8451
8541
  usage: new UsageAccumulator(),
8452
8542
  nudgeAttempts: 0,
8453
8543
  stopFeedbackAttempts: 0,
@@ -9472,6 +9562,7 @@ async function runAgentLoop(inputs) {
9472
9562
  ctx.finalStatus = "error";
9473
9563
  }
9474
9564
  sendSpan?.setAttribute("status", ctx.finalStatus);
9565
+ if (ctx.stoppedByDoomLoop === true) sendSpan?.setAttribute("stoppedByDoomLoop", true);
9475
9566
  if (inputs.telemetry?.includeContent === true && ctx.finalText.length > 0) {
9476
9567
  sendSpan?.addEvent("response", { content: ctx.finalText });
9477
9568
  }
@@ -9498,7 +9589,8 @@ async function runAgentLoop(inputs) {
9498
9589
  ...usage !== void 0 ? { usage } : {},
9499
9590
  ...cost !== void 0 ? { cost } : {},
9500
9591
  ...ctx.error !== void 0 ? { error: ctx.error } : {},
9501
- ...ctx.stoppedAtIterationLimit === true ? { stoppedAtIterationLimit: true } : {}
9592
+ ...ctx.stoppedAtIterationLimit === true ? { stoppedAtIterationLimit: true } : {},
9593
+ ...ctx.stoppedByDoomLoop === true ? { stoppedByDoomLoop: true } : {}
9502
9594
  };
9503
9595
  } finally {
9504
9596
  if (ctxRef !== void 0 && ctxRef.memoryProviderHandle !== void 0 && inputs.memoryProvider !== void 0) {
@@ -9657,8 +9749,26 @@ async function continueOrTerminate(inputs, ctx, llmOutput) {
9657
9749
  }
9658
9750
  }
9659
9751
  pushToolConversationSteps(ctx, llmOutput.toolCalls, toolResults);
9752
+ if (await inspectDoomLoop(inputs, ctx, llmOutput.toolCalls) === "stop") return "done";
9660
9753
  return handleToolErrorContinuation(inputs, ctx, toolResults);
9661
9754
  }
9755
+ async function inspectDoomLoop(inputs, ctx, toolCalls) {
9756
+ if (ctx.doomLoop === void 0) return "continue";
9757
+ const verdict = firstDoomLoopVerdict(ctx.doomLoop, toolCalls);
9758
+ if (verdict.kind === "hard") {
9759
+ ctx.stoppedByDoomLoop = true;
9760
+ await emitAssistantTextStep(
9761
+ inputs,
9762
+ ctx,
9763
+ verdict.message ?? "Stopped: repeated identical tool calls made no progress."
9764
+ );
9765
+ return "stop";
9766
+ }
9767
+ if (verdict.kind === "soft") {
9768
+ ctx.messages.push({ role: "user", content: [{ type: "text", text: verdict.message ?? "" }] });
9769
+ }
9770
+ return "continue";
9771
+ }
9662
9772
 
9663
9773
  // src/internal/llm/fallback-client.ts
9664
9774
  init_errors();
@@ -12203,6 +12313,8 @@ function buildLoopInputs(options, runId, userText) {
12203
12313
  // M1-2: per-send iteration ceiling (validated above). The loop reads
12204
12314
  // inputs.maxIterations (default 8 when unset).
12205
12315
  ...maxIterations !== void 0 ? { maxIterations } : {},
12316
+ // Doom-loop guard config (default on; `false` disables, object tunes thresholds).
12317
+ ...options.sendOptions.doomLoop !== void 0 ? { doomLoop: options.sendOptions.doomLoop } : {},
12206
12318
  // D315-D317 — tool lifecycle hooks (cost tracking + audit + retry/alert)
12207
12319
  ...options.agentOptions.onToolStart !== void 0 ? { onToolStart: options.agentOptions.onToolStart } : {},
12208
12320
  ...options.agentOptions.onToolEnd !== void 0 ? { onToolEnd: options.agentOptions.onToolEnd } : {},
@@ -12335,6 +12447,7 @@ var RealLocalRun = class extends FixtureRunBase {
12335
12447
  if (output.usage !== void 0) this.script.usage = output.usage;
12336
12448
  if (output.cost !== void 0) this.script.cost = output.cost;
12337
12449
  if (output.stoppedAtIterationLimit === true) this.script.stoppedAtIterationLimit = true;
12450
+ if (output.stoppedByDoomLoop === true) this.script.stoppedByDoomLoop = true;
12338
12451
  if (output.error !== void 0 && this.script.errorDetail === void 0) {
12339
12452
  this.script.errorDetail = {
12340
12453
  message: output.error.message,