@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.15.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- bec2077: Make the leaked-dialect recovery **request-scoped (R5)**. The opt-in `extractToolCallsFromContent` recovery previously promoted ANY `<function=NAME>` block leaked into assistant text on an enabled route, so a code assistant printing a literal `<function=example>` in a fenced code block could be wrongly turned into a tool call. Recovery now gates on an exact, case-sensitive allowlist derived automatically from the current request's declared tools (`request.tools`): the per-route flag stays the coarse enable, and the allowlist is the precise false-positive guard. A request with no tools recovers nothing; a gated-out block keeps its text visible (it is not silently deleted). No public API change — the allowlist is derived from the tools you already pass. Mirrors openclaw's `@openclaw/tool-call-repair` allowlist.
|
|
8
|
+
|
|
9
|
+
## 2.15.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- d7057f2: Add a **doom-loop / no-progress guard** to the agent loop. The loop now detects when the model repeats IDENTICAL tool calls (same name + same canonical input) that make no progress — the qwen3-coder `read_file`/`not_found` failure mode where the model retries the same failing call and the run grinds to the iteration ceiling — and stops early with a typed `no_progress` terminal instead of hanging. A pure `DoomLoopTracker` (canonical key-sorted-JSON signature + a consecutive-identical counter) escalates from a one-time guidance nudge at a soft threshold to a hard stop; the hard stop surfaces on `RunResult.stoppedByDoomLoop` and, through the continuation driver, as `terminal: "no_progress"` (so the outer loop does not re-send). It complements — does not replace — the existing empty-round `no_progress` (a different failure mode: model stuck repeating vs model gone silent). On by default with generous thresholds (soft 3 / hard 5); tune or disable per send via `SendOptions.doomLoop` (`false` to disable, or `{ softThreshold, hardThreshold }` to tune). Dependency-free. Grounded in a SOTA study of cline's LoopDetectionTracker + opencode's doom-loop.
|
|
14
|
+
|
|
3
15
|
## 2.14.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/dist/a2a/index.cjs
CHANGED
|
@@ -2336,6 +2336,7 @@ function applyScriptMetrics(base, script) {
|
|
|
2336
2336
|
if (script.usage !== void 0) base.usage = script.usage;
|
|
2337
2337
|
if (script.cost !== void 0) base.cost = script.cost;
|
|
2338
2338
|
if (script.stoppedAtIterationLimit === true) base.stoppedAtIterationLimit = true;
|
|
2339
|
+
if (script.stoppedByDoomLoop === true) base.stoppedByDoomLoop = true;
|
|
2339
2340
|
}
|
|
2340
2341
|
var FixtureRunBase;
|
|
2341
2342
|
var init_fixture_run_base = __esm({
|
|
@@ -6812,6 +6813,98 @@ var init_budget_gate = __esm({
|
|
|
6812
6813
|
}
|
|
6813
6814
|
});
|
|
6814
6815
|
|
|
6816
|
+
// src/internal/agent-loop/doom-loop-tracker.ts
|
|
6817
|
+
function createDoomLoopTracker(option) {
|
|
6818
|
+
if (option === false) return void 0;
|
|
6819
|
+
return new DoomLoopTracker(option);
|
|
6820
|
+
}
|
|
6821
|
+
function assertValidThresholds(soft, hard) {
|
|
6822
|
+
for (const [label, value] of [
|
|
6823
|
+
["softThreshold", soft],
|
|
6824
|
+
["hardThreshold", hard]
|
|
6825
|
+
]) {
|
|
6826
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
6827
|
+
throw new ConfigurationError(
|
|
6828
|
+
`doomLoop.${label} must be a positive integer (received ${value}).`,
|
|
6829
|
+
{ code: "invalid_doom_loop_threshold" }
|
|
6830
|
+
);
|
|
6831
|
+
}
|
|
6832
|
+
}
|
|
6833
|
+
}
|
|
6834
|
+
function sortKeys(value) {
|
|
6835
|
+
if (value === null || typeof value !== "object") return value;
|
|
6836
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
6837
|
+
const out = {};
|
|
6838
|
+
for (const key of Object.keys(value).sort()) {
|
|
6839
|
+
out[key] = sortKeys(value[key]);
|
|
6840
|
+
}
|
|
6841
|
+
return out;
|
|
6842
|
+
}
|
|
6843
|
+
function signatureOf(call) {
|
|
6844
|
+
const { input } = call;
|
|
6845
|
+
let inputSig;
|
|
6846
|
+
if (input === null || input === void 0) inputSig = "null";
|
|
6847
|
+
else if (typeof input !== "object") inputSig = String(input);
|
|
6848
|
+
else {
|
|
6849
|
+
try {
|
|
6850
|
+
inputSig = JSON.stringify(sortKeys(input)) ?? "null";
|
|
6851
|
+
} catch {
|
|
6852
|
+
inputSig = String(input);
|
|
6853
|
+
}
|
|
6854
|
+
}
|
|
6855
|
+
return `${call.name}\0${inputSig}`;
|
|
6856
|
+
}
|
|
6857
|
+
function firstDoomLoopVerdict(tracker, calls) {
|
|
6858
|
+
let escalation = { kind: "ok" };
|
|
6859
|
+
for (const call of calls) {
|
|
6860
|
+
const v = tracker.inspect(call);
|
|
6861
|
+
if (v.kind === "hard") return v;
|
|
6862
|
+
if (v.kind === "soft" && escalation.kind === "ok") escalation = v;
|
|
6863
|
+
}
|
|
6864
|
+
return escalation;
|
|
6865
|
+
}
|
|
6866
|
+
var DEFAULT_CONFIG, DoomLoopTracker;
|
|
6867
|
+
var init_doom_loop_tracker = __esm({
|
|
6868
|
+
"src/internal/agent-loop/doom-loop-tracker.ts"() {
|
|
6869
|
+
init_errors();
|
|
6870
|
+
DEFAULT_CONFIG = { softThreshold: 3, hardThreshold: 5 };
|
|
6871
|
+
DoomLoopTracker = class {
|
|
6872
|
+
#config;
|
|
6873
|
+
#lastSignature = "";
|
|
6874
|
+
#count = 0;
|
|
6875
|
+
constructor(config) {
|
|
6876
|
+
const softThreshold = config?.softThreshold ?? DEFAULT_CONFIG.softThreshold;
|
|
6877
|
+
const hardThreshold = config?.hardThreshold ?? DEFAULT_CONFIG.hardThreshold;
|
|
6878
|
+
assertValidThresholds(softThreshold, hardThreshold);
|
|
6879
|
+
this.#config = { softThreshold, hardThreshold };
|
|
6880
|
+
}
|
|
6881
|
+
inspect(call) {
|
|
6882
|
+
const signature = signatureOf(call);
|
|
6883
|
+
this.#count = signature === this.#lastSignature ? this.#count + 1 : 1;
|
|
6884
|
+
this.#lastSignature = signature;
|
|
6885
|
+
const count = this.#count;
|
|
6886
|
+
if (count >= this.#config.hardThreshold) {
|
|
6887
|
+
return {
|
|
6888
|
+
kind: "hard",
|
|
6889
|
+
message: `Detected ${count} consecutive identical calls to \`${call.name}\`; stopping to avoid a loop.`
|
|
6890
|
+
};
|
|
6891
|
+
}
|
|
6892
|
+
if (count === this.#config.softThreshold) {
|
|
6893
|
+
return {
|
|
6894
|
+
kind: "soft",
|
|
6895
|
+
message: `Detected ${count} consecutive identical calls to \`${call.name}\`; try a different approach.`
|
|
6896
|
+
};
|
|
6897
|
+
}
|
|
6898
|
+
return { kind: "ok" };
|
|
6899
|
+
}
|
|
6900
|
+
reset() {
|
|
6901
|
+
this.#lastSignature = "";
|
|
6902
|
+
this.#count = 0;
|
|
6903
|
+
}
|
|
6904
|
+
};
|
|
6905
|
+
}
|
|
6906
|
+
});
|
|
6907
|
+
|
|
6815
6908
|
// src/internal/budget/usage-accumulator.ts
|
|
6816
6909
|
var UsageAccumulator;
|
|
6817
6910
|
var init_usage_accumulator = __esm({
|
|
@@ -7001,6 +7094,7 @@ async function initLoopContext(inputs) {
|
|
|
7001
7094
|
tools,
|
|
7002
7095
|
finalText: "",
|
|
7003
7096
|
finalStatus: "finished",
|
|
7097
|
+
doomLoop: createDoomLoopTracker(inputs.doomLoop),
|
|
7004
7098
|
usage: new UsageAccumulator(),
|
|
7005
7099
|
nudgeAttempts: 0,
|
|
7006
7100
|
stopFeedbackAttempts: 0,
|
|
@@ -7053,6 +7147,7 @@ function sanitize(name) {
|
|
|
7053
7147
|
var init_loop_context_init = __esm({
|
|
7054
7148
|
"src/internal/agent-loop/loop-context-init.ts"() {
|
|
7055
7149
|
init_usage_accumulator();
|
|
7150
|
+
init_doom_loop_tracker();
|
|
7056
7151
|
init_message_builders();
|
|
7057
7152
|
}
|
|
7058
7153
|
});
|
|
@@ -8165,6 +8260,7 @@ async function runAgentLoop(inputs) {
|
|
|
8165
8260
|
ctx.finalStatus = "error";
|
|
8166
8261
|
}
|
|
8167
8262
|
sendSpan?.setAttribute("status", ctx.finalStatus);
|
|
8263
|
+
if (ctx.stoppedByDoomLoop === true) sendSpan?.setAttribute("stoppedByDoomLoop", true);
|
|
8168
8264
|
if (inputs.telemetry?.includeContent === true && ctx.finalText.length > 0) {
|
|
8169
8265
|
sendSpan?.addEvent("response", { content: ctx.finalText });
|
|
8170
8266
|
}
|
|
@@ -8191,7 +8287,8 @@ async function runAgentLoop(inputs) {
|
|
|
8191
8287
|
...usage !== void 0 ? { usage } : {},
|
|
8192
8288
|
...cost !== void 0 ? { cost } : {},
|
|
8193
8289
|
...ctx.error !== void 0 ? { error: ctx.error } : {},
|
|
8194
|
-
...ctx.stoppedAtIterationLimit === true ? { stoppedAtIterationLimit: true } : {}
|
|
8290
|
+
...ctx.stoppedAtIterationLimit === true ? { stoppedAtIterationLimit: true } : {},
|
|
8291
|
+
...ctx.stoppedByDoomLoop === true ? { stoppedByDoomLoop: true } : {}
|
|
8195
8292
|
};
|
|
8196
8293
|
} finally {
|
|
8197
8294
|
if (ctxRef !== void 0 && ctxRef.memoryProviderHandle !== void 0 && inputs.memoryProvider !== void 0) {
|
|
@@ -8350,8 +8447,26 @@ async function continueOrTerminate(inputs, ctx, llmOutput) {
|
|
|
8350
8447
|
}
|
|
8351
8448
|
}
|
|
8352
8449
|
pushToolConversationSteps(ctx, llmOutput.toolCalls, toolResults);
|
|
8450
|
+
if (await inspectDoomLoop(inputs, ctx, llmOutput.toolCalls) === "stop") return "done";
|
|
8353
8451
|
return handleToolErrorContinuation(inputs, ctx, toolResults);
|
|
8354
8452
|
}
|
|
8453
|
+
async function inspectDoomLoop(inputs, ctx, toolCalls) {
|
|
8454
|
+
if (ctx.doomLoop === void 0) return "continue";
|
|
8455
|
+
const verdict = firstDoomLoopVerdict(ctx.doomLoop, toolCalls);
|
|
8456
|
+
if (verdict.kind === "hard") {
|
|
8457
|
+
ctx.stoppedByDoomLoop = true;
|
|
8458
|
+
await emitAssistantTextStep(
|
|
8459
|
+
inputs,
|
|
8460
|
+
ctx,
|
|
8461
|
+
verdict.message ?? "Stopped: repeated identical tool calls made no progress."
|
|
8462
|
+
);
|
|
8463
|
+
return "stop";
|
|
8464
|
+
}
|
|
8465
|
+
if (verdict.kind === "soft") {
|
|
8466
|
+
ctx.messages.push({ role: "user", content: [{ type: "text", text: verdict.message ?? "" }] });
|
|
8467
|
+
}
|
|
8468
|
+
return "continue";
|
|
8469
|
+
}
|
|
8355
8470
|
var MAX_NUDGE_ATTEMPTS, MAX_STOP_FEEDBACK_ATTEMPTS;
|
|
8356
8471
|
var init_loop = __esm({
|
|
8357
8472
|
"src/internal/agent-loop/loop.ts"() {
|
|
@@ -8359,6 +8474,7 @@ var init_loop = __esm({
|
|
|
8359
8474
|
init_safe_call();
|
|
8360
8475
|
init_validate_response();
|
|
8361
8476
|
init_budget_gate();
|
|
8477
|
+
init_doom_loop_tracker();
|
|
8362
8478
|
init_loop_context_init();
|
|
8363
8479
|
init_loop_llm_stream();
|
|
8364
8480
|
init_message_builders();
|
|
@@ -10216,11 +10332,16 @@ var init_sanitize_tool_input = __esm({
|
|
|
10216
10332
|
});
|
|
10217
10333
|
|
|
10218
10334
|
// src/internal/llm/hermes-tool-extract.ts
|
|
10219
|
-
function extractHermesToolCalls(content, makeId) {
|
|
10335
|
+
function extractHermesToolCalls(content, makeId, allowedToolNames) {
|
|
10336
|
+
const isPromoted = (name) => name.length > 0 && (allowedToolNames === void 0 || allowedToolNames.has(name));
|
|
10220
10337
|
const toolCalls = [];
|
|
10338
|
+
const droppedNames = [];
|
|
10221
10339
|
for (const block of content.matchAll(HERMES_BLOCK)) {
|
|
10222
10340
|
const name = (block[1] ?? "").trim();
|
|
10223
|
-
if (name
|
|
10341
|
+
if (!isPromoted(name)) {
|
|
10342
|
+
if (name.length > 0 && allowedToolNames !== void 0) droppedNames.push(name);
|
|
10343
|
+
continue;
|
|
10344
|
+
}
|
|
10224
10345
|
toolCalls.push({
|
|
10225
10346
|
type: "tool_use",
|
|
10226
10347
|
id: makeId(),
|
|
@@ -10228,8 +10349,11 @@ function extractHermesToolCalls(content, makeId) {
|
|
|
10228
10349
|
input: parseHermesParams(block[2] ?? "")
|
|
10229
10350
|
});
|
|
10230
10351
|
}
|
|
10231
|
-
const residualText = toolCalls.length === 0 ? content : content.replace(
|
|
10232
|
-
|
|
10352
|
+
const residualText = toolCalls.length === 0 ? content : content.replace(
|
|
10353
|
+
HERMES_BLOCK,
|
|
10354
|
+
(full, rawName) => isPromoted((rawName ?? "").trim()) ? "" : full
|
|
10355
|
+
).trim();
|
|
10356
|
+
return { toolCalls, residualText, droppedNames };
|
|
10233
10357
|
}
|
|
10234
10358
|
function parseHermesParams(inner) {
|
|
10235
10359
|
const input = {};
|
|
@@ -10434,7 +10558,10 @@ var init_openai2 = __esm({
|
|
|
10434
10558
|
}
|
|
10435
10559
|
const accumulator = new OpenAIStreamAccumulator(
|
|
10436
10560
|
this.options.extractToolCallsFromContent ?? false,
|
|
10437
|
-
providerId
|
|
10561
|
+
providerId,
|
|
10562
|
+
// R5: request-scoped allowlist — leaked recovery only promotes a block whose name is a tool the
|
|
10563
|
+
// model was actually given. Empty set (no tools) recovers nothing.
|
|
10564
|
+
new Set(request.tools?.map((tool) => tool.name) ?? [])
|
|
10438
10565
|
);
|
|
10439
10566
|
for await (const record of parseSseStream(response.body, signal)) {
|
|
10440
10567
|
if (record.data === "[DONE]") break;
|
|
@@ -10464,13 +10591,18 @@ var init_openai2 = __esm({
|
|
|
10464
10591
|
/**
|
|
10465
10592
|
* @param extractFromContent opt-in leaked-dialect safe-parse (theokit#58). Default false.
|
|
10466
10593
|
* @param providerName provider id, used only to label the recovery log line.
|
|
10594
|
+
* @param allowedToolNames R5 request-scoped allowlist — built from `request.tools` at `stream()`;
|
|
10595
|
+
* leaked recovery in `finish()` only promotes a block whose name is in this set. `undefined`
|
|
10596
|
+
* (direct construction) recovers all (back-compat); an empty set recovers nothing.
|
|
10467
10597
|
*/
|
|
10468
|
-
constructor(extractFromContent = false, providerName = "openai") {
|
|
10598
|
+
constructor(extractFromContent = false, providerName = "openai", allowedToolNames) {
|
|
10469
10599
|
this.extractFromContent = extractFromContent;
|
|
10470
10600
|
this.providerName = providerName;
|
|
10601
|
+
this.allowedToolNames = allowedToolNames;
|
|
10471
10602
|
}
|
|
10472
10603
|
extractFromContent;
|
|
10473
10604
|
providerName;
|
|
10605
|
+
allowedToolNames;
|
|
10474
10606
|
text = "";
|
|
10475
10607
|
stopReason = "end_turn";
|
|
10476
10608
|
inputTokens;
|
|
@@ -10541,7 +10673,8 @@ var init_openai2 = __esm({
|
|
|
10541
10673
|
if (this.extractFromContent && toolCalls.length === 0) {
|
|
10542
10674
|
const recovered = extractHermesToolCalls(
|
|
10543
10675
|
this.text,
|
|
10544
|
-
() => `hermes-${globalThis.crypto.randomUUID()}
|
|
10676
|
+
() => `hermes-${globalThis.crypto.randomUUID()}`,
|
|
10677
|
+
this.allowedToolNames
|
|
10545
10678
|
);
|
|
10546
10679
|
if (recovered.toolCalls.length > 0) {
|
|
10547
10680
|
toolCalls.push(...recovered.toolCalls);
|
|
@@ -10549,6 +10682,12 @@ var init_openai2 = __esm({
|
|
|
10549
10682
|
stopReason = "tool_use";
|
|
10550
10683
|
process.stderr.write(
|
|
10551
10684
|
`[theokit-sdk] recovered ${recovered.toolCalls.length} leaked tool call(s) from assistant content (provider="${this.providerName}", names=${recovered.toolCalls.map((c) => c.name).join(",")})
|
|
10685
|
+
`
|
|
10686
|
+
);
|
|
10687
|
+
}
|
|
10688
|
+
if (recovered.droppedNames.length > 0) {
|
|
10689
|
+
process.stderr.write(
|
|
10690
|
+
`[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(",")})
|
|
10552
10691
|
`
|
|
10553
10692
|
);
|
|
10554
10693
|
}
|
|
@@ -11443,6 +11582,8 @@ function buildLoopInputs(options, runId, userText) {
|
|
|
11443
11582
|
// M1-2: per-send iteration ceiling (validated above). The loop reads
|
|
11444
11583
|
// inputs.maxIterations (default 8 when unset).
|
|
11445
11584
|
...maxIterations !== void 0 ? { maxIterations } : {},
|
|
11585
|
+
// Doom-loop guard config (default on; `false` disables, object tunes thresholds).
|
|
11586
|
+
...options.sendOptions.doomLoop !== void 0 ? { doomLoop: options.sendOptions.doomLoop } : {},
|
|
11446
11587
|
// D315-D317 — tool lifecycle hooks (cost tracking + audit + retry/alert)
|
|
11447
11588
|
...options.agentOptions.onToolStart !== void 0 ? { onToolStart: options.agentOptions.onToolStart } : {},
|
|
11448
11589
|
...options.agentOptions.onToolEnd !== void 0 ? { onToolEnd: options.agentOptions.onToolEnd } : {},
|
|
@@ -11591,6 +11732,7 @@ var init_real_local_run = __esm({
|
|
|
11591
11732
|
if (output.usage !== void 0) this.script.usage = output.usage;
|
|
11592
11733
|
if (output.cost !== void 0) this.script.cost = output.cost;
|
|
11593
11734
|
if (output.stoppedAtIterationLimit === true) this.script.stoppedAtIterationLimit = true;
|
|
11735
|
+
if (output.stoppedByDoomLoop === true) this.script.stoppedByDoomLoop = true;
|
|
11594
11736
|
if (output.error !== void 0 && this.script.errorDetail === void 0) {
|
|
11595
11737
|
this.script.errorDetail = {
|
|
11596
11738
|
message: output.error.message,
|
|
@@ -14582,6 +14724,7 @@ function isEmptyRound(result) {
|
|
|
14582
14724
|
return (result.result ?? "").trim() === "";
|
|
14583
14725
|
}
|
|
14584
14726
|
function classifyRound(result, round, maxRounds, emptyStreak) {
|
|
14727
|
+
if (result.stoppedByDoomLoop === true) return "no_progress";
|
|
14585
14728
|
if (result.stoppedAtIterationLimit !== true) return "done";
|
|
14586
14729
|
if (isEmptyRound(result) && emptyStreak >= 1) return "no_progress";
|
|
14587
14730
|
if (round >= maxRounds) return "step_limit";
|