textopt 0.0.0 → 0.2.0
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 +65 -25
- package/dist/bootstrap-search/index.cjs +159 -73
- package/dist/bootstrap-search/index.d.cts +32 -10
- package/dist/bootstrap-search/index.d.mts +32 -10
- package/dist/bootstrap-search/index.mjs +150 -66
- package/dist/demos-9v5ts7F3.cjs +244 -0
- package/dist/{demos-B0pVQjYC.d.mts → demos-ASsSXYXA.d.mts} +10 -3
- package/dist/demos-Brobjfuc.mjs +215 -0
- package/dist/{demos-BTuzFNsp.d.cts → demos-ByaLZy-Z.d.cts} +10 -3
- package/dist/file-cache.cjs +27 -8
- package/dist/file-cache.d.cts +13 -0
- package/dist/file-cache.d.mts +13 -0
- package/dist/file-cache.mjs +27 -8
- package/dist/gepa/index.cjs +128 -80
- package/dist/gepa/index.d.cts +15 -7
- package/dist/gepa/index.d.mts +15 -7
- package/dist/gepa/index.mjs +101 -55
- package/dist/index.cjs +157 -30
- package/dist/index.d.cts +177 -7
- package/dist/index.d.mts +177 -7
- package/dist/index.mjs +139 -18
- package/dist/{math-COOofUyv.cjs → math-BhlziRPc.cjs} +60 -9
- package/dist/math-Dqme4rYz.mjs +123 -0
- package/dist/mipro/index.cjs +104 -70
- package/dist/mipro/index.d.cts +17 -14
- package/dist/mipro/index.d.mts +17 -14
- package/dist/mipro/index.mjs +90 -58
- package/dist/opro/index.cjs +136 -51
- package/dist/opro/index.d.cts +17 -9
- package/dist/opro/index.d.mts +17 -9
- package/dist/opro/index.mjs +121 -38
- package/dist/{optimizer-B7SpRwl7.d.cts → optimizer-4Zv-Zt2t.d.cts} +90 -5
- package/dist/{optimizer-DqCoth_w.d.mts → optimizer-Ds5mzYjz.d.mts} +90 -5
- package/dist/random-search/index.cjs +99 -49
- package/dist/random-search/index.d.cts +15 -13
- package/dist/random-search/index.d.mts +15 -13
- package/dist/random-search/index.mjs +89 -41
- package/dist/{reflection-Cr_upzU0.d.mts → reflection-CMezGu6u.d.mts} +38 -14
- package/dist/{reflection-CQToe-5B.d.cts → reflection-D0A7eahD.d.cts} +38 -14
- package/dist/reporting-bq007_2z.d.cts +294 -0
- package/dist/reporting-bq007_2z.d.mts +294 -0
- package/dist/simba/index.cjs +216 -83
- package/dist/simba/index.d.cts +53 -13
- package/dist/simba/index.d.mts +53 -13
- package/dist/simba/index.mjs +206 -75
- package/dist/testing.cjs +1 -0
- package/dist/testing.d.cts +5 -3
- package/dist/testing.d.mts +5 -3
- package/dist/testing.mjs +1 -1
- package/dist/{evaluation-OZOp6TB7.cjs → warnings-CWRJF-jA.cjs} +228 -5
- package/dist/{evaluation-BV0nSZVx.mjs → warnings-OxvDi9kN.mjs} +175 -6
- package/docs/adapters.md +169 -0
- package/docs/benchmark.md +90 -0
- package/docs/data-prep.md +113 -0
- package/docs/distillation.md +128 -0
- package/docs/evaluation.md +87 -0
- package/docs/metric-preflight.md +132 -0
- package/docs/optimizers.md +293 -0
- package/docs/tuning.md +130 -0
- package/package.json +6 -4
- package/dist/demos-B9BJiNKz.cjs +0 -143
- package/dist/demos-Degx6UmP.mjs +0 -126
- package/dist/math-DhrDmpFS.mjs +0 -78
- package/dist/types-CWv4IQFF.d.cts +0 -129
- package/dist/types-CWv4IQFF.d.mts +0 -129
|
@@ -30,6 +30,8 @@ function createBudget(args) {
|
|
|
30
30
|
}
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/cache.ts
|
|
33
|
+
/** What `stableHash` returns for any value that serializes to `{}`. */
|
|
34
|
+
const EMPTY_OBJECT_HASH = stableHash({});
|
|
33
35
|
/**
|
|
34
36
|
* The loop re-evaluates unchanged candidates against the same validation
|
|
35
37
|
* instances constantly (every accepted child inherits most of its parent's
|
|
@@ -65,6 +67,20 @@ function stableHash(value) {
|
|
|
65
67
|
}
|
|
66
68
|
return `${hash32(serialized, 2166136261)}${hash32(serialized, 16777619)}`;
|
|
67
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Names one data instance for the evaluation cache: a content hash, so the same
|
|
72
|
+
* row is the same instance wherever it appears in a run.
|
|
73
|
+
*
|
|
74
|
+
* Falls back to the row's position when the datum carries nothing the hash can
|
|
75
|
+
* read. A Map, a Set and a class instance holding its state privately all
|
|
76
|
+
* serialize to `{}`, and an id two rows share serves each of them the score the
|
|
77
|
+
* other measured. Position is a weaker id — it is only stable while the data is
|
|
78
|
+
* — but it is one instance per row, which is what the cache needs to be sound.
|
|
79
|
+
*/
|
|
80
|
+
function defaultInstanceId(args) {
|
|
81
|
+
const hash = stableHash(args.datum);
|
|
82
|
+
return hash === "" || hash === EMPTY_OBJECT_HASH ? String(args.index) : hash;
|
|
83
|
+
}
|
|
68
84
|
function createMemoryCache(args = {}) {
|
|
69
85
|
const { maxEntries = 1e5, entries: initial = [] } = args;
|
|
70
86
|
const entries = new Map(initial);
|
|
@@ -172,25 +188,39 @@ const DEFAULT_RETRY = {
|
|
|
172
188
|
*/
|
|
173
189
|
var BudgetExhausted = class extends Error {};
|
|
174
190
|
function createEvaluator(args) {
|
|
175
|
-
const { adapter, budget, cache, trackOutputs = false, onEvaluation, signal, cacheHits: initialCacheHits = 0, retry, cacheNamespace } = args;
|
|
191
|
+
const { adapter, budget, cache, trackOutputs = false, onEvaluation, signal, cacheHits: initialCacheHits = 0, usage: initialUsage, retry, cacheNamespace } = args;
|
|
176
192
|
const { attempts: retryAttempts, delayMs: retryDelayMs } = {
|
|
177
193
|
...DEFAULT_RETRY,
|
|
178
194
|
...retry
|
|
179
195
|
};
|
|
196
|
+
if (initialUsage !== void 0) assertUsage({
|
|
197
|
+
reading: initialUsage,
|
|
198
|
+
source: "Checkpoint carries"
|
|
199
|
+
});
|
|
180
200
|
let cacheHits = initialCacheHits;
|
|
181
201
|
let unchargedCalls = 0;
|
|
182
202
|
const usage = {
|
|
203
|
+
inputTokens: 0,
|
|
204
|
+
outputTokens: 0,
|
|
205
|
+
totalTokens: 0,
|
|
206
|
+
costUsd: 0,
|
|
207
|
+
rollouts: 0,
|
|
208
|
+
...initialUsage
|
|
209
|
+
};
|
|
210
|
+
/** What `charge: false` bought, kept out of the totals a ceiling reads. */
|
|
211
|
+
const unchargedUsage = {
|
|
183
212
|
inputTokens: 0,
|
|
184
213
|
outputTokens: 0,
|
|
185
214
|
totalTokens: 0,
|
|
186
215
|
costUsd: 0,
|
|
187
216
|
rollouts: 0
|
|
188
217
|
};
|
|
189
|
-
/** Folds one adapter call's reported usage into the
|
|
218
|
+
/** Folds one adapter call's reported usage into the totals that bought it. */
|
|
190
219
|
function recordUsage(args) {
|
|
191
|
-
|
|
220
|
+
const totals = args.charge ? usage : unchargedUsage;
|
|
221
|
+
totals.rollouts += args.rollouts;
|
|
192
222
|
for (const rollout of args.evaluation.usage ?? []) addUsage({
|
|
193
|
-
totals
|
|
223
|
+
totals,
|
|
194
224
|
rollout
|
|
195
225
|
});
|
|
196
226
|
}
|
|
@@ -252,7 +282,8 @@ function createEvaluator(args) {
|
|
|
252
282
|
});
|
|
253
283
|
recordUsage({
|
|
254
284
|
evaluation,
|
|
255
|
-
rollouts: rows.length
|
|
285
|
+
rollouts: rows.length,
|
|
286
|
+
charge
|
|
256
287
|
});
|
|
257
288
|
return evaluation;
|
|
258
289
|
} catch (err) {
|
|
@@ -395,6 +426,18 @@ function createEvaluator(args) {
|
|
|
395
426
|
cacheHits: () => cacheHits,
|
|
396
427
|
unchargedCalls: () => unchargedCalls,
|
|
397
428
|
usage: () => ({ ...usage }),
|
|
429
|
+
unchargedUsage: () => ({ ...unchargedUsage }),
|
|
430
|
+
absorbUsage: (spent) => {
|
|
431
|
+
assertUsage({
|
|
432
|
+
reading: spent,
|
|
433
|
+
source: "Absorbed usage carries"
|
|
434
|
+
});
|
|
435
|
+
usage.inputTokens += spent.inputTokens;
|
|
436
|
+
usage.outputTokens += spent.outputTokens;
|
|
437
|
+
usage.totalTokens += spent.totalTokens;
|
|
438
|
+
usage.costUsd += spent.costUsd;
|
|
439
|
+
usage.rollouts += spent.rollouts;
|
|
440
|
+
},
|
|
398
441
|
entries: () => cache?.entries?.(),
|
|
399
442
|
restore: (entries) => {
|
|
400
443
|
for (const [key, cached] of entries) cache?.set(key, cached);
|
|
@@ -471,11 +514,29 @@ function requireMeasuredMean(args) {
|
|
|
471
514
|
function addUsage(args) {
|
|
472
515
|
const { totals, rollout } = args;
|
|
473
516
|
const { inputTokens = 0, outputTokens = 0, costUsd = 0 } = rollout;
|
|
517
|
+
assertUsage({
|
|
518
|
+
reading: rollout,
|
|
519
|
+
source: "Adapter reported"
|
|
520
|
+
});
|
|
474
521
|
totals.inputTokens += inputTokens;
|
|
475
522
|
totals.outputTokens += outputTokens;
|
|
476
523
|
totals.totalTokens += rollout.totalTokens ?? inputTokens + outputTokens;
|
|
477
524
|
totals.costUsd += costUsd;
|
|
478
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Refuses a reading the totals cannot hold, where it enters rather than once it
|
|
528
|
+
* has been absorbed. A NaN folded in makes every later `maxCostUsd` comparison
|
|
529
|
+
* false, so the ceiling stops holding without saying so, and a value that is
|
|
530
|
+
* not a number at all concatenates onto the totals instead of adding to them.
|
|
531
|
+
* `RolloutUsage` binds TypeScript callers and nothing else.
|
|
532
|
+
*/
|
|
533
|
+
function assertUsage(args) {
|
|
534
|
+
const { reading, source } = args;
|
|
535
|
+
for (const [field, value] of Object.entries(reading)) {
|
|
536
|
+
if (value === void 0) continue;
|
|
537
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error(`${source} ${field} as ${value}; usage must be a non-negative finite number`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
479
540
|
function transientIndices(evaluation) {
|
|
480
541
|
const { transient } = evaluation;
|
|
481
542
|
if (transient === void 0) return [];
|
|
@@ -518,6 +579,114 @@ function delay(milliseconds) {
|
|
|
518
579
|
});
|
|
519
580
|
}
|
|
520
581
|
//#endregion
|
|
582
|
+
//#region src/reporting.ts
|
|
583
|
+
/**
|
|
584
|
+
* Narrows an event off any optimizer's union to an acceptance. The tag is
|
|
585
|
+
* enough: every optimizer's `candidateAccepted` intersects `CandidateAccepted`,
|
|
586
|
+
* so carrying the payload is a compile-time obligation rather than a hope.
|
|
587
|
+
*/
|
|
588
|
+
function isCandidateAccepted(event) {
|
|
589
|
+
return event.type === "candidateAccepted";
|
|
590
|
+
}
|
|
591
|
+
/** Narrows an event off any optimizer's union to the end of the run. */
|
|
592
|
+
function isRunFinished(event) {
|
|
593
|
+
return event.type === "finish";
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Fans one event out to every reporter, absorbing whatever they throw. A
|
|
597
|
+
* reporter is an observer of the search, never a participant in it: a logging
|
|
598
|
+
* endpoint that is down must not decide a run's outcome.
|
|
599
|
+
*/
|
|
600
|
+
function createEmitter(reporters) {
|
|
601
|
+
return function emit(event) {
|
|
602
|
+
for (const reporter of reporters) try {
|
|
603
|
+
reporter.onEvent?.(event);
|
|
604
|
+
} catch (err) {
|
|
605
|
+
console.warn("[textopt] reporter threw while handling an event", {
|
|
606
|
+
type: event.type,
|
|
607
|
+
err
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Gives every reporter its one chance to upload what it buffered. Called from
|
|
614
|
+
* a `finally` rather than after the run: a reporter that buffers has the most
|
|
615
|
+
* to say about a run that aborted or threw, and that is exactly the run that
|
|
616
|
+
* never reaches its last line.
|
|
617
|
+
*/
|
|
618
|
+
async function flushReporters(reporters) {
|
|
619
|
+
await Promise.all(reporters.map(async (reporter) => {
|
|
620
|
+
try {
|
|
621
|
+
await reporter.flush?.();
|
|
622
|
+
} catch (err) {
|
|
623
|
+
console.warn("[textopt] reporter threw while flushing", { err });
|
|
624
|
+
}
|
|
625
|
+
}));
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* A scored batch as the per-instance row a reporter should read: an instance
|
|
629
|
+
* an infrastructure failure left unmeasured becomes `undefined` rather than
|
|
630
|
+
* the zero the adapter reported for it.
|
|
631
|
+
*
|
|
632
|
+
* The same distinction `measuredMean` makes when it averages — a row of zeros
|
|
633
|
+
* and a row of unknowns describe very different runs.
|
|
634
|
+
*/
|
|
635
|
+
function instanceRow(batch) {
|
|
636
|
+
return batch.scores.map((score, index) => batch.transient[index] === true ? void 0 : score);
|
|
637
|
+
}
|
|
638
|
+
//#endregion
|
|
639
|
+
//#region src/warnings.ts
|
|
640
|
+
/**
|
|
641
|
+
* The validation set a run will actually select against, and whatever the
|
|
642
|
+
* choice costs it.
|
|
643
|
+
*
|
|
644
|
+
* Defaulting to the training set is the right default for a first run and the
|
|
645
|
+
* wrong number to report from one. It is worse than ordinary overfitting under
|
|
646
|
+
* reflective search: the reflection prompt asks the model to mine domain facts
|
|
647
|
+
* out of the traces it is shown, so those facts come out of the very instances
|
|
648
|
+
* that then select the candidate carrying them. `"reuseTraining"` is the same
|
|
649
|
+
* behaviour with the caller's name on it, and silences the warning.
|
|
650
|
+
*/
|
|
651
|
+
function resolveValidationSet(args) {
|
|
652
|
+
const { validationSet, trainingSet } = args;
|
|
653
|
+
if (validationSet === void 0) return {
|
|
654
|
+
validationSet: trainingSet,
|
|
655
|
+
warnings: [{
|
|
656
|
+
code: "validationSetReusesTraining",
|
|
657
|
+
message: "No validationSet was given, so the search selected candidates on the training instances reflection read. bestScore is fitted to them; pass a validationSet, or a testSet to measure the gap, or validationSet: \"reuseTraining\" to accept it."
|
|
658
|
+
}]
|
|
659
|
+
};
|
|
660
|
+
return {
|
|
661
|
+
validationSet: validationSet === "reuseTraining" ? trainingSet : validationSet,
|
|
662
|
+
warnings: []
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* What the seed's own validation row says about whether the run could have
|
|
667
|
+
* learned anything.
|
|
668
|
+
*
|
|
669
|
+
* A search ranks candidates by how they differ across instances, so a seed row
|
|
670
|
+
* with no spread leaves nothing to rank: at the ceiling every proposal is a tie
|
|
671
|
+
* the acceptance test resolves by noise, and at the floor no proposal has a
|
|
672
|
+
* partial improvement to build on. Both produce a run that spends its whole
|
|
673
|
+
* budget and reports a stop reason that looks like any other.
|
|
674
|
+
*/
|
|
675
|
+
function seedScoreWarnings(args) {
|
|
676
|
+
const { scores, perfectScore } = args;
|
|
677
|
+
const measured = scores.filter((score) => score !== void 0);
|
|
678
|
+
if (measured.length === 0) return [];
|
|
679
|
+
if (measured.every((score) => score >= perfectScore)) return [{
|
|
680
|
+
code: "seedScoreSaturated",
|
|
681
|
+
message: `The seed candidate already scores ${perfectScore} on every validation instance, so no proposal has anything to improve on and the search ranks ties. Use a harder validation set or a metric that separates these instances.`
|
|
682
|
+
}];
|
|
683
|
+
if (measured.every((score) => score <= 0)) return [{
|
|
684
|
+
code: "seedScoreFloored",
|
|
685
|
+
message: "The seed candidate scores 0 on every validation instance. That is a seed with everything to gain when the feedback says what is missing, and a metric that scores nothing when it does not — the score alone cannot tell the two apart. Check the metric against a candidate you know is good before reading much into this run."
|
|
686
|
+
}];
|
|
687
|
+
return [];
|
|
688
|
+
}
|
|
689
|
+
//#endregion
|
|
521
690
|
Object.defineProperty(exports, "BudgetExhausted", {
|
|
522
691
|
enumerable: true,
|
|
523
692
|
get: function() {
|
|
@@ -536,6 +705,12 @@ Object.defineProperty(exports, "candidateFingerprint", {
|
|
|
536
705
|
return candidateFingerprint;
|
|
537
706
|
}
|
|
538
707
|
});
|
|
708
|
+
Object.defineProperty(exports, "candidateHash", {
|
|
709
|
+
enumerable: true,
|
|
710
|
+
get: function() {
|
|
711
|
+
return candidateHash;
|
|
712
|
+
}
|
|
713
|
+
});
|
|
539
714
|
Object.defineProperty(exports, "componentNames", {
|
|
540
715
|
enumerable: true,
|
|
541
716
|
get: function() {
|
|
@@ -560,6 +735,12 @@ Object.defineProperty(exports, "createDeadline", {
|
|
|
560
735
|
return createDeadline;
|
|
561
736
|
}
|
|
562
737
|
});
|
|
738
|
+
Object.defineProperty(exports, "createEmitter", {
|
|
739
|
+
enumerable: true,
|
|
740
|
+
get: function() {
|
|
741
|
+
return createEmitter;
|
|
742
|
+
}
|
|
743
|
+
});
|
|
563
744
|
Object.defineProperty(exports, "createEvaluator", {
|
|
564
745
|
enumerable: true,
|
|
565
746
|
get: function() {
|
|
@@ -572,6 +753,36 @@ Object.defineProperty(exports, "createMemoryCache", {
|
|
|
572
753
|
return createMemoryCache;
|
|
573
754
|
}
|
|
574
755
|
});
|
|
756
|
+
Object.defineProperty(exports, "defaultInstanceId", {
|
|
757
|
+
enumerable: true,
|
|
758
|
+
get: function() {
|
|
759
|
+
return defaultInstanceId;
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
Object.defineProperty(exports, "flushReporters", {
|
|
763
|
+
enumerable: true,
|
|
764
|
+
get: function() {
|
|
765
|
+
return flushReporters;
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
Object.defineProperty(exports, "instanceRow", {
|
|
769
|
+
enumerable: true,
|
|
770
|
+
get: function() {
|
|
771
|
+
return instanceRow;
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
Object.defineProperty(exports, "isCandidateAccepted", {
|
|
775
|
+
enumerable: true,
|
|
776
|
+
get: function() {
|
|
777
|
+
return isCandidateAccepted;
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
Object.defineProperty(exports, "isRunFinished", {
|
|
781
|
+
enumerable: true,
|
|
782
|
+
get: function() {
|
|
783
|
+
return isRunFinished;
|
|
784
|
+
}
|
|
785
|
+
});
|
|
575
786
|
Object.defineProperty(exports, "measuredMean", {
|
|
576
787
|
enumerable: true,
|
|
577
788
|
get: function() {
|
|
@@ -584,12 +795,24 @@ Object.defineProperty(exports, "requireMeasuredMean", {
|
|
|
584
795
|
return requireMeasuredMean;
|
|
585
796
|
}
|
|
586
797
|
});
|
|
798
|
+
Object.defineProperty(exports, "resolveValidationSet", {
|
|
799
|
+
enumerable: true,
|
|
800
|
+
get: function() {
|
|
801
|
+
return resolveValidationSet;
|
|
802
|
+
}
|
|
803
|
+
});
|
|
587
804
|
Object.defineProperty(exports, "runFingerprint", {
|
|
588
805
|
enumerable: true,
|
|
589
806
|
get: function() {
|
|
590
807
|
return runFingerprint;
|
|
591
808
|
}
|
|
592
809
|
});
|
|
810
|
+
Object.defineProperty(exports, "seedScoreWarnings", {
|
|
811
|
+
enumerable: true,
|
|
812
|
+
get: function() {
|
|
813
|
+
return seedScoreWarnings;
|
|
814
|
+
}
|
|
815
|
+
});
|
|
593
816
|
Object.defineProperty(exports, "stableHash", {
|
|
594
817
|
enumerable: true,
|
|
595
818
|
get: function() {
|
|
@@ -30,6 +30,8 @@ function createBudget(args) {
|
|
|
30
30
|
}
|
|
31
31
|
//#endregion
|
|
32
32
|
//#region src/cache.ts
|
|
33
|
+
/** What `stableHash` returns for any value that serializes to `{}`. */
|
|
34
|
+
const EMPTY_OBJECT_HASH = stableHash({});
|
|
33
35
|
/**
|
|
34
36
|
* The loop re-evaluates unchanged candidates against the same validation
|
|
35
37
|
* instances constantly (every accepted child inherits most of its parent's
|
|
@@ -65,6 +67,20 @@ function stableHash(value) {
|
|
|
65
67
|
}
|
|
66
68
|
return `${hash32(serialized, 2166136261)}${hash32(serialized, 16777619)}`;
|
|
67
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Names one data instance for the evaluation cache: a content hash, so the same
|
|
72
|
+
* row is the same instance wherever it appears in a run.
|
|
73
|
+
*
|
|
74
|
+
* Falls back to the row's position when the datum carries nothing the hash can
|
|
75
|
+
* read. A Map, a Set and a class instance holding its state privately all
|
|
76
|
+
* serialize to `{}`, and an id two rows share serves each of them the score the
|
|
77
|
+
* other measured. Position is a weaker id — it is only stable while the data is
|
|
78
|
+
* — but it is one instance per row, which is what the cache needs to be sound.
|
|
79
|
+
*/
|
|
80
|
+
function defaultInstanceId(args) {
|
|
81
|
+
const hash = stableHash(args.datum);
|
|
82
|
+
return hash === "" || hash === EMPTY_OBJECT_HASH ? String(args.index) : hash;
|
|
83
|
+
}
|
|
68
84
|
function createMemoryCache(args = {}) {
|
|
69
85
|
const { maxEntries = 1e5, entries: initial = [] } = args;
|
|
70
86
|
const entries = new Map(initial);
|
|
@@ -172,25 +188,39 @@ const DEFAULT_RETRY = {
|
|
|
172
188
|
*/
|
|
173
189
|
var BudgetExhausted = class extends Error {};
|
|
174
190
|
function createEvaluator(args) {
|
|
175
|
-
const { adapter, budget, cache, trackOutputs = false, onEvaluation, signal, cacheHits: initialCacheHits = 0, retry, cacheNamespace } = args;
|
|
191
|
+
const { adapter, budget, cache, trackOutputs = false, onEvaluation, signal, cacheHits: initialCacheHits = 0, usage: initialUsage, retry, cacheNamespace } = args;
|
|
176
192
|
const { attempts: retryAttempts, delayMs: retryDelayMs } = {
|
|
177
193
|
...DEFAULT_RETRY,
|
|
178
194
|
...retry
|
|
179
195
|
};
|
|
196
|
+
if (initialUsage !== void 0) assertUsage({
|
|
197
|
+
reading: initialUsage,
|
|
198
|
+
source: "Checkpoint carries"
|
|
199
|
+
});
|
|
180
200
|
let cacheHits = initialCacheHits;
|
|
181
201
|
let unchargedCalls = 0;
|
|
182
202
|
const usage = {
|
|
203
|
+
inputTokens: 0,
|
|
204
|
+
outputTokens: 0,
|
|
205
|
+
totalTokens: 0,
|
|
206
|
+
costUsd: 0,
|
|
207
|
+
rollouts: 0,
|
|
208
|
+
...initialUsage
|
|
209
|
+
};
|
|
210
|
+
/** What `charge: false` bought, kept out of the totals a ceiling reads. */
|
|
211
|
+
const unchargedUsage = {
|
|
183
212
|
inputTokens: 0,
|
|
184
213
|
outputTokens: 0,
|
|
185
214
|
totalTokens: 0,
|
|
186
215
|
costUsd: 0,
|
|
187
216
|
rollouts: 0
|
|
188
217
|
};
|
|
189
|
-
/** Folds one adapter call's reported usage into the
|
|
218
|
+
/** Folds one adapter call's reported usage into the totals that bought it. */
|
|
190
219
|
function recordUsage(args) {
|
|
191
|
-
|
|
220
|
+
const totals = args.charge ? usage : unchargedUsage;
|
|
221
|
+
totals.rollouts += args.rollouts;
|
|
192
222
|
for (const rollout of args.evaluation.usage ?? []) addUsage({
|
|
193
|
-
totals
|
|
223
|
+
totals,
|
|
194
224
|
rollout
|
|
195
225
|
});
|
|
196
226
|
}
|
|
@@ -252,7 +282,8 @@ function createEvaluator(args) {
|
|
|
252
282
|
});
|
|
253
283
|
recordUsage({
|
|
254
284
|
evaluation,
|
|
255
|
-
rollouts: rows.length
|
|
285
|
+
rollouts: rows.length,
|
|
286
|
+
charge
|
|
256
287
|
});
|
|
257
288
|
return evaluation;
|
|
258
289
|
} catch (err) {
|
|
@@ -395,6 +426,18 @@ function createEvaluator(args) {
|
|
|
395
426
|
cacheHits: () => cacheHits,
|
|
396
427
|
unchargedCalls: () => unchargedCalls,
|
|
397
428
|
usage: () => ({ ...usage }),
|
|
429
|
+
unchargedUsage: () => ({ ...unchargedUsage }),
|
|
430
|
+
absorbUsage: (spent) => {
|
|
431
|
+
assertUsage({
|
|
432
|
+
reading: spent,
|
|
433
|
+
source: "Absorbed usage carries"
|
|
434
|
+
});
|
|
435
|
+
usage.inputTokens += spent.inputTokens;
|
|
436
|
+
usage.outputTokens += spent.outputTokens;
|
|
437
|
+
usage.totalTokens += spent.totalTokens;
|
|
438
|
+
usage.costUsd += spent.costUsd;
|
|
439
|
+
usage.rollouts += spent.rollouts;
|
|
440
|
+
},
|
|
398
441
|
entries: () => cache?.entries?.(),
|
|
399
442
|
restore: (entries) => {
|
|
400
443
|
for (const [key, cached] of entries) cache?.set(key, cached);
|
|
@@ -471,11 +514,29 @@ function requireMeasuredMean(args) {
|
|
|
471
514
|
function addUsage(args) {
|
|
472
515
|
const { totals, rollout } = args;
|
|
473
516
|
const { inputTokens = 0, outputTokens = 0, costUsd = 0 } = rollout;
|
|
517
|
+
assertUsage({
|
|
518
|
+
reading: rollout,
|
|
519
|
+
source: "Adapter reported"
|
|
520
|
+
});
|
|
474
521
|
totals.inputTokens += inputTokens;
|
|
475
522
|
totals.outputTokens += outputTokens;
|
|
476
523
|
totals.totalTokens += rollout.totalTokens ?? inputTokens + outputTokens;
|
|
477
524
|
totals.costUsd += costUsd;
|
|
478
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Refuses a reading the totals cannot hold, where it enters rather than once it
|
|
528
|
+
* has been absorbed. A NaN folded in makes every later `maxCostUsd` comparison
|
|
529
|
+
* false, so the ceiling stops holding without saying so, and a value that is
|
|
530
|
+
* not a number at all concatenates onto the totals instead of adding to them.
|
|
531
|
+
* `RolloutUsage` binds TypeScript callers and nothing else.
|
|
532
|
+
*/
|
|
533
|
+
function assertUsage(args) {
|
|
534
|
+
const { reading, source } = args;
|
|
535
|
+
for (const [field, value] of Object.entries(reading)) {
|
|
536
|
+
if (value === void 0) continue;
|
|
537
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error(`${source} ${field} as ${value}; usage must be a non-negative finite number`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
479
540
|
function transientIndices(evaluation) {
|
|
480
541
|
const { transient } = evaluation;
|
|
481
542
|
if (transient === void 0) return [];
|
|
@@ -518,4 +579,112 @@ function delay(milliseconds) {
|
|
|
518
579
|
});
|
|
519
580
|
}
|
|
520
581
|
//#endregion
|
|
521
|
-
|
|
582
|
+
//#region src/reporting.ts
|
|
583
|
+
/**
|
|
584
|
+
* Narrows an event off any optimizer's union to an acceptance. The tag is
|
|
585
|
+
* enough: every optimizer's `candidateAccepted` intersects `CandidateAccepted`,
|
|
586
|
+
* so carrying the payload is a compile-time obligation rather than a hope.
|
|
587
|
+
*/
|
|
588
|
+
function isCandidateAccepted(event) {
|
|
589
|
+
return event.type === "candidateAccepted";
|
|
590
|
+
}
|
|
591
|
+
/** Narrows an event off any optimizer's union to the end of the run. */
|
|
592
|
+
function isRunFinished(event) {
|
|
593
|
+
return event.type === "finish";
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Fans one event out to every reporter, absorbing whatever they throw. A
|
|
597
|
+
* reporter is an observer of the search, never a participant in it: a logging
|
|
598
|
+
* endpoint that is down must not decide a run's outcome.
|
|
599
|
+
*/
|
|
600
|
+
function createEmitter(reporters) {
|
|
601
|
+
return function emit(event) {
|
|
602
|
+
for (const reporter of reporters) try {
|
|
603
|
+
reporter.onEvent?.(event);
|
|
604
|
+
} catch (err) {
|
|
605
|
+
console.warn("[textopt] reporter threw while handling an event", {
|
|
606
|
+
type: event.type,
|
|
607
|
+
err
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Gives every reporter its one chance to upload what it buffered. Called from
|
|
614
|
+
* a `finally` rather than after the run: a reporter that buffers has the most
|
|
615
|
+
* to say about a run that aborted or threw, and that is exactly the run that
|
|
616
|
+
* never reaches its last line.
|
|
617
|
+
*/
|
|
618
|
+
async function flushReporters(reporters) {
|
|
619
|
+
await Promise.all(reporters.map(async (reporter) => {
|
|
620
|
+
try {
|
|
621
|
+
await reporter.flush?.();
|
|
622
|
+
} catch (err) {
|
|
623
|
+
console.warn("[textopt] reporter threw while flushing", { err });
|
|
624
|
+
}
|
|
625
|
+
}));
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* A scored batch as the per-instance row a reporter should read: an instance
|
|
629
|
+
* an infrastructure failure left unmeasured becomes `undefined` rather than
|
|
630
|
+
* the zero the adapter reported for it.
|
|
631
|
+
*
|
|
632
|
+
* The same distinction `measuredMean` makes when it averages — a row of zeros
|
|
633
|
+
* and a row of unknowns describe very different runs.
|
|
634
|
+
*/
|
|
635
|
+
function instanceRow(batch) {
|
|
636
|
+
return batch.scores.map((score, index) => batch.transient[index] === true ? void 0 : score);
|
|
637
|
+
}
|
|
638
|
+
//#endregion
|
|
639
|
+
//#region src/warnings.ts
|
|
640
|
+
/**
|
|
641
|
+
* The validation set a run will actually select against, and whatever the
|
|
642
|
+
* choice costs it.
|
|
643
|
+
*
|
|
644
|
+
* Defaulting to the training set is the right default for a first run and the
|
|
645
|
+
* wrong number to report from one. It is worse than ordinary overfitting under
|
|
646
|
+
* reflective search: the reflection prompt asks the model to mine domain facts
|
|
647
|
+
* out of the traces it is shown, so those facts come out of the very instances
|
|
648
|
+
* that then select the candidate carrying them. `"reuseTraining"` is the same
|
|
649
|
+
* behaviour with the caller's name on it, and silences the warning.
|
|
650
|
+
*/
|
|
651
|
+
function resolveValidationSet(args) {
|
|
652
|
+
const { validationSet, trainingSet } = args;
|
|
653
|
+
if (validationSet === void 0) return {
|
|
654
|
+
validationSet: trainingSet,
|
|
655
|
+
warnings: [{
|
|
656
|
+
code: "validationSetReusesTraining",
|
|
657
|
+
message: "No validationSet was given, so the search selected candidates on the training instances reflection read. bestScore is fitted to them; pass a validationSet, or a testSet to measure the gap, or validationSet: \"reuseTraining\" to accept it."
|
|
658
|
+
}]
|
|
659
|
+
};
|
|
660
|
+
return {
|
|
661
|
+
validationSet: validationSet === "reuseTraining" ? trainingSet : validationSet,
|
|
662
|
+
warnings: []
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* What the seed's own validation row says about whether the run could have
|
|
667
|
+
* learned anything.
|
|
668
|
+
*
|
|
669
|
+
* A search ranks candidates by how they differ across instances, so a seed row
|
|
670
|
+
* with no spread leaves nothing to rank: at the ceiling every proposal is a tie
|
|
671
|
+
* the acceptance test resolves by noise, and at the floor no proposal has a
|
|
672
|
+
* partial improvement to build on. Both produce a run that spends its whole
|
|
673
|
+
* budget and reports a stop reason that looks like any other.
|
|
674
|
+
*/
|
|
675
|
+
function seedScoreWarnings(args) {
|
|
676
|
+
const { scores, perfectScore } = args;
|
|
677
|
+
const measured = scores.filter((score) => score !== void 0);
|
|
678
|
+
if (measured.length === 0) return [];
|
|
679
|
+
if (measured.every((score) => score >= perfectScore)) return [{
|
|
680
|
+
code: "seedScoreSaturated",
|
|
681
|
+
message: `The seed candidate already scores ${perfectScore} on every validation instance, so no proposal has anything to improve on and the search ranks ties. Use a harder validation set or a metric that separates these instances.`
|
|
682
|
+
}];
|
|
683
|
+
if (measured.every((score) => score <= 0)) return [{
|
|
684
|
+
code: "seedScoreFloored",
|
|
685
|
+
message: "The seed candidate scores 0 on every validation instance. That is a seed with everything to gain when the feedback says what is missing, and a metric that scores nothing when it does not — the score alone cannot tell the two apart. Check the metric against a candidate you know is good before reading much into this run."
|
|
686
|
+
}];
|
|
687
|
+
return [];
|
|
688
|
+
}
|
|
689
|
+
//#endregion
|
|
690
|
+
export { createBudget as S, componentNames as _, instanceRow as a, defaultInstanceId as b, BudgetExhausted as c, measuredMean as d, requireMeasuredMean as f, runFingerprint as g, candidateFingerprint as h, flushReporters as i, costExhausted as l, assertResumable as m, seedScoreWarnings as n, isCandidateAccepted as o, createDeadline as p, createEmitter as r, isRunFinished as s, resolveValidationSet as t, createEvaluator as u, candidateHash as v, stableHash as x, createMemoryCache as y };
|