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/faq.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item #55 — distill repeated user questions into an auto-discovered FAQ skill.
|
|
3
|
+
*
|
|
4
|
+
* `crewhaus faq distill` clusters recurring `user_message` questions across
|
|
5
|
+
* sessions (deterministic token clustering — the same greedy-jaccard approach
|
|
6
|
+
* `graders suggest` uses, no model call), pairs each cluster with its best-rated
|
|
7
|
+
* answer (the up-rated turn's output), and emits a SKILL.md FAQ skill into
|
|
8
|
+
* `.crewhaus/skills/faq/` so `@crewhaus/skills-registry` auto-discovers it —
|
|
9
|
+
* its name + description go into the prompt and the body loads on demand when
|
|
10
|
+
* the model invokes the `Skill` tool (the registry is lazy, not eager).
|
|
11
|
+
*
|
|
12
|
+
* Everything here is pure + deterministic so it is unit-testable; the FS access
|
|
13
|
+
* and PII/secret redaction wiring live in `apps/cli/src/index.ts`. Both the
|
|
14
|
+
* harvested ANSWER and the representative QUESTION are user/model text that may
|
|
15
|
+
* carry pasted credentials, so the caller redacts BOTH (via
|
|
16
|
+
* `SYNTHESIZE_PII_DETECTORS`) before they land in the skill body (#55 F4).
|
|
17
|
+
*
|
|
18
|
+
* The emitted SKILL.md is guaranteed valid per skills-registry's `parseSkillFile`
|
|
19
|
+
* (leading `---` frontmatter with `name` + `description`, then the FAQ body).
|
|
20
|
+
*/
|
|
21
|
+
import type { FeedbackRecord, LoggedEvent, SessionTurn } from "./feedback";
|
|
22
|
+
/** One recurring-question cluster paired with its best answer. */
|
|
23
|
+
export type FaqEntry = {
|
|
24
|
+
/** The representative (most-frequent-token, then shortest) question. */
|
|
25
|
+
readonly question: string;
|
|
26
|
+
/** The best-rated answer for the cluster (already redacted by the caller). */
|
|
27
|
+
readonly answer: string;
|
|
28
|
+
/** How many distinct sessions asked a question in this cluster. */
|
|
29
|
+
readonly sessionCount: number;
|
|
30
|
+
/** How many questions total fell into this cluster. */
|
|
31
|
+
readonly occurrences: number;
|
|
32
|
+
};
|
|
33
|
+
export type ClusterOptions = {
|
|
34
|
+
/** Jaccard overlap at/above which a question joins a cluster's seed. Default 0.5. */
|
|
35
|
+
readonly threshold?: number;
|
|
36
|
+
/** Minimum number of DISTINCT sessions that must ask a clustered question
|
|
37
|
+
* for it to become an FAQ entry. Default 2 (recurring across sessions — a
|
|
38
|
+
* one-off, or a single session that repeats itself, is not an FAQ). */
|
|
39
|
+
readonly minOccurrences?: number;
|
|
40
|
+
/** Normalized rating at/above which an answer is "good". Default 0.7. */
|
|
41
|
+
readonly minScore?: number;
|
|
42
|
+
/** Redactor for answers (async). Identity by default. */
|
|
43
|
+
readonly redact?: (text: string) => Promise<string>;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Build the FAQ entries from per-session turns + feedback. Deterministic:
|
|
47
|
+
* questions are clustered greedily by normalized-token Jaccard overlap in a
|
|
48
|
+
* stable visit order; each cluster keeps its best-rated answer (highest score,
|
|
49
|
+
* ties broken by session/turn order). Only clusters asked in ≥ `minOccurrences`
|
|
50
|
+
* DISTINCT sessions (#55 F9) AND with a qualifying answer (score ≥ `minScore`)
|
|
51
|
+
* become entries. Entries come back most-recurring-first.
|
|
52
|
+
*/
|
|
53
|
+
export declare function distillFaq(turnsBySession: ReadonlyArray<SessionTurn>, feedback: ReadonlyArray<FeedbackRecord>, opts?: ClusterOptions): Promise<FaqEntry[]>;
|
|
54
|
+
/** Derive `SessionTurn`s from many sessions' event logs. Convenience wrapper so
|
|
55
|
+
* the CLI and tests build the clustering input the same way. */
|
|
56
|
+
export declare function turnsFromSessions(perSession: ReadonlyArray<{
|
|
57
|
+
sessionId: string;
|
|
58
|
+
events: ReadonlyArray<LoggedEvent>;
|
|
59
|
+
}>): SessionTurn[];
|
|
60
|
+
/**
|
|
61
|
+
* Render a valid SKILL.md for the FAQ. Frontmatter carries `name` +
|
|
62
|
+
* `description` (the required pair skills-registry enforces) plus `triggers`;
|
|
63
|
+
* the body is the Q→A list. Guaranteed to parse via `parseSkillFile`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildFaqSkill(entries: ReadonlyArray<FaqEntry>, opts?: {
|
|
66
|
+
name?: string;
|
|
67
|
+
harnessName?: string;
|
|
68
|
+
}): string;
|
package/dist/faq.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item #55 — distill repeated user questions into an auto-discovered FAQ skill.
|
|
3
|
+
*
|
|
4
|
+
* `crewhaus faq distill` clusters recurring `user_message` questions across
|
|
5
|
+
* sessions (deterministic token clustering — the same greedy-jaccard approach
|
|
6
|
+
* `graders suggest` uses, no model call), pairs each cluster with its best-rated
|
|
7
|
+
* answer (the up-rated turn's output), and emits a SKILL.md FAQ skill into
|
|
8
|
+
* `.crewhaus/skills/faq/` so `@crewhaus/skills-registry` auto-discovers it —
|
|
9
|
+
* its name + description go into the prompt and the body loads on demand when
|
|
10
|
+
* the model invokes the `Skill` tool (the registry is lazy, not eager).
|
|
11
|
+
*
|
|
12
|
+
* Everything here is pure + deterministic so it is unit-testable; the FS access
|
|
13
|
+
* and PII/secret redaction wiring live in `apps/cli/src/index.ts`. Both the
|
|
14
|
+
* harvested ANSWER and the representative QUESTION are user/model text that may
|
|
15
|
+
* carry pasted credentials, so the caller redacts BOTH (via
|
|
16
|
+
* `SYNTHESIZE_PII_DETECTORS`) before they land in the skill body (#55 F4).
|
|
17
|
+
*
|
|
18
|
+
* The emitted SKILL.md is guaranteed valid per skills-registry's `parseSkillFile`
|
|
19
|
+
* (leading `---` frontmatter with `name` + `description`, then the FAQ body).
|
|
20
|
+
*/
|
|
21
|
+
import { deriveTurns, mergeFeedback, normalizeRating } from "./feedback";
|
|
22
|
+
import { normalizeEvidenceTokens } from "./graders-suggest";
|
|
23
|
+
function jaccard(a, b) {
|
|
24
|
+
let intersection = 0;
|
|
25
|
+
for (const t of a)
|
|
26
|
+
if (b.has(t))
|
|
27
|
+
intersection += 1;
|
|
28
|
+
const union = a.size + b.size - intersection;
|
|
29
|
+
return union === 0 ? 0 : intersection / union;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the FAQ entries from per-session turns + feedback. Deterministic:
|
|
33
|
+
* questions are clustered greedily by normalized-token Jaccard overlap in a
|
|
34
|
+
* stable visit order; each cluster keeps its best-rated answer (highest score,
|
|
35
|
+
* ties broken by session/turn order). Only clusters asked in ≥ `minOccurrences`
|
|
36
|
+
* DISTINCT sessions (#55 F9) AND with a qualifying answer (score ≥ `minScore`)
|
|
37
|
+
* become entries. Entries come back most-recurring-first.
|
|
38
|
+
*/
|
|
39
|
+
export async function distillFaq(turnsBySession, feedback, opts = {}) {
|
|
40
|
+
const threshold = opts.threshold ?? 0.5;
|
|
41
|
+
const minOccurrences = opts.minOccurrences ?? 2;
|
|
42
|
+
const minScore = opts.minScore ?? 0.7;
|
|
43
|
+
const redact = opts.redact ?? (async (t) => t);
|
|
44
|
+
// Rating lookup keyed by session#turn.
|
|
45
|
+
const scoreByKey = new Map();
|
|
46
|
+
for (const fb of mergeFeedback(feedback)) {
|
|
47
|
+
const s = normalizeRating(fb);
|
|
48
|
+
if (s !== undefined)
|
|
49
|
+
scoreByKey.set(`${fb.sessionId}#${fb.turnNumber}`, s);
|
|
50
|
+
}
|
|
51
|
+
const records = turnsBySession
|
|
52
|
+
.filter((t) => t.input.trim() !== "")
|
|
53
|
+
.map((t) => ({
|
|
54
|
+
sessionId: t.sessionId,
|
|
55
|
+
turnNumber: t.turnNumber,
|
|
56
|
+
question: t.input.trim(),
|
|
57
|
+
answer: t.output,
|
|
58
|
+
score: scoreByKey.get(`${t.sessionId}#${t.turnNumber}`),
|
|
59
|
+
}))
|
|
60
|
+
.sort((a, b) => a.sessionId.localeCompare(b.sessionId) ||
|
|
61
|
+
a.turnNumber - b.turnNumber ||
|
|
62
|
+
a.question.localeCompare(b.question));
|
|
63
|
+
const clusters = [];
|
|
64
|
+
for (const rec of records) {
|
|
65
|
+
const tokens = new Set(normalizeEvidenceTokens(rec.question));
|
|
66
|
+
if (tokens.size === 0)
|
|
67
|
+
continue;
|
|
68
|
+
let bestIdx = -1;
|
|
69
|
+
let bestOverlap = 0;
|
|
70
|
+
for (let i = 0; i < clusters.length; i += 1) {
|
|
71
|
+
const overlap = jaccard(tokens, clusters[i].seed);
|
|
72
|
+
if (overlap > bestOverlap) {
|
|
73
|
+
bestOverlap = overlap;
|
|
74
|
+
bestIdx = i;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (bestIdx >= 0 && bestOverlap >= threshold) {
|
|
78
|
+
const c = clusters[bestIdx];
|
|
79
|
+
c.items.push(rec);
|
|
80
|
+
c.tokenSets.push(tokens);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
clusters.push({ seed: tokens, items: [rec], tokenSets: [tokens] });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const entries = [];
|
|
87
|
+
for (const c of clusters) {
|
|
88
|
+
// #55 F9 — "recurring" means asked across DISTINCT sessions, not the same
|
|
89
|
+
// session twice. Gate on the distinct-session count so two questions from
|
|
90
|
+
// one session don't manufacture an FAQ entry at minOccurrences:2.
|
|
91
|
+
const sessionCount = new Set(c.items.map((i) => i.sessionId)).size;
|
|
92
|
+
if (sessionCount < minOccurrences)
|
|
93
|
+
continue;
|
|
94
|
+
// Best answer: highest-scored qualifying turn (stable tie-break by order).
|
|
95
|
+
const qualifying = c.items
|
|
96
|
+
.filter((i) => i.score !== undefined && i.score >= minScore && i.answer.trim() !== "")
|
|
97
|
+
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0) ||
|
|
98
|
+
a.sessionId.localeCompare(b.sessionId) ||
|
|
99
|
+
a.turnNumber - b.turnNumber);
|
|
100
|
+
const best = qualifying[0];
|
|
101
|
+
if (best === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
// Representative question: the shortest question in the cluster (most
|
|
104
|
+
// canonical phrasing), stable tie-break. #55 F4 — the representative
|
|
105
|
+
// question is a raw user turn rendered as a Markdown heading in SKILL.md,
|
|
106
|
+
// so it is redacted with the SAME redactor as the answer (a question can
|
|
107
|
+
// paste a secret/email just as an answer can).
|
|
108
|
+
const rawQuestion = [...c.items]
|
|
109
|
+
.map((i) => i.question)
|
|
110
|
+
.sort((a, b) => a.length - b.length || a.localeCompare(b))[0];
|
|
111
|
+
const question = (await redact(rawQuestion)).trim();
|
|
112
|
+
const answer = (await redact(best.answer)).trim();
|
|
113
|
+
if (question === "" || answer === "")
|
|
114
|
+
continue;
|
|
115
|
+
entries.push({
|
|
116
|
+
question,
|
|
117
|
+
answer,
|
|
118
|
+
sessionCount,
|
|
119
|
+
occurrences: c.items.length,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return entries.sort((a, b) => b.occurrences - a.occurrences || a.question.localeCompare(b.question));
|
|
123
|
+
}
|
|
124
|
+
/** Derive `SessionTurn`s from many sessions' event logs. Convenience wrapper so
|
|
125
|
+
* the CLI and tests build the clustering input the same way. */
|
|
126
|
+
export function turnsFromSessions(perSession) {
|
|
127
|
+
const turns = [];
|
|
128
|
+
for (const { sessionId, events } of perSession) {
|
|
129
|
+
for (const t of deriveTurns(events))
|
|
130
|
+
turns.push({ ...t, sessionId });
|
|
131
|
+
}
|
|
132
|
+
return turns;
|
|
133
|
+
}
|
|
134
|
+
/** YAML-escape a scalar for the SKILL.md frontmatter (JSON is a YAML subset). */
|
|
135
|
+
function yamlScalar(s) {
|
|
136
|
+
return JSON.stringify(s);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Render a valid SKILL.md for the FAQ. Frontmatter carries `name` +
|
|
140
|
+
* `description` (the required pair skills-registry enforces) plus `triggers`;
|
|
141
|
+
* the body is the Q→A list. Guaranteed to parse via `parseSkillFile`.
|
|
142
|
+
*/
|
|
143
|
+
export function buildFaqSkill(entries, opts = {}) {
|
|
144
|
+
const name = opts.name ?? "faq";
|
|
145
|
+
const label = opts.harnessName ?? "this harness";
|
|
146
|
+
const description = `Frequently asked questions for ${label}, distilled from recurring user questions and their best-rated answers. Consult before answering common questions.`;
|
|
147
|
+
const triggers = ["faq", "frequently asked", "common question", "how do i"];
|
|
148
|
+
const bodyLines = [
|
|
149
|
+
`# FAQ — ${label}`,
|
|
150
|
+
"",
|
|
151
|
+
"These are recurring user questions paired with the answers users rated best. Prefer these answers for matching questions.",
|
|
152
|
+
"",
|
|
153
|
+
];
|
|
154
|
+
entries.forEach((e, i) => {
|
|
155
|
+
bodyLines.push(`## Q${i + 1}: ${e.question}`);
|
|
156
|
+
bodyLines.push("");
|
|
157
|
+
bodyLines.push(e.answer);
|
|
158
|
+
bodyLines.push("");
|
|
159
|
+
});
|
|
160
|
+
const frontmatter = [
|
|
161
|
+
"---",
|
|
162
|
+
`name: ${yamlScalar(name)}`,
|
|
163
|
+
`description: ${yamlScalar(description)}`,
|
|
164
|
+
`triggers: ${JSON.stringify(triggers)}`,
|
|
165
|
+
"---",
|
|
166
|
+
].join("\n");
|
|
167
|
+
return `${frontmatter}\n${bodyLines.join("\n")}`;
|
|
168
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
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: "json_path";
|
|
171
|
+
path: string;
|
|
172
|
+
expected?: unknown;
|
|
173
|
+
} | {
|
|
174
|
+
name: string;
|
|
175
|
+
type: "llm_judge";
|
|
176
|
+
rubric: {
|
|
177
|
+
criteria: Array<{
|
|
178
|
+
name: string;
|
|
179
|
+
description: string;
|
|
180
|
+
anchors: RubricAnchors;
|
|
181
|
+
}>;
|
|
182
|
+
passing_score?: number;
|
|
183
|
+
};
|
|
184
|
+
model?: string;
|
|
185
|
+
};
|
|
186
|
+
export type GradersConfigObject = {
|
|
187
|
+
graders: GraderSpecObject[];
|
|
188
|
+
};
|
|
189
|
+
export type DistillOptions = {
|
|
190
|
+
/** Normalized score at/above which a turn is a positive (gold) sample. */
|
|
191
|
+
minScore: number;
|
|
192
|
+
/** Emit a single `llm_judge` grader seeded from comment themes instead of the
|
|
193
|
+
* deterministic grader. Opt-in (each eval sample then costs a judge call). */
|
|
194
|
+
judge?: boolean;
|
|
195
|
+
/** Judge model baked into the emitted llm_judge grader (else the runner's
|
|
196
|
+
* `--judge-model` applies at eval time). */
|
|
197
|
+
judgeModel?: string;
|
|
198
|
+
};
|
|
199
|
+
export type DistillStats = {
|
|
200
|
+
totalFeedback: number;
|
|
201
|
+
matchedTurns: number;
|
|
202
|
+
unmatchedFeedback: number;
|
|
203
|
+
positives: number;
|
|
204
|
+
negatives: number;
|
|
205
|
+
};
|
|
206
|
+
export type DistillResult = {
|
|
207
|
+
samples: Sample[];
|
|
208
|
+
graders: GradersConfigObject;
|
|
209
|
+
stats: DistillStats;
|
|
210
|
+
/** Non-fatal warnings (e.g. a rating whose turn is missing from the log). */
|
|
211
|
+
warnings: string[];
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Tag-all distillation. Every rated turn becomes a Sample; positively-rated
|
|
215
|
+
* turns (normalized ≥ minScore) — and any turn with a `correction` — become
|
|
216
|
+
* GOLD (expected_output set), the rest carry a low `metadata.user_rating` and
|
|
217
|
+
* no expected_output so they feed the optimizer's failure channel. A single
|
|
218
|
+
* deterministic grader is synthesized from the positive turns' behavior
|
|
219
|
+
* (exactly one, to avoid the hard-AND collapse in `all(...)`).
|
|
220
|
+
*/
|
|
221
|
+
export declare function distill(turns: ReadonlyArray<SessionTurn>, feedback: ReadonlyArray<FeedbackRecord>, opts: DistillOptions): DistillResult;
|
|
222
|
+
/**
|
|
223
|
+
* Pick the single deterministic grader that best captures the up-rated
|
|
224
|
+
* behavior. Prefers a `tool_call_sequence` over the tools every tool-using
|
|
225
|
+
* positive turn shared (falling back to the most common tool); if the positive
|
|
226
|
+
* turns used no tools, a `contains` over a distinctive token common to their
|
|
227
|
+
* answers; failing that, a non-empty-answer floor (with a warning).
|
|
228
|
+
*/
|
|
229
|
+
export declare function synthesizeGraders(positiveTurns: ReadonlyArray<DerivedTurn>, warnings?: string[]): GradersConfigObject;
|
|
230
|
+
/**
|
|
231
|
+
* Build a single `llm_judge` grader seeded from the aggregated comment themes.
|
|
232
|
+
* One criterion ("user_preference") with fixed 1–5 anchors and a description
|
|
233
|
+
* folding a short, quoted, clipped summary of what users praised vs criticized.
|
|
234
|
+
* Emitted as the SOLE grader so it is never hard-ANDed with a deterministic one
|
|
235
|
+
* (the `all(...)` min-collapse). The untrusted comment text is bounded and
|
|
236
|
+
* quoted-as-data; the rubric is trusted context sent verbatim to the judge.
|
|
237
|
+
*/
|
|
238
|
+
export declare function buildJudgeRubricGrader(positiveComments: ReadonlyArray<string>, negativeComments: ReadonlyArray<string>, model?: string): GraderSpecObject;
|
|
239
|
+
/** One SampleSchema-valid JSON object per line (round-trips through loadJsonl). */
|
|
240
|
+
export declare function samplesToJsonl(samples: ReadonlyArray<Sample>): string;
|
|
241
|
+
/** Emit a GradersConfigSchema-valid graders.yaml. Scalars are JSON-encoded so
|
|
242
|
+
* arbitrary substrings/tool names stay YAML-safe (JSON is a YAML subset).
|
|
243
|
+
* `headerLines` replaces the distill provenance header (each line must be a
|
|
244
|
+
* full `# …` YAML comment). */
|
|
245
|
+
export declare function gradersConfigToYaml(config: GradersConfigObject, headerLines?: ReadonlyArray<string>): string;
|
|
246
|
+
export {};
|