@mindstudio-ai/remy 0.1.329 → 0.1.330
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 +249 -25
- package/dist/index.js +258 -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) {
|
|
@@ -3031,6 +3146,13 @@ var queryDatabaseTool = {
|
|
|
3031
3146
|
// src/usageLedger.ts
|
|
3032
3147
|
import fs16 from "fs";
|
|
3033
3148
|
var LEDGER_FILE = ".logs/usage.ndjson";
|
|
3149
|
+
function thinkingTokensFromBilling(billingEvents, outputTokens) {
|
|
3150
|
+
if (!billingEvents?.length) {
|
|
3151
|
+
return 0;
|
|
3152
|
+
}
|
|
3153
|
+
const billedResponseUnits = billingEvents.filter((e) => e.eventType.endsWith("-response")).reduce((sum, e) => sum + e.numUnits, 0);
|
|
3154
|
+
return Math.max(0, billedResponseUnits - outputTokens);
|
|
3155
|
+
}
|
|
3034
3156
|
var fd = null;
|
|
3035
3157
|
function nanoToDollars(nano) {
|
|
3036
3158
|
return typeof nano === "number" ? nano / 1e9 : void 0;
|
|
@@ -8961,6 +9083,13 @@ function getActionChain(startName) {
|
|
|
8961
9083
|
}
|
|
8962
9084
|
|
|
8963
9085
|
// src/errors.ts
|
|
9086
|
+
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;
|
|
9087
|
+
function isContextOverflowError(message, code) {
|
|
9088
|
+
if (code === "context_overflow") {
|
|
9089
|
+
return true;
|
|
9090
|
+
}
|
|
9091
|
+
return OVERFLOW_PATTERN.test(message);
|
|
9092
|
+
}
|
|
8964
9093
|
var patterns = [
|
|
8965
9094
|
[
|
|
8966
9095
|
/Network error/i,
|
|
@@ -8980,6 +9109,14 @@ var patterns = [
|
|
|
8980
9109
|
"The AI service is temporarily unavailable. Please try again."
|
|
8981
9110
|
],
|
|
8982
9111
|
[/Stream stalled/i, "The connection was interrupted. Please try again."],
|
|
9112
|
+
[
|
|
9113
|
+
// A too-large-context failure that survived the automatic compact-and-retry
|
|
9114
|
+
// (or came from an older server). Kept ahead of the generic fallback so a
|
|
9115
|
+
// raw provider string (e.g. Gemini's INVALID_ARGUMENT JSON) never reaches
|
|
9116
|
+
// the user.
|
|
9117
|
+
OVERFLOW_PATTERN,
|
|
9118
|
+
"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."
|
|
9119
|
+
],
|
|
8983
9120
|
[
|
|
8984
9121
|
/content filter|Output blocked/i,
|
|
8985
9122
|
"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 +9421,33 @@ async function runTurn(params) {
|
|
|
9284
9421
|
let lastCallInputTokens = 0;
|
|
9285
9422
|
let lastCallCacheCreation = 0;
|
|
9286
9423
|
let lastCallCacheRead = 0;
|
|
9287
|
-
let
|
|
9288
|
-
const
|
|
9424
|
+
let recoveries = 0;
|
|
9425
|
+
const MAX_RECOVERIES = 2;
|
|
9426
|
+
let midTurnCompactions = 0;
|
|
9427
|
+
const MAX_MID_TURN_COMPACTIONS = 2;
|
|
9428
|
+
let overflowRecovered = false;
|
|
9429
|
+
const compactNow = async (reason) => {
|
|
9430
|
+
log15.warn("Compacting mid-turn", {
|
|
9431
|
+
requestId,
|
|
9432
|
+
reason,
|
|
9433
|
+
lastCallInputTokens
|
|
9434
|
+
});
|
|
9435
|
+
onEvent({ type: "status", message: "Compacting the conversation\u2026" });
|
|
9436
|
+
try {
|
|
9437
|
+
await triggerCompaction(state, apiConfig, {
|
|
9438
|
+
blocking: true,
|
|
9439
|
+
requestId,
|
|
9440
|
+
model,
|
|
9441
|
+
origin: "gate"
|
|
9442
|
+
});
|
|
9443
|
+
applyPendingSummaries(state);
|
|
9444
|
+
} catch (err) {
|
|
9445
|
+
log15.error("Mid-turn compaction failed", {
|
|
9446
|
+
requestId,
|
|
9447
|
+
error: err?.message ?? String(err)
|
|
9448
|
+
});
|
|
9449
|
+
}
|
|
9450
|
+
};
|
|
9289
9451
|
const statusWatcher = isFirstMessage ? { stop() {
|
|
9290
9452
|
}, pause() {
|
|
9291
9453
|
}, resume() {
|
|
@@ -9432,6 +9594,7 @@ async function runTurn(params) {
|
|
|
9432
9594
|
onEvent({ type: "tool_input_delta", id, name, result: content });
|
|
9433
9595
|
}
|
|
9434
9596
|
}
|
|
9597
|
+
let streamError = null;
|
|
9435
9598
|
try {
|
|
9436
9599
|
for await (const event of streamChatWithRetry(
|
|
9437
9600
|
{
|
|
@@ -9450,9 +9613,13 @@ async function runTurn(params) {
|
|
|
9450
9613
|
onRetry: (attempt) => {
|
|
9451
9614
|
onEvent({
|
|
9452
9615
|
type: "status",
|
|
9453
|
-
message: `Lost connection, retrying (attempt ${attempt + 2} of
|
|
9616
|
+
message: `Lost connection, retrying (attempt ${attempt + 2} of ${MAX_RETRIES})`
|
|
9454
9617
|
});
|
|
9455
|
-
}
|
|
9618
|
+
},
|
|
9619
|
+
// Watch the streamed reasoning/text for a runaway repetition loop
|
|
9620
|
+
// and abort the in-flight call early (RPT-1225). Surfaces as a
|
|
9621
|
+
// `repetition_loop` error handled below.
|
|
9622
|
+
detectRepetition: true
|
|
9456
9623
|
}
|
|
9457
9624
|
)) {
|
|
9458
9625
|
if (signal?.aborted) {
|
|
@@ -9476,7 +9643,7 @@ async function runTurn(params) {
|
|
|
9476
9643
|
emitTextBlockSnapshot2(false);
|
|
9477
9644
|
break;
|
|
9478
9645
|
}
|
|
9479
|
-
case "thinking":
|
|
9646
|
+
case "thinking": {
|
|
9480
9647
|
if (event.text === "") {
|
|
9481
9648
|
thinkingBlockStartTimes.push(event.ts);
|
|
9482
9649
|
if (textBlockOpen) {
|
|
@@ -9486,6 +9653,7 @@ async function runTurn(params) {
|
|
|
9486
9653
|
}
|
|
9487
9654
|
onEvent({ type: "thinking", text: event.text });
|
|
9488
9655
|
break;
|
|
9656
|
+
}
|
|
9489
9657
|
case "thinking_complete": {
|
|
9490
9658
|
const startedAt = thinkingBlockStartTimes[thinkingCompleteCount] ?? event.ts;
|
|
9491
9659
|
contentBlocks.push({
|
|
@@ -9583,6 +9751,10 @@ async function runTurn(params) {
|
|
|
9583
9751
|
outputTokens: event.usage.outputTokens,
|
|
9584
9752
|
cacheCreationTokens: event.usage.cacheCreationTokens,
|
|
9585
9753
|
cacheReadTokens: event.usage.cacheReadTokens,
|
|
9754
|
+
thinkingTokens: thinkingTokensFromBilling(
|
|
9755
|
+
event.billingEvents,
|
|
9756
|
+
event.usage.outputTokens
|
|
9757
|
+
) || void 0,
|
|
9586
9758
|
cost: nanoToDollars(event.cost),
|
|
9587
9759
|
billingEvents: event.billingEvents,
|
|
9588
9760
|
durationMs: Date.now() - iterStart,
|
|
@@ -9592,13 +9764,11 @@ async function runTurn(params) {
|
|
|
9592
9764
|
});
|
|
9593
9765
|
break;
|
|
9594
9766
|
case "error":
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
});
|
|
9601
|
-
return;
|
|
9767
|
+
streamError = { error: event.error, code: event.code };
|
|
9768
|
+
break;
|
|
9769
|
+
}
|
|
9770
|
+
if (streamError) {
|
|
9771
|
+
break;
|
|
9602
9772
|
}
|
|
9603
9773
|
}
|
|
9604
9774
|
} catch (err) {
|
|
@@ -9643,6 +9813,51 @@ async function runTurn(params) {
|
|
|
9643
9813
|
saveSession(state);
|
|
9644
9814
|
return;
|
|
9645
9815
|
}
|
|
9816
|
+
if (streamError) {
|
|
9817
|
+
const { error, code } = streamError;
|
|
9818
|
+
if (code === "repetition_loop") {
|
|
9819
|
+
if (recoveries < MAX_RECOVERIES && !signal?.aborted) {
|
|
9820
|
+
recoveries++;
|
|
9821
|
+
log15.warn("Repetition loop \u2014 nudging model to continue", {
|
|
9822
|
+
requestId,
|
|
9823
|
+
attempt: recoveries
|
|
9824
|
+
});
|
|
9825
|
+
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.";
|
|
9826
|
+
state.messages.push({
|
|
9827
|
+
role: "user",
|
|
9828
|
+
content: nudge,
|
|
9829
|
+
hidden: true
|
|
9830
|
+
});
|
|
9831
|
+
onEvent({ type: "user_message", text: nudge, hidden: true });
|
|
9832
|
+
continue;
|
|
9833
|
+
}
|
|
9834
|
+
statusWatcher.stop();
|
|
9835
|
+
saveSession(state);
|
|
9836
|
+
log15.warn("Repetition loop over recovery cap \u2014 ending turn", {
|
|
9837
|
+
requestId
|
|
9838
|
+
});
|
|
9839
|
+
onEvent({
|
|
9840
|
+
type: "error",
|
|
9841
|
+
error: "The model kept repeating itself without making progress, so Remy stopped the turn to avoid runaway cost. Try again, or rephrase your request.",
|
|
9842
|
+
lastCallInputTokens
|
|
9843
|
+
});
|
|
9844
|
+
return;
|
|
9845
|
+
}
|
|
9846
|
+
if (isContextOverflowError(error, code) && !overflowRecovered && !signal?.aborted) {
|
|
9847
|
+
overflowRecovered = true;
|
|
9848
|
+
await compactNow("context overflow");
|
|
9849
|
+
continue;
|
|
9850
|
+
}
|
|
9851
|
+
statusWatcher.stop();
|
|
9852
|
+
saveSession(state);
|
|
9853
|
+
onEvent({
|
|
9854
|
+
type: "error",
|
|
9855
|
+
error: friendlyError(error),
|
|
9856
|
+
...code ? { code } : {},
|
|
9857
|
+
lastCallInputTokens
|
|
9858
|
+
});
|
|
9859
|
+
return;
|
|
9860
|
+
}
|
|
9646
9861
|
if (contentBlocks.length > 0) {
|
|
9647
9862
|
state.messages.push({
|
|
9648
9863
|
role: "assistant",
|
|
@@ -9662,14 +9877,14 @@ async function runTurn(params) {
|
|
|
9662
9877
|
});
|
|
9663
9878
|
}
|
|
9664
9879
|
const toolCalls = getToolCalls(contentBlocks);
|
|
9665
|
-
if (toolCalls.length === 0 && (stopReason === "repetition" || stopReason === "max_tokens") &&
|
|
9666
|
-
|
|
9880
|
+
if (toolCalls.length === 0 && (stopReason === "repetition" || stopReason === "max_tokens") && recoveries < MAX_RECOVERIES && !signal?.aborted) {
|
|
9881
|
+
recoveries++;
|
|
9667
9882
|
log15.warn("Abnormal stop \u2014 nudging model to continue", {
|
|
9668
9883
|
requestId,
|
|
9669
9884
|
stopReason,
|
|
9670
|
-
attempt:
|
|
9885
|
+
attempt: recoveries
|
|
9671
9886
|
});
|
|
9672
|
-
const nudge = "Your previous response was cut off \u2014 it
|
|
9887
|
+
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
9888
|
state.messages.push({ role: "user", content: nudge, hidden: true });
|
|
9674
9889
|
onEvent({ type: "user_message", text: nudge, hidden: true });
|
|
9675
9890
|
continue;
|
|
@@ -9862,6 +10077,11 @@ async function runTurn(params) {
|
|
|
9862
10077
|
isToolError: r.isError
|
|
9863
10078
|
});
|
|
9864
10079
|
}
|
|
10080
|
+
const { forceCompactAt } = getContextLimits(parentModel);
|
|
10081
|
+
if (lastCallInputTokens > forceCompactAt && midTurnCompactions < MAX_MID_TURN_COMPACTIONS && !signal?.aborted) {
|
|
10082
|
+
midTurnCompactions++;
|
|
10083
|
+
await compactNow(`context ${lastCallInputTokens} > ${forceCompactAt}`);
|
|
10084
|
+
}
|
|
9865
10085
|
if (takeSteering && !signal?.aborted) {
|
|
9866
10086
|
const injected = (await takeSteering()).filter(
|
|
9867
10087
|
(e) => e.text.trim().length > 0 || (e.attachments?.length ?? 0) > 0
|
|
@@ -10911,6 +11131,10 @@ var HeadlessSession = class {
|
|
|
10911
11131
|
);
|
|
10912
11132
|
return;
|
|
10913
11133
|
case "error":
|
|
11134
|
+
if (typeof e.lastCallInputTokens === "number") {
|
|
11135
|
+
this.sessionStats.lastContextSize = e.lastCallInputTokens;
|
|
11136
|
+
this.persistStats();
|
|
11137
|
+
}
|
|
10914
11138
|
this.emit(
|
|
10915
11139
|
"error",
|
|
10916
11140
|
{ 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
|
}
|
|
@@ -9311,6 +9440,12 @@ var init_resolve = __esm({
|
|
|
9311
9440
|
});
|
|
9312
9441
|
|
|
9313
9442
|
// src/errors.ts
|
|
9443
|
+
function isContextOverflowError(message, code) {
|
|
9444
|
+
if (code === "context_overflow") {
|
|
9445
|
+
return true;
|
|
9446
|
+
}
|
|
9447
|
+
return OVERFLOW_PATTERN.test(message);
|
|
9448
|
+
}
|
|
9314
9449
|
function friendlyError(raw) {
|
|
9315
9450
|
for (const [pattern, message] of patterns) {
|
|
9316
9451
|
if (pattern.test(raw)) {
|
|
@@ -9319,10 +9454,11 @@ function friendlyError(raw) {
|
|
|
9319
9454
|
}
|
|
9320
9455
|
return `Something went wrong: ${raw}`;
|
|
9321
9456
|
}
|
|
9322
|
-
var patterns;
|
|
9457
|
+
var OVERFLOW_PATTERN, patterns;
|
|
9323
9458
|
var init_errors = __esm({
|
|
9324
9459
|
"src/errors.ts"() {
|
|
9325
9460
|
"use strict";
|
|
9461
|
+
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
9462
|
patterns = [
|
|
9327
9463
|
[
|
|
9328
9464
|
/Network error/i,
|
|
@@ -9342,6 +9478,14 @@ var init_errors = __esm({
|
|
|
9342
9478
|
"The AI service is temporarily unavailable. Please try again."
|
|
9343
9479
|
],
|
|
9344
9480
|
[/Stream stalled/i, "The connection was interrupted. Please try again."],
|
|
9481
|
+
[
|
|
9482
|
+
// A too-large-context failure that survived the automatic compact-and-retry
|
|
9483
|
+
// (or came from an older server). Kept ahead of the generic fallback so a
|
|
9484
|
+
// raw provider string (e.g. Gemini's INVALID_ARGUMENT JSON) never reaches
|
|
9485
|
+
// the user.
|
|
9486
|
+
OVERFLOW_PATTERN,
|
|
9487
|
+
"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."
|
|
9488
|
+
],
|
|
9345
9489
|
[
|
|
9346
9490
|
/content filter|Output blocked/i,
|
|
9347
9491
|
"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 +10131,33 @@ async function runTurn(params) {
|
|
|
9987
10131
|
let lastCallInputTokens = 0;
|
|
9988
10132
|
let lastCallCacheCreation = 0;
|
|
9989
10133
|
let lastCallCacheRead = 0;
|
|
9990
|
-
let
|
|
9991
|
-
const
|
|
10134
|
+
let recoveries = 0;
|
|
10135
|
+
const MAX_RECOVERIES = 2;
|
|
10136
|
+
let midTurnCompactions = 0;
|
|
10137
|
+
const MAX_MID_TURN_COMPACTIONS = 2;
|
|
10138
|
+
let overflowRecovered = false;
|
|
10139
|
+
const compactNow = async (reason) => {
|
|
10140
|
+
log14.warn("Compacting mid-turn", {
|
|
10141
|
+
requestId,
|
|
10142
|
+
reason,
|
|
10143
|
+
lastCallInputTokens
|
|
10144
|
+
});
|
|
10145
|
+
onEvent({ type: "status", message: "Compacting the conversation\u2026" });
|
|
10146
|
+
try {
|
|
10147
|
+
await triggerCompaction(state, apiConfig, {
|
|
10148
|
+
blocking: true,
|
|
10149
|
+
requestId,
|
|
10150
|
+
model,
|
|
10151
|
+
origin: "gate"
|
|
10152
|
+
});
|
|
10153
|
+
applyPendingSummaries(state);
|
|
10154
|
+
} catch (err) {
|
|
10155
|
+
log14.error("Mid-turn compaction failed", {
|
|
10156
|
+
requestId,
|
|
10157
|
+
error: err?.message ?? String(err)
|
|
10158
|
+
});
|
|
10159
|
+
}
|
|
10160
|
+
};
|
|
9992
10161
|
const statusWatcher = isFirstMessage ? { stop() {
|
|
9993
10162
|
}, pause() {
|
|
9994
10163
|
}, resume() {
|
|
@@ -10135,6 +10304,7 @@ async function runTurn(params) {
|
|
|
10135
10304
|
onEvent({ type: "tool_input_delta", id, name, result: content });
|
|
10136
10305
|
}
|
|
10137
10306
|
}
|
|
10307
|
+
let streamError = null;
|
|
10138
10308
|
try {
|
|
10139
10309
|
for await (const event of streamChatWithRetry(
|
|
10140
10310
|
{
|
|
@@ -10153,9 +10323,13 @@ async function runTurn(params) {
|
|
|
10153
10323
|
onRetry: (attempt) => {
|
|
10154
10324
|
onEvent({
|
|
10155
10325
|
type: "status",
|
|
10156
|
-
message: `Lost connection, retrying (attempt ${attempt + 2} of
|
|
10326
|
+
message: `Lost connection, retrying (attempt ${attempt + 2} of ${MAX_RETRIES})`
|
|
10157
10327
|
});
|
|
10158
|
-
}
|
|
10328
|
+
},
|
|
10329
|
+
// Watch the streamed reasoning/text for a runaway repetition loop
|
|
10330
|
+
// and abort the in-flight call early (RPT-1225). Surfaces as a
|
|
10331
|
+
// `repetition_loop` error handled below.
|
|
10332
|
+
detectRepetition: true
|
|
10159
10333
|
}
|
|
10160
10334
|
)) {
|
|
10161
10335
|
if (signal?.aborted) {
|
|
@@ -10179,7 +10353,7 @@ async function runTurn(params) {
|
|
|
10179
10353
|
emitTextBlockSnapshot2(false);
|
|
10180
10354
|
break;
|
|
10181
10355
|
}
|
|
10182
|
-
case "thinking":
|
|
10356
|
+
case "thinking": {
|
|
10183
10357
|
if (event.text === "") {
|
|
10184
10358
|
thinkingBlockStartTimes.push(event.ts);
|
|
10185
10359
|
if (textBlockOpen) {
|
|
@@ -10189,6 +10363,7 @@ async function runTurn(params) {
|
|
|
10189
10363
|
}
|
|
10190
10364
|
onEvent({ type: "thinking", text: event.text });
|
|
10191
10365
|
break;
|
|
10366
|
+
}
|
|
10192
10367
|
case "thinking_complete": {
|
|
10193
10368
|
const startedAt = thinkingBlockStartTimes[thinkingCompleteCount] ?? event.ts;
|
|
10194
10369
|
contentBlocks.push({
|
|
@@ -10286,6 +10461,10 @@ async function runTurn(params) {
|
|
|
10286
10461
|
outputTokens: event.usage.outputTokens,
|
|
10287
10462
|
cacheCreationTokens: event.usage.cacheCreationTokens,
|
|
10288
10463
|
cacheReadTokens: event.usage.cacheReadTokens,
|
|
10464
|
+
thinkingTokens: thinkingTokensFromBilling(
|
|
10465
|
+
event.billingEvents,
|
|
10466
|
+
event.usage.outputTokens
|
|
10467
|
+
) || void 0,
|
|
10289
10468
|
cost: nanoToDollars(event.cost),
|
|
10290
10469
|
billingEvents: event.billingEvents,
|
|
10291
10470
|
durationMs: Date.now() - iterStart,
|
|
@@ -10295,13 +10474,11 @@ async function runTurn(params) {
|
|
|
10295
10474
|
});
|
|
10296
10475
|
break;
|
|
10297
10476
|
case "error":
|
|
10298
|
-
|
|
10299
|
-
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
});
|
|
10304
|
-
return;
|
|
10477
|
+
streamError = { error: event.error, code: event.code };
|
|
10478
|
+
break;
|
|
10479
|
+
}
|
|
10480
|
+
if (streamError) {
|
|
10481
|
+
break;
|
|
10305
10482
|
}
|
|
10306
10483
|
}
|
|
10307
10484
|
} catch (err) {
|
|
@@ -10346,6 +10523,51 @@ async function runTurn(params) {
|
|
|
10346
10523
|
saveSession(state);
|
|
10347
10524
|
return;
|
|
10348
10525
|
}
|
|
10526
|
+
if (streamError) {
|
|
10527
|
+
const { error, code } = streamError;
|
|
10528
|
+
if (code === "repetition_loop") {
|
|
10529
|
+
if (recoveries < MAX_RECOVERIES && !signal?.aborted) {
|
|
10530
|
+
recoveries++;
|
|
10531
|
+
log14.warn("Repetition loop \u2014 nudging model to continue", {
|
|
10532
|
+
requestId,
|
|
10533
|
+
attempt: recoveries
|
|
10534
|
+
});
|
|
10535
|
+
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.";
|
|
10536
|
+
state.messages.push({
|
|
10537
|
+
role: "user",
|
|
10538
|
+
content: nudge,
|
|
10539
|
+
hidden: true
|
|
10540
|
+
});
|
|
10541
|
+
onEvent({ type: "user_message", text: nudge, hidden: true });
|
|
10542
|
+
continue;
|
|
10543
|
+
}
|
|
10544
|
+
statusWatcher.stop();
|
|
10545
|
+
saveSession(state);
|
|
10546
|
+
log14.warn("Repetition loop over recovery cap \u2014 ending turn", {
|
|
10547
|
+
requestId
|
|
10548
|
+
});
|
|
10549
|
+
onEvent({
|
|
10550
|
+
type: "error",
|
|
10551
|
+
error: "The model kept repeating itself without making progress, so Remy stopped the turn to avoid runaway cost. Try again, or rephrase your request.",
|
|
10552
|
+
lastCallInputTokens
|
|
10553
|
+
});
|
|
10554
|
+
return;
|
|
10555
|
+
}
|
|
10556
|
+
if (isContextOverflowError(error, code) && !overflowRecovered && !signal?.aborted) {
|
|
10557
|
+
overflowRecovered = true;
|
|
10558
|
+
await compactNow("context overflow");
|
|
10559
|
+
continue;
|
|
10560
|
+
}
|
|
10561
|
+
statusWatcher.stop();
|
|
10562
|
+
saveSession(state);
|
|
10563
|
+
onEvent({
|
|
10564
|
+
type: "error",
|
|
10565
|
+
error: friendlyError(error),
|
|
10566
|
+
...code ? { code } : {},
|
|
10567
|
+
lastCallInputTokens
|
|
10568
|
+
});
|
|
10569
|
+
return;
|
|
10570
|
+
}
|
|
10349
10571
|
if (contentBlocks.length > 0) {
|
|
10350
10572
|
state.messages.push({
|
|
10351
10573
|
role: "assistant",
|
|
@@ -10365,14 +10587,14 @@ async function runTurn(params) {
|
|
|
10365
10587
|
});
|
|
10366
10588
|
}
|
|
10367
10589
|
const toolCalls = getToolCalls(contentBlocks);
|
|
10368
|
-
if (toolCalls.length === 0 && (stopReason === "repetition" || stopReason === "max_tokens") &&
|
|
10369
|
-
|
|
10590
|
+
if (toolCalls.length === 0 && (stopReason === "repetition" || stopReason === "max_tokens") && recoveries < MAX_RECOVERIES && !signal?.aborted) {
|
|
10591
|
+
recoveries++;
|
|
10370
10592
|
log14.warn("Abnormal stop \u2014 nudging model to continue", {
|
|
10371
10593
|
requestId,
|
|
10372
10594
|
stopReason,
|
|
10373
|
-
attempt:
|
|
10595
|
+
attempt: recoveries
|
|
10374
10596
|
});
|
|
10375
|
-
const nudge = "Your previous response was cut off \u2014 it
|
|
10597
|
+
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
10598
|
state.messages.push({ role: "user", content: nudge, hidden: true });
|
|
10377
10599
|
onEvent({ type: "user_message", text: nudge, hidden: true });
|
|
10378
10600
|
continue;
|
|
@@ -10565,6 +10787,11 @@ async function runTurn(params) {
|
|
|
10565
10787
|
isToolError: r.isError
|
|
10566
10788
|
});
|
|
10567
10789
|
}
|
|
10790
|
+
const { forceCompactAt } = getContextLimits(parentModel);
|
|
10791
|
+
if (lastCallInputTokens > forceCompactAt && midTurnCompactions < MAX_MID_TURN_COMPACTIONS && !signal?.aborted) {
|
|
10792
|
+
midTurnCompactions++;
|
|
10793
|
+
await compactNow(`context ${lastCallInputTokens} > ${forceCompactAt}`);
|
|
10794
|
+
}
|
|
10568
10795
|
if (takeSteering && !signal?.aborted) {
|
|
10569
10796
|
const injected = (await takeSteering()).filter(
|
|
10570
10797
|
(e) => e.text.trim().length > 0 || (e.attachments?.length ?? 0) > 0
|
|
@@ -10606,6 +10833,7 @@ var init_agent = __esm({
|
|
|
10606
10833
|
init_sentinel();
|
|
10607
10834
|
init_trigger2();
|
|
10608
10835
|
init_surfaces();
|
|
10836
|
+
init_trigger();
|
|
10609
10837
|
init_toolRegistry();
|
|
10610
10838
|
init_historyLimits();
|
|
10611
10839
|
log14 = createLogger("agent");
|
|
@@ -11958,6 +12186,10 @@ var init_headless = __esm({
|
|
|
11958
12186
|
);
|
|
11959
12187
|
return;
|
|
11960
12188
|
case "error":
|
|
12189
|
+
if (typeof e.lastCallInputTokens === "number") {
|
|
12190
|
+
this.sessionStats.lastContextSize = e.lastCallInputTokens;
|
|
12191
|
+
this.persistStats();
|
|
12192
|
+
}
|
|
11961
12193
|
this.emit(
|
|
11962
12194
|
"error",
|
|
11963
12195
|
{ error: e.error, ...e.code ? { code: e.code } : {} },
|