crewhaus 0.1.7 → 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 +10 -3
- package/dist/advice-apply.d.ts +182 -0
- package/dist/advice-apply.js +286 -0
- package/dist/advise-rules.d.ts +348 -0
- package/dist/advise-rules.js +905 -0
- package/dist/alert-sink.d.ts +48 -0
- package/dist/alert-sink.js +86 -0
- package/dist/approval-gate.d.ts +127 -0
- package/dist/approval-gate.js +254 -0
- package/dist/audit-verify.d.ts +69 -0
- package/dist/audit-verify.js +97 -0
- package/dist/autodistill.d.ts +113 -0
- package/dist/autodistill.js +256 -0
- package/dist/channel-provision.d.ts +360 -0
- package/dist/channel-provision.js +881 -0
- package/dist/ci-scaffold.d.ts +31 -0
- package/dist/ci-scaffold.js +343 -0
- package/dist/compile-check.d.ts +90 -0
- package/dist/compile-check.js +285 -0
- package/dist/compliance-schedule.d.ts +35 -0
- package/dist/compliance-schedule.js +36 -0
- package/dist/context-pressure.d.ts +80 -0
- package/dist/context-pressure.js +166 -0
- package/dist/dataset-mine.d.ts +172 -0
- package/dist/dataset-mine.js +403 -0
- package/dist/datasets.d.ts +124 -0
- package/dist/datasets.js +260 -0
- package/dist/deploy-canary.d.ts +83 -0
- package/dist/deploy-canary.js +87 -0
- package/dist/doctor-checks.d.ts +33 -0
- package/dist/doctor-checks.js +92 -0
- package/dist/doctor-detect.d.ts +108 -0
- package/dist/doctor-detect.js +214 -0
- package/dist/doctor-fix.d.ts +81 -0
- package/dist/doctor-fix.js +164 -0
- package/dist/egress-triage.d.ts +121 -0
- package/dist/egress-triage.js +261 -0
- package/dist/eval-bridge.d.ts +114 -0
- package/dist/eval-bridge.js +158 -0
- package/dist/eval-coverage.d.ts +140 -0
- package/dist/eval-coverage.js +428 -0
- package/dist/eval-history.d.ts +48 -0
- package/dist/eval-history.js +157 -0
- package/dist/eval-matrix.d.ts +80 -0
- package/dist/eval-matrix.js +182 -0
- package/dist/eval-sentinel.d.ts +65 -0
- package/dist/eval-sentinel.js +132 -0
- package/dist/faq.d.ts +68 -0
- package/dist/faq.js +168 -0
- package/dist/feedback.d.ts +246 -0
- package/dist/feedback.js +612 -0
- package/dist/fewshot.d.ts +83 -0
- package/dist/fewshot.js +158 -0
- package/dist/fleet.d.ts +207 -0
- package/dist/fleet.js +488 -0
- package/dist/flywheel.d.ts +193 -0
- package/dist/flywheel.js +519 -0
- package/dist/graders-suggest.d.ts +186 -0
- package/dist/graders-suggest.js +658 -0
- package/dist/incident.d.ts +99 -0
- package/dist/incident.js +217 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +11727 -740
- package/dist/init-interactive.d.ts +105 -0
- package/dist/init-interactive.js +208 -0
- package/dist/intents.d.ts +105 -0
- package/dist/intents.js +292 -0
- package/dist/judge-calibrate.d.ts +137 -0
- package/dist/judge-calibrate.js +247 -0
- package/dist/justification-calibrate.d.ts +150 -0
- package/dist/justification-calibrate.js +262 -0
- package/dist/justification-gate.d.ts +27 -6
- package/dist/justification-gate.js +30 -6
- package/dist/knowledge-sync.d.ts +179 -0
- package/dist/knowledge-sync.js +551 -0
- package/dist/lessons.d.ts +87 -0
- package/dist/lessons.js +207 -0
- package/dist/lint.d.ts +127 -0
- package/dist/lint.js +226 -0
- package/dist/loadtest.d.ts +114 -0
- package/dist/loadtest.js +196 -0
- package/dist/marketplace-cli.d.ts +110 -0
- package/dist/marketplace-cli.js +250 -0
- package/dist/mcp-doctor.d.ts +121 -0
- package/dist/mcp-doctor.js +249 -0
- package/dist/model-scan.d.ts +116 -0
- package/dist/model-scan.js +226 -0
- package/dist/onchain-tune.d.ts +164 -0
- package/dist/onchain-tune.js +346 -0
- package/dist/permissions-suggest.d.ts +126 -0
- package/dist/permissions-suggest.js +333 -0
- package/dist/pii-tune.d.ts +107 -0
- package/dist/pii-tune.js +122 -0
- package/dist/propose.d.ts +117 -0
- package/dist/propose.js +184 -0
- package/dist/refresh-goldens.d.ts +82 -0
- package/dist/refresh-goldens.js +221 -0
- package/dist/regression-pin.d.ts +160 -0
- package/dist/regression-pin.js +281 -0
- package/dist/retention.d.ts +193 -0
- package/dist/retention.js +607 -0
- package/dist/retire.d.ts +118 -0
- package/dist/retire.js +291 -0
- package/dist/right-size.d.ts +100 -0
- package/dist/right-size.js +123 -0
- package/dist/scaffold-evals.d.ts +138 -0
- package/dist/scaffold-evals.js +410 -0
- package/dist/scope-audit-drift.d.ts +139 -0
- package/dist/scope-audit-drift.js +260 -0
- package/dist/security-corpus.d.ts +237 -0
- package/dist/security-corpus.js +516 -0
- package/dist/security-digest.d.ts +173 -0
- package/dist/security-digest.js +650 -0
- package/dist/sessions-index.d.ts +27 -0
- package/dist/sessions-index.js +51 -0
- package/dist/slo-doctor.d.ts +67 -0
- package/dist/slo-doctor.js +119 -0
- package/dist/slo-sink.d.ts +96 -0
- package/dist/slo-sink.js +107 -0
- package/dist/spec-changelog.d.ts +102 -0
- package/dist/spec-changelog.js +237 -0
- package/dist/state-backup.d.ts +223 -0
- package/dist/state-backup.js +648 -0
- package/dist/tools-cli.d.ts +170 -0
- package/dist/tools-cli.js +298 -0
- package/dist/triage.d.ts +202 -0
- package/dist/triage.js +403 -0
- package/dist/upgrade.d.ts +57 -0
- package/dist/upgrade.js +113 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +27 -0
- package/dist/voice-eval.d.ts +138 -0
- package/dist/voice-eval.js +309 -0
- package/dist/watch.d.ts +58 -0
- package/dist/watch.js +97 -0
- package/package.json +89 -64
package/dist/feedback.js
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response-feedback core — the pure, side-effect-free half of the
|
|
3
|
+
* `crewhaus rate` / `crewhaus feedback` / `crewhaus distill` subcommands.
|
|
4
|
+
*
|
|
5
|
+
* A `FeedbackRecord` is one human rating placed on one assistant turn (a
|
|
6
|
+
* thumbs up/down, a 1–5 star vote, an arbitrary scale vote, and/or a
|
|
7
|
+
* free-text comment or correction). The capture surfaces write it durably as
|
|
8
|
+
* a `user_feedback` event-log line (payload = FeedbackRecord); the web-UI host
|
|
9
|
+
* writes bare records to `.crewhaus/feedback/*.jsonl`. `distill` reads both,
|
|
10
|
+
* pairs each rating to a conversation turn, and emits the two artifacts the
|
|
11
|
+
* R-eval stack already consumes: a `Sample[]` dataset (`@crewhaus/eval-dataset`)
|
|
12
|
+
* and a `graders.yaml` (`@crewhaus/eval-grader`).
|
|
13
|
+
*
|
|
14
|
+
* Everything here is pure so it is unit-testable; all filesystem access lives
|
|
15
|
+
* in `apps/cli/src/index.ts` (mirrors `doctor-checks.ts` / `scope-audit.ts`).
|
|
16
|
+
*
|
|
17
|
+
* turnNumber convention: the 1-based ordinal of USER-TEXT turns in a session.
|
|
18
|
+
* A `user_message` whose content is a string (or an array containing a `text`
|
|
19
|
+
* block and no `tool_result` block) starts a new turn; a `user_message` that
|
|
20
|
+
* carries only `tool_result` blocks is a tool-result echo, and one marked
|
|
21
|
+
* `synthetic: true` (a runtime-injected loop/continue/tombstone nudge) is NOT a
|
|
22
|
+
* turn. This is the same count the CLI `--turn` flag, the web-UI (prompts
|
|
23
|
+
* sent), and the runtime's runContext.turnNumber all compute, so a rating keys
|
|
24
|
+
* back to the exact exchange it rated even after a recovery fires mid-session.
|
|
25
|
+
*/
|
|
26
|
+
/** The event-log EventKind under which a FeedbackRecord is persisted. */
|
|
27
|
+
export const FEEDBACK_EVENT_KIND = "user_feedback";
|
|
28
|
+
export const FEEDBACK_SCHEMA_VERSION = 1;
|
|
29
|
+
const SESSION_ID_REGEX = /^sess_[0-9a-f]{16}$/;
|
|
30
|
+
/** Max stored length for a free-text comment/correction. Untrusted input (a
|
|
31
|
+
* hand-edited or web-UI-supplied `.crewhaus/feedback` line) flows into
|
|
32
|
+
* expected_output/metadata and the optimizer meta-prompt, so it is bounded at
|
|
33
|
+
* ingestion and stripped of control chars (newline/tab kept). */
|
|
34
|
+
export const MAX_FEEDBACK_TEXT = 8192;
|
|
35
|
+
export function clipFeedbackText(s) {
|
|
36
|
+
// Drop C0/C1 control chars and DEL (keeping tab, newline, carriage return),
|
|
37
|
+
// then bound the length. Built by code point so the source carries no
|
|
38
|
+
// literal control characters.
|
|
39
|
+
let out = "";
|
|
40
|
+
for (const ch of s) {
|
|
41
|
+
const c = ch.codePointAt(0) ?? 0;
|
|
42
|
+
const printable = c === 9 || c === 10 || c === 13 || (c >= 0x20 && c !== 0x7f && !(c >= 0x80 && c <= 0x9f));
|
|
43
|
+
if (printable)
|
|
44
|
+
out += ch;
|
|
45
|
+
if (out.length >= MAX_FEEDBACK_TEXT)
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
return out.slice(0, MAX_FEEDBACK_TEXT);
|
|
49
|
+
}
|
|
50
|
+
function isValidRating(rating) {
|
|
51
|
+
const { thumbs, stars, scale } = rating;
|
|
52
|
+
if (thumbs !== undefined && thumbs !== "up" && thumbs !== "down")
|
|
53
|
+
return false;
|
|
54
|
+
if (stars !== undefined) {
|
|
55
|
+
if (typeof stars !== "number" || !Number.isInteger(stars) || stars < 1 || stars > 5)
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (scale !== undefined) {
|
|
59
|
+
if (typeof scale !== "object" || scale === null)
|
|
60
|
+
return false;
|
|
61
|
+
const s = scale;
|
|
62
|
+
if (!["value", "min", "max"].every((k) => typeof s[k] === "number" && Number.isFinite(s[k]))) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
/** Narrow an arbitrary parsed JSON value to a FeedbackRecord. Tolerant of the
|
|
69
|
+
* optional fields; rejects anything missing the required core OR carrying a
|
|
70
|
+
* malformed rating (so a corrupt/crafted line can't coerce a NaN/spurious
|
|
71
|
+
* score downstream). */
|
|
72
|
+
export function isFeedbackRecord(value) {
|
|
73
|
+
if (typeof value !== "object" || value === null)
|
|
74
|
+
return false;
|
|
75
|
+
const v = value;
|
|
76
|
+
if (v["schemaVersion"] !== 1)
|
|
77
|
+
return false;
|
|
78
|
+
if (typeof v["id"] !== "string")
|
|
79
|
+
return false;
|
|
80
|
+
if (typeof v["sessionId"] !== "string" || !SESSION_ID_REGEX.test(v["sessionId"])) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (typeof v["turnNumber"] !== "number" || !Number.isFinite(v["turnNumber"]))
|
|
84
|
+
return false;
|
|
85
|
+
const modality = v["modality"];
|
|
86
|
+
if (modality !== "binary" &&
|
|
87
|
+
modality !== "stars" &&
|
|
88
|
+
modality !== "scale" &&
|
|
89
|
+
modality !== "comment") {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (typeof v["rating"] !== "object" || v["rating"] === null)
|
|
93
|
+
return false;
|
|
94
|
+
if (!isValidRating(v["rating"]))
|
|
95
|
+
return false;
|
|
96
|
+
if (typeof v["source"] !== "string")
|
|
97
|
+
return false;
|
|
98
|
+
if (typeof v["ts"] !== "string")
|
|
99
|
+
return false;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Assemble + validate a FeedbackRecord from capture-surface inputs. The
|
|
104
|
+
* modality is inferred from which rating fields are present (thumbs → binary,
|
|
105
|
+
* stars → stars, score → scale, else → comment). Throws a plain Error (the CLI
|
|
106
|
+
* routes it through `die`) when no rating signal at all is supplied.
|
|
107
|
+
*/
|
|
108
|
+
export function buildFeedbackRecord(input) {
|
|
109
|
+
const rating = {};
|
|
110
|
+
let modality;
|
|
111
|
+
if (input.thumbs !== undefined) {
|
|
112
|
+
rating.thumbs = input.thumbs;
|
|
113
|
+
modality = "binary";
|
|
114
|
+
}
|
|
115
|
+
if (input.stars !== undefined) {
|
|
116
|
+
if (!Number.isInteger(input.stars) || input.stars < 1 || input.stars > 5) {
|
|
117
|
+
throw new Error(`--stars must be an integer 1–5 (got ${input.stars})`);
|
|
118
|
+
}
|
|
119
|
+
rating.stars = input.stars;
|
|
120
|
+
modality = "stars";
|
|
121
|
+
}
|
|
122
|
+
if (input.score !== undefined) {
|
|
123
|
+
if (!Number.isFinite(input.score) || input.score < 0 || input.score > 1) {
|
|
124
|
+
throw new Error(`--score must be a number in [0,1] (got ${input.score})`);
|
|
125
|
+
}
|
|
126
|
+
rating.scale = { value: input.score, min: 0, max: 1 };
|
|
127
|
+
modality = "scale";
|
|
128
|
+
}
|
|
129
|
+
if (modality === undefined) {
|
|
130
|
+
if (input.comment === undefined && input.correction === undefined) {
|
|
131
|
+
throw new Error("no rating supplied — give --thumbs, --stars, --score, --text, or --correction");
|
|
132
|
+
}
|
|
133
|
+
modality = "comment";
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
schemaVersion: FEEDBACK_SCHEMA_VERSION,
|
|
137
|
+
id: input.id,
|
|
138
|
+
sessionId: input.sessionId,
|
|
139
|
+
...(input.runId !== undefined ? { runId: input.runId } : {}),
|
|
140
|
+
turnNumber: input.turnNumber,
|
|
141
|
+
...(input.targetSpanId !== undefined ? { targetSpanId: input.targetSpanId } : {}),
|
|
142
|
+
modality,
|
|
143
|
+
rating,
|
|
144
|
+
...(input.comment !== undefined ? { comment: clipFeedbackText(input.comment) } : {}),
|
|
145
|
+
...(input.correction !== undefined ? { correction: clipFeedbackText(input.correction) } : {}),
|
|
146
|
+
source: input.source,
|
|
147
|
+
...(input.rater !== undefined ? { rater: input.rater } : {}),
|
|
148
|
+
ts: input.ts,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Normalize a rating to [0,1] — the same convention `GradeResult.score` uses
|
|
153
|
+
* ((n-1)/4 for a 1–5 judge score). Returns `undefined` for a comment-only
|
|
154
|
+
* record (no numeric signal). Thumbs: up→1, down→0. Stars n→(n-1)/4. Scale
|
|
155
|
+
* value→(value-min)/(max-min), clamped.
|
|
156
|
+
*/
|
|
157
|
+
export function normalizeRating(r) {
|
|
158
|
+
const { thumbs, stars, scale } = r.rating;
|
|
159
|
+
if (thumbs !== undefined)
|
|
160
|
+
return thumbs === "up" ? 1 : 0;
|
|
161
|
+
if (stars !== undefined)
|
|
162
|
+
return clamp01((stars - 1) / 4);
|
|
163
|
+
if (scale !== undefined) {
|
|
164
|
+
const span = scale.max - scale.min;
|
|
165
|
+
if (span <= 0)
|
|
166
|
+
return undefined;
|
|
167
|
+
return clamp01((scale.value - scale.min) / span);
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
function clamp01(n) {
|
|
172
|
+
return n < 0 ? 0 : n > 1 ? 1 : n;
|
|
173
|
+
}
|
|
174
|
+
function turnKey(sessionId, turnNumber) {
|
|
175
|
+
return `${sessionId}#${turnNumber}`;
|
|
176
|
+
}
|
|
177
|
+
function contentBlocks(payload) {
|
|
178
|
+
const content = payload?.content;
|
|
179
|
+
if (typeof content === "string")
|
|
180
|
+
return { blocks: [], text: content };
|
|
181
|
+
if (Array.isArray(content))
|
|
182
|
+
return { blocks: content };
|
|
183
|
+
return { blocks: [] };
|
|
184
|
+
}
|
|
185
|
+
/** The text when a `user_message` is a real user-text turn (string content, or
|
|
186
|
+
* an array with ≥1 text block and no tool_result block); undefined otherwise.
|
|
187
|
+
* Runtime-injected nudges (loop warnings, "continue"/tombstone recovery) carry
|
|
188
|
+
* `synthetic: true` and are NOT turns — skipping them keeps deriveTurns's turn
|
|
189
|
+
* ordinal aligned with the runtime's runContext.turnNumber and the web UI. */
|
|
190
|
+
function userTurnText(payload) {
|
|
191
|
+
if (payload !== null &&
|
|
192
|
+
typeof payload === "object" &&
|
|
193
|
+
payload.synthetic === true) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
const { blocks, text } = contentBlocks(payload);
|
|
197
|
+
if (text !== undefined)
|
|
198
|
+
return text;
|
|
199
|
+
const hasToolResult = blocks.some((b) => b.type === "tool_result");
|
|
200
|
+
if (hasToolResult)
|
|
201
|
+
return undefined;
|
|
202
|
+
const texts = blocks
|
|
203
|
+
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
204
|
+
.map((b) => b.text);
|
|
205
|
+
return texts.length > 0 ? texts.join("\n") : undefined;
|
|
206
|
+
}
|
|
207
|
+
function assistantText(payload) {
|
|
208
|
+
const { blocks, text } = contentBlocks(payload);
|
|
209
|
+
if (text !== undefined)
|
|
210
|
+
return text;
|
|
211
|
+
return blocks
|
|
212
|
+
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
213
|
+
.map((b) => b.text)
|
|
214
|
+
.join("\n");
|
|
215
|
+
}
|
|
216
|
+
function assistantToolNames(payload) {
|
|
217
|
+
const { blocks } = contentBlocks(payload);
|
|
218
|
+
return blocks
|
|
219
|
+
.filter((b) => b.type === "tool_use" && typeof b.name === "string")
|
|
220
|
+
.map((b) => b.name);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Reconstruct conversation turns from a session transcript. Each user-text
|
|
224
|
+
* message opens a turn; the assistant's messages until the next user-text
|
|
225
|
+
* message form its response. `output` is the LAST assistant text (the answer);
|
|
226
|
+
* `toolNames` are the unique tool names the assistant called, in first-seen
|
|
227
|
+
* order.
|
|
228
|
+
*/
|
|
229
|
+
export function deriveTurns(events) {
|
|
230
|
+
const turns = [];
|
|
231
|
+
let current;
|
|
232
|
+
let counter = 0;
|
|
233
|
+
const finalize = () => {
|
|
234
|
+
if (!current)
|
|
235
|
+
return;
|
|
236
|
+
turns.push({
|
|
237
|
+
turnNumber: current.turnNumber,
|
|
238
|
+
input: current.input,
|
|
239
|
+
output: current.texts.length > 0 ? current.texts[current.texts.length - 1] : "",
|
|
240
|
+
toolNames: current.tools,
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
for (const ev of events) {
|
|
244
|
+
if (ev.kind === "user_message") {
|
|
245
|
+
const text = userTurnText(ev.payload);
|
|
246
|
+
if (text !== undefined) {
|
|
247
|
+
finalize();
|
|
248
|
+
counter += 1;
|
|
249
|
+
current = { turnNumber: counter, input: text, texts: [], tools: [], toolSeen: new Set() };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else if (ev.kind === "assistant_message" && current) {
|
|
253
|
+
const t = assistantText(ev.payload);
|
|
254
|
+
if (t !== "")
|
|
255
|
+
current.texts.push(t);
|
|
256
|
+
for (const name of assistantToolNames(ev.payload)) {
|
|
257
|
+
if (!current.toolSeen.has(name)) {
|
|
258
|
+
current.toolSeen.add(name);
|
|
259
|
+
current.tools.push(name);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
finalize();
|
|
265
|
+
return turns;
|
|
266
|
+
}
|
|
267
|
+
/** Extract FeedbackRecords from parsed JSON objects. Accepts both event-log
|
|
268
|
+
* envelopes (`{ kind: "user_feedback", payload }`) and bare records (the
|
|
269
|
+
* web-UI host's `.crewhaus/feedback/*.jsonl`). */
|
|
270
|
+
export function extractFeedbackRecords(objects) {
|
|
271
|
+
const out = [];
|
|
272
|
+
for (const obj of objects) {
|
|
273
|
+
if (obj && typeof obj === "object" && obj.kind === FEEDBACK_EVENT_KIND) {
|
|
274
|
+
const payload = obj.payload;
|
|
275
|
+
if (isFeedbackRecord(payload))
|
|
276
|
+
out.push(payload);
|
|
277
|
+
}
|
|
278
|
+
else if (isFeedbackRecord(obj)) {
|
|
279
|
+
out.push(obj);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Collapse records to one per (sessionId, turnNumber). Rather than pure
|
|
286
|
+
* last-write-wins — which would let a later comment-only `feedback` erase an
|
|
287
|
+
* earlier `rate` on the same turn — fields are merged chronologically: the
|
|
288
|
+
* newest value of each field wins, but a later comment-only record keeps the
|
|
289
|
+
* earlier record's rating. So `rate --thumbs up` then `feedback --text "…"`
|
|
290
|
+
* yields one record carrying BOTH.
|
|
291
|
+
*/
|
|
292
|
+
export function mergeFeedback(records) {
|
|
293
|
+
const sorted = [...records].sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0));
|
|
294
|
+
const byKey = new Map();
|
|
295
|
+
for (const r of sorted) {
|
|
296
|
+
const key = turnKey(r.sessionId, r.turnNumber);
|
|
297
|
+
const existing = byKey.get(key);
|
|
298
|
+
byKey.set(key, existing === undefined ? r : foldRecords(existing, r));
|
|
299
|
+
}
|
|
300
|
+
return [...byKey.values()];
|
|
301
|
+
}
|
|
302
|
+
function hasRating(r) {
|
|
303
|
+
return (r.rating.thumbs !== undefined || r.rating.stars !== undefined || r.rating.scale !== undefined);
|
|
304
|
+
}
|
|
305
|
+
/** Merge an earlier + later record for the same turn (later ts wins per field;
|
|
306
|
+
* a comment-only later record does not erase the earlier rating). */
|
|
307
|
+
function foldRecords(earlier, later) {
|
|
308
|
+
const ratingRecord = hasRating(later) ? later : hasRating(earlier) ? earlier : later;
|
|
309
|
+
const comment = later.comment ?? earlier.comment;
|
|
310
|
+
const correction = later.correction ?? earlier.correction;
|
|
311
|
+
const rater = later.rater ?? earlier.rater;
|
|
312
|
+
const runId = later.runId ?? earlier.runId;
|
|
313
|
+
const targetSpanId = later.targetSpanId ?? earlier.targetSpanId;
|
|
314
|
+
return {
|
|
315
|
+
schemaVersion: FEEDBACK_SCHEMA_VERSION,
|
|
316
|
+
id: later.id,
|
|
317
|
+
sessionId: later.sessionId,
|
|
318
|
+
turnNumber: later.turnNumber,
|
|
319
|
+
modality: hasRating(ratingRecord) ? ratingRecord.modality : "comment",
|
|
320
|
+
rating: ratingRecord.rating,
|
|
321
|
+
source: later.source,
|
|
322
|
+
ts: later.ts,
|
|
323
|
+
...(runId !== undefined ? { runId } : {}),
|
|
324
|
+
...(targetSpanId !== undefined ? { targetSpanId } : {}),
|
|
325
|
+
...(comment !== undefined ? { comment } : {}),
|
|
326
|
+
...(correction !== undefined ? { correction } : {}),
|
|
327
|
+
...(rater !== undefined ? { rater } : {}),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Tag-all distillation. Every rated turn becomes a Sample; positively-rated
|
|
332
|
+
* turns (normalized ≥ minScore) — and any turn with a `correction` — become
|
|
333
|
+
* GOLD (expected_output set), the rest carry a low `metadata.user_rating` and
|
|
334
|
+
* no expected_output so they feed the optimizer's failure channel. A single
|
|
335
|
+
* deterministic grader is synthesized from the positive turns' behavior
|
|
336
|
+
* (exactly one, to avoid the hard-AND collapse in `all(...)`).
|
|
337
|
+
*/
|
|
338
|
+
export function distill(turns, feedback, opts) {
|
|
339
|
+
const turnByKey = new Map();
|
|
340
|
+
for (const t of turns)
|
|
341
|
+
turnByKey.set(turnKey(t.sessionId, t.turnNumber), t);
|
|
342
|
+
const merged = mergeFeedback(feedback);
|
|
343
|
+
const samples = [];
|
|
344
|
+
const positiveTurns = [];
|
|
345
|
+
const positiveComments = [];
|
|
346
|
+
const negativeComments = [];
|
|
347
|
+
const warnings = [];
|
|
348
|
+
let matched = 0;
|
|
349
|
+
let unmatched = 0;
|
|
350
|
+
let positives = 0;
|
|
351
|
+
let negatives = 0;
|
|
352
|
+
for (const fb of merged) {
|
|
353
|
+
const turn = turnByKey.get(turnKey(fb.sessionId, fb.turnNumber));
|
|
354
|
+
if (turn === undefined) {
|
|
355
|
+
unmatched += 1;
|
|
356
|
+
warnings.push(`rating for ${fb.sessionId} turn ${fb.turnNumber} has no matching turn in the transcript — skipped`);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
matched += 1;
|
|
360
|
+
const score = normalizeRating(fb);
|
|
361
|
+
const isPositive = (score !== undefined && score >= opts.minScore) || fb.correction !== undefined;
|
|
362
|
+
// Gold = the user's correction, else the assistant's answer for a positive
|
|
363
|
+
// turn. Omit an empty answer (e.g. a turn that errored before replying) so
|
|
364
|
+
// we never emit a meaningless empty-string gold.
|
|
365
|
+
const goldCandidate = fb.correction ?? (isPositive ? turn.output : undefined);
|
|
366
|
+
const expectedOutput = goldCandidate !== undefined && goldCandidate.trim() !== "" ? goldCandidate : undefined;
|
|
367
|
+
const metadata = {
|
|
368
|
+
sessionId: fb.sessionId,
|
|
369
|
+
turnNumber: fb.turnNumber,
|
|
370
|
+
source: fb.source,
|
|
371
|
+
raw_rating: fb.rating,
|
|
372
|
+
};
|
|
373
|
+
if (score !== undefined)
|
|
374
|
+
metadata["user_rating"] = score;
|
|
375
|
+
if (fb.comment !== undefined)
|
|
376
|
+
metadata["comment"] = fb.comment;
|
|
377
|
+
if (fb.correction !== undefined)
|
|
378
|
+
metadata["correction"] = fb.correction;
|
|
379
|
+
if (fb.rater !== undefined)
|
|
380
|
+
metadata["rater"] = fb.rater;
|
|
381
|
+
const sample = { id: `${fb.sessionId}_t${fb.turnNumber}`, input: turn.input, metadata };
|
|
382
|
+
if (expectedOutput !== undefined)
|
|
383
|
+
sample.expected_output = expectedOutput;
|
|
384
|
+
if (isPositive && turn.toolNames.length > 0)
|
|
385
|
+
sample.expected_tools = turn.toolNames;
|
|
386
|
+
samples.push(sample);
|
|
387
|
+
if (isPositive) {
|
|
388
|
+
positives += 1;
|
|
389
|
+
positiveTurns.push(turn);
|
|
390
|
+
if (fb.comment !== undefined)
|
|
391
|
+
positiveComments.push(fb.comment);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
negatives += 1;
|
|
395
|
+
if (fb.comment !== undefined)
|
|
396
|
+
negativeComments.push(fb.comment);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
samples,
|
|
401
|
+
graders: opts.judge === true
|
|
402
|
+
? { graders: [buildJudgeRubricGrader(positiveComments, negativeComments, opts.judgeModel)] }
|
|
403
|
+
: synthesizeGraders(positiveTurns, warnings),
|
|
404
|
+
stats: {
|
|
405
|
+
totalFeedback: merged.length,
|
|
406
|
+
matchedTurns: matched,
|
|
407
|
+
unmatchedFeedback: unmatched,
|
|
408
|
+
positives,
|
|
409
|
+
negatives,
|
|
410
|
+
},
|
|
411
|
+
warnings,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const STOPWORDS = new Set([
|
|
415
|
+
"the",
|
|
416
|
+
"and",
|
|
417
|
+
"that",
|
|
418
|
+
"this",
|
|
419
|
+
"with",
|
|
420
|
+
"from",
|
|
421
|
+
"your",
|
|
422
|
+
"have",
|
|
423
|
+
"will",
|
|
424
|
+
"here",
|
|
425
|
+
"there",
|
|
426
|
+
"what",
|
|
427
|
+
"when",
|
|
428
|
+
"which",
|
|
429
|
+
"would",
|
|
430
|
+
"could",
|
|
431
|
+
"should",
|
|
432
|
+
"about",
|
|
433
|
+
"into",
|
|
434
|
+
"then",
|
|
435
|
+
"than",
|
|
436
|
+
"them",
|
|
437
|
+
"they",
|
|
438
|
+
"you",
|
|
439
|
+
"for",
|
|
440
|
+
"are",
|
|
441
|
+
"was",
|
|
442
|
+
"were",
|
|
443
|
+
"its",
|
|
444
|
+
]);
|
|
445
|
+
/**
|
|
446
|
+
* Pick the single deterministic grader that best captures the up-rated
|
|
447
|
+
* behavior. Prefers a `tool_call_sequence` over the tools every tool-using
|
|
448
|
+
* positive turn shared (falling back to the most common tool); if the positive
|
|
449
|
+
* turns used no tools, a `contains` over a distinctive token common to their
|
|
450
|
+
* answers; failing that, a non-empty-answer floor (with a warning).
|
|
451
|
+
*/
|
|
452
|
+
export function synthesizeGraders(positiveTurns, warnings = []) {
|
|
453
|
+
const toolCounts = new Map();
|
|
454
|
+
let turnsWithTools = 0;
|
|
455
|
+
for (const t of positiveTurns) {
|
|
456
|
+
const uniq = [...new Set(t.toolNames)];
|
|
457
|
+
if (uniq.length > 0)
|
|
458
|
+
turnsWithTools += 1;
|
|
459
|
+
for (const name of uniq)
|
|
460
|
+
toolCounts.set(name, (toolCounts.get(name) ?? 0) + 1);
|
|
461
|
+
}
|
|
462
|
+
if (turnsWithTools > 0 && toolCounts.size > 0) {
|
|
463
|
+
const inEvery = [...toolCounts.entries()]
|
|
464
|
+
.filter(([, c]) => c === turnsWithTools)
|
|
465
|
+
.map(([n]) => n);
|
|
466
|
+
let expected = inEvery;
|
|
467
|
+
if (expected.length === 0) {
|
|
468
|
+
const top = [...toolCounts.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
469
|
+
expected = top ? [top[0]] : [];
|
|
470
|
+
}
|
|
471
|
+
if (expected.length > 0) {
|
|
472
|
+
return {
|
|
473
|
+
graders: [{ name: "preferred_tools", type: "tool_call_sequence", expected, mode: "set" }],
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const token = commonToken(positiveTurns.map((t) => t.output));
|
|
478
|
+
if (token !== undefined) {
|
|
479
|
+
return {
|
|
480
|
+
graders: [
|
|
481
|
+
{ name: "preferred_phrase", type: "contains", substring: token, case_insensitive: true },
|
|
482
|
+
],
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
warnings.push("no consistent tool or phrase signal in the up-rated turns — emitted a non-empty-answer floor grader; add ratings or edit graders.yaml for a sharper eval");
|
|
486
|
+
return { graders: [{ name: "non_empty_answer", type: "regex", pattern: "\\S" }] };
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Build a single `llm_judge` grader seeded from the aggregated comment themes.
|
|
490
|
+
* One criterion ("user_preference") with fixed 1–5 anchors and a description
|
|
491
|
+
* folding a short, quoted, clipped summary of what users praised vs criticized.
|
|
492
|
+
* Emitted as the SOLE grader so it is never hard-ANDed with a deterministic one
|
|
493
|
+
* (the `all(...)` min-collapse). The untrusted comment text is bounded and
|
|
494
|
+
* quoted-as-data; the rubric is trusted context sent verbatim to the judge.
|
|
495
|
+
*/
|
|
496
|
+
export function buildJudgeRubricGrader(positiveComments, negativeComments, model) {
|
|
497
|
+
const praised = summarizeComments(positiveComments);
|
|
498
|
+
const criticized = summarizeComments(negativeComments);
|
|
499
|
+
const praisedNote = praised !== undefined ? ` Users praised responses like: '${praised}'.` : "";
|
|
500
|
+
const criticizedNote = criticized !== undefined ? ` Users criticized responses like: '${criticized}'.` : "";
|
|
501
|
+
const description = `Judge how well the response matches the qualities real users prefer, learned from their ratings.${praisedNote}${criticizedNote}`;
|
|
502
|
+
return {
|
|
503
|
+
name: "user_preference",
|
|
504
|
+
type: "llm_judge",
|
|
505
|
+
rubric: {
|
|
506
|
+
criteria: [
|
|
507
|
+
{
|
|
508
|
+
name: "user_preference",
|
|
509
|
+
description,
|
|
510
|
+
anchors: {
|
|
511
|
+
"1": "Strongly conflicts with what users liked; clearly exhibits the criticized traits.",
|
|
512
|
+
"2": "Mostly misses the qualities users preferred.",
|
|
513
|
+
"3": "Partially matches user preferences; a mixed response.",
|
|
514
|
+
"4": "Largely matches the qualities users praised.",
|
|
515
|
+
"5": "Strongly matches the user-preferred style and avoids the criticized traits.",
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
passing_score: 3,
|
|
520
|
+
},
|
|
521
|
+
...(model !== undefined ? { model } : {}),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
/** Dedupe, join, and clip a batch of comments into one short quoted summary for
|
|
525
|
+
* a rubric description (~400 chars). Undefined when there are no comments. */
|
|
526
|
+
function summarizeComments(comments) {
|
|
527
|
+
const cleaned = [...new Set(comments.map((c) => c.trim()).filter((c) => c !== ""))];
|
|
528
|
+
if (cleaned.length === 0)
|
|
529
|
+
return undefined;
|
|
530
|
+
const joined = clipFeedbackText(cleaned.slice(0, 8).join("; ")).replace(/'/g, "");
|
|
531
|
+
return joined.length > 400 ? `${joined.slice(0, 400)}…` : joined;
|
|
532
|
+
}
|
|
533
|
+
/** The most frequent distinctive token (alnum, length ≥ 4, non-stopword)
|
|
534
|
+
* appearing in at least half of the answers. Undefined when none qualifies. */
|
|
535
|
+
function commonToken(answers) {
|
|
536
|
+
const nonEmpty = answers.filter((a) => a.trim() !== "");
|
|
537
|
+
if (nonEmpty.length === 0)
|
|
538
|
+
return undefined;
|
|
539
|
+
const docFreq = new Map();
|
|
540
|
+
for (const answer of nonEmpty) {
|
|
541
|
+
const seen = new Set();
|
|
542
|
+
for (const raw of answer.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
543
|
+
if (raw.length < 4 || STOPWORDS.has(raw) || seen.has(raw))
|
|
544
|
+
continue;
|
|
545
|
+
seen.add(raw);
|
|
546
|
+
docFreq.set(raw, (docFreq.get(raw) ?? 0) + 1);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const threshold = Math.ceil(nonEmpty.length / 2);
|
|
550
|
+
const ranked = [...docFreq.entries()]
|
|
551
|
+
.filter(([, c]) => c >= threshold)
|
|
552
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
553
|
+
return ranked.length > 0 ? ranked[0]?.[0] : undefined;
|
|
554
|
+
}
|
|
555
|
+
// -------- serialization --------
|
|
556
|
+
/** One SampleSchema-valid JSON object per line (round-trips through loadJsonl). */
|
|
557
|
+
export function samplesToJsonl(samples) {
|
|
558
|
+
return `${samples.map((s) => JSON.stringify(s)).join("\n")}\n`;
|
|
559
|
+
}
|
|
560
|
+
/** The default graders.yaml header — distill's provenance note. Callers with
|
|
561
|
+
* a different provenance (scaffold-evals item 13, graders suggest item 4)
|
|
562
|
+
* pass their own header lines instead. */
|
|
563
|
+
const DISTILL_GRADERS_HEADER = [
|
|
564
|
+
"# Synthesized by `crewhaus distill` from user ratings.",
|
|
565
|
+
"# Exactly one grader — stacking graders hard-ANDs their scores (see eval-grader `all`).",
|
|
566
|
+
];
|
|
567
|
+
/** Emit a GradersConfigSchema-valid graders.yaml. Scalars are JSON-encoded so
|
|
568
|
+
* arbitrary substrings/tool names stay YAML-safe (JSON is a YAML subset).
|
|
569
|
+
* `headerLines` replaces the distill provenance header (each line must be a
|
|
570
|
+
* full `# …` YAML comment). */
|
|
571
|
+
export function gradersConfigToYaml(config, headerLines = DISTILL_GRADERS_HEADER) {
|
|
572
|
+
const lines = [...headerLines, "graders:"];
|
|
573
|
+
for (const g of config.graders) {
|
|
574
|
+
lines.push(` - name: ${JSON.stringify(g.name)}`);
|
|
575
|
+
lines.push(` type: ${g.type}`);
|
|
576
|
+
if (g.type === "tool_call_sequence") {
|
|
577
|
+
lines.push(` expected: ${JSON.stringify(g.expected)}`);
|
|
578
|
+
lines.push(` mode: ${g.mode}`);
|
|
579
|
+
}
|
|
580
|
+
else if (g.type === "contains") {
|
|
581
|
+
lines.push(` substring: ${JSON.stringify(g.substring)}`);
|
|
582
|
+
if (g.case_insensitive !== undefined)
|
|
583
|
+
lines.push(` case_insensitive: ${g.case_insensitive}`);
|
|
584
|
+
}
|
|
585
|
+
else if (g.type === "json_path") {
|
|
586
|
+
lines.push(` path: ${JSON.stringify(g.path)}`);
|
|
587
|
+
if (g.expected !== undefined)
|
|
588
|
+
lines.push(` expected: ${JSON.stringify(g.expected)}`);
|
|
589
|
+
}
|
|
590
|
+
else if (g.type === "llm_judge") {
|
|
591
|
+
lines.push(" rubric:");
|
|
592
|
+
lines.push(" criteria:");
|
|
593
|
+
for (const c of g.rubric.criteria) {
|
|
594
|
+
lines.push(` - name: ${JSON.stringify(c.name)}`);
|
|
595
|
+
lines.push(` description: ${JSON.stringify(c.description)}`);
|
|
596
|
+
lines.push(" anchors:");
|
|
597
|
+
for (const k of ["1", "2", "3", "4", "5"]) {
|
|
598
|
+
lines.push(` ${JSON.stringify(k)}: ${JSON.stringify(c.anchors[k])}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (g.rubric.passing_score !== undefined) {
|
|
602
|
+
lines.push(` passing_score: ${g.rubric.passing_score}`);
|
|
603
|
+
}
|
|
604
|
+
if (g.model !== undefined)
|
|
605
|
+
lines.push(` model: ${JSON.stringify(g.model)}`);
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
lines.push(` pattern: ${JSON.stringify(g.pattern)}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return `${lines.join("\n")}\n`;
|
|
612
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item #54 — harvest up-rated turns into a golden few-shot pool.
|
|
3
|
+
*
|
|
4
|
+
* `crewhaus fewshot harvest` mines POSITIVELY-rated conversation turns (the
|
|
5
|
+
* same `user_feedback` ratings `distill` consumes) into a curated pool of
|
|
6
|
+
* input→good-output few-shot examples. The pool is a versioned, provenance-
|
|
7
|
+
* tagged JSONL under `.crewhaus/fewshot/<spec>.jsonl`; `crewhaus optimize
|
|
8
|
+
* --few-shot <file>` injects the top-K examples into the agent prompt as
|
|
9
|
+
* in-context demonstrations, and `agent.instructions` can carry them at
|
|
10
|
+
* compile time via `formatFewShotForPrompt`.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is pure + deterministic (stable ordering, id-based dedupe)
|
|
13
|
+
* so it is unit-testable; all filesystem access + the model/redactor wiring
|
|
14
|
+
* lives in `apps/cli/src/index.ts` (mirrors `feedback.ts` / `dataset-mine.ts`).
|
|
15
|
+
* The harvested OUTPUT text is the rated assistant answer (or the user's
|
|
16
|
+
* correction), so it is PII/secret-redacted before it lands in the pool: the
|
|
17
|
+
* caller threads a redactor built from `SYNTHESIZE_PII_DETECTORS` (the shared
|
|
18
|
+
* secret/API-key + DEFAULT_PII detector set) so a pasted credential never
|
|
19
|
+
* survives into the pool or the optimizer meta-prompt.
|
|
20
|
+
*/
|
|
21
|
+
import type { FeedbackRecord, SessionTurn } from "./feedback";
|
|
22
|
+
/** Schema version of a persisted few-shot pool line. */
|
|
23
|
+
export declare const FEWSHOT_SCHEMA_VERSION: 1;
|
|
24
|
+
/** One curated few-shot example: an input paired with a known-good output. */
|
|
25
|
+
export type FewShotExample = {
|
|
26
|
+
readonly schemaVersion: 1;
|
|
27
|
+
/** Stable id: `${sessionId}_t${turnNumber}` — the join key, dedupes reruns. */
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly input: string;
|
|
30
|
+
readonly output: string;
|
|
31
|
+
/** [0,1] normalized rating that qualified this turn (or 1 for a correction). */
|
|
32
|
+
readonly score: number;
|
|
33
|
+
/** Provenance for audit + optimizer trust. */
|
|
34
|
+
readonly provenance: {
|
|
35
|
+
readonly sessionId: string;
|
|
36
|
+
readonly turnNumber: number;
|
|
37
|
+
/** "rating" (up-rated answer) | "correction" (user-supplied better answer). */
|
|
38
|
+
readonly source: "rating" | "correction";
|
|
39
|
+
readonly rater?: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
export type HarvestOptions = {
|
|
43
|
+
/** Normalized score at/above which a rated turn qualifies. Default 0.7. */
|
|
44
|
+
readonly minScore?: number;
|
|
45
|
+
/** Async redactor applied to the harvested OUTPUT (and correction). The CLI
|
|
46
|
+
* passes a `SYNTHESIZE_PII_DETECTORS`-backed redactor; tests pass a stub. */
|
|
47
|
+
readonly redact?: (text: string) => Promise<string>;
|
|
48
|
+
};
|
|
49
|
+
export type HarvestStats = {
|
|
50
|
+
readonly totalFeedback: number;
|
|
51
|
+
readonly qualified: number;
|
|
52
|
+
readonly skippedUnmatched: number;
|
|
53
|
+
readonly skippedEmpty: number;
|
|
54
|
+
};
|
|
55
|
+
export type HarvestResult = {
|
|
56
|
+
readonly examples: FewShotExample[];
|
|
57
|
+
readonly stats: HarvestStats;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Harvest positively-rated turns into few-shot examples. A turn qualifies when
|
|
61
|
+
* its merged rating normalizes to ≥ `minScore`, OR it carries a `correction`
|
|
62
|
+
* (the user's own better answer — always gold). The example OUTPUT is the
|
|
63
|
+
* correction when present, else the assistant's answer; empty answers are
|
|
64
|
+
* skipped. Deterministic: examples are sorted by (score desc, id asc) and
|
|
65
|
+
* deduped by id, so re-harvesting the same feedback is idempotent.
|
|
66
|
+
*/
|
|
67
|
+
export declare function harvestFewShot(turns: ReadonlyArray<SessionTurn>, feedback: ReadonlyArray<FeedbackRecord>, opts?: HarvestOptions): Promise<HarvestResult>;
|
|
68
|
+
/** Narrow an arbitrary parsed JSON value to a persisted FewShotExample. */
|
|
69
|
+
export declare function isFewShotExample(value: unknown): value is FewShotExample;
|
|
70
|
+
/**
|
|
71
|
+
* Merge a freshly-harvested pool into an existing one, deduping by id (the
|
|
72
|
+
* newer example wins so re-runs refresh outputs without duplicating). Returns
|
|
73
|
+
* the deterministically-ordered union.
|
|
74
|
+
*/
|
|
75
|
+
export declare function mergePools(existing: ReadonlyArray<FewShotExample>, fresh: ReadonlyArray<FewShotExample>): FewShotExample[];
|
|
76
|
+
/** Serialize a pool to JSONL (one example per line). */
|
|
77
|
+
export declare function poolToJsonl(examples: ReadonlyArray<FewShotExample>): string;
|
|
78
|
+
/**
|
|
79
|
+
* Render the top-K examples as an in-context few-shot block for the system
|
|
80
|
+
* prompt / `agent.instructions`. Empty input → "" so callers append
|
|
81
|
+
* unconditionally. `k` caps how many examples are injected (default 5).
|
|
82
|
+
*/
|
|
83
|
+
export declare function formatFewShotForPrompt(examples: ReadonlyArray<FewShotExample>, k?: number): string;
|