@mindstudio-ai/remy 0.1.329 → 0.1.331
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/headless.js +250 -25
- package/dist/index.js +259 -26
- package/package.json +1 -1
package/dist/headless.js
CHANGED
|
@@ -100,6 +100,87 @@ function resolveConfig(flags) {
|
|
|
100
100
|
return { apiKey, baseUrl: baseUrl2, appId };
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// src/loopGuard.ts
|
|
104
|
+
var PARAGRAPH_REPEAT_THRESHOLD = 6;
|
|
105
|
+
var MIN_PARAGRAPH_CHARS = 80;
|
|
106
|
+
var CHUNK_SIZE = 60;
|
|
107
|
+
var CHUNK_WINDOW = 5e3;
|
|
108
|
+
var CHUNK_REPEAT_THRESHOLD = 10;
|
|
109
|
+
var LONE_HEADER = /^\*{1,3}[^*].*\*{1,3}$/;
|
|
110
|
+
function normalizeParagraph(seg) {
|
|
111
|
+
const trimmed = seg.trim();
|
|
112
|
+
if (!trimmed || LONE_HEADER.test(trimmed)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const key = trimmed.toLowerCase().replace(/\s+/g, " ");
|
|
116
|
+
if (key.length < MIN_PARAGRAPH_CHARS) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return key;
|
|
120
|
+
}
|
|
121
|
+
var RepetitionDetector = class {
|
|
122
|
+
// Paragraph detector: incomplete tail awaiting its closing blank line, plus
|
|
123
|
+
// occurrence counts of completed, normalized paragraphs.
|
|
124
|
+
paragraphBuffer = "";
|
|
125
|
+
paragraphCounts = /* @__PURE__ */ new Map();
|
|
126
|
+
// Rolling-chunk detector: the trailing window of raw fed text.
|
|
127
|
+
window = "";
|
|
128
|
+
fired = false;
|
|
129
|
+
/**
|
|
130
|
+
* Feed the next streamed text fragment. Returns a signal the first time a
|
|
131
|
+
* loop is recognized, then null forever after (the caller aborts on the
|
|
132
|
+
* first signal; further feeds are harmless no-ops).
|
|
133
|
+
*/
|
|
134
|
+
feed(text) {
|
|
135
|
+
if (this.fired || !text) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return this.feedParagraphs(text) ?? this.feedChunks(text);
|
|
139
|
+
}
|
|
140
|
+
feedParagraphs(text) {
|
|
141
|
+
this.paragraphBuffer += text;
|
|
142
|
+
const segments = this.paragraphBuffer.split(/\n\s*\n/);
|
|
143
|
+
this.paragraphBuffer = segments.pop() ?? "";
|
|
144
|
+
for (const seg of segments) {
|
|
145
|
+
const key = normalizeParagraph(seg);
|
|
146
|
+
if (!key) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const count = (this.paragraphCounts.get(key) ?? 0) + 1;
|
|
150
|
+
this.paragraphCounts.set(key, count);
|
|
151
|
+
if (count >= PARAGRAPH_REPEAT_THRESHOLD) {
|
|
152
|
+
this.fired = true;
|
|
153
|
+
return {
|
|
154
|
+
kind: "paragraph",
|
|
155
|
+
repeats: count,
|
|
156
|
+
sample: seg.trim().slice(0, 160)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
feedChunks(text) {
|
|
163
|
+
this.window = (this.window + text).slice(-CHUNK_WINDOW);
|
|
164
|
+
if (this.window.length < CHUNK_SIZE * CHUNK_REPEAT_THRESHOLD) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const tail = this.window.slice(-CHUNK_SIZE);
|
|
168
|
+
if (!tail.trim()) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const occurrences = this.window.split(tail).length - 1;
|
|
172
|
+
if (occurrences >= CHUNK_REPEAT_THRESHOLD) {
|
|
173
|
+
this.fired = true;
|
|
174
|
+
return {
|
|
175
|
+
kind: "chunk",
|
|
176
|
+
repeats: occurrences,
|
|
177
|
+
sample: tail.replace(/\s+/g, " ").trim().slice(0, 160)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
103
184
|
// src/api.ts
|
|
104
185
|
var log2 = createLogger("api");
|
|
105
186
|
async function* streamChat(params) {
|
|
@@ -311,17 +392,51 @@ async function* streamChatWithRetry(params, options) {
|
|
|
311
392
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
312
393
|
const buffer = [];
|
|
313
394
|
let retryableFailure = false;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
395
|
+
const detector = options?.detectRepetition ? new RepetitionDetector() : null;
|
|
396
|
+
const streamAbort = new AbortController();
|
|
397
|
+
const onCallerAbort = () => streamAbort.abort();
|
|
398
|
+
if (params.signal) {
|
|
399
|
+
if (params.signal.aborted) {
|
|
400
|
+
streamAbort.abort();
|
|
401
|
+
} else {
|
|
402
|
+
params.signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
for await (const event of streamChat({
|
|
407
|
+
...params,
|
|
408
|
+
signal: streamAbort.signal
|
|
409
|
+
})) {
|
|
410
|
+
if (event.type === "error") {
|
|
411
|
+
if (isRetryableError(event.error, event.code) && attempt < MAX_RETRIES - 1) {
|
|
412
|
+
options?.onRetry?.(attempt, event.error);
|
|
413
|
+
retryableFailure = true;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
yield event;
|
|
417
|
+
return;
|
|
320
418
|
}
|
|
321
|
-
|
|
322
|
-
|
|
419
|
+
if (detector && (event.type === "text" || event.type === "thinking")) {
|
|
420
|
+
const loop = detector.feed(event.text);
|
|
421
|
+
if (loop) {
|
|
422
|
+
log2.warn("Repetition loop detected \u2014 aborting call", {
|
|
423
|
+
requestId: params.requestId,
|
|
424
|
+
kind: loop.kind,
|
|
425
|
+
repeats: loop.repeats
|
|
426
|
+
});
|
|
427
|
+
streamAbort.abort();
|
|
428
|
+
yield {
|
|
429
|
+
type: "error",
|
|
430
|
+
error: "Response stopped: the model was repeating its reasoning without making progress.",
|
|
431
|
+
code: "repetition_loop"
|
|
432
|
+
};
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
buffer.push(event);
|
|
323
437
|
}
|
|
324
|
-
|
|
438
|
+
} finally {
|
|
439
|
+
params.signal?.removeEventListener("abort", onCallerAbort);
|
|
325
440
|
}
|
|
326
441
|
if (retryableFailure) {
|
|
327
442
|
if (params.signal?.aborted) {
|
|
@@ -543,6 +658,7 @@ var TEXT_MODELS = {
|
|
|
543
658
|
"kimi-k3": { forceCompactAt: 85e4 },
|
|
544
659
|
"deepseek-v4-flash-0731": { forceCompactAt: 85e4 },
|
|
545
660
|
"deepseek-v4-pro": { forceCompactAt: 85e4 },
|
|
661
|
+
"deepseek-v4.1-flash": { forceCompactAt: 85e4 },
|
|
546
662
|
"qwen3.8-2.4t-a95b-deepinfra": { forceCompactAt: 2e5 },
|
|
547
663
|
// 262K window
|
|
548
664
|
"qwen3.8-27b-deepinfra": { forceCompactAt: 2e5 },
|
|
@@ -3031,6 +3147,13 @@ var queryDatabaseTool = {
|
|
|
3031
3147
|
// src/usageLedger.ts
|
|
3032
3148
|
import fs16 from "fs";
|
|
3033
3149
|
var LEDGER_FILE = ".logs/usage.ndjson";
|
|
3150
|
+
function thinkingTokensFromBilling(billingEvents, outputTokens) {
|
|
3151
|
+
if (!billingEvents?.length) {
|
|
3152
|
+
return 0;
|
|
3153
|
+
}
|
|
3154
|
+
const billedResponseUnits = billingEvents.filter((e) => e.eventType.endsWith("-response")).reduce((sum, e) => sum + e.numUnits, 0);
|
|
3155
|
+
return Math.max(0, billedResponseUnits - outputTokens);
|
|
3156
|
+
}
|
|
3034
3157
|
var fd = null;
|
|
3035
3158
|
function nanoToDollars(nano) {
|
|
3036
3159
|
return typeof nano === "number" ? nano / 1e9 : void 0;
|
|
@@ -8961,6 +9084,13 @@ function getActionChain(startName) {
|
|
|
8961
9084
|
}
|
|
8962
9085
|
|
|
8963
9086
|
// src/errors.ts
|
|
9087
|
+
var OVERFLOW_PATTERN = /input token count.*exceeds|exceeds the maximum number of tokens|prompt is too long|context[_ ]length[_ ]exceeded|maximum context length|HTTP 413|payload too large|request entity too large/i;
|
|
9088
|
+
function isContextOverflowError(message, code) {
|
|
9089
|
+
if (code === "context_overflow") {
|
|
9090
|
+
return true;
|
|
9091
|
+
}
|
|
9092
|
+
return OVERFLOW_PATTERN.test(message);
|
|
9093
|
+
}
|
|
8964
9094
|
var patterns = [
|
|
8965
9095
|
[
|
|
8966
9096
|
/Network error/i,
|
|
@@ -8980,6 +9110,14 @@ var patterns = [
|
|
|
8980
9110
|
"The AI service is temporarily unavailable. Please try again."
|
|
8981
9111
|
],
|
|
8982
9112
|
[/Stream stalled/i, "The connection was interrupted. Please try again."],
|
|
9113
|
+
[
|
|
9114
|
+
// A too-large-context failure that survived the automatic compact-and-retry
|
|
9115
|
+
// (or came from an older server). Kept ahead of the generic fallback so a
|
|
9116
|
+
// raw provider string (e.g. Gemini's INVALID_ARGUMENT JSON) never reaches
|
|
9117
|
+
// the user.
|
|
9118
|
+
OVERFLOW_PATTERN,
|
|
9119
|
+
"This conversation outgrew the model's context window. Remy compacted it and tried again; if you keep seeing this, run /compact or start a new conversation."
|
|
9120
|
+
],
|
|
8983
9121
|
[
|
|
8984
9122
|
/content filter|Output blocked/i,
|
|
8985
9123
|
"The AI model's content moderation filter blocked this response. These are usually false positives, we apologize for the interruption. Rephrasing your request typically fixes this."
|
|
@@ -9284,8 +9422,33 @@ async function runTurn(params) {
|
|
|
9284
9422
|
let lastCallInputTokens = 0;
|
|
9285
9423
|
let lastCallCacheCreation = 0;
|
|
9286
9424
|
let lastCallCacheRead = 0;
|
|
9287
|
-
let
|
|
9288
|
-
const
|
|
9425
|
+
let recoveries = 0;
|
|
9426
|
+
const MAX_RECOVERIES = 2;
|
|
9427
|
+
let midTurnCompactions = 0;
|
|
9428
|
+
const MAX_MID_TURN_COMPACTIONS = 2;
|
|
9429
|
+
let overflowRecovered = false;
|
|
9430
|
+
const compactNow = async (reason) => {
|
|
9431
|
+
log15.warn("Compacting mid-turn", {
|
|
9432
|
+
requestId,
|
|
9433
|
+
reason,
|
|
9434
|
+
lastCallInputTokens
|
|
9435
|
+
});
|
|
9436
|
+
onEvent({ type: "status", message: "Compacting the conversation\u2026" });
|
|
9437
|
+
try {
|
|
9438
|
+
await triggerCompaction(state, apiConfig, {
|
|
9439
|
+
blocking: true,
|
|
9440
|
+
requestId,
|
|
9441
|
+
model,
|
|
9442
|
+
origin: "gate"
|
|
9443
|
+
});
|
|
9444
|
+
applyPendingSummaries(state);
|
|
9445
|
+
} catch (err) {
|
|
9446
|
+
log15.error("Mid-turn compaction failed", {
|
|
9447
|
+
requestId,
|
|
9448
|
+
error: err?.message ?? String(err)
|
|
9449
|
+
});
|
|
9450
|
+
}
|
|
9451
|
+
};
|
|
9289
9452
|
const statusWatcher = isFirstMessage ? { stop() {
|
|
9290
9453
|
}, pause() {
|
|
9291
9454
|
}, resume() {
|
|
@@ -9432,6 +9595,7 @@ async function runTurn(params) {
|
|
|
9432
9595
|
onEvent({ type: "tool_input_delta", id, name, result: content });
|
|
9433
9596
|
}
|
|
9434
9597
|
}
|
|
9598
|
+
let streamError = null;
|
|
9435
9599
|
try {
|
|
9436
9600
|
for await (const event of streamChatWithRetry(
|
|
9437
9601
|
{
|
|
@@ -9450,9 +9614,13 @@ async function runTurn(params) {
|
|
|
9450
9614
|
onRetry: (attempt) => {
|
|
9451
9615
|
onEvent({
|
|
9452
9616
|
type: "status",
|
|
9453
|
-
message: `Lost connection, retrying (attempt ${attempt + 2} of
|
|
9617
|
+
message: `Lost connection, retrying (attempt ${attempt + 2} of ${MAX_RETRIES})`
|
|
9454
9618
|
});
|
|
9455
|
-
}
|
|
9619
|
+
},
|
|
9620
|
+
// Watch the streamed reasoning/text for a runaway repetition loop
|
|
9621
|
+
// and abort the in-flight call early (RPT-1225). Surfaces as a
|
|
9622
|
+
// `repetition_loop` error handled below.
|
|
9623
|
+
detectRepetition: true
|
|
9456
9624
|
}
|
|
9457
9625
|
)) {
|
|
9458
9626
|
if (signal?.aborted) {
|
|
@@ -9476,7 +9644,7 @@ async function runTurn(params) {
|
|
|
9476
9644
|
emitTextBlockSnapshot2(false);
|
|
9477
9645
|
break;
|
|
9478
9646
|
}
|
|
9479
|
-
case "thinking":
|
|
9647
|
+
case "thinking": {
|
|
9480
9648
|
if (event.text === "") {
|
|
9481
9649
|
thinkingBlockStartTimes.push(event.ts);
|
|
9482
9650
|
if (textBlockOpen) {
|
|
@@ -9486,6 +9654,7 @@ async function runTurn(params) {
|
|
|
9486
9654
|
}
|
|
9487
9655
|
onEvent({ type: "thinking", text: event.text });
|
|
9488
9656
|
break;
|
|
9657
|
+
}
|
|
9489
9658
|
case "thinking_complete": {
|
|
9490
9659
|
const startedAt = thinkingBlockStartTimes[thinkingCompleteCount] ?? event.ts;
|
|
9491
9660
|
contentBlocks.push({
|
|
@@ -9583,6 +9752,10 @@ async function runTurn(params) {
|
|
|
9583
9752
|
outputTokens: event.usage.outputTokens,
|
|
9584
9753
|
cacheCreationTokens: event.usage.cacheCreationTokens,
|
|
9585
9754
|
cacheReadTokens: event.usage.cacheReadTokens,
|
|
9755
|
+
thinkingTokens: thinkingTokensFromBilling(
|
|
9756
|
+
event.billingEvents,
|
|
9757
|
+
event.usage.outputTokens
|
|
9758
|
+
) || void 0,
|
|
9586
9759
|
cost: nanoToDollars(event.cost),
|
|
9587
9760
|
billingEvents: event.billingEvents,
|
|
9588
9761
|
durationMs: Date.now() - iterStart,
|
|
@@ -9592,13 +9765,11 @@ async function runTurn(params) {
|
|
|
9592
9765
|
});
|
|
9593
9766
|
break;
|
|
9594
9767
|
case "error":
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
});
|
|
9601
|
-
return;
|
|
9768
|
+
streamError = { error: event.error, code: event.code };
|
|
9769
|
+
break;
|
|
9770
|
+
}
|
|
9771
|
+
if (streamError) {
|
|
9772
|
+
break;
|
|
9602
9773
|
}
|
|
9603
9774
|
}
|
|
9604
9775
|
} catch (err) {
|
|
@@ -9643,6 +9814,51 @@ async function runTurn(params) {
|
|
|
9643
9814
|
saveSession(state);
|
|
9644
9815
|
return;
|
|
9645
9816
|
}
|
|
9817
|
+
if (streamError) {
|
|
9818
|
+
const { error, code } = streamError;
|
|
9819
|
+
if (code === "repetition_loop") {
|
|
9820
|
+
if (recoveries < MAX_RECOVERIES && !signal?.aborted) {
|
|
9821
|
+
recoveries++;
|
|
9822
|
+
log15.warn("Repetition loop \u2014 nudging model to continue", {
|
|
9823
|
+
requestId,
|
|
9824
|
+
attempt: recoveries
|
|
9825
|
+
});
|
|
9826
|
+
const nudge = "Your previous response was stopped because it kept repeating the same reasoning without making progress. Do not deliberate further \u2014 take the next concrete action now: make the tool call or give the answer.";
|
|
9827
|
+
state.messages.push({
|
|
9828
|
+
role: "user",
|
|
9829
|
+
content: nudge,
|
|
9830
|
+
hidden: true
|
|
9831
|
+
});
|
|
9832
|
+
onEvent({ type: "user_message", text: nudge, hidden: true });
|
|
9833
|
+
continue;
|
|
9834
|
+
}
|
|
9835
|
+
statusWatcher.stop();
|
|
9836
|
+
saveSession(state);
|
|
9837
|
+
log15.warn("Repetition loop over recovery cap \u2014 ending turn", {
|
|
9838
|
+
requestId
|
|
9839
|
+
});
|
|
9840
|
+
onEvent({
|
|
9841
|
+
type: "error",
|
|
9842
|
+
error: "The model kept repeating itself without making progress, so Remy stopped the turn to avoid runaway cost. Try again, or rephrase your request.",
|
|
9843
|
+
lastCallInputTokens
|
|
9844
|
+
});
|
|
9845
|
+
return;
|
|
9846
|
+
}
|
|
9847
|
+
if (isContextOverflowError(error, code) && !overflowRecovered && !signal?.aborted) {
|
|
9848
|
+
overflowRecovered = true;
|
|
9849
|
+
await compactNow("context overflow");
|
|
9850
|
+
continue;
|
|
9851
|
+
}
|
|
9852
|
+
statusWatcher.stop();
|
|
9853
|
+
saveSession(state);
|
|
9854
|
+
onEvent({
|
|
9855
|
+
type: "error",
|
|
9856
|
+
error: friendlyError(error),
|
|
9857
|
+
...code ? { code } : {},
|
|
9858
|
+
lastCallInputTokens
|
|
9859
|
+
});
|
|
9860
|
+
return;
|
|
9861
|
+
}
|
|
9646
9862
|
if (contentBlocks.length > 0) {
|
|
9647
9863
|
state.messages.push({
|
|
9648
9864
|
role: "assistant",
|
|
@@ -9662,14 +9878,14 @@ async function runTurn(params) {
|
|
|
9662
9878
|
});
|
|
9663
9879
|
}
|
|
9664
9880
|
const toolCalls = getToolCalls(contentBlocks);
|
|
9665
|
-
if (toolCalls.length === 0 && (stopReason === "repetition" || stopReason === "max_tokens") &&
|
|
9666
|
-
|
|
9881
|
+
if (toolCalls.length === 0 && (stopReason === "repetition" || stopReason === "max_tokens") && recoveries < MAX_RECOVERIES && !signal?.aborted) {
|
|
9882
|
+
recoveries++;
|
|
9667
9883
|
log15.warn("Abnormal stop \u2014 nudging model to continue", {
|
|
9668
9884
|
requestId,
|
|
9669
9885
|
stopReason,
|
|
9670
|
-
attempt:
|
|
9886
|
+
attempt: recoveries
|
|
9671
9887
|
});
|
|
9672
|
-
const nudge = "Your previous response was cut off \u2014 it
|
|
9888
|
+
const nudge = "Your previous response was cut off before you finished \u2014 it hit the output limit (often from over-long reasoning) or degenerated into repeated text, and the unusable part was removed. Stop deliberating, reassess where you are, and continue with a concrete tool call rather than restating your plan.";
|
|
9673
9889
|
state.messages.push({ role: "user", content: nudge, hidden: true });
|
|
9674
9890
|
onEvent({ type: "user_message", text: nudge, hidden: true });
|
|
9675
9891
|
continue;
|
|
@@ -9862,6 +10078,11 @@ async function runTurn(params) {
|
|
|
9862
10078
|
isToolError: r.isError
|
|
9863
10079
|
});
|
|
9864
10080
|
}
|
|
10081
|
+
const { forceCompactAt } = getContextLimits(parentModel);
|
|
10082
|
+
if (lastCallInputTokens > forceCompactAt && midTurnCompactions < MAX_MID_TURN_COMPACTIONS && !signal?.aborted) {
|
|
10083
|
+
midTurnCompactions++;
|
|
10084
|
+
await compactNow(`context ${lastCallInputTokens} > ${forceCompactAt}`);
|
|
10085
|
+
}
|
|
9865
10086
|
if (takeSteering && !signal?.aborted) {
|
|
9866
10087
|
const injected = (await takeSteering()).filter(
|
|
9867
10088
|
(e) => e.text.trim().length > 0 || (e.attachments?.length ?? 0) > 0
|
|
@@ -10911,6 +11132,10 @@ var HeadlessSession = class {
|
|
|
10911
11132
|
);
|
|
10912
11133
|
return;
|
|
10913
11134
|
case "error":
|
|
11135
|
+
if (typeof e.lastCallInputTokens === "number") {
|
|
11136
|
+
this.sessionStats.lastContextSize = e.lastCallInputTokens;
|
|
11137
|
+
this.persistStats();
|
|
11138
|
+
}
|
|
10914
11139
|
this.emit(
|
|
10915
11140
|
"error",
|
|
10916
11141
|
{ error: e.error, ...e.code ? { code: e.code } : {} },
|
package/dist/index.js
CHANGED
|
@@ -84,6 +84,93 @@ var init_logger = __esm({
|
|
|
84
84
|
}
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
// src/loopGuard.ts
|
|
88
|
+
function normalizeParagraph(seg) {
|
|
89
|
+
const trimmed = seg.trim();
|
|
90
|
+
if (!trimmed || LONE_HEADER.test(trimmed)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const key = trimmed.toLowerCase().replace(/\s+/g, " ");
|
|
94
|
+
if (key.length < MIN_PARAGRAPH_CHARS) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return key;
|
|
98
|
+
}
|
|
99
|
+
var PARAGRAPH_REPEAT_THRESHOLD, MIN_PARAGRAPH_CHARS, CHUNK_SIZE, CHUNK_WINDOW, CHUNK_REPEAT_THRESHOLD, LONE_HEADER, RepetitionDetector;
|
|
100
|
+
var init_loopGuard = __esm({
|
|
101
|
+
"src/loopGuard.ts"() {
|
|
102
|
+
"use strict";
|
|
103
|
+
PARAGRAPH_REPEAT_THRESHOLD = 6;
|
|
104
|
+
MIN_PARAGRAPH_CHARS = 80;
|
|
105
|
+
CHUNK_SIZE = 60;
|
|
106
|
+
CHUNK_WINDOW = 5e3;
|
|
107
|
+
CHUNK_REPEAT_THRESHOLD = 10;
|
|
108
|
+
LONE_HEADER = /^\*{1,3}[^*].*\*{1,3}$/;
|
|
109
|
+
RepetitionDetector = class {
|
|
110
|
+
// Paragraph detector: incomplete tail awaiting its closing blank line, plus
|
|
111
|
+
// occurrence counts of completed, normalized paragraphs.
|
|
112
|
+
paragraphBuffer = "";
|
|
113
|
+
paragraphCounts = /* @__PURE__ */ new Map();
|
|
114
|
+
// Rolling-chunk detector: the trailing window of raw fed text.
|
|
115
|
+
window = "";
|
|
116
|
+
fired = false;
|
|
117
|
+
/**
|
|
118
|
+
* Feed the next streamed text fragment. Returns a signal the first time a
|
|
119
|
+
* loop is recognized, then null forever after (the caller aborts on the
|
|
120
|
+
* first signal; further feeds are harmless no-ops).
|
|
121
|
+
*/
|
|
122
|
+
feed(text) {
|
|
123
|
+
if (this.fired || !text) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return this.feedParagraphs(text) ?? this.feedChunks(text);
|
|
127
|
+
}
|
|
128
|
+
feedParagraphs(text) {
|
|
129
|
+
this.paragraphBuffer += text;
|
|
130
|
+
const segments = this.paragraphBuffer.split(/\n\s*\n/);
|
|
131
|
+
this.paragraphBuffer = segments.pop() ?? "";
|
|
132
|
+
for (const seg of segments) {
|
|
133
|
+
const key = normalizeParagraph(seg);
|
|
134
|
+
if (!key) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const count = (this.paragraphCounts.get(key) ?? 0) + 1;
|
|
138
|
+
this.paragraphCounts.set(key, count);
|
|
139
|
+
if (count >= PARAGRAPH_REPEAT_THRESHOLD) {
|
|
140
|
+
this.fired = true;
|
|
141
|
+
return {
|
|
142
|
+
kind: "paragraph",
|
|
143
|
+
repeats: count,
|
|
144
|
+
sample: seg.trim().slice(0, 160)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
feedChunks(text) {
|
|
151
|
+
this.window = (this.window + text).slice(-CHUNK_WINDOW);
|
|
152
|
+
if (this.window.length < CHUNK_SIZE * CHUNK_REPEAT_THRESHOLD) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const tail = this.window.slice(-CHUNK_SIZE);
|
|
156
|
+
if (!tail.trim()) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const occurrences = this.window.split(tail).length - 1;
|
|
160
|
+
if (occurrences >= CHUNK_REPEAT_THRESHOLD) {
|
|
161
|
+
this.fired = true;
|
|
162
|
+
return {
|
|
163
|
+
kind: "chunk",
|
|
164
|
+
repeats: occurrences,
|
|
165
|
+
sample: tail.replace(/\s+/g, " ").trim().slice(0, 160)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
87
174
|
// src/api.ts
|
|
88
175
|
async function* streamChat(params) {
|
|
89
176
|
const { baseUrl: baseUrl2, apiKey, signal, requestId, model, ...rest } = params;
|
|
@@ -286,17 +373,51 @@ async function* streamChatWithRetry(params, options) {
|
|
|
286
373
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
287
374
|
const buffer = [];
|
|
288
375
|
let retryableFailure = false;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
376
|
+
const detector = options?.detectRepetition ? new RepetitionDetector() : null;
|
|
377
|
+
const streamAbort = new AbortController();
|
|
378
|
+
const onCallerAbort = () => streamAbort.abort();
|
|
379
|
+
if (params.signal) {
|
|
380
|
+
if (params.signal.aborted) {
|
|
381
|
+
streamAbort.abort();
|
|
382
|
+
} else {
|
|
383
|
+
params.signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
for await (const event of streamChat({
|
|
388
|
+
...params,
|
|
389
|
+
signal: streamAbort.signal
|
|
390
|
+
})) {
|
|
391
|
+
if (event.type === "error") {
|
|
392
|
+
if (isRetryableError(event.error, event.code) && attempt < MAX_RETRIES - 1) {
|
|
393
|
+
options?.onRetry?.(attempt, event.error);
|
|
394
|
+
retryableFailure = true;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
yield event;
|
|
398
|
+
return;
|
|
295
399
|
}
|
|
296
|
-
|
|
297
|
-
|
|
400
|
+
if (detector && (event.type === "text" || event.type === "thinking")) {
|
|
401
|
+
const loop = detector.feed(event.text);
|
|
402
|
+
if (loop) {
|
|
403
|
+
log.warn("Repetition loop detected \u2014 aborting call", {
|
|
404
|
+
requestId: params.requestId,
|
|
405
|
+
kind: loop.kind,
|
|
406
|
+
repeats: loop.repeats
|
|
407
|
+
});
|
|
408
|
+
streamAbort.abort();
|
|
409
|
+
yield {
|
|
410
|
+
type: "error",
|
|
411
|
+
error: "Response stopped: the model was repeating its reasoning without making progress.",
|
|
412
|
+
code: "repetition_loop"
|
|
413
|
+
};
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
buffer.push(event);
|
|
298
418
|
}
|
|
299
|
-
|
|
419
|
+
} finally {
|
|
420
|
+
params.signal?.removeEventListener("abort", onCallerAbort);
|
|
300
421
|
}
|
|
301
422
|
if (retryableFailure) {
|
|
302
423
|
if (params.signal?.aborted) {
|
|
@@ -375,6 +496,7 @@ var init_api = __esm({
|
|
|
375
496
|
"src/api.ts"() {
|
|
376
497
|
"use strict";
|
|
377
498
|
init_logger();
|
|
499
|
+
init_loopGuard();
|
|
378
500
|
log = createLogger("api");
|
|
379
501
|
MAX_RETRIES = 5;
|
|
380
502
|
INITIAL_BACKOFF_MS = 1e3;
|
|
@@ -1543,6 +1665,13 @@ var init_sentinel = __esm({
|
|
|
1543
1665
|
|
|
1544
1666
|
// src/usageLedger.ts
|
|
1545
1667
|
import fs9 from "fs";
|
|
1668
|
+
function thinkingTokensFromBilling(billingEvents, outputTokens) {
|
|
1669
|
+
if (!billingEvents?.length) {
|
|
1670
|
+
return 0;
|
|
1671
|
+
}
|
|
1672
|
+
const billedResponseUnits = billingEvents.filter((e) => e.eventType.endsWith("-response")).reduce((sum, e) => sum + e.numUnits, 0);
|
|
1673
|
+
return Math.max(0, billedResponseUnits - outputTokens);
|
|
1674
|
+
}
|
|
1546
1675
|
function nanoToDollars(nano) {
|
|
1547
1676
|
return typeof nano === "number" ? nano / 1e9 : void 0;
|
|
1548
1677
|
}
|
|
@@ -2191,6 +2320,7 @@ var init_surfaces = __esm({
|
|
|
2191
2320
|
"kimi-k3": { forceCompactAt: 85e4 },
|
|
2192
2321
|
"deepseek-v4-flash-0731": { forceCompactAt: 85e4 },
|
|
2193
2322
|
"deepseek-v4-pro": { forceCompactAt: 85e4 },
|
|
2323
|
+
"deepseek-v4.1-flash": { forceCompactAt: 85e4 },
|
|
2194
2324
|
"qwen3.8-2.4t-a95b-deepinfra": { forceCompactAt: 2e5 },
|
|
2195
2325
|
// 262K window
|
|
2196
2326
|
"qwen3.8-27b-deepinfra": { forceCompactAt: 2e5 },
|
|
@@ -9311,6 +9441,12 @@ var init_resolve = __esm({
|
|
|
9311
9441
|
});
|
|
9312
9442
|
|
|
9313
9443
|
// src/errors.ts
|
|
9444
|
+
function isContextOverflowError(message, code) {
|
|
9445
|
+
if (code === "context_overflow") {
|
|
9446
|
+
return true;
|
|
9447
|
+
}
|
|
9448
|
+
return OVERFLOW_PATTERN.test(message);
|
|
9449
|
+
}
|
|
9314
9450
|
function friendlyError(raw) {
|
|
9315
9451
|
for (const [pattern, message] of patterns) {
|
|
9316
9452
|
if (pattern.test(raw)) {
|
|
@@ -9319,10 +9455,11 @@ function friendlyError(raw) {
|
|
|
9319
9455
|
}
|
|
9320
9456
|
return `Something went wrong: ${raw}`;
|
|
9321
9457
|
}
|
|
9322
|
-
var patterns;
|
|
9458
|
+
var OVERFLOW_PATTERN, patterns;
|
|
9323
9459
|
var init_errors = __esm({
|
|
9324
9460
|
"src/errors.ts"() {
|
|
9325
9461
|
"use strict";
|
|
9462
|
+
OVERFLOW_PATTERN = /input token count.*exceeds|exceeds the maximum number of tokens|prompt is too long|context[_ ]length[_ ]exceeded|maximum context length|HTTP 413|payload too large|request entity too large/i;
|
|
9326
9463
|
patterns = [
|
|
9327
9464
|
[
|
|
9328
9465
|
/Network error/i,
|
|
@@ -9342,6 +9479,14 @@ var init_errors = __esm({
|
|
|
9342
9479
|
"The AI service is temporarily unavailable. Please try again."
|
|
9343
9480
|
],
|
|
9344
9481
|
[/Stream stalled/i, "The connection was interrupted. Please try again."],
|
|
9482
|
+
[
|
|
9483
|
+
// A too-large-context failure that survived the automatic compact-and-retry
|
|
9484
|
+
// (or came from an older server). Kept ahead of the generic fallback so a
|
|
9485
|
+
// raw provider string (e.g. Gemini's INVALID_ARGUMENT JSON) never reaches
|
|
9486
|
+
// the user.
|
|
9487
|
+
OVERFLOW_PATTERN,
|
|
9488
|
+
"This conversation outgrew the model's context window. Remy compacted it and tried again; if you keep seeing this, run /compact or start a new conversation."
|
|
9489
|
+
],
|
|
9345
9490
|
[
|
|
9346
9491
|
/content filter|Output blocked/i,
|
|
9347
9492
|
"The AI model's content moderation filter blocked this response. These are usually false positives, we apologize for the interruption. Rephrasing your request typically fixes this."
|
|
@@ -9987,8 +10132,33 @@ async function runTurn(params) {
|
|
|
9987
10132
|
let lastCallInputTokens = 0;
|
|
9988
10133
|
let lastCallCacheCreation = 0;
|
|
9989
10134
|
let lastCallCacheRead = 0;
|
|
9990
|
-
let
|
|
9991
|
-
const
|
|
10135
|
+
let recoveries = 0;
|
|
10136
|
+
const MAX_RECOVERIES = 2;
|
|
10137
|
+
let midTurnCompactions = 0;
|
|
10138
|
+
const MAX_MID_TURN_COMPACTIONS = 2;
|
|
10139
|
+
let overflowRecovered = false;
|
|
10140
|
+
const compactNow = async (reason) => {
|
|
10141
|
+
log14.warn("Compacting mid-turn", {
|
|
10142
|
+
requestId,
|
|
10143
|
+
reason,
|
|
10144
|
+
lastCallInputTokens
|
|
10145
|
+
});
|
|
10146
|
+
onEvent({ type: "status", message: "Compacting the conversation\u2026" });
|
|
10147
|
+
try {
|
|
10148
|
+
await triggerCompaction(state, apiConfig, {
|
|
10149
|
+
blocking: true,
|
|
10150
|
+
requestId,
|
|
10151
|
+
model,
|
|
10152
|
+
origin: "gate"
|
|
10153
|
+
});
|
|
10154
|
+
applyPendingSummaries(state);
|
|
10155
|
+
} catch (err) {
|
|
10156
|
+
log14.error("Mid-turn compaction failed", {
|
|
10157
|
+
requestId,
|
|
10158
|
+
error: err?.message ?? String(err)
|
|
10159
|
+
});
|
|
10160
|
+
}
|
|
10161
|
+
};
|
|
9992
10162
|
const statusWatcher = isFirstMessage ? { stop() {
|
|
9993
10163
|
}, pause() {
|
|
9994
10164
|
}, resume() {
|
|
@@ -10135,6 +10305,7 @@ async function runTurn(params) {
|
|
|
10135
10305
|
onEvent({ type: "tool_input_delta", id, name, result: content });
|
|
10136
10306
|
}
|
|
10137
10307
|
}
|
|
10308
|
+
let streamError = null;
|
|
10138
10309
|
try {
|
|
10139
10310
|
for await (const event of streamChatWithRetry(
|
|
10140
10311
|
{
|
|
@@ -10153,9 +10324,13 @@ async function runTurn(params) {
|
|
|
10153
10324
|
onRetry: (attempt) => {
|
|
10154
10325
|
onEvent({
|
|
10155
10326
|
type: "status",
|
|
10156
|
-
message: `Lost connection, retrying (attempt ${attempt + 2} of
|
|
10327
|
+
message: `Lost connection, retrying (attempt ${attempt + 2} of ${MAX_RETRIES})`
|
|
10157
10328
|
});
|
|
10158
|
-
}
|
|
10329
|
+
},
|
|
10330
|
+
// Watch the streamed reasoning/text for a runaway repetition loop
|
|
10331
|
+
// and abort the in-flight call early (RPT-1225). Surfaces as a
|
|
10332
|
+
// `repetition_loop` error handled below.
|
|
10333
|
+
detectRepetition: true
|
|
10159
10334
|
}
|
|
10160
10335
|
)) {
|
|
10161
10336
|
if (signal?.aborted) {
|
|
@@ -10179,7 +10354,7 @@ async function runTurn(params) {
|
|
|
10179
10354
|
emitTextBlockSnapshot2(false);
|
|
10180
10355
|
break;
|
|
10181
10356
|
}
|
|
10182
|
-
case "thinking":
|
|
10357
|
+
case "thinking": {
|
|
10183
10358
|
if (event.text === "") {
|
|
10184
10359
|
thinkingBlockStartTimes.push(event.ts);
|
|
10185
10360
|
if (textBlockOpen) {
|
|
@@ -10189,6 +10364,7 @@ async function runTurn(params) {
|
|
|
10189
10364
|
}
|
|
10190
10365
|
onEvent({ type: "thinking", text: event.text });
|
|
10191
10366
|
break;
|
|
10367
|
+
}
|
|
10192
10368
|
case "thinking_complete": {
|
|
10193
10369
|
const startedAt = thinkingBlockStartTimes[thinkingCompleteCount] ?? event.ts;
|
|
10194
10370
|
contentBlocks.push({
|
|
@@ -10286,6 +10462,10 @@ async function runTurn(params) {
|
|
|
10286
10462
|
outputTokens: event.usage.outputTokens,
|
|
10287
10463
|
cacheCreationTokens: event.usage.cacheCreationTokens,
|
|
10288
10464
|
cacheReadTokens: event.usage.cacheReadTokens,
|
|
10465
|
+
thinkingTokens: thinkingTokensFromBilling(
|
|
10466
|
+
event.billingEvents,
|
|
10467
|
+
event.usage.outputTokens
|
|
10468
|
+
) || void 0,
|
|
10289
10469
|
cost: nanoToDollars(event.cost),
|
|
10290
10470
|
billingEvents: event.billingEvents,
|
|
10291
10471
|
durationMs: Date.now() - iterStart,
|
|
@@ -10295,13 +10475,11 @@ async function runTurn(params) {
|
|
|
10295
10475
|
});
|
|
10296
10476
|
break;
|
|
10297
10477
|
case "error":
|
|
10298
|
-
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
});
|
|
10304
|
-
return;
|
|
10478
|
+
streamError = { error: event.error, code: event.code };
|
|
10479
|
+
break;
|
|
10480
|
+
}
|
|
10481
|
+
if (streamError) {
|
|
10482
|
+
break;
|
|
10305
10483
|
}
|
|
10306
10484
|
}
|
|
10307
10485
|
} catch (err) {
|
|
@@ -10346,6 +10524,51 @@ async function runTurn(params) {
|
|
|
10346
10524
|
saveSession(state);
|
|
10347
10525
|
return;
|
|
10348
10526
|
}
|
|
10527
|
+
if (streamError) {
|
|
10528
|
+
const { error, code } = streamError;
|
|
10529
|
+
if (code === "repetition_loop") {
|
|
10530
|
+
if (recoveries < MAX_RECOVERIES && !signal?.aborted) {
|
|
10531
|
+
recoveries++;
|
|
10532
|
+
log14.warn("Repetition loop \u2014 nudging model to continue", {
|
|
10533
|
+
requestId,
|
|
10534
|
+
attempt: recoveries
|
|
10535
|
+
});
|
|
10536
|
+
const nudge = "Your previous response was stopped because it kept repeating the same reasoning without making progress. Do not deliberate further \u2014 take the next concrete action now: make the tool call or give the answer.";
|
|
10537
|
+
state.messages.push({
|
|
10538
|
+
role: "user",
|
|
10539
|
+
content: nudge,
|
|
10540
|
+
hidden: true
|
|
10541
|
+
});
|
|
10542
|
+
onEvent({ type: "user_message", text: nudge, hidden: true });
|
|
10543
|
+
continue;
|
|
10544
|
+
}
|
|
10545
|
+
statusWatcher.stop();
|
|
10546
|
+
saveSession(state);
|
|
10547
|
+
log14.warn("Repetition loop over recovery cap \u2014 ending turn", {
|
|
10548
|
+
requestId
|
|
10549
|
+
});
|
|
10550
|
+
onEvent({
|
|
10551
|
+
type: "error",
|
|
10552
|
+
error: "The model kept repeating itself without making progress, so Remy stopped the turn to avoid runaway cost. Try again, or rephrase your request.",
|
|
10553
|
+
lastCallInputTokens
|
|
10554
|
+
});
|
|
10555
|
+
return;
|
|
10556
|
+
}
|
|
10557
|
+
if (isContextOverflowError(error, code) && !overflowRecovered && !signal?.aborted) {
|
|
10558
|
+
overflowRecovered = true;
|
|
10559
|
+
await compactNow("context overflow");
|
|
10560
|
+
continue;
|
|
10561
|
+
}
|
|
10562
|
+
statusWatcher.stop();
|
|
10563
|
+
saveSession(state);
|
|
10564
|
+
onEvent({
|
|
10565
|
+
type: "error",
|
|
10566
|
+
error: friendlyError(error),
|
|
10567
|
+
...code ? { code } : {},
|
|
10568
|
+
lastCallInputTokens
|
|
10569
|
+
});
|
|
10570
|
+
return;
|
|
10571
|
+
}
|
|
10349
10572
|
if (contentBlocks.length > 0) {
|
|
10350
10573
|
state.messages.push({
|
|
10351
10574
|
role: "assistant",
|
|
@@ -10365,14 +10588,14 @@ async function runTurn(params) {
|
|
|
10365
10588
|
});
|
|
10366
10589
|
}
|
|
10367
10590
|
const toolCalls = getToolCalls(contentBlocks);
|
|
10368
|
-
if (toolCalls.length === 0 && (stopReason === "repetition" || stopReason === "max_tokens") &&
|
|
10369
|
-
|
|
10591
|
+
if (toolCalls.length === 0 && (stopReason === "repetition" || stopReason === "max_tokens") && recoveries < MAX_RECOVERIES && !signal?.aborted) {
|
|
10592
|
+
recoveries++;
|
|
10370
10593
|
log14.warn("Abnormal stop \u2014 nudging model to continue", {
|
|
10371
10594
|
requestId,
|
|
10372
10595
|
stopReason,
|
|
10373
|
-
attempt:
|
|
10596
|
+
attempt: recoveries
|
|
10374
10597
|
});
|
|
10375
|
-
const nudge = "Your previous response was cut off \u2014 it
|
|
10598
|
+
const nudge = "Your previous response was cut off before you finished \u2014 it hit the output limit (often from over-long reasoning) or degenerated into repeated text, and the unusable part was removed. Stop deliberating, reassess where you are, and continue with a concrete tool call rather than restating your plan.";
|
|
10376
10599
|
state.messages.push({ role: "user", content: nudge, hidden: true });
|
|
10377
10600
|
onEvent({ type: "user_message", text: nudge, hidden: true });
|
|
10378
10601
|
continue;
|
|
@@ -10565,6 +10788,11 @@ async function runTurn(params) {
|
|
|
10565
10788
|
isToolError: r.isError
|
|
10566
10789
|
});
|
|
10567
10790
|
}
|
|
10791
|
+
const { forceCompactAt } = getContextLimits(parentModel);
|
|
10792
|
+
if (lastCallInputTokens > forceCompactAt && midTurnCompactions < MAX_MID_TURN_COMPACTIONS && !signal?.aborted) {
|
|
10793
|
+
midTurnCompactions++;
|
|
10794
|
+
await compactNow(`context ${lastCallInputTokens} > ${forceCompactAt}`);
|
|
10795
|
+
}
|
|
10568
10796
|
if (takeSteering && !signal?.aborted) {
|
|
10569
10797
|
const injected = (await takeSteering()).filter(
|
|
10570
10798
|
(e) => e.text.trim().length > 0 || (e.attachments?.length ?? 0) > 0
|
|
@@ -10606,6 +10834,7 @@ var init_agent = __esm({
|
|
|
10606
10834
|
init_sentinel();
|
|
10607
10835
|
init_trigger2();
|
|
10608
10836
|
init_surfaces();
|
|
10837
|
+
init_trigger();
|
|
10609
10838
|
init_toolRegistry();
|
|
10610
10839
|
init_historyLimits();
|
|
10611
10840
|
log14 = createLogger("agent");
|
|
@@ -11958,6 +12187,10 @@ var init_headless = __esm({
|
|
|
11958
12187
|
);
|
|
11959
12188
|
return;
|
|
11960
12189
|
case "error":
|
|
12190
|
+
if (typeof e.lastCallInputTokens === "number") {
|
|
12191
|
+
this.sessionStats.lastContextSize = e.lastCallInputTokens;
|
|
12192
|
+
this.persistStats();
|
|
12193
|
+
}
|
|
11961
12194
|
this.emit(
|
|
11962
12195
|
"error",
|
|
11963
12196
|
{ error: e.error, ...e.code ? { code: e.code } : {} },
|