@moxxy/sdk 0.14.1 → 0.14.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.
Files changed (39) hide show
  1. package/dist/mode/collect-stream.d.ts +68 -0
  2. package/dist/mode/collect-stream.d.ts.map +1 -0
  3. package/dist/mode/collect-stream.js +181 -0
  4. package/dist/mode/collect-stream.js.map +1 -0
  5. package/dist/mode/project-messages.d.ts +84 -0
  6. package/dist/mode/project-messages.d.ts.map +1 -0
  7. package/dist/mode/project-messages.js +392 -0
  8. package/dist/mode/project-messages.js.map +1 -0
  9. package/dist/mode/single-shot.d.ts +17 -0
  10. package/dist/mode/single-shot.d.ts.map +1 -0
  11. package/dist/mode/single-shot.js +53 -0
  12. package/dist/mode/single-shot.js.map +1 -0
  13. package/dist/mode/stable-hash.d.ts +7 -0
  14. package/dist/mode/stable-hash.d.ts.map +1 -0
  15. package/dist/mode/stable-hash.js +20 -0
  16. package/dist/mode/stable-hash.js.map +1 -0
  17. package/dist/mode/stuck-loop.d.ts +39 -0
  18. package/dist/mode/stuck-loop.d.ts.map +1 -0
  19. package/dist/mode/stuck-loop.js +51 -0
  20. package/dist/mode/stuck-loop.js.map +1 -0
  21. package/dist/mode-helpers.d.ts +15 -175
  22. package/dist/mode-helpers.d.ts.map +1 -1
  23. package/dist/mode-helpers.js +14 -666
  24. package/dist/mode-helpers.js.map +1 -1
  25. package/dist/provider-utils.d.ts.map +1 -1
  26. package/dist/provider-utils.js +24 -0
  27. package/dist/provider-utils.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/elision-state.test.ts +46 -0
  30. package/src/mode/collect-stream.ts +247 -0
  31. package/src/mode/project-messages.test.ts +121 -0
  32. package/src/mode/project-messages.ts +461 -0
  33. package/src/mode/single-shot.ts +63 -0
  34. package/src/mode/stable-hash.ts +20 -0
  35. package/src/mode/stuck-loop.ts +89 -0
  36. package/src/mode-helpers.ts +32 -850
  37. package/src/mode.test.ts +20 -0
  38. package/src/provider-utils.ts +24 -0
  39. package/src/token-accounting.test.ts +90 -0
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { migrateModeName } from './mode.js';
3
+
4
+ describe('migrateModeName', () => {
5
+ it('maps every legacy mode name to its current target', () => {
6
+ expect(migrateModeName('tool-use')).toBe('default');
7
+ expect(migrateModeName('deep-research')).toBe('research');
8
+ expect(migrateModeName('plan-execute')).toBe('default');
9
+ expect(migrateModeName('bmad')).toBe('default');
10
+ expect(migrateModeName('developer')).toBe('default');
11
+ });
12
+
13
+ it('passes a current/unknown name through unchanged (identity)', () => {
14
+ expect(migrateModeName('default')).toBe('default');
15
+ expect(migrateModeName('goal')).toBe('goal');
16
+ expect(migrateModeName('research')).toBe('research');
17
+ expect(migrateModeName('some-future-mode')).toBe('some-future-mode');
18
+ expect(migrateModeName('')).toBe('');
19
+ });
20
+ });
@@ -173,6 +173,30 @@ export function zodToJsonSchema(schema: unknown): unknown {
173
173
  const items = zodToJsonSchema((def as unknown as { type: unknown }).type);
174
174
  return { type: 'array', items };
175
175
  }
176
+ if (typeName === 'ZodRecord') {
177
+ // `z.record(keyType, valueType)` — an open-ended object whose values share
178
+ // one schema. Carry the value schema through `additionalProperties` rather
179
+ // than discarding it to the permissive fallback below.
180
+ const valueType = (def as unknown as { valueType: unknown }).valueType;
181
+ return { type: 'object', additionalProperties: zodToJsonSchema(valueType) };
182
+ }
183
+ if (typeName === 'ZodTuple') {
184
+ // ZodTuple._def.items is the ordered element schemas; `rest` (if set) is the
185
+ // variadic tail. Emit a fixed-position `items` array + an `additionalItems`
186
+ // schema for the rest, mirroring JSON-schema's positional-tuple form.
187
+ const tupleDef = def as unknown as { items: ReadonlyArray<unknown>; rest?: unknown };
188
+ const items = tupleDef.items.map((i) => zodToJsonSchema(i));
189
+ return {
190
+ type: 'array',
191
+ items,
192
+ ...(tupleDef.rest != null ? { additionalItems: zodToJsonSchema(tupleDef.rest) } : {}),
193
+ };
194
+ }
195
+ if (typeName === 'ZodAny' || typeName === 'ZodUnknown') {
196
+ // Genuinely unconstrained — advertise an empty (permissive) schema instead
197
+ // of a false `object` shape that would reject non-object values.
198
+ return {};
199
+ }
176
200
  // Truly unknown — fall back to a permissive "any object" but spell out
177
201
  // `properties: {}` and `additionalProperties: true` so strict validators
178
202
  // accept the schema instead of bailing on a missing `properties` field.
@@ -4,6 +4,7 @@ import {
4
4
  asEventId,
5
5
  asSessionId,
6
6
  asTurnId,
7
+ summarizeSessionTokensFromEvents,
7
8
  summarizeTokensByModel,
8
9
  type ModelUsageTotals,
9
10
  type MoxxyEvent,
@@ -78,6 +79,95 @@ describe('summarizeTokensByModel', () => {
78
79
  });
79
80
  });
