@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.
- package/CHANGELOG.md +12 -0
- package/dist/a2a/index.cjs +151 -8
- package/dist/a2a/index.cjs.map +1 -1
- package/dist/a2a/index.js +151 -8
- package/dist/a2a/index.js.map +1 -1
- package/dist/{cron-CpxLdAXc.d.cts → cron-BxLSz1UH.d.cts} +1 -1
- package/dist/{cron-CL_9nfhQ.d.ts → cron-DcaoP7aW.d.ts} +1 -1
- package/dist/cron.cjs +144 -8
- package/dist/cron.cjs.map +1 -1
- package/dist/cron.d.cts +2 -2
- package/dist/cron.d.ts +2 -2
- package/dist/cron.js +144 -8
- package/dist/cron.js.map +1 -1
- package/dist/{errors-9yw4UQwX.d.cts → errors-Bart0ptP.d.cts} +1 -1
- package/dist/{errors-DFiY-NHK.d.ts → errors-DJuuubJK.d.ts} +1 -1
- package/dist/errors.d.cts +2 -2
- package/dist/eval.cjs +144 -8
- package/dist/eval.cjs.map +1 -1
- package/dist/eval.js +144 -8
- package/dist/eval.js.map +1 -1
- package/dist/index.cjs +144 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +144 -8
- package/dist/index.js.map +1 -1
- package/dist/internal/agent-loop/doom-loop-tracker.d.ts +22 -0
- package/dist/internal/agent-loop/loop-types.d.ts +6 -0
- package/dist/internal/llm/hermes-tool-extract.d.ts +5 -1
- package/dist/internal/llm/openai.d.ts +5 -1
- package/dist/{run-TMdc7gmo.d.cts → run-DXy_MVwz.d.cts} +29 -1
- package/dist/{run-TMdc7gmo.d.ts → run-DXy_MVwz.d.ts} +29 -1
- package/dist/types/run.d.ts +28 -0
- package/package.json +1 -1
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();
|
|
@@ -13540,11 +13650,16 @@ function sanitizeToolInput(input, options) {
|
|
|
13540
13650
|
// src/internal/llm/hermes-tool-extract.ts
|
|
13541
13651
|
var HERMES_BLOCK = /<function=\s*([^>\s]+)\s*>([\s\S]*?)<\/tool_call>/g;
|
|
13542
13652
|
var HERMES_PARAM = /<parameter=\s*([^>\s]+)\s*>([\s\S]*?)<\/parameter>/g;
|
|
13543
|
-
function extractHermesToolCalls(content, makeId) {
|
|
13653
|
+
function extractHermesToolCalls(content, makeId, allowedToolNames) {
|
|
13654
|
+
const isPromoted = (name) => name.length > 0 && (allowedToolNames === void 0 || allowedToolNames.has(name));
|
|
13544
13655
|
const toolCalls = [];
|
|
13656
|
+
const droppedNames = [];
|
|
13545
13657
|
for (const block of content.matchAll(HERMES_BLOCK)) {
|
|
13546
13658
|
const name = (block[1] ?? "").trim();
|
|
13547
|
-
if (name
|
|
13659
|
+
if (!isPromoted(name)) {
|
|
13660
|
+
if (name.length > 0 && allowedToolNames !== void 0) droppedNames.push(name);
|
|
13661
|
+
continue;
|
|
13662
|
+
}
|
|
13548
13663
|
toolCalls.push({
|
|
13549
13664
|
type: "tool_use",
|
|
13550
13665
|
id: makeId(),
|
|
@@ -13552,8 +13667,11 @@ function extractHermesToolCalls(content, makeId) {
|
|
|
13552
13667
|
input: parseHermesParams(block[2] ?? "")
|
|
13553
13668
|
});
|
|
13554
13669
|
}
|
|
13555
|
-
const residualText = toolCalls.length === 0 ? content : content.replace(
|
|
13556
|
-
|
|
13670
|
+
const residualText = toolCalls.length === 0 ? content : content.replace(
|
|
13671
|
+
HERMES_BLOCK,
|
|
13672
|
+
(full, rawName) => isPromoted((rawName ?? "").trim()) ? "" : full
|
|
13673
|
+
).trim();
|
|
13674
|
+
return { toolCalls, residualText, droppedNames };
|
|
13557
13675
|
}
|
|
13558
13676
|
function parseHermesParams(inner) {
|
|
13559
13677
|
const input = {};
|
|
@@ -13637,7 +13755,10 @@ var OpenAIClient = class {
|
|
|
13637
13755
|
}
|
|
13638
13756
|
const accumulator = new OpenAIStreamAccumulator(
|
|
13639
13757
|
this.options.extractToolCallsFromContent ?? false,
|
|
13640
|
-
providerId
|
|
13758
|
+
providerId,
|
|
13759
|
+
// R5: request-scoped allowlist — leaked recovery only promotes a block whose name is a tool the
|
|
13760
|
+
// model was actually given. Empty set (no tools) recovers nothing.
|
|
13761
|
+
new Set(request.tools?.map((tool) => tool.name) ?? [])
|
|
13641
13762
|
);
|
|
13642
13763
|
for await (const record of parseSseStream(response.body, signal)) {
|
|
13643
13764
|
if (record.data === "[DONE]") break;
|
|
@@ -13667,13 +13788,18 @@ var OpenAIStreamAccumulator = class {
|
|
|
13667
13788
|
/**
|
|
13668
13789
|
* @param extractFromContent opt-in leaked-dialect safe-parse (theokit#58). Default false.
|
|
13669
13790
|
* @param providerName provider id, used only to label the recovery log line.
|
|
13791
|
+
* @param allowedToolNames R5 request-scoped allowlist — built from `request.tools` at `stream()`;
|
|
13792
|
+
* leaked recovery in `finish()` only promotes a block whose name is in this set. `undefined`
|
|
13793
|
+
* (direct construction) recovers all (back-compat); an empty set recovers nothing.
|
|
13670
13794
|
*/
|
|
13671
|
-
constructor(extractFromContent = false, providerName = "openai") {
|
|
13795
|
+
constructor(extractFromContent = false, providerName = "openai", allowedToolNames) {
|
|
13672
13796
|
this.extractFromContent = extractFromContent;
|
|
13673
13797
|
this.providerName = providerName;
|
|
13798
|
+
this.allowedToolNames = allowedToolNames;
|
|
13674
13799
|
}
|
|
13675
13800
|
extractFromContent;
|
|
13676
13801
|
providerName;
|
|
13802
|
+
allowedToolNames;
|
|
13677
13803
|
text = "";
|
|
13678
13804
|
stopReason = "end_turn";
|
|
13679
13805
|
inputTokens;
|
|
@@ -13744,7 +13870,8 @@ var OpenAIStreamAccumulator = class {
|
|
|
13744
13870
|
if (this.extractFromContent && toolCalls.length === 0) {
|
|
13745
13871
|
const recovered = extractHermesToolCalls(
|
|
13746
13872
|
this.text,
|
|
13747
|
-
() => `hermes-${globalThis.crypto.randomUUID()}
|
|
13873
|
+
() => `hermes-${globalThis.crypto.randomUUID()}`,
|
|
13874
|
+
this.allowedToolNames
|
|
13748
13875
|
);
|
|
13749
13876
|
if (recovered.toolCalls.length > 0) {
|
|
13750
13877
|
toolCalls.push(...recovered.toolCalls);
|
|
@@ -13752,6 +13879,12 @@ var OpenAIStreamAccumulator = class {
|
|
|
13752
13879
|
stopReason = "tool_use";
|
|
13753
13880
|
process.stderr.write(
|
|
13754
13881
|
`[theokit-sdk] recovered ${recovered.toolCalls.length} leaked tool call(s) from assistant content (provider="${this.providerName}", names=${recovered.toolCalls.map((c) => c.name).join(",")})
|
|
13882
|
+
`
|
|
13883
|
+
);
|
|
13884
|
+
}
|
|
13885
|
+
if (recovered.droppedNames.length > 0) {
|
|
13886
|
+
process.stderr.write(
|
|
13887
|
+
`[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(",")})
|
|
13755
13888
|
`
|
|
13756
13889
|
);
|
|
13757
13890
|
}
|
|
@@ -14684,6 +14817,8 @@ function buildLoopInputs(options, runId, userText) {
|
|
|
14684
14817
|
// M1-2: per-send iteration ceiling (validated above). The loop reads
|
|
14685
14818
|
// inputs.maxIterations (default 8 when unset).
|
|
14686
14819
|
...maxIterations !== void 0 ? { maxIterations } : {},
|
|
14820
|
+
// Doom-loop guard config (default on; `false` disables, object tunes thresholds).
|
|
14821
|
+
...options.sendOptions.doomLoop !== void 0 ? { doomLoop: options.sendOptions.doomLoop } : {},
|
|
14687
14822
|
// D315-D317 — tool lifecycle hooks (cost tracking + audit + retry/alert)
|
|
14688
14823
|
...options.agentOptions.onToolStart !== void 0 ? { onToolStart: options.agentOptions.onToolStart } : {},
|
|
14689
14824
|
...options.agentOptions.onToolEnd !== void 0 ? { onToolEnd: options.agentOptions.onToolEnd } : {},
|
|
@@ -14816,6 +14951,7 @@ var RealLocalRun = class extends FixtureRunBase {
|
|
|
14816
14951
|
if (output.usage !== void 0) this.script.usage = output.usage;
|
|
14817
14952
|
if (output.cost !== void 0) this.script.cost = output.cost;
|
|
14818
14953
|
if (output.stoppedAtIterationLimit === true) this.script.stoppedAtIterationLimit = true;
|
|
14954
|
+
if (output.stoppedByDoomLoop === true) this.script.stoppedByDoomLoop = true;
|
|
14819
14955
|
if (output.error !== void 0 && this.script.errorDetail === void 0) {
|
|
14820
14956
|
this.script.errorDetail = {
|
|
14821
14957
|
message: output.error.message,
|