@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/index.cjs CHANGED
@@ -2817,6 +2817,7 @@ function isEmptyRound(result) {
2817
2817
  return (result.result ?? "").trim() === "";
2818
2818
  }
2819
2819
  function classifyRound(result, round, maxRounds, emptyStreak) {
2820
+ if (result.stoppedByDoomLoop === true) return "no_progress";
2820
2821
  if (result.stoppedAtIterationLimit !== true) return "done";
2821
2822
  if (isEmptyRound(result) && emptyStreak >= 1) return "no_progress";
2822
2823
  if (round >= maxRounds) return "step_limit";
@@ -6982,6 +6983,7 @@ function applyScriptMetrics(base, script) {
6982
6983
  if (script.usage !== void 0) base.usage = script.usage;
6983
6984
  if (script.cost !== void 0) base.cost = script.cost;
6984
6985
  if (script.stoppedAtIterationLimit === true) base.stoppedAtIterationLimit = true;
6986
+ if (script.stoppedByDoomLoop === true) base.stoppedByDoomLoop = true;
6985
6987
  }
6986
6988
 
6987
6989
  // src/internal/runtime/cloud/cloud-run.ts
@@ -10733,6 +10735,93 @@ function evaluateBudgetGate(tracker) {
10733
10735
  }
10734
10736
  }
10735
10737
 
10738
+ // src/internal/agent-loop/doom-loop-tracker.ts
10739
+ init_errors();
10740
+ function createDoomLoopTracker(option) {
10741
+ if (option === false) return void 0;
10742
+ return new DoomLoopTracker(option);
10743
+ }
10744
+ var DEFAULT_CONFIG = { softThreshold: 3, hardThreshold: 5 };
10745
+ function assertValidThresholds(soft, hard) {
10746
+ for (const [label, value] of [
10747
+ ["softThreshold", soft],
10748
+ ["hardThreshold", hard]
10749
+ ]) {
10750
+ if (!Number.isInteger(value) || value < 1) {
10751
+ throw new exports.ConfigurationError(
10752
+ `doomLoop.${label} must be a positive integer (received ${value}).`,
10753
+ { code: "invalid_doom_loop_threshold" }
10754
+ );
10755
+ }
10756
+ }
10757
+ }
10758
+ function sortKeys(value) {
10759
+ if (value === null || typeof value !== "object") return value;
10760
+ if (Array.isArray(value)) return value.map(sortKeys);
10761
+ const out = {};
10762
+ for (const key2 of Object.keys(value).sort()) {
10763
+ out[key2] = sortKeys(value[key2]);
10764
+ }
10765
+ return out;
10766
+ }
10767
+ function signatureOf(call) {
10768
+ const { input } = call;
10769
+ let inputSig;
10770
+ if (input === null || input === void 0) inputSig = "null";
10771
+ else if (typeof input !== "object") inputSig = String(input);
10772
+ else {
10773
+ try {
10774
+ inputSig = JSON.stringify(sortKeys(input)) ?? "null";
10775
+ } catch {
10776
+ inputSig = String(input);
10777
+ }
10778
+ }
10779
+ return `${call.name}\0${inputSig}`;
10780
+ }
10781
+ var DoomLoopTracker = class {
10782
+ #config;
10783
+ #lastSignature = "";
10784
+ #count = 0;
10785
+ constructor(config) {
10786
+ const softThreshold = config?.softThreshold ?? DEFAULT_CONFIG.softThreshold;
10787
+ const hardThreshold = config?.hardThreshold ?? DEFAULT_CONFIG.hardThreshold;
10788
+ assertValidThresholds(softThreshold, hardThreshold);
10789
+ this.#config = { softThreshold, hardThreshold };
10790
+ }
10791
+ inspect(call) {
10792
+ const signature = signatureOf(call);
10793
+ this.#count = signature === this.#lastSignature ? this.#count + 1 : 1;
10794
+ this.#lastSignature = signature;
10795
+ const count = this.#count;
10796
+ if (count >= this.#config.hardThreshold) {
10797
+ return {
10798
+ kind: "hard",
10799
+ message: `Detected ${count} consecutive identical calls to \`${call.name}\`; stopping to avoid a loop.`
10800
+ };
10801
+ }
10802
+ if (count === this.#config.softThreshold) {
10803
+ return {
10804
+ kind: "soft",
10805
+ message: `Detected ${count} consecutive identical calls to \`${call.name}\`; try a different approach.`
10806
+ };
10807
+ }
10808
+ return { kind: "ok" };
10809
+ }
10810
+ reset() {
10811
+ this.#lastSignature = "";
10812
+ this.#count = 0;
10813
+ }
10814
+ };
10815
+ function firstDoomLoopVerdict(tracker, calls) {
10816
+ let escalation = { kind: "ok" };
10817
+ for (const call of calls) {
10818
+ const v = tracker.inspect(call);
10819
+ if (v.kind === "hard") return v;
10820
+ if (v.kind === "soft" && escalation.kind === "ok") escalation = v;
10821
+ }
10822
+ return escalation;
10823
+ }
10824
+
10736
10825
  // src/internal/budget/usage-accumulator.ts