80
81
 
82
+ describe('summarizeSessionTokensFromEvents — cost fold', () => {
83
+ it('counts only responses that reported usage', () => {
84
+ const s = summarizeSessionTokensFromEvents([
85
+ resp(0, {}), // no token fields → not counted
86
+ resp(1, { inputTokens: 100, outputTokens: 10 }),
87
+ resp(2, { cacheReadTokens: 50 }),
88
+ ]);
89
+ expect(s.calls).toBe(2);
90
+ expect(s.totalInput).toBe(100);
91
+ expect(s.totalCacheRead).toBe(50);
92
+ expect(s.totalOutput).toBe(10);
93
+ expect(s.totalPrompt).toBe(150);
94
+ });
95
+
96
+ it('applies the read 0.1x / write 1.25x billing multipliers', () => {
97
+ const s = summarizeSessionTokensFromEvents([
98
+ resp(0, { inputTokens: 100, cacheReadTokens: 1000, cacheCreationTokens: 200 }),
99
+ ]);
100
+ // totalPrompt = 100 + 1000 + 200 = 1300 (== uncachedInputEq)
101
+ expect(s.totalPrompt).toBe(1300);
102
+ expect(s.uncachedInputEq).toBe(1300);
103
+ // billedInputEq = 100 + 1000*0.1 + 200*1.25 = 100 + 100 + 250 = 450
104
+ expect(s.billedInputEq).toBeCloseTo(450, 6);
105
+ expect(s.cacheHitRate).toBeCloseTo(1000 / 1300, 6);
106
+ expect(s.savedRatio).toBeCloseTo(1 - 450 / 1300, 6);
107
+ });
108
+
109
+ it('guards divide-by-zero when no prompt tokens were reported', () => {
110
+ const s = summarizeSessionTokensFromEvents([resp(0, { outputTokens: 5 })]);
111
+ expect(s.totalPrompt).toBe(0);
112
+ expect(s.cacheHitRate).toBe(0);
113
+ expect(s.savedRatio).toBe(0);
114
+ // No writes → cache considered effective (deliberately-off cache never alarms).
115
+ expect(s.cacheEffective).toBe(true);
116
+ });
117
+
118
+ it('reports cacheEffective=true for a healthy cache (high read ratio)', () => {
119
+ const events = Array.from({ length: 6 }, (_, i) =>
120
+ resp(i, { inputTokens: 10, cacheReadTokens: 1000, cacheCreationTokens: 100 }),
121
+ );
122
+ const s = summarizeSessionTokensFromEvents(events);
123
+ expect(s.calls).toBe(6);
124
+ expect(s.cacheHitRate).toBeGreaterThan(0.05);
125
+ expect(s.savedRatio).toBeGreaterThan(0);
126
+ expect(s.cacheEffective).toBe(true);
127
+ });
128
+
129
+ it('trips cacheEffective=false on broken cache: >=5 calls, writes>0, near-zero reads', () => {
130
+ // 5 calls each writing cache but never reading it → cacheHitRate 0.
131
+ const events = Array.from({ length: 5 }, (_, i) =>
132
+ resp(i, { inputTokens: 100, cacheCreationTokens: 500, cacheReadTokens: 0 }),
133
+ );
134
+ const s = summarizeSessionTokensFromEvents(events);
135
+ expect(s.calls).toBe(5);
136
+ expect(s.totalCacheCreation).toBeGreaterThan(0);
137
+ expect(s.cacheHitRate).toBe(0);
138
+ expect(s.cacheEffective).toBe(false);
139
+ });
140
+
141
+ it('exempts the broken-cache trip below 5 calls (boundary at calls===4 vs 5)', () => {
142
+ const make = (n: number) =>
143
+ summarizeSessionTokensFromEvents(
144
+ Array.from({ length: n }, (_, i) =>
145
+ resp(i, { inputTokens: 100, cacheCreationTokens: 500, cacheReadTokens: 0 }),
146
+ ),
147
+ );
148
+ expect(make(4).cacheEffective).toBe(true); // < 5 → exempt
149
+ expect(make(5).cacheEffective).toBe(false); // >= 5 → trips
150
+ });
151
+
152
+ it('exempts when there are no cache writes even with zero reads over many calls', () => {
153
+ const events = Array.from({ length: 8 }, (_, i) =>
154
+ resp(i, { inputTokens: 100, cacheCreationTokens: 0, cacheReadTokens: 0 }),
155
+ );
156
+ expect(summarizeSessionTokensFromEvents(events).cacheEffective).toBe(true);
157
+ });
158
+
159
+ it('does not trip when cacheHitRate sits exactly at the 0.05 threshold', () => {
160
+ // hitRate === 0.05 is NOT < 0.05, so the cache stays effective.
161
+ // read/(input+read+write) = 125 / 2500 = 0.05.
162
+ const events = Array.from({ length: 5 }, (_, i) =>
163
+ resp(i, { inputTokens: 1875, cacheReadTokens: 125, cacheCreationTokens: 500 }),
164
+ );
165
+ const s = summarizeSessionTokensFromEvents(events);
166
+ expect(s.cacheHitRate).toBeCloseTo(0.05, 6);
167
+ expect(s.cacheEffective).toBe(true);
168
+ });
169
+ });
170
+
81
171
  describe('addModelTotals', () => {
82
172
  it('adds field-wise', () => {
83
173
  const a: ModelUsageTotals = { calls: 1, inputTokens: 10, outputTokens: 2, cacheReadTokens: 3, cacheCreationTokens: 4 };