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.
Files changed (65) hide show
  1. package/README.md +65 -25
  2. package/dist/bootstrap-search/index.cjs +159 -73
  3. package/dist/bootstrap-search/index.d.cts +32 -10
  4. package/dist/bootstrap-search/index.d.mts +32 -10
  5. package/dist/bootstrap-search/index.mjs +150 -66
  6. package/dist/demos-9v5ts7F3.cjs +244 -0
  7. package/dist/{demos-B0pVQjYC.d.mts → demos-ASsSXYXA.d.mts} +10 -3
  8. package/dist/demos-Brobjfuc.mjs +215 -0
  9. package/dist/{demos-BTuzFNsp.d.cts → demos-ByaLZy-Z.d.cts} +10 -3
  10. package/dist/file-cache.cjs +27 -8
  11. package/dist/file-cache.d.cts +13 -0
  12. package/dist/file-cache.d.mts +13 -0
  13. package/dist/file-cache.mjs +27 -8
  14. package/dist/gepa/index.cjs +128 -80
  15. package/dist/gepa/index.d.cts +15 -7
  16. package/dist/gepa/index.d.mts +15 -7
  17. package/dist/gepa/index.mjs +101 -55
  18. package/dist/index.cjs +157 -30
  19. package/dist/index.d.cts +177 -7
  20. package/dist/index.d.mts +177 -7
  21. package/dist/index.mjs +139 -18
  22. package/dist/{math-COOofUyv.cjs → math-BhlziRPc.cjs} +60 -9
  23. package/dist/math-Dqme4rYz.mjs +123 -0
  24. package/dist/mipro/index.cjs +104 -70
  25. package/dist/mipro/index.d.cts +17 -14
  26. package/dist/mipro/index.d.mts +17 -14
  27. package/dist/mipro/index.mjs +90 -58
  28. package/dist/opro/index.cjs +136 -51
  29. package/dist/opro/index.d.cts +17 -9
  30. package/dist/opro/index.d.mts +17 -9
  31. package/dist/opro/index.mjs +121 -38
  32. package/dist/{optimizer-B7SpRwl7.d.cts → optimizer-4Zv-Zt2t.d.cts} +90 -5
  33. package/dist/{optimizer-DqCoth_w.d.mts → optimizer-Ds5mzYjz.d.mts} +90 -5
  34. package/dist/random-search/index.cjs +99 -49
  35. package/dist/random-search/index.d.cts +15 -13
  36. package/dist/random-search/index.d.mts +15 -13
  37. package/dist/random-search/index.mjs +89 -41
  38. package/dist/{reflection-Cr_upzU0.d.mts → reflection-CMezGu6u.d.mts} +38 -14
  39. package/dist/{reflection-CQToe-5B.d.cts → reflection-D0A7eahD.d.cts} +38 -14
  40. package/dist/reporting-bq007_2z.d.cts +294 -0
  41. package/dist/reporting-bq007_2z.d.mts +294 -0
  42. package/dist/simba/index.cjs +216 -83
  43. package/dist/simba/index.d.cts +53 -13
  44. package/dist/simba/index.d.mts +53 -13
  45. package/dist/simba/index.mjs +206 -75
  46. package/dist/testing.cjs +1 -0
  47. package/dist/testing.d.cts +5 -3
  48. package/dist/testing.d.mts +5 -3
  49. package/dist/testing.mjs +1 -1
  50. package/dist/{evaluation-OZOp6TB7.cjs → warnings-CWRJF-jA.cjs} +228 -5
  51. package/dist/{evaluation-BV0nSZVx.mjs → warnings-OxvDi9kN.mjs} +175 -6
  52. package/docs/adapters.md +169 -0
  53. package/docs/benchmark.md +90 -0
  54. package/docs/data-prep.md +113 -0
  55. package/docs/distillation.md +128 -0
  56. package/docs/evaluation.md +87 -0
  57. package/docs/metric-preflight.md +132 -0
  58. package/docs/optimizers.md +293 -0
  59. package/docs/tuning.md +130 -0
  60. package/package.json +6 -4
  61. package/dist/demos-B9BJiNKz.cjs +0 -143
  62. package/dist/demos-Degx6UmP.mjs +0 -126
  63. package/dist/math-DhrDmpFS.mjs +0 -78
  64. package/dist/types-CWv4IQFF.d.cts +0 -129
  65. package/dist/types-CWv4IQFF.d.mts +0 -129
