crewhaus 0.1.6 → 0.1.8

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.
@@ -0,0 +1,239 @@
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 declare const FEEDBACK_EVENT_KIND: "user_feedback";
28
+ export declare const FEEDBACK_SCHEMA_VERSION: 1;
29
+ export type FeedbackModality = "binary" | "stars" | "scale" | "comment";
30
+ export type FeedbackSource = "user" | "ui" | "channel" | "cli";
31
+ export type FeedbackRating = {
32
+ thumbs?: "up" | "down";
33
+ stars?: number;
34
+ scale?: {
35
+ value: number;
36
+ min: number;
37
+ max: number;
38
+ };
39
+ };
40
+ export type FeedbackRecord = {
41
+ schemaVersion: 1;
42
+ id: string;
43
+ sessionId: string;
44
+ runId?: string;
45
+ /** 1-based ordinal of the rated user-text turn. */
46
+ turnNumber: number;
47
+ /** spanId of the rated model_response, when the capture surface knows it. */
48
+ targetSpanId?: string;
49
+ modality: FeedbackModality;
50
+ rating: FeedbackRating;
51
+ comment?: string;
52
+ /** A user-supplied better answer — becomes `expected_output` at distill. */
53
+ correction?: string;
54
+ source: FeedbackSource;
55
+ rater?: string;
56
+ /** ISO 8601 timestamp. */
57
+ ts: string;
58
+ };
59
+ /** Max stored length for a free-text comment/correction. Untrusted input (a
60
+ * hand-edited or web-UI-supplied `.crewhaus/feedback` line) flows into
61
+ * expected_output/metadata and the optimizer meta-prompt, so it is bounded at
62
+ * ingestion and stripped of control chars (newline/tab kept). */
63
+ export declare const MAX_FEEDBACK_TEXT = 8192;
64
+ export declare function clipFeedbackText(s: string): string;
65
+ /** Narrow an arbitrary parsed JSON value to a FeedbackRecord. Tolerant of the
66
+ * optional fields; rejects anything missing the required core OR carrying a
67
+ * malformed rating (so a corrupt/crafted line can't coerce a NaN/spurious
68
+ * score downstream). */
69
+ export declare function isFeedbackRecord(value: unknown): value is FeedbackRecord;
70
+ export type BuildFeedbackInput = {
71
+ id: string;
72
+ sessionId: string;
73
+ turnNumber: number;
74
+ ts: string;
75
+ source: FeedbackSource;
76
+ runId?: string;
77
+ targetSpanId?: string;
78
+ rater?: string;
79
+ thumbs?: "up" | "down";
80
+ stars?: number;
81
+ /** A [0,1] scalar score (stored as scale {min:0,max:1}). */
82
+ score?: number;
83
+ comment?: string;
84
+ correction?: string;
85
+ };
86
+ /**
87
+ * Assemble + validate a FeedbackRecord from capture-surface inputs. The
88
+ * modality is inferred from which rating fields are present (thumbs → binary,
89
+ * stars → stars, score → scale, else → comment). Throws a plain Error (the CLI
90
+ * routes it through `die`) when no rating signal at all is supplied.
91
+ */
92
+ export declare function buildFeedbackRecord(input: BuildFeedbackInput): FeedbackRecord;
93
+ /**
94
+ * Normalize a rating to [0,1] — the same convention `GradeResult.score` uses
95
+ * ((n-1)/4 for a 1–5 judge score). Returns `undefined` for a comment-only
96
+ * record (no numeric signal). Thumbs: up→1, down→0. Stars n→(n-1)/4. Scale
97
+ * value→(value-min)/(max-min), clamped.
98
+ */
99
+ export declare function normalizeRating(r: FeedbackRecord): number | undefined;
100
+ /** A parsed event-log line: `{ ts, version, kind, payload }`. */
101
+ export type LoggedEvent = {
102
+ kind?: string;
103
+ payload?: unknown;
104
+ };
105
+ export type DerivedTurn = {
106
+ turnNumber: number;
107
+ /** The user's text prompt for this turn. */
108
+ input: string;
109
+ /** The assistant's final text answer for this turn (what was rated). */
110
+ output: string;
111
+ /** Tool names the assistant called this turn, verbatim (PascalCase), unique. */
112
+ toolNames: string[];
113
+ };
114
+ /** A DerivedTurn tagged with the session it came from — the join key for
115
+ * `distill`, so ratings across many sessions never collide on turnNumber. */
116
+ export type SessionTurn = DerivedTurn & {
117
+ sessionId: string;
118
+ };
119
+ /**
120
+ * Reconstruct conversation turns from a session transcript. Each user-text
121
+ * message opens a turn; the assistant's messages until the next user-text
122
+ * message form its response. `output` is the LAST assistant text (the answer);
123
+ * `toolNames` are the unique tool names the assistant called, in first-seen
124
+ * order.
125
+ */
126
+ export declare function deriveTurns(events: ReadonlyArray<LoggedEvent>): DerivedTurn[];
127
+ /** Extract FeedbackRecords from parsed JSON objects. Accepts both event-log
128
+ * envelopes (`{ kind: "user_feedback", payload }`) and bare records (the
129
+ * web-UI host's `.crewhaus/feedback/*.jsonl`). */
130
+ export declare function extractFeedbackRecords(objects: ReadonlyArray<unknown>): FeedbackRecord[];
131
+ /**
132
+ * Collapse records to one per (sessionId, turnNumber). Rather than pure
133
+ * last-write-wins — which would let a later comment-only `feedback` erase an
134
+ * earlier `rate` on the same turn — fields are merged chronologically: the
135
+ * newest value of each field wins, but a later comment-only record keeps the
136
+ * earlier record's rating. So `rate --thumbs up` then `feedback --text "…"`
137
+ * yields one record carrying BOTH.
138
+ */
139
+ export declare function mergeFeedback(records: ReadonlyArray<FeedbackRecord>): FeedbackRecord[];
140
+ type Sample = {
141
+ id: string;
142
+ input: string;
143
+ expected_output?: string;
144
+ expected_tools?: string[];
145
+ metadata?: Record<string, unknown>;
146
+ };
147
+ export type RubricAnchors = {
148
+ "1": string;
149
+ "2": string;
150
+ "3": string;
151
+ "4": string;
152
+ "5": string;
153
+ };
154
+ export type GraderSpecObject = {
155
+ name: string;
156
+ type: "tool_call_sequence";
157
+ expected: string[];
158
+ mode: "set" | "subseq" | "exact";
159
+ } | {
160
+ name: string;
161
+ type: "contains";
162
+ substring: string;
163
+ case_insensitive?: boolean;
164
+ } | {
165
+ name: string;
166
+ type: "regex";
167
+ pattern: string;
168
+ } | {
169
+ name: string;
170
+ type: "llm_judge";
171
+ rubric: {
172
+ criteria: Array<{
173
+ name: string;
174
+ description: string;
175
+ anchors: RubricAnchors;
176
+ }>;
177
+ passing_score?: number;
178
+ };
179
+ model?: string;
180
+ };
181
+ export type GradersConfigObject = {
182
+ graders: GraderSpecObject[];
183
+ };
184
+ export type DistillOptions = {
185
+ /** Normalized score at/above which a turn is a positive (gold) sample. */
186
+ minScore: number;
187
+ /** Emit a single `llm_judge` grader seeded from comment themes instead of the
188
+ * deterministic grader. Opt-in (each eval sample then costs a judge call). */
189
+ judge?: boolean;
190
+ /** Judge model baked into the emitted llm_judge grader (else the runner's
191
+ * `--judge-model` applies at eval time). */
192
+ judgeModel?: string;
193
+ };
194
+ export type DistillStats = {
195
+ totalFeedback: number;
196
+ matchedTurns: number;
197
+ unmatchedFeedback: number;
198
+ positives: number;
199
+ negatives: number;
200
+ };
201
+ export type DistillResult = {
202
+ samples: Sample[];
203
+ graders: GradersConfigObject;
204
+ stats: DistillStats;
205
+ /** Non-fatal warnings (e.g. a rating whose turn is missing from the log). */
206
+ warnings: string[];
207
+ };
208
+ /**
209
+ * Tag-all distillation. Every rated turn becomes a Sample; positively-rated
210
+ * turns (normalized ≥ minScore) — and any turn with a `correction` — become
211
+ * GOLD (expected_output set), the rest carry a low `metadata.user_rating` and
212
+ * no expected_output so they feed the optimizer's failure channel. A single
213
+ * deterministic grader is synthesized from the positive turns' behavior
214
+ * (exactly one, to avoid the hard-AND collapse in `all(...)`).
215
+ */
216
+ export declare function distill(turns: ReadonlyArray<SessionTurn>, feedback: ReadonlyArray<FeedbackRecord>, opts: DistillOptions): DistillResult;
217
+ /**
218
+ * Pick the single deterministic grader that best captures the up-rated
219
+ * behavior. Prefers a `tool_call_sequence` over the tools every tool-using
220
+ * positive turn shared (falling back to the most common tool); if the positive
221
+ * turns used no tools, a `contains` over a distinctive token common to their
222
+ * answers; failing that, a non-empty-answer floor (with a warning).
223
+ */
224
+ export declare function synthesizeGraders(positiveTurns: ReadonlyArray<DerivedTurn>, warnings?: string[]): GradersConfigObject;
225
+ /**
226
+ * Build a single `llm_judge` grader seeded from the aggregated comment themes.
227
+ * One criterion ("user_preference") with fixed 1–5 anchors and a description
228
+ * folding a short, quoted, clipped summary of what users praised vs criticized.
229
+ * Emitted as the SOLE grader so it is never hard-ANDed with a deterministic one
230
+ * (the `all(...)` min-collapse). The untrusted comment text is bounded and
231
+ * quoted-as-data; the rubric is trusted context sent verbatim to the judge.
232
+ */
233
+ export declare function buildJudgeRubricGrader(positiveComments: ReadonlyArray<string>, negativeComments: ReadonlyArray<string>, model?: string): GraderSpecObject;
234
+ /** One SampleSchema-valid JSON object per line (round-trips through loadJsonl). */
235
+ export declare function samplesToJsonl(samples: ReadonlyArray<Sample>): string;
236
+ /** Emit a GradersConfigSchema-valid graders.yaml. Scalars are JSON-encoded so
237
+ * arbitrary substrings/tool names stay YAML-safe (JSON is a YAML subset). */
238
+ export declare function gradersConfigToYaml(config: GradersConfigObject): string;
239
+ export {};