dsh-client-auto-continue 0.7.1 → 0.7.3
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/README.md +10 -6
- package/README.zh.md +10 -6
- package/lib/client.js +101 -17
- package/lib/client.js.map +2 -2
- package/lib/index.js +8 -4
- package/lib/types/client/engine.d.ts +9 -4
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/settings-card.d.ts +2 -0
- package/lib/types/index.d.ts +12 -4
- package/package.json +1 -1
- package/src/client/engine.ts +91 -17
- package/src/client/locales.ts +13 -5
- package/src/client/settings-card.tsx +26 -0
- package/src/index.ts +8 -4
package/lib/index.js
CHANGED
|
@@ -45,10 +45,14 @@ var AutoContinueSchema = z.object({
|
|
|
45
45
|
loopGuard: z.boolean().default(true),
|
|
46
46
|
/** A model message shorter than this many chars counts as a short sentence (loop signal). */
|
|
47
47
|
loopShortChars: z.natural().min(1).default(40),
|
|
48
|
-
/** Consecutive short sentences with no tool call in between trip the loop guard. */
|
|
49
|
-
|
|
50
|
-
/** Consecutive
|
|
51
|
-
|
|
48
|
+
/** Consecutive short sentences within this window (ms) with no tool call in between trip the loop guard. */
|
|
49
|
+
loopWindowMs: z.natural().min(1e3).default(3e4),
|
|
50
|
+
/** Consecutive short sentences trip the loop guard. */
|
|
51
|
+
loopShortCount: z.natural().min(2).default(12),
|
|
52
|
+
/** Consecutive identical short sentences trip the loop guard (strongest spinning signal). */
|
|
53
|
+
loopRepeatText: z.natural().min(2).default(4),
|
|
54
|
+
/** Consecutive identical tool calls with identical arguments AND results trip the loop guard. */
|
|
55
|
+
loopToolRepeat: z.natural().min(2).default(5),
|
|
52
56
|
/** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
|
|
53
57
|
loopText: z.string().default("(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)")
|
|
54
58
|
});
|
|
@@ -55,10 +55,14 @@ export interface AutoContinueSettings {
|
|
|
55
55
|
loopGuard?: boolean;
|
|
56
56
|
/** A model message shorter than this many chars counts as a "short sentence" (loop signal). */
|
|
57
57
|
loopShortChars?: number;
|
|
58
|
-
/** Consecutive short sentences with no tool call in between trip the loop guard. */
|
|
58
|
+
/** Consecutive short sentences within this window (ms) with no tool call in between trip the loop guard. */
|
|
59
|
+
loopWindowMs?: number;
|
|
60
|
+
/** Consecutive short sentences trip the loop guard. */
|
|
59
61
|
loopShortCount?: number;
|
|
60
|
-
/** Consecutive identical tool calls trip the loop guard. */
|
|
62
|
+
/** Consecutive identical tool calls with identical arguments AND identical results trip the loop guard. */
|
|
61
63
|
loopToolRepeat?: number;
|
|
64
|
+
/** Consecutive identical short sentences trip the loop guard (strongest spinning signal). */
|
|
65
|
+
loopRepeatText?: number;
|
|
62
66
|
/** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
|
|
63
67
|
loopText?: string;
|
|
64
68
|
}
|
|
@@ -186,8 +190,9 @@ export declare class AutoContinueRunner {
|
|
|
186
190
|
/** 从 assistant/message 事件提取纯文本。 */
|
|
187
191
|
private assistantText;
|
|
188
192
|
/**
|
|
189
|
-
* loop guard 信号 1(空转):
|
|
190
|
-
* 短句 = 模型消息文本短于 loopShortChars;
|
|
193
|
+
* loop guard 信号 1(空转): 时间窗内连续短句且期间无工具调用。
|
|
194
|
+
* 短句 = 模型消息文本短于 loopShortChars; 长句、工具调用、或短句间隔超过
|
|
195
|
+
* loopWindowMs(正常思考的短文本散布在长时间里)都会重置计数。
|
|
191
196
|
*/
|
|
192
197
|
private onAssistantMessage;
|
|
193
198
|
/** 两个循环信号的公共检查; 命中且本回合未打断过则打断。 */
|
|
@@ -56,8 +56,12 @@ export declare const zh: {
|
|
|
56
56
|
'field.loopGuardHint': string;
|
|
57
57
|
'field.loopShortChars': string;
|
|
58
58
|
'field.loopShortCharsHint': string;
|
|
59
|
+
'field.loopWindowMs': string;
|
|
60
|
+
'field.loopWindowMsHint': string;
|
|
59
61
|
'field.loopShortCount': string;
|
|
60
62
|
'field.loopShortCountHint': string;
|
|
63
|
+
'field.loopRepeatText': string;
|
|
64
|
+
'field.loopRepeatTextHint': string;
|
|
61
65
|
'field.loopToolRepeat': string;
|
|
62
66
|
'field.loopToolRepeatHint': string;
|
|
63
67
|
'field.loopText': string;
|
|
@@ -25,7 +25,9 @@ export interface AutoContinueSettingsCardState extends CardShell {
|
|
|
25
25
|
notify: CardFieldState;
|
|
26
26
|
loopGuard: CardFieldState;
|
|
27
27
|
loopShortChars: CardFieldState;
|
|
28
|
+
loopWindowMs: CardFieldState;
|
|
28
29
|
loopShortCount: CardFieldState;
|
|
30
|
+
loopRepeatText: CardFieldState;
|
|
29
31
|
loopToolRepeat: CardFieldState;
|
|
30
32
|
loopText: CardFieldState;
|
|
31
33
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -51,9 +51,13 @@ export declare const AutoContinueSchema: z<Schemastery.ObjectS<{
|
|
|
51
51
|
loopGuard: z<boolean, boolean>;
|
|
52
52
|
/** A model message shorter than this many chars counts as a short sentence (loop signal). */
|
|
53
53
|
loopShortChars: z<number, number>;
|
|
54
|
-
/** Consecutive short sentences with no tool call in between trip the loop guard. */
|
|
54
|
+
/** Consecutive short sentences within this window (ms) with no tool call in between trip the loop guard. */
|
|
55
|
+
loopWindowMs: z<number, number>;
|
|
56
|
+
/** Consecutive short sentences trip the loop guard. */
|
|
55
57
|
loopShortCount: z<number, number>;
|
|
56
|
-
/** Consecutive identical
|
|
58
|
+
/** Consecutive identical short sentences trip the loop guard (strongest spinning signal). */
|
|
59
|
+
loopRepeatText: z<number, number>;
|
|
60
|
+
/** Consecutive identical tool calls with identical arguments AND results trip the loop guard. */
|
|
57
61
|
loopToolRepeat: z<number, number>;
|
|
58
62
|
/** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
|
|
59
63
|
loopText: z<string, string>;
|
|
@@ -100,9 +104,13 @@ export declare const AutoContinueSchema: z<Schemastery.ObjectS<{
|
|
|
100
104
|
loopGuard: z<boolean, boolean>;
|
|
101
105
|
/** A model message shorter than this many chars counts as a short sentence (loop signal). */
|
|
102
106
|
loopShortChars: z<number, number>;
|
|
103
|
-
/** Consecutive short sentences with no tool call in between trip the loop guard. */
|
|
107
|
+
/** Consecutive short sentences within this window (ms) with no tool call in between trip the loop guard. */
|
|
108
|
+
loopWindowMs: z<number, number>;
|
|
109
|
+
/** Consecutive short sentences trip the loop guard. */
|
|
104
110
|
loopShortCount: z<number, number>;
|
|
105
|
-
/** Consecutive identical
|
|
111
|
+
/** Consecutive identical short sentences trip the loop guard (strongest spinning signal). */
|
|
112
|
+
loopRepeatText: z<number, number>;
|
|
113
|
+
/** Consecutive identical tool calls with identical arguments AND results trip the loop guard. */
|
|
106
114
|
loopToolRepeat: z<number, number>;
|
|
107
115
|
/** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
|
|
108
116
|
loopText: z<string, string>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-client-auto-continue",
|
|
3
3
|
"description": "DSH Web UI plugin: automatically sends \"继续\" (continue) when a request is interrupted by network errors or other non-human causes",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
package/src/client/engine.ts
CHANGED
|
@@ -64,10 +64,14 @@ export interface AutoContinueSettings {
|
|
|
64
64
|
loopGuard?: boolean;
|
|
65
65
|
/** A model message shorter than this many chars counts as a "short sentence" (loop signal). */
|
|
66
66
|
loopShortChars?: number;
|
|
67
|
-
/** Consecutive short sentences with no tool call in between trip the loop guard. */
|
|
67
|
+
/** Consecutive short sentences within this window (ms) with no tool call in between trip the loop guard. */
|
|
68
|
+
loopWindowMs?: number;
|
|
69
|
+
/** Consecutive short sentences trip the loop guard. */
|
|
68
70
|
loopShortCount?: number;
|
|
69
|
-
/** Consecutive identical tool calls trip the loop guard. */
|
|
71
|
+
/** Consecutive identical tool calls with identical arguments AND identical results trip the loop guard. */
|
|
70
72
|
loopToolRepeat?: number;
|
|
73
|
+
/** Consecutive identical short sentences trip the loop guard (strongest spinning signal). */
|
|
74
|
+
loopRepeatText?: number;
|
|
71
75
|
/** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
|
|
72
76
|
loopText?: string;
|
|
73
77
|
}
|
|
@@ -98,8 +102,10 @@ export const DEFAULT_CONFIG: AutoContinueConfig = {
|
|
|
98
102
|
paused: false,
|
|
99
103
|
loopGuard: true,
|
|
100
104
|
loopShortChars: 40,
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
loopWindowMs: 30000,
|
|
106
|
+
loopShortCount: 12,
|
|
107
|
+
loopRepeatText: 4,
|
|
108
|
+
loopToolRepeat: 5,
|
|
103
109
|
loopText: '(检测到你可能陷入循环, 请停止重复刚才的动作, 换一种方式继续)',
|
|
104
110
|
};
|
|
105
111
|
|
|
@@ -152,7 +158,9 @@ export function resolveConfig(section: AutoContinueSettings | undefined): AutoCo
|
|
|
152
158
|
paused: booleanOr(value.paused, DEFAULT_CONFIG.paused),
|
|
153
159
|
loopGuard: booleanOr(value.loopGuard, DEFAULT_CONFIG.loopGuard),
|
|
154
160
|
loopShortChars: Math.max(1, numberOr(value.loopShortChars, DEFAULT_CONFIG.loopShortChars)),
|
|
161
|
+
loopWindowMs: Math.max(1000, numberOr(value.loopWindowMs, DEFAULT_CONFIG.loopWindowMs)),
|
|
155
162
|
loopShortCount: Math.max(2, numberOr(value.loopShortCount, DEFAULT_CONFIG.loopShortCount)),
|
|
163
|
+
loopRepeatText: Math.max(2, numberOr(value.loopRepeatText, DEFAULT_CONFIG.loopRepeatText)),
|
|
156
164
|
loopToolRepeat: Math.max(2, numberOr(value.loopToolRepeat, DEFAULT_CONFIG.loopToolRepeat)),
|
|
157
165
|
loopText:
|
|
158
166
|
typeof value.loopText === 'string' && value.loopText.trim() !== ''
|
|
@@ -596,8 +604,28 @@ interface SessionState {
|
|
|
596
604
|
pendingRecoveryAt: number;
|
|
597
605
|
/** 当前连续短句数(loop guard 信号 1: 空转)。 */
|
|
598
606
|
shortRun: number;
|
|
599
|
-
/**
|
|
600
|
-
|
|
607
|
+
/** 最后一条短句的时间(时间窗判定用)。 */
|
|
608
|
+
lastShortAt: number;
|
|
609
|
+
/** 最后一条模型消息的文本(相同文本重复判定用)。 */
|
|
610
|
+
lastAssistantText: string;
|
|
611
|
+
/** 连续相同文本消息数(最强空转信号, 不限长度)。 */
|
|
612
|
+
sameTextRun: number;
|
|
613
|
+
/**
|
|
614
|
+
* 工具重复信号(loop guard 信号 2: 死循环)。
|
|
615
|
+
* 只有「同工具 + 同参数 + 同结果」的连续调用才累计; 参数或结果有变化视为有进展, 计数重置。
|
|
616
|
+
*/
|
|
617
|
+
toolRun:
|
|
618
|
+
| {
|
|
619
|
+
/** 工具名 + 参数(用于判定是否同一调用)。 */
|
|
620
|
+
key: string;
|
|
621
|
+
/** 连续相同调用数(结果确认后更新)。 */
|
|
622
|
+
count: number;
|
|
623
|
+
/** 上次该调用的结果摘要(比较用)。 */
|
|
624
|
+
lastResult: string | undefined;
|
|
625
|
+
/** 本次调用等待结果确认。 */
|
|
626
|
+
waiting: boolean;
|
|
627
|
+
}
|
|
628
|
+
| undefined;
|
|
601
629
|
/** 本回合已触发过 loop guard(防重复打断)。 */
|
|
602
630
|
loopFired: boolean;
|
|
603
631
|
/** 我们主动 cancel 过本回合(区分用户停止)。 */
|
|
@@ -620,6 +648,9 @@ const freshState = (): SessionState => ({
|
|
|
620
648
|
lastTurn: undefined,
|
|
621
649
|
pendingRecoveryAt: 0,
|
|
622
650
|
shortRun: 0,
|
|
651
|
+
lastShortAt: 0,
|
|
652
|
+
lastAssistantText: '',
|
|
653
|
+
sameTextRun: 0,
|
|
623
654
|
toolRun: undefined,
|
|
624
655
|
loopFired: false,
|
|
625
656
|
loopCancelled: false,
|
|
@@ -769,19 +800,35 @@ export class AutoContinueRunner {
|
|
|
769
800
|
const state = this.state(frame.sessionId);
|
|
770
801
|
state.lastTool = name;
|
|
771
802
|
state.lastToolResult = 'pending'; // 已发起, 尚未见结果
|
|
772
|
-
// loop guard 信号 2:
|
|
803
|
+
// loop guard 信号 2: 同工具+同参数才可能是循环; 参数变化 = 有进展
|
|
804
|
+
// (工具调用本身也重置短句信号)。计数在结果确认后才推进。
|
|
773
805
|
state.shortRun = 0;
|
|
774
|
-
|
|
775
|
-
|
|
806
|
+
const key = `${name}\n${frame.event.data.arguments}`;
|
|
807
|
+
if (state.toolRun?.key === key) {
|
|
808
|
+
state.toolRun.waiting = true; // 结果到达时与上次结果比较
|
|
776
809
|
} else {
|
|
777
|
-
state.toolRun = {
|
|
810
|
+
state.toolRun = { key, count: 1, lastResult: undefined, waiting: false };
|
|
778
811
|
}
|
|
779
|
-
this.checkLoop(frame.sessionId, state);
|
|
780
812
|
}
|
|
781
813
|
} else if (frame.event.type === 'tool/result') {
|
|
782
814
|
const state = this.state(frame.sessionId);
|
|
783
815
|
if (state.lastToolResult === 'pending') {
|
|
784
|
-
|
|
816
|
+
const facts = toolResultFacts(frame.event.data);
|
|
817
|
+
state.lastToolResult = facts;
|
|
818
|
+
// 结果确认: 与上次相同 → 计数推进; 不同 → 有进展, 重置
|
|
819
|
+
const run = state.toolRun;
|
|
820
|
+
if (run !== undefined && run.waiting) {
|
|
821
|
+
run.waiting = false;
|
|
822
|
+
if (run.lastResult !== undefined && run.lastResult === facts.excerpt) {
|
|
823
|
+
run.count += 1;
|
|
824
|
+
this.checkLoop(frame.sessionId, state);
|
|
825
|
+
} else {
|
|
826
|
+
run.lastResult = facts.excerpt;
|
|
827
|
+
run.count = 1;
|
|
828
|
+
}
|
|
829
|
+
} else if (run !== undefined && !run.waiting) {
|
|
830
|
+
run.lastResult = facts.excerpt;
|
|
831
|
+
}
|
|
785
832
|
}
|
|
786
833
|
} else if (frame.event.type === 'assistant/message') {
|
|
787
834
|
const state = this.state(frame.sessionId);
|
|
@@ -812,8 +859,9 @@ export class AutoContinueRunner {
|
|
|
812
859
|
}
|
|
813
860
|
|
|
814
861
|
/**
|
|
815
|
-
* loop guard 信号 1(空转):
|
|
816
|
-
* 短句 = 模型消息文本短于 loopShortChars;
|
|
862
|
+
* loop guard 信号 1(空转): 时间窗内连续短句且期间无工具调用。
|
|
863
|
+
* 短句 = 模型消息文本短于 loopShortChars; 长句、工具调用、或短句间隔超过
|
|
864
|
+
* loopWindowMs(正常思考的短文本散布在长时间里)都会重置计数。
|
|
817
865
|
*/
|
|
818
866
|
private onAssistantMessage(
|
|
819
867
|
sessionId: SessionId,
|
|
@@ -822,10 +870,26 @@ export class AutoContinueRunner {
|
|
|
822
870
|
): void {
|
|
823
871
|
if (!this.getConfig().loopGuard) return;
|
|
824
872
|
const text = this.assistantText(event);
|
|
825
|
-
|
|
873
|
+
const trimmed = text.trim();
|
|
874
|
+
// 相同文本重复(不限长度): 模型反复输出完全相同的消息是最强的循环信号,
|
|
875
|
+
// 例如 "Let me test variants of the regex..." 连续 7 遍
|
|
876
|
+
if (trimmed !== '' && trimmed === state.lastAssistantText) {
|
|
877
|
+
state.sameTextRun += 1;
|
|
878
|
+
} else {
|
|
879
|
+
state.lastAssistantText = trimmed;
|
|
880
|
+
state.sameTextRun = 1;
|
|
881
|
+
}
|
|
882
|
+
// 短句计数(长度 < loopShortChars 且落在时间窗内): 空转信号
|
|
883
|
+
if (trimmed.length < this.getConfig().loopShortChars) {
|
|
884
|
+
const now = Date.now();
|
|
885
|
+
if (now - state.lastShortAt > this.getConfig().loopWindowMs) {
|
|
886
|
+
state.shortRun = 0; // 超过时间窗: 上一次短句太久远, 不算连续
|
|
887
|
+
}
|
|
826
888
|
state.shortRun += 1;
|
|
889
|
+
state.lastShortAt = now;
|
|
827
890
|
} else {
|
|
828
891
|
state.shortRun = 0; // 长句 = 有实际输出, 重置
|
|
892
|
+
state.lastShortAt = 0;
|
|
829
893
|
}
|
|
830
894
|
this.checkLoop(sessionId, state);
|
|
831
895
|
}
|
|
@@ -836,11 +900,15 @@ export class AutoContinueRunner {
|
|
|
836
900
|
if (state.loopFired) return;
|
|
837
901
|
if (!state.running) return; // 只干预运行中的回合
|
|
838
902
|
const config = this.getConfig();
|
|
839
|
-
if (state.
|
|
903
|
+
if (state.sameTextRun >= config.loopRepeatText) {
|
|
904
|
+
this.log(`检测到空转循环 ${sessionId}: 连续 ${state.sameTextRun} 条相同消息`);
|
|
905
|
+
void this.interruptLoop(sessionId, state);
|
|
906
|
+
} else if (state.shortRun >= config.loopShortCount) {
|
|
840
907
|
this.log(`检测到空转循环 ${sessionId}: 连续 ${state.shortRun} 条短句且无工具调用`);
|
|
841
908
|
void this.interruptLoop(sessionId, state);
|
|
842
909
|
} else if (state.toolRun !== undefined && state.toolRun.count >= config.loopToolRepeat) {
|
|
843
|
-
|
|
910
|
+
const toolName = state.toolRun.key.split('\n')[0] ?? '?';
|
|
911
|
+
this.log(`检测到工具死循环 ${sessionId}: 「${toolName}」连续 ${state.toolRun.count} 次(同参数同结果)`);
|
|
844
912
|
void this.interruptLoop(sessionId, state);
|
|
845
913
|
}
|
|
846
914
|
}
|
|
@@ -882,6 +950,9 @@ export class AutoContinueRunner {
|
|
|
882
950
|
state.lastToolResult = undefined;
|
|
883
951
|
// loop guard 状态按回合重置
|
|
884
952
|
state.shortRun = 0;
|
|
953
|
+
state.lastShortAt = 0;
|
|
954
|
+
state.lastAssistantText = '';
|
|
955
|
+
state.sameTextRun = 0;
|
|
885
956
|
state.toolRun = undefined;
|
|
886
957
|
state.loopFired = false;
|
|
887
958
|
state.loopCancelled = false;
|
|
@@ -905,6 +976,9 @@ export class AutoContinueRunner {
|
|
|
905
976
|
state.consecutive = 0;
|
|
906
977
|
state.pendingRecoveryAt = 0;
|
|
907
978
|
state.shortRun = 0;
|
|
979
|
+
state.lastShortAt = 0;
|
|
980
|
+
state.lastAssistantText = '';
|
|
981
|
+
state.sameTextRun = 0;
|
|
908
982
|
state.toolRun = undefined;
|
|
909
983
|
state.lastAttemptAt = 0;
|
|
910
984
|
this.schedule(sessionId, 'loop:aborted');
|
package/src/client/locales.ts
CHANGED
|
@@ -57,10 +57,14 @@ export const zh = {
|
|
|
57
57
|
'field.loopGuardHint': '检测运行中的回合空转: 连续短句且无工具调用, 或连续调用相同工具时, 自动取消并用循环提示文本重启回合。',
|
|
58
58
|
'field.loopShortChars': '短句长度上限 (字符)',
|
|
59
59
|
'field.loopShortCharsHint': '模型消息文本短于该值计为一条短句(空转信号)。',
|
|
60
|
+
'field.loopWindowMs': '短句时间窗 (ms)',
|
|
61
|
+
'field.loopWindowMsHint': '连续短句必须落在这个时间窗内; 正常思考的短文本散布在长时间里不会被误判。',
|
|
60
62
|
'field.loopShortCount': '连续短句阈值',
|
|
61
|
-
'field.loopShortCountHint': '
|
|
62
|
-
'field.
|
|
63
|
-
'field.
|
|
63
|
+
'field.loopShortCountHint': '时间窗内连续多少条短句且期间无工具调用时判定空转循环。',
|
|
64
|
+
'field.loopRepeatText': '相同消息重复次数',
|
|
65
|
+
'field.loopRepeatTextHint': '连续输出多少条完全相同的消息时判定空转(最强信号, 不限长度, 如模型反复说同一句话)。',
|
|
66
|
+
'field.loopToolRepeat': '同工具重复次数',
|
|
67
|
+
'field.loopToolRepeatHint': '同工具+同参数+同结果的连续调用多少次时判定死循环; 参数或结果有变化视为有进展。',
|
|
64
68
|
'field.loopText': '循环提示文本',
|
|
65
69
|
'field.loopTextHint': '打断后重启回合时发送的文本, 支持 {tool} 占位符。',
|
|
66
70
|
'stats.byCode': '按错误码统计',
|
|
@@ -143,10 +147,14 @@ export const en: Record<SettingsCardKey, string> = {
|
|
|
143
147
|
'field.loopGuardHint': 'Detects a running turn spinning in place — many short sentences with no tool calls, or the same tool repeating — cancels it and restarts with the loop text.',
|
|
144
148
|
'field.loopShortChars': 'Short-sentence max (chars)',
|
|
145
149
|
'field.loopShortCharsHint': 'A model message shorter than this counts as a short sentence (spinning signal).',
|
|
150
|
+
'field.loopWindowMs': 'Short-sentence window (ms)',
|
|
151
|
+
'field.loopWindowMsHint': 'Consecutive short sentences must land inside this window; normal thinking spread over time is not misjudged.',
|
|
146
152
|
'field.loopShortCount': 'Short-sentence threshold',
|
|
147
|
-
'field.loopShortCountHint': 'How many consecutive short sentences with no tool call trip the loop guard.',
|
|
153
|
+
'field.loopShortCountHint': 'How many consecutive short sentences inside the window, with no tool call, trip the loop guard.',
|
|
154
|
+
'field.loopRepeatText': 'Identical message count',
|
|
155
|
+
'field.loopRepeatTextHint': 'How many consecutive identical messages trip the guard (strongest signal, any length, e.g. the model repeating the same line).',
|
|
148
156
|
'field.loopToolRepeat': 'Same-tool repeat count',
|
|
149
|
-
'field.loopToolRepeatHint': 'How many consecutive
|
|
157
|
+
'field.loopToolRepeatHint': 'How many consecutive calls of the same tool with identical arguments and results trip the loop guard; a changed argument or result counts as progress.',
|
|
150
158
|
'field.loopText': 'Loop text',
|
|
151
159
|
'field.loopTextHint': 'Text sent after the loop guard restarts a turn; supports the {tool} placeholder.',
|
|
152
160
|
'stats.byCode': 'By error code',
|
|
@@ -57,7 +57,9 @@ export interface AutoContinueSettingsCardState extends CardShell {
|
|
|
57
57
|
notify: CardFieldState;
|
|
58
58
|
loopGuard: CardFieldState;
|
|
59
59
|
loopShortChars: CardFieldState;
|
|
60
|
+
loopWindowMs: CardFieldState;
|
|
60
61
|
loopShortCount: CardFieldState;
|
|
62
|
+
loopRepeatText: CardFieldState;
|
|
61
63
|
loopToolRepeat: CardFieldState;
|
|
62
64
|
loopText: CardFieldState;
|
|
63
65
|
}
|
|
@@ -101,7 +103,9 @@ export class AutoContinueSettingsCardController {
|
|
|
101
103
|
booleanField('notify'),
|
|
102
104
|
booleanField('loopGuard'),
|
|
103
105
|
numberField('loopShortChars', 1),
|
|
106
|
+
numberField('loopWindowMs', 1000),
|
|
104
107
|
numberField('loopShortCount', 2),
|
|
108
|
+
numberField('loopRepeatText', 2),
|
|
105
109
|
numberField('loopToolRepeat', 2),
|
|
106
110
|
textField('loopText'),
|
|
107
111
|
]);
|
|
@@ -132,7 +136,9 @@ export class AutoContinueSettingsCardController {
|
|
|
132
136
|
notify: this.form.field('notify'),
|
|
133
137
|
loopGuard: this.form.field('loopGuard'),
|
|
134
138
|
loopShortChars: this.form.field('loopShortChars'),
|
|
139
|
+
loopWindowMs: this.form.field('loopWindowMs'),
|
|
135
140
|
loopShortCount: this.form.field('loopShortCount'),
|
|
141
|
+
loopRepeatText: this.form.field('loopRepeatText'),
|
|
136
142
|
loopToolRepeat: this.form.field('loopToolRepeat'),
|
|
137
143
|
loopText: this.form.field('loopText'),
|
|
138
144
|
};
|
|
@@ -617,6 +623,16 @@ export function AutoContinueSettingsCard(props: AutoContinueSettingsCardProps) {
|
|
|
617
623
|
onEdit={(text) => props.edit('loopShortChars', text)}
|
|
618
624
|
onReset={() => props.resetField('loopShortChars')}
|
|
619
625
|
/>
|
|
626
|
+
<ValueField
|
|
627
|
+
id="auto-continue-loop-window-ms"
|
|
628
|
+
label={t('field.loopWindowMs')}
|
|
629
|
+
hint={t('field.loopWindowMsHint')}
|
|
630
|
+
numeric
|
|
631
|
+
{...shared}
|
|
632
|
+
{...state.loopWindowMs}
|
|
633
|
+
onEdit={(text) => props.edit('loopWindowMs', text)}
|
|
634
|
+
onReset={() => props.resetField('loopWindowMs')}
|
|
635
|
+
/>
|
|
620
636
|
<ValueField
|
|
621
637
|
id="auto-continue-loop-short-count"
|
|
622
638
|
label={t('field.loopShortCount')}
|
|
@@ -637,6 +653,16 @@ export function AutoContinueSettingsCard(props: AutoContinueSettingsCardProps) {
|
|
|
637
653
|
onEdit={(text) => props.edit('loopToolRepeat', text)}
|
|
638
654
|
onReset={() => props.resetField('loopToolRepeat')}
|
|
639
655
|
/>
|
|
656
|
+
<ValueField
|
|
657
|
+
id="auto-continue-loop-repeat-text"
|
|
658
|
+
label={t('field.loopRepeatText')}
|
|
659
|
+
hint={t('field.loopRepeatTextHint')}
|
|
660
|
+
numeric
|
|
661
|
+
{...shared}
|
|
662
|
+
{...state.loopRepeatText}
|
|
663
|
+
onEdit={(text) => props.edit('loopRepeatText', text)}
|
|
664
|
+
onReset={() => props.resetField('loopRepeatText')}
|
|
665
|
+
/>
|
|
640
666
|
<ValueField
|
|
641
667
|
id="auto-continue-loop-text"
|
|
642
668
|
label={t('field.loopText')}
|
package/src/index.ts
CHANGED
|
@@ -60,10 +60,14 @@ export const AutoContinueSchema = z.object({
|
|
|
60
60
|
loopGuard: z.boolean().default(true),
|
|
61
61
|
/** A model message shorter than this many chars counts as a short sentence (loop signal). */
|
|
62
62
|
loopShortChars: z.natural().min(1).default(40),
|
|
63
|
-
/** Consecutive short sentences with no tool call in between trip the loop guard. */
|
|
64
|
-
|
|
65
|
-
/** Consecutive
|
|
66
|
-
|
|
63
|
+
/** Consecutive short sentences within this window (ms) with no tool call in between trip the loop guard. */
|
|
64
|
+
loopWindowMs: z.natural().min(1000).default(30000),
|
|
65
|
+
/** Consecutive short sentences trip the loop guard. */
|
|
66
|
+
loopShortCount: z.natural().min(2).default(12),
|
|
67
|
+
/** Consecutive identical short sentences trip the loop guard (strongest spinning signal). */
|
|
68
|
+
loopRepeatText: z.natural().min(2).default(4),
|
|
69
|
+
/** Consecutive identical tool calls with identical arguments AND results trip the loop guard. */
|
|
70
|
+
loopToolRepeat: z.natural().min(2).default(5),
|
|
67
71
|
/** Text sent after the loop guard cancels and restarts a turn (supports {tool}). */
|
|
68
72
|
loopText: z
|
|
69
73
|
.string()
|