@@ -0,0 +1,244 @@
1
+ const require_warnings = require("./warnings-CWRJF-jA.cjs");
2
+ //#region src/harvest.ts
3
+ /**
4
+ * Run a candidate over data and keep the rollouts the metric rewarded.
5
+ *
6
+ * The library's one paid collection primitive, with two consumers: a few-shot
7
+ * block wants four of these, and a distillation set wants thousands. Both are
8
+ * the same pass — run the candidate, score it, keep what cleared the bar — so
9
+ * both share the budget, retry and transient-failure handling that pass needs.
10
+ *
11
+ * Which data to sweep is the caller's decision and the consequential one. A
12
+ * validation set is the wrong choice: it is the set that selected the candidate,
13
+ * so the rollouts it yields are enriched for the candidate's fit to those
14
+ * instances rather than to the task. Prefer the training set, or a pool held
15
+ * out of the run entirely.
16
+ */
17
+ async function harvestRollouts(args) {
18
+ const { adapter, candidate, data, minScore, maxRollouts = Number.POSITIVE_INFINITY, batchSize = Math.min(maxRollouts, data.length), maxMetricCalls = data.length, maxCostUsd, rng, signal } = args;
19
+ if (data.length === 0) throw new Error("harvestRollouts requires non-empty data");
20
+ const budget = require_warnings.createBudget({ maxMetricCalls });
21
+ const evaluator = require_warnings.createEvaluator({
22
+ adapter,
23
+ budget,
24
+ ...signal === void 0 ? {} : { signal }
25
+ });
26
+ const order = rng === void 0 ? [...data] : rng.shuffle(data);
27
+ const rollouts = [];
28
+ let attempted = 0;
29
+ for (let start = 0; start < order.length; start += batchSize) {
30
+ if (rollouts.length >= maxRollouts || signal?.aborted) break;
31
+ if (require_warnings.costExhausted({
32
+ usage: evaluator.usage(),
33
+ maxCostUsd
34
+ })) break;
35
+ const batch = order.slice(start, start + Math.min(batchSize, budget.remaining()));
36
+ if (batch.length === 0) break;
37
+ const evaluation = await evaluator.evaluateTraced({
38
+ candidate,
39
+ batch,
40
+ split: "train",
41
+ phase: "seed",
42
+ candidateId: null,
43
+ iteration: 0
44
+ });
45
+ if (evaluation === null) break;
46
+ attempted += batch.length;
47
+ for (let index = 0; index < batch.length; index += 1) {
48
+ const score = evaluation.scores[index];
49
+ if (!(minScore === void 0 ? score > 0 : score >= minScore) || rollouts.length >= maxRollouts) continue;
50
+ rollouts.push({
51
+ input: batch[index],
52
+ output: evaluation.outputs[index],
53
+ score
54
+ });
55
+ }
56
+ }
57
+ return {
58
+ rollouts,
59
+ metricCalls: budget.spent(),
60
+ usage: evaluator.usage(),
61
+ attempted
62
+ };
63
+ }
64
+ //#endregion
65
+ //#region src/demos.ts
66
+ const DEMO_OPEN = "<demo>";
67
+ const DEMO_CLOSE = "</demo>";
68
+ const DEMO_BLOCK = /<demo>\s*([\s\S]*?)\s*<\/demo>/g;
69
+ const DEMO_PARTS = /<input>\s*([\s\S]*?)\s*<\/input>\s*<output>\s*([\s\S]*?)\s*<\/output>/;
70
+ const DELIMITER_TAG = /<(\/?)(demo|input|output)>/g;
71
+ const ESCAPED_DELIMITER_TAG = /&lt;(\/?)(demo|input|output)>/g;
72
+ const DEFAULT_MAX_DEMOS = 4;
73
+ /**
74
+ * Harvest demonstrations by running a candidate over the training set and keeping
75
+ * the rollouts the metric rewarded.
76
+ *
77
+ * The cheapest signal in the whole library: a rollout that scored well is
78
+ * already paid for, and turning it into a few-shot block costs one pass over
79
+ * the data rather than a search. Instruction search and demonstrations pull on
80
+ * different parts of a model's behaviour — instructions on what to do,
81
+ * examples on what the output should look like — so a seed carrying both
82
+ * starts somewhere neither reaches alone.
83
+ */
84
+ async function harvestFewShotExamples(args) {
85
+ const { adapter, candidate, trainingSet, minScore, maxDemos = DEFAULT_MAX_DEMOS, batchSize = maxDemos, maxMetricCalls = trainingSet.length, maxCostUsd, rng, renderDemo, signal } = args;
86
+ if (trainingSet.length === 0) throw new Error("harvestFewShotExamples requires a non-empty trainingSet");
87
+ const harvest = await harvestRollouts({
88
+ adapter,
89
+ candidate,
90
+ data: trainingSet,
91
+ maxRollouts: maxDemos,
92
+ batchSize,
93
+ maxMetricCalls,
94
+ ...maxCostUsd === void 0 ? {} : { maxCostUsd },
95
+ ...minScore === void 0 ? {} : { minScore },
96
+ ...rng === void 0 ? {} : { rng },
97
+ ...signal === void 0 ? {} : { signal }
98
+ });
99
+ return {
100
+ demos: harvest.rollouts,
101
+ block: formatDemos(harvest.rollouts, renderDemo === void 0 ? {} : { render: renderDemo }),
102
+ metricCalls: harvest.metricCalls,
103
+ usage: harvest.usage,
104
+ attempted: harvest.attempted
105
+ };
106
+ }
107
+ /**
108
+ * Render demos as the text a candidate component holds.
109
+ *
110
+ * Delimited rather than free-form so `parseDemos` can read them back: a demo
111
+ * component is edited over the course of a run, and a block that cannot be
112
+ * parsed can only be replaced wholesale, throwing away every example found
113
+ * before it.
114
+ */
115
+ function formatDemos(demos, options = {}) {
116
+ const { render = renderDefault } = options;
117
+ if (demos.length === 0) return "";
118
+ return demos.map((demo, index) => `${DEMO_OPEN}\n${render({
119
+ demo,
120
+ index
121
+ })}\n${DEMO_CLOSE}`).join("\n");
122
+ }
123
+ /**
124
+ * Rewrite the demos a component holds, leaving everything else it says intact.
125
+ *
126
+ * A component is not always only examples. SIMBA appends advice to the same
127
+ * text it appends demonstrations to, so replacing the component wholesale with
128
+ * a fresh block would delete the instructions the other mutation wrote. The
129
+ * replacement lands where the first demo was, so a block a caller placed after
130
+ * its preamble stays after it.
131
+ */
132
+ function replaceDemos(args) {
133
+ const { text, demos, render } = args;
134
+ const block = formatDemos(demos, render === void 0 ? {} : { render });
135
+ const [before, ...rest] = text.replace(DEMO_BLOCK, "\0").split("\0");
136
+ if (rest.length === 0) return join([text, block]);
137
+ return join([
138
+ before ?? "",
139
+ block,
140
+ rest.join("")
141
+ ]);
142
+ }
143
+ /**
144
+ * Recover the demos from a formatted block, ignoring anything written around
145
+ * them. Text a model rewrote and mangled yields the demos it left intact
146
+ * rather than throwing: a malformed example is worth less than the rest of the
147
+ * block, not more than it.
148
+ */
149
+ function parseDemos(text) {
150
+ const demos = [];
151
+ for (const match of text.matchAll(DEMO_BLOCK)) {
152
+ const parts = (match[1] ?? "").match(DEMO_PARTS);
153
+ if (parts === null) continue;
154
+ const input = parseValue(parts[1] ?? "");
155
+ const output = parseValue(parts[2] ?? "");
156
+ demos.push({
157
+ input,
158
+ output
159
+ });
160
+ }
161
+ return demos;
162
+ }
163
+ /** Joins what survived a replacement, without leaving blank runs behind. */
164
+ function join(parts) {
165
+ return parts.map((part) => part.trim()).filter((part) => part.length > 0).join("\n\n");
166
+ }
167
+ function renderDefault(args) {
168
+ const { demo } = args;
169
+ return [
170
+ "<input>",
171
+ serialize(demo.input),
172
+ "</input>",
173
+ "<output>",
174
+ serialize(demo.output),
175
+ "</output>"
176
+ ].join("\n");
177
+ }
178
+ /** Strings stay as they are; anything else is shown as JSON. */
179
+ function serialize(value) {
180
+ if (typeof value === "string") return escapeDelimiters(value);
181
+ try {
182
+ return escapeDelimiters(JSON.stringify(value, null, 2) ?? String(value));
183
+ } catch {
184
+ return escapeDelimiters(String(value));
185
+ }
186
+ }
187
+ function parseValue(text) {
188
+ const unescaped = unescapeDelimiters(text);
189
+ try {
190
+ return JSON.parse(unescaped);
191
+ } catch {
192
+ return unescaped;
193
+ }
194
+ }
195
+ /**
196
+ * Neutralize the tags a demo block is delimited by, so a value carrying one
197
+ * cannot end the block it sits in.
198
+ *
199
+ * A system that quotes its own prompt back produces exactly that: the output
200
+ * worth keeping is a demonstration containing `</demo>`, and appending it raw
201
+ * closes the outer block early. What comes back is not the demo that went in,
202
+ * and SIMBA reparses and rewrites the block at every step, so the damage
203
+ * compounds over a run rather than showing up once.
204
+ *
205
+ * The escape is escaped first, so a value that already reads `&lt;demo>`
206
+ * survives the round trip as itself.
207
+ */
208
+ function escapeDelimiters(text) {
209
+ return text.replaceAll("&lt;", "&amp;lt;").replace(DELIMITER_TAG, "&lt;$1$2>");
210
+ }
211
+ function unescapeDelimiters(text) {
212
+ return text.replace(ESCAPED_DELIMITER_TAG, "<$1$2>").replaceAll("&amp;lt;", "&lt;");
213
+ }
214
+ //#endregion
215
+ Object.defineProperty(exports, "formatDemos", {
216
+ enumerable: true,
217
+ get: function() {
218
+ return formatDemos;
219
+ }
220
+ });
221
+ Object.defineProperty(exports, "harvestFewShotExamples", {
222
+ enumerable: true,
223
+ get: function() {
224
+ return harvestFewShotExamples;
225
+ }
226
+ });
227
+ Object.defineProperty(exports, "harvestRollouts", {
228
+ enumerable: true,
229
+ get: function() {
230
+ return harvestRollouts;
231
+ }
232
+ });
233
+ Object.defineProperty(exports, "parseDemos", {
234
+ enumerable: true,
235
+ get: function() {
236
+ return parseDemos;
237
+ }
238
+ });
239
+ Object.defineProperty(exports, "replaceDemos", {
240
+ enumerable: true,
241
+ get: function() {
242
+ return replaceDemos;
243
+ }
244
+ });
@@ -1,4 +1,4 @@
1
- import { n as Candidate, t as Adapter } from "./types-CWv4IQFF.mjs";
1
+ import { S as UsageTotals, f as Adapter, p as Candidate } from "./reporting-bq007_2z.mjs";
2
2
  import { t as Rng } from "./rng-BR5MOedA.mjs";
