@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.
@@ -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
- /** Override the retry back-off sleep (test seam). Returns a restore fn. */
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,2EAA2E;AAC3E,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,CAqNjF"}
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"}
@@ -35,7 +35,13 @@ let sleepImpl = (ms, signal) => new Promise((resolve) => {
35
35
  };
36
36
  signal.addEventListener('abort', onAbort, { once: true });
37
37
  });
38
- /** Override the retry back-off sleep (test seam). Returns a restore fn. */
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
- const maxIterations = ctx.maxIterations ?? 500;
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":"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"}
1
+ {"version":3,"file":"turn-iterator.js","sourceRoot":"","sources":["../src/turn-iterator.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxxy/mode-default",
3
- "version": "0.1.0",
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.0"
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.3.0",
55
- "@moxxy/testing": "0.0.25",
54
+ "@moxxy/core": "0.5.1",
55
+ "@moxxy/vitest-preset": "0.0.0",
56
56
  "@moxxy/tsconfig": "0.0.0",
57
- "@moxxy/vitest-preset": "0.0.0"
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) => {
@@ -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
- /** Override the retry back-off sleep (test seam). Returns a restore fn. */
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
- const maxIterations = ctx.maxIterations ?? 500;
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,