@moxxy/mode-default 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/turn-iterator.d.ts +7 -1
- package/dist/turn-iterator.d.ts.map +1 -1
- package/dist/turn-iterator.js +49 -3
- package/dist/turn-iterator.js.map +1 -1
- package/package.json +5 -5
- package/src/index.test.ts +83 -0
- package/src/turn-iterator.ts +51 -3
package/dist/turn-iterator.d.ts
CHANGED
|
@@ -13,7 +13,13 @@ export declare const DEFAULT_MODE_NAME = "default";
|
|
|
13
13
|
* budget and is handled before this.)
|
|
14
14
|
*/
|
|
15
15
|
export declare const MAX_CONSECUTIVE_RETRIES = 6;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Override the retry back-off sleep (test seam). Returns a restore fn that
|
|
18
|
+
* callers MUST invoke (in a `finally`) — `sleepImpl` is a module-scoped
|
|
19
|
+
* singleton shared process-wide, so a leaked override bleeds the fake sleep
|
|
20
|
+
* into every other turn/test running in the same worker (parallel subagent
|
|
21
|
+
* fan-out, multiple Sessions in one host). Test-only; never call from prod.
|
|
22
|
+
*/
|
|
17
23
|
export declare function __setRetrySleepForTests(fn: (ms: number, signal: AbortSignal) => Promise<void>): () => void;
|
|
18
24
|
export declare function runDefaultMode(ctx: ModeContext): AsyncIterable<MoxxyEvent>;
|
|
19
25
|
//# sourceMappingURL=turn-iterator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-iterator.d.ts","sourceRoot":"","sources":["../src/turn-iterator.ts"],"names":[],"mappings":"AAAA,OAAO,EAWL,KAAK,WAAW,EAChB,KAAK,UAAU,EAEhB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAE3C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AA0BzC
|
|
1
|
+
{"version":3,"file":"turn-iterator.d.ts","sourceRoot":"","sources":["../src/turn-iterator.ts"],"names":[],"mappings":"AAAA,OAAO,EAWL,KAAK,WAAW,EAChB,KAAK,UAAU,EAEhB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,iBAAiB,YAAY,CAAC;AAE3C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AA0BzC;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GACrD,MAAM,IAAI,CAMZ;AAED,wBAAuB,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC,CA+PjF"}
|
package/dist/turn-iterator.js
CHANGED
|
@@ -35,7 +35,13 @@ let sleepImpl = (ms, signal) => new Promise((resolve) => {
|
|
|
35
35
|
};
|
|
36
36
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
37
37
|
});
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Override the retry back-off sleep (test seam). Returns a restore fn that
|
|
40
|
+
* callers MUST invoke (in a `finally`) — `sleepImpl` is a module-scoped
|
|
41
|
+
* singleton shared process-wide, so a leaked override bleeds the fake sleep
|
|
42
|
+
* into every other turn/test running in the same worker (parallel subagent
|
|
43
|
+
* fan-out, multiple Sessions in one host). Test-only; never call from prod.
|
|
44
|
+
*/
|
|
39
45
|
export function __setRetrySleepForTests(fn) {
|
|
40
46
|
const prev = sleepImpl;
|
|
41
47
|
sleepImpl = fn;
|
|
@@ -48,7 +54,14 @@ export async function* runDefaultMode(ctx) {
|
|
|
48
54
|
// glitch causing an infinite retry, bad prompt, etc.) — primary
|
|
49
55
|
// termination signal is the stuck-loop detector, which catches the
|
|
50
56
|
// common "model keeps calling the same tool" case ~10 iterations in.
|
|
51
|
-
|
|
57
|
+
// Coerce a caller/config-supplied bound to a positive integer; a degenerate
|
|
58
|
+
// value (0, negative, NaN, fractional) would otherwise make the loop never
|
|
59
|
+
// run and emit a misleading "exceeded maxIterations" fatal. Treat anything
|
|
60
|
+
// un-coercible (NaN) as the default rather than failing the turn.
|
|
61
|
+
const requestedMaxIterations = ctx.maxIterations;
|
|
62
|
+
const maxIterations = typeof requestedMaxIterations === 'number' && Number.isFinite(requestedMaxIterations)
|
|
63
|
+
? Math.max(1, Math.floor(requestedMaxIterations))
|
|
64
|
+
: 500;
|
|
52
65
|
const detector = createStuckLoopDetector();
|
|
53
66
|
// Reactive-compaction budget per overflow episode. If the provider keeps
|
|
54
67
|
// rejecting for context size even after compacting this many times, give up
|
|
@@ -101,6 +114,21 @@ export async function* runDefaultMode(ctx) {
|
|
|
101
114
|
iteration,
|
|
102
115
|
stablePrefixIndex,
|
|
103
116
|
});
|
|
117
|
+
// A user cancellation WHILE the provider stream was being consumed surfaces
|
|
118
|
+
// as a non-retryable provider `error` ("The operation was aborted") rather
|
|
119
|
+
// than a clean abort — collectProviderStream catches the fetch AbortError
|
|
120
|
+
// and classifies it as fatal. Treat it as the cancellation it is so
|
|
121
|
+
// downstream channels render a 'stopped' turn, not a failed/error turn.
|
|
122
|
+
if (ctx.signal.aborted) {
|
|
123
|
+
yield await ctx.emit({
|
|
124
|
+
type: 'abort',
|
|
125
|
+
sessionId: ctx.sessionId,
|
|
126
|
+
turnId: ctx.turnId,
|
|
127
|
+
source: 'system',
|
|
128
|
+
reason: 'signal aborted during provider stream',
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
104
132
|
yield await ctx.emit({
|
|
105
133
|
type: 'provider_response',
|
|
106
134
|
sessionId: ctx.sessionId,
|
|
@@ -117,9 +145,12 @@ export async function* runDefaultMode(ctx) {
|
|
|
117
145
|
// the auto-compact-on-overflow path.
|
|
118
146
|
if (isContextOverflowError(error.message) &&
|
|
119
147
|
reactiveCompactions < MAX_REACTIVE_COMPACTIONS) {
|
|
120
|
-
reactiveCompactions += 1;
|
|
121
148
|
const compacted = await runCompactionIfNeeded(ctx, { force: true });
|
|
122
149
|
if (compacted) {
|
|
150
|
+
// Only count an attempt that actually compacted against the budget —
|
|
151
|
+
// a no-op (overflow lives in the un-compactable recent tail) must not
|
|
152
|
+
// deny a later, genuinely compactable overflow its retry.
|
|
153
|
+
reactiveCompactions += 1;
|
|
123
154
|
yield await ctx.emit({
|
|
124
155
|
type: 'error',
|
|
125
156
|
sessionId: ctx.sessionId,
|
|
@@ -208,6 +239,21 @@ export async function* runDefaultMode(ctx) {
|
|
|
208
239
|
if (stuck)
|
|
209
240
|
return;
|
|
210
241
|
if (text || stopReason === 'end_turn' || toolUses.length === 0) {
|
|
242
|
+
// A completion with no text, no tool uses, and a non-natural stop (e.g.
|
|
243
|
+
// 'max_tokens' truncated to nothing) yields a blank assistant bubble that
|
|
244
|
+
// silently swallows the truncation signal. Surface a retryable note so the
|
|
245
|
+
// user sees why the turn produced nothing, alongside the (preserved)
|
|
246
|
+
// empty assistant_message.
|
|
247
|
+
if (!text && toolUses.length === 0 && stopReason !== 'end_turn') {
|
|
248
|
+
yield await ctx.emit({
|
|
249
|
+
type: 'error',
|
|
250
|
+
sessionId: ctx.sessionId,
|
|
251
|
+
turnId: ctx.turnId,
|
|
252
|
+
source: 'system',
|
|
253
|
+
kind: 'retryable',
|
|
254
|
+
message: `provider returned an empty completion (stopReason: ${stopReason ?? 'unknown'})`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
211
257
|
yield await ctx.emit({
|
|
212
258
|
type: 'assistant_message',
|
|
213
259
|
sessionId: ctx.sessionId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-iterator.js","sourceRoot":"","sources":["../src/turn-iterator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,qBAAqB,EACrB,uBAAuB,EACvB,0BAA0B,EAC1B,eAAe,EACf,sBAAsB,EACtB,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,gBAAgB,GAIjB,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAE3C;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAEzC,6EAA6E;AAC7E,SAAS,cAAc,CAAC,OAAe;IACrC,MAAM,IAAI,GAAG,GAAG,CAAC;IACjB,MAAM,GAAG,GAAG,MAAM,CAAC;IACnB,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC;AAED,8EAA8E;AAC9E,gFAAgF;AAChF,oDAAoD;AACpD,IAAI,SAAS,GAAG,CAAC,EAAU,EAAE,MAAmB,EAAiB,EAAE,CACjE,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;IAC5B,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,OAAO,EAAE,CAAC;IACrC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;QAC5B,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7C,OAAO,EAAE,CAAC;IACZ,CAAC,EAAE,EAAE,CAAC,CAAC;IACP,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,YAAY,CAAC,KAAK,CAAC,CAAC;QACpB,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC;IACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AAC5D,CAAC,CAAC,CAAC;AAEL
|
|
1
|
+
{"version":3,"file":"turn-iterator.js","sourceRoot":"","sources":["../src/turn-iterator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,2BAA2B,EAC3B,qBAAqB,EACrB,uBAAuB,EACvB,0BAA0B,EAC1B,eAAe,EACf,sBAAsB,EACtB,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,gBAAgB,GAIjB,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAE3C;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAEzC,6EAA6E;AAC7E,SAAS,cAAc,CAAC,OAAe;IACrC,MAAM,IAAI,GAAG,GAAG,CAAC;IACjB,MAAM,GAAG,GAAG,MAAM,CAAC;IACnB,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC;AAED,8EAA8E;AAC9E,gFAAgF;AAChF,oDAAoD;AACpD,IAAI,SAAS,GAAG,CAAC,EAAU,EAAE,MAAmB,EAAiB,EAAE,CACjE,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;IAC5B,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,OAAO,EAAE,CAAC;IACrC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;QAC5B,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7C,OAAO,EAAE,CAAC;IACZ,CAAC,EAAE,EAAE,CAAC,CAAC;IACP,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,YAAY,CAAC,KAAK,CAAC,CAAC;QACpB,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC;IACF,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AAC5D,CAAC,CAAC,CAAC;AAEL;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CACrC,EAAsD;IAEtD,MAAM,IAAI,GAAG,SAAS,CAAC;IACvB,SAAS,GAAG,EAAE,CAAC;IACf,OAAO,GAAG,EAAE;QACV,SAAS,GAAG,IAAI,CAAC;IACnB,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,cAAc,CAAC,GAAgB;IACpD,qEAAqE;IACrE,gEAAgE;IAChE,mEAAmE;IACnE,qEAAqE;IACrE,4EAA4E;IAC5E,2EAA2E;IAC3E,2EAA2E;IAC3E,kEAAkE;IAClE,MAAM,sBAAsB,GAAG,GAAG,CAAC,aAAa,CAAC;IACjD,MAAM,aAAa,GACjB,OAAO,sBAAsB,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,sBAAsB,CAAC;QACnF,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;QACjD,CAAC,CAAC,GAAG,CAAC;IACV,MAAM,QAAQ,GAAG,uBAAuB,EAAE,CAAC;IAC3C,yEAAyE;IACzE,4EAA4E;IAC5E,2EAA2E;IAC3E,4EAA4E;IAC5E,MAAM,wBAAwB,GAAG,CAAC,CAAC;IACnC,IAAI,mBAAmB,GAAG,CAAC,CAAC;IAC5B,4EAA4E;IAC5E,wEAAwE;IACxE,IAAI,kBAAkB,GAAG,CAAC,CAAC;IAE3B,KAAK,IAAI,SAAS,GAAG,CAAC,EAAE,SAAS,IAAI,aAAa,EAAE,SAAS,EAAE,EAAE,CAAC;QAChE,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;gBACnB,IAAI,EAAE,OAAO;gBACb,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,MAAM,EAAE,QAAQ;gBAChB,MAAM,EAAE,gBAAgB;aACzB,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;YACnB,IAAI,EAAE,gBAAgB;YACtB,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,MAAM,EAAE,QAAQ;YAChB,QAAQ,EAAE,iBAAiB;YAC3B,SAAS;SACV,CAAC,CAAC;QAEH,kEAAkE;QAClE,kEAAkE;QAClE,iEAAiE;QACjE,8DAA8D;QAC9D,qDAAqD;QACrD,MAAM,qBAAqB,CAAC,GAAG,CAAC,CAAC;QACjC,4EAA4E;QAC5E,sEAAsE;QACtE,uCAAuC;QACvC,MAAM,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAE9B,MAAM,EAAE,QAAQ,EAAE,iBAAiB,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;QAC3D,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;YACnB,IAAI,EAAE,kBAAkB;YACxB,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,MAAM,EAAE,QAAQ;YAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI;YAC3B,KAAK,EAAE,GAAG,CAAC,KAAK;SACjB,CAAC,CAAC;QAEH,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,qBAAqB,CACzF,GAAG,EACH,QAAQ,EACR;YACE,SAAS;YACT,iBAAiB;SAClB,CACF,CAAC;QAEF,4EAA4E;QAC5E,2EAA2E;QAC3E,0EAA0E;QAC1E,oEAAoE;QACpE,wEAAwE;QACxE,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;gBACnB,IAAI,EAAE,OAAO;gBACb,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,MAAM,EAAE,QAAQ;gBAChB,MAAM,EAAE,uCAAuC;aAChD,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;YACnB,IAAI,EAAE,mBAAmB;YACzB,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,MAAM,EAAE,QAAQ;YAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI;YAC3B,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,GAAG,gBAAgB,CAAC,KAAK,CAAC;SAC3B,CAAC,CAAC;QAEH,IAAI,KAAK,EAAE,CAAC;YACV,qEAAqE;YACrE,mEAAmE;YACnE,wEAAwE;YACxE,qCAAqC;YACrC,IACE,sBAAsB,CAAC,KAAK,CAAC,OAAO,CAAC;gBACrC,mBAAmB,GAAG,wBAAwB,EAC9C,CAAC;gBACD,MAAM,SAAS,GAAG,MAAM,qBAAqB,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;gBACpE,IAAI,SAAS,EAAE,CAAC;oBACd,qEAAqE;oBACrE,sEAAsE;oBACtE,0DAA0D;oBAC1D,mBAAmB,IAAI,CAAC,CAAC;oBACzB,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;wBACnB,IAAI,EAAE,OAAO;wBACb,SAAS,EAAE,GAAG,CAAC,SAAS;wBACxB,MAAM,EAAE,GAAG,CAAC,MAAM;wBAClB,MAAM,EAAE,QAAQ;wBAChB,IAAI,EAAE,WAAW;wBACjB,OAAO,EAAE,2DAA2D;qBACrE,CAAC,CAAC;oBACH,SAAS;gBACX,CAAC;YACH,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;gBACrB,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;oBACnB,IAAI,EAAE,OAAO;oBACb,SAAS,EAAE,GAAG,CAAC,SAAS;oBACxB,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,MAAM,EAAE,QAAQ;oBAChB,IAAI,EAAE,OAAO;oBACb,OAAO,EAAE,KAAK,CAAC,OAAO;iBACvB,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YACD,qEAAqE;YACrE,sEAAsE;YACtE,uEAAuE;YACvE,kBAAkB,IAAI,CAAC,CAAC;YACxB,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;gBACnB,IAAI,EAAE,OAAO;gBACb,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,MAAM,EAAE,QAAQ;gBAChB,IAAI,EAAE,WAAW;gBACjB,OAAO,EAAE,KAAK,CAAC,OAAO;aACvB,CAAC,CAAC;YACH,IAAI,kBAAkB,IAAI,uBAAuB,EAAE,CAAC;gBAClD,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;oBACnB,IAAI,EAAE,OAAO;oBACb,SAAS,EAAE,GAAG,CAAC,SAAS;oBACxB,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,MAAM,EAAE,QAAQ;oBAChB,IAAI,EAAE,OAAO;oBACb,OAAO,EACL,6CAA6C,kBAAkB,kBAAkB;wBACjF,UAAU,KAAK,CAAC,OAAO,kDAAkD;iBAC5E,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YACD,MAAM,SAAS,CAAC,cAAc,CAAC,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;YAChE,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACvB,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;oBACnB,IAAI,EAAE,OAAO;oBACb,SAAS,EAAE,GAAG,CAAC,SAAS;oBACxB,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,MAAM,EAAE,QAAQ;oBAChB,MAAM,EAAE,sCAAsC;iBAC/C,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YACD,SAAS;QACX,CAAC;QACD,qEAAqE;QACrE,mBAAmB,GAAG,CAAC,CAAC;QACxB,kBAAkB,GAAG,CAAC,CAAC;QAEvB,yEAAyE;QACzE,qEAAqE;QACrE,yEAAyE;QACzE,6EAA6E;QAC7E,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;gBACnB,IAAI,EAAE,mBAAmB;gBACzB,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,MAAM,EAAE,OAAO;gBACf,OAAO,EAAE,SAAS,CAAC,IAAI;gBACvB,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClE,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjD,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACnE,CAAC,CAAC;QACL,CAAC;QAED,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,0BAA0B,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE;YACvE,oBAAoB,EAAE,gEAAgE;YACtF,QAAQ,EAAE,mEAAmE;YAC7E,YAAY,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE,CACzC,6DAA6D,QAAQ,WAAW;gBAChF,GAAG,KAAK,UAAU,GAAG,kDAAkD;gBACvE,oBAAoB;SACvB,CAAC,CAAC;QACH,IAAI,KAAK;YAAE,OAAO;QAElB,IAAI,IAAI,IAAI,UAAU,KAAK,UAAU,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/D,wEAAwE;YACxE,0EAA0E;YAC1E,2EAA2E;YAC3E,qEAAqE;YACrE,2BAA2B;YAC3B,IAAI,CAAC,IAAI,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,UAAU,KAAK,UAAU,EAAE,CAAC;gBAChE,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;oBACnB,IAAI,EAAE,OAAO;oBACb,SAAS,EAAE,GAAG,CAAC,SAAS;oBACxB,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,MAAM,EAAE,QAAQ;oBAChB,IAAI,EAAE,WAAW;oBACjB,OAAO,EAAE,sDAAsD,UAAU,IAAI,SAAS,GAAG;iBAC1F,CAAC,CAAC;YACL,CAAC;YACD,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;gBACnB,IAAI,EAAE,mBAAmB;gBACzB,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,MAAM,EAAE,OAAO;gBACf,OAAO,EAAE,IAAI;gBACb,UAAU;aACX,CAAC,CAAC;QACL,CAAC;QAED,wEAAwE;QACxE,sEAAsE;QACtE,uEAAuE;QACvE,mEAAmE;QACnE,uEAAuE;QACvE,uEAAuE;QACvE,uEAAuE;QACvE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAElC,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,eAAe,CAAC,GAAG,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;QAChE,IAAI,MAAM;YAAE,OAAO;IACrB,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC;QACnB,IAAI,EAAE,OAAO;QACb,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,MAAM,EAAE,QAAQ;QAChB,IAAI,EAAE,OAAO;QACb,OAAO,EAAE,6CAA6C,aAAa,GAAG;KACvE,CAAC,CAAC;AACL,CAAC;AAED,SAAS,aAAa,CAAC,GAAgB;IACrC,sEAAsE;IACtE,kEAAkE;IAClE,4DAA4D;IAC5D,sDAAsD;IACtD,MAAM,YAAY,GAAG,2BAA2B,CAAC,GAAG,CAAC,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IACtF,OAAO,eAAe,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;AAC7E,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moxxy/mode-default",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Default loop strategy (mode) for moxxy — a Claude Code-style ReAct tool-use loop. Register it into an @moxxy/core Session to give your agent its turn-by-turn reasoning + tool-calling behaviour.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"moxxy",
|
|
@@ -44,17 +44,17 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@moxxy/sdk": "0.15.
|
|
47
|
+
"@moxxy/sdk": "0.15.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^22.10.0",
|
|
51
51
|
"typescript": "^5.7.3",
|
|
52
52
|
"vitest": "^2.1.8",
|
|
53
53
|
"zod": "^3.24.0",
|
|
54
|
-
"@moxxy/core": "0.
|
|
55
|
-
"@moxxy/
|
|
54
|
+
"@moxxy/core": "0.5.1",
|
|
55
|
+
"@moxxy/vitest-preset": "0.0.0",
|
|
56
56
|
"@moxxy/tsconfig": "0.0.0",
|
|
57
|
-
"@moxxy/
|
|
57
|
+
"@moxxy/testing": "0.0.28"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build": "tsc -p tsconfig.json",
|
package/src/index.test.ts
CHANGED
|
@@ -13,6 +13,23 @@ const retryableErrorReply = (message = 'rate limited (429)'): ReadonlyArray<Prov
|
|
|
13
13
|
{ type: 'message_end', stopReason: 'end_turn' },
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A scripted reply mimicking the real provider stack when the user aborts WHILE
|
|
18
|
+
* the stream is being consumed: the fetch AbortError is caught and classified
|
|
19
|
+
* non-retryable with the canonical "operation was aborted" message.
|
|
20
|
+
*/
|
|
21
|
+
const abortDuringStreamReply = (): ReadonlyArray<ProviderEvent> => [
|
|
22
|
+
{ type: 'message_start', model: 'fake' },
|
|
23
|
+
{ type: 'error', message: 'The operation was aborted', retryable: false },
|
|
24
|
+
{ type: 'message_end', stopReason: 'end_turn' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** A reply with no text, no tools, and a non-natural stop (truncated to empty). */
|
|
28
|
+
const emptyMaxTokensReply = (): ReadonlyArray<ProviderEvent> => [
|
|
29
|
+
{ type: 'message_start', model: 'fake' },
|
|
30
|
+
{ type: 'message_end', stopReason: 'max_tokens' },
|
|
31
|
+
];
|
|
32
|
+
|
|
16
33
|
const sessionWith = (provider: FakeProvider): Session => {
|
|
17
34
|
const session = createFakeSession({ provider });
|
|
18
35
|
session.pluginHost.registerStatic(defaultModePlugin);
|
|
@@ -357,6 +374,72 @@ describe('defaultMode end-to-end', () => {
|
|
|
357
374
|
}
|
|
358
375
|
});
|
|
359
376
|
|
|
377
|
+
it('emits a clean abort (not a fatal error) when aborted during the provider stream', async () => {
|
|
378
|
+
// Regression: a cancellation WHILE collectProviderStream is consuming the
|
|
379
|
+
// stream surfaces as a non-retryable provider error ("operation was
|
|
380
|
+
// aborted"). The loop must recognize the set abort signal and emit a clean
|
|
381
|
+
// `abort` event, NOT a `kind: 'fatal'` error that renders as a failed turn.
|
|
382
|
+
// Abort synchronously at the start of the provider call so the signal is set
|
|
383
|
+
// by the time collectProviderStream returns the abort-classified error.
|
|
384
|
+
let abort: () => void = () => {};
|
|
385
|
+
const provider = new FakeProvider({
|
|
386
|
+
script: [abortDuringStreamReply()],
|
|
387
|
+
onRequest: () => abort(),
|
|
388
|
+
});
|
|
389
|
+
const session = sessionWith(provider);
|
|
390
|
+
abort = () => session.abort('test abort mid-stream');
|
|
391
|
+
|
|
392
|
+
const events = await collectTurn(session, 'hi');
|
|
393
|
+
const aborted = events.find((e) => e.type === 'abort');
|
|
394
|
+
expect(aborted).toBeDefined();
|
|
395
|
+
if (aborted?.type !== 'abort') throw new Error('expected abort');
|
|
396
|
+
expect(aborted.reason).toMatch(/provider stream/);
|
|
397
|
+
// No fatal error event leaked the raw abort message.
|
|
398
|
+
const fatal = events.filter((e) => e.type === 'error' && e.kind === 'fatal');
|
|
399
|
+
expect(fatal).toHaveLength(0);
|
|
400
|
+
// And the raw "operation was aborted" message was never surfaced as an error.
|
|
401
|
+
const rawAbortError = events.filter(
|
|
402
|
+
(e) => e.type === 'error' && /operation was aborted/i.test(e.message),
|
|
403
|
+
);
|
|
404
|
+
expect(rawAbortError).toHaveLength(0);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('coerces a degenerate maxIterations (0) to at least one iteration instead of an instant fatal', async () => {
|
|
408
|
+
// Regression: an unvalidated maxIterations of 0 made the loop body never run
|
|
409
|
+
// and emit a misleading "exceeded maxIterations" fatal. A coerced bound runs
|
|
410
|
+
// the turn normally.
|
|
411
|
+
const provider = new FakeProvider({ script: [textReply('hello')] });
|
|
412
|
+
const session = sessionWith(provider);
|
|
413
|
+
|
|
414
|
+
const events = await collectTurn(session, 'hi', { maxIterations: 0 });
|
|
415
|
+
// The single iteration ran and produced a normal assistant message.
|
|
416
|
+
const last = events[events.length - 1];
|
|
417
|
+
if (last.type !== 'assistant_message') throw new Error('expected assistant_message last');
|
|
418
|
+
expect(last.content).toBe('hello');
|
|
419
|
+
// No fatal "exceeded maxIterations" fired on the very first turn.
|
|
420
|
+
const fatal = events.filter((e) => e.type === 'error' && e.kind === 'fatal');
|
|
421
|
+
expect(fatal).toHaveLength(0);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('surfaces an empty-completion note when a turn truncates to no text and no tools', async () => {
|
|
425
|
+
// Regression: a max_tokens completion truncated to nothing emitted only a
|
|
426
|
+
// blank assistant bubble. Now a retryable note explains why the turn was
|
|
427
|
+
// empty, alongside the (preserved) empty assistant_message.
|
|
428
|
+
const provider = new FakeProvider({ script: [emptyMaxTokensReply()] });
|
|
429
|
+
const session = sessionWith(provider);
|
|
430
|
+
|
|
431
|
+
const events = await collectTurn(session, 'hi');
|
|
432
|
+
const note = events.filter(
|
|
433
|
+
(e) => e.type === 'error' && e.kind === 'retryable' && /empty completion/i.test(e.message),
|
|
434
|
+
);
|
|
435
|
+
expect(note).toHaveLength(1);
|
|
436
|
+
// The empty assistant_message is still emitted (existing behavior preserved).
|
|
437
|
+
const assistant = events.filter((e) => e.type === 'assistant_message');
|
|
438
|
+
expect(assistant).toHaveLength(1);
|
|
439
|
+
if (assistant[0]?.type !== 'assistant_message') throw new Error('expected assistant_message');
|
|
440
|
+
expect(assistant[0].content).toBe('');
|
|
441
|
+
});
|
|
442
|
+
|
|
360
443
|
it('gives up with a fatal error after the bounded retry count, not maxIterations', async () => {
|
|
361
444
|
const delays: number[] = [];
|
|
362
445
|
const restore = __setRetrySleepForTests(async (ms) => {
|
package/src/turn-iterator.ts
CHANGED
|
@@ -54,7 +54,13 @@ let sleepImpl = (ms: number, signal: AbortSignal): Promise<void> =>
|
|
|
54
54
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Override the retry back-off sleep (test seam). Returns a restore fn that
|
|
59
|
+
* callers MUST invoke (in a `finally`) — `sleepImpl` is a module-scoped
|
|
60
|
+
* singleton shared process-wide, so a leaked override bleeds the fake sleep
|
|
61
|
+
* into every other turn/test running in the same worker (parallel subagent
|
|
62
|
+
* fan-out, multiple Sessions in one host). Test-only; never call from prod.
|
|
63
|
+
*/
|
|
58
64
|
export function __setRetrySleepForTests(
|
|
59
65
|
fn: (ms: number, signal: AbortSignal) => Promise<void>,
|
|
60
66
|
): () => void {
|
|
@@ -70,7 +76,15 @@ export async function* runDefaultMode(ctx: ModeContext): AsyncIterable<MoxxyEven
|
|
|
70
76
|
// glitch causing an infinite retry, bad prompt, etc.) — primary
|
|
71
77
|
// termination signal is the stuck-loop detector, which catches the
|
|
72
78
|
// common "model keeps calling the same tool" case ~10 iterations in.
|
|
73
|
-
|
|
79
|
+
// Coerce a caller/config-supplied bound to a positive integer; a degenerate
|
|
80
|
+
// value (0, negative, NaN, fractional) would otherwise make the loop never
|
|
81
|
+
// run and emit a misleading "exceeded maxIterations" fatal. Treat anything
|
|
82
|
+
// un-coercible (NaN) as the default rather than failing the turn.
|
|
83
|
+
const requestedMaxIterations = ctx.maxIterations;
|
|
84
|
+
const maxIterations =
|
|
85
|
+
typeof requestedMaxIterations === 'number' && Number.isFinite(requestedMaxIterations)
|
|
86
|
+
? Math.max(1, Math.floor(requestedMaxIterations))
|
|
87
|
+
: 500;
|
|
74
88
|
const detector = createStuckLoopDetector();
|
|
75
89
|
// Reactive-compaction budget per overflow episode. If the provider keeps
|
|
76
90
|
// rejecting for context size even after compacting this many times, give up
|
|
@@ -133,6 +147,22 @@ export async function* runDefaultMode(ctx: ModeContext): AsyncIterable<MoxxyEven
|
|
|
133
147
|
},
|
|
134
148
|
);
|
|
135
149
|
|
|
150
|
+
// A user cancellation WHILE the provider stream was being consumed surfaces
|
|
151
|
+
// as a non-retryable provider `error` ("The operation was aborted") rather
|
|
152
|
+
// than a clean abort — collectProviderStream catches the fetch AbortError
|
|
153
|
+
// and classifies it as fatal. Treat it as the cancellation it is so
|
|
154
|
+
// downstream channels render a 'stopped' turn, not a failed/error turn.
|
|
155
|
+
if (ctx.signal.aborted) {
|
|
156
|
+
yield await ctx.emit({
|
|
157
|
+
type: 'abort',
|
|
158
|
+
sessionId: ctx.sessionId,
|
|
159
|
+
turnId: ctx.turnId,
|
|
160
|
+
source: 'system',
|
|
161
|
+
reason: 'signal aborted during provider stream',
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
136
166
|
yield await ctx.emit({
|
|
137
167
|
type: 'provider_response',
|
|
138
168
|
sessionId: ctx.sessionId,
|
|
@@ -152,9 +182,12 @@ export async function* runDefaultMode(ctx: ModeContext): AsyncIterable<MoxxyEven
|
|
|
152
182
|
isContextOverflowError(error.message) &&
|
|
153
183
|
reactiveCompactions < MAX_REACTIVE_COMPACTIONS
|
|
154
184
|
) {
|
|
155
|
-
reactiveCompactions += 1;
|
|
156
185
|
const compacted = await runCompactionIfNeeded(ctx, { force: true });
|
|
157
186
|
if (compacted) {
|
|
187
|
+
// Only count an attempt that actually compacted against the budget —
|
|
188
|
+
// a no-op (overflow lives in the un-compactable recent tail) must not
|
|
189
|
+
// deny a later, genuinely compactable overflow its retry.
|
|
190
|
+
reactiveCompactions += 1;
|
|
158
191
|
yield await ctx.emit({
|
|
159
192
|
type: 'error',
|
|
160
193
|
sessionId: ctx.sessionId,
|
|
@@ -247,6 +280,21 @@ export async function* runDefaultMode(ctx: ModeContext): AsyncIterable<MoxxyEven
|
|
|
247
280
|
if (stuck) return;
|
|
248
281
|
|
|
249
282
|
if (text || stopReason === 'end_turn' || toolUses.length === 0) {
|
|
283
|
+
// A completion with no text, no tool uses, and a non-natural stop (e.g.
|
|
284
|
+
// 'max_tokens' truncated to nothing) yields a blank assistant bubble that
|
|
285
|
+
// silently swallows the truncation signal. Surface a retryable note so the
|
|
286
|
+
// user sees why the turn produced nothing, alongside the (preserved)
|
|
287
|
+
// empty assistant_message.
|
|
288
|
+
if (!text && toolUses.length === 0 && stopReason !== 'end_turn') {
|
|
289
|
+
yield await ctx.emit({
|
|
290
|
+
type: 'error',
|
|
291
|
+
sessionId: ctx.sessionId,
|
|
292
|
+
turnId: ctx.turnId,
|
|
293
|
+
source: 'system',
|
|
294
|
+
kind: 'retryable',
|
|
295
|
+
message: `provider returned an empty completion (stopReason: ${stopReason ?? 'unknown'})`,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
250
298
|
yield await ctx.emit({
|
|
251
299
|
type: 'assistant_message',
|
|
252
300
|
sessionId: ctx.sessionId,
|