3
3
  //#region src/demos.d.ts
4
4
  /**
@@ -23,6 +23,8 @@ interface BootstrapResult<Datum, Output> {
23
23
  block: string;
24
24
  /** Rollouts this cost. Bootstrapping is cheap, not free. */
25
25
  metricCalls: number;
26
+ /** Tokens and dollars this cost, for a caller that bounds spend. */
27
+ usage: UsageTotals;
26
28
  attempted: number;
27
29
  }
28
30
  /**
@@ -36,7 +38,7 @@ interface BootstrapResult<Datum, Output> {
36
38
  * examples on what the output should look like — so a seed carrying both
37
39
  * starts somewhere neither reaches alone.
38
40
  */
39
- declare function bootstrapDemos<Datum, Trajectory, Output, K extends string = string>(args: {
41
+ declare function harvestFewShotExamples<Datum, Trajectory, Output, K extends string = string>(args: {
40
42
  adapter: Adapter<Datum, Trajectory, Output, K>;
41
43
  /** The candidate to run. Usually the seed, sometimes a run's winner. */
42
44
  candidate: Candidate<K>;
@@ -61,6 +63,11 @@ declare function bootstrapDemos<Datum, Trajectory, Output, K extends string = st
61
63
  batchSize?: number;
62
64
  /** Ceiling on rollouts. Defaults to one pass over the trainingSet. */
63
65
  maxMetricCalls?: number;
66
+ /**
67
+ * Ceiling on dollars this pass may spend, checked between batches. Harvesting
68
+ * runs on its own evaluator, so a caller bounding spend has to say so here.
69
+ */
70
+ maxCostUsd?: number;
64
71
  /** Shuffles the trainingSet first, so demos are not all drawn from its head. */
65
72
  rng?: Rng;
66
73
  renderDemo?: DemoRenderer<Datum, Output>;
@@ -85,4 +92,4 @@ declare function formatDemos<Datum, Output>(demos: readonly Demo<Datum, Output>[
85
92
  */
86
93
  declare function parseDemos(text: string): Demo[];
87
94
  //#endregion
88
- export { formatDemos as a, bootstrapDemos as i, Demo as n, parseDemos as o, DemoRenderer as r, BootstrapResult as t };
95
+ export { harvestFewShotExamples as a, formatDemos as i, Demo as n, parseDemos as o, DemoRenderer as r, BootstrapResult as t };
@@ -0,0 +1,215 @@
1
+ import { S as createBudget, l as costExhausted, u as createEvaluator } from "./warnings-OxvDi9kN.mjs";
2
+ //#region src/harvest.ts
3
+ /**
4
+ * Run a candidate over data and keep the rollouts the metric rewarded.
5
+ *
6
+ * The library's one paid collection primitive, with two consumers: a few-shot
7
+ * block wants four of these, and a distillation set wants thousands. Both are
8
+ * the same pass — run the candidate, score it, keep what cleared the bar — so
9
+ * both share the budget, retry and transient-failure handling that pass needs.
10
+ *
11
+ * Which data to sweep is the caller's decision and the consequential one. A
12
+ * validation set is the wrong choice: it is the set that selected the candidate,
13
+ * so the rollouts it yields are enriched for the candidate's fit to those
14
+ * instances rather than to the task. Prefer the training set, or a pool held
15
+ * out of the run entirely.
16
+ */
17
+ async function harvestRollouts(args) {
18
+ const { adapter, candidate, data, minScore, maxRollouts = Number.POSITIVE_INFINITY, batchSize = Math.min(maxRollouts, data.length), maxMetricCalls = data.length, maxCostUsd, rng, signal } = args;
19
+ if (data.length === 0) throw new Error("harvestRollouts requires non-empty data");
20
+ const budget = createBudget({ maxMetricCalls });
21
+ const evaluator = createEvaluator({
22
+ adapter,
23
+ budget,
24
+ ...signal === void 0 ? {} : { signal }
25
+ });
26
+ const order = rng === void 0 ? [...data] : rng.shuffle(data);
27
+ const rollouts = [];
28
+ let attempted = 0;
29
+ for (let start = 0; start < order.length; start += batchSize) {
30
+ if (rollouts.length >= maxRollouts || signal?.aborted) break;
31
+ if (costExhausted({
32
+ usage: evaluator.usage(),
33
+ maxCostUsd
34
+ })) break;
35
+ const batch = order.slice(start, start + Math.min(batchSize, budget.remaining()));
36
+ if (batch.length === 0) break;
37
+ const evaluation = await evaluator.evaluateTraced({
38
+ candidate,
39
+ batch,
40
+ split: "train",
41
+ phase: "seed",
42
+ candidateId: null,
43
+ iteration: 0
44
+ });
45
+ if (evaluation === null) break;
46
+ attempted += batch.length;
47
+ for (let index = 0; index < batch.length; index += 1) {
48
+ const score = evaluation.scores[index];
49
+ if (!(minScore === void 0 ? score > 0 : score >= minScore) || rollouts.length >= maxRollouts) continue;
50
+ rollouts.push({
51
+ input: batch[index],
52
+ output: evaluation.outputs[index],
53
+ score
54
+ });
55
+ }
56
+ }
57
+ return {
58
+ rollouts,
59
+ metricCalls: budget.spent(),
60
+ usage: evaluator.usage(),
61
+ attempted
62
+ };
63
+ }
64
+ //#endregion
65
+ //#region src/demos.ts
66
+ const DEMO_OPEN = "<demo>";
67
+ const DEMO_CLOSE = "</demo>";
68
+ const DEMO_BLOCK = /<demo>\s*([\s\S]*?)\s*<\/demo>/g;
69
+ const DEMO_PARTS = /<input>\s*([\s\S]*?)\s*<\/input>\s*<output>\s*([\s\S]*?)\s*<\/output>/;
70
+ const DELIMITER_TAG = /<(\/?)(demo|input|output)>/g;
71
+ const ESCAPED_DELIMITER_TAG = /&lt;(\/?)(demo|input|output)>/g;
72
+ const DEFAULT_MAX_DEMOS = 4;
73
+ /**
74
+ * Harvest demonstrations by running a candidate over the training set and keeping
75
+ * the rollouts the metric rewarded.
76
+ *
77
+ * The cheapest signal in the whole library: a rollout that scored well is
78
+ * already paid for, and turning it into a few-shot block costs one pass over
79
+ * the data rather than a search. Instruction search and demonstrations pull on
80
+ * different parts of a model's behaviour — instructions on what to do,
81
+ * examples on what the output should look like — so a seed carrying both
82
+ * starts somewhere neither reaches alone.
83
+ */
84
+ async function harvestFewShotExamples(args) {
85
+ const { adapter, candidate, trainingSet, minScore, maxDemos = DEFAULT_MAX_DEMOS, batchSize = maxDemos, maxMetricCalls = trainingSet.length, maxCostUsd, rng, renderDemo, signal } = args;
86
+ if (trainingSet.length === 0) throw new Error("harvestFewShotExamples requires a non-empty trainingSet");
87
+ const harvest = await harvestRollouts({
88
+ adapter,
89
+ candidate,
90
+ data: trainingSet,
91
+ maxRollouts: maxDemos,
92
+ batchSize,
93
+ maxMetricCalls,
94
+ ...maxCostUsd === void 0 ? {} : { maxCostUsd },
95
+ ...minScore === void 0 ? {} : { minScore },
96
+ ...rng === void 0 ? {} : { rng },
97
+ ...signal === void 0 ? {} : { signal }
98
+ });
99
+ return {
100
+ demos: harvest.rollouts,
101
+ block: formatDemos(harvest.rollouts, renderDemo === void 0 ? {} : { render: renderDemo }),
102
+ metricCalls: harvest.metricCalls,
103
+ usage: harvest.usage,
104
+ attempted: harvest.attempted
105
+ };
106
+ }
107
+ /**
108
+ * Render demos as the text a candidate component holds.
109
+ *
110
+ * Delimited rather than free-form so `parseDemos` can read them back: a demo
111
+ * component is edited over the course of a run, and a block that cannot be
112
+ * parsed can only be replaced wholesale, throwing away every example found
113
+ * before it.
114
+ */
115
+ function formatDemos(demos, options = {}) {
116
+ const { render = renderDefault } = options;
117
+ if (demos.length === 0) return "";
118
+ return demos.map((demo, index) => `${DEMO_OPEN}\n${render({
119
+ demo,
120
+ index
121
+ })}\n${DEMO_CLOSE}`).join("\n");
122
+ }
123
+ /**
124
+ * Rewrite the demos a component holds, leaving everything else it says intact.
125
+ *
126
+ * A component is not always only examples. SIMBA appends advice to the same
127
+ * text it appends demonstrations to, so replacing the component wholesale with
128
+ * a fresh block would delete the instructions the other mutation wrote. The
129
+ * replacement lands where the first demo was, so a block a caller placed after
130
+ * its preamble stays after it.
131
+ */
132
+ function replaceDemos(args) {
133
+ const { text, demos, render } = args;
134
+ const block = formatDemos(demos, render === void 0 ? {} : { render });
135
+ const [before, ...rest] = text.replace(DEMO_BLOCK, "\0").split("\0");
136
+ if (rest.length === 0) return join([text, block]);
137
+ return join([
138
+ before ?? "",
139
+ block,
140
+ rest.join("")
141
+ ]);
142
+ }
143
+ /**
144
+ * Recover the demos from a formatted block, ignoring anything written around
145
+ * them. Text a model rewrote and mangled yields the demos it left intact
146
+ * rather than throwing: a malformed example is worth less than the rest of the
147
+ * block, not more than it.
148
+ */
149
+ function parseDemos(text) {
150
+ const demos = [];
151
+ for (const match of text.matchAll(DEMO_BLOCK)) {
152
+ const parts = (match[1] ?? "").match(DEMO_PARTS);
153
+ if (parts === null) continue;
154
+ const input = parseValue(parts[1] ?? "");
155
+ const output = parseValue(parts[2] ?? "");
156
+ demos.push({
157
+ input,
158
+ output
159
+ });
160
+ }
161
+ return demos;
162
+ }
163
+ /** Joins what survived a replacement, without leaving blank runs behind. */
164
+ function join(parts) {
165
+ return parts.map((part) => part.trim()).filter((part) => part.length > 0).join("\n\n");
166
+ }
167
+ function renderDefault(args) {
168
+ const { demo } = args;
169
+ return [
170
+ "<input>",
171
+ serialize(demo.input),
172
+ "</input>",
173
+ "<output>",
174
+ serialize(demo.output),
175
+ "</output>"
176
+ ].join("\n");
177
+ }
178
+ /** Strings stay as they are; anything else is shown as JSON. */
179
+ function serialize(value) {
180
+ if (typeof value === "string") return escapeDelimiters(value);
181
+ try {
182
+ return escapeDelimiters(JSON.stringify(value, null, 2) ?? String(value));
183
+ } catch {
184
+ return escapeDelimiters(String(value));
185
+ }
186
+ }
187
+ function parseValue(text) {
188
+ const unescaped = unescapeDelimiters(text);
189
+ try {
190
+ return JSON.parse(unescaped);
191
+ } catch {
192
+ return unescaped;
193
+ }
194
+ }
195
+ /**
196
+ * Neutralize the tags a demo block is delimited by, so a value carrying one
197
+ * cannot end the block it sits in.
198
+ *
199
+ * A system that quotes its own prompt back produces exactly that: the output
200
+ * worth keeping is a demonstration containing `</demo>`, and appending it raw
201
+ * closes the outer block early. What comes back is not the demo that went in,
202
+ * and SIMBA reparses and rewrites the block at every step, so the damage
203
+ * compounds over a run rather than showing up once.
204
+ *
205
+ * The escape is escaped first, so a value that already reads `&lt;demo>`
206
+ * survives the round trip as itself.
207
+ */
208
+ function escapeDelimiters(text) {
209
+ return text.replaceAll("&lt;", "&amp;lt;").replace(DELIMITER_TAG, "&lt;$1$2>");
210
+ }
211
+ function unescapeDelimiters(text) {
212
+ return text.replace(ESCAPED_DELIMITER_TAG, "<$1$2>").replaceAll("&amp;lt;", "&lt;");
213
+ }
214
+ //#endregion
215
+ export { harvestRollouts as a, replaceDemos as i, harvestFewShotExamples as n, parseDemos as r, formatDemos as t };
@@ -1,4 +1,4 @@
1
- import { n as Candidate, t as Adapter } from "./types-CWv4IQFF.cjs";
1
+ import { S as UsageTotals, f as Adapter, p as Candidate } from "./reporting-bq007_2z.cjs";
2
2
  import { t as Rng } from "./rng-BR5MOedA.cjs";
3
3
  //#region src/demos.d.ts
4
4
  /**
@@ -23,6 +23,8 @@ interface BootstrapResult<Datum, Output> {
23
23
  block: string;
24
24
  /** Rollouts this cost. Bootstrapping is cheap, not free. */
25
25
  metricCalls: number;
26
+ /** Tokens and dollars this cost, for a caller that bounds spend. */
27
+ usage: UsageTotals;
26
28
  attempted: number;
27
29
  }
28
30
  /**
@@ -36,7 +38,7 @@ interface BootstrapResult<Datum, Output> {
36
38
  * examples on what the output should look like — so a seed carrying both
37
39
  * starts somewhere neither reaches alone.
38
40
  */
39
- declare function bootstrapDemos<Datum, Trajectory, Output, K extends string = string>(args: {
41
+ declare function harvestFewShotExamples<Datum, Trajectory, Output, K extends string = string>(args: {
40
42
  adapter: Adapter<Datum, Trajectory, Output, K>;
41
43
  /** The candidate to run. Usually the seed, sometimes a run's winner. */
42
44
  candidate: Candidate<K>;
@@ -61,6 +63,11 @@ declare function bootstrapDemos<Datum, Trajectory, Output, K extends string = st
61
63
  batchSize?: number;
62
64
  /** Ceiling on rollouts. Defaults to one pass over the trainingSet. */
63
65
  maxMetricCalls?: number;
66
+ /**
67
+ * Ceiling on dollars this pass may spend, checked between batches. Harvesting
68
+ * runs on its own evaluator, so a caller bounding spend has to say so here.
69
+ */
70
+ maxCostUsd?: number;
64
71
  /** Shuffles the trainingSet first, so demos are not all drawn from its head. */
65
72
  rng?: Rng;
66
73
  renderDemo?: DemoRenderer<Datum, Output>;
@@ -85,4 +92,4 @@ declare function formatDemos<Datum, Output>(demos: readonly Demo<Datum, Output>[
85
92
  */
86
93
  declare function parseDemos(text: string): Demo[];
87
94
  //#endregion
88
- export { formatDemos as a, bootstrapDemos as i, Demo as n, parseDemos as o, DemoRenderer as r, BootstrapResult as t };
95
+ export { harvestFewShotExamples as a, formatDemos as i, Demo as n, parseDemos as o, DemoRenderer as r, BootstrapResult as t };
@@ -13,20 +13,33 @@ let node_path = require("node:path");
13
13
  * Append-only rather than rewritten: a score is never invalidated (the key
14
14
  * names the candidate, the instance, and the environment), and a log survives
15
15
  * a process killed mid-write, which a file rewritten in place does not.
16
+ *
17
+ * `namespace` is what makes that invariant true. A cached score measures a
18
+ * whole system, not a candidate, and this log outlives every part of that
19
+ * system a run does not pass through the key: the model id behind an alias the
20
+ * provider upgraded, the decoding settings, the scorer's own version. It is
21
+ * required rather than optional because the failure it prevents is silent —
22
+ * scores from one system served to a run of another, with a normal-looking
23
+ * result and no way to read afterwards that it happened.
16
24
  */
17
25
  function createFileCache(args) {
18
- const { path, maxEntries = 1e6 } = args;
26
+ const { path, namespace, maxEntries = 1e6 } = args;
27
+ if (namespace.trim() === "") throw new Error("createFileCache requires a non-empty namespace naming the system these scores measure");
19
28
  (0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
20
- const entries = readLog(path);
29
+ const log = readLog(path);
30
+ const entries = log.entries;
31
+ const scope = (key) => `${namespace}\u0000${key}`;
32
+ if (log.unterminated) (0, node_fs.appendFileSync)(path, "\n");
21
33
  return {
22
- get: (key) => entries.get(key),
34
+ get: (key) => entries.get(scope(key)),
23
35
  set: (key, cached) => {
24
- if (entries.size >= maxEntries && !entries.has(key)) {
36
+ const scoped = scope(key);
37
+ if (entries.size >= maxEntries && !entries.has(scoped)) {
25
38
  const oldest = entries.keys().next();
26
39
  if (!oldest.done) entries.delete(oldest.value);
27
40
  }
28
- entries.set(key, cached);
29
- (0, node_fs.appendFileSync)(path, `${JSON.stringify([key, cached])}\n`);
41
+ entries.set(scoped, cached);
42
+ (0, node_fs.appendFileSync)(path, `${JSON.stringify([scoped, cached])}\n`);
30
43
  }
31
44
  };
32
45
  }
@@ -42,14 +55,20 @@ function readLog(path) {
42
55
  try {
43
56
  contents = (0, node_fs.readFileSync)(path, "utf8");
44
57
  } catch {
45
- return entries;
58
+ return {
59
+ entries,
60
+ unterminated: false
61
+ };
46
62
  }
47
63
  for (const line of contents.split("\n")) {
48
64
  if (line.length === 0) continue;
49
65
  const entry = parseEntry(line);
50
66
  if (entry !== void 0) entries.set(entry[0], entry[1]);
51
67
  }
52
- return entries;
68
+ return {
69
+ entries,
70
+ unterminated: contents.length > 0 && !contents.endsWith("\n")
71
+ };
53
72
  }
54
73
  function parseEntry(line) {
55
74
  let parsed;
@@ -11,9 +11,22 @@ import { n as EvaluationCache } from "./cache-CuSo0NJ8.cjs";
11
11
  * Append-only rather than rewritten: a score is never invalidated (the key
12
12
  * names the candidate, the instance, and the environment), and a log survives
13
13
  * a process killed mid-write, which a file rewritten in place does not.
14
+ *
15
+ * `namespace` is what makes that invariant true. A cached score measures a
16
+ * whole system, not a candidate, and this log outlives every part of that
17
+ * system a run does not pass through the key: the model id behind an alias the
18
+ * provider upgraded, the decoding settings, the scorer's own version. It is
19
+ * required rather than optional because the failure it prevents is silent —
20
+ * scores from one system served to a run of another, with a normal-looking
21
+ * result and no way to read afterwards that it happened.
14
22
  */
15
23
  declare function createFileCache(args: {
16
24
  path: string;
25
+ /**
26
+ * Names the system these scores measure — model id, decoding settings,
27
+ * scorer version. Change it whenever any of them changes.
28
+ */
29
+ namespace: string;
17
30
  /** Entries kept in memory. The file itself is never trimmed. */
18
31
  maxEntries?: number;
19
32
  }): EvaluationCache;
@@ -11,9 +11,22 @@ import { n as EvaluationCache } from "./cache-CuSo0NJ8.mjs";
11
11
  * Append-only rather than rewritten: a score is never invalidated (the key
12
12
  * names the candidate, the instance, and the environment), and a log survives
13
13
  * a process killed mid-write, which a file rewritten in place does not.
14
+ *
15
+ * `namespace` is what makes that invariant true. A cached score measures a
16
+ * whole system, not a candidate, and this log outlives every part of that
17
+ * system a run does not pass through the key: the model id behind an alias the
18
+ * provider upgraded, the decoding settings, the scorer's own version. It is
19
+ * required rather than optional because the failure it prevents is silent —
20
+ * scores from one system served to a run of another, with a normal-looking
21
+ * result and no way to read afterwards that it happened.
14
22
  */
15
23
  declare function createFileCache(args: {
16
24
  path: string;
25
+ /**
26
+ * Names the system these scores measure — model id, decoding settings,
27
+ * scorer version. Change it whenever any of them changes.
28
+ */
29
+ namespace: string;
17
30
  /** Entries kept in memory. The file itself is never trimmed. */
18
31
  maxEntries?: number;
19
32
  }): EvaluationCache;