10737
10826
  var UsageAccumulator = class {
10738
10827
  input = 0;
@@ -10913,6 +11002,7 @@ async function initLoopContext(inputs) {
10913
11002
  tools,
10914
11003
  finalText: "",
10915
11004
  finalStatus: "finished",
11005
+ doomLoop: createDoomLoopTracker(inputs.doomLoop),
10916
11006
  usage: new UsageAccumulator(),
10917
11007
  nudgeAttempts: 0,
10918
11008
  stopFeedbackAttempts: 0,
@@ -11937,6 +12027,7 @@ async function runAgentLoop(inputs) {
11937
12027
  ctx.finalStatus = "error";
11938
12028
  }
11939
12029
  sendSpan?.setAttribute("status", ctx.finalStatus);
12030
+ if (ctx.stoppedByDoomLoop === true) sendSpan?.setAttribute("stoppedByDoomLoop", true);
11940
12031
  if (inputs.telemetry?.includeContent === true && ctx.finalText.length > 0) {
11941
12032
  sendSpan?.addEvent("response", { content: ctx.finalText });
11942
12033
  }
@@ -11963,7 +12054,8 @@ async function runAgentLoop(inputs) {
11963
12054
  ...usage !== void 0 ? { usage } : {},
11964
12055
  ...cost !== void 0 ? { cost } : {},
11965
12056
  ...ctx.error !== void 0 ? { error: ctx.error } : {},
11966
- ...ctx.stoppedAtIterationLimit === true ? { stoppedAtIterationLimit: true } : {}
12057
+ ...ctx.stoppedAtIterationLimit === true ? { stoppedAtIterationLimit: true } : {},
12058
+ ...ctx.stoppedByDoomLoop === true ? { stoppedByDoomLoop: true } : {}
11967
12059
  };
11968
12060
  } finally {
11969
12061
  if (ctxRef !== void 0 && ctxRef.memoryProviderHandle !== void 0 && inputs.memoryProvider !== void 0) {
@@ -12122,8 +12214,26 @@ async function continueOrTerminate(inputs, ctx, llmOutput) {
12122
12214
  }
12123
12215
  }
12124
12216
  pushToolConversationSteps(ctx, llmOutput.toolCalls, toolResults);
12217
+ if (await inspectDoomLoop(inputs, ctx, llmOutput.toolCalls) === "stop") return "done";
12125
12218
  return handleToolErrorContinuation(inputs, ctx, toolResults);
12126
12219
  }
12220
+ async function inspectDoomLoop(inputs, ctx, toolCalls) {
12221
+ if (ctx.doomLoop === void 0) return "continue";
12222
+ const verdict = firstDoomLoopVerdict(ctx.doomLoop, toolCalls);
12223
+ if (verdict.kind === "hard") {
12224
+ ctx.stoppedByDoomLoop = true;
12225
+ await emitAssistantTextStep(
12226
+ inputs,
12227
+ ctx,
12228
+ verdict.message ?? "Stopped: repeated identical tool calls made no progress."
12229
+ );
12230
+ return "stop";
12231
+ }
12232
+ if (verdict.kind === "soft") {
12233
+ ctx.messages.push({ role: "user", content: [{ type: "text", text: verdict.message ?? "" }] });
12234
+ }
12235
+ return "continue";
12236
+ }
12127
12237
 
12128
12238
  // src/internal/llm/fallback-client.ts
12129
12239
  init_errors();
@@ -14684,6 +14794,8 @@ function buildLoopInputs(options, runId, userText) {
14684
14794
  // M1-2: per-send iteration ceiling (validated above). The loop reads
14685
14795
  // inputs.maxIterations (default 8 when unset).
14686
14796
  ...maxIterations !== void 0 ? { maxIterations } : {},
14797
+ // Doom-loop guard config (default on; `false` disables, object tunes thresholds).
14798
+ ...options.sendOptions.doomLoop !== void 0 ? { doomLoop: options.sendOptions.doomLoop } : {},
14687
14799
  // D315-D317 — tool lifecycle hooks (cost tracking + audit + retry/alert)
14688
14800
  ...options.agentOptions.onToolStart !== void 0 ? { onToolStart: options.agentOptions.onToolStart } : {},
14689
14801
  ...options.agentOptions.onToolEnd !== void 0 ? { onToolEnd: options.agentOptions.onToolEnd } : {},
@@ -14816,6 +14928,7 @@ var RealLocalRun = class extends FixtureRunBase {
14816
14928
  if (output.usage !== void 0) this.script.usage = output.usage;
14817
14929
  if (output.cost !== void 0) this.script.cost = output.cost;
14818
14930
  if (output.stoppedAtIterationLimit === true) this.script.stoppedAtIterationLimit = true;
14931
+ if (output.stoppedByDoomLoop === true) this.script.stoppedByDoomLoop = true;
14819
14932
  if (output.error !== void 0 && this.script.errorDetail === void 0) {
14820
14933
  this.script.errorDetail = {
14821
14934
  message: output.error